2026-05-07 11:50:10 +08:00
|
|
|
|
<template>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
<section class="workbench">
|
|
|
|
|
|
<PanelHead
|
|
|
|
|
|
v-if="showHeader"
|
|
|
|
|
|
eyebrow="Personal Workspace"
|
|
|
|
|
|
title="个人工作台"
|
|
|
|
|
|
note="把今天要处理的待办、报销进度和制度更新集中到一个入口。"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<article class="panel assistant-hero">
|
|
|
|
|
|
<div class="assistant-visual" aria-hidden="true">
|
2026-05-07 11:50:10 +08:00
|
|
|
|
<span class="assistant-glow"></span>
|
|
|
|
|
|
<img class="assistant-image" :src="robotAssistant" alt="" />
|
2026-05-05 18:22:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="assistant-copy">
|
2026-05-25 13:35:39 +08:00
|
|
|
|
<h3>嗨,{{ assistantGreetingName }},描述您想做的事,AI 会直接帮您处理</h3>
|
|
|
|
|
|
<p>我会自动识别您的意图,协助完成费用申请、报销、查询和制度问答等业务工作,耐心把事情推进到可执行的下一步。</p>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="assistant-input">
|
2026-05-12 01:27:49 +00:00
|
|
|
|
<input
|
|
|
|
|
|
ref="fileInputRef"
|
|
|
|
|
|
class="assistant-file-input"
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
multiple
|
|
|
|
|
|
accept=".pdf,.jpg,.jpeg,.png,.webp,.doc,.docx,.xls,.xlsx"
|
|
|
|
|
|
@change="handleWorkbenchFilesChange"
|
|
|
|
|
|
/>
|
2026-05-05 23:47:20 +08:00
|
|
|
|
<textarea
|
|
|
|
|
|
v-model="assistantDraft"
|
2026-05-06 11:00:38 +08:00
|
|
|
|
rows="1"
|
2026-05-05 23:47:20 +08:00
|
|
|
|
placeholder="例如:我昨天请客户吃饭花了 860 元,还打车去了客户公司"
|
2026-05-12 01:27:49 +00:00
|
|
|
|
@keydown.enter.prevent="handleWorkbenchEnter"
|
2026-05-05 23:47:20 +08:00
|
|
|
|
/>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-12 06:39:26 +00:00
|
|
|
|
<div v-if="selectedFiles.length" class="assistant-file-strip">
|
|
|
|
|
|
<span class="assistant-file-note">已带入 {{ selectedFiles.length }} 份附件</span>
|
|
|
|
|
|
<span v-for="file in selectedFiles" :key="file.name" class="assistant-file-chip">{{ file.name }}</span>
|
|
|
|
|
|
<button type="button" class="assistant-file-clear" @click="clearSelectedFiles">清空</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-05 18:22:47 +08:00
|
|
|
|
<div class="assistant-tools">
|
2026-05-13 13:12:28 +00:00
|
|
|
|
<button type="button" class="ghost-action" :disabled="Boolean(pendingAction)" @click="triggerFileUpload">
|
2026-05-05 18:22:47 +08:00
|
|
|
|
<i class="mdi mdi-upload-outline"></i>
|
|
|
|
|
|
<span>上传票据</span>
|
|
|
|
|
|
</button>
|
2026-05-12 06:39:26 +00:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="hero-action"
|
2026-05-13 13:12:28 +00:00
|
|
|
|
:disabled="Boolean(pendingAction)"
|
|
|
|
|
|
@click="handleExpenseConversationAction"
|
2026-05-12 06:39:26 +00:00
|
|
|
|
>
|
2026-05-13 13:12:28 +00:00
|
|
|
|
<i :class="pendingAction === 'expense' ? 'mdi mdi-loading mdi-spin' : expenseActionIcon"></i>
|
|
|
|
|
|
<span>{{ pendingAction === 'expense' ? '处理中...' : expenseActionLabel }}</span>
|
2026-05-07 11:50:10 +08:00
|
|
|
|
</button>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
|
2026-05-15 06:56:51 +00:00
|
|
|
|
<div class="workbench-grid">
|
2026-05-05 18:22:47 +08:00
|
|
|
|
<article class="panel list-panel">
|
|
|
|
|
|
<div class="section-head">
|
2026-05-05 22:35:38 +08:00
|
|
|
|
<div class="title-with-badge">
|
2026-05-14 15:43:28 +00:00
|
|
|
|
<h3>报销待办</h3>
|
2026-05-05 22:35:38 +08:00
|
|
|
|
<span class="alert-badge">{{ todoAlertCount }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button type="button" class="link-action">查看全部 <i class="mdi mdi-chevron-right"></i></button>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="list-body">
|
|
|
|
|
|
<div v-for="item in todoItems" :key="item.title" class="todo-row">
|
2026-05-20 21:00:47 +08:00
|
|
|
|
<WorkbenchListIcon
|
|
|
|
|
|
:icon-key="item.iconKey"
|
|
|
|
|
|
:color="item.color"
|
|
|
|
|
|
:accent="item.accent"
|
|
|
|
|
|
/>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="todo-copy">
|
|
|
|
|
|
<strong>{{ item.title }}</strong>
|
2026-05-05 22:35:38 +08:00
|
|
|
|
<p class="todo-advice">
|
|
|
|
|
|
<span class="todo-advice-label">{{ item.tipLabel }}</span>
|
|
|
|
|
|
<span class="todo-advice-text">{{ item.suggestion }}</span>
|
|
|
|
|
|
</p>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
<button type="button" class="row-action" @click="emit('open-assistant')">{{ item.action }}</button>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
|
|
|
|
<article class="panel list-panel">
|
|
|
|
|
|
<div class="section-head">
|
2026-05-05 22:35:38 +08:00
|
|
|
|
<div class="title-with-badge">
|
|
|
|
|
|
<h3>报销进度</h3>
|
|
|
|
|
|
<span class="alert-badge">{{ progressAlertCount }}</span>
|
|
|
|
|
|
</div>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
<button type="button" class="link-action">查看全部 <i class="mdi mdi-chevron-right"></i></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="list-body">
|
|
|
|
|
|
<div v-for="item in progressItems" :key="item.id" class="progress-row">
|
2026-05-20 21:00:47 +08:00
|
|
|
|
<WorkbenchListIcon
|
|
|
|
|
|
:icon-key="item.iconKey"
|
|
|
|
|
|
:color="item.color"
|
|
|
|
|
|
:accent="item.accent"
|
|
|
|
|
|
/>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="todo-copy progress-copy">
|
2026-05-05 22:35:38 +08:00
|
|
|
|
<strong>{{ item.title }}</strong>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
<p>提交时间:{{ item.date }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-05 22:35:38 +08:00
|
|
|
|
<strong class="progress-amount">{{ item.amount }}</strong>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
<span class="progress-status" :class="item.tone">{{ item.status }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<article class="panel policy-panel">
|
|
|
|
|
|
<div class="section-head">
|
|
|
|
|
|
<h3>最新报销制度</h3>
|
|
|
|
|
|
<button type="button" class="link-action">查看全部 <i class="mdi mdi-chevron-right"></i></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="policy-table">
|
|
|
|
|
|
<div class="policy-head policy-row">
|
2026-05-05 22:35:38 +08:00
|
|
|
|
<span class="policy-title-cell">制度名称</span>
|
|
|
|
|
|
<span class="policy-summary-cell">摘要</span>
|
|
|
|
|
|
<span class="policy-date-cell">发布日期</span>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-for="item in policyItems" :key="item.name" class="policy-row">
|
2026-05-05 22:35:38 +08:00
|
|
|
|
<strong class="policy-title-cell">{{ item.name }}</strong>
|
|
|
|
|
|
<span class="policy-summary-cell">{{ item.summary }}</span>
|
|
|
|
|
|
<span class="policy-date-cell">{{ item.date }}</span>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</section>
|
2026-05-12 06:39:26 +00:00
|
|
|
|
|
2026-05-05 18:22:47 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-05-21 16:09:47 +08:00
|
|
|
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
2026-05-05 18:22:47 +08:00
|
|
|
|
import PanelHead from '../shared/PanelHead.vue'
|
2026-05-20 21:00:47 +08:00
|
|
|
|
import WorkbenchListIcon from '../shared/WorkbenchListIcon.vue'
|
2026-05-07 11:50:10 +08:00
|
|
|
|
import robotAssistant from '../../assets/robot-helper.png'
|
2026-05-12 06:39:26 +00:00
|
|
|
|
import { useSystemState } from '../../composables/useSystemState.js'
|
2026-05-13 03:27:30 +00:00
|
|
|
|
import { useToast } from '../../composables/useToast.js'
|
2026-05-12 06:39:26 +00:00
|
|
|
|
import { clearUserConversations, fetchLatestConversation } from '../../services/orchestrator.js'
|
2026-05-21 16:09:47 +08:00
|
|
|
|
import {
|
|
|
|
|
|
ASSISTANT_SESSION_SNAPSHOT_EVENT,
|
|
|
|
|
|
hasAssistantSessionSnapshot
|
|
|
|
|
|
} from '../../utils/assistantSessionSnapshot.js'
|
2026-05-05 18:22:47 +08:00
|
|
|
|
|
2026-05-13 13:12:28 +00:00
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
showHeader: { type: Boolean, default: true },
|
|
|
|
|
|
assistantModalOpen: { type: Boolean, default: false }
|
2026-05-05 18:22:47 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const emit = defineEmits(['open-assistant'])
|
2026-05-12 06:39:26 +00:00
|
|
|
|
const { currentUser } = useSystemState()
|
2026-05-13 03:27:30 +00:00
|
|
|
|
const { toast } = useToast()
|
2026-05-05 23:47:20 +08:00
|
|
|
|
const assistantDraft = ref('')
|
2026-05-12 01:27:49 +00:00
|
|
|
|
const fileInputRef = ref(null)
|
2026-05-12 06:39:26 +00:00
|
|
|
|
const selectedFiles = ref([])
|
|
|
|
|
|
const pendingAction = ref('')
|
2026-05-13 13:12:28 +00:00
|
|
|
|
const latestExpenseConversation = ref(null)
|
2026-05-21 16:09:47 +08:00
|
|
|
|
const hasLocalExpenseSnapshot = ref(false)
|
2026-05-13 13:12:28 +00:00
|
|
|
|
const MAX_ATTACHMENTS = 10
|
|
|
|
|
|
const SESSION_TYPE_EXPENSE = 'expense'
|
|
|
|
|
|
const SESSION_TYPE_KNOWLEDGE = 'knowledge'
|
|
|
|
|
|
|
2026-05-21 16:09:47 +08:00
|
|
|
|
const hasExpenseConversation = computed(() =>
|
|
|
|
|
|
Boolean(latestExpenseConversation.value?.conversation_id || latestExpenseConversation.value?.conversationId)
|
|
|
|
|
|
|| hasLocalExpenseSnapshot.value
|
|
|
|
|
|
)
|
2026-05-13 13:12:28 +00:00
|
|
|
|
const expenseActionLabel = computed(() => (hasExpenseConversation.value ? '继续报销' : '新建报销'))
|
|
|
|
|
|
const expenseActionIcon = computed(() => (hasExpenseConversation.value ? 'mdi mdi-history' : 'mdi mdi-magnify-scan'))
|
2026-05-20 21:00:47 +08:00
|
|
|
|
const assistantGreetingName = computed(() => {
|
|
|
|
|
|
const user = currentUser.value || {}
|
|
|
|
|
|
return String(user.name || user.username || '同事').trim() || '同事'
|
|
|
|
|
|
})
|
2026-05-13 13:12:28 +00:00
|
|
|
|
|
|
|
|
|
|
function buildSelectedFileKey(file) {
|
|
|
|
|
|
return [file?.name, file?.size, file?.lastModified, file?.type].join('__')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mergeSelectedFiles(existingFiles, incomingFiles) {
|
|
|
|
|
|
const nextFiles = []
|
|
|
|
|
|
const seen = new Set()
|
|
|
|
|
|
|
|
|
|
|
|
for (const file of existingFiles) {
|
|
|
|
|
|
const key = buildSelectedFileKey(file)
|
|
|
|
|
|
if (seen.has(key)) continue
|
|
|
|
|
|
seen.add(key)
|
|
|
|
|
|
nextFiles.push(file)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let overflowCount = 0
|
|
|
|
|
|
|
|
|
|
|
|
for (const file of incomingFiles) {
|
|
|
|
|
|
const key = buildSelectedFileKey(file)
|
|
|
|
|
|
if (seen.has(key)) continue
|
|
|
|
|
|
if (nextFiles.length >= MAX_ATTACHMENTS) {
|
|
|
|
|
|
overflowCount += 1
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
seen.add(key)
|
|
|
|
|
|
nextFiles.push(file)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
files: nextFiles,
|
|
|
|
|
|
overflowCount
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-12 06:39:26 +00:00
|
|
|
|
|
|
|
|
|
|
function resolveCurrentUserId() {
|
|
|
|
|
|
const user = currentUser.value || {}
|
|
|
|
|
|
return String(user.username || user.name || 'anonymous').trim() || 'anonymous'
|
|
|
|
|
|
}
|
2026-05-05 23:47:20 +08:00
|
|
|
|
|
2026-05-12 06:39:26 +00:00
|
|
|
|
function buildAssistantPayload() {
|
|
|
|
|
|
return {
|
2026-05-05 23:47:20 +08:00
|
|
|
|
prompt: assistantDraft.value.trim(),
|
2026-05-12 06:39:26 +00:00
|
|
|
|
source: 'workbench',
|
|
|
|
|
|
files: Array.from(selectedFiles.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearSelectedFiles() {
|
|
|
|
|
|
selectedFiles.value = []
|
|
|
|
|
|
if (fileInputRef.value) {
|
|
|
|
|
|
fileInputRef.value.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resetWorkbenchDraft() {
|
|
|
|
|
|
assistantDraft.value = ''
|
|
|
|
|
|
clearSelectedFiles()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function emitAssistant(payload) {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
emit('open-assistant', payload)
|
2026-05-12 06:39:26 +00:00
|
|
|
|
resetWorkbenchDraft()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadLatestConversation() {
|
2026-05-14 15:43:28 +00:00
|
|
|
|
const payload = await fetchLatestConversation(resolveCurrentUserId(), SESSION_TYPE_EXPENSE, {
|
|
|
|
|
|
preferRecoverable: true
|
|
|
|
|
|
})
|
2026-05-12 06:39:26 +00:00
|
|
|
|
return payload?.found ? payload.conversation || null : null
|
2026-05-05 23:47:20 +08:00
|
|
|
|
}
|
2026-05-05 18:22:47 +08:00
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
function handleWorkbenchEnter(event) {
|
|
|
|
|
|
if (event.isComposing) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 13:12:28 +00:00
|
|
|
|
handleExpenseConversationAction()
|
2026-05-12 01:27:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function triggerFileUpload() {
|
|
|
|
|
|
fileInputRef.value?.click()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleWorkbenchFilesChange(event) {
|
2026-05-13 13:12:28 +00:00
|
|
|
|
const mergeResult = mergeSelectedFiles(selectedFiles.value, Array.from(event.target.files ?? []))
|
|
|
|
|
|
selectedFiles.value = mergeResult.files
|
|
|
|
|
|
if (mergeResult.overflowCount > 0) {
|
|
|
|
|
|
toast(`一次最多上传 ${MAX_ATTACHMENTS} 份附件,已保留前 ${MAX_ATTACHMENTS} 份。`)
|
2026-05-12 06:39:26 +00:00
|
|
|
|
}
|
2026-05-13 13:12:28 +00:00
|
|
|
|
if (fileInputRef.value) {
|
|
|
|
|
|
fileInputRef.value.value = ''
|
2026-05-12 06:39:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-12 01:27:49 +00:00
|
|
|
|
|
2026-05-13 13:12:28 +00:00
|
|
|
|
async function refreshLatestExpenseConversation() {
|
2026-05-21 16:09:47 +08:00
|
|
|
|
refreshLocalExpenseSnapshot()
|
2026-05-12 06:39:26 +00:00
|
|
|
|
try {
|
2026-05-13 13:12:28 +00:00
|
|
|
|
latestExpenseConversation.value = await loadLatestConversation()
|
2026-05-12 06:39:26 +00:00
|
|
|
|
} catch (error) {
|
2026-05-13 13:12:28 +00:00
|
|
|
|
console.warn('Failed to refresh latest expense conversation:', error)
|
|
|
|
|
|
latestExpenseConversation.value = null
|
2026-05-12 06:39:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 16:09:47 +08:00
|
|
|
|
function refreshLocalExpenseSnapshot() {
|
|
|
|
|
|
hasLocalExpenseSnapshot.value = hasAssistantSessionSnapshot(resolveCurrentUserId(), SESSION_TYPE_EXPENSE)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleAssistantSessionSnapshotChange(event) {
|
|
|
|
|
|
const sessionType = String(event?.detail?.sessionType || '').trim()
|
|
|
|
|
|
if (!sessionType || sessionType === SESSION_TYPE_EXPENSE) {
|
|
|
|
|
|
refreshLocalExpenseSnapshot()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 13:12:28 +00:00
|
|
|
|
async function clearKnowledgeHistoryBeforeExpense() {
|
|
|
|
|
|
await clearUserConversations(resolveCurrentUserId(), SESSION_TYPE_KNOWLEDGE)
|
2026-05-12 06:39:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 13:12:28 +00:00
|
|
|
|
async function handleExpenseConversationAction() {
|
|
|
|
|
|
if (pendingAction.value) {
|
2026-05-12 06:39:26 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 13:12:28 +00:00
|
|
|
|
const nextPayload = buildAssistantPayload()
|
2026-05-19 17:24:13 +00:00
|
|
|
|
const shouldOpenImmediately = Boolean(nextPayload.prompt || nextPayload.files.length)
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldOpenImmediately) {
|
|
|
|
|
|
emitAssistant({
|
|
|
|
|
|
...nextPayload,
|
|
|
|
|
|
conversation: null
|
|
|
|
|
|
})
|
|
|
|
|
|
void clearKnowledgeHistoryBeforeExpense().catch((error) => {
|
|
|
|
|
|
console.warn('Failed to clear knowledge history before expense:', error)
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pendingAction.value = 'expense'
|
2026-05-12 06:39:26 +00:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-05-13 13:12:28 +00:00
|
|
|
|
await clearKnowledgeHistoryBeforeExpense()
|
|
|
|
|
|
const conversation = await loadLatestConversation()
|
|
|
|
|
|
latestExpenseConversation.value = conversation
|
|
|
|
|
|
emitAssistant({
|
|
|
|
|
|
...nextPayload,
|
|
|
|
|
|
conversation
|
|
|
|
|
|
})
|
2026-05-12 06:39:26 +00:00
|
|
|
|
} catch (error) {
|
2026-05-13 13:12:28 +00:00
|
|
|
|
console.warn('Failed to open expense conversation:', error)
|
|
|
|
|
|
toast(error?.message || '打开报销会话失败,请稍后重试。')
|
2026-05-12 06:39:26 +00:00
|
|
|
|
} finally {
|
2026-05-13 13:12:28 +00:00
|
|
|
|
pendingAction.value = ''
|
2026-05-12 06:39:26 +00:00
|
|
|
|
}
|
2026-05-12 01:27:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 18:22:47 +08:00
|
|
|
|
const todoItems = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '业务招待报销建议补参与人员',
|
2026-05-05 22:35:38 +08:00
|
|
|
|
tipLabel: 'AI 建议',
|
|
|
|
|
|
suggestion: '补充客户单位、客户人数、我方陪同人员',
|
2026-05-05 18:22:47 +08:00
|
|
|
|
action: '去补充',
|
2026-05-20 21:00:47 +08:00
|
|
|
|
iconKey: 'hospitality',
|
2026-05-27 09:17:57 +08:00
|
|
|
|
color: 'var(--theme-primary-active)',
|
|
|
|
|
|
accent: 'var(--theme-primary-soft-strong)'
|
2026-05-05 18:22:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '差旅报销单待提交',
|
2026-05-05 22:35:38 +08:00
|
|
|
|
tipLabel: 'AI 建议',
|
|
|
|
|
|
suggestion: '补齐出发交通,可直接生成报销单',
|
2026-05-05 18:22:47 +08:00
|
|
|
|
action: '继续填写',
|
2026-05-20 21:00:47 +08:00
|
|
|
|
iconKey: 'travelDraft',
|
2026-05-27 09:17:57 +08:00
|
|
|
|
color: 'var(--success-hover)',
|
|
|
|
|
|
accent: 'var(--success-line)'
|
2026-05-05 18:22:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '有 5 张票据未关联报销单',
|
2026-05-05 22:35:38 +08:00
|
|
|
|
tipLabel: 'AI 建议',
|
|
|
|
|
|
suggestion: '其中 3 张疑似交通费,可合并生成交通报销',
|
2026-05-05 18:22:47 +08:00
|
|
|
|
action: '去整理',
|
2026-05-20 21:00:47 +08:00
|
|
|
|
iconKey: 'receipts',
|
2026-05-27 09:17:57 +08:00
|
|
|
|
color: 'var(--chart-blue)',
|
|
|
|
|
|
accent: 'var(--theme-primary-soft-strong)'
|
2026-05-05 18:22:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-05-05 22:35:38 +08:00
|
|
|
|
const todoAlertCount = todoItems.length
|
|
|
|
|
|
|
2026-05-05 18:22:47 +08:00
|
|
|
|
const progressItems = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'travel',
|
|
|
|
|
|
title: '差旅报销',
|
|
|
|
|
|
amount: '¥3,280',
|
|
|
|
|
|
date: '2026-05-03',
|
|
|
|
|
|
status: '主管审批中',
|
|
|
|
|
|
tone: 'success',
|
2026-05-20 21:00:47 +08:00
|
|
|
|
iconKey: 'flight',
|
2026-05-27 09:17:57 +08:00
|
|
|
|
color: 'var(--theme-primary-active)',
|
|
|
|
|
|
accent: 'var(--theme-primary-soft-strong)'
|
2026-05-05 18:22:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'transport',
|
|
|
|
|
|
title: '交通报销',
|
|
|
|
|
|
amount: '¥126',
|
|
|
|
|
|
date: '2026-05-02',
|
|
|
|
|
|
status: '财务复核中',
|
|
|
|
|
|
tone: 'info',
|
2026-05-20 21:00:47 +08:00
|
|
|
|
iconKey: 'transport',
|
2026-05-27 09:17:57 +08:00
|
|
|
|
color: 'var(--chart-blue)',
|
|
|
|
|
|
accent: 'var(--theme-primary-soft-strong)'
|
2026-05-05 18:22:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'office',
|
|
|
|
|
|
title: '办公采购',
|
|
|
|
|
|
amount: '¥458',
|
|
|
|
|
|
date: '2026-05-01',
|
|
|
|
|
|
status: '已到账',
|
|
|
|
|
|
tone: 'mint',
|
2026-05-20 21:00:47 +08:00
|
|
|
|
iconKey: 'procurement',
|
2026-05-27 09:17:57 +08:00
|
|
|
|
color: 'var(--success)',
|
|
|
|
|
|
accent: 'var(--success-line)'
|
2026-05-05 18:22:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-05-05 22:35:38 +08:00
|
|
|
|
const progressAlertCount = progressItems.filter((item) => item.status !== '已到账').length
|
|
|
|
|
|
|
2026-05-05 18:22:47 +08:00
|
|
|
|
const policyItems = [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '差旅报销管理办法(2026版)',
|
|
|
|
|
|
summary: '更新住宿标准与交通等级规则',
|
2026-05-05 22:35:38 +08:00
|
|
|
|
date: '2026-05-04'
|
2026-05-05 18:22:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '业务招待费用报销规范',
|
|
|
|
|
|
summary: '明确参与人员与事由填写要求',
|
2026-05-05 22:35:38 +08:00
|
|
|
|
date: '2026-05-02'
|
2026-05-05 18:22:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '交通费用报销细则',
|
|
|
|
|
|
summary: '补充网约车与停车费报销说明',
|
2026-05-05 22:35:38 +08:00
|
|
|
|
date: '2026-04-28'
|
2026-05-05 18:22:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '票据与附件提交规范通知',
|
|
|
|
|
|
summary: '统一附件命名与上传要求',
|
2026-05-05 22:35:38 +08:00
|
|
|
|
date: '2026-04-25'
|
2026-05-05 18:22:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
]
|
2026-05-13 13:12:28 +00:00
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
2026-05-21 16:09:47 +08:00
|
|
|
|
refreshLocalExpenseSnapshot()
|
2026-05-13 13:12:28 +00:00
|
|
|
|
refreshLatestExpenseConversation()
|
2026-05-21 16:09:47 +08:00
|
|
|
|
window.addEventListener(ASSISTANT_SESSION_SNAPSHOT_EVENT, handleAssistantSessionSnapshotChange)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
window.removeEventListener(ASSISTANT_SESSION_SNAPSHOT_EVENT, handleAssistantSessionSnapshotChange)
|
2026-05-13 13:12:28 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.assistantModalOpen,
|
|
|
|
|
|
(open, previous) => {
|
|
|
|
|
|
if (previous && !open) {
|
|
|
|
|
|
refreshLatestExpenseConversation()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
2026-05-05 18:22:47 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-05-27 09:17:57 +08:00
|
|
|
|
<style scoped src="../../assets/styles/components/personal-workbench.css"></style>
|