feat(web): 工作台 AI 模式与差旅/风险建议交互优化
- 新增 PersonalWorkbenchAiMode 组件、AI 侧边栏与 orb 机器人视觉资源 - 新增 aiApplicationDraftModel / aiExpenseDraftModel / aiWorkbenchConversationStore 及业务准入 aiSidebarBusinessAccess,支撑 AI 模式下的申请与报销草稿 - 顶栏、侧边栏、工作台样式重构,适配 AI 模式切换与响应式布局 - 同步 steward plan/off_topic、差旅报销引导流、风险建议卡片等测试
This commit is contained in:
@@ -18,32 +18,45 @@
|
||||
<p>{{ decisionDescription }}</p>
|
||||
</div>
|
||||
<div class="employee-risk-decision-action">
|
||||
<span>建议结论</span>
|
||||
<strong :class="decisionTone">{{ decisionAction }}</strong>
|
||||
<span>是否建议通过</span>
|
||||
<strong :class="decisionTone">{{ decisionBadgeLabel }}</strong>
|
||||
<p>{{ decisionAction }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="employee-risk-profile-section" aria-label="单据风险依据">
|
||||
<dl class="employee-risk-review-summary" aria-label="审核建议摘要">
|
||||
<div
|
||||
v-for="item in reviewSummaryItems"
|
||||
:key="item.key"
|
||||
:class="['employee-risk-review-item', item.tone]"
|
||||
>
|
||||
<dt>{{ item.label }}</dt>
|
||||
<dd>{{ item.value }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<section class="employee-risk-profile-section" aria-label="单据关键依据">
|
||||
<div class="employee-risk-section-head">
|
||||
<span>{{ stageBasisTitle }}</span>
|
||||
<small>{{ stageBasisHint }}</small>
|
||||
</div>
|
||||
<div v-if="compactEvidenceItems.length" class="employee-risk-profile-list">
|
||||
<article
|
||||
v-for="item in compactEvidenceItems"
|
||||
<details
|
||||
v-for="(item, index) in compactEvidenceItems"
|
||||
:key="item.code"
|
||||
:class="['employee-risk-evidence-row', item.tone]"
|
||||
:open="index === 0"
|
||||
>
|
||||
<div class="employee-risk-evidence-title">
|
||||
<summary class="employee-risk-evidence-title">
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.status }}</strong>
|
||||
</div>
|
||||
</summary>
|
||||
<ul v-if="item.evidence.length">
|
||||
<li v-for="basis in item.evidence" :key="basis">{{ basis }}</li>
|
||||
</ul>
|
||||
</article>
|
||||
</details>
|
||||
</div>
|
||||
<p v-else class="employee-risk-muted">当前未识别到需要重点展示的单据风险依据。</p>
|
||||
<p v-else class="employee-risk-muted">当前未识别到需要重点展示的单据依据。</p>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
@@ -95,12 +108,12 @@ export default {
|
||||
}
|
||||
return 'normal'
|
||||
})
|
||||
const stageTitle = computed(() => props.isApplicationDocument ? '申请审核建议' : '报销审核建议')
|
||||
const stageBasisTitle = computed(() => props.isApplicationDocument ? '申请单风险依据' : '报销单风险依据')
|
||||
const stageTitle = computed(() => props.isApplicationDocument ? '申请审核建议' : 'AI建议')
|
||||
const stageBasisTitle = computed(() => props.isApplicationDocument ? '申请单关键依据' : '报销单关键依据')
|
||||
const stageBasisHint = computed(() => (
|
||||
props.isApplicationDocument
|
||||
? '仅展示申请单本身的金额、预算触发、事由和规则命中依据。'
|
||||
: '仅展示报销单本身的票据、金额、行程和规则命中依据。'
|
||||
? '默认只展开最关键的申请依据,其他细节点开查看。'
|
||||
: '默认只展开最关键的报销依据,其他细节点开查看。'
|
||||
))
|
||||
const decisionTitle = computed(() => resolveDecision(decisionTone.value, props.isApplicationDocument).title)
|
||||
const decisionAction = computed(() => {
|
||||
@@ -111,25 +124,26 @@ export default {
|
||||
})
|
||||
const decisionBadgeLabel = computed(() => {
|
||||
if (decisionTone.value === 'high') {
|
||||
return '高风险'
|
||||
return '不通过'
|
||||
}
|
||||
if (decisionTone.value === 'medium') {
|
||||
return '需关注'
|
||||
return '待补充'
|
||||
}
|
||||
return '可审批'
|
||||
return '可通过'
|
||||
})
|
||||
const decisionDescription = computed(() => {
|
||||
const riskCount = currentRiskCards.value.length
|
||||
const subject = props.isApplicationDocument ? '申请' : '报销'
|
||||
if (riskCount) {
|
||||
if (!props.isApplicationDocument && riskExplanationItems.value.length) {
|
||||
return `当前报销已识别 ${riskCount} 个需核对风险点,用户已补充异常说明,审批人应核对说明与票据佐证是否充分。`
|
||||
return `当前${subject}识别到 ${riskCount} 个需核对风险点,已补充说明但仍建议先核对票据与行程。`
|
||||
}
|
||||
return `${props.isApplicationDocument ? '当前申请' : '当前报销'}已识别 ${riskCount} 个需核对风险点,审批人应优先查看中高风险依据。`
|
||||
return `当前${subject}识别到 ${riskCount} 个需核对风险点,请优先查看高风险依据。`
|
||||
}
|
||||
if (materialIssues.value.length || sceneIssues.value.length) {
|
||||
return `${props.isApplicationDocument ? '当前申请' : '当前报销'}存在材料或业务说明不完整,建议补齐后再继续处理。`
|
||||
return `当前${subject}存在材料或业务说明不完整,建议补齐后再处理。`
|
||||
}
|
||||
return `${props.isApplicationDocument ? '当前申请' : '当前报销'}未发现中高风险阻断项,可结合当前环节权限按流程处理。`
|
||||
return `当前${subject}未发现中高风险阻断项,可按流程继续处理。`
|
||||
})
|
||||
const stageEvidenceItems = computed(() => (
|
||||
props.isApplicationDocument ? buildApplicationEvidence() : buildReimbursementEvidence()
|
||||
@@ -139,6 +153,38 @@ export default {
|
||||
const sourceItems = abnormalItems.length ? abnormalItems : stageEvidenceItems.value
|
||||
return sourceItems.map((item) => ({ ...item }))
|
||||
})
|
||||
const stageRiskFactSummary = computed(() => buildStageRiskFactSummary({
|
||||
isApplicationDocument: props.isApplicationDocument,
|
||||
riskCount: currentRiskCards.value.length,
|
||||
highCount: highRiskCards.value.length,
|
||||
mediumCount: mediumRiskCards.value.length,
|
||||
materialIssueCount: materialIssues.value.length,
|
||||
sceneIssueCount: sceneIssues.value.length
|
||||
}))
|
||||
const stageReviewBasisSummary = computed(() => buildStageReviewBasisSummary(
|
||||
compactEvidenceItems.value,
|
||||
props.isApplicationDocument
|
||||
))
|
||||
const reviewSummaryItems = computed(() => [
|
||||
{
|
||||
key: 'fact',
|
||||
label: '风险概览',
|
||||
tone: decisionTone.value,
|
||||
value: stageRiskFactSummary.value
|
||||
},
|
||||
{
|
||||
key: 'basis',
|
||||
label: '重点依据',
|
||||
tone: decisionTone.value,
|
||||
value: stageReviewBasisSummary.value
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
label: '审核建议',
|
||||
tone: decisionTone.value,
|
||||
value: decisionAction.value
|
||||
}
|
||||
])
|
||||
|
||||
function buildApplicationEvidence() {
|
||||
const budgetCards = currentRiskCards.value.filter((card) => /预算|余额|占用|超预算/.test(cardText(card)))
|
||||
@@ -217,28 +263,68 @@ export default {
|
||||
decisionDescription,
|
||||
decisionAction,
|
||||
decisionTitle,
|
||||
reviewSummaryItems,
|
||||
stageBasisHint,
|
||||
stageBasisTitle,
|
||||
stageEvidenceItems,
|
||||
stageReviewBasisSummary,
|
||||
stageRiskFactSummary,
|
||||
stageTitle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildStageRiskFactSummary({
|
||||
isApplicationDocument,
|
||||
riskCount = 0,
|
||||
highCount = 0,
|
||||
mediumCount = 0,
|
||||
materialIssueCount = 0,
|
||||
sceneIssueCount = 0
|
||||
} = {}) {
|
||||
const subject = isApplicationDocument ? '申请单' : '报销单'
|
||||
if (riskCount > 0) {
|
||||
return `${subject}识别 ${riskCount} 个需核对风险点,高风险 ${highCount} 个,中风险 ${mediumCount} 个。`
|
||||
}
|
||||
const issueCount = materialIssueCount + sceneIssueCount
|
||||
if (issueCount > 0) {
|
||||
return `${subject}暂无中高风险命中,但仍有 ${issueCount} 个材料或业务说明项需要补齐。`
|
||||
}
|
||||
return `${subject}未识别到中高风险阻断项。`
|
||||
}
|
||||
|
||||
function buildStageReviewBasisSummary(evidenceItems = [], isApplicationDocument = false) {
|
||||
const abnormalLabels = evidenceItems
|
||||
.filter((item) => isAbnormalEvidence(item))
|
||||
.map((item) => String(item?.label || '').trim())
|
||||
.filter(Boolean)
|
||||
if (abnormalLabels.length) {
|
||||
return `重点核对${abnormalLabels.join('、')}。`
|
||||
}
|
||||
return isApplicationDocument
|
||||
? '重点看申请金额、预算触发和事由是否一致。'
|
||||
: '重点看票据、金额、行程和附件是否一致。'
|
||||
}
|
||||
|
||||
function resolveDecision(tone, isApplicationDocument) {
|
||||
const subject = isApplicationDocument ? '申请' : '报销'
|
||||
const map = {
|
||||
normal: {
|
||||
title: `当前${subject}未发现中高风险阻断项`,
|
||||
action: `可按权限继续审批${isApplicationDocument ? ',系统会按预算结果决定是否跳过预算复核。' : ',后续进入财务或付款流程。'}`
|
||||
title: '建议通过',
|
||||
action: isApplicationDocument
|
||||
? '可按权限继续审核,系统会按预算结果决定是否进入下一步。'
|
||||
: '可按权限继续审批,后续进入财务或付款流程。'
|
||||
},
|
||||
medium: {
|
||||
title: `当前${subject}存在中风险,建议核对后处理`,
|
||||
action: isApplicationDocument ? '建议核对预算占用、申请事由和金额依据后再通过。' : '建议核对票据、金额和业务说明后再通过。'
|
||||
title: '建议补充后通过',
|
||||
action: isApplicationDocument
|
||||
? '建议补充预算占用、申请事由和金额依据后再通过。'
|
||||
: '建议补充票据、金额或业务说明后再通过。'
|
||||
},
|
||||
high: {
|
||||
title: `当前${subject}存在高风险,不建议直接通过`,
|
||||
action: isApplicationDocument ? '建议退回补充申请依据,或要求预算管理者复核。' : '建议退回补充票据、行程说明或超标原因。'
|
||||
title: '不建议通过',
|
||||
action: isApplicationDocument
|
||||
? '建议退回补充申请依据,或要求预算管理者复核。'
|
||||
: '建议退回补充票据、行程说明或超标原因。'
|
||||
}
|
||||
}
|
||||
return map[tone] || map.normal
|
||||
|
||||
@@ -278,6 +278,7 @@
|
||||
<div
|
||||
v-if="message.role === 'assistant' && !message.reviewPayload && !message.queryPayload && message.suggestedActions?.length"
|
||||
class="message-suggested-actions"
|
||||
:class="{ 'compact-guidance-actions': message.assistantVariant === 'compact_guidance' }"
|
||||
>
|
||||
<button
|
||||
v-for="action in message.suggestedActions"
|
||||
|
||||
Reference in New Issue
Block a user