style: 全局 UI 主题皮肤重构与样式模块化
引入 Element Plus 主题定制和主题皮肤 composable,将全局 样式拆分为组件级独立 CSS 文件(侧边栏、顶栏、工作台等), 统一色彩变量和间距规范,重构所有视图和组件样式以适配新 主题系统,优化图表和知识图谱组件视觉表现,提取审计和差 旅报销相关子组件。
This commit is contained in:
623
web/src/components/travel/TravelReimbursementInsightPanel.vue
Normal file
623
web/src/components/travel/TravelReimbursementInsightPanel.vue
Normal file
@@ -0,0 +1,623 @@
|
||||
<template>
|
||||
<div
|
||||
class="insight-panel-shell"
|
||||
:class="{ collapsed: !ui.showInsightPanel }"
|
||||
:aria-hidden="(!ui.showInsightPanel).toString()"
|
||||
>
|
||||
<aside class="insight-panel">
|
||||
<div
|
||||
v-if="!ui.isKnowledgeSession"
|
||||
class="insight-head"
|
||||
:class="{ 'review-mode': ui.activeReviewPayload || ui.isReviewFlowDrawer }"
|
||||
>
|
||||
<div>
|
||||
<div v-if="!ui.activeReviewPayload && !ui.isReviewFlowDrawer" class="insight-head-eyebrow">
|
||||
<span class="intent-pill" :class="ui.currentInsight.intent">{{ ui.currentIntentLabel }}</span>
|
||||
</div>
|
||||
<div v-else class="review-insight-title-row">
|
||||
<div class="review-insight-title-copy">
|
||||
<h3>{{ ui.reviewDrawerTitle }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<h3 v-if="!ui.activeReviewPayload && !ui.isReviewFlowDrawer">{{ ui.currentInsight.title }}</h3>
|
||||
<p v-if="!ui.activeReviewPayload && !ui.isReviewFlowDrawer">{{ ui.currentInsight.summary }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="ui.activeReviewPayload || ui.isReviewFlowDrawer" class="review-insight-tools">
|
||||
<button
|
||||
v-if="ui.activeReviewPayload && ui.reviewOverviewDrawerAvailable"
|
||||
type="button"
|
||||
class="review-insight-switch-icon-btn"
|
||||
:class="{
|
||||
available: true,
|
||||
active: ui.isReviewOverviewDrawer
|
||||
}"
|
||||
:disabled="ui.submitting || ui.reviewActionBusy"
|
||||
title="报销识别核对"
|
||||
aria-label="报销识别核对"
|
||||
@click="ui.switchToReviewOverviewDrawer"
|
||||
>
|
||||
<i :class="ui.isReviewOverviewDrawer ? 'mdi mdi-clipboard-check' : 'mdi mdi-clipboard-check-outline'"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="ui.activeReviewPayload && ui.reviewDocumentDrawerAvailable"
|
||||
type="button"
|
||||
class="review-insight-switch-icon-btn"
|
||||
:class="{
|
||||
available: ui.reviewDocumentDrawerAvailable,
|
||||
active: ui.reviewDocumentDrawerAvailable && ui.isReviewDocumentDrawer
|
||||
}"
|
||||
:disabled="ui.submitting || ui.reviewActionBusy"
|
||||
title="单据识别"
|
||||
aria-label="单据识别"
|
||||
@click="ui.toggleReviewDocumentDrawer"
|
||||
>
|
||||
<i :class="ui.reviewDocumentDrawerIcon"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="ui.activeReviewPayload && ui.reviewRiskDrawerAvailable"
|
||||
type="button"
|
||||
class="review-insight-switch-icon-btn risk"
|
||||
:class="{
|
||||
available: ui.reviewRiskDrawerAvailable,
|
||||
active: ui.reviewRiskDrawerAvailable && ui.isReviewRiskDrawer
|
||||
}"
|
||||
:disabled="ui.submitting || ui.reviewActionBusy"
|
||||
title="显示风险"
|
||||
aria-label="显示风险"
|
||||
@click="ui.toggleReviewRiskDrawer"
|
||||
>
|
||||
<i :class="ui.reviewRiskDrawerIcon"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="review-insight-switch-icon-btn flow"
|
||||
:class="{
|
||||
available: ui.reviewFlowDrawerAvailable,
|
||||
active: ui.reviewFlowDrawerAvailable && ui.isReviewFlowDrawer,
|
||||
running: ui.flowOverallStatusTone === 'running'
|
||||
}"
|
||||
:disabled="!ui.reviewFlowDrawerAvailable || ui.submitting || ui.reviewActionBusy"
|
||||
title="调用流程"
|
||||
aria-label="调用流程"
|
||||
@click="ui.toggleReviewFlowDrawer"
|
||||
>
|
||||
<i :class="ui.reviewFlowDrawerIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="confidence-card" v-if="!ui.activeReviewPayload && !ui.isReviewFlowDrawer">
|
||||
<span>{{ ui.currentInsight.metricLabel }}</span>
|
||||
<strong>{{ ui.currentInsight.metricValue }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Transition name="insight-switch" mode="out-in">
|
||||
<div
|
||||
:key="`${ui.activeSessionType}-${ui.currentInsight.intent}-${ui.currentInsight.title}-${ui.reviewDrawerMode}`"
|
||||
class="insight-body"
|
||||
:class="{ 'document-review-body': ui.isReviewDocumentDrawer }"
|
||||
>
|
||||
<template v-if="ui.isKnowledgeSession">
|
||||
<section class="insight-card knowledge-hot-card">
|
||||
<div class="card-head">
|
||||
<h4>热门问题 Top 10</h4>
|
||||
</div>
|
||||
<div class="knowledge-question-list">
|
||||
<button
|
||||
v-for="(item, index) in ui.hotKnowledgeQuestions"
|
||||
:key="item"
|
||||
type="button"
|
||||
class="knowledge-question-btn"
|
||||
:disabled="ui.submitting || ui.reviewActionBusy || ui.deleteSessionBusy || ui.sessionSwitchBusy"
|
||||
@click="ui.askHotKnowledgeQuestion(item)"
|
||||
>
|
||||
<span
|
||||
class="knowledge-question-index"
|
||||
:class="ui.resolveKnowledgeRankTone(index)"
|
||||
>
|
||||
{{ ui.resolveKnowledgeRankLabel(index) }}
|
||||
</span>
|
||||
<span class="knowledge-question-copy">{{ item }}</span>
|
||||
<i class="mdi mdi-arrow-top-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="ui.isReviewFlowDrawer">
|
||||
<section class="review-flow-panel">
|
||||
<div class="review-flow-summary">
|
||||
<span class="flow-status-chip" :class="ui.flowOverallStatusTone">{{ ui.flowOverallStatusText }}</span>
|
||||
<span>总耗时 {{ ui.flowTotalDurationText }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="flow-icon-btn"
|
||||
:disabled="!ui.flowRunId || ui.flowRefreshBusy"
|
||||
title="刷新流程"
|
||||
aria-label="刷新流程"
|
||||
@click="ui.refreshFlowRunDetail"
|
||||
>
|
||||
<i :class="ui.flowRefreshBusy ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-refresh'"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="ui.flowSteps.length" class="review-flow-list">
|
||||
<article
|
||||
v-for="(step, index) in ui.flowSteps"
|
||||
:key="step.key"
|
||||
class="flow-step-item"
|
||||
:class="step.status"
|
||||
>
|
||||
<div class="flow-step-rail">
|
||||
<span>{{ index + 1 }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flow-step-card">
|
||||
<header>
|
||||
<strong>{{ step.title }}</strong>
|
||||
<div class="flow-step-side">
|
||||
<span class="flow-step-status" :class="step.status">{{ ui.resolveFlowStepStatusLabel(step) }}</span>
|
||||
<time>{{ ui.formatFlowStepDuration(step) }}</time>
|
||||
</div>
|
||||
</header>
|
||||
<p class="flow-step-tool">工具:{{ step.tool }}</p>
|
||||
<p class="flow-step-detail">{{ ui.resolveFlowStepDetail(step) }}</p>
|
||||
<p v-if="step.error" class="flow-step-error">{{ step.error }}</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-else class="flow-empty-state compact">
|
||||
<i class="mdi mdi-timeline-question-outline"></i>
|
||||
<strong>暂无识别流程</strong>
|
||||
<p>发起识别后,这里会显示调用步骤和耗时。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="ui.currentInsight.intent === 'agent' && ui.currentInsight.agent">
|
||||
<template v-if="ui.activeReviewPayload">
|
||||
<template v-if="ui.reviewOverviewDrawerAvailable && !ui.isReviewDocumentDrawer && !ui.isReviewRiskDrawer && !ui.isReviewFlowDrawer">
|
||||
<section class="review-side-card review-side-overview-card">
|
||||
<div class="review-side-intent-row">
|
||||
<i class="mdi mdi-account-outline"></i>
|
||||
<span>用户意图:</span>
|
||||
<strong>{{ ui.reviewIntentText }}</strong>
|
||||
</div>
|
||||
<section class="review-side-grid compact">
|
||||
<article
|
||||
v-for="item in ui.reviewFactCards"
|
||||
:key="item.key"
|
||||
class="review-side-metric-card"
|
||||
:class="{
|
||||
editable: item.editor,
|
||||
editing: ui.reviewInlineEditorKey === item.key,
|
||||
invalid: Boolean(ui.reviewInlineErrors[item.key]),
|
||||
wide: item.wide
|
||||
}"
|
||||
@click="ui.openInlineReviewEditor(item.key)"
|
||||
>
|
||||
<span class="review-side-metric-icon">
|
||||
<i :class="item.icon"></i>
|
||||
</span>
|
||||
<div class="review-side-metric-copy">
|
||||
<small>{{ item.label }}</small>
|
||||
<template v-if="ui.reviewInlineEditorKey === item.key && item.editor === 'date'">
|
||||
<input
|
||||
v-model="ui.reviewInlineForm[item.modelKey]"
|
||||
class="review-inline-input"
|
||||
:class="{ invalid: Boolean(ui.reviewInlineErrors[item.key]) }"
|
||||
type="text"
|
||||
:placeholder="`仅支持 ${ui.DATE_INPUT_FORMAT}`"
|
||||
@click.stop
|
||||
@input="ui.clearInlineReviewFieldError(item.key)"
|
||||
@blur="ui.commitInlineReviewEditor"
|
||||
@keydown.enter.prevent="ui.commitInlineReviewEditor"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="ui.reviewInlineEditorKey === item.key && item.editor === 'amount'">
|
||||
<input
|
||||
v-model="ui.reviewInlineForm[item.modelKey]"
|
||||
class="review-inline-input"
|
||||
:class="{ invalid: Boolean(ui.reviewInlineErrors[item.key]) }"
|
||||
type="text"
|
||||
:placeholder="item.placeholder"
|
||||
@click.stop
|
||||
@input="ui.clearInlineReviewFieldError(item.key)"
|
||||
@blur="ui.commitInlineReviewEditor"
|
||||
@keydown.enter.prevent="ui.commitInlineReviewEditor"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="ui.reviewInlineEditorKey === item.key && item.editor === 'text'">
|
||||
<input
|
||||
v-model="ui.reviewInlineForm[item.modelKey]"
|
||||
class="review-inline-input"
|
||||
:class="{ invalid: Boolean(ui.reviewInlineErrors[item.key]) }"
|
||||
type="text"
|
||||
:placeholder="item.placeholder"
|
||||
@click.stop
|
||||
@input="ui.clearInlineReviewFieldError(item.key)"
|
||||
@blur="ui.commitInlineReviewEditor"
|
||||
@keydown.enter.prevent="ui.commitInlineReviewEditor"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="ui.reviewInlineEditorKey === item.key && item.editor === 'textarea'">
|
||||
<textarea
|
||||
v-model="ui.reviewInlineForm[item.modelKey]"
|
||||
class="review-inline-input review-inline-textarea"
|
||||
:class="{ invalid: Boolean(ui.reviewInlineErrors[item.key]) }"
|
||||
:placeholder="item.placeholder"
|
||||
rows="3"
|
||||
@click.stop
|
||||
@input="ui.clearInlineReviewFieldError(item.key)"
|
||||
@blur="ui.commitInlineReviewEditor"
|
||||
@keydown.enter.stop
|
||||
></textarea>
|
||||
</template>
|
||||
<template v-else-if="ui.reviewInlineEditorKey === item.key && item.editor === 'select'">
|
||||
<div class="review-inline-select-list" @click.stop>
|
||||
<button
|
||||
v-for="scene in ui.REVIEW_SCENE_OPTIONS"
|
||||
:key="scene"
|
||||
type="button"
|
||||
class="review-inline-select-option"
|
||||
:class="{ active: ui.reviewInlineForm.scene_label === scene }"
|
||||
@click.stop="ui.selectInlineScene(scene)"
|
||||
>
|
||||
{{ scene }}
|
||||
</button>
|
||||
<input
|
||||
v-if="ui.reviewInlineForm.scene_label === ui.REVIEW_SCENE_OTHER_OPTION"
|
||||
v-model="ui.reviewInlineForm.reason_value"
|
||||
class="review-inline-input review-inline-select-custom"
|
||||
:class="{ invalid: Boolean(ui.reviewInlineErrors[item.key]) }"
|
||||
type="text"
|
||||
placeholder="请输入具体事由"
|
||||
@click.stop
|
||||
@input="ui.clearInlineReviewFieldError(item.key)"
|
||||
@blur="ui.commitInlineReviewEditor"
|
||||
@keydown.enter.prevent="ui.commitInlineReviewEditor"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<strong v-else :title="item.value">{{ item.value }}</strong>
|
||||
<span v-if="ui.reviewInlineErrors[item.key]" class="review-inline-error">
|
||||
{{ ui.reviewInlineErrors[item.key] }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-if="item.key !== 'attachments'" class="review-side-edit-hint">修改</span>
|
||||
<span v-else class="review-side-edit-hint upload">{{ ui.reviewInlinePendingFiles.length ? '已选择' : '上传' }}</span>
|
||||
</article>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="review-side-card">
|
||||
<div class="review-side-head">
|
||||
<strong>报销分类</strong>
|
||||
<span class="review-side-confidence">置信度 {{ ui.reviewPanelConfidence }}</span>
|
||||
</div>
|
||||
<div class="review-side-category-grid">
|
||||
<button
|
||||
v-for="item in ui.reviewCategoryOptions"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="review-side-category-card"
|
||||
:class="{ active: item.active }"
|
||||
@click="ui.selectReviewCategory(item)"
|
||||
>
|
||||
<div class="review-side-category-copy">
|
||||
<strong>{{ item.label }}</strong>
|
||||
<p>{{ item.is_other && ui.reviewSelectedOtherCategory ? ui.reviewSelectedOtherCategory : item.caption }}</p>
|
||||
</div>
|
||||
<i v-if="item.active" class="mdi mdi-check-circle review-side-group-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="ui.reviewOtherCategoryOpen" class="review-other-category-popover">
|
||||
<button
|
||||
v-for="item in ui.reviewOtherCategoryOptions"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="review-other-category-option"
|
||||
:class="{ active: ui.reviewSelectedOtherCategory === item.label }"
|
||||
@click="ui.selectReviewOtherCategory(item)"
|
||||
>
|
||||
{{ item.label }} · {{ item.confidenceLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</template>
|
||||
|
||||
<template v-else-if="ui.isReviewFlowDrawer">
|
||||
<section class="review-flow-panel">
|
||||
<div class="review-flow-summary">
|
||||
<span class="flow-status-chip" :class="ui.flowOverallStatusTone">{{ ui.flowOverallStatusText }}</span>
|
||||
<span>总耗时 {{ ui.flowTotalDurationText }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="flow-icon-btn"
|
||||
:disabled="!ui.flowRunId || ui.flowRefreshBusy"
|
||||
title="刷新流程"
|
||||
aria-label="刷新流程"
|
||||
@click="ui.refreshFlowRunDetail"
|
||||
>
|
||||
<i :class="ui.flowRefreshBusy ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-refresh'"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="ui.flowSteps.length" class="review-flow-list">
|
||||
<article
|
||||
v-for="(step, index) in ui.flowSteps"
|
||||
:key="step.key"
|
||||
class="flow-step-item"
|
||||
:class="step.status"
|
||||
>
|
||||
<div class="flow-step-rail">
|
||||
<span>{{ index + 1 }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flow-step-card">
|
||||
<header>
|
||||
<strong>{{ step.title }}</strong>
|
||||
<div class="flow-step-side">
|
||||
<span class="flow-step-status" :class="step.status">{{ ui.resolveFlowStepStatusLabel(step) }}</span>
|
||||
<time>{{ ui.formatFlowStepDuration(step) }}</time>
|
||||
</div>
|
||||
</header>
|
||||
<p class="flow-step-tool">工具:{{ step.tool }}</p>
|
||||
<p class="flow-step-detail">{{ ui.resolveFlowStepDetail(step) }}</p>
|
||||
<p v-if="step.error" class="flow-step-error">{{ step.error }}</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-else class="flow-empty-state compact">
|
||||
<i class="mdi mdi-timeline-question-outline"></i>
|
||||
<strong>暂无识别流程</strong>
|
||||
<p>发起识别后,这里会显示调用步骤和耗时。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="ui.isReviewDocumentDrawer">
|
||||
<section class="review-side-card review-document-switch-card review-ticket-drawer">
|
||||
<div class="review-side-head review-document-switch-head">
|
||||
<div class="review-side-head-copy">
|
||||
<strong>票据识别结果卡片</strong>
|
||||
<p>逐张查看 OCR 结果,可直接修正后再切回核对滑窗。</p>
|
||||
</div>
|
||||
<div class="review-document-nav">
|
||||
<button
|
||||
type="button"
|
||||
class="review-document-nav-btn"
|
||||
:disabled="ui.activeReviewDocumentIndex === 0"
|
||||
aria-label="上一张票据"
|
||||
@click="ui.goReviewDocument(-1)"
|
||||
>
|
||||
<i class="mdi mdi-chevron-left"></i>
|
||||
</button>
|
||||
<span>{{ ui.activeReviewDocumentIndex + 1 }} / {{ ui.reviewDocumentCount }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="review-document-nav-btn"
|
||||
:disabled="ui.activeReviewDocumentIndex >= ui.reviewDocumentCount - 1"
|
||||
aria-label="下一张票据"
|
||||
@click="ui.goReviewDocument(1)"
|
||||
>
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="ui.activeReviewDocument" class="review-document-stage">
|
||||
<div class="review-document-stage-head">
|
||||
<div class="review-document-stage-copy">
|
||||
<strong :title="ui.activeReviewDocument.filename">{{ ui.activeReviewDocument.filename }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="review-document-meta-chip-row">
|
||||
<span class="review-document-meta-chip">{{ ui.activeReviewDocument.documentTypeLabel }}</span>
|
||||
<span class="review-document-meta-chip">{{ ui.activeReviewDocument.expenseTypeLabel }}</span>
|
||||
<span class="review-document-meta-chip confidence">{{ ui.activeReviewDocument.confidenceLabel }}</span>
|
||||
</div>
|
||||
|
||||
<div class="review-document-scroll">
|
||||
<div
|
||||
class="review-document-preview-card"
|
||||
:class="[
|
||||
ui.activeReviewDocumentPreview?.kind || 'file',
|
||||
{ clickable: ui.canPreviewActiveReviewDocument }
|
||||
]"
|
||||
:role="ui.canPreviewActiveReviewDocument ? 'button' : null"
|
||||
:tabindex="ui.canPreviewActiveReviewDocument ? 0 : null"
|
||||
@click="ui.canPreviewActiveReviewDocument ? ui.openActiveReviewDocumentPreview() : null"
|
||||
@keydown.enter.prevent="ui.canPreviewActiveReviewDocument ? ui.openActiveReviewDocumentPreview() : null"
|
||||
@keydown.space.prevent="ui.canPreviewActiveReviewDocument ? ui.openActiveReviewDocumentPreview() : null"
|
||||
>
|
||||
<img
|
||||
v-if="ui.activeReviewDocumentPreview?.kind === 'image' && ui.activeReviewDocumentPreview?.url"
|
||||
:src="ui.activeReviewDocumentPreview.url"
|
||||
:alt="ui.activeReviewDocument.filename"
|
||||
/>
|
||||
<div v-else-if="ui.activeReviewDocumentPreview?.kind === 'pdf'" class="review-document-preview-placeholder">
|
||||
<i class="mdi mdi-file-pdf-box"></i>
|
||||
<strong>PDF 票据文件</strong>
|
||||
<p>当前文件还没有生成图片预览,可先核对下方识别字段。</p>
|
||||
</div>
|
||||
<div v-else class="review-document-preview-placeholder">
|
||||
<i class="mdi mdi-file-search-outline"></i>
|
||||
<strong>当前无可预览票据</strong>
|
||||
<p>这张票据还没有可用预览,可先核对下方识别字段。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="review-document-edit-field summary">
|
||||
<span>票据摘要</span>
|
||||
<textarea
|
||||
v-model="ui.activeReviewDocument.summary"
|
||||
rows="3"
|
||||
:disabled="ui.submitting || ui.reviewActionBusy"
|
||||
placeholder="可根据票据图片修正 OCR 摘要"
|
||||
></textarea>
|
||||
</label>
|
||||
|
||||
<label class="review-document-edit-field">
|
||||
<span>票据场景</span>
|
||||
<input
|
||||
v-model="ui.activeReviewDocument.scene_label"
|
||||
type="text"
|
||||
:disabled="ui.submitting || ui.reviewActionBusy"
|
||||
placeholder="例如:出租车/网约车票据 / 火车/高铁票"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div v-if="ui.activeReviewDocument.fields.length" class="review-document-edit-grid">
|
||||
<label
|
||||
v-for="field in ui.activeReviewDocument.fields"
|
||||
:key="`${ui.activeReviewDocument.filename}-${field.label}`"
|
||||
class="review-document-edit-field"
|
||||
>
|
||||
<span>{{ field.label }}</span>
|
||||
<input
|
||||
v-model="field.value"
|
||||
type="text"
|
||||
:disabled="ui.submitting || ui.reviewActionBusy"
|
||||
:placeholder="`修正 ${field.label}`"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div v-else class="review-side-empty compact">
|
||||
<span class="review-side-empty-icon">
|
||||
<i class="mdi mdi-text-recognition"></i>
|
||||
</span>
|
||||
<strong>暂无结构化字段</strong>
|
||||
<p>当前只返回了摘要信息,你仍然可以直接修改上面的票据摘要。</p>
|
||||
</div>
|
||||
|
||||
<div v-if="ui.activeReviewDocument.warnings?.length" class="review-document-warning-list">
|
||||
<article
|
||||
v-for="warning in ui.activeReviewDocument.warnings"
|
||||
:key="`${ui.activeReviewDocument.filename}-${warning}`"
|
||||
class="review-document-warning-item"
|
||||
>
|
||||
<i class="mdi mdi-alert-circle-outline"></i>
|
||||
<span>{{ warning }}</span>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="ui.isReviewRiskDrawer">
|
||||
<section class="review-side-card review-side-risk-card">
|
||||
<div class="review-side-head">
|
||||
<div class="review-side-head-copy">
|
||||
<strong>差旅合规提示</strong>
|
||||
<p>结合票据识别结果与差旅规则,逐项查看需要处理的风险点。</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="review-side-risk-summary">{{ ui.reviewRiskSummary }}</p>
|
||||
<div v-if="ui.reviewRiskItems.length" class="review-side-risk-list">
|
||||
<button
|
||||
v-for="item in ui.reviewRiskItems"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="review-side-risk-item"
|
||||
:class="item.level"
|
||||
@click="ui.appendReviewRiskBriefToConversation(item)"
|
||||
>
|
||||
<span class="review-side-risk-icon" :title="item.levelLabel">
|
||||
<i :class="item.icon"></i>
|
||||
</span>
|
||||
<span class="review-side-risk-copy">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<p>{{ item.summary }}</p>
|
||||
</span>
|
||||
<span class="review-side-risk-meta">
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else-if="ui.reviewRiskEmpty" class="review-side-empty">
|
||||
<span class="review-side-empty-icon">
|
||||
<i class="mdi mdi-shield-check-outline"></i>
|
||||
</span>
|
||||
<strong>暂无风险提示</strong>
|
||||
<p>当前没有需要额外处理的结构化风险点。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<button
|
||||
v-if="ui.reviewHasUnsavedChanges"
|
||||
type="button"
|
||||
class="review-side-save-pill"
|
||||
:disabled="ui.reviewActionBusy || ui.submitting"
|
||||
@click="ui.saveInlineReviewChanges"
|
||||
>
|
||||
<i class="mdi mdi-content-save-outline"></i>
|
||||
保存右侧修改
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<section v-if="ui.currentInsight.agent.citations?.length && !ui.currentInsight.agent.queryPayload && !ui.activeReviewPayload" class="insight-card">
|
||||
<div class="card-head">
|
||||
<h4>制度依据</h4>
|
||||
</div>
|
||||
<div class="citation-stack">
|
||||
<article v-for="item in ui.currentInsight.agent.citations" :key="item.code" class="citation-card">
|
||||
<header>
|
||||
<strong>{{ item.title }}</strong>
|
||||
<span>{{ item.version || item.source_type }}</span>
|
||||
</header>
|
||||
<p>{{ item.excerpt || item.code }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<template v-if="!ui.activeReviewPayload">
|
||||
<section class="insight-card primary">
|
||||
<div class="card-head">
|
||||
<h4>识别结果</h4>
|
||||
</div>
|
||||
<div class="note-block">
|
||||
<strong>{{ ui.currentInsight.title }}</strong>
|
||||
<p>{{ ui.currentInsight.summary }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="ui.currentInsight.agent.riskFlags?.length" class="insight-card">
|
||||
<div class="card-head">
|
||||
<h4>风险标签</h4>
|
||||
</div>
|
||||
<div class="capability-chip-row">
|
||||
<span v-for="item in ui.currentInsight.agent.riskFlags" :key="item" class="risk-chip">{{ item }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</Transition>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TravelReimbursementInsightPanel',
|
||||
props: {
|
||||
ui: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/components/travel-reimbursement-insight-panel.css"></style>
|
||||
430
web/src/components/travel/TravelReimbursementMessageItem.vue
Normal file
430
web/src/components/travel/TravelReimbursementMessageItem.vue
Normal 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>
|
||||
62
web/src/components/travel/TravelRequestApprovalDialog.vue
Normal file
62
web/src/components/travel/TravelRequestApprovalDialog.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<ConfirmDialog
|
||||
:open="open"
|
||||
:badge="badge"
|
||||
badge-tone="info"
|
||||
:title="title"
|
||||
:description="description"
|
||||
cancel-text="返回核对"
|
||||
:confirm-text="confirmText"
|
||||
:busy-text="busyText"
|
||||
confirm-tone="primary"
|
||||
confirm-icon="mdi mdi-check-circle-outline"
|
||||
:busy="busy"
|
||||
@close="emit('close')"
|
||||
@confirm="emit('confirm')"
|
||||
>
|
||||
<div class="submit-confirm-summary" aria-label="领导审批通过摘要">
|
||||
<div class="submit-confirm-row">
|
||||
<span>单据编号</span>
|
||||
<strong>{{ documentNo }}</strong>
|
||||
</div>
|
||||
<div class="submit-confirm-row">
|
||||
<span>当前节点</span>
|
||||
<strong>{{ node }}</strong>
|
||||
</div>
|
||||
<div class="submit-confirm-row">
|
||||
<span>{{ summaryLabel }}</span>
|
||||
<strong>{{ nextStage }}</strong>
|
||||
</div>
|
||||
<div class="submit-confirm-row">
|
||||
<span>{{ opinionTitle }}</span>
|
||||
<strong>{{ normalizedOpinion }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
import ConfirmDialog from '../shared/ConfirmDialog.vue'
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, required: true },
|
||||
badge: { type: String, required: true },
|
||||
title: { type: String, required: true },
|
||||
description: { type: String, required: true },
|
||||
confirmText: { type: String, required: true },
|
||||
busyText: { type: String, required: true },
|
||||
busy: { type: Boolean, required: true },
|
||||
documentNo: { type: [String, Number], required: true },
|
||||
node: { type: String, default: '' },
|
||||
summaryLabel: { type: String, required: true },
|
||||
nextStage: { type: String, required: true },
|
||||
opinionTitle: { type: String, required: true },
|
||||
opinion: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'confirm'])
|
||||
|
||||
const normalizedOpinion = computed(() => props.opinion.trim() || '未填写')
|
||||
</script>
|
||||
31
web/src/components/travel/TravelRequestDeleteDialog.vue
Normal file
31
web/src/components/travel/TravelRequestDeleteDialog.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<ConfirmDialog
|
||||
:open="open"
|
||||
:badge="badge"
|
||||
badge-tone="danger"
|
||||
:title="title"
|
||||
:description="description"
|
||||
cancel-text="取消"
|
||||
confirm-text="确认删除"
|
||||
busy-text="删除中..."
|
||||
confirm-tone="danger"
|
||||
confirm-icon="mdi mdi-trash-can-outline"
|
||||
:busy="busy"
|
||||
@close="emit('close')"
|
||||
@confirm="emit('confirm')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ConfirmDialog from '../shared/ConfirmDialog.vue'
|
||||
|
||||
defineProps({
|
||||
open: { type: Boolean, required: true },
|
||||
badge: { type: String, required: true },
|
||||
title: { type: String, required: true },
|
||||
description: { type: String, required: true },
|
||||
busy: { type: Boolean, required: true }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'confirm'])
|
||||
</script>
|
||||
23
web/src/components/travel/TravelRequestReturnDialog.vue
Normal file
23
web/src/components/travel/TravelRequestReturnDialog.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<ReturnReasonDialog
|
||||
:open="open"
|
||||
:title="title"
|
||||
:description="description"
|
||||
:busy="busy"
|
||||
@close="emit('close')"
|
||||
@confirm="emit('confirm', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ReturnReasonDialog from '../shared/ReturnReasonDialog.vue'
|
||||
|
||||
defineProps({
|
||||
open: { type: Boolean, required: true },
|
||||
title: { type: String, required: true },
|
||||
description: { type: String, required: true },
|
||||
busy: { type: Boolean, required: true }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'confirm'])
|
||||
</script>
|
||||
Reference in New Issue
Block a user