OMS_H5/src/views/PurchaseDetail/index.vue

966 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="purchase-detail-page">
<!-- 导航栏 -->
<van-nav-bar
title="采购详情"
left-arrow
@click-left="goBack"
/>
<van-loading v-if="detailLoading" class="loading-container" size="24px">
加载中...
</van-loading>
<div v-else-if="currentPurchase" class="page-content">
<!-- 采购单号标题 -->
<div class="purchase-header">
<div class="purchase-title">
{{ currentPurchase.purchaseNo }}
</div>
<div class="project-status" v-if="route.query.readonly !== 'true'">
待审批
</div>
<div class="project-status completed" v-else>
已审批
</div>
</div>
<!-- Tab标签页 -->
<van-tabs v-model:active="activeTab" sticky>
<!-- 订单信息 -->
<van-tab title="订单信息" name="order">
<div class="tab-content">
<div class="card">
<div class="card-header">
<span>基本信息</span>
</div>
<div class="card-body">
<div class="info-grid">
<div class="info-item">
<span class="label">采购单号</span>
<span class="value">{{ currentPurchase.purchaseNo || '' }}</span>
</div>
<div class="info-item">
<span class="label">发起日期</span>
<span class="value">{{ formatDate(currentPurchase.createTime) }}</span>
</div>
<div class="info-item">
<span class="label">采购员</span>
<span class="value">{{ currentPurchase.purchaserName || '' }}</span>
</div>
<div class="info-item">
<span class="label">联系电话</span>
<span class="value">{{ currentPurchase.purchaserMobile || '' }}</span>
</div>
<div class="info-item">
<span class="label">联系邮箱</span>
<span class="value">{{ currentPurchase.purchaserEmail || '' }}</span>
</div>
<div class="info-item">
<span class="label">入库仓</span>
<span class="value">{{ currentPurchase.warehouseName || '' }}</span>
</div>
<div class="info-item">
<span class="label">付款方式</span>
<span class="value">{{ currentPurchase.payMethod==='1'?'出库付款':'入库付款' }}</span>
</div>
<div class="info-item">
<span class="label">汇智负责人</span>
<span class="value">{{ currentPurchase.ownerName || '' }}</span>
</div>
<div class="info-item full-width">
<span class="label">备注</span>
<span class="value">{{ currentPurchase.remark || '无' }}</span>
</div>
</div>
</div>
</div>
</div>
</van-tab>
<!-- 采购列表 -->
<van-tab title="采购列表" name="items">
<div class="tab-content">
<div v-if="purchaseItems.length > 0">
<div v-for="(item, index) in purchaseItems" :key="item.id" class="product-card">
<div class="product-header">
<span class="product-index">{{ index + 1 }}</span>
<div class="product-main-info">
<div class="product-code-price">
<span class="product-code">{{ item.productCode }}</span>
<span class="product-total-price">{{ formatAmount(item.price * item.quantity) }}</span>
</div>
</div>
</div>
<div class="product-details">
<div class="product-row">
<span class="product-label">产品型号</span>
<span class="product-value">{{ item.productModel }}</span>
</div>
<div class="product-row">
<span class="product-label">描述</span>
<span class="product-value">{{ item.productDescription }}</span>
</div>
<div class="product-row">
<span class="product-label">数量</span>
<span class="product-value">{{ item.quantity }}</span>
</div>
<div class="product-row">
<span class="product-label">单价</span>
<span class="product-value">{{ formatAmount(item.price) }}</span>
</div>
</div>
</div>
<!-- 总计 -->
<div class="product-summary">
<div class="summary-row">
<span class="summary-label">产品总数</span>
<span class="summary-value">{{ purchaseItems.length }}</span>
</div>
<div class="summary-row final-total">
<span class="summary-label">总金额</span>
<span class="summary-value final-amount">{{ formatAmount(getTotalAmount()) }}</span>
</div>
</div>
</div>
<!-- 无产品信息时的提示 -->
<div v-else class="empty-state">
<van-empty description="暂无采购产品" />
</div>
</div>
</van-tab>
<!-- 附件 -->
<van-tab title="附件" name="attachment">
<div class="tab-content">
<div class="card" v-if="attachmentList.length">
<div class="card-body">
<div class="file-list">
<div v-for="file in attachmentList" :key="file.id" class="file-item"
@click="previewFile(file)">
<van-icon name="description"/>
<div class="file-info">
<div class="file-name">{{ file.fileName }}</div>
<div class="file-meta"> {{ formatDate(file.uploadTime) }}</div>
</div>
<van-icon name="arrow"/>
</div>
</div>
</div>
</div>
<!-- 无附件时的提示 -->
<div v-else class="empty-state">
<van-empty description="暂无附件"/>
</div>
</div>
</van-tab>
<!-- 审批历史 -->
<van-tab title="审批历史" name="approval">
<div class="tab-content">
<div class="card" v-if="approvalHistory.length">
<div class="card-body">
<van-steps direction="vertical" :active="approvalHistory.length">
<van-step v-for="(record, index) in approvalHistory" :key="record.todoId || index">
<template #inactive-icon>
<van-icon
:name="getStepIcon(record.approveStatus)"
:color="getApprovalStatusColor(record.approveStatus)"
/>
</template>
<template #active-icon>
<van-icon
:name="getStepIcon(record.approveStatus)"
:color="getApprovalStatusColor(record.approveStatus)"
/>
</template>
<div class="approval-item">
<div class="approval-user">提交人:{{ record.approveUserName }}</div>
<div v-if="record.nextAllApproveUserName" class="approval-next-user">
接受人:{{ record.nextAllApproveUserName }}
</div>
<div class="approval-time">{{ formatDate(record.approveTime, 'YYYY-MM-DD HH:mm') }}</div>
<div v-if="record.approveOpinion" class="approval-opinion">
审批意见:{{ record.approveOpinion }}
</div>
</div>
</van-step>
</van-steps>
</div>
</div>
<!-- 无审批历史时的提示 -->
<div v-else class="empty-state">
<van-empty description="暂无审批历史" />
</div>
</div>
</van-tab>
</van-tabs>
</div>
<!-- 审批操作按钮 -->
<div v-if="showApprovalButtons" class="approval-actions">
<van-button
type="default"
size="large"
@click="showApprovalDialog(0)"
:loading="submitting"
>
驳回
</van-button>
<van-button
type="primary"
size="large"
@click="showApprovalDialog(1)"
:loading="submitting"
>
通过
</van-button>
</div>
<!-- 审批意见弹窗 -->
<van-popup
v-model:show="approvalDialogVisible"
position="bottom"
round
:style="{ height: '50%' }"
>
<div class="approval-dialog">
<div class="dialog-header">
<span>审批意见</span>
<van-icon name="cross" @click="approvalDialogVisible = false"/>
</div>
<div class="dialog-body">
<!-- 默认意见标签 -->
<div class="opinion-tags">
<div class="tags-title">常用意见</div>
<div class="tags-container">
<van-tag
v-for="tag in getOpinionTags()"
:key="tag"
:type="selectedTag === tag ? 'primary' : 'default'"
size="medium"
@click="selectTag(tag)"
class="opinion-tag"
>
{{ tag }}
</van-tag>
</div>
</div>
<!-- 意见输入框 -->
<div class="opinion-input">
<van-field
v-model="approvalOpinion"
type="textarea"
:placeholder="currentApprovalStatus === 0 ? '请输入驳回原因' : '请输入审批意见'"
rows="4"
autosize
maxlength="500"
show-word-limit
/>
</div>
</div>
<div class="dialog-footer">
<van-button
type="primary"
block
@click="submitApproval"
:loading="submitting"
>
确认{{ currentApprovalStatus === 0 ? '驳回' : '通过' }}
</van-button>
</div>
</div>
</van-popup>
<!-- PDF预览弹窗 -->
<van-popup
v-model:show="pdfPreviewVisible"
position="bottom"
round
:style="{ height: '90%' }"
closeable
>
<div class="pdf-preview-container">
<div class="pdf-content">
<vue-pdf-embed
v-if="pdfUrl"
:source="pdfUrl"
class="vue-pdf-embed"
@loaded="handlePdfLoaded"
@loading-failed="handlePdfError"
/>
</div>
</div>
</van-popup>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import { showToast, showSuccessToast } from 'vant'
import { usePurchaseStore } from '@/store/purchase'
import { getPurchaseApprovalHistory, submitPurchaseApproval } from '@/api/purchase'
import { formatAmount, formatDate, getApprovalStatusColor, getFilePreviewUrl } from '@/utils'
import type {ApprovalStatus, ApproveBtn, AttachmentFile, FileType} from '@/types'
import VuePdfEmbed from 'vue-pdf-embed'
import * as pdfjsLib from 'pdfjs-dist'
// PDF worker
// 使 import.meta.url Vite worker
const workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.js', import.meta.url).toString()
pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc
const route = useRoute()
const router = useRouter()
const purchaseStore = usePurchaseStore()
const { currentPurchase, currentPurchaseTodo, purchaseItems, detailLoading } = storeToRefs(purchaseStore)
// Tab
const activeTab = ref('order')
//
const approvalHistory = ref<any[]>([])
// 审批相关
const approvalDialogVisible = ref(false)
const pdfPreviewVisible = ref(false)
const pdfUrl = ref('')
const pdfLoading = ref(false)
const approvalOpinion = ref('')
const currentApprovalStatus = ref<ApproveBtn>(3)
const submitting = ref(false)
const selectedTag = ref('')
// 获取附件列表
const attachmentList = computed(() => {
if (!currentPurchase.value) return []
// 如果有fileLog且不为空则将其包装为数组返回
if ((currentPurchase.value as any).fileLog) {
return [(currentPurchase.value as any).fileLog]
}
})
// 是否显示审批按钮
const showApprovalButtons = computed(() => {
// 如果是只读模式(来自已审批列表),则不显示审批按钮
if (route.query.readonly === 'true') {
return false
}
// 其他情况显示审批按钮
return true
})
// 获取步骤图标
const getStepIcon = (status?: ApprovalStatus) => {
if (status === undefined || status === null) return 'clock'
const iconMap = {
1: 'clock',
2: 'close',
3: 'success'
}
return iconMap[status] || 'clock'
}
// 预览文件
const previewFile = (file: AttachmentFile) => {
if (!file.filePath) {
showToast('文件路径不存在')
return
}
const url = getFilePreviewUrl(file.filePath)
const isPdf = file.fileName?.toLowerCase().endsWith('.pdf') || file.filePath?.toLowerCase().endsWith('.pdf')
if (isPdf) {
pdfUrl.value = url
pdfPreviewVisible.value = true
pdfLoading.value = true
} else {
window.open(url, '_blank')
}
}
const handlePdfLoaded = () => {
pdfLoading.value = true
}
const handlePdfError = (error: any) => {
console.error('PDF加载失败:', error)
pdfLoading.value = false
showToast('PDF加载失败')
}
// 计算总金额
const getTotalAmount = () => {
if (!purchaseItems.value || purchaseItems.value.length === 0) return 0
return purchaseItems.value.reduce((sum, item) => sum + (item.price * item.quantity), 0)
}
// 返回上一页
const goBack = () => {
const from = route.query.from as string;
if (from) {
router.push(`/?tab=${from}`);
} else {
if (window.history.length > 1) {
router.back();
} else {
router.push('/');
}
}
}
// 显示审批弹窗
const showApprovalDialog = (status: ApproveBtn) => {
currentApprovalStatus.value = status
approvalOpinion.value = ''
selectedTag.value = ''
approvalDialogVisible.value = true
}
// 获取默认意见标签
const getOpinionTags = () => {
if (currentApprovalStatus.value === 0) {
// 驳回常用意见
return ['经审查有问题,驳回']
} else {
// 通过常用意见
return ['所有信息已阅,审核通过']
}
}
// 选择标签
const selectTag = (tag: string) => {
if (selectedTag.value === tag) {
// 如果点击的是已选中的标签,则取消选择并清空输入框
selectedTag.value = ''
approvalOpinion.value = ''
} else {
// 选择新标签并填入输入框
selectedTag.value = tag
approvalOpinion.value = tag
}
}
// 提交审批
const submitApproval = async () => {
if (!currentPurchaseTodo.value) {
showToast('审批信息不完整')
return
}
const opinion = approvalOpinion.value.trim()
if (currentApprovalStatus.value === 0 && !opinion) {
showToast('请输入驳回原因')
return
}
submitting.value = true
try {
const params = {
businessKey: currentPurchase.value?.purchaseNo,
processKey: currentPurchaseTodo.value.processKey,
taskId: currentPurchaseTodo.value.taskId,
variables: {
comment: opinion,
approveBtn: currentApprovalStatus.value
}
}
console.log('提交审批参数:', params)
await submitPurchaseApproval(params)
showToast('1234')
showSuccessToast(currentApprovalStatus.value === 0 ? '驳回成功' : '审批通过')
approvalDialogVisible.value = false
// 审批完成后返回列表
router.push('/list?tab=purchase')
} catch (error) {
console.error('提交审批失败:', error)
showToast('提交审批失败')
} finally {
submitting.value = false
}
}
// 获取审批历史
const fetchApprovalHistory = async (purchaseNo: string) => {
try {
const response = await getPurchaseApprovalHistory(purchaseNo)
if (response.data && response.data.data) {
approvalHistory.value = response.data.data
console.log('获取审批历史成功:', approvalHistory.value)
}
} catch (error) {
console.error('获取审批历史失败:', error)
approvalHistory.value = []
}
}
onMounted(async () => {
const purchaseNo = route.params.id as string
if (purchaseNo) {
console.log('采购详情页面加载,采购单号:', purchaseNo)
try {
await purchaseStore.fetchPurchaseDetail(purchaseNo)
console.log('获取采购详情成功:', currentPurchase.value)
// 获取审批历史
await fetchApprovalHistory(purchaseNo)
} catch (error) {
console.error('获取采购详情失败:', error)
showToast('获取采购详情失败')
}
}
})
</script>
<style lang="scss" scoped>
.purchase-detail-page {
min-height: 100vh;
background-color: var(--background-color-secondary);
padding-bottom: 80px; // 为底部按钮留出空间
}
.page-content {
// tabs组件自己控制间距
}
.purchase-header {
background: var(--background-color-primary);
padding: var(--spacing-lg);
border-bottom: 1px solid var(--divider-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.purchase-title {
font-size: 18px;
font-weight: 600;
color: var(--text-color-primary);
flex: 1;
word-break: break-all;
}
.project-status {
background: #FFF7E6;
color: #D48806;
padding: 4px 12px;
border-radius: var(--border-radius-sm);
font-size: 12px;
font-weight: 500;
border: 1px solid #FFD591;
&.completed {
background: #F6FFED;
color: #52C41A;
border: 1px solid #B7EB8F;
}
}
.tab-content {
padding: var(--spacing-lg);
}
.loading-container {
padding: 60px var(--spacing-lg);
text-align: center;
}
.empty-state {
padding: 60px var(--spacing-lg);
}
.card {
background: var(--background-color-primary);
border-radius: var(--border-radius-md);
overflow: hidden;
margin-bottom: var(--spacing-md);
}
.card-header {
padding: var(--spacing-md) var(--spacing-lg);
border-bottom: 1px solid var(--divider-color);
font-size: 16px;
font-weight: 600;
color: var(--text-color-primary);
}
.card-body {
padding: var(--spacing-lg);
}
.info-grid {
display: grid;
gap: var(--spacing-md);
}
.info-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
&.full-width {
flex-direction: column;
gap: 8px;
}
.label {
color: var(--text-color-secondary);
font-size: 14px;
min-width: 100px;
flex-shrink: 0;
}
.value {
color: var(--text-color-primary);
font-size: 14px;
text-align: right;
word-break: break-all;
flex: 1;
}
}
// 产品相关样式
.product-card {
background: var(--background-color-primary);
border-radius: var(--border-radius-md);
margin-bottom: var(--spacing-md);
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
&:last-child {
margin-bottom: 0;
}
}
.product-header {
background: var(--background-color-secondary);
padding: var(--spacing-md);
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.product-index {
background: var(--primary-color);
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 500;
flex-shrink: 0;
}
.product-main-info {
flex: 1;
}
.product-code-price {
display: flex;
justify-content: space-between;
align-items: center;
.product-code {
font-size: 16px;
font-weight: 600;
color: var(--text-color-primary);
}
.product-total-price {
font-size: 18px;
font-weight: 700;
color: var(--primary-color);
}
}
.product-details {
padding: var(--spacing-md);
}
.product-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--spacing-sm);
&:last-child {
margin-bottom: 0;
}
}
.product-label {
color: var(--text-color-secondary);
font-size: 14px;
min-width: 80px;
flex-shrink: 0;
}
.product-value {
color: var(--text-color-primary);
font-size: 14px;
text-align: right;
flex: 1;
word-break: break-all;
}
// 产品总计样式
.product-summary {
background: var(--background-color-primary);
border-radius: var(--border-radius-md);
padding: var(--spacing-lg);
margin-top: var(--spacing-lg);
border: 2px solid var(--primary-color);
}
.summary-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
&:last-child {
margin-bottom: 0;
}
}
.summary-label {
font-size: 16px;
font-weight: 500;
color: var(--text-color-primary);
}
.summary-value {
font-size: 16px;
font-weight: 600;
color: var(--text-color-primary);
&.final-amount {
font-size: 20px;
font-weight: 700;
color: var(--primary-color);
}
}
.final-total {
border-top: 2px solid var(--divider-color);
padding-top: var(--spacing-md);
margin-top: var(--spacing-md);
.summary-label {
font-size: 18px;
font-weight: 600;
color: var(--primary-color);
}
}
// 附件列表样式
.file-list {
.file-item {
display: flex;
align-items: center;
padding: var(--spacing-md);
background: var(--background-color-tertiary);
border-radius: var(--border-radius-sm);
margin-bottom: var(--spacing-sm);
cursor: pointer;
&:last-child {
margin-bottom: 0;
}
&:active {
background: var(--border-color);
}
.van-icon:first-child {
color: var(--primary-color);
margin-right: var(--spacing-md);
}
.file-info {
flex: 1;
.file-name {
font-size: 14px;
color: var(--text-color-primary);
margin-bottom: var(--spacing-xs);
}
.file-meta {
font-size: 12px;
color: var(--text-color-tertiary);
}
}
.van-icon:last-child {
color: var(--text-color-tertiary);
}
}
}
// 审批历史样式
.approval-item {
.approval-user,
.approval-time,
.approval-next-user {
font-size: 12px;
color: var(--text-color-secondary);
margin-bottom: var(--spacing-xs);
}
.approval-next-user {
color: var(--primary-color);
font-weight: 500;
}
.approval-opinion {
font-size: 12px;
color: var(--text-color-primary);
background: var(--background-color-tertiary);
padding: var(--spacing-sm);
border-radius: var(--border-radius-sm);
line-height: 1.4;
}
}
:deep(.van-steps--vertical) {
padding-left: 0;
}
:deep(.van-step__content) {
padding-bottom: var(--spacing-lg);
}
// 审批操作按钮
.approval-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: var(--spacing-lg);
background: var(--background-color-primary);
border-top: 1px solid var(--divider-color);
display: flex;
gap: var(--spacing-lg);
.van-button {
flex: 1;
}
}
// 审批弹窗
.approval-dialog {
height: 100%;
display: flex;
flex-direction: column;
.dialog-header {
padding: var(--spacing-lg);
border-bottom: 1px solid var(--divider-color);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 500;
.van-icon {
cursor: pointer;
}
}
.dialog-body {
flex: 1;
padding: var(--spacing-lg);
overflow-y: auto;
}
.dialog-footer {
padding: var(--spacing-lg);
border-top: 1px solid var(--divider-color);
}
}
// 意见标签样式
.opinion-tags {
margin-bottom: var(--spacing-lg);
.tags-title {
font-size: 14px;
color: var(--text-color-secondary);
margin-bottom: var(--spacing-md);
font-weight: 500;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-sm);
.opinion-tag {
cursor: pointer;
transition: all 0.2s ease;
&:hover {
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
}
}
}
.opinion-input {
.van-field {
border: 1px solid var(--divider-color);
border-radius: var(--border-radius-sm);
background: var(--background-color-secondary);
}
}
.pdf-preview-container {
height: 100%;
padding-top: 40px;
background: var(--background-color-secondary);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
position: relative;
}
.pdf-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
.pdf-content {
min-height: 100%;
background: white;
}
.vue-pdf-embed {
width: 100%;
display: block;
}
</style>