feat: 新增预算助手报告组件并优化报销交互细节
新增预算助手报告视图模型和组件,优化报销洞察面板和消息项 样式细节,完善预算中心页面布局和文档中心视图,增强报销创 建会话管理和提交编排器,调整 Vite 构建配置,补充单元测试。
This commit is contained in:
452
web/src/components/travel/BudgetAssistantReport.vue
Normal file
452
web/src/components/travel/BudgetAssistantReport.vue
Normal 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>
|
||||
@@ -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`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user