feat: 完善报销单审批流程及退回原因追踪

新增直属领导审批通过接口和审批待办列表查询,报销单退回
支持原因码分类和审批环节标记,优化票据附件去重和路径
回退查找,前端新增退回原因对话框、审批收件箱和工作台
图标组件,补充工具函数和单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-20 21:00:47 +08:00
parent f8b25a7ccc
commit 002bf4f756
62 changed files with 5331 additions and 2101 deletions

View File

@@ -14,8 +14,7 @@
</div>
<div class="assistant-copy">
<span class="assistant-tag">AI 报销助手</span>
<h3>描述费用或上传票据AI 直接帮你判断怎么报</h3>
<h3>{{ assistantGreetingName }}描述费用或上传票据AI 直接帮你判断怎么报</h3>
<p>自动识别报销类别核对附件完整性并生成可继续提交的报销草稿</p>
<div class="assistant-input">
@@ -71,9 +70,11 @@
<div class="list-body">
<div v-for="item in todoItems" :key="item.title" class="todo-row">
<div class="todo-icon" :style="{ '--icon-color': item.color }">
<i :class="item.icon"></i>
</div>
<WorkbenchListIcon
:icon-key="item.iconKey"
:color="item.color"
:accent="item.accent"
/>
<div class="todo-copy">
<strong>{{ item.title }}</strong>
@@ -99,9 +100,11 @@
<div class="list-body">
<div v-for="item in progressItems" :key="item.id" class="progress-row">
<div class="todo-icon" :style="{ '--icon-color': item.color }">
<i :class="item.icon"></i>
</div>
<WorkbenchListIcon
:icon-key="item.iconKey"
:color="item.color"
:accent="item.accent"
/>
<div class="todo-copy progress-copy">
<strong>{{ item.title }}</strong>
@@ -142,6 +145,7 @@
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import PanelHead from '../shared/PanelHead.vue'
import WorkbenchListIcon from '../shared/WorkbenchListIcon.vue'
import robotAssistant from '../../assets/robot-helper.png'
import { useSystemState } from '../../composables/useSystemState.js'
import { useToast } from '../../composables/useToast.js'
@@ -167,6 +171,10 @@ const SESSION_TYPE_KNOWLEDGE = 'knowledge'
const hasExpenseConversation = computed(() => Boolean(latestExpenseConversation.value?.conversation_id || latestExpenseConversation.value?.conversationId))
const expenseActionLabel = computed(() => (hasExpenseConversation.value ? '继续报销' : '新建报销'))
const expenseActionIcon = computed(() => (hasExpenseConversation.value ? 'mdi mdi-history' : 'mdi mdi-magnify-scan'))
const assistantGreetingName = computed(() => {
const user = currentUser.value || {}
return String(user.name || user.username || '同事').trim() || '同事'
})
function buildSelectedFileKey(file) {
return [file?.name, file?.size, file?.lastModified, file?.type].join('__')
@@ -318,24 +326,27 @@ const todoItems = [
tipLabel: 'AI 建议',
suggestion: '补充客户单位、客户人数、我方陪同人员',
action: '去补充',
icon: 'mdi mdi-account-group-outline',
color: '#10b981'
iconKey: 'hospitality',
color: '#0d9668',
accent: '#6ee7b7'
},
{
title: '差旅报销单待提交',
tipLabel: 'AI 建议',
suggestion: '补齐出发交通,可直接生成报销单',
action: '继续填写',
icon: 'mdi mdi-briefcase-outline',
color: '#16a34a'
iconKey: 'travelDraft',
color: '#15803d',
accent: '#86efac'
},
{
title: '有 5 张票据未关联报销单',
tipLabel: 'AI 建议',
suggestion: '其中 3 张疑似交通费,可合并生成交通报销',
action: '去整理',
icon: 'mdi mdi-receipt-text-outline',
color: '#3b82f6'
iconKey: 'receipts',
color: '#2563eb',
accent: '#93c5fd'
}
]
@@ -349,8 +360,9 @@ const progressItems = [
date: '2026-05-03',
status: '主管审批中',
tone: 'success',
icon: 'mdi mdi-airplane',
color: '#10b981'
iconKey: 'flight',
color: '#0d9668',
accent: '#6ee7b7'
},
{
id: 'transport',
@@ -359,8 +371,9 @@ const progressItems = [
date: '2026-05-02',
status: '财务复核中',
tone: 'info',
icon: 'mdi mdi-car-outline',
color: '#3b82f6'
iconKey: 'transport',
color: '#2563eb',
accent: '#93c5fd'
},
{
id: 'office',
@@ -369,8 +382,9 @@ const progressItems = [
date: '2026-05-01',
status: '已到账',
tone: 'mint',
icon: 'mdi mdi-cart-outline',
color: '#16a34a'
iconKey: 'procurement',
color: '#059669',
accent: '#a7f3d0'
}
]
@@ -522,19 +536,6 @@ watch(
align-content: center;
}
.assistant-tag {
display: inline-flex;
width: fit-content;
align-items: center;
padding: 6px 12px;
border-radius: 999px;
background: linear-gradient(135deg, rgba(16, 185, 129, 0.14), rgba(59, 130, 246, 0.12));
color: #0f766e;
font-size: 12px;
font-weight: 800;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);
}
.assistant-copy h3 {
color: #0f172a;
font-size: 26px;
@@ -798,7 +799,7 @@ watch(
.todo-row,
.progress-row {
display: grid;
grid-template-columns: 48px minmax(0, 1fr) auto;
grid-template-columns: 56px minmax(0, 1fr) auto;
gap: 14px;
align-items: center;
padding: 14px 0;
@@ -811,17 +812,6 @@ watch(
border-top: 0;
}
.todo-icon {
width: 48px;
height: 48px;
display: grid;
place-items: center;
border-radius: 14px;
background: color-mix(in srgb, var(--icon-color) 12%, white);
color: var(--icon-color);
font-size: 24px;
}
.todo-copy {
min-width: 0;
}
@@ -877,7 +867,7 @@ watch(
}
.progress-row {
grid-template-columns: 48px minmax(0, 1fr) minmax(84px, auto) minmax(104px, auto);
grid-template-columns: 56px minmax(0, 1fr) minmax(84px, auto) minmax(104px, auto);
gap: 14px 16px;
}
@@ -1107,7 +1097,7 @@ watch(
.todo-row,
.progress-row {
grid-template-columns: 48px minmax(0, 1fr);
grid-template-columns: 56px minmax(0, 1fr);
}
.progress-amount {