feat(web): update travel request and reimbursement views
This commit is contained in:
@@ -3,15 +3,6 @@
|
|||||||
<Transition name="assistant-modal">
|
<Transition name="assistant-modal">
|
||||||
<div class="assistant-overlay">
|
<div class="assistant-overlay">
|
||||||
<section class="assistant-modal">
|
<section class="assistant-modal">
|
||||||
<div class="assistant-modal-stage">
|
|
||||||
<header class="assistant-header">
|
|
||||||
<div class="assistant-header-main">
|
|
||||||
<div>
|
|
||||||
<h2>财务AI工作台</h2>
|
|
||||||
<p>个人工作台、发起报销、智能录入统一走这里,右侧会根据你的意图实时切换状态视图。</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="assistant-header-actions">
|
<div class="assistant-header-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -35,15 +26,25 @@
|
|||||||
<i class="mdi mdi-delete-outline"></i>
|
<i class="mdi mdi-delete-outline"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="close-btn"
|
class="assistant-close-btn"
|
||||||
type="button"
|
type="button"
|
||||||
title="关闭工作台"
|
title="关闭工作台"
|
||||||
aria-label="关闭对话工作台"
|
aria-label="关闭对话工作台"
|
||||||
@click="requestCloseWorkbench"
|
@pointerdown.stop.prevent="requestCloseWorkbench"
|
||||||
|
@click.stop.prevent="requestCloseWorkbench"
|
||||||
>
|
>
|
||||||
<i class="mdi mdi-close"></i>
|
<i class="mdi mdi-close"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="assistant-modal-stage">
|
||||||
|
<header class="assistant-header">
|
||||||
|
<div class="assistant-header-main">
|
||||||
|
<div>
|
||||||
|
<h2>财务AI工作台</h2>
|
||||||
|
<p>个人工作台、发起报销、智能录入统一走这里,右侧会根据你的意图实时切换状态视图。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="assistant-layout" :class="{ 'can-show-insight': hasInsightPanelContent, 'has-insight': showInsightPanel }">
|
<div class="assistant-layout" :class="{ 'can-show-insight': hasInsightPanelContent, 'has-insight': showInsightPanel }">
|
||||||
@@ -882,22 +883,6 @@
|
|||||||
@confirm="confirmDeleteCurrentSession"
|
@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
|
<ConfirmDialog
|
||||||
:open="reviewCancelDialogOpen"
|
:open="reviewCancelDialogOpen"
|
||||||
badge="取消核对"
|
badge="取消核对"
|
||||||
|
|||||||
@@ -3,40 +3,39 @@
|
|||||||
<div class="approval-detail">
|
<div class="approval-detail">
|
||||||
<div class="detail-scroll">
|
<div class="detail-scroll">
|
||||||
<article class="detail-hero panel">
|
<article class="detail-hero panel">
|
||||||
<div class="hero-topline">
|
<div class="hero-banner">
|
||||||
|
<div class="hero-banner-main">
|
||||||
<div class="applicant-card">
|
<div class="applicant-card">
|
||||||
<div class="portrait">{{ profile.avatar }}</div>
|
<div class="portrait">
|
||||||
|
<img src="/assets/person.png" alt="" />
|
||||||
|
</div>
|
||||||
<div class="applicant-copy">
|
<div class="applicant-copy">
|
||||||
|
<div class="applicant-name-row">
|
||||||
<h2>{{ profile.name }}</h2>
|
<h2>{{ profile.name }}</h2>
|
||||||
<p>{{ profile.position }}</p>
|
<span class="identity-badge">{{ profile.identity }}</span>
|
||||||
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
<div class="hero-stat-strip">
|
<div class="hero-fact-grid">
|
||||||
<div v-for="stat in heroStats" :key="stat.label" :class="['hero-stat', { emphasis: stat.emphasis }]">
|
<div v-for="item in heroFactItems" :key="item.key" class="hero-fact">
|
||||||
<span>{{ stat.label }}</span>
|
<div class="hero-fact-label">
|
||||||
<strong v-if="stat.kind === 'text'">{{ stat.value }}</strong>
|
<i v-if="item.icon" :class="item.icon"></i>
|
||||||
<b v-else :class="[stat.className, stat.tone]">{{ stat.value }}</b>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
<span>{{ item.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
<strong>{{ item.value }}</strong>
|
<strong :class="item.valueClass">{{ item.value }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="progress-card panel">
|
||||||
<div class="progress-block">
|
<div class="progress-block">
|
||||||
<div class="progress-head">
|
<div class="progress-head">
|
||||||
<h3>{{ isTravelRequest ? '差旅进度' : '报销进度' }}</h3>
|
<h3>{{ isTravelRequest ? '差旅进度' : '报销进度' }}</h3>
|
||||||
|
|||||||
@@ -1976,10 +1976,8 @@ export default {
|
|||||||
const reviewCancelDialogOpen = ref(false)
|
const reviewCancelDialogOpen = ref(false)
|
||||||
const reviewEditDialogOpen = ref(false)
|
const reviewEditDialogOpen = ref(false)
|
||||||
const deleteSessionDialogOpen = ref(false)
|
const deleteSessionDialogOpen = ref(false)
|
||||||
const leaveKnowledgeDialogOpen = ref(false)
|
|
||||||
const reviewActionBusy = ref(false)
|
const reviewActionBusy = ref(false)
|
||||||
const deleteSessionBusy = ref(false)
|
const deleteSessionBusy = ref(false)
|
||||||
const leaveKnowledgeBusy = ref(false)
|
|
||||||
const reviewEditFields = ref([])
|
const reviewEditFields = ref([])
|
||||||
const reviewActionMessageId = ref('')
|
const reviewActionMessageId = ref('')
|
||||||
const reviewInlineForm = ref(createEmptyInlineReviewState())
|
const reviewInlineForm = ref(createEmptyInlineReviewState())
|
||||||
@@ -2035,6 +2033,7 @@ export default {
|
|||||||
}
|
}
|
||||||
return labels[currentInsight.value.intent] ?? 'AI 处理中'
|
return labels[currentInsight.value.intent] ?? 'AI 处理中'
|
||||||
})
|
})
|
||||||
|
let knowledgeSessionResetPromise = Promise.resolve()
|
||||||
const canDeleteCurrentSession = computed(
|
const canDeleteCurrentSession = computed(
|
||||||
() => Boolean(conversationId.value) || messages.value.some((item) => item.role === 'user')
|
() => Boolean(conversationId.value) || messages.value.some((item) => item.role === 'user')
|
||||||
)
|
)
|
||||||
@@ -2190,6 +2189,27 @@ export default {
|
|||||||
return buildEmptySessionState(targetSessionType)
|
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) {
|
async function switchSessionType(targetSessionType) {
|
||||||
const normalizedTarget = String(targetSessionType || '').trim() || SESSION_TYPE_EXPENSE
|
const normalizedTarget = String(targetSessionType || '').trim() || SESSION_TYPE_EXPENSE
|
||||||
if (normalizedTarget === activeSessionType.value || sessionSwitchBusy.value) {
|
if (normalizedTarget === activeSessionType.value || sessionSwitchBusy.value) {
|
||||||
@@ -2257,6 +2277,7 @@ export default {
|
|||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
void clearKnowledgeSessionOnEntry()
|
||||||
currentInsight.value = currentInsight.value || buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value)
|
currentInsight.value = currentInsight.value || buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value)
|
||||||
if (props.initialPrompt?.trim() || props.initialFiles.length) {
|
if (props.initialPrompt?.trim() || props.initialFiles.length) {
|
||||||
const initialMerge = mergeFilesWithLimit([], Array.from(props.initialFiles), MAX_ATTACHMENTS)
|
const initialMerge = mergeFilesWithLimit([], Array.from(props.initialFiles), MAX_ATTACHMENTS)
|
||||||
@@ -2585,15 +2606,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function requestCloseWorkbench() {
|
function requestCloseWorkbench() {
|
||||||
if (submitting.value || reviewActionBusy.value || deleteSessionBusy.value || leaveKnowledgeBusy.value || sessionSwitchBusy.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isKnowledgeSession.value) {
|
|
||||||
leaveKnowledgeDialogOpen.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('close')
|
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() {
|
async function saveInlineReviewChanges() {
|
||||||
if (!activeReviewPayload.value || !reviewHasUnsavedChanges.value || reviewActionBusy.value) return
|
if (!activeReviewPayload.value || !reviewHasUnsavedChanges.value || reviewActionBusy.value) return
|
||||||
|
|
||||||
@@ -3092,10 +3079,8 @@ export default {
|
|||||||
reviewCancelDialogOpen,
|
reviewCancelDialogOpen,
|
||||||
reviewEditDialogOpen,
|
reviewEditDialogOpen,
|
||||||
deleteSessionDialogOpen,
|
deleteSessionDialogOpen,
|
||||||
leaveKnowledgeDialogOpen,
|
|
||||||
reviewActionBusy,
|
reviewActionBusy,
|
||||||
deleteSessionBusy,
|
deleteSessionBusy,
|
||||||
leaveKnowledgeBusy,
|
|
||||||
reviewEditFields,
|
reviewEditFields,
|
||||||
documentPreviewDialog,
|
documentPreviewDialog,
|
||||||
shortcuts,
|
shortcuts,
|
||||||
@@ -3144,8 +3129,6 @@ export default {
|
|||||||
openDeleteSessionDialog,
|
openDeleteSessionDialog,
|
||||||
closeDeleteSessionDialog,
|
closeDeleteSessionDialog,
|
||||||
confirmDeleteCurrentSession,
|
confirmDeleteCurrentSession,
|
||||||
closeLeaveKnowledgeDialog,
|
|
||||||
confirmLeaveKnowledgeSession,
|
|
||||||
openInlineReviewEditor,
|
openInlineReviewEditor,
|
||||||
closeInlineReviewEditor,
|
closeInlineReviewEditor,
|
||||||
commitInlineReviewEditor,
|
commitInlineReviewEditor,
|
||||||
|
|||||||
@@ -452,11 +452,6 @@ export default {
|
|||||||
department: request.value.profileDepartment,
|
department: request.value.profileDepartment,
|
||||||
grade: request.value.profileGrade,
|
grade: request.value.profileGrade,
|
||||||
manager: request.value.profileManager,
|
manager: request.value.profileManager,
|
||||||
facts: [
|
|
||||||
{ label: '部门', value: request.value.profileDepartment },
|
|
||||||
{ label: '职级', value: request.value.profileGrade },
|
|
||||||
{ label: '直属上司', value: request.value.profileManager }
|
|
||||||
],
|
|
||||||
avatar: request.value.profileAvatar
|
avatar: request.value.profileAvatar
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -484,43 +479,44 @@ export default {
|
|||||||
{ immediate: true }
|
{ 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: '报销金额',
|
label: '报销金额',
|
||||||
value: request.value.amountDisplay,
|
value: request.value.amountDisplay,
|
||||||
kind: 'text',
|
icon: '',
|
||||||
emphasis: true
|
valueClass: 'amount'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '报销类型',
|
key: 'type',
|
||||||
|
label: isTravelRequest.value ? '差旅类型' : '报销类型',
|
||||||
value: request.value.typeLabel,
|
value: request.value.typeLabel,
|
||||||
kind: 'text'
|
icon: '',
|
||||||
|
valueClass: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '当前节点',
|
key: 'status',
|
||||||
|
label: '当前状态',
|
||||||
value: request.value.node,
|
value: request.value.node,
|
||||||
kind: 'pill',
|
icon: '',
|
||||||
className: 'state-pill',
|
valueClass: 'status'
|
||||||
tone: request.value.approvalTone
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '审批状态',
|
|
||||||
value: request.value.approval,
|
|
||||||
kind: 'pill',
|
|
||||||
className: 'approval-pill',
|
|
||||||
tone: request.value.approvalTone
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
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(() =>
|
const progressSteps = computed(() =>
|
||||||
Array.isArray(request.value.progressSteps) && request.value.progressSteps.length
|
Array.isArray(request.value.progressSteps) && request.value.progressSteps.length
|
||||||
? request.value.progressSteps
|
? request.value.progressSteps
|
||||||
@@ -1136,8 +1132,7 @@ export default {
|
|||||||
handleExpenseFileChange,
|
handleExpenseFileChange,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
hasExpenseRiskColumn,
|
hasExpenseRiskColumn,
|
||||||
heroStats,
|
heroFactItems,
|
||||||
heroSummaryItems,
|
|
||||||
isDraftRequest,
|
isDraftRequest,
|
||||||
isTravelRequest,
|
isTravelRequest,
|
||||||
locationInputPlaceholder,
|
locationInputPlaceholder,
|
||||||
|
|||||||
Reference in New Issue
Block a user