feat: 报销审批流重构与管家计划全链路贯通

- 重构报销状态注册表、审批流路由与平台风险标记
- 完善管家意图规划器与模型计划构建器全链路
- 新增 OCR Worker 脚本、数据库会话管理与通知状态
- 优化文档中心、日志视图、预算中心与员工管理交互
- 增强工作台摘要、图标资源与全局主题样式
- 补充审批路由、状态注册、OCR 服务与管家规划器测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-06 17:19:07 +08:00
parent f60cebadb8
commit e124e4bbcb
162 changed files with 9161 additions and 1941 deletions

View File

@@ -9,7 +9,9 @@
<article class="panel assistant-hero" :style="{ '--assistant-bg-image': `url(${workbenchHeroBackground})` }">
<div class="assistant-copy">
<h1>{{ displayUserName }}我是您的 <span>小财管家</span></h1>
<h1 class="assistant-hero-title">
{{ typedTitlePrefix }}<span v-if="titleTypingDone">小财管家</span><span v-if="!titleTypingDone" class="typing-cursor">|</span>
</h1>
<input
ref="fileInputRef"
@@ -173,11 +175,12 @@
<div :class="['capability-grid', capabilityGridClass]" aria-label="AI 财务助手能力">
<button
v-for="item in visibleAssistantCapabilities"
v-for="(item, index) in visibleAssistantCapabilities"
:key="item.key"
type="button"
class="capability-card panel"
:class="`capability-card--${item.tone}`"
:style="{ '--delay': `${index * 80 + 100}ms` }"
@click="openCapabilityAssistant(item)"
>
<WorkbenchListIcon
@@ -196,18 +199,19 @@
</div>
<div class="workbench-content-grid">
<article class="panel workbench-card progress-panel">
<article class="panel workbench-card progress-panel" style="--delay: 200ms;">
<div class="section-head">
<h2>费用进度</h2>
</div>
<div class="progress-list">
<button
v-for="item in visibleProgressItems"
v-for="(item, index) in visibleProgressItems"
:key="item.id"
type="button"
class="progress-row"
:class="{ 'has-long-duration-divider': item.hasLongDurationDivider }"
:style="{ '--item-index': index }"
@click="openWorkbenchTarget(item)"
>
<span class="progress-time-wrapper">
@@ -256,7 +260,7 @@
</article>
<aside class="side-column">
<article class="panel workbench-card side-panel expense-stats-panel">
<article class="panel workbench-card side-panel expense-stats-panel" style="--delay: 300ms;">
<div class="section-head side-card-head">
<h2>费用统计</h2>
<button
@@ -273,10 +277,11 @@
<div class="insight-metric-list" aria-label="费用统计">
<div
v-for="item in visibleExpenseStatItems"
v-for="(item, index) in visibleExpenseStatItems"
:key="item.key"
class="insight-metric-row"
:class="`insight-metric-row--${item.tone}`"
:style="{ '--item-index': index }"
>
<span class="insight-metric-icon" aria-hidden="true">
<i :class="item.icon"></i>
@@ -289,7 +294,7 @@
</div>
</article>
<article class="panel workbench-card side-panel usage-profile-panel">
<article class="panel workbench-card side-panel usage-profile-panel" style="--delay: 400ms;">
<div class="section-head side-card-head">
<h2>用户画像</h2>
<button
@@ -306,10 +311,11 @@
<div class="insight-profile-list" aria-label="用户画像">
<div
v-for="metric in visibleUsageProfileMetrics"
v-for="(metric, index) in visibleUsageProfileMetrics"
:key="metric.key"
class="insight-profile-card"
:class="`insight-profile-card--${metric.tone}`"
:style="{ '--item-index': index }"
>
<span class="insight-profile-icon" aria-hidden="true">
<i :class="metric.icon"></i>
@@ -441,6 +447,35 @@ const displayUserName = computed(() => {
const user = currentUser.value || {}
return String(user.name || user.username || '同事').trim() || '同事'
})
const heroTitleText = computed(() => `嗨,${displayUserName.value},我是您的 `)
const typedTitlePrefix = ref('')
const titleTypingDone = ref(false)
let typingInterval = null
const startTypewriter = () => {
typedTitlePrefix.value = ''
titleTypingDone.value = false
clearInterval(typingInterval)
let i = 0
const text = heroTitleText.value
typingInterval = setInterval(() => {
if (i < text.length) {
typedTitlePrefix.value += text.charAt(i)
i++
} else {
clearInterval(typingInterval)
titleTypingDone.value = true
}
}, 60)
}
watch(displayUserName, (newVal, oldVal) => {
if (oldVal !== newVal && titleTypingDone.value) {
typedTitlePrefix.value = `嗨,${newVal},我是您的 `
}
})
const expenseActionLabel = computed(() => (hasExpenseConversation.value ? '继续报销' : '新建报销'))
const isComposerPending = computed(() => Boolean(pendingAction.value))
const composerPendingLabel = computed(() => {
@@ -861,6 +896,7 @@ async function handleExpenseConversationAction() {
}
onMounted(() => {
startTypewriter()
refreshLocalExpenseSnapshot()
refreshLatestExpenseConversation()
loadCurrentEmployeeProfile()
@@ -869,6 +905,7 @@ onMounted(() => {
})
onBeforeUnmount(() => {
clearInterval(typingInterval)
clearPendingAction()
document.removeEventListener('click', handleWorkbenchDatePickerOutside)
window.removeEventListener(ASSISTANT_SESSION_SNAPSHOT_EVENT, handleAssistantSessionSnapshotChange)