feat(web): update travel request and reimbursement views

This commit is contained in:
caoxiaozhu
2026-05-14 07:10:46 +00:00
parent 476d5fdf93
commit 4a72b977ba
4 changed files with 109 additions and 147 deletions

View File

@@ -3,6 +3,40 @@
<Transition name="assistant-modal">
<div class="assistant-overlay">
<section class="assistant-modal">
<div class="assistant-header-actions">
<button
type="button"
class="assistant-toggle-btn"
:class="{ disabled: !hasInsightPanelContent }"
:disabled="!hasInsightPanelContent || sessionSwitchBusy"
:title="insightPanelToggleLabel"
:aria-label="insightPanelToggleLabel"
@click="toggleInsightPanel"
>
<i :class="showInsightPanel ? 'mdi mdi-arrow-collapse-right' : 'mdi mdi-arrow-expand-right'"></i>
</button>
<button
type="button"
class="session-trash-btn"
:disabled="!canDeleteCurrentSession || submitting || reviewActionBusy || deleteSessionBusy || sessionSwitchBusy"
title="删除当前会话"
aria-label="删除当前会话"
@click="openDeleteSessionDialog"
>
<i class="mdi mdi-delete-outline"></i>
</button>
<button
class="assistant-close-btn"
type="button"
title="关闭工作台"
aria-label="关闭对话工作台"
@pointerdown.stop.prevent="requestCloseWorkbench"
@click.stop.prevent="requestCloseWorkbench"
>
<i class="mdi mdi-close"></i>
</button>
</div>
<div class="assistant-modal-stage">
<header class="assistant-header">
<div class="assistant-header-main">
@@ -11,39 +45,6 @@
<p>个人工作台发起报销智能录入统一走这里右侧会根据你的意图实时切换状态视图</p>
</div>
</div>
<div class="assistant-header-actions">
<button
type="button"
class="assistant-toggle-btn"
:class="{ disabled: !hasInsightPanelContent }"
:disabled="!hasInsightPanelContent || sessionSwitchBusy"
:title="insightPanelToggleLabel"
:aria-label="insightPanelToggleLabel"
@click="toggleInsightPanel"
>
<i :class="showInsightPanel ? 'mdi mdi-arrow-collapse-right' : 'mdi mdi-arrow-expand-right'"></i>
</button>
<button
type="button"
class="session-trash-btn"
:disabled="!canDeleteCurrentSession || submitting || reviewActionBusy || deleteSessionBusy || sessionSwitchBusy"
title="删除当前会话"
aria-label="删除当前会话"
@click="openDeleteSessionDialog"
>
<i class="mdi mdi-delete-outline"></i>
</button>
<button
class="close-btn"
type="button"
title="关闭工作台"
aria-label="关闭对话工作台"
@click="requestCloseWorkbench"
>
<i class="mdi mdi-close"></i>
</button>
</div>
</header>
<div class="assistant-layout" :class="{ 'can-show-insight': hasInsightPanelContent, 'has-insight': showInsightPanel }">
@@ -882,22 +883,6 @@
@confirm="confirmDeleteCurrentSession"
/>
<ConfirmDialog
:open="leaveKnowledgeDialogOpen"
badge="离开问答"
badge-tone="warning"
title="离开页面会清除当前知识问答会话"
description="确认离开后,当前财务知识问答 session 会被清除且不再保留。刷新页面不算退出。"
cancel-text="继续停留"
confirm-text="确认离开"
busy-text="清理中..."
confirm-tone="danger"
confirm-icon="mdi mdi-exit-to-app"
:busy="leaveKnowledgeBusy"
@close="closeLeaveKnowledgeDialog"
@confirm="confirmLeaveKnowledgeSession"
/>
<ConfirmDialog
:open="reviewCancelDialogOpen"
badge="取消核对"

View File

@@ -3,40 +3,39 @@
<div class="approval-detail">
<div class="detail-scroll">
<article class="detail-hero panel">
<div class="hero-topline">
<div class="applicant-card">
<div class="portrait">{{ profile.avatar }}</div>
<div class="applicant-copy">
<h2>{{ profile.name }}</h2>
<p>{{ profile.position }}</p>
<div class="applicant-meta">
<div v-for="item in profile.facts" :key="item.label" class="applicant-meta-item">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
<div class="hero-banner">
<div class="hero-banner-main">
<div class="applicant-card">
<div class="portrait">
<img src="/assets/person.png" alt="" />
</div>
<div class="applicant-copy">
<div class="applicant-name-row">
<h2>{{ profile.name }}</h2>
<span class="identity-badge">{{ profile.identity }}</span>
</div>
<div class="applicant-meta-line">
<span><em>部门</em><strong>{{ profile.department }}</strong></span>
<span><em>职级</em><strong>{{ profile.grade }}</strong></span>
<span><em>直属上司</em><strong>{{ profile.manager }}</strong></span>
</div>
</div>
</div>
</div>
<div class="hero-stat-strip">
<div v-for="stat in heroStats" :key="stat.label" :class="['hero-stat', { emphasis: stat.emphasis }]">
<span>{{ stat.label }}</span>
<strong v-if="stat.kind === 'text'">{{ stat.value }}</strong>
<b v-else :class="[stat.className, stat.tone]">{{ stat.value }}</b>
<div class="hero-fact-grid">
<div v-for="item in heroFactItems" :key="item.key" class="hero-fact">
<div class="hero-fact-label">
<i v-if="item.icon" :class="item.icon"></i>
<span>{{ item.label }}</span>
</div>
<strong :class="item.valueClass">{{ item.value }}</strong>
</div>
</div>
</div>
</div>
</article>
<div class="hero-summary-panel">
<div v-for="item in heroSummaryItems" :key="item.label" class="hero-summary-item">
<div class="hero-summary-label">
<span class="hero-summary-icon"><i :class="item.icon"></i></span>
<span>{{ item.label }}</span>
</div>
<strong>{{ item.value }}</strong>
</div>
</div>
<article class="progress-card panel">
<div class="progress-block">
<div class="progress-head">
<h3>{{ isTravelRequest ? '差旅进度' : '报销进度' }}</h3>

View File

@@ -1976,10 +1976,8 @@ export default {
const reviewCancelDialogOpen = ref(false)
const reviewEditDialogOpen = ref(false)
const deleteSessionDialogOpen = ref(false)
const leaveKnowledgeDialogOpen = ref(false)
const reviewActionBusy = ref(false)
const deleteSessionBusy = ref(false)
const leaveKnowledgeBusy = ref(false)
const reviewEditFields = ref([])
const reviewActionMessageId = ref('')
const reviewInlineForm = ref(createEmptyInlineReviewState())
@@ -2035,6 +2033,7 @@ export default {
}
return labels[currentInsight.value.intent] ?? 'AI 处理中'
})
let knowledgeSessionResetPromise = Promise.resolve()
const canDeleteCurrentSession = computed(
() => Boolean(conversationId.value) || messages.value.some((item) => item.role === 'user')
)
@@ -2190,6 +2189,27 @@ export default {
return buildEmptySessionState(targetSessionType)
}
function resetKnowledgeSessionSnapshot() {
const emptyKnowledgeState = buildEmptySessionState(SESSION_TYPE_KNOWLEDGE)
sessionSnapshots.value[SESSION_TYPE_KNOWLEDGE] = emptyKnowledgeState
if (activeSessionType.value === SESSION_TYPE_KNOWLEDGE) {
applySessionState(emptyKnowledgeState)
}
}
function clearKnowledgeSessionOnEntry() {
resetKnowledgeSessionSnapshot()
knowledgeSessionResetPromise = clearUserConversations(resolveCurrentUserId(), SESSION_TYPE_KNOWLEDGE)
.catch((error) => {
console.warn('Failed to clear knowledge session on entry:', error)
})
.finally(() => {
resetKnowledgeSessionSnapshot()
})
return knowledgeSessionResetPromise
}
async function switchSessionType(targetSessionType) {
const normalizedTarget = String(targetSessionType || '').trim() || SESSION_TYPE_EXPENSE
if (normalizedTarget === activeSessionType.value || sessionSwitchBusy.value) {
@@ -2257,6 +2277,7 @@ export default {
)
onMounted(() => {
void clearKnowledgeSessionOnEntry()
currentInsight.value = currentInsight.value || buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value)
if (props.initialPrompt?.trim() || props.initialFiles.length) {
const initialMerge = mergeFilesWithLimit([], Array.from(props.initialFiles), MAX_ATTACHMENTS)
@@ -2585,15 +2606,6 @@ export default {
}
function requestCloseWorkbench() {
if (submitting.value || reviewActionBusy.value || deleteSessionBusy.value || leaveKnowledgeBusy.value || sessionSwitchBusy.value) {
return
}
if (isKnowledgeSession.value) {
leaveKnowledgeDialogOpen.value = true
return
}
emit('close')
}
@@ -2663,31 +2675,6 @@ export default {
}
}
function closeLeaveKnowledgeDialog() {
if (leaveKnowledgeBusy.value) {
return
}
leaveKnowledgeDialogOpen.value = false
}
async function confirmLeaveKnowledgeSession() {
if (leaveKnowledgeBusy.value || sessionSwitchBusy.value) {
return
}
leaveKnowledgeBusy.value = true
try {
await clearUserConversations(resolveCurrentUserId(), SESSION_TYPE_KNOWLEDGE)
sessionSnapshots.value[SESSION_TYPE_KNOWLEDGE] = buildEmptySessionState(SESSION_TYPE_KNOWLEDGE)
leaveKnowledgeDialogOpen.value = false
emit('close')
} catch (error) {
toast(error?.message || '清理知识问答会话失败,请稍后重试。')
} finally {
leaveKnowledgeBusy.value = false
}
}
async function saveInlineReviewChanges() {
if (!activeReviewPayload.value || !reviewHasUnsavedChanges.value || reviewActionBusy.value) return
@@ -3092,10 +3079,8 @@ export default {
reviewCancelDialogOpen,
reviewEditDialogOpen,
deleteSessionDialogOpen,
leaveKnowledgeDialogOpen,
reviewActionBusy,
deleteSessionBusy,
leaveKnowledgeBusy,
reviewEditFields,
documentPreviewDialog,
shortcuts,
@@ -3144,8 +3129,6 @@ export default {
openDeleteSessionDialog,
closeDeleteSessionDialog,
confirmDeleteCurrentSession,
closeLeaveKnowledgeDialog,
confirmLeaveKnowledgeSession,
openInlineReviewEditor,
closeInlineReviewEditor,
commitInlineReviewEditor,

View File

@@ -452,11 +452,6 @@ export default {
department: request.value.profileDepartment,
grade: request.value.profileGrade,
manager: request.value.profileManager,
facts: [
{ label: '部门', value: request.value.profileDepartment },
{ label: '职级', value: request.value.profileGrade },
{ label: '直属上司', value: request.value.profileManager }
],
avatar: request.value.profileAvatar
}))
@@ -484,43 +479,44 @@ export default {
{ immediate: true }
)
const heroStats = computed(() => [
const heroFactItems = computed(() => [
{
key: 'document',
label: '报销单号',
value: request.value.documentNo || request.value.id,
icon: 'mdi mdi-camera-outline',
valueClass: ''
},
{
key: 'date',
label: '日期',
value: request.value.applyTime || request.value.occurredDisplay,
icon: 'mdi mdi-calendar-month-outline',
valueClass: ''
},
{
key: 'amount',
label: '报销金额',
value: request.value.amountDisplay,
kind: 'text',
emphasis: true
icon: '',
valueClass: 'amount'
},
{
label: '报销类型',
key: 'type',
label: isTravelRequest.value ? '差旅类型' : '报销类型',
value: request.value.typeLabel,
kind: 'text'
icon: '',
valueClass: ''
},
{
label: '当前节点',
key: 'status',
label: '当前状态',
value: request.value.node,
kind: 'pill',
className: 'state-pill',
tone: request.value.approvalTone
},
{
label: '审批状态',
value: request.value.approval,
kind: 'pill',
className: 'approval-pill',
tone: request.value.approvalTone
icon: '',
valueClass: 'status'
}
])
const heroSummaryItems = computed(() => {
return [
{ label: '单号', value: request.value.id, icon: 'mdi mdi-pound-box-outline' },
{ label: '发生时间', value: request.value.occurredDisplay, icon: 'mdi mdi-calendar-month-outline' },
{ label: '费用明细', value: `${expenseItems.value.length}`, icon: 'mdi mdi-format-list-bulleted-square' },
{ label: '申请时间', value: request.value.applyTime, icon: 'mdi mdi-timer-sand' }
]
})
const progressSteps = computed(() =>
Array.isArray(request.value.progressSteps) && request.value.progressSteps.length
? request.value.progressSteps
@@ -1136,8 +1132,7 @@ export default {
handleExpenseFileChange,
handleSubmit,
hasExpenseRiskColumn,
heroStats,
heroSummaryItems,
heroFactItems,
isDraftRequest,
isTravelRequest,
locationInputPlaceholder,