feat: 新增预算助手报告组件并优化报销交互细节

新增预算助手报告视图模型和组件,优化报销洞察面板和消息项
样式细节,完善预算中心页面布局和文档中心视图,增强报销创
建会话管理和提交编排器,调整 Vite 构建配置,补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-27 12:27:17 +08:00
parent b1a9c8a194
commit 7d32eae74e
23 changed files with 1197 additions and 464 deletions

View File

@@ -0,0 +1,452 @@
<template>
<section class="budget-assistant-report" aria-label="预算编制分析报告">
<header class="budget-report-head">
<div>
<span class="budget-report-kicker">{{ report.basePeriod }} {{ report.targetPeriod }}</span>
<h3>{{ report.title }}</h3>
<p>{{ report.departmentName }} · {{ report.subtitle }}</p>
</div>
<span class="budget-report-badge">
<i class="mdi mdi-chart-donut"></i>
分析报告
</span>
</header>
<div class="budget-report-summary-grid">
<article
v-for="(metric, index) in summaryCards"
:key="metric.label"
class="budget-report-summary-card"
:style="{ '--delay': `${index * 55}ms`, '--accent': metric.color }"
>
<span>{{ metric.label }}</span>
<strong>{{ metric.value }}</strong>
<small>{{ metric.hint }}</small>
</article>
</div>
<section class="budget-report-main">
<article class="budget-report-chart-panel">
<div class="budget-report-section-head">
<strong>上季度费用结构</strong>
<span>{{ report.centerLabel }}</span>
</div>
<DonutChart
:items="report.items"
:center-value="report.centerValue"
:center-label="report.centerLabel"
/>
</article>
<article class="budget-report-macro-panel">
<div class="budget-report-section-head">
<strong>总体判断</strong>
<span>宏观分析</span>
</div>
<ol class="budget-report-insight-list">
<li v-for="item in report.macroInsights" :key="item">{{ item }}</li>
</ol>
</article>
</section>
<section class="budget-report-detail-panel">
<div class="budget-report-section-head">
<strong>费用类型拆解</strong>
<span>用于编制下一季度预算</span>
</div>
<div class="budget-report-expense-list">
<article
v-for="(item, index) in report.items"
:key="item.key"
class="budget-report-expense-card"
:style="{ '--accent': item.color, '--delay': `${index * 70}ms` }"
>
<header>
<span class="budget-report-dot"></span>
<strong>{{ item.name }}</strong>
<em :class="item.trendTone">{{ item.trend }}</em>
</header>
<div class="budget-report-expense-stats">
<span>开销 {{ item.amountDisplay }}</span>
<span>占比 {{ item.share }}</span>
<span>建议 {{ item.recommendedDisplay }}</span>
</div>
<p>{{ item.risk }}</p>
<ul>
<li v-for="driver in item.drivers" :key="driver">{{ driver }}</li>
</ul>
<footer>{{ item.suggestion }}</footer>
</article>
</div>
</section>
<section class="budget-report-action-panel">
<div>
<strong>编制建议</strong>
<p v-for="item in report.recommendations" :key="item">{{ item }}</p>
</div>
<span>{{ report.generatedAt }}</span>
</section>
</section>
</template>
<script setup>
import { computed } from 'vue'
import DonutChart from '../charts/DonutChart.vue'
const props = defineProps({
report: {
type: Object,
required: true
}
})
const summaryCards = computed(() => [
{
label: '上季度预算',
value: props.report.summary?.totalBudget || '—',
hint: '作为编制基准',
color: 'var(--theme-primary)'
},
{
label: '上季度开销',
value: props.report.summary?.totalSpend || '—',
hint: '按四类预算口径汇总',
color: 'var(--chart-blue)'
},
{
label: '预算使用率',
value: props.report.summary?.usageRate || '—',
hint: '未触达风险线',
color: 'var(--chart-amber)'
},
{
label: '建议编制额',
value: props.report.summary?.recommendedTotal || '—',
hint: '含业务增长预留',
color: 'var(--chart-purple)'
}
])
</script>
<style scoped>
.budget-assistant-report {
display: grid;
gap: 12px;
margin-top: 12px;
color: #1f2937;
animation: budgetReportIn 420ms var(--ease, ease) both;
}
.budget-report-head,
.budget-report-main,
.budget-report-detail-panel,
.budget-report-action-panel,
.budget-report-summary-card {
border: 1px solid #dbe4ee;
border-radius: 8px;
background: #fff;
box-shadow: 0 8px 20px rgba(15, 23, 42, .05);
}
.budget-report-head {
padding: 14px 16px;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
border-left: 3px solid var(--theme-primary);
}
.budget-report-kicker,
.budget-report-head p,
.budget-report-section-head span,
.budget-report-action-panel span {
color: #64748b;
font-size: 12px;
font-weight: 700;
}
.budget-report-head h3 {
margin: 4px 0 4px;
color: #0f172a;
font-size: 17px;
font-weight: 850;
line-height: 1.3;
}
.budget-report-head p,
.budget-report-action-panel p {
margin: 0;
}
.budget-report-badge {
min-height: 28px;
display: inline-flex;
align-items: center;
gap: 5px;
padding: 0 10px;
border-radius: 999px;
background: var(--theme-primary-soft);
color: var(--theme-primary-active);
font-size: 12px;
font-weight: 800;
white-space: nowrap;
}
.budget-report-summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.budget-report-summary-card {
min-height: 86px;
padding: 10px 12px;
border-left: 3px solid var(--accent);
display: grid;
align-content: center;
gap: 4px;
animation: budgetReportItemIn 460ms var(--ease, ease) both;
animation-delay: var(--delay, 0ms);
}
.budget-report-summary-card span,
.budget-report-summary-card small {
color: #64748b;
font-size: 11px;
font-weight: 650;
}
.budget-report-summary-card strong {
color: #0f172a;
font-size: 18px;
font-weight: 850;
line-height: 1.1;
}
.budget-report-main {
display: grid;
grid-template-columns: minmax(260px, .9fr) minmax(0, 1.1fr);
gap: 14px;
padding: 14px;
}
.budget-report-chart-panel,
.budget-report-macro-panel {
min-width: 0;
}
.budget-report-section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.budget-report-section-head strong,
.budget-report-action-panel strong {
color: #111827;
font-size: 14px;
font-weight: 850;
}
.budget-report-chart-panel :deep(.donut-chart) {
min-height: 224px;
}
.budget-report-chart-panel :deep(.donut-body) {
height: 138px;
margin-top: 8px;
}
.budget-report-insight-list {
display: grid;
gap: 9px;
margin: 0;
padding-left: 20px;
}
.budget-report-insight-list li {
color: #334155;
font-size: 13px;
font-weight: 650;
line-height: 1.55;
}
.budget-report-detail-panel {
padding: 14px;
}
.budget-report-expense-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.budget-report-expense-card {
min-width: 0;
padding: 12px;
border: 1px solid #e2e8f0;
border-left: 3px solid var(--accent);
border-radius: 8px;
background: #fbfdff;
animation: budgetReportItemIn 460ms var(--ease, ease) both;
animation-delay: var(--delay, 0ms);
}
.budget-report-expense-card header,
.budget-report-expense-stats {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.budget-report-expense-card header {
margin-bottom: 8px;
}
.budget-report-dot {
width: 9px;
height: 9px;
border-radius: 3px;
background: var(--accent);
flex: 0 0 auto;
}
.budget-report-expense-card header strong {
color: #0f172a;
font-size: 14px;
font-weight: 850;
}
.budget-report-expense-card header em {
margin-left: auto;
padding: 1px 7px;
border-radius: 999px;
font-size: 11px;
font-style: normal;
font-weight: 850;
}
.budget-report-expense-card header em.risk {
background: rgba(127, 29, 29, .09);
color: #7f1d1d;
}
.budget-report-expense-card header em.warn {
background: rgba(245, 158, 11, .14);
color: #b45309;
}
.budget-report-expense-card header em.stable {
background: rgba(22, 163, 74, .1);
color: #15803d;
}
.budget-report-expense-stats {
flex-wrap: wrap;
margin-bottom: 8px;
}
.budget-report-expense-stats span {
min-height: 22px;
display: inline-flex;
align-items: center;
padding: 0 7px;
border-radius: 6px;
background: #f1f5f9;
color: #475569;
font-size: 11px;
font-weight: 750;
}
.budget-report-expense-card p,
.budget-report-expense-card li,
.budget-report-expense-card footer,
.budget-report-action-panel p {
color: #334155;
font-size: 12px;
font-weight: 620;
line-height: 1.55;
}
.budget-report-expense-card p {
margin: 0 0 8px;
}
.budget-report-expense-card ul {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 0 0 8px;
padding: 0;
list-style: none;
}
.budget-report-expense-card li {
padding: 2px 7px;
border-radius: 999px;
background: #fff;
border: 1px solid #e2e8f0;
}
.budget-report-expense-card footer {
padding-top: 8px;
border-top: 1px solid #e2e8f0;
}
.budget-report-action-panel {
padding: 14px 16px;
display: flex;
justify-content: space-between;
gap: 14px;
border-left: 3px solid var(--chart-amber);
}
.budget-report-action-panel div {
display: grid;
gap: 6px;
}
@keyframes budgetReportIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes budgetReportItemIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 900px) {
.budget-report-summary-grid,
.budget-report-main,
.budget-report-expense-list {
grid-template-columns: 1fr;
}
}
@media (prefers-reduced-motion: reduce) {
.budget-assistant-report,
.budget-report-summary-card,
.budget-report-expense-card {
animation: none;
}
}
</style>

View File

@@ -147,7 +147,7 @@
<TransitionGroup
v-if="(ui.visibleFlowSteps || ui.flowSteps).length"
name="flow-step-reveal"
name="el-fade-in-linear"
tag="div"
class="review-flow-list"
>
@@ -356,7 +356,7 @@
<TransitionGroup
v-if="(ui.visibleFlowSteps || ui.flowSteps).length"
name="flow-step-reveal"
name="el-fade-in-linear"
tag="div"
class="review-flow-list"
>
@@ -630,7 +630,7 @@ export default {
methods: {
resolveFlowStepStyle(index) {
return {
transitionDelay: `${Math.min(Number(index) || 0, 5) * 70}ms`
transitionDelay: `${500 + Math.min(Number(index) || 0, 5) * 80}ms`
}
}
}

View File

@@ -35,6 +35,11 @@
@click="ui.handleAssistantMarkdownClick($event, message)"
></div>
<BudgetAssistantReport
v-if="message.role === 'assistant' && message.budgetReport"
:report="message.budgetReport"
/>
<div
v-if="message.role === 'assistant' && message.applicationPreview"
class="application-preview-table"
@@ -427,11 +432,13 @@
</template>
<script>
import BudgetAssistantReport from './BudgetAssistantReport.vue'
import EnterpriseSelect from '../shared/EnterpriseSelect.vue'
export default {
name: 'TravelReimbursementMessageItem',
components: {
BudgetAssistantReport,
EnterpriseSelect
},
props: {