fix: 优化报销创建页面样式与洞察面板交互
修复侧边栏和审计视图样式细节,完善差旅报销洞察面板和消息 组件布局,优化报销创建页面会话管理和流程状态持久化,增强 申请预览工具函数和导航图标,补充单元测试。
This commit is contained in:
@@ -163,16 +163,19 @@ import {
|
||||
HOT_KNOWLEDGE_QUESTIONS,
|
||||
INTENT_LABELS,
|
||||
SCENARIO_LABELS,
|
||||
SESSION_TYPE_BUDGET,
|
||||
SESSION_TYPE_APPLICATION,
|
||||
SESSION_TYPE_APPROVAL,
|
||||
SESSION_TYPE_EXPENSE,
|
||||
SESSION_TYPE_KNOWLEDGE,
|
||||
canUseBudgetAssistantSession,
|
||||
aiAvatar,
|
||||
buildExpenseIntentConfirmationMessage,
|
||||
buildExpenseSceneSelectionMessage,
|
||||
buildMessageMeta,
|
||||
buildWelcomeInsight,
|
||||
createMessage,
|
||||
filterAssistantSessionModes,
|
||||
resolveAssistantSessionMode,
|
||||
resolveKnowledgeRankLabel,
|
||||
resolveKnowledgeRankTone,
|
||||
@@ -610,11 +613,12 @@ export default {
|
||||
const isKnowledgeSession = computed(() => activeSessionType.value === SESSION_TYPE_KNOWLEDGE)
|
||||
const isApplicationSession = computed(() => activeSessionType.value === SESSION_TYPE_APPLICATION)
|
||||
const activeAssistantMode = computed(() => resolveAssistantSessionMode(activeSessionType.value))
|
||||
const assistantHeaderTitle = computed(() => activeAssistantMode.value?.label || '财务助手')
|
||||
const assistantHeaderDescription = computed(() => activeAssistantMode.value?.description || '个人财务中心')
|
||||
const assistantHeaderTitle = computed(() => '个人工作台')
|
||||
const assistantHeaderDescription = computed(() => '个人工作窗,一站式费控解决枢纽')
|
||||
const {
|
||||
flowRunId,
|
||||
flowSteps,
|
||||
visibleFlowSteps,
|
||||
flowRefreshBusy,
|
||||
completedFlowStepCount,
|
||||
flowOverallStatusTone,
|
||||
@@ -1084,7 +1088,7 @@ export default {
|
||||
const isReviewOverviewDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_REVIEW)
|
||||
|
||||
const shortcuts = computed(() =>
|
||||
ASSISTANT_SESSION_MODE_OPTIONS.map((mode) => ({
|
||||
filterAssistantSessionModes(ASSISTANT_SESSION_MODE_OPTIONS, currentUser.value).map((mode) => ({
|
||||
label: mode.label,
|
||||
icon: mode.icon,
|
||||
action: 'switch_view',
|
||||
@@ -1099,7 +1103,11 @@ export default {
|
||||
// reviewDrawerMode.value = resolveReviewRiskBriefs(payload).length
|
||||
// ? REVIEW_DRAWER_MODE_RISK
|
||||
// : REVIEW_DRAWER_MODE_REVIEW
|
||||
const shouldKeepFlowDrawer = reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW && flowSteps.value.length > 0
|
||||
resetReviewDrawerFromPayload(payload)
|
||||
if (shouldKeepFlowDrawer) {
|
||||
reviewDrawerMode.value = REVIEW_DRAWER_MODE_FLOW
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
@@ -1359,6 +1367,10 @@ export default {
|
||||
|
||||
async function runShortcut(shortcut) {
|
||||
if (shortcut?.action === 'switch_view' && shortcut?.targetSessionType) {
|
||||
if (shortcut.targetSessionType === SESSION_TYPE_BUDGET && !canUseBudgetAssistantSession(currentUser.value)) {
|
||||
toast('目前暂无权限访问预算编制助手')
|
||||
return
|
||||
}
|
||||
if (shortcut.active) {
|
||||
return
|
||||
}
|
||||
@@ -1444,6 +1456,10 @@ export default {
|
||||
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
||||
const targetSessionType = String(actionPayload.session_type || '').trim()
|
||||
if (!targetSessionType) return
|
||||
if (targetSessionType === SESSION_TYPE_BUDGET && !canUseBudgetAssistantSession(currentUser.value)) {
|
||||
toast('目前暂无权限访问预算编制助手')
|
||||
return
|
||||
}
|
||||
const carryText = String(actionPayload.carry_text || '').trim()
|
||||
const carryFiles = actionPayload.carry_files ? Array.from(attachedFiles.value || []) : []
|
||||
if (!lockSuggestedActionMessage(message, action)) return
|
||||
@@ -1714,6 +1730,14 @@ export default {
|
||||
return buildApplicationPreviewFooterMessage(message.applicationPreview)
|
||||
}
|
||||
|
||||
function resolveApplicationPreviewMissingFields(message) {
|
||||
if (!message?.applicationPreview) {
|
||||
return []
|
||||
}
|
||||
const normalizedPreview = normalizeApplicationPreview(message.applicationPreview)
|
||||
return Array.isArray(normalizedPreview.missingFields) ? normalizedPreview.missingFields : []
|
||||
}
|
||||
|
||||
function openApplicationSubmitConfirm(message) {
|
||||
if (!message) {
|
||||
return
|
||||
@@ -2128,6 +2152,7 @@ export default {
|
||||
resolveApplicationPreviewRows,
|
||||
resolveApplicationPreviewEditorControl,
|
||||
resolveApplicationPreviewEditorOptions,
|
||||
resolveApplicationPreviewMissingFields,
|
||||
isApplicationPreviewEditing,
|
||||
openApplicationPreviewEditor,
|
||||
commitApplicationPreviewEditor,
|
||||
@@ -2193,6 +2218,7 @@ export default {
|
||||
flowRefreshBusy: flowRefreshBusy.value,
|
||||
refreshFlowRunDetail,
|
||||
flowSteps: flowSteps.value,
|
||||
visibleFlowSteps: visibleFlowSteps.value,
|
||||
resolveFlowStepStatusLabel,
|
||||
formatFlowStepDuration,
|
||||
resolveFlowStepDetail,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { buildSuggestedActionKey } from '../../utils/suggestedActionKey.js'
|
||||
import { normalizeExpenseQueryPayload } from './travelReimbursementExpenseQueryModel.js'
|
||||
import { buildAgentInsight, buildReviewFilePreviewsFromReviewPayload } from './travelReimbursementAttachmentModel.js'
|
||||
import { resolveExpenseTypeLabel } from './travelReimbursementReviewModel.js'
|
||||
import { isBudgetMonitorUser, isExecutiveUser } from '../../utils/accessControl.js'
|
||||
import {
|
||||
GUIDED_ACTION_OPEN_TRAVEL_CALCULATOR,
|
||||
GUIDED_ACTION_START_APPLICATION,
|
||||
@@ -13,12 +14,14 @@ export const SESSION_TYPE_EXPENSE = 'expense'
|
||||
export const SESSION_TYPE_APPLICATION = 'application'
|
||||
export const SESSION_TYPE_APPROVAL = 'approval'
|
||||
export const SESSION_TYPE_KNOWLEDGE = 'knowledge'
|
||||
export const SESSION_TYPE_BUDGET = 'budget'
|
||||
|
||||
export const ASSISTANT_SESSION_TYPES = [
|
||||
SESSION_TYPE_APPLICATION,
|
||||
SESSION_TYPE_EXPENSE,
|
||||
SESSION_TYPE_APPROVAL,
|
||||
SESSION_TYPE_KNOWLEDGE
|
||||
SESSION_TYPE_KNOWLEDGE,
|
||||
SESSION_TYPE_BUDGET
|
||||
]
|
||||
|
||||
export const ASSISTANT_SESSION_MODE_OPTIONS = [
|
||||
@@ -45,9 +48,39 @@ export const ASSISTANT_SESSION_MODE_OPTIONS = [
|
||||
label: '财务知识助手',
|
||||
icon: 'mdi mdi-book-open-page-variant-outline',
|
||||
description: '只处理财务制度、标准规则、票据要求和政策解释'
|
||||
},
|
||||
{
|
||||
key: SESSION_TYPE_BUDGET,
|
||||
label: '预算编制助手',
|
||||
icon: 'mdi mdi-calculator-variant-outline',
|
||||
description: '帮助你进行预算编制与预算相关问题的整理'
|
||||
}
|
||||
]
|
||||
|
||||
export function canUseBudgetAssistantSession(user = null) {
|
||||
return Boolean(isBudgetMonitorUser(user) || isExecutiveUser(user))
|
||||
}
|
||||
|
||||
function canUseAssistantSessionType(sessionType, user = null) {
|
||||
const normalized = String(sessionType || '').trim()
|
||||
if (normalized === SESSION_TYPE_BUDGET) {
|
||||
return canUseBudgetAssistantSession(user)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function filterAssistantSessionModes(sessionModes = [], user = null) {
|
||||
return Array.isArray(sessionModes)
|
||||
? sessionModes.filter((mode) => canUseAssistantSessionType(mode?.key, user))
|
||||
: []
|
||||
}
|
||||
|
||||
export function filterAssistantSessionTypes(sessionTypes = [], user = null) {
|
||||
return Array.isArray(sessionTypes)
|
||||
? sessionTypes.filter((sessionType) => canUseAssistantSessionType(String(sessionType || '').trim(), user))
|
||||
: []
|
||||
}
|
||||
|
||||
export function normalizeAssistantSessionType(sessionType, fallback = SESSION_TYPE_EXPENSE) {
|
||||
const normalized = String(sessionType || '').trim()
|
||||
if (ASSISTANT_SESSION_TYPES.includes(normalized)) {
|
||||
|
||||
@@ -100,6 +100,16 @@ export function useTravelReimbursementFlow({
|
||||
const runningFlowStep = computed(
|
||||
() => flowSteps.value.find((step) => step.status === FLOW_STEP_STATUS_RUNNING) || null
|
||||
)
|
||||
const visibleFlowSteps = computed(() => {
|
||||
const visibleSteps = []
|
||||
for (const step of flowSteps.value) {
|
||||
visibleSteps.push(step)
|
||||
if (step.status !== FLOW_STEP_STATUS_COMPLETED) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return visibleSteps
|
||||
})
|
||||
const flowOverallStatusTone = computed(() => {
|
||||
if (flowSteps.value.some((step) => step.status === FLOW_STEP_STATUS_FAILED)) {
|
||||
return 'failed'
|
||||
@@ -209,7 +219,8 @@ export function useTravelReimbursementFlow({
|
||||
durationMs: normalizedPatch.durationMs ?? null,
|
||||
startedAt: normalizedPatch.startedAt || 0,
|
||||
finishedAt: normalizedPatch.finishedAt || 0,
|
||||
error: normalizedPatch.error || ''
|
||||
error: normalizedPatch.error || '',
|
||||
deferredCompletion: Boolean(normalizedPatch.deferredCompletion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,8 +273,15 @@ export function useTravelReimbursementFlow({
|
||||
startedAt,
|
||||
finishedAt: now,
|
||||
durationMs: hasExplicitDuration ? explicitDuration : Math.max(0, now - startedAt),
|
||||
error: ''
|
||||
error: '',
|
||||
deferredCompletion: false
|
||||
})
|
||||
if (
|
||||
flowSteps.value.length
|
||||
&& flowSteps.value.every((step) => [FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED].includes(step.status))
|
||||
) {
|
||||
flowFinishedAt.value = now
|
||||
}
|
||||
}
|
||||
|
||||
function failFlowStep(key, detail = '', error = '', patch = {}) {
|
||||
@@ -291,19 +309,18 @@ export function useTravelReimbursementFlow({
|
||||
const normalizedDuration = Number(durationMs)
|
||||
const hasMeasuredDuration = Number.isFinite(normalizedDuration) && normalizedDuration > 0
|
||||
if (!currentStep || currentStep.status === FLOW_STEP_STATUS_PENDING) {
|
||||
if (!hasMeasuredDuration && !currentStep?.startedAt) {
|
||||
upsertFlowStep(key, {
|
||||
...patch,
|
||||
status: FLOW_STEP_STATUS_COMPLETED,
|
||||
detail: detail || findFlowDefinition(key)?.completedText || '',
|
||||
startedAt: 0,
|
||||
finishedAt: 0,
|
||||
durationMs: null,
|
||||
error: ''
|
||||
})
|
||||
return
|
||||
}
|
||||
startFlowStep(key, patch)
|
||||
const revealOrder = flowSteps.value.length
|
||||
startFlowStep(key, { ...patch, deferredCompletion: true })
|
||||
const completionTimer = window.setTimeout(() => {
|
||||
completeFlowStep(
|
||||
key,
|
||||
detail || findFlowDefinition(key)?.completedText || '',
|
||||
hasMeasuredDuration ? normalizedDuration : null,
|
||||
{ deferredCompletion: false }
|
||||
)
|
||||
}, 280 + Math.min(revealOrder, 4) * 180)
|
||||
flowSimulationTimers.push(completionTimer)
|
||||
return
|
||||
}
|
||||
completeFlowStep(key, detail, hasMeasuredDuration ? normalizedDuration : null, patch)
|
||||
}
|
||||
@@ -606,16 +623,18 @@ export function useTravelReimbursementFlow({
|
||||
const sceneSelectionPending = isExpenseSceneSelectionResult(payload)
|
||||
flowSteps.value
|
||||
.filter((step) => ![FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED].includes(step.status))
|
||||
.filter((step) => !step.deferredCompletion)
|
||||
.forEach((step) => {
|
||||
const detail = sceneSelectionPending && step.key === 'expense-scene-selection'
|
||||
? '已暂停后续识别,请先在主对话中选择报销场景。'
|
||||
: resolveFlowStepDetail({ ...step, status: FLOW_STEP_STATUS_COMPLETED })
|
||||
completeFlowStep(step.key, detail)
|
||||
})
|
||||
flowFinishedAt.value = Date.now()
|
||||
if (reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW && !sceneSelectionPending) {
|
||||
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
||||
}
|
||||
flowFinishedAt.value = flowSteps.value.some(
|
||||
(step) => ![FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED].includes(step.status)
|
||||
)
|
||||
? 0
|
||||
: Date.now()
|
||||
}
|
||||
|
||||
async function refreshFlowRunDetail() {
|
||||
@@ -679,6 +698,7 @@ export function useTravelReimbursementFlow({
|
||||
flowStartedAt,
|
||||
flowFinishedAt,
|
||||
flowSteps,
|
||||
visibleFlowSteps,
|
||||
flowRefreshBusy,
|
||||
flowTick,
|
||||
completedFlowStepCount,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from './travelReimbursementAttachmentModel.js'
|
||||
import {
|
||||
ASSISTANT_SESSION_TYPES,
|
||||
filterAssistantSessionTypes,
|
||||
SESSION_TYPE_APPLICATION,
|
||||
SESSION_TYPE_EXPENSE,
|
||||
buildInitialInsightFromConversation,
|
||||
@@ -43,6 +44,25 @@ export function useTravelReimbursementSessionState({
|
||||
scrollToBottom,
|
||||
getSessionRuntimeRefs = () => ({})
|
||||
}) {
|
||||
function resolveAccessibleSessionTypes() {
|
||||
return filterAssistantSessionTypes(ASSISTANT_SESSION_TYPES, currentUser.value)
|
||||
}
|
||||
|
||||
function resolveAccessibleSessionType(rawSessionType, fallback = resolveDefaultSessionTypeFromEntry()) {
|
||||
const normalized = normalizeAssistantSessionType(rawSessionType, fallback)
|
||||
const accessible = resolveAccessibleSessionTypes()
|
||||
if (accessible.includes(normalized)) {
|
||||
return normalized
|
||||
}
|
||||
|
||||
const fallbackNormalized = String(fallback || '').trim()
|
||||
if (accessible.includes(fallbackNormalized)) {
|
||||
return fallbackNormalized
|
||||
}
|
||||
|
||||
return accessible[0] || SESSION_TYPE_APPLICATION
|
||||
}
|
||||
|
||||
function resolveDefaultSessionTypeFromEntry() {
|
||||
return props.entrySource === 'application' ? SESSION_TYPE_APPLICATION : SESSION_TYPE_EXPENSE
|
||||
}
|
||||
@@ -65,7 +85,10 @@ export function useTravelReimbursementSessionState({
|
||||
}
|
||||
|
||||
function buildConversationSessionState(conversation, fallbackSessionType = resolveDefaultSessionTypeFromEntry()) {
|
||||
const sessionType = resolveInitialSessionType(conversation, fallbackSessionType)
|
||||
const sessionType = resolveAccessibleSessionType(
|
||||
resolveInitialSessionType(conversation, fallbackSessionType),
|
||||
fallbackSessionType
|
||||
)
|
||||
const restoredMessages = refreshWelcomeQuickActions(normalizeInitialConversationMessages(conversation), sessionType)
|
||||
const initialInsight = buildInitialInsightFromConversation(conversation)
|
||||
const restoredReviewFilePreviews = buildReviewFilePreviewsFromMessages(restoredMessages)
|
||||
@@ -90,7 +113,10 @@ export function useTravelReimbursementSessionState({
|
||||
}
|
||||
|
||||
function buildEmptySessionState(sessionType) {
|
||||
const normalizedSessionType = normalizeAssistantSessionType(sessionType, resolveDefaultSessionTypeFromEntry())
|
||||
const normalizedSessionType = resolveAccessibleSessionType(
|
||||
sessionType,
|
||||
resolveDefaultSessionTypeFromEntry()
|
||||
)
|
||||
return {
|
||||
sessionType: normalizedSessionType,
|
||||
messages: [
|
||||
@@ -120,7 +146,7 @@ export function useTravelReimbursementSessionState({
|
||||
return null
|
||||
}
|
||||
|
||||
const sessionType = normalizeAssistantSessionType(
|
||||
const sessionType = resolveAccessibleSessionType(
|
||||
state.sessionType || snapshot.sessionType || fallbackSessionType,
|
||||
fallbackSessionType
|
||||
)
|
||||
@@ -159,9 +185,12 @@ export function useTravelReimbursementSessionState({
|
||||
}
|
||||
|
||||
const defaultInitialSessionType = resolveDefaultSessionTypeFromEntry()
|
||||
const initialSessionType = props.initialConversation
|
||||
? resolveInitialSessionType(props.initialConversation, defaultInitialSessionType)
|
||||
: defaultInitialSessionType
|
||||
const initialSessionType = resolveAccessibleSessionType(
|
||||
props.initialConversation
|
||||
? resolveInitialSessionType(props.initialConversation, defaultInitialSessionType)
|
||||
: defaultInitialSessionType,
|
||||
defaultInitialSessionType
|
||||
)
|
||||
const shouldPersistLocalSnapshot = props.entrySource !== 'detail'
|
||||
const conversationInitialState = props.initialConversation
|
||||
? buildConversationSessionState(props.initialConversation, initialSessionType)
|
||||
@@ -188,7 +217,7 @@ export function useTravelReimbursementSessionState({
|
||||
const draftClaimId = ref(initialSessionState.draftClaimId)
|
||||
const reviewFilePreviews = ref(initialSessionState.reviewFilePreviews)
|
||||
const sessionSnapshots = ref(
|
||||
ASSISTANT_SESSION_TYPES.reduce((result, sessionType) => {
|
||||
resolveAccessibleSessionTypes().reduce((result, sessionType) => {
|
||||
result[sessionType] = null
|
||||
return result
|
||||
}, {})
|
||||
@@ -202,7 +231,7 @@ export function useTravelReimbursementSessionState({
|
||||
function buildPersistableSessionState(sessionState) {
|
||||
const state = sessionState || captureCurrentSessionState()
|
||||
return {
|
||||
sessionType: normalizeAssistantSessionType(state.sessionType, resolveDefaultSessionTypeFromEntry()),
|
||||
sessionType: resolveAccessibleSessionType(state.sessionType, resolveDefaultSessionTypeFromEntry()),
|
||||
messages: serializeSessionMessages(state.messages),
|
||||
conversationId: String(state.conversationId || '').trim(),
|
||||
draftClaimId: String(state.draftClaimId || '').trim(),
|
||||
@@ -258,7 +287,10 @@ export function useTravelReimbursementSessionState({
|
||||
function applySessionState(sessionState) {
|
||||
const runtimeRefs = getSessionRuntimeRefs()
|
||||
const nextState = sessionState || buildEmptySessionState(activeSessionType.value)
|
||||
activeSessionType.value = normalizeAssistantSessionType(nextState.sessionType, resolveDefaultSessionTypeFromEntry())
|
||||
activeSessionType.value = resolveAccessibleSessionType(
|
||||
nextState.sessionType,
|
||||
resolveDefaultSessionTypeFromEntry()
|
||||
)
|
||||
messages.value = Array.isArray(nextState.messages) && nextState.messages.length
|
||||
? nextState.messages
|
||||
: [
|
||||
@@ -301,7 +333,7 @@ export function useTravelReimbursementSessionState({
|
||||
}
|
||||
|
||||
async function loadLatestSessionState(targetSessionType) {
|
||||
const normalizedTarget = normalizeAssistantSessionType(targetSessionType, resolveDefaultSessionTypeFromEntry())
|
||||
const normalizedTarget = resolveAccessibleSessionType(targetSessionType, resolveDefaultSessionTypeFromEntry())
|
||||
const payload = await fetchLatestConversation(resolveCurrentUserId(), normalizedTarget, {
|
||||
preferRecoverable: normalizedTarget === SESSION_TYPE_EXPENSE
|
||||
})
|
||||
@@ -312,7 +344,7 @@ export function useTravelReimbursementSessionState({
|
||||
}
|
||||
|
||||
async function switchSessionType(targetSessionType) {
|
||||
const normalizedTarget = normalizeAssistantSessionType(targetSessionType, resolveDefaultSessionTypeFromEntry())
|
||||
const normalizedTarget = resolveAccessibleSessionType(targetSessionType, resolveDefaultSessionTypeFromEntry())
|
||||
if (normalizedTarget === activeSessionType.value || sessionSwitchBusy.value) {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user