Files
yunzerwebsiteallinone/uniapp/pages/erp/document/detail.vue
T
2026-10-19 01:13:17 +08:00

581 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<view v-if="loading && !doc" class="state-box">
<text class="state-text">加载中...</text>
</view>
<block v-else-if="doc">
<!-- 概要 -->
<view class="head-card">
<view class="head-top">
<text class="head-type">{{ doc.doc_type_label || doc.doc_type }}</text>
<u-tag :text="doc.status_label || docStatusLabel(doc.status)" :type="docStatusType(doc.status)" size="mini" />
</view>
<text class="head-amount">¥{{ moneyText(doc.actual_amount) }}</text>
<text class="head-no">{{ doc.doc_no }}</text>
<view class="head-grid">
<view class="head-cell">
<text class="cell-label">业务日期</text>
<text class="cell-value">{{ doc.biz_date }}</text>
</view>
<view class="head-cell">
<text class="cell-label">数量合计</text>
<text class="cell-value">{{ qtyText(doc.total_qty) }}</text>
</view>
<view class="head-cell">
<text class="cell-label">优惠金额</text>
<text class="cell-value">¥{{ moneyText(doc.discount_amount) }}</text>
</view>
</view>
</view>
<!-- 往来与仓储 -->
<view class="card">
<view class="line">
<text class="line-label">{{ partnerLabel }}</text>
<text class="line-value">{{ doc.partner_name || '—' }}</text>
</view>
<view class="line">
<text class="line-label">仓库</text>
<text class="line-value">{{ doc.warehouse_name || '—' }}</text>
</view>
<view v-if="doc.to_warehouse_name" class="line">
<text class="line-label">调入仓库</text>
<text class="line-value">{{ doc.to_warehouse_name }}</text>
</view>
<view v-if="doc.source_doc_no" class="line" @tap="goSource">
<text class="line-label">来源单据</text>
<view class="line-value link">
<text>{{ doc.source_doc_no }}</text>
<FaIcon name="chevron-right" color="#2563eb" :size="14" />
</view>
</view>
<view class="line">
<text class="line-label">制单</text>
<text class="line-value">{{ doc.create_user_name || '—' }} · {{ doc.create_time_text }}</text>
</view>
<view v-if="doc.auditor_name" class="line">
<text class="line-label">审核</text>
<text class="line-value">{{ doc.auditor_name }} · {{ doc.audit_time_text }}</text>
</view>
<view v-if="doc.remark" class="remark">
<text>备注:{{ doc.remark }}</text>
</view>
</view>
<!-- 明细 -->
<view class="card">
<view class="block-head">
<text class="block-title">商品明细({{ items.length }} 行)</text>
<text class="block-tip">点行可看实时库存</text>
</view>
<view v-for="(item, index) in items" :key="item.id || index" class="item-row">
<view class="item-head">
<text class="item-name">{{ item.product_name }}</text>
<text class="item-amount">¥{{ moneyText(item.amount) }}</text>
</view>
<view class="item-meta">
<text v-if="item.spec">{{ item.spec }}</text>
<text>{{ qtyText(item.quantity) }} {{ item.unit }} × ¥{{ moneyText(item.unit_price) }}</text>
<text v-if="item.batch_no">批次 {{ item.batch_no }}</text>
<text>结存 {{ qtyText(item.stock_qty) }}</text>
</view>
</view>
<view v-if="!items.length" class="item-empty">
<text>暂无明细</text>
</view>
</view>
<!-- 审批实例 -->
<view v-if="instance" class="card">
<view class="block-head">
<text class="block-title">审批进度</text>
<u-tag :text="instance.status_label || ''" type="primary" size="mini" />
</view>
<view class="line">
<text class="line-label">流程</text>
<text class="line-value">{{ instance.flow_name || '—' }}</text>
</view>
<view class="line">
<text class="line-label">当前节点</text>
<text class="line-value">{{ instance.current_node || '已结束' }}</text>
</view>
<view v-for="task in instance.tasks || []" :key="task.task_id" class="task-row">
<view class="task-dot" :class="taskClass(task)"></view>
<view class="task-body">
<view class="task-line1">
<text class="task-node">{{ task.node_name }}</text>
<text class="task-status">{{ task.status_label }}</text>
</view>
<text class="task-sub">
{{ task.approver_name }}{{ task.finish_time ? ' · ' + dateTimeText(task.finish_time) : '' }}
</text>
<text v-if="task.comment" class="task-comment">{{ task.comment }}</text>
</view>
</view>
</view>
</block>
<!-- 操作栏 -->
<view v-if="doc" class="actions">
<block v-if="isDraft">
<view class="act ghost" @tap="goEdit">编辑</view>
<view class="act ghost" @tap="onDelete">删除</view>
<view class="act" :class="{ disabled: busy }" @tap="onSubmit">提交审批</view>
<view class="act primary" :class="{ disabled: busy }" @tap="onApprove">审核生效</view>
</block>
<block v-else-if="doc.status === DOC_STATUS.APPROVING">
<view class="act ghost" @tap="onCancel">撤回作废</view>
</block>
<block v-else-if="doc.status === DOC_STATUS.APPROVED">
<view v-if="nextTypes.length" class="act ghost" @tap="onCancel">作废</view>
<view
v-for="next in nextTypes"
:key="next.value"
class="act primary"
@tap="goDownstream(next.value)"
>
生成{{ next.label }}
</view>
</block>
<block v-else>
<view class="act ghost" @tap="goEdit">再次编辑</view>
</block>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import FaIcon from '@/components/FaIcon.vue'
import {
DOC_STATUS,
cancelDoc,
dateTimeText,
deleteDoc,
docStatusLabel,
docStatusType,
approveDoc,
fetchDocTypes,
getDoc,
moneyText,
qtyText,
submitDoc
} from '@/api/erp.js'
/** 已生效单据可继续生成的下游单据(与后端来源单类型白名单一致) */
const NEXT_MAP = {
purchase_order: ['purchase_in'],
sales_order: ['sales_out'],
purchase_in: ['purchase_return'],
sales_out: ['sales_return', 'other_out']
}
const id = ref(0)
const doc = ref(null)
const loading = ref(false)
const busy = ref(false)
const docTypes = ref([])
const items = computed(() => (doc.value || {}).items || [])
const instance = computed(() => (doc.value || {}).instance || null)
const isDraft = computed(() => {
const s = (doc.value || {}).status
return s === DOC_STATUS.DRAFT || s === DOC_STATUS.REJECTED
})
const partnerLabel = computed(() => {
const type = (doc.value || {}).partner_type
if (type === 'supplier') return '供应商'
if (type === 'customer') return '客户'
return '往来单位'
})
const nextTypes = computed(() => {
const types = NEXT_MAP[(doc.value || {}).doc_type] || []
return types.map(value => {
const hit = docTypes.value.find(item => item.value === value)
return { value, label: hit ? hit.label : value }
})
})
onLoad(query => {
id.value = Number((query || {}).id || 0)
})
onShow(() => {
load()
loadTypes()
})
async function load() {
if (!id.value) return
loading.value = true
try {
doc.value = await getDoc(id.value)
} catch (e) {
doc.value = null
} finally {
loading.value = false
}
}
async function loadTypes() {
try {
docTypes.value = await fetchDocTypes()
} catch (e) {
docTypes.value = []
}
}
function taskClass(task) {
const label = String(task.status_label || '')
if (label.includes('通过') || label.includes('完成')) return 'done'
if (label.includes('驳回')) return 'rejected'
if (label.includes('待') || label.includes('进行')) return 'pending'
return 'waiting'
}
function goEdit() {
uni.navigateTo({ url: `/pages/erp/document/edit?id=${id.value}` })
}
function goSource() {
const sourceId = (doc.value || {}).source_doc_id
if (!sourceId) return
uni.navigateTo({ url: `/pages/erp/document/detail?id=${sourceId}` })
}
function goDownstream(docType) {
uni.navigateTo({ url: `/pages/erp/document/edit?doc_type=${docType}&source_doc_id=${id.value}` })
}
async function onApprove() {
if (busy.value) return
busy.value = true
try {
await approveDoc(id.value)
uni.showToast({ title: '已审核生效', icon: 'success' })
await load()
} catch (e) {
// 错误提示已由请求层统一弹出
} finally {
busy.value = false
}
}
async function onSubmit() {
if (busy.value) return
busy.value = true
try {
const instanceId = await submitDoc(id.value)
uni.showToast({ title: instanceId ? '已提交审批' : '已提交', icon: 'success' })
await load()
} catch (e) {
// 未绑定审批流等业务失败已在请求层提示,可退回用「审核生效」
} finally {
busy.value = false
}
}
async function onCancel() {
const confirmed = await confirm('作废后单据不再参与库存与报表,确定继续?')
if (!confirmed) return
await cancelDoc(id.value)
uni.showToast({ title: '已作废', icon: 'success' })
await load()
}
async function onDelete() {
const confirmed = await confirm('删除该草稿单据?')
if (!confirmed) return
await deleteDoc(id.value)
uni.showToast({ title: '已删除', icon: 'success' })
setTimeout(() => uni.navigateBack(), 400)
}
function confirm(content) {
return new Promise(resolve => {
uni.showModal({
title: '提示',
content,
success: res => resolve(!!res.confirm),
fail: () => resolve(false)
})
})
}
</script>
<style lang="scss" scoped>
@import '@/src/styles/page-common.scss';
.page {
min-height: 100vh;
background: $color-bg-page;
padding: 24rpx $page-padding-x calc(160rpx + env(safe-area-inset-bottom));
}
.state-box {
display: flex;
justify-content: center;
padding: 160rpx 0;
}
.state-text {
font-size: 28rpx;
color: $color-text-muted;
}
.head-card {
padding: 32rpx 28rpx;
margin-bottom: 24rpx;
border-radius: $radius-lg;
background: $hero-gradient;
color: #ffffff;
box-shadow: $shadow-card;
}
.head-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.head-type {
font-size: 30rpx;
font-weight: 600;
}
.head-amount {
display: block;
margin-top: 16rpx;
font-size: 48rpx;
font-weight: 600;
}
.head-no {
display: block;
margin-top: 6rpx;
font-size: 23rpx;
opacity: 0.85;
}
.head-grid {
display: flex;
margin-top: 24rpx;
padding-top: 20rpx;
border-top: 1rpx solid rgba(255, 255, 255, 0.25);
}
.head-cell {
flex: 1;
}
.card {
@include card;
padding: 28rpx;
margin-bottom: 24rpx;
}
.block-head {
@include block-head;
}
.block-title {
@include section-title;
}
.block-tip {
font-size: 22rpx;
color: $color-text-muted;
}
.line {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
padding: 14rpx 0;
}
.line-label {
font-size: 26rpx;
color: $color-text-muted;
flex-shrink: 0;
}
.line-value {
flex: 1;
text-align: right;
font-size: 26rpx;
color: $color-text;
}
.line-value.link {
color: $color-primary;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4rpx;
}
.remark {
margin-top: 12rpx;
padding-top: 16rpx;
border-top: 1rpx solid $color-divider;
font-size: 24rpx;
color: $color-text-secondary;
line-height: 1.6;
}
.item-row {
padding: 20rpx 0;
border-bottom: 1rpx solid $color-divider;
&:last-child {
border-bottom: none;
}
}
.item-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.item-name {
font-size: 28rpx;
color: $color-text;
font-weight: 500;
}
.item-amount {
font-size: 28rpx;
color: $color-text;
font-weight: 600;
}
.item-meta {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
margin-top: 8rpx;
font-size: 22rpx;
color: $color-text-muted;
}
.item-empty {
padding: 40rpx 0;
text-align: center;
font-size: 26rpx;
color: $color-text-muted;
}
.task-row {
display: flex;
gap: 16rpx;
padding: 18rpx 0;
}
.task-dot {
width: 18rpx;
height: 18rpx;
margin-top: 10rpx;
border-radius: 50%;
flex-shrink: 0;
background: $color-border;
&.done {
background: $color-success;
}
&.rejected {
background: $color-danger;
}
&.pending {
background: $color-warning;
}
}
.task-body {
flex: 1;
}
.task-line1 {
display: flex;
align-items: center;
justify-content: space-between;
}
.task-node {
font-size: 26rpx;
color: $color-text;
font-weight: 500;
}
.task-status {
font-size: 22rpx;
color: $color-text-muted;
}
.task-sub {
display: block;
margin-top: 6rpx;
font-size: 22rpx;
color: $color-text-muted;
}
.task-comment {
display: block;
margin-top: 8rpx;
padding: 12rpx 16rpx;
border-radius: $radius-sm;
background: $color-bg-page;
font-size: 23rpx;
color: $color-text-secondary;
}
.actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
gap: 16rpx;
padding: 20rpx $page-padding-x calc(20rpx + env(safe-area-inset-bottom));
background: $color-card;
box-shadow: 0 -4rpx 16rpx rgba(15, 23, 42, 0.06);
@include dialog-responsive;
}
.act {
flex: 1;
height: 78rpx;
line-height: 78rpx;
text-align: center;
border-radius: $radius-full;
font-size: 26rpx;
background: $color-bg-page;
color: $color-text-secondary;
&.ghost {
background: $color-bg-page;
color: $color-text-secondary;
}
&.primary {
background: #2563eb;
color: #ffffff;
font-weight: 500;
}
&.disabled {
opacity: 0.5;
}
&:active {
opacity: 0.85;
}
}
</style>