feat: 报销预审会话状态管理与工作台交互增强
- 新增差旅报销会话状态管理与对话模型重构 - 增强风险观测服务与运行时聊天上下文作用域 - 优化工作台图标资源、助理意图识别与摘要工具 - 完善报销创建视图样式与差旅详情页标准调整交互 - 补充风险观测、运行时聊天与报销端点测试覆盖
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
|
||||
<article class="panel assistant-hero" :style="{ '--assistant-bg-image': `url(${workbenchHeroBackground})` }">
|
||||
<div class="assistant-copy">
|
||||
<h1>嗨,{{ displayUserName }},我是您的 <span>AI 费用助手</span></h1>
|
||||
<h1>嗨,{{ displayUserName }},我是您的 <span>小财管家</span></h1>
|
||||
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
@@ -26,7 +26,7 @@
|
||||
v-model="assistantDraft"
|
||||
maxlength="1000"
|
||||
rows="2"
|
||||
placeholder="请输入费用申请、报销问题、预算查询或制度问答..."
|
||||
placeholder="一次性描述申请、报销和附件处理事项,小财管家会先拆解再执行..."
|
||||
:readonly="isComposerPending"
|
||||
@keydown.enter.prevent="handleWorkbenchEnter"
|
||||
/>
|
||||
@@ -180,7 +180,12 @@
|
||||
:class="`capability-card--${item.tone}`"
|
||||
@click="openCapabilityAssistant(item)"
|
||||
>
|
||||
<span class="capability-icon"><i :class="item.icon"></i></span>
|
||||
<WorkbenchListIcon
|
||||
class="capability-icon"
|
||||
:icon-key="item.key"
|
||||
color="var(--capability-color)"
|
||||
accent="var(--capability-soft)"
|
||||
/>
|
||||
<span class="capability-copy">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<small>{{ item.primary }}</small>
|
||||
@@ -221,8 +226,8 @@
|
||||
<small>{{ item.id }}</small>
|
||||
</span>
|
||||
|
||||
<span class="progress-type" :title="item.expenseTypeLabel || '其他费用'">
|
||||
<strong>{{ item.expenseTypeLabel || '其他费用' }}</strong>
|
||||
<span class="progress-type" :title="`${item.documentTypeLabel} · ${item.expenseTypeLabel || '其他费用'}`">
|
||||
<strong>{{ item.documentTypeLabel }} · {{ item.expenseTypeLabel || '其他费用' }}</strong>
|
||||
</span>
|
||||
|
||||
<span class="progress-steps" aria-hidden="true">
|
||||
@@ -349,9 +354,10 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import PanelHead from '../shared/PanelHead.vue'
|
||||
import WorkbenchListIcon from '../shared/WorkbenchListIcon.vue'
|
||||
import ExpenseStatsDetailModal from './ExpenseStatsDetailModal.vue'
|
||||
import ExpenseProfileDetailModal from './ExpenseProfileDetailModal.vue'
|
||||
import workbenchHeroBackground from '../../assets/personal-workbench-hero-bg-theme-base.webp'
|
||||
import workbenchHeroBackground from '../../assets/images/hero-3d-banner.png'
|
||||
import { useSystemState } from '../../composables/useSystemState.js'
|
||||
import { useToast } from '../../composables/useToast.js'
|
||||
import { useWorkbenchComposerDate } from '../../composables/useWorkbenchComposerDate.js'
|
||||
@@ -425,6 +431,7 @@ let employeeProfileLoadSeq = 0
|
||||
const MAX_ATTACHMENTS = 10
|
||||
const SESSION_TYPE_EXPENSE = 'expense'
|
||||
const SESSION_TYPE_KNOWLEDGE = 'knowledge'
|
||||
const SESSION_TYPE_STEWARD = 'steward'
|
||||
|
||||
const hasExpenseConversation = computed(() =>
|
||||
Boolean(latestExpenseConversation.value?.conversation_id || latestExpenseConversation.value?.conversationId)
|
||||
@@ -607,6 +614,7 @@ function buildAssistantPayload() {
|
||||
return {
|
||||
prompt: buildWorkbenchPromptText(),
|
||||
source: 'workbench',
|
||||
sessionType: SESSION_TYPE_STEWARD,
|
||||
files: Array.from(selectedFiles.value)
|
||||
}
|
||||
}
|
||||
@@ -674,7 +682,7 @@ function applyQuickPrompt(prompt) {
|
||||
focusAssistantInput()
|
||||
}
|
||||
|
||||
function openPromptAssistant(prompt) {
|
||||
function openPromptAssistant(prompt, sessionType = SESSION_TYPE_STEWARD) {
|
||||
if (pendingAction.value) {
|
||||
return
|
||||
}
|
||||
@@ -682,6 +690,7 @@ function openPromptAssistant(prompt) {
|
||||
const payload = {
|
||||
prompt: buildWorkbenchPromptText(prompt),
|
||||
source: 'workbench',
|
||||
sessionType,
|
||||
files: Array.from(selectedFiles.value),
|
||||
conversation: null
|
||||
}
|
||||
@@ -704,7 +713,7 @@ function openWorkbenchTarget(item) {
|
||||
return
|
||||
}
|
||||
|
||||
openPromptAssistant(item?.prompt || `查询 ${item?.id || ''} 的费用进度`)
|
||||
openPromptAssistant(item?.prompt || `查询 ${item?.id || ''} 的费用进度`, SESSION_TYPE_EXPENSE)
|
||||
}
|
||||
|
||||
function openCapabilityAssistant(item) {
|
||||
|
||||
@@ -99,29 +99,33 @@ function handleCancel() {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.shared-confirm-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10020;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
background: rgba(15, 23, 42, 0.32);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.shared-confirm-card {
|
||||
width: min(480px, 100%);
|
||||
.shared-confirm-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10020;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 20px;
|
||||
background: rgba(15, 23, 42, 0.32);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.shared-confirm-card {
|
||||
width: min(480px, calc(100vw - 40px));
|
||||
max-height: calc(100vh - 40px);
|
||||
max-height: calc(100dvh - 40px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(var(--theme-primary-rgb, 58, 124, 165), 0.14);
|
||||
border-radius: 8px;
|
||||
border-radius: 4px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(var(--theme-primary-rgb, 58, 124, 165), 0.10), transparent 36%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(247, 250, 252, 0.98));
|
||||
box-shadow: 0 28px 56px rgba(15, 23, 42, 0.18);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shared-confirm-badge {
|
||||
@@ -165,12 +169,18 @@ function handleCancel() {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.shared-confirm-body {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.shared-confirm-body {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
min-height: 0;
|
||||
max-height: min(380px, calc(100dvh - 300px));
|
||||
overflow-y: auto;
|
||||
padding-right: 2px;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.shared-confirm-actions {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
@@ -257,12 +267,22 @@ function handleCancel() {
|
||||
|
||||
.shared-confirm-card--compact {
|
||||
width: min(360px, 100%);
|
||||
max-height: calc(100vh - 36px);
|
||||
max-height: calc(100dvh - 36px);
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.shared-confirm-card--review {
|
||||
width: min(560px, calc(100vw - 40px));
|
||||
}
|
||||
|
||||
.shared-confirm-card--review .shared-confirm-body {
|
||||
max-height: min(420px, calc(100dvh - 292px));
|
||||
}
|
||||
|
||||
.shared-confirm-card--compact h4 {
|
||||
font-size: 15px;
|
||||
line-height: 1.35;
|
||||
@@ -287,16 +307,23 @@ function handleCancel() {
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.shared-confirm-mask {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.shared-confirm-card {
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.shared-confirm-card h4 {
|
||||
font-size: 19px;
|
||||
|
||||
.shared-confirm-card {
|
||||
width: min(100%, calc(100vw - 28px));
|
||||
max-height: calc(100vh - 28px);
|
||||
max-height: calc(100dvh - 28px);
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.shared-confirm-body {
|
||||
max-height: min(360px, calc(100dvh - 260px));
|
||||
}
|
||||
|
||||
.shared-confirm-card h4 {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.shared-confirm-actions {
|
||||
|
||||
@@ -31,22 +31,21 @@ const iconStyle = computed(() => iconMeta.value.style)
|
||||
<style scoped>
|
||||
.workbench-list-icon {
|
||||
position: relative;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
width: var(--workbench-list-icon-size, 48px);
|
||||
height: var(--workbench-list-icon-size, 48px);
|
||||
flex-shrink: 0;
|
||||
color: var(--icon-color, var(--theme-primary));
|
||||
}
|
||||
|
||||
.workbench-list-icon__halo {
|
||||
position: absolute;
|
||||
inset: -3px;
|
||||
border-radius: 20px;
|
||||
background: radial-gradient(
|
||||
circle at 50% 40%,
|
||||
color-mix(in srgb, var(--icon-color, var(--theme-primary)) 24%, transparent) 0%,
|
||||
transparent 72%
|
||||
);
|
||||
opacity: 0.7;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
border-radius: 2px;
|
||||
background: color-mix(in srgb, var(--icon-color, var(--theme-primary)) 78%, #ffffff);
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.workbench-list-icon__panel {
|
||||
@@ -57,26 +56,25 @@ const iconStyle = computed(() => iconMeta.value.style)
|
||||
display: grid;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
border-radius: 18px;
|
||||
border: 1px solid color-mix(in srgb, var(--icon-color, var(--theme-primary)) 18%, var(--line, #e2e8f0));
|
||||
border-radius: 4px;
|
||||
border: 1px solid color-mix(in srgb, var(--icon-color, var(--theme-primary)) 22%, var(--line, #e2e8f0));
|
||||
background:
|
||||
radial-gradient(circle at 24% 16%, rgba(255, 255, 255, 0.98), transparent 46%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(255, 255, 255, 0.44)),
|
||||
linear-gradient(
|
||||
160deg,
|
||||
color-mix(in srgb, var(--icon-accent, var(--theme-primary-soft)) 72%, #fff) 0%,
|
||||
#fff 44%,
|
||||
color-mix(in srgb, var(--icon-color, var(--theme-primary)) 7%, var(--surface-soft, #f8fafc)) 100%
|
||||
135deg,
|
||||
color-mix(in srgb, var(--icon-accent, var(--theme-primary-soft)) 64%, #fff) 0%,
|
||||
#fff 52%,
|
||||
color-mix(in srgb, var(--icon-color, var(--theme-primary)) 8%, var(--surface-soft, #f8fafc)) 100%
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.98),
|
||||
0 1px 2px rgba(15, 23, 42, 0.04),
|
||||
0 10px 20px color-mix(in srgb, var(--icon-color, var(--theme-primary)) 12%, transparent);
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9),
|
||||
0 1px 2px rgba(15, 23, 42, 0.045);
|
||||
}
|
||||
|
||||
.workbench-list-icon__shine {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.55), transparent 38%);
|
||||
background: linear-gradient(110deg, rgba(255, 255, 255, 0.42), transparent 44%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -85,16 +83,15 @@ const iconStyle = computed(() => iconMeta.value.style)
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
width: var(--workbench-list-icon-art-size, 28px);
|
||||
height: var(--workbench-list-icon-art-size, 28px);
|
||||
}
|
||||
|
||||
.workbench-list-icon__art :deep(.workbench-heroicon) {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
width: var(--workbench-list-icon-art-size, 28px);
|
||||
height: var(--workbench-list-icon-art-size, 28px);
|
||||
display: block;
|
||||
color: color-mix(in srgb, var(--icon-color, var(--theme-primary)) 86%, var(--theme-primary-active));
|
||||
filter: drop-shadow(0 2px 5px color-mix(in srgb, var(--icon-color, var(--theme-primary)) 18%, transparent));
|
||||
}
|
||||
|
||||
.workbench-list-icon--outline .workbench-list-icon__art :deep(.workbench-heroicon) {
|
||||
|
||||
@@ -10,11 +10,43 @@
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div class="message-bubble" :class="ui.buildMessageBubbleClass(message)">
|
||||
<div class="message-stack">
|
||||
<details
|
||||
v-if="message.role === 'assistant' && message.stewardPlan && (message.stewardPlan.streamStatus === 'streaming' || message.stewardPlan.thinkingEvents?.length)"
|
||||
class="steward-intent-bubble"
|
||||
:open="message.stewardPlan.streamStatus === 'streaming'"
|
||||
aria-label="小财管家意图识别智能体"
|
||||
>
|
||||
<summary>
|
||||
<span>
|
||||
<i class="mdi mdi-brain"></i>
|
||||
意图识别智能体
|
||||
</span>
|
||||
<small>{{ message.stewardPlan.streamStatus === 'streaming' ? '识别中' : `${message.stewardPlan.thinkingEvents?.length || 0} 步` }}</small>
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</summary>
|
||||
<ol v-if="message.stewardPlan.thinkingEvents?.length" class="steward-intent-event-list">
|
||||
<li
|
||||
v-for="event in (message.stewardPlan.thinkingEvents || []).slice(0, message.stewardPlan.visibleThinkingEventCount || message.stewardPlan.thinkingEvents?.length || 0)"
|
||||
:key="`${message.id}-${event.eventId}`"
|
||||
>
|
||||
<strong>{{ event.title }}</strong>
|
||||
<span>{{ event.content }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
<p v-else class="steward-intent-empty">正在建立任务上下文...</p>
|
||||
</details>
|
||||
|
||||
<div
|
||||
v-if="!message.stewardPlan || message.stewardPlan.streamStatus !== 'streaming' || message.text"
|
||||
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"
|
||||
@@ -40,6 +72,59 @@
|
||||
:report="message.budgetReport"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="message.role === 'assistant' && message.stewardPlan && message.stewardPlan.streamStatus !== 'streaming'"
|
||||
class="steward-plan-block"
|
||||
role="group"
|
||||
aria-label="小财管家任务计划"
|
||||
>
|
||||
<div v-if="message.stewardPlan.tasks?.length" class="steward-task-list">
|
||||
<article
|
||||
v-for="task in message.stewardPlan.tasks"
|
||||
:key="`${message.id}-${task.taskId}`"
|
||||
class="steward-task-card"
|
||||
>
|
||||
<header>
|
||||
<span>{{ task.taskTypeLabel }}</span>
|
||||
<small>{{ task.assignedAgentLabel }}</small>
|
||||
</header>
|
||||
<strong>{{ task.title }}</strong>
|
||||
<p>{{ task.summary }}</p>
|
||||
<div class="steward-task-meta">
|
||||
<span>置信度 {{ Math.round((task.confidence || 0) * 100) }}%</span>
|
||||
<span v-if="task.missingFields?.length">待补充 {{ task.missingFields.join('、') }}</span>
|
||||
<span v-else>字段已齐备</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-if="message.stewardPlan.attachmentGroups?.length" class="steward-attachment-list">
|
||||
<article
|
||||
v-for="group in message.stewardPlan.attachmentGroups"
|
||||
:key="`${message.id}-${group.groupId}`"
|
||||
class="steward-attachment-card"
|
||||
>
|
||||
<header>
|
||||
<span>{{ group.sceneLabel }}</span>
|
||||
<small>{{ Math.round((group.confidence || 0) * 100) }}%</small>
|
||||
</header>
|
||||
<p>{{ group.rationale }}</p>
|
||||
<div class="steward-attachment-chip-row">
|
||||
<span
|
||||
v-for="name in group.attachmentNames"
|
||||
:key="`${group.groupId}-in-${name}`"
|
||||
class="steward-attachment-chip include"
|
||||
>{{ name }}</span>
|
||||
<span
|
||||
v-for="name in group.excludedAttachmentNames"
|
||||
:key="`${group.groupId}-out-${name}`"
|
||||
class="steward-attachment-chip exclude"
|
||||
>排除:{{ name }}</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="message.role === 'assistant' && message.applicationPreview"
|
||||
class="application-preview-table"
|
||||
@@ -472,6 +557,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="ui.isOperationFeedbackVisible(message)"
|
||||
|
||||
Reference in New Issue
Block a user