style: 全局 UI 主题皮肤重构与样式模块化

引入 Element Plus 主题定制和主题皮肤 composable,将全局
样式拆分为组件级独立 CSS 文件(侧边栏、顶栏、工作台等),
统一色彩变量和间距规范,重构所有视图和组件样式以适配新
主题系统,优化图表和知识图谱组件视觉表现,提取审计和差
旅报销相关子组件。
This commit is contained in:
caoxiaozhu
2026-05-27 09:17:57 +08:00
parent df49103f23
commit 2dcc72102d
112 changed files with 10983 additions and 8996 deletions

View File

@@ -0,0 +1,430 @@
<template>
<article
class="message-row"
:class="message.role"
>
<span class="message-avatar">
<img
:src="message.role === 'assistant' ? ui.aiAvatar : ui.userAvatar"
:alt="message.role === 'assistant' ? '财务助手头像' : '用户头像'"
/>
</span>
<div class="message-bubble" :class="ui.buildMessageBubbleClass(message)">
<header class="message-meta">
<strong>{{ message.role === 'assistant' ? (message.assistantName || ui.ASSISTANT_DISPLAY_NAME) : '我' }}</strong>
<time>{{ message.time }}</time>
</header>
<div
v-if="message.text && message.role === 'assistant' && message.reviewPayload && ui.buildReviewMainMessageText(message)"
class="review-summary message-answer-content message-answer-markdown"
v-html="ui.renderMarkdown(ui.buildReviewMainMessageText(message))"
@click="ui.handleAssistantMarkdownClick($event, message)"
></div>
<div
v-else-if="message.text && message.role !== 'assistant'"
class="message-answer-content message-answer-markdown message-rich-text"
v-html="ui.renderMarkdown(message.text)"
></div>
<div
v-else-if="message.text && message.role === 'assistant'"
class="message-answer-content message-answer-markdown"
v-html="ui.renderMarkdown(message.text)"
@click="ui.handleAssistantMarkdownClick($event, message)"
></div>
<div
v-if="message.role === 'assistant' && message.applicationPreview"
class="application-preview-table"
role="table"
aria-label="申请信息核对表"
>
<div class="application-preview-row head" role="row">
<span role="columnheader">字段</span>
<span role="columnheader">内容</span>
</div>
<div
v-for="row in ui.resolveApplicationPreviewRows(message)"
:key="`${message.id}-${row.key}`"
class="application-preview-row"
:class="{
missing: row.missing,
editable: row.editable && !ui.submitting && !ui.reviewActionBusy && !ui.sessionSwitchBusy,
highlight: row.highlight
}"
role="row"
:tabindex="row.editable && !ui.submitting && !ui.reviewActionBusy && !ui.sessionSwitchBusy ? 0 : -1"
:aria-label="row.editable ? `编辑${row.label}` : row.label"
@click="row.editable && !ui.isApplicationPreviewEditing(message, row.key) && !ui.submitting && !ui.reviewActionBusy && !ui.sessionSwitchBusy && ui.openApplicationPreviewEditor(message, row.key, row.value)"
@keydown.enter.prevent="row.editable && !ui.isApplicationPreviewEditing(message, row.key) && !ui.submitting && !ui.reviewActionBusy && !ui.sessionSwitchBusy && ui.openApplicationPreviewEditor(message, row.key, row.value)"
@keydown.space.prevent="row.editable && !ui.isApplicationPreviewEditing(message, row.key) && !ui.submitting && !ui.reviewActionBusy && !ui.sessionSwitchBusy && ui.openApplicationPreviewEditor(message, row.key, row.value)"
>
<span class="application-preview-label" role="cell">{{ row.label }}</span>
<span class="application-preview-value" role="cell">
<input
v-if="ui.isApplicationPreviewEditing(message, row.key) && ui.resolveApplicationPreviewEditorControl(row.key) !== 'select'"
v-model="ui.applicationPreviewEditor.draftValue"
class="application-preview-input"
type="text"
autofocus
@click.stop
@keydown.stop="ui.handleApplicationPreviewEditorKeydown($event, message)"
@blur="ui.commitApplicationPreviewEditor(message)"
/>
<EnterpriseSelect
v-else-if="ui.isApplicationPreviewEditing(message, row.key)"
v-model="ui.applicationPreviewEditor.draftValue"
class="application-preview-input application-preview-select"
:options="ui.resolveApplicationPreviewEditorOptions(row.key)"
clearable
:teleported="false"
autofocus
@click.stop
@change="ui.commitApplicationPreviewEditor(message)"
@keydown.stop="ui.handleApplicationPreviewEditorKeydown($event, message)"
@blur="ui.commitApplicationPreviewEditor(message)"
/>
<template v-else>
<span class="application-preview-text">{{ row.value }}</span>
<button
v-if="row.editable"
type="button"
class="application-preview-edit-btn"
title="修改内容"
aria-label="修改内容"
:disabled="ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
@click.stop="ui.openApplicationPreviewEditor(message, row.key, row.value)"
>
<i class="mdi mdi-pencil-outline"></i>
</button>
</template>
</span>
</div>
</div>
<div
v-if="message.role === 'assistant' && message.applicationPreview && ui.buildApplicationPreviewFooterText(message)"
class="application-preview-footer message-answer-content message-answer-markdown"
v-html="ui.renderMarkdown(ui.buildApplicationPreviewFooterText(message))"
@click="ui.handleAssistantMarkdownClick($event, message)"
></div>
<div
v-if="message.role === 'assistant' && message.welcomeQuickActions?.length"
class="welcome-quick-actions"
>
<p class="welcome-quick-actions-title">您可以对我进行以下操作</p>
<div class="welcome-quick-action-grid">
<button
v-for="action in message.welcomeQuickActions"
:key="`${message.id}-${action.label}`"
type="button"
class="welcome-quick-action-btn"
:disabled="ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
@click="ui.runWelcomeQuickAction(action)"
>
<i :class="action.icon"></i>
<span>{{ action.label }}</span>
</button>
</div>
</div>
<div v-if="message.role === 'assistant' && !message.reviewPayload && !message.queryPayload && message.meta?.length" class="message-meta-row">
<span
v-for="item in message.meta"
:key="item"
class="message-meta-chip"
:class="message.metaTone"
>
{{ item }}
</span>
</div>
<div
v-if="message.role === 'assistant' && !message.reviewPayload && !message.queryPayload && message.suggestedActions?.length"
class="message-suggested-actions"
>
<button
v-for="action in message.suggestedActions"
:key="`${message.id}-${action.action_type}-${action.label}`"
type="button"
class="message-suggested-action-btn"
:class="{
selected: ui.isSuggestedActionSelected(message, action),
locked: message.suggestedActionsLocked
}"
:disabled="message.suggestedActionsLocked || ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
@click="ui.handleSuggestedAction(message, action)"
>
<span class="message-suggested-action-icon" aria-hidden="true">
<i :class="action.icon || 'mdi mdi-shape-outline'"></i>
</span>
<span class="message-suggested-action-copy">
<span class="message-suggested-action-title">{{ action.label }}</span>
<small v-if="action.description">{{ action.description }}</small>
</span>
<i class="message-suggested-action-arrow mdi mdi-arrow-right" aria-hidden="true"></i>
</button>
</div>
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.riskFlags?.length" class="message-detail-block">
<strong>风险标签</strong>
<div class="message-detail-chip-row">
<span v-for="item in message.riskFlags" :key="item" class="message-risk-chip">{{ item }}</span>
</div>
</div>
<details
v-if="message.role === 'assistant' && !message.reviewPayload && !message.queryPayload && message.citations?.length"
class="message-detail-block message-citation-disclosure"
>
<summary>
<strong>引用依据</strong>
<span>{{ message.citations.length }} </span>
<i class="mdi mdi-chevron-down"></i>
</summary>
<div class="message-citation-list">
<article v-for="item in message.citations" :key="`${message.id}-${item.code}`" class="message-citation-card">
<header>
<span>{{ item.title }}</span>
<small>{{ item.version || item.source_type }}</small>
</header>
<p>{{ item.excerpt || item.code }}</p>
</article>
</div>
</details>
<div
v-if="message.role === 'assistant' && !message.reviewPayload && message.queryPayload?.resultType === 'expense_claim_list'"
class="message-detail-block expense-query-block"
>
<strong>
{{ message.queryPayload.title || (message.queryPayload.selectionMode === 'draft_association' ? '选择关联草稿' : '最近 5 条筛选结果') }}
</strong>
<p v-if="ui.buildExpenseQueryWindowLabel(message.queryPayload)" class="expense-query-window-label">
{{ ui.buildExpenseQueryWindowLabel(message.queryPayload) }}
</p>
<div v-if="message.queryPayload.statusGroups?.length" class="expense-query-summary-row">
<span
v-for="item in message.queryPayload.statusGroups"
:key="`${message.id}-${item.key}`"
class="expense-query-summary-chip"
:class="item.key"
>
{{ item.label }} {{ item.count }}
</span>
</div>
<div v-if="message.queryPayload.records?.length" class="expense-query-record-list compact">
<button
v-for="record in ui.getExpenseQueryVisibleRecords(message.queryPayload)"
:key="`${message.id}-${record.claimId}`"
type="button"
class="expense-query-record-card"
:class="{
selectable: message.queryPayload.selectionMode === 'draft_association',
selected: message.selectedQueryRecordId === record.claimId || message.queryPayload.selectedClaimId === record.claimId,
locked: message.querySelectionLocked || message.queryPayload.selectionLocked
}"
:disabled="message.queryPayload.selectionMode === 'draft_association' && (message.querySelectionLocked || message.queryPayload.selectionLocked)"
@click="ui.handleExpenseQueryRecordClick(message, record)"
>
<div class="expense-query-record-main">
<div class="expense-query-record-top">
<strong>{{ record.claimNo }}</strong>
<span class="expense-query-record-status" :class="record.statusGroup || 'other'">
{{ record.statusLabel }}
</span>
</div>
<p>{{ record.summary }}</p>
<div class="expense-query-record-meta">
<span>{{ record.expenseTypeLabel }}</span>
<span>{{ record.dateDisplay }}</span>
<span>{{ record.amountDisplay }}</span>
</div>
<div v-if="record.riskItems?.length" class="expense-query-risk-row">
<button
v-for="risk in record.riskItems"
:key="`${message.id}-${record.claimId}-${risk.key}`"
type="button"
class="expense-query-risk-chip"
:class="risk.level"
@click.stop="ui.appendExpenseQueryRiskToConversation(record, risk)"
>
<span>{{ record.claimNo }}</span>
<strong>{{ risk.levelLabel }}</strong>
<em>{{ risk.title }}</em>
</button>
</div>
</div>
<i class="mdi mdi-chevron-right"></i>
</button>
<div
v-if="ui.getExpenseQueryTotalPages(message.queryPayload) > 1"
class="expense-query-pager"
>
<button
type="button"
class="expense-query-pager-btn"
:disabled="ui.getExpenseQueryActivePage(message.queryPayload) === 1"
aria-label="上一页"
@click="ui.shiftExpenseQueryPage(message, -1)"
>
<i class="mdi mdi-chevron-left"></i>
</button>
<div class="expense-query-pager-dots" aria-label="单据分页">
<button
v-for="page in ui.getExpenseQueryTotalPages(message.queryPayload)"
:key="`${message.id}-query-page-${page}`"
type="button"
class="expense-query-pager-dot"
:class="{ active: ui.getExpenseQueryActivePage(message.queryPayload) === page }"
:aria-label="` ${page} `"
@click="ui.setExpenseQueryPage(message, page)"
></button>
</div>
<button
type="button"
class="expense-query-pager-btn"
:disabled="ui.getExpenseQueryActivePage(message.queryPayload) === ui.getExpenseQueryTotalPages(message.queryPayload)"
aria-label="下一页"
@click="ui.shiftExpenseQueryPage(message, 1)"
>
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
</div>
<div v-else class="expense-query-empty">
<i class="mdi mdi-file-search-outline"></i>
<span>{{ message.queryPayload.emptyText || '当前没有可直接展开的近期待办单据。' }}</span>
</div>
<p
v-if="ui.buildExpenseQueryHint(message.queryPayload)"
class="expense-query-hint message-answer-markdown"
v-html="ui.renderMarkdown(ui.buildExpenseQueryHint(message.queryPayload))"
@click="ui.handleAssistantMarkdownClick($event, message)"
>
</p>
</div>
<div v-if="message.role === 'assistant' && message.reviewPayload" class="message-detail-block review-message-block">
<div class="review-plain-followup">
<template
v-for="followup in [ui.buildReviewPlainFollowupForMessage(message)]"
:key="`${message.id}-review-followup`"
>
<h3
class="review-plain-lead"
:class="{ danger: followup.tone === 'danger' }"
>
{{ followup.lead }}
</h3>
<p v-if="followup.summary" class="review-plain-summary">
{{ followup.summary }}
</p>
<ul v-if="followup.items.length" class="review-plain-list">
<li
v-for="item in followup.items"
:key="`${message.id}-${item.key}`"
>
<span class="review-plain-label">{{ item.label }}</span>
<span>{{ item.text }}</span>
</li>
</ul>
<p
v-for="line in followup.notes"
:key="`${message.id}-note-${line}`"
class="review-plain-note"
>
{{ line }}
</p>
<p v-if="ui.canUseInlineSaveDraft(message)" class="review-inline-save-copy">
请核查上面的关键信息您也可以暂时不处理上述的这些内容我可以帮你先保存为
<button
type="button"
class="review-inline-draft-link"
:disabled="ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
@click="ui.handleInlineSaveDraft(message)"
>
草稿
</button>
</p>
</template>
<div
v-if="ui.buildReviewNextStepRichCopyForMessage(message)"
class="review-next-step-rich-copy message-answer-markdown"
v-html="ui.renderMarkdown(ui.buildReviewNextStepRichCopyForMessage(message))"
@click="ui.handleAssistantMarkdownClick($event, message)"
></div>
<div
v-if="ui.resolveReviewFooterActions(message.reviewPayload).length"
class="review-footer-actions"
>
<div class="review-footer-btn-row">
<button
v-for="action in ui.resolveReviewFooterActions(message.reviewPayload)"
:key="`${message.id}-${action.action_type}`"
type="button"
:class="['review-footer-btn', action.emphasis === 'primary' ? 'primary' : '']"
:disabled="ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
@click="ui.handleReviewAction(message, action)"
>
{{ action.label || ui.buildReviewPrimaryButtonLabel(message.reviewPayload, message.draftPayload) }}
</button>
</div>
</div>
</div>
</div>
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.draftPayload" class="draft-preview">
<header>
<strong>{{ message.draftPayload.title }}</strong>
<span>待人工确认</span>
</header>
<pre>{{ message.draftPayload.body }}</pre>
</div>
<div v-if="message.attachments?.length" class="message-files">
<span v-for="file in message.attachments" :key="file" class="file-chip">
<i class="mdi mdi-paperclip"></i>
{{ file }}
</span>
</div>
</div>
</article>
</template>
<script>
import EnterpriseSelect from '../shared/EnterpriseSelect.vue'
export default {
name: 'TravelReimbursementMessageItem',
components: {
EnterpriseSelect
},
props: {
message: {
type: Object,
required: true
},
ui: {
type: Object,
required: true
}
}
}
</script>
<style scoped src="../../assets/styles/components/travel-reimbursement-message-item.css"></style>