diff --git a/UI/429d8ccd-3c00-40e1-9460-07b5f84a99c8.png b/UI/429d8ccd-3c00-40e1-9460-07b5f84a99c8.png deleted file mode 100644 index 421395c..0000000 Binary files a/UI/429d8ccd-3c00-40e1-9460-07b5f84a99c8.png and /dev/null differ diff --git a/UI/c781e06b-a874-47a0-9c40-b679dd8f958c.png b/UI/c781e06b-a874-47a0-9c40-b679dd8f958c.png deleted file mode 100644 index 933db3a..0000000 Binary files a/UI/c781e06b-a874-47a0-9c40-b679dd8f958c.png and /dev/null differ diff --git a/ai-reimbursement-mac-prototype.html b/ai-reimbursement-mac-prototype.html deleted file mode 100644 index b7934b7..0000000 --- a/ai-reimbursement-mac-prototype.html +++ /dev/null @@ -1,2158 +0,0 @@ - - - - - - AI 报销预审中台 - macOS Prototype - - - -
-
-
- -
- -
-
- 演示环境 · Mock OCR - -
-
- -
- - -
-
-
-
- AI 操作层 -

把一段报销意图变成可预审的差旅报销单

-

面向员工的入口页:输入报销意图,系统创建任务并引导上传发票、火车票、机票行程单等材料。

-
-
- - -
-
- -
-
-
-
-

报销意图

-

自然语言输入会交给受理 Agent 提取出差地点、时间、费用类型和上下文。

-
- IntakeAgent -
- -
- - - -
-
- -
-
- -
-
-
-

最近任务

-

用于返回草稿、查看预审结果或审计轨迹。

-
-
-
- - - -
-
-
- -
-
今日自动识别48发票、火车票、行程单合计
-
需人工补件7主要来自住宿流水和费用日期异常
-
平均预审耗时42sMock OCR 环境下的端到端耗时
-
-
- -
-
-
- 材料收集 -

上传票据并启动 OCR / Agent 编排

-

支持多文件拖拽,文件类型限定为 PDF、JPG、PNG。开始识别后进入 CREATED → MATERIAL_COLLECTING → PARSING → DRAFT_GENERATED。

-
-
- - -
-
- -
-
-
-
-

票据上传

-

拖入真实文件会显示文件名;不选择文件也可以使用内置样例继续演示。

-
- PDF / JPG / PNG -
- -
-
准备启动 Agent0%
-
-
-
- -
-
-
-

已上传文件

-

OCR 结果会在草稿页以低置信度标注和可编辑字段展示。

-
- -
-
-
-
-
- -
-
-
- 影子报销账本 -

OCR 已生成报销草稿,关键字段可人工修正

-

草稿页对应 ShadowReimbursement + ReimbursementItem,编辑后再执行规则引擎预审。

-
-
- - -
-
- -
-
-
-

报销人信息

-

Agent 从意图和组织配置中补齐部门、成本中心、项目。

-
- AI 自动生成 -
-
-
-
-
-
-
-
- -
-
-
-
-

费用明细

-

金额可直接编辑,风险标签会在预审后刷新。

-
- 待预审 -
-
- - - - - - - - - - - - - -
费用类型金额税额发生日期城市商户风险
-
-
- -
-
-
-

AI 识别标注

-

低置信度字段和附件缺口会在这里提示。

-
-
-
- ParseAgent 发现 2 个需要关注的点 - 住宿金额高于上海 T2 标准 18%,且酒店流水附件未找到。出租车票日期与出差日期匹配,但商户字段置信度 0.71。 -
-
-
高铁票 · 可信
-
酒店发票 · 需流水
-
出租车票 · 低置信
-
-
-
-
- -
-
-
- 规则预审 -

预审结果:需补件后再提交

-

规则引擎执行 6 条 MVP 核心规则,并由 ExplainAgent 转化为面向用户的解释和修改建议。

-
-
- - - -
-
- -
-
62
-
-

发现 2 个风险项和 1 个缺件项

-

当前可以继续补充酒店流水,并调整住宿金额或补充审批说明。补件后可重新预审。

-
- 需补件 -
- -
-
-
-
-

命中规则

-

每条规则展示问题、制度依据和建议动作。

-
-
-
-
- -
-
-
-

通过项

-

通过项用来建立用户信心,减少重复沟通。

-
-
-
-
-
-
- -
-
-
- 补件交互 -

按规则命中项补充材料和说明

-

补件页支持补充附件、补充说明、修改字段三类动作,提交后回到预审结果页重新计算风险。

-
-
- - -
-
- -
-
-
-
-

待补件清单

-

由 ExplainAgent 根据缺件类规则自动创建。

-
- 2 项 -
-
-
-
-
-

补充酒店流水

-

住宿费必须提供酒店水单或入住明细,用于核对入住日期、房费和实际支付金额。

-
- 补充附件 -
- -
-
-
-
-

说明住宿标准超额原因

-

上海 T2 住宿标准为 ¥650/晚,当前票据折算 ¥768/晚。可说明客户指定酒店或会务协议价。

-
- 补充说明 -
-
- - -
-
-
-
- -
-
-
-

补件预览

-

提交后系统会自动触发 MATERIAL_COLLECTING → PRECHECKING。

-
-
-
-
尚未添加补件附件。可以直接提交说明,系统会用样例酒店流水完成演示。
-
-
-
-
- -
-
-
- 提交确认 -

确认最终报销单并模拟同步后端系统

-

预审通过后,SyncAgent 将影子账本映射为标准报销单,并写入同步记录和审计日志。

-
-
- - -
-
- -
-
-
-
-

最终摘要

-

提交前不可编辑,若需调整请回到草稿。

-
- 预审通过 -
-
报销人林一舟 · 销售华东
-
报销事由上海客户拜访差旅
-
费用总额¥0.00
-
附件数量4 份
-
同步目标expense_system
-
- -
-
-
-

同步设置

-

MVP 默认使用 mock adapter,展示提交中、已同步、同步失败重试状态。

-
-
-
- - - -
-
-
等待提交0%
-
-
-
-
-
- -
-
-
- 审计日志 -

关键操作均写入可追溯时间线

-

记录文件上传、OCR 识别、Agent 调用、规则命中、用户补件、用户确认和后端同步。

-
-
-
- - - - -
-
-
- -
-
-
-
-
- - -
-
-
- -
- - - - diff --git a/index.html b/index.html index a1906f6..8ae61bb 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,8 @@ + + ReimburseOps - 企业报销智能运营台 diff --git a/reimbursement-admin-prototype.html b/reimbursement-admin-prototype.html deleted file mode 100644 index a1906f6..0000000 --- a/reimbursement-admin-prototype.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - ReimburseOps - 企业报销智能运营台 - - -
- - - diff --git a/src/App.vue b/src/App.vue index 262293a..2d4d753 100644 --- a/src/App.vue +++ b/src/App.vue @@ -21,7 +21,9 @@ :class="{ 'chat-main': activeView === 'chat', 'overview-main': activeView === 'overview', - 'requests-main': activeView === 'requests' + 'requests-main': activeView === 'requests', + 'approval-main': activeView === 'approval', + 'policies-main': activeView === 'policies' }" > + + @@ -118,6 +124,7 @@ import OverviewView from './views/OverviewView.vue' import ChatView from './views/ChatView.vue' import TravelReimbursementCreateView from './views/TravelReimbursementCreateView.vue' import RequestsView from './views/RequestsView.vue' +import ApprovalCenterView from './views/ApprovalCenterView.vue' import PoliciesView from './views/PoliciesView.vue' import AuditView from './views/AuditView.vue' @@ -219,7 +226,9 @@ function backToRequests() { grid-template-rows: auto minmax(0, 1fr); overflow: hidden; } -.main.requests-main { +.main.requests-main, +.main.approval-main, +.main.policies-main { height: 100dvh; grid-template-rows: auto minmax(0, 1fr); overflow: hidden; @@ -229,7 +238,9 @@ function backToRequests() { min-height: 0; overflow: hidden; } -.workarea.requests-workarea { +.workarea.requests-workarea, +.workarea.approval-workarea, +.workarea.policies-workarea { min-height: 0; overflow: hidden; padding: 20px 24px; diff --git a/src/assets/styles/global.css b/src/assets/styles/global.css index f60d834..8a62fae 100644 --- a/src/assets/styles/global.css +++ b/src/assets/styles/global.css @@ -23,11 +23,12 @@ --nav-muted: #7d89a5; --radius: 8px; --ease: cubic-bezier(.2, .8, .2, 1); - font-family: Inter, "SF Pro Display", "Segoe UI", "PingFang SC", "Microsoft YaHei", Arial, sans-serif; + font-family: "LXGW WenKai", Inter, "SF Pro Display", "PingFang SC", sans-serif; } * { box-sizing: border-box; } body { margin: 0; min-height: 100dvh; color: var(--text); background: var(--bg); } +.mdi { line-height: 1; vertical-align: middle; } button, input, select, textarea { font: inherit; } button { cursor: pointer; } button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 3px solid rgba(16,185,129,.20); outline-offset: 2px; } diff --git a/src/components/charts/DonutChart.vue b/src/components/charts/DonutChart.vue index 927b0ba..8ce96ea 100644 --- a/src/components/charts/DonutChart.vue +++ b/src/components/charts/DonutChart.vue @@ -26,6 +26,7 @@ import { Tooltip, Legend } from 'chart.js' +import { useAnimationProgress } from '../../composables/useAnimationProgress.js' ChartJS.register(ArcElement, Tooltip, Legend) @@ -35,10 +36,12 @@ const props = defineProps({ centerLabel: { type: String, required: true } }) +const progress = useAnimationProgress([() => props.items], 1150) + const chartData = computed(() => ({ labels: props.items.map((i) => i.name), datasets: [{ - data: props.items.map((i) => i.value), + data: props.items.map((i) => Math.max(Number((i.value * progress.value).toFixed(1)), 0.001)), backgroundColor: props.items.map((i) => i.color), borderWidth: 0, cutout: '68%', @@ -50,6 +53,19 @@ const chartData = computed(() => ({ const chartOptions = { responsive: true, maintainAspectRatio: false, + animation: { + animateRotate: true, + animateScale: true, + duration: 900, + easing: 'easeOutQuart' + }, + transitions: { + active: { + animation: { + duration: 180 + } + } + }, plugins: { legend: { display: false }, tooltip: { @@ -64,7 +80,7 @@ const chartOptions = { usePointStyle: true, callbacks: { label: (ctx) => { - const total = ctx.dataset.data.reduce((a, b) => a + b, 0) + const total = ctx.dataset.data.reduce((a, b) => a + b, 0) || 1 const pct = ((ctx.parsed / total) * 100).toFixed(1) return ` ${ctx.label}: ${pct}%` } @@ -98,6 +114,8 @@ const chartOptions = { place-content: center; pointer-events: none; text-align: center; + animation: donutCenterIn 620ms ease both; + animation-delay: 360ms; } .donut-center strong { @@ -117,6 +135,8 @@ const chartOptions = { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 16px; + animation: donutLegendIn 560ms ease both; + animation-delay: 480ms; } .legend-row { @@ -142,4 +162,33 @@ const chartOptions = { color: #94a3b8; font-size: 11px; } + +@keyframes donutCenterIn { + from { + opacity: 0; + transform: scale(.92); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes donutLegendIn { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (prefers-reduced-motion: reduce) { + .donut-center, + .donut-legend { + animation: none; + } +} diff --git a/src/components/charts/GaugeChart.vue b/src/components/charts/GaugeChart.vue index d9ad929..fc50ce9 100644 --- a/src/components/charts/GaugeChart.vue +++ b/src/components/charts/GaugeChart.vue @@ -3,7 +3,7 @@
- {{ ratio }}% + {{ animatedRatio }}% 已执行
@@ -32,6 +32,7 @@ import { ArcElement, Tooltip } from 'chart.js' +import { useAnimationProgress } from '../../composables/useAnimationProgress.js' ChartJS.register(ArcElement, Tooltip) @@ -43,11 +44,13 @@ const props = defineProps({ }) const ratioValue = computed(() => Number(props.ratio)) +const progress = useAnimationProgress([() => props.ratio], 1150) +const animatedRatio = computed(() => Number((ratioValue.value * progress.value).toFixed(0))) const chartData = computed(() => ({ labels: ['已执行', '剩余'], datasets: [{ - data: [ratioValue.value, 100 - ratioValue.value], + data: [animatedRatio.value, 100 - animatedRatio.value], backgroundColor: ['#10b981', '#e2e8f0'], borderWidth: 0 }] @@ -59,6 +62,11 @@ const chartOptions = { rotation: -90, circumference: 180, cutout: '65%', + animation: { + animateRotate: true, + duration: 900, + easing: 'easeOutQuart' + }, plugins: { legend: { display: false }, tooltip: { enabled: false } @@ -88,6 +96,8 @@ const chartOptions = { transform: translateX(-50%); text-align: center; pointer-events: none; + animation: gaugeCenterIn 620ms ease both; + animation-delay: 360ms; } .gauge-center strong { @@ -109,6 +119,8 @@ const chartOptions = { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; text-align: center; + animation: gaugeSummaryIn 560ms ease both; + animation-delay: 500ms; } .gauge-summary span { @@ -124,4 +136,33 @@ const chartOptions = { font-size: 13px; font-weight: 500; } - \ No newline at end of file + +@keyframes gaugeCenterIn { + from { + opacity: 0; + transform: translateX(-50%) translateY(8px); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} + +@keyframes gaugeSummaryIn { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (prefers-reduced-motion: reduce) { + .gauge-center, + .gauge-summary { + animation: none; + } +} + diff --git a/src/components/charts/TrendChart.vue b/src/components/charts/TrendChart.vue index 9b6bb19..052b25d 100644 --- a/src/components/charts/TrendChart.vue +++ b/src/components/charts/TrendChart.vue @@ -25,6 +25,7 @@ import { Tooltip, Legend } from 'chart.js' +import { useAnimationProgress } from '../../composables/useAnimationProgress.js' ChartJS.register(CategoryScale, LinearScale, BarElement, PointElement, LineElement, Filler, Tooltip, Legend) @@ -35,12 +36,22 @@ const props = defineProps({ avgHours: { type: Array, required: true } }) +const progress = useAnimationProgress([ + () => props.labels, + () => props.applications, + () => props.approved, + () => props.avgHours +], 1200) + +const scaleSeries = (series, decimals = 0) => + series.map((value) => Number((Number(value) * progress.value).toFixed(decimals))) + const chartData = computed(() => ({ labels: props.labels, datasets: [ { label: '申请量(单)', - data: props.applications, + data: scaleSeries(props.applications), backgroundColor: '#10b981', borderRadius: 4, barPercentage: 0.6, @@ -49,7 +60,7 @@ const chartData = computed(() => ({ }, { label: '审批完成量(单)', - data: props.approved, + data: scaleSeries(props.approved), backgroundColor: '#3b82f6', borderRadius: 4, barPercentage: 0.6, @@ -58,7 +69,7 @@ const chartData = computed(() => ({ }, { label: '平均审批时长(小时)', - data: props.avgHours, + data: scaleSeries(props.avgHours, 1), borderColor: '#8b5cf6', backgroundColor: 'transparent', borderWidth: 2, @@ -77,6 +88,10 @@ const chartData = computed(() => ({ const chartOptions = { responsive: true, maintainAspectRatio: false, + animation: { + duration: 900, + easing: 'easeOutQuart' + }, interaction: { mode: 'index', intersect: false diff --git a/src/components/layout/SidebarRail.vue b/src/components/layout/SidebarRail.vue index 315929c..e8c0e9b 100644 --- a/src/components/layout/SidebarRail.vue +++ b/src/components/layout/SidebarRail.vue @@ -9,7 +9,7 @@ 星海科技 @@ -34,7 +34,7 @@ 张晓明 财务管理员 - + @@ -52,8 +52,9 @@ const emit = defineEmits(['navigate', 'openChat']) const sidebarMeta = { overview: { label: '总览' }, requests: { label: '差旅申请/报销' }, + approval: { label: '审批中心', badge: '12' }, chat: { label: 'AI助手' }, - policies: { label: '政策规则' }, + policies: { label: '知识管理' }, audit: { label: '审计追踪' } } @@ -278,7 +279,7 @@ const decoratedNavItems = computed(() => text-overflow: ellipsis; } -.rail-user .pi { +.rail-user .mdi { justify-self: end; color: #718096; font-size: 13px; diff --git a/src/components/layout/TopBar.vue b/src/components/layout/TopBar.vue index 41f5a95..4c92044 100644 --- a/src/components/layout/TopBar.vue +++ b/src/components/layout/TopBar.vue @@ -7,8 +7,8 @@
- - + + @@ -36,7 +36,7 @@
- + {{ activeDateLabel }} @@ -64,7 +64,7 @@ aria-haspopup="dialog" @click="calendarOpen = !calendarOpen" > - + 选择时间段 @@ -72,7 +72,7 @@
选择看板时间段
@@ -87,12 +87,6 @@
-
- - - -
-
- @@ -164,6 +153,8 @@ const emit = defineEmits([ const isChat = computed(() => props.activeView === 'chat') const isOverview = computed(() => props.activeView === 'overview') const isRequests = computed(() => props.activeView === 'requests') +const isApproval = computed(() => props.activeView === 'approval') +const isPolicies = computed(() => props.activeView === 'policies') const calendarOpen = ref(false) const draftStart = ref(props.customRange.start) const draftEnd = ref(props.customRange.end) @@ -357,7 +348,7 @@ function formatRangeLabel(start, end) { white-space: nowrap; } -.range-meta .pi { +.range-meta .mdi { color: #10b981; } @@ -514,28 +505,6 @@ function formatRangeLabel(start, end) { outline: none; } -.quick-presets { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 8px; -} - -.quick-presets button { - height: 34px; - border: 1px solid #e2e8f0; - border-radius: 8px; - background: #f8fafc; - color: #334155; - font-size: 13px; - font-weight: 700; -} - -.quick-presets button:hover { - border-color: rgba(16, 185, 129, .28); - background: #ecfdf5; - color: #0f9f78; -} - .ghost-btn, .apply-btn { height: 36px; @@ -562,20 +531,6 @@ function formatRangeLabel(start, end) { background: #cbd5e1; } -.date-chip { - height: 40px; - display: inline-flex; - align-items: center; - gap: 8px; - padding: 0 32px 0 12px; - border: 1px solid #cbd5e1; - border-radius: 6px; - background: #fff; - color: var(--text); - font-size: 14px; - font-weight: 400; -} - .month-chip { height: 42px; display: inline-flex; @@ -590,7 +545,7 @@ function formatRangeLabel(start, end) { font-weight: 750; } -.month-chip .pi:first-child { +.month-chip .mdi:first-child { color: #64748b; } @@ -599,32 +554,6 @@ function formatRangeLabel(start, end) { color: #0f9f78; } -.date-chip .pi { - color: #94a3b8; -} - -.top-btn { - height: 40px; - display: inline-flex; - align-items: center; - gap: 8px; - padding: 0 16px; - border: 0; - border-radius: 6px; - font-size: 14px; - font-weight: 650; - transition: background 160ms ease; -} - -.top-btn.primary { - background: var(--primary); - color: #fff; -} - -.top-btn.primary:hover { - background: #0ea672; -} - .icon-btn, .profile-btn { border: 0; @@ -705,7 +634,7 @@ function formatRangeLabel(start, end) { font-size: 12px; } -.profile-btn .pi { +.profile-btn .mdi { color: #64748b; } diff --git a/src/composables/useNavigation.js b/src/composables/useNavigation.js index 74d7528..b38d663 100644 --- a/src/composables/useNavigation.js +++ b/src/composables/useNavigation.js @@ -18,6 +18,14 @@ export const navItems = [ title: '差旅申请/报销', desc: '查看员工差旅报销单据、跟踪进度、发起新申请' }, + { + id: 'approval', + label: '审批中心', + navHint: '待审批单据与批量处理', + icon: icons.approval, + title: '审批中心', + desc: '统一处理待审批单据,聚焦效率、风险与 SLA' + }, { id: 'chat', label: 'AI助手', @@ -28,11 +36,11 @@ export const navItems = [ }, { id: 'policies', - label: '政策规则', - navHint: '制度与校验规则', + label: '知识管理', + navHint: '制度、文档与知识图谱', icon: icons.file, - title: '政策规则中心', - desc: '维护差旅、招待、采购和发票校验规则。' + title: '财务知识管理中心', + desc: '上传制度文档、沉淀财务知识、构建知识图谱,面向员工问答与知识管理' }, { id: 'audit', diff --git a/src/data/icons.js b/src/data/icons.js index d316dbb..3a5824b 100644 --- a/src/data/icons.js +++ b/src/data/icons.js @@ -3,6 +3,7 @@ const iconPath = (content) => `

今日你可以这样问我

@@ -29,11 +29,11 @@

今天我最应该关注哪些问题?

- +
- +

基于当前数据,你优先关注以下 3 项:

    @@ -46,25 +46,25 @@
- +

为你生成行动建议:

    -
  • 紧急处理:处理即将超时的 3 笔单据,避免 SLA 逾期。
  • -
  • 风险关注:审核市场部高风险差旅报销,重点关注差旅与超标费用。
  • -
  • 信息补齐:提醒申请人补齐酒店入住清单,加快审批进度。
  • -
  • 效率优化:当前审批瓶颈在财务审批,建议分配或优先处理。
  • +
  • 紧急处理:处理即将超时的 3 笔单据,避免 SLA 逾期。
  • +
  • 风险关注:审核市场部高风险差旅报销,重点关注差旅与超标费用。
  • +
  • 信息补齐:提醒申请人补齐酒店入住清单,加快审批进度。
  • +
  • 效率优化:当前审批瓶颈在财务审批,建议分配或优先处理。
- +

{{ message.text }}

- +
@@ -79,19 +79,19 @@
{{ draft.length }}/2000
@@ -115,13 +115,13 @@

B. 场景建议

- +
@@ -142,7 +142,7 @@

D. 最近提问 / 常用问题

@@ -176,14 +176,14 @@ const uploadInput = ref(null) const promptPage = ref(0) const prompts = [ - { icon: 'pi pi-chart-line', text: '今天有哪些关键指标异常?' }, - { icon: 'pi pi-file-check', text: '哪些单据最需要优先处理?' }, - { icon: 'pi pi-shield', text: '高风险报销主要集中在哪些部门?' }, - { icon: 'pi pi-clock', text: '本周审批效率相比昨天如何?' }, - { icon: 'pi pi-lightbulb', text: '给我当前报销场景的处理建议' }, - { icon: 'pi pi-building', text: '生成一份运营简报' }, - { icon: 'pi pi-filter', text: '找出即将超时的单据' }, - { icon: 'pi pi-wallet', text: '分析本月预算执行压力' } + { icon: 'mdi mdi-chart-line', text: '今天有哪些关键指标异常?' }, + { icon: 'mdi mdi-file-document-outline-check', text: '哪些单据最需要优先处理?' }, + { icon: 'mdi mdi-shield-outline', text: '高风险报销主要集中在哪些部门?' }, + { icon: 'mdi mdi-clock-outline', text: '本周审批效率相比昨天如何?' }, + { icon: 'mdi mdi-lightbulb-outline', text: '给我当前报销场景的处理建议' }, + { icon: 'mdi mdi-office-building', text: '生成一份运营简报' }, + { icon: 'mdi mdi-filter-outline', text: '找出即将超时的单据' }, + { icon: 'mdi mdi-wallet', text: '分析本月预算执行压力' } ] const visiblePrompts = computed(() => { @@ -192,23 +192,23 @@ const visiblePrompts = computed(() => { }) const focusItems = [ - { icon: 'pi pi-star-fill', tone: 'danger', label: '3 单即将超时', value: '30 分钟内超时' }, - { icon: 'pi pi-exclamation-triangle', tone: 'warning', label: '市场部高风险占比最高', value: '高风险 2 单' }, - { icon: 'pi pi-arrow-right-arrow-left', tone: 'info', label: '重复报销风险 1 笔', value: '待核查' }, - { icon: 'pi pi-check-circle', tone: 'success', label: '1 笔缺失附件', value: '待补充' } + { icon: 'mdi mdi-star', tone: 'danger', label: '3 单即将超时', value: '30 分钟内超时' }, + { icon: 'mdi mdi-alert', tone: 'warning', label: '市场部高风险占比最高', value: '高风险 2 单' }, + { icon: 'mdi mdi-swap-horizontal', tone: 'info', label: '重复报销风险 1 笔', value: '待核查' }, + { icon: 'mdi mdi-check-circle', tone: 'success', label: '1 笔缺失附件', value: '待补充' } ] const suggestions = [ - { icon: 'pi pi-send', text: '优先处理差旅报销(占待审 62%)' }, - { icon: 'pi pi-file-plus', text: '先补齐缺失附件再提交审批' }, - { icon: 'pi pi-car', text: '对超标出租车费用要求补充说明' } + { icon: 'mdi mdi-send', text: '优先处理差旅报销(占待审 62%)' }, + { icon: 'mdi mdi-file-document-outline-plus', text: '先补齐缺失附件再提交审批' }, + { icon: 'mdi mdi-car', text: '对超标出租车费用要求补充说明' } ] const analysisActions = [ - { icon: 'pi pi-wave-pulse', text: '异常原因分析' }, - { icon: 'pi pi-building', text: '部门对比' }, - { icon: 'pi pi-shield', text: '风险趋势' }, - { icon: 'pi pi-filter', text: '审批瓶颈' } + { icon: 'mdi mdi-waveform', text: '异常原因分析' }, + { icon: 'mdi mdi-office-building', text: '部门对比' }, + { icon: 'mdi mdi-shield-outline', text: '风险趋势' }, + { icon: 'mdi mdi-filter-outline', text: '审批瓶颈' } ] const recentQuestions = [ diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index cb93a58..19e874f 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -26,7 +26,7 @@
报销单 - +
@@ -35,7 +35,7 @@
风险预警 - 14 单 + 14 单 较昨日 ↑ 16.7%
@@ -76,13 +76,13 @@
- + 安全登录 · 数据加密传输 · 如需帮助请联系管理员
@@ -147,9 +147,9 @@ const remember = ref(true) const showPassword = ref(false) const features = [ - { title: '智能审单', desc: 'AI 自动识别票据与规则,提升准确率与效率', icon: 'pi pi-file', tone: 'green' }, - { title: '异常预警', desc: '多维风险识别与预警,主动防控风险', icon: 'pi pi-bell', tone: 'red' }, - { title: 'SLA 监控', desc: '实时监控服务水平协议,保障审批及时性', icon: 'pi pi-sync', tone: 'blue' } + { title: '智能审单', desc: 'AI 自动识别票据与规则,提升准确率与效率', icon: 'mdi mdi-file-document-outline', tone: 'green' }, + { title: '异常预警', desc: '多维风险识别与预警,主动防控风险', icon: 'mdi mdi-bell-outline', tone: 'red' }, + { title: 'SLA 监控', desc: '实时监控服务水平协议,保障审批及时性', icon: 'mdi mdi-sync', tone: 'blue' } ] const LogoMark = { @@ -593,7 +593,7 @@ const LogoMark = { min-height: 52px; } -.field > .pi { +.field > .mdi { position: absolute; left: 16px; color: #64748b; @@ -740,7 +740,7 @@ const LogoMark = { font-size: 13px; } -.security-note .pi { +.security-note .mdi { color: #94a3b8; } diff --git a/src/views/OverviewView.vue b/src/views/OverviewView.vue index 578ed9c..3e93bcb 100644 --- a/src/views/OverviewView.vue +++ b/src/views/OverviewView.vue @@ -5,23 +5,19 @@ v-for="metric in kpiMetrics" :key="metric.label" class="kpi-card panel" - :style="{ '--accent': metric.accent }" + :style="{ '--accent': metric.accent, '--delay': `${metric.delay}ms` }" > -
-
- -
-
-

{{ metric.label }}

- {{ metric.displayValue }} -
+
+ + {{ metric.label }}
-
- - + {{ metric.displayValue }} +
+ + {{ metric.changeText }} - {{ metric.delta }} + {{ metric.delta }}
@@ -29,7 +25,7 @@
-

报销申请与审批趋势

+

报销申请与审批趋势

@@ -45,7 +41,7 @@
-

费用结构

+

费用结构

* 百分比为占待处理金额比例

@@ -53,7 +49,7 @@
-

风险异常分布

+

风险异常分布

* 近30天数据

@@ -63,7 +59,7 @@
-

部门报销排行(待处理金额)

+

部门报销排行(待处理金额)

@@ -74,11 +70,16 @@
-

审批瓶颈(平均处理时长)

+

审批瓶颈(平均处理时长)

-
+
{{ item.avatar }}
@@ -93,12 +94,12 @@
- +
-

预算执行率(本月)

+

预算执行率(本月)

- +
@@ -163,18 +164,23 @@ const formatCompact = (value) => { const formatCurrency = (value) => formatCompact(value) -const kpiMetrics = computed(() => metricBlueprints.map((metric) => { +const formatMetricValue = (metric, value) => { + if (metric.key === 'pendingAmount') return formatCurrency(Math.round(value)) + if (metric.key === 'avgSla') return `${value.toFixed(1)} ${metric.unit}` + if (metric.unit === '%') return `${Math.round(value)} ${metric.unit}` + if (metric.unit) return `${Math.round(value)} ${metric.unit}` + return `${Math.round(value)}` +} + +const kpiMetrics = computed(() => metricBlueprints.map((metric, index) => { const rawValue = demoTotals[metric.key] - const displayValue = metric.key === 'pendingAmount' - ? formatCurrency(rawValue) - : metric.unit && !String(rawValue).endsWith(metric.unit) - ? `${rawValue} ${metric.unit}` - : `${rawValue}` + const displayValue = formatMetricValue(metric, rawValue) return { ...metric, displayValue, - changeText: metric.change + changeText: metric.change, + delay: index * 55 } })) @@ -222,86 +228,104 @@ const rankedDepartments = computed(() => { } .kpi-card { - padding: 20px; + position: relative; + padding: 20px 20px 16px; display: flex; flex-direction: column; - gap: 0; + border-left: 3px solid var(--accent); + animation: dashboardItemIn 520ms var(--ease) both; + animation-delay: var(--delay, 0ms); + transition: box-shadow 200ms ease, transform 200ms ease; } -.kpi-top { - flex: 1; +.kpi-card:hover { + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06); + transform: translateY(-1px); +} + +.kpi-head { display: flex; - align-items: flex-start; - justify-content: space-between; + align-items: center; gap: 10px; + margin-bottom: 16px; } -.kpi-top > div { - min-width: 0; - overflow: hidden; +.kpi-icon { + width: 36px; + height: 36px; + display: grid; + place-items: center; + border-radius: 8px; + background: color-mix(in srgb, var(--accent) 10%, white); + color: var(--accent); + font-size: 18px; + flex: 0 0 auto; + animation: iconPop 560ms var(--ease) both; + animation-delay: calc(var(--delay, 0ms) + 100ms); } -.kpi-top p { - margin: 0 0 6px; +.kpi-label { color: #64748b; font-size: 13px; + font-weight: 500; + line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.kpi-top strong { +.kpi-value { display: block; - color: #1e293b; - font-size: clamp(18px, 1.6vw, 22px); - line-height: 1.2; - font-weight: 700; + height: 32px; + line-height: 32px; + color: #0f172a; + font-size: clamp(20px, 1.6vw, 26px); + line-height: 1; + font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; + margin-bottom: 16px; + letter-spacing: 0; } -.kpi-icon { - width: 44px; - height: 44px; - display: grid; - place-items: center; - border-radius: 10px; - background: color-mix(in srgb, var(--accent) 10%, white); - color: var(--accent); - font-size: 20px; - flex: 0 0 auto; -} - -.kpi-bottom { +.kpi-trend { display: flex; align-items: center; justify-content: space-between; gap: 8px; - margin-top: 12px; padding-top: 12px; border-top: 1px solid #f1f5f9; } -.kpi-bottom span { +.kpi-badge { display: inline-flex; align-items: center; - gap: 4px; - font-size: 13px; - font-weight: 600; + gap: 3px; + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 700; + line-height: 1.6; } -.kpi-bottom.up span { +.kpi-badge.up { + background: rgba(239, 68, 68, 0.08); color: #dc2626; } -.kpi-bottom.down span { +.kpi-badge.down { + background: rgba(22, 163, 74, 0.08); color: #16a34a; } -.kpi-bottom small { +.kpi-badge .mdi { + font-size: 13px; +} + +.kpi-delta { color: #94a3b8; font-size: 12px; - text-align: right; + white-space: nowrap; } .content-grid { @@ -313,8 +337,16 @@ const rankedDepartments = computed(() => { .dashboard-card { padding: 20px; transition: box-shadow 200ms ease, transform 200ms ease; + animation: dashboardItemIn 560ms var(--ease) both; } +.top-grid .dashboard-card:nth-child(1) { animation-delay: 80ms; } +.top-grid .dashboard-card:nth-child(2) { animation-delay: 150ms; } +.top-grid .dashboard-card:nth-child(3) { animation-delay: 220ms; } +.bottom-grid .dashboard-card:nth-child(1) { animation-delay: 290ms; } +.bottom-grid .dashboard-card:nth-child(2) { animation-delay: 360ms; } +.bottom-grid .dashboard-card:nth-child(3) { animation-delay: 430ms; } + .dashboard-card:hover { box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06); transform: translateY(-1px); @@ -346,7 +378,7 @@ const rankedDepartments = computed(() => { line-height: 1.35; } -.card-head .pi { +.card-head .mdi { color: #94a3b8; font-size: 12px; vertical-align: 1px; @@ -393,6 +425,8 @@ const rankedDepartments = computed(() => { align-items: center; justify-content: space-between; gap: 12px; + animation: listRowIn 460ms var(--ease) both; + animation-delay: var(--delay, 0ms); } .reviewer { @@ -472,6 +506,51 @@ const rankedDepartments = computed(() => { font-size: 14px; } +@keyframes dashboardItemIn { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes listRowIn { + from { + opacity: 0; + transform: translateX(-10px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes iconPop { + 0% { + opacity: 0; + transform: scale(.82); + } + 70% { + opacity: 1; + transform: scale(1.04); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +@media (prefers-reduced-motion: reduce) { + .kpi-card, + .dashboard-card, + .bottleneck-row { + animation: none; + } +} + @media (max-width: 1320px) { .kpi-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); diff --git a/src/views/PoliciesView.vue b/src/views/PoliciesView.vue index 78f10e6..4429856 100644 --- a/src/views/PoliciesView.vue +++ b/src/views/PoliciesView.vue @@ -6,7 +6,7 @@
-

{{ item.label }}

+

{{ item.label }}

{{ item.value }} {{ item.meta }}
@@ -21,9 +21,9 @@
- + 拖拽文档到此处,或点击上传 支持 PDF / Word / Excel / PPT 文档,单个文件不超过 100MB
@@ -53,7 +53,7 @@ 文件名称 标签 - 上传时间 + 上传时间 版本 状态 上传人 @@ -75,7 +75,7 @@ {{ doc.version }} {{ doc.state }} {{ doc.owner }} - + @@ -83,12 +83,12 @@
共 18 条 - +
- + - +
@@ -100,7 +100,7 @@
-

点击任意行可查看单据详情

+

点击任意行可查看单据详情

+ + + + + + + + + + + + + @@ -71,6 +116,7 @@ + @@ -79,7 +125,7 @@
@@ -89,22 +135,20 @@
- 共 26 条,目前第 1 页 + 共 {{ totalCount }} 条,目前第 {{ currentPage }} 页
- - - - - + + +
- +
单号 出差事由 出差城市 出差时间申请时间 申请金额 当前节点 审批状态{{ row.reason }} {{ row.city }} {{ row.period }}{{ row.applyTime }} {{ row.amount }} {{ row.node }} {{ row.approval }}