feat: 优化差旅报销预审流程与个人工作台 UI 体系
- 完善 user_agent_application 申请差旅报销预审槽位与消息组装 - 增强预算助理报告与风险建议卡片交互 - 重构登录页视觉样式与移动端响应式适配 - 优化个人工作台、文档中心、政策中心、员工管理等页面布局 - 拆分 travelRequestDetailPreReviewModel 为 advice/submit 模型 - 补充报销草稿、风险复核、Item Sync 与模板执行器测试覆盖
This commit is contained in:
@@ -194,7 +194,7 @@
|
||||
:class="{ 'is-disabled': skill.usesJsonRiskRule && skill.statusValue === 'generating' }"
|
||||
@click="emit('open-asset-detail', skill)"
|
||||
>
|
||||
<td>
|
||||
<td :data-label="tableColumns.name">
|
||||
<div class="skill-name-cell">
|
||||
<span class="skill-avatar" :class="skill.badgeTone">{{ skill.short }}</span>
|
||||
<div>
|
||||
@@ -203,8 +203,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ skill.category }}</td>
|
||||
<td>
|
||||
<td :data-label="tableColumns.category">{{ skill.category }}</td>
|
||||
<td :data-label="tableColumns.owner">
|
||||
<span
|
||||
v-if="skill.usesJsonRiskRule"
|
||||
class="json-risk-meta-badge"
|
||||
@@ -214,20 +214,20 @@
|
||||
</span>
|
||||
<template v-else>{{ skill.owner }}</template>
|
||||
</td>
|
||||
<td><span class="scope-pill">{{ skill.scope }}</span></td>
|
||||
<td v-if="showRuntimeColumn">{{ skill.model }}</td>
|
||||
<td v-if="showVersionColumn">{{ skill.versionDisplay || skill.version }}</td>
|
||||
<td v-if="showStatusColumn">
|
||||
<td :data-label="tableColumns.scope"><span class="scope-pill">{{ skill.scope }}</span></td>
|
||||
<td v-if="showRuntimeColumn" :data-label="tableColumns.runtime">{{ skill.model }}</td>
|
||||
<td v-if="showVersionColumn" :data-label="tableColumns.version">{{ skill.versionDisplay || skill.version }}</td>
|
||||
<td v-if="showStatusColumn" :data-label="tableColumns.status || '状态'">
|
||||
<span class="status-pill" :class="skill.statusTone">{{ skill.status }}</span>
|
||||
</td>
|
||||
<td v-if="showMetricColumn">{{ skill.hitRate }}</td>
|
||||
<td v-if="showOnlineColumn">
|
||||
<td v-if="showMetricColumn" :data-label="tableColumns.metric">{{ skill.hitRate }}</td>
|
||||
<td v-if="showOnlineColumn" data-label="是否上线">
|
||||
<span class="status-pill" :class="skill.isOnlineTone">{{ skill.isOnlineLabel }}</span>
|
||||
</td>
|
||||
<td v-if="showEnabledColumn">
|
||||
<td v-if="showEnabledColumn" data-label="是否启用">
|
||||
<span class="status-pill" :class="skill.isEnabledTone">{{ skill.isEnabledLabel }}</span>
|
||||
</td>
|
||||
<td>{{ skill.updatedAt }}</td>
|
||||
<td :data-label="tableColumns.updatedAt || '最近更新'">{{ skill.updatedAt }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -156,15 +156,15 @@
|
||||
<strong class="doc-id">{{ employee.name }}</strong>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="doc-kind-tag application">{{ employee.skillCategory }}</span></td>
|
||||
<td>{{ employee.owner }}</td>
|
||||
<td><span class="type-tag other">{{ employee.scope }}</span></td>
|
||||
<td>{{ employee.executionMode }}</td>
|
||||
<td>
|
||||
<td data-label="技能类型"><span class="doc-kind-tag application">{{ employee.skillCategory }}</span></td>
|
||||
<td data-label="维护归口">{{ employee.owner }}</td>
|
||||
<td data-label="执行计划"><span class="type-tag other">{{ employee.scope }}</span></td>
|
||||
<td data-label="触发方式">{{ employee.executionMode }}</td>
|
||||
<td data-label="资产状态">
|
||||
<span :class="['status-tag', employee.statusTone]">{{ employee.status }}</span>
|
||||
</td>
|
||||
<td><span :class="['status-tag', employee.enabledTone]">{{ employee.enabledLabel }}</span></td>
|
||||
<td>{{ employee.updatedAt || '-' }}</td>
|
||||
<td data-label="启动状态"><span :class="['status-tag', employee.enabledTone]">{{ employee.enabledLabel }}</span></td>
|
||||
<td data-label="最近更新">{{ employee.updatedAt || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -292,4 +292,21 @@ function changePageSize(size) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.digital-employee-list-panel {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.digital-employee-list-panel :deep(.table-wrap) {
|
||||
flex: 0 0 auto;
|
||||
min-height: 0;
|
||||
display: block;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.digital-employee-list-panel :deep(.list-foot) {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 12px 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
note="把费用申请、报销进度、制度问答和待办处理集中到一个入口。"
|
||||
/>
|
||||
|
||||
<article class="panel assistant-hero" :style="{ '--assistant-bg-image': `url(${homepageBackground})` }">
|
||||
<article class="panel assistant-hero" :style="{ '--assistant-bg-image': `url(${workbenchHeroBackground})` }">
|
||||
<div class="assistant-copy">
|
||||
<h1>嗨,{{ displayUserName }},我是您的 <span>AI 费用助手</span></h1>
|
||||
|
||||
@@ -358,16 +358,17 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import PanelHead from '../shared/PanelHead.vue'
|
||||
import ExpenseProfileDetailModal from './ExpenseProfileDetailModal.vue'
|
||||
import WorkbenchListIcon from '../shared/WorkbenchListIcon.vue'
|
||||
import homepageBackground from '../../assets/homepage_backgraound.png'
|
||||
import workbenchHeroBackground from '../../assets/personal-workbench-hero-bg-theme-base.webp'
|
||||
import { useSystemState } from '../../composables/useSystemState.js'
|
||||
import { useToast } from '../../composables/useToast.js'
|
||||
import { useWorkbenchComposerDate } from '../../composables/useWorkbenchComposerDate.js'
|
||||
import {
|
||||
assistantCapabilities,
|
||||
buildExpenseStatItems,
|
||||
filterAssistantCapabilitiesForUser,
|
||||
progressItems,
|
||||
progressSteps,
|
||||
quickPromptItems,
|
||||
resolveWorkbenchCapabilityGridClass,
|
||||
todoItems,
|
||||
} from '../../data/personalWorkbench.js'
|
||||
import { fetchAgentRuns } from '../../services/agentAssets.js'
|
||||
@@ -433,9 +434,6 @@ let employeeProfileLoadSeq = 0
|
||||
const MAX_ATTACHMENTS = 10
|
||||
const SESSION_TYPE_EXPENSE = 'expense'
|
||||
const SESSION_TYPE_KNOWLEDGE = 'knowledge'
|
||||
const FINANCIAL_CAPABILITY_KEYS = new Set(['budget-planning', 'finance-analysis'])
|
||||
const FINANCIAL_CAPABILITY_ROLE_CODES = new Set(['budget_monitor', 'executive', 'admin'])
|
||||
const FINANCIAL_CAPABILITY_ROLE_LABELS = new Set(['预算监控员', '高级财务人员', '管理员'])
|
||||
|
||||
const hasExpenseConversation = computed(() =>
|
||||
Boolean(latestExpenseConversation.value?.conversation_id || latestExpenseConversation.value?.conversationId)
|
||||
@@ -456,28 +454,8 @@ const composerPendingLabel = computed(() => {
|
||||
}
|
||||
return ''
|
||||
})
|
||||
const currentRoleCodes = computed(() => {
|
||||
const user = currentUser.value || {}
|
||||
const rawCodes = Array.isArray(user.roleCodes)
|
||||
? user.roleCodes
|
||||
: Array.isArray(user.role_codes)
|
||||
? user.role_codes
|
||||
: []
|
||||
return new Set(rawCodes.map((code) => String(code || '').trim().toLowerCase()).filter(Boolean))
|
||||
})
|
||||
const canViewFinancialCapabilities = computed(() => {
|
||||
const user = currentUser.value || {}
|
||||
const roleLabel = String(user.role || '').trim()
|
||||
return Boolean(user.isAdmin)
|
||||
|| FINANCIAL_CAPABILITY_ROLE_LABELS.has(roleLabel)
|
||||
|| Array.from(currentRoleCodes.value).some((code) => FINANCIAL_CAPABILITY_ROLE_CODES.has(code))
|
||||
})
|
||||
const visibleAssistantCapabilities = computed(() =>
|
||||
assistantCapabilities.filter((item) => canViewFinancialCapabilities.value || !FINANCIAL_CAPABILITY_KEYS.has(item.key))
|
||||
)
|
||||
const capabilityGridClass = computed(() =>
|
||||
canViewFinancialCapabilities.value ? 'capability-grid--privileged' : 'capability-grid--standard'
|
||||
)
|
||||
const visibleAssistantCapabilities = computed(() => filterAssistantCapabilitiesForUser(currentUser.value))
|
||||
const capabilityGridClass = computed(() => resolveWorkbenchCapabilityGridClass(currentUser.value))
|
||||
const expenseStatItems = computed(() => buildExpenseStatItems(props.workbenchSummary))
|
||||
const visibleExpenseStatItems = computed(() => {
|
||||
const preferredKeys = ['monthly-amount', 'monthly-count', 'in-review', 'pending-payment']
|
||||
@@ -817,6 +795,7 @@ watch(currentUserProfileKey, (nextKey, previousKey) => {
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/components/personal-workbench.css"></style>
|
||||
<style scoped src="../../assets/styles/components/personal-workbench-glass.css"></style>
|
||||
<style scoped src="../../assets/styles/components/personal-workbench-composer-date.css"></style>
|
||||
<style scoped src="../../assets/styles/components/personal-workbench-insights.css"></style>
|
||||
<style scoped src="../../assets/styles/components/personal-workbench-responsive.css"></style>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<section class="budget-report-main">
|
||||
<article class="budget-report-chart-panel">
|
||||
<div class="budget-report-section-head">
|
||||
<strong>上季度费用结构</strong>
|
||||
<strong>{{ expenseStructureTitle }}</strong>
|
||||
<span>{{ report.centerLabel }}</span>
|
||||
</div>
|
||||
<DonutChart
|
||||
@@ -52,7 +52,7 @@
|
||||
<section class="budget-report-detail-panel">
|
||||
<div class="budget-report-section-head">
|
||||
<strong>费用类型拆解</strong>
|
||||
<span>用于编制下一季度预算</span>
|
||||
<span>用于编制{{ report.periodType || '下一期预算' }}</span>
|
||||
</div>
|
||||
<div class="budget-report-expense-list">
|
||||
<article
|
||||
@@ -83,17 +83,21 @@
|
||||
<section class="budget-report-editor-panel">
|
||||
<div class="budget-report-section-head">
|
||||
<strong>预算构成编辑</strong>
|
||||
<span>{{ report.periodType || '预算' }} · 可直接调整</span>
|
||||
<span>{{ editorSubtitle }}</span>
|
||||
</div>
|
||||
|
||||
<div class="budget-editor-table" role="table" aria-label="预算构成编辑表">
|
||||
<div
|
||||
class="budget-editor-table"
|
||||
:class="{ 'is-review': isReviewMode }"
|
||||
role="table"
|
||||
aria-label="预算构成编辑表"
|
||||
>
|
||||
<div class="budget-editor-row head" role="row">
|
||||
<span role="columnheader">费用类型</span>
|
||||
<span role="columnheader">编制金额</span>
|
||||
<span role="columnheader">提醒</span>
|
||||
<span role="columnheader">告警</span>
|
||||
<span role="columnheader">风险</span>
|
||||
<span role="columnheader">预算金额</span>
|
||||
<span v-if="isReviewMode" role="columnheader">建议预算</span>
|
||||
<span role="columnheader">预算说明</span>
|
||||
<span v-if="isReviewMode" role="columnheader">建议</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -104,43 +108,50 @@
|
||||
>
|
||||
<strong role="cell">{{ row.name }}</strong>
|
||||
<label role="cell">
|
||||
<span>金额</span>
|
||||
<input v-model.number="row.budgetAmount" type="number" min="0" step="1000" />
|
||||
<span>预算金额</span>
|
||||
<input
|
||||
v-model.number="row.budgetAmount"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1000"
|
||||
:readonly="isReviewMode"
|
||||
/>
|
||||
</label>
|
||||
<label role="cell">
|
||||
<span>提醒</span>
|
||||
<input v-model.number="row.reminderThreshold" type="number" min="0" max="100" step="1" />
|
||||
<label v-if="isReviewMode" role="cell">
|
||||
<span>建议预算</span>
|
||||
<input v-model.number="row.suggestedBudget" type="number" min="0" step="1000" />
|
||||
</label>
|
||||
<label role="cell">
|
||||
<span>告警</span>
|
||||
<input v-model.number="row.alertThreshold" type="number" min="0" max="100" step="1" />
|
||||
<label class="budget-editor-note-cell" role="cell">
|
||||
<span>预算说明</span>
|
||||
<textarea v-model="row.submittedNote" :readonly="isReviewMode" rows="2" />
|
||||
</label>
|
||||
<label role="cell">
|
||||
<span>风险</span>
|
||||
<input v-model.number="row.riskThreshold" type="number" min="0" max="100" step="1" />
|
||||
<label v-if="isReviewMode" class="budget-editor-note-cell" role="cell">
|
||||
<span>建议</span>
|
||||
<textarea v-model="row.financeSuggestion" rows="2" />
|
||||
</label>
|
||||
<textarea v-model="row.note" role="cell" rows="2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="budget-editor-footer">
|
||||
<div>
|
||||
<span>当前编制总额</span>
|
||||
<span>{{ totalLabel }}</span>
|
||||
<strong>{{ editableTotalDisplay }}</strong>
|
||||
<small>{{ draftStatusText }}</small>
|
||||
</div>
|
||||
<button type="button" class="budget-editor-secondary" @click="applyRecommendedBudget">
|
||||
应用建议
|
||||
</button>
|
||||
<button type="button" class="budget-editor-primary" @click="generateBudgetDraft">
|
||||
生成预算草案
|
||||
<button
|
||||
type="button"
|
||||
class="budget-editor-primary"
|
||||
:class="{ danger: isReviewMode && hasReviewChanges }"
|
||||
@click="submitBudgetEditorAction"
|
||||
>
|
||||
{{ primaryActionLabel }}
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
<section class="budget-report-action-panel">
|
||||
<div>
|
||||
<strong>编制建议</strong>
|
||||
<strong>{{ recommendationTitle }}</strong>
|
||||
<p v-for="item in report.recommendations" :key="item">{{ item }}</p>
|
||||
</div>
|
||||
<span>{{ report.generatedAt }}</span>
|
||||
@@ -162,6 +173,7 @@ const props = defineProps({
|
||||
|
||||
const draftRows = reactive([])
|
||||
const draftStatus = ref('editing')
|
||||
const initialReviewSnapshot = ref('')
|
||||
|
||||
const formatAmount = (value) =>
|
||||
`¥${Number(value || 0).toLocaleString('zh-CN', {
|
||||
@@ -177,44 +189,94 @@ function resetDraftRows() {
|
||||
key: item.key,
|
||||
name: item.name,
|
||||
budgetAmount: Number(item.budgetAmount ?? item.recommendedBudget ?? 0),
|
||||
reminderThreshold: Number(item.reminderThreshold ?? 70),
|
||||
alertThreshold: Number(item.alertThreshold ?? 80),
|
||||
riskThreshold: Number(item.riskThreshold ?? 90),
|
||||
note: String(item.note || item.suggestion || '')
|
||||
suggestedBudget: Number(item.suggestedBudget ?? item.recommendedBudget ?? item.budgetAmount ?? 0),
|
||||
submittedNote: String(item.submittedNote || item.note || item.suggestion || ''),
|
||||
financeSuggestion: String(item.financeSuggestion || '')
|
||||
})))
|
||||
)
|
||||
draftStatus.value = 'editing'
|
||||
initialReviewSnapshot.value = buildReviewSnapshot()
|
||||
}
|
||||
|
||||
watch(() => props.report, resetDraftRows, { immediate: true })
|
||||
|
||||
const editableTotalDisplay = computed(() =>
|
||||
formatAmount(draftRows.reduce((sum, item) => sum + Number(item.budgetAmount || 0), 0))
|
||||
formatAmount(draftRows.reduce((sum, item) => {
|
||||
const value = isReviewMode.value ? item.suggestedBudget : item.budgetAmount
|
||||
return sum + Number(value || 0)
|
||||
}, 0))
|
||||
)
|
||||
|
||||
const isReviewMode = computed(() =>
|
||||
props.report.mode === 'review' || props.report.editableDraft?.mode === 'review'
|
||||
)
|
||||
|
||||
const editorSubtitle = computed(() =>
|
||||
isReviewMode.value
|
||||
? '高级财务审核 · 修改建议预算或建议后将回退预算'
|
||||
: `${props.report.periodType || '预算'} · 仅编辑本部门预算`
|
||||
)
|
||||
|
||||
const totalLabel = computed(() => isReviewMode.value ? '建议预算总额' : '当前编制总额')
|
||||
|
||||
function buildReviewSnapshot() {
|
||||
return JSON.stringify(draftRows.map((row) => ({
|
||||
key: row.key,
|
||||
suggestedBudget: Number(row.suggestedBudget || 0),
|
||||
financeSuggestion: String(row.financeSuggestion || '').trim()
|
||||
})))
|
||||
}
|
||||
|
||||
const hasReviewChanges = computed(() =>
|
||||
isReviewMode.value && buildReviewSnapshot() !== initialReviewSnapshot.value
|
||||
)
|
||||
|
||||
const draftStatusText = computed(() =>
|
||||
draftStatus.value === 'generated'
|
||||
? '已生成本轮预算草案,后续可提交高级财务审核'
|
||||
: '调整后可生成预算草案'
|
||||
draftStatus.value === 'returned'
|
||||
? '已标记回退预算,请预算管理者按建议调整后再次提交'
|
||||
: draftStatus.value === 'formed'
|
||||
? '已形成预算,可进入预算中心正式生效'
|
||||
: isReviewMode.value
|
||||
? '未调整建议时可形成预算;调整后将回退预算'
|
||||
: '保存后提交高级财务人员审核'
|
||||
)
|
||||
|
||||
function applyRecommendedBudget() {
|
||||
resetDraftRows()
|
||||
}
|
||||
const baseBudgetLabel = computed(() =>
|
||||
isReviewMode.value
|
||||
? '提交预算'
|
||||
: props.report.periodType === '年度预算' ? '去年预算' : '上季度预算'
|
||||
)
|
||||
|
||||
function generateBudgetDraft() {
|
||||
draftStatus.value = 'generated'
|
||||
const expenseStructureTitle = computed(() =>
|
||||
isReviewMode.value
|
||||
? '提交预算费用结构'
|
||||
: props.report.periodType === '年度预算' ? '去年费用结构' : '上季度费用结构'
|
||||
)
|
||||
|
||||
const recommendationTitle = computed(() => isReviewMode.value ? '审核建议' : '编制建议')
|
||||
|
||||
const primaryActionLabel = computed(() => {
|
||||
if (!isReviewMode.value) return '保存预算'
|
||||
return hasReviewChanges.value ? '回退预算' : '形成预算'
|
||||
})
|
||||
|
||||
function submitBudgetEditorAction() {
|
||||
if (!isReviewMode.value) {
|
||||
draftStatus.value = 'formed'
|
||||
return
|
||||
}
|
||||
draftStatus.value = hasReviewChanges.value ? 'returned' : 'formed'
|
||||
}
|
||||
|
||||
const summaryCards = computed(() => [
|
||||
{
|
||||
label: '上季度预算',
|
||||
label: baseBudgetLabel.value,
|
||||
value: props.report.summary?.totalBudget || '—',
|
||||
hint: '作为编制基准',
|
||||
color: 'var(--theme-primary)'
|
||||
},
|
||||
{
|
||||
label: '上季度开销',
|
||||
label: props.report.centerLabel || '上季度开销',
|
||||
value: props.report.summary?.totalSpend || '—',
|
||||
hint: '按四类预算口径汇总',
|
||||
color: 'var(--theme-secondary)'
|
||||
@@ -396,12 +458,21 @@ const summaryCards = computed(() => [
|
||||
|
||||
.budget-editor-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(64px, .7fr) minmax(118px, .95fr) repeat(3, minmax(68px, .55fr)) minmax(220px, 1.6fr);
|
||||
grid-template-columns: minmax(82px, .7fr) minmax(128px, .9fr) minmax(280px, 2fr);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.budget-editor-table.is-review .budget-editor-row {
|
||||
grid-template-columns:
|
||||
minmax(82px, .65fr)
|
||||
minmax(122px, .75fr)
|
||||
minmax(122px, .8fr)
|
||||
minmax(240px, 1.6fr)
|
||||
minmax(240px, 1.6fr);
|
||||
}
|
||||
|
||||
.budget-editor-row.head {
|
||||
min-height: 34px;
|
||||
padding: 0 8px;
|
||||
@@ -465,6 +536,13 @@ const summaryCards = computed(() => [
|
||||
outline: 3px solid var(--theme-focus-ring, rgba(58, 124, 165, .12));
|
||||
}
|
||||
|
||||
.budget-editor-row input[readonly],
|
||||
.budget-editor-row textarea[readonly] {
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.budget-editor-footer {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
@@ -510,6 +588,10 @@ const summaryCards = computed(() => [
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.budget-editor-primary.danger {
|
||||
background: #7f1d1d;
|
||||
}
|
||||
|
||||
.budget-editor-secondary {
|
||||
border: 1px solid #d7e0ea;
|
||||
background: #fff;
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<strong>{{ decisionTitle }}</strong>
|
||||
<p>{{ decisionDescription }}</p>
|
||||
</div>
|
||||
<div v-if="compactAdviceItems.length" class="employee-risk-advice-list">
|
||||
<p v-for="item in compactAdviceItems" :key="item">{{ item }}</p>
|
||||
</div>
|
||||
<div class="employee-risk-action">
|
||||
<span>建议动作</span>
|
||||
<strong :class="decisionTone">{{ decisionAction }}</strong>
|
||||
</div>
|
||||
<div v-if="compactAdviceItems.length" class="employee-risk-advice-list">
|
||||
<p v-for="item in compactAdviceItems" :key="item">{{ item }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="employee-risk-profile-section">
|
||||
@@ -315,8 +315,20 @@ function normalizeBusinessStage(value) {
|
||||
|
||||
function resolveReimbursementMaterialIssues(items) {
|
||||
return items
|
||||
.filter((item) => !item?.isSystemGenerated && !String(item?.invoiceId || '').trim())
|
||||
.map((item) => `未上传票据:${item.name || item.category || item.desc || '未命名明细'}`)
|
||||
.filter((item) => !item?.isSystemGenerated && isRequiredMaterialItem(item) && !String(item?.invoiceId || '').trim())
|
||||
.map((item) => `住宿材料待补充:${item.name || item.category || item.desc || '住宿明细'}`)
|
||||
}
|
||||
|
||||
function isRequiredMaterialItem(item) {
|
||||
const text = [
|
||||
item?.itemType,
|
||||
item?.typeCode,
|
||||
item?.name,
|
||||
item?.category,
|
||||
item?.desc,
|
||||
item?.itemReason
|
||||
].map((value) => String(value || '').trim()).join(' ')
|
||||
return /hotel_ticket|hotel|住宿|酒店|水单/.test(text)
|
||||
}
|
||||
|
||||
function resolveSceneIssues(request, items, isApplicationDocument) {
|
||||
@@ -522,7 +534,7 @@ function uniqueTexts(values) {
|
||||
|
||||
.employee-risk-ai-note {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(220px, 38%);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
@@ -568,14 +580,18 @@ function uniqueTexts(values) {
|
||||
}
|
||||
|
||||
.employee-risk-action {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.employee-risk-action span {
|
||||
@@ -592,6 +608,7 @@ function uniqueTexts(values) {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.employee-risk-action strong.medium {
|
||||
|
||||
@@ -333,6 +333,66 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="message.role === 'assistant' && ui.shouldShowDraftSavedCard(message)"
|
||||
class="draft-preview application-draft-preview"
|
||||
:class="{ 'reimbursement-draft-preview': !ui.isApplicationDraftPayload(message.draftPayload) }"
|
||||
>
|
||||
<template v-if="ui.isApplicationDraftPayload(message.draftPayload)">
|
||||
<header class="application-draft-head">
|
||||
<span class="application-draft-icon" aria-hidden="true">
|
||||
<i class="mdi mdi-file-document-check-outline"></i>
|
||||
</span>
|
||||
<span class="application-draft-title">
|
||||
<strong>申请单据已生成</strong>
|
||||
<small>已为本次业务生成申请单,请按需查看完整详情。</small>
|
||||
</span>
|
||||
<span class="application-draft-status">{{ ui.resolveApplicationDraftStatusLabel(message.draftPayload) }}</span>
|
||||
</header>
|
||||
<div class="application-draft-brief" role="group" aria-label="申请单据简要信息">
|
||||
<div
|
||||
v-for="item in ui.buildApplicationDraftSummaryItems(message.draftPayload)"
|
||||
:key="`${message.id}-application-draft-${item.label}`"
|
||||
class="application-draft-brief-item"
|
||||
:class="{ 'is-primary': item.label === '单号' }"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="application-draft-footer">
|
||||
<p>
|
||||
完整审批链、附件和明细可在单据详情中
|
||||
<button
|
||||
type="button"
|
||||
class="application-draft-detail-link"
|
||||
:disabled="ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
|
||||
@click="ui.openApplicationDraftDetail(message)"
|
||||
>查看</button>。
|
||||
</p>
|
||||
</footer>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="reimbursement-draft-card" role="group" aria-label="报销草稿已生成">
|
||||
<span class="reimbursement-draft-icon" aria-hidden="true">
|
||||
<i class="mdi mdi-file-document-edit-outline"></i>
|
||||
</span>
|
||||
<div class="reimbursement-draft-main">
|
||||
<strong>报销草稿已生成</strong>
|
||||
<p>
|
||||
单号:<span>{{ ui.resolveReimbursementDraftClaimNo(message.draftPayload) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="reimbursement-draft-link"
|
||||
:disabled="ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
|
||||
@click="ui.openApplicationDraftDetail(message)"
|
||||
>查看详情</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="message.role === 'assistant' && message.reviewPayload" class="message-detail-block review-message-block">
|
||||
<div class="review-plain-followup">
|
||||
<template
|
||||
@@ -405,54 +465,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="message.role === 'assistant' && !message.reviewPayload && message.draftPayload"
|
||||
class="draft-preview"
|
||||
:class="{ 'application-draft-preview': ui.isApplicationDraftPayload(message.draftPayload) }"
|
||||
>
|
||||
<template v-if="ui.isApplicationDraftPayload(message.draftPayload)">
|
||||
<header class="application-draft-head">
|
||||
<span class="application-draft-icon" aria-hidden="true">
|
||||
<i class="mdi mdi-file-document-check-outline"></i>
|
||||
</span>
|
||||
<span class="application-draft-title">
|
||||
<strong>申请单据已生成</strong>
|
||||
<small>已为本次业务生成申请单,请按需查看完整详情。</small>
|
||||
</span>
|
||||
<span class="application-draft-status">{{ ui.resolveApplicationDraftStatusLabel(message.draftPayload) }}</span>
|
||||
</header>
|
||||
<div class="application-draft-brief" role="group" aria-label="申请单据简要信息">
|
||||
<div
|
||||
v-for="item in ui.buildApplicationDraftSummaryItems(message.draftPayload)"
|
||||
:key="`${message.id}-application-draft-${item.label}`"
|
||||
class="application-draft-brief-item"
|
||||
:class="{ 'is-primary': item.label === '单号' }"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="application-draft-footer">
|
||||
<p>
|
||||
完整审批链、附件和明细可在单据详情中
|
||||
<button
|
||||
type="button"
|
||||
class="application-draft-detail-link"
|
||||
:disabled="ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
|
||||
@click="ui.openApplicationDraftDetail(message)"
|
||||
>查看</button>。
|
||||
</p>
|
||||
</footer>
|
||||
</template>
|
||||
<template v-else>
|
||||
<header>
|
||||
<strong>{{ message.draftPayload.title }}</strong>
|
||||
<span>待人工确认</span>
|
||||
</header>
|
||||
<pre>{{ message.draftPayload.body }}</pre>
|
||||
</template>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user