优化报销功能

This commit is contained in:
hero920103 committed 2026-10-16 23:46:49 +08:00
1 parent 395c7f717e
commit 4306cbd74b
19 files changed
+2059 -534

No files matched your search

+152 -80
View File
@@ -74,30 +74,45 @@
</view>
</view>
<!-- 审批记录 -->
<!-- 审批记录(与审批中心一致的审批流样式) -->
<view class="block-card">
<view class="block-head">
<text class="block-title">审批记录</text>
<text class="block-total">{{ records.length }} 条</text>
</view>
<view v-if="!records.length" class="items-empty">
<view v-if="!flowNodes.length" class="items-empty">
<text>暂无审批记录</text>
</view>
<view v-else class="timeline">
<view v-for="record in records" :key="record.id" class="timeline-item">
<view class="timeline-dot" />
<view class="timeline-content">
<text class="timeline-title">
{{ getExpenseActionLabel(record.action) }} · {{ record.approver_name || '-' }}
</text>
<text v-if="record.comment" class="timeline-comment">{{ record.comment }}</text>
<text class="timeline-time">{{ formatTime(record.create_time) }}</text>
<view v-else class="timeline-list">
<view
v-for="(node, idx) in flowNodes"
:key="node.key"
class="tl-item"
:class="{ edge: node.isEdge, current: node.isCurrent }"
>
<view class="tl-rail">
<view class="tl-avatar-wrap">
<view class="tl-avatar" :class="{ 'avatar-end': node.isEdge }">{{ node.avatar }}</view>
<view class="tl-badge" :class="node.badgeClass">{{ node.badgeIcon }}</view>
</view>
<view v-if="idx !== flowNodes.length - 1" class="tl-line" />
</view>
<view class="tl-main">
<view class="tl-head">
<view class="tl-title">
<text class="tl-name">{{ node.title }}</text>
<text v-if="node.sub" class="tl-sub">{{ node.sub }}</text>
</view>
<text class="tl-time">{{ node.time }}</text>
</view>
<view v-if="node.comment" class="tl-comment">{{ node.comment }}</view>
</view>
</view>
</view>
</view>
</template>
<!-- 操作按钮(统一放详情页,仅草稿可操作) -->
<!-- 操作按钮:草稿可删除/编辑/提交;审批通过后由报销人确认到账 -->
<view v-if="detail && isDraft" class="footer">
<view class="footer-btn">
<u-button type="error" plain icon="trash" text="删除" @click="onDelete" />
@@ -109,16 +124,24 @@
<u-button type="primary" icon="arrow-up" text="提交审批" @click="onSubmit" />
</view>
</view>
<!-- 审批通过:等待报销人确认款项是否已到账 -->
<view v-else-if="detail && isApproved" class="footer">
<view class="footer-btn">
<u-button type="success" icon="checkmark-circle" text="确认到账" @click="onConfirmPaid" />
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad, onShow, onUniAppEvent } from '@dcloudio/uni-app'
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
import {
getExpense,
deleteExpense,
submitExpense,
confirmExpensePaid,
fetchExpenseRecords,
getExpenseStatusLabel,
getExpenseStatusType,
@@ -135,12 +158,95 @@ const records = ref([])
const loading = ref(false)
const isDraft = computed(() => Number(detail.value?.status) === EXPENSE_STATUS.DRAFT)
// 审批通过:等待报销人本人确认款项到账
const isApproved = computed(() => Number(detail.value?.status) === EXPENSE_STATUS.APPROVED)
const descText = computed(() => {
const desc = (detail.value?.description || '').trim()
return desc || ''
})
/** 审批记录 -> 审批流节点:每条记录一个节点,末尾补「流程结束」节点展示单据当前状态 */
const flowNodes = computed(() => {
const nodes = (records.value || []).map((record, index) => {
const action = Number(record.action)
const step = Number(record.step)
return {
key: `record-${record.id || index}`,
avatar: avatarOf(record.approver_name),
badgeClass: badgeClassOfAction(action),
badgeIcon: badgeIconOfAction(action),
title:
action === 1
? `${record.approver_name || '发起人'}(申请人)`
: record.approver_name || '审批人',
sub:
step > 0
? `${getExpenseActionLabel(action)} · 第 ${step} 步`
: getExpenseActionLabel(action),
time: shortTime(record.create_time),
comment: record.comment ? String(record.comment) : '',
isEdge: action === 1,
isCurrent: false,
}
})
if (!detail.value) return nodes
nodes.push(buildEndNode())
return nodes
})
/** 末尾「流程结束」节点:显示单据当前状态,让申请人一眼看出流程是否走完 */
function buildEndNode() {
const status = Number(detail.value?.status)
const map = {
0: { label: '草稿', cls: 'badge-skip', icon: '—' },
1: { label: '审批中', cls: 'badge-running', icon: '·' },
2: { label: '已通过', cls: 'badge-pass', icon: '✓' },
3: { label: '已驳回', cls: 'badge-reject', icon: '✕' },
4: { label: '已撤回', cls: 'badge-skip', icon: '—' },
5: { label: '已打款', cls: 'badge-pass', icon: '✓' },
}
const hit = map[status] || { label: '未知', cls: 'badge-skip', icon: '—' }
return {
key: 'flow-end',
avatar: '结',
badgeClass: hit.cls,
badgeIcon: hit.icon,
title: '流程结束',
sub: hit.label,
time: shortTime(detail.value?.update_time),
comment: '',
isEdge: true,
isCurrent: status === 1,
}
}
function avatarOf(name) {
const text = String(name || '').trim()
return text ? text.slice(0, 1) : '系'
}
/** 动作角标:1提交/2通过 -> 绿✓;3驳回 -> 红✕;4转审 -> 橙;5撤回 -> 灰 */
function badgeClassOfAction(action) {
if (action === 3) return 'badge-reject'
if (action === 1 || action === 2) return 'badge-pass'
if (action === 4) return 'badge-running'
return 'badge-skip'
}
function badgeIconOfAction(action) {
if (action === 3) return '✕'
if (action === 1 || action === 2) return '✓'
if (action === 4) return '·'
return '—'
}
/** 时间只展示到分钟 */
function shortTime(value) {
if (!value) return ''
return String(value).slice(0, 16)
}
onLoad((query) => {
if (query?.id) {
expenseId.value = query.id
@@ -152,12 +258,18 @@ onShow(() => {
if (expenseId.value) loadDetail()
})
// 监听审批通过后的刷新事件
onUniAppEvent('expense-updated', (data) => {
// 监听审批通过 / 确认到账后的刷新事件。
// 新版 uni-app 已不再导出 onUniAppEvent,改用全局事件总线并在页面卸载时解绑。
function handleExpenseUpdated(data) {
if (data?.id === expenseId.value) {
// 延迟一点刷新,确保后端数据已同步
setTimeout(() => loadDetail(), 300)
}
}
uni.$on('expense-updated', handleExpenseUpdated)
onUnload(() => {
uni.$off('expense-updated', handleExpenseUpdated)
})
/** 后端时间字段可能为 "YYYY-MM-DD HH:mm:ss" 格式,iOS 需转 T 分隔 */
@@ -236,6 +348,29 @@ function onSubmit() {
})
}
/** 报销人确认款项已到账:确认后单据状态变为「已打款」 */
async function onConfirmPaid() {
const confirmed = await new Promise((resolve) => {
uni.showModal({
title: '确认到账',
content: `确认已收到 ¥${moneyText(detail.value?.total_amount)} 的报销款吗?确认后单据状态变为「已打款」。`,
confirmText: '确认已到账',
cancelText: '暂未到账',
success: (res) => resolve(!!res.confirm),
fail: () => resolve(false)
})
})
if (!confirmed) return
try {
await confirmExpensePaid(expenseId.value)
uni.showToast({ title: '已确认到账', icon: 'success' })
await loadDetail()
uni.$emit('expense-updated', { id: expenseId.value })
} catch (e) {
// request 封装已弹出服务端错误提示
}
}
function onDelete() {
uni.showModal({
title: '删除报销单',
@@ -443,71 +578,8 @@ function onDelete() {
color: $color-text-muted;
}
.timeline {
padding-top: 4rpx;
}
.timeline-item {
display: flex;
gap: 20rpx;
padding-bottom: 28rpx;
position: relative;
&:last-child {
padding-bottom: 4rpx;
&::after {
display: none;
}
}
&::after {
content: '';
position: absolute;
left: 9rpx;
top: 24rpx;
bottom: 0;
width: 2rpx;
background: $color-divider;
}
}
.timeline-dot {
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background: #2563eb;
flex-shrink: 0;
margin-top: 4rpx;
}
.timeline-content {
flex: 1;
min-width: 0;
}
.timeline-title {
display: block;
font-size: 26rpx;
color: $color-text;
font-weight: 500;
}
.timeline-comment {
display: block;
margin-top: 6rpx;
font-size: 24rpx;
color: $color-text-secondary;
line-height: 1.6;
word-break: break-all;
}
.timeline-time {
display: block;
margin-top: 6rpx;
font-size: 22rpx;
color: $color-text-muted;
}
/* 审批记录沿用审批中心的审批流样式 */
@include flow-timeline;
.footer {
position: fixed;