2026-05-28 09:30:34 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<section class="workbench" aria-label="个人工作台">
|
2026-05-05 18:22:47 +08:00
|
|
|
|
<PanelHead
|
|
|
|
|
|
v-if="showHeader"
|
|
|
|
|
|
eyebrow="Personal Workspace"
|
|
|
|
|
|
title="个人工作台"
|
2026-05-28 09:30:34 +08:00
|
|
|
|
note="把费用申请、报销进度、制度问答和待办处理集中到一个入口。"
|
2026-05-05 18:22:47 +08:00
|
|
|
|
/>
|
|
|
|
|
|
|
2026-06-02 14:01:51 +08:00
|
|
|
|
<article class="panel assistant-hero" :style="{ '--assistant-bg-image': `url(${workbenchHeroBackground})` }">
|
2026-05-05 18:22:47 +08:00
|
|
|
|
<div class="assistant-copy">
|
2026-05-28 12:09:49 +08:00
|
|
|
|
<h1>嗨,{{ displayUserName }},我是您的 <span>AI 费用助手</span></h1>
|
2026-05-28 09:30:34 +08:00
|
|
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
ref="fileInputRef"
|
|
|
|
|
|
class="assistant-file-input"
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
multiple
|
|
|
|
|
|
accept=".pdf,.jpg,.jpeg,.png,.webp,.doc,.docx,.xls,.xlsx"
|
|
|
|
|
|
@change="handleWorkbenchFilesChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="assistant-composer">
|
2026-05-05 23:47:20 +08:00
|
|
|
|
<textarea
|
2026-05-28 09:30:34 +08:00
|
|
|
|
ref="assistantInputRef"
|
2026-05-05 23:47:20 +08:00
|
|
|
|
v-model="assistantDraft"
|
2026-05-28 09:30:34 +08:00
|
|
|
|
maxlength="1000"
|
|
|
|
|
|
rows="2"
|
|
|
|
|
|
placeholder="请输入费用申请、报销问题、预算查询或制度问答..."
|
2026-05-30 15:46:51 +08:00
|
|
|
|
:readonly="isComposerPending"
|
2026-05-12 01:27:49 +00:00
|
|
|
|
@keydown.enter.prevent="handleWorkbenchEnter"
|
2026-05-05 23:47:20 +08:00
|
|
|
|
/>
|
2026-05-28 09:30:34 +08:00
|
|
|
|
|
2026-05-30 15:46:51 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="composerPendingLabel"
|
|
|
|
|
|
class="assistant-intent-status"
|
|
|
|
|
|
role="status"
|
|
|
|
|
|
aria-live="polite"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-loading mdi-spin"></i>
|
|
|
|
|
|
<span>{{ composerPendingLabel }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="workbenchDateTagLabel" class="workbench-date-chip-row">
|
|
|
|
|
|
<span class="workbench-date-chip">
|
|
|
|
|
|
<i class="mdi mdi-calendar-check"></i>
|
|
|
|
|
|
<span>{{ workbenchDateTagLabel }}</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-label="移除日期"
|
|
|
|
|
|
:disabled="Boolean(pendingAction)"
|
|
|
|
|
|
@click="removeWorkbenchDateTag"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-close"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-28 09:30:34 +08:00
|
|
|
|
<div class="composer-toolbar">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="composer-icon-button"
|
|
|
|
|
|
title="上传附件"
|
|
|
|
|
|
aria-label="上传附件"
|
|
|
|
|
|
:disabled="Boolean(pendingAction)"
|
|
|
|
|
|
@click="triggerFileUpload"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-paperclip"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
2026-05-30 15:46:51 +08:00
|
|
|
|
<div class="workbench-date-anchor">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="composer-icon-button"
|
|
|
|
|
|
:class="{ active: workbenchDatePickerOpen }"
|
|
|
|
|
|
title="选择日期"
|
|
|
|
|
|
aria-label="选择日期"
|
|
|
|
|
|
:aria-expanded="workbenchDatePickerOpen"
|
|
|
|
|
|
:disabled="Boolean(pendingAction)"
|
|
|
|
|
|
@click.stop="toggleWorkbenchDatePicker"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i class="mdi mdi-calendar-range"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="workbenchDatePickerOpen"
|
|
|
|
|
|
class="composer-date-popover"
|
|
|
|
|
|
role="dialog"
|
|
|
|
|
|
aria-label="日期选择"
|
|
|
|
|
|
@click.stop
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="composer-date-mode-tabs">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="composer-date-mode-btn"
|
|
|
|
|
|
:class="{ active: workbenchDateMode === 'single' }"
|
|
|
|
|
|
@click="setWorkbenchDateMode('single')"
|
|
|
|
|
|
>
|
|
|
|
|
|
当天
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="composer-date-mode-btn"
|
|
|
|
|
|
:class="{ active: workbenchDateMode === 'range' }"
|
|
|
|
|
|
@click="setWorkbenchDateMode('range')"
|
|
|
|
|
|
>
|
|
|
|
|
|
时间段
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="workbenchDateMode === 'single'" class="composer-date-fields">
|
|
|
|
|
|
<label class="composer-date-field">
|
|
|
|
|
|
<span>日期</span>
|
|
|
|
|
|
<input v-model="workbenchSingleDate" type="date" @change="handleWorkbenchDateInputChange('single')" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="composer-date-fields composer-date-fields-range">
|
|
|
|
|
|
<label class="composer-date-field">
|
|
|
|
|
|
<span>开始</span>
|
|
|
|
|
|
<input v-model="workbenchRangeStartDate" type="date" @change="handleWorkbenchDateInputChange('range-start')" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<span class="composer-date-range-sep">至</span>
|
|
|
|
|
|
<label class="composer-date-field">
|
|
|
|
|
|
<span>结束</span>
|
|
|
|
|
|
<input v-model="workbenchRangeEndDate" type="date" :min="workbenchRangeStartDate" @change="handleWorkbenchDateInputChange('range-end')" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<p v-if="workbenchDateMode === 'range' && !workbenchCanApplyDateSelection" class="composer-date-hint">
|
|
|
|
|
|
请确认结束日期不早于开始日期。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-28 09:30:34 +08:00
|
|
|
|
|
|
|
|
|
|
<span class="composer-count">{{ assistantDraft.length }}/1000</span>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="composer-send-button"
|
|
|
|
|
|
:disabled="Boolean(pendingAction)"
|
2026-05-30 15:46:51 +08:00
|
|
|
|
:aria-label="composerPendingLabel || expenseActionLabel"
|
2026-05-28 09:30:34 +08:00
|
|
|
|
@click="handleExpenseConversationAction"
|
|
|
|
|
|
>
|
2026-05-30 15:46:51 +08:00
|
|
|
|
<i :class="pendingAction ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-send'"></i>
|
2026-05-28 09:30:34 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
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-28 09:30:34 +08:00
|
|
|
|
<div class="quick-prompts" aria-label="常用提问">
|
|
|
|
|
|
<span>常用提问:</span>
|
2026-05-12 06:39:26 +00:00
|
|
|
|
<button
|
2026-05-28 09:30:34 +08:00
|
|
|
|
v-for="prompt in quickPromptItems"
|
|
|
|
|
|
:key="prompt"
|
2026-05-12 06:39:26 +00:00
|
|
|
|
type="button"
|
2026-05-28 09:30:34 +08:00
|
|
|
|
@click="applyQuickPrompt(prompt)"
|
2026-05-12 06:39:26 +00:00
|
|
|
|
>
|
2026-05-28 09:30:34 +08:00
|
|
|
|
{{ prompt }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" class="quick-more" @click="emit('open-assistant')">
|
|
|
|
|
|
更多
|
|
|
|
|
|
<i class="mdi mdi-chevron-right"></i>
|
2026-05-07 11:50:10 +08:00
|
|
|
|
</button>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
|
2026-05-28 12:09:49 +08:00
|
|
|
|
<div :class="['capability-grid', capabilityGridClass]" aria-label="AI 财务助手能力">
|
2026-05-28 09:30:34 +08:00
|
|
|
|
<button
|
2026-05-28 12:09:49 +08:00
|
|
|
|
v-for="item in visibleAssistantCapabilities"
|
|
|
|
|
|
:key="item.key"
|
2026-05-28 09:30:34 +08:00
|
|
|
|
type="button"
|
|
|
|
|
|
class="capability-card panel"
|
|
|
|
|
|
:class="`capability-card--${item.tone}`"
|
2026-05-30 15:46:51 +08:00
|
|
|
|
@click="openCapabilityAssistant(item)"
|
2026-05-28 09:30:34 +08:00
|
|
|
|
>
|
|
|
|
|
|
<span class="capability-icon"><i :class="item.icon"></i></span>
|
|
|
|
|
|
<span class="capability-copy">
|
|
|
|
|
|
<strong>{{ item.title }}</strong>
|
|
|
|
|
|
<small>{{ item.primary }}</small>
|
|
|
|
|
|
<small>{{ item.secondary }}</small>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<i class="mdi mdi-chevron-right capability-arrow"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="workbench-content-grid">
|
|
|
|
|
|
<article class="panel workbench-card progress-panel">
|
2026-05-05 18:22:47 +08:00
|
|
|
|
<div class="section-head">
|
2026-05-28 09:30:34 +08:00
|
|
|
|
<h2>费用进度</h2>
|
|
|
|
|
|
<button type="button" class="link-action">全部进度 <i class="mdi mdi-chevron-right"></i></button>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-28 09:30:34 +08:00
|
|
|
|
<div class="progress-list">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="item in visibleProgressItems"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="progress-row"
|
2026-06-03 12:38:17 +08:00
|
|
|
|
:class="{ 'has-long-duration-divider': item.hasLongDurationDivider }"
|
2026-06-03 09:25:23 +08:00
|
|
|
|
@click="openWorkbenchTarget(item)"
|
2026-05-28 09:30:34 +08:00
|
|
|
|
>
|
2026-06-03 12:28:21 +08:00
|
|
|
|
<span class="progress-time">
|
|
|
|
|
|
<time :datetime="item.updatedAt || ''">{{ item.displayTime }}</time>
|
|
|
|
|
|
<small>更新</small>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
2026-05-28 09:30:34 +08:00
|
|
|
|
<span class="progress-identity">
|
|
|
|
|
|
<strong>{{ item.id }}</strong>
|
|
|
|
|
|
<small>{{ item.title }}</small>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
|
|
<span class="progress-steps" aria-hidden="true">
|
|
|
|
|
|
<span
|
2026-06-03 09:25:23 +08:00
|
|
|
|
v-for="step in item.steps"
|
|
|
|
|
|
:key="step.label"
|
2026-05-28 09:30:34 +08:00
|
|
|
|
class="progress-step"
|
|
|
|
|
|
:class="{
|
2026-06-03 09:25:23 +08:00
|
|
|
|
'is-done': step.done,
|
|
|
|
|
|
'is-current': step.current,
|
|
|
|
|
|
'is-future': !step.done && !step.current
|
2026-05-28 09:30:34 +08:00
|
|
|
|
}"
|
|
|
|
|
|
>
|
2026-06-03 09:25:23 +08:00
|
|
|
|
<i :class="step.done || step.current ? 'mdi mdi-check' : 'mdi mdi-minus'"></i>
|
|
|
|
|
|
<small>{{ step.label }}</small>
|
2026-05-28 09:30:34 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
|
|
<span class="progress-result">
|
|
|
|
|
|
<span class="progress-status" :class="`progress-status--${item.statusTone}`">{{ item.status }}</span>
|
|
|
|
|
|
<strong>{{ item.amount }}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
</div>
|
2026-05-28 09:30:34 +08:00
|
|
|
|
|
2026-05-05 18:22:47 +08:00
|
|
|
|
</article>
|
|
|
|
|
|
|
2026-05-28 09:30:34 +08:00
|
|
|
|
<aside class="side-column">
|
|
|
|
|
|
<article class="panel workbench-card side-panel expense-stats-panel">
|
|
|
|
|
|
<div class="section-head side-card-head">
|
|
|
|
|
|
<h2>费用统计</h2>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="detail-action"
|
|
|
|
|
|
@click="openPromptAssistant('查看我的费用统计详情,并说明本月报销金额、审批中和待付款的主要变化。')"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span>查看详情</span>
|
|
|
|
|
|
<i class="mdi mdi-chevron-right"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
|
2026-05-28 09:30:34 +08:00
|
|
|
|
<div class="insight-metric-list" aria-label="费用统计">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="item in visibleExpenseStatItems"
|
|
|
|
|
|
:key="item.key"
|
|
|
|
|
|
class="insight-metric-row"
|
|
|
|
|
|
:class="`insight-metric-row--${item.tone}`"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="insight-metric-label">{{ item.label }}</span>
|
|
|
|
|
|
<strong class="insight-metric-value">
|
|
|
|
|
|
{{ item.value }}<small v-if="item.unit">{{ item.unit }}</small>
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
|
|
|
|
<article class="panel workbench-card side-panel usage-profile-panel">
|
|
|
|
|
|
<div class="section-head side-card-head">
|
2026-05-28 16:24:59 +08:00
|
|
|
|
<h2>用户画像</h2>
|
2026-05-28 09:30:34 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="detail-action"
|
2026-05-28 12:09:49 +08:00
|
|
|
|
aria-haspopup="dialog"
|
|
|
|
|
|
:aria-expanded="expenseProfileModalOpen"
|
|
|
|
|
|
@click="openExpenseProfileModal"
|
2026-05-28 09:30:34 +08:00
|
|
|
|
>
|
|
|
|
|
|
<span>查看详情</span>
|
2026-05-28 16:24:59 +08:00
|
|
|
|
<i :class="employeeProfileLoading ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-chevron-right'"></i>
|
2026-05-28 09:30:34 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
|
2026-05-28 16:24:59 +08:00
|
|
|
|
<div class="insight-profile-list" aria-label="用户画像">
|
2026-05-28 09:30:34 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-for="metric in visibleUsageProfileMetrics"
|
|
|
|
|
|
:key="metric.key"
|
|
|
|
|
|
class="insight-profile-card"
|
|
|
|
|
|
:class="`insight-profile-card--${metric.tone}`"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="insight-profile-icon" aria-hidden="true">
|
|
|
|
|
|
<i :class="metric.icon"></i>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div class="insight-profile-copy">
|
|
|
|
|
|
<span class="insight-profile-label">{{ metric.label }}</span>
|
|
|
|
|
|
<strong class="insight-profile-value">
|
|
|
|
|
|
{{ metric.value }}<small>{{ metric.unit }}</small>
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
<span class="insight-profile-hint">{{ metric.hint }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
</div>
|
2026-05-28 12:09:49 +08:00
|
|
|
|
|
|
|
|
|
|
<ExpenseProfileDetailModal
|
|
|
|
|
|
:visible="expenseProfileModalOpen"
|
|
|
|
|
|
:user-name="displayUserName"
|
|
|
|
|
|
:metrics="expenseProfileModalMetrics"
|
|
|
|
|
|
:tags="expenseProfileTags"
|
|
|
|
|
|
:radar-dimensions="expenseProfileRadarDimensions"
|
2026-05-30 15:46:51 +08:00
|
|
|
|
:radar-default-view="expenseProfileRadarDefaultView"
|
2026-05-28 12:09:49 +08:00
|
|
|
|
:operations="expenseProfileOperations"
|
2026-05-28 16:24:59 +08:00
|
|
|
|
:loading="employeeProfileLoading"
|
|
|
|
|
|
:error-message="employeeProfileError"
|
|
|
|
|
|
:empty-reason="expenseProfileEmptyReason"
|
2026-05-28 12:09:49 +08:00
|
|
|
|
@close="closeExpenseProfileModal"
|
|
|
|
|
|
/>
|
2026-05-05 18:22:47 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-05-28 09:30:34 +08:00
|
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
2026-05-05 18:22:47 +08:00
|
|
|
|
import PanelHead from '../shared/PanelHead.vue'
|
2026-05-28 12:09:49 +08:00
|
|
|
|
import ExpenseProfileDetailModal from './ExpenseProfileDetailModal.vue'
|
2026-06-02 14:01:51 +08:00
|
|
|
|
import workbenchHeroBackground from '../../assets/personal-workbench-hero-bg-theme-base.webp'
|
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-30 15:46:51 +08:00
|
|
|
|
import { useWorkbenchComposerDate } from '../../composables/useWorkbenchComposerDate.js'
|
2026-05-28 09:30:34 +08:00
|
|
|
|
import {
|
|
|
|
|
|
buildExpenseStatItems,
|
2026-06-02 14:01:51 +08:00
|
|
|
|
filterAssistantCapabilitiesForUser,
|
2026-05-28 09:30:34 +08:00
|
|
|
|
quickPromptItems,
|
2026-06-02 14:01:51 +08:00
|
|
|
|
resolveWorkbenchCapabilityGridClass,
|
2026-05-28 09:30:34 +08:00
|
|
|
|
} from '../../data/personalWorkbench.js'
|
2026-05-28 16:24:59 +08:00
|
|
|
|
import { fetchAgentRuns } from '../../services/agentAssets.js'
|
2026-05-12 06:39:26 +00:00
|
|
|
|
import { clearUserConversations, fetchLatestConversation } from '../../services/orchestrator.js'
|
2026-05-28 16:24:59 +08:00
|
|
|
|
import { fetchCurrentEmployeeLatestProfile } from '../../services/reimbursements.js'
|
2026-05-21 16:09:47 +08:00
|
|
|
|
import {
|
|
|
|
|
|
ASSISTANT_SESSION_SNAPSHOT_EVENT,
|
|
|
|
|
|
hasAssistantSessionSnapshot
|
|
|
|
|
|
} from '../../utils/assistantSessionSnapshot.js'
|
2026-05-30 15:46:51 +08:00
|
|
|
|
import { buildWorkbenchCapabilityAssistantPayload } from '../../utils/personalWorkbenchAssistantEntry.js'
|
2026-05-28 16:24:59 +08:00
|
|
|
|
import {
|
|
|
|
|
|
buildProfileOperationsFromAgentRuns,
|
|
|
|
|
|
buildUserProfileMetricCards,
|
|
|
|
|
|
buildUserProfileSummaryMetrics,
|
|
|
|
|
|
normalizeUserProfileRadarDimensions,
|
|
|
|
|
|
normalizeUserProfileTags,
|
2026-05-30 15:46:51 +08:00
|
|
|
|
resolveUserProfileDefaultRadarView,
|
2026-05-28 16:24:59 +08:00
|
|
|
|
resolveCurrentUserProfileError
|
|
|
|
|
|
} from '../../utils/employeeProfileViewModel.js'
|
2026-05-13 13:12:28 +00:00
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
showHeader: { type: Boolean, default: true },
|
2026-05-28 09:30:34 +08:00
|
|
|
|
assistantModalOpen: { type: Boolean, default: false },
|
|
|
|
|
|
workbenchSummary: { type: Object, default: () => ({}) }
|
2026-05-05 18:22:47 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-03 09:25:23 +08:00
|
|
|
|
const emit = defineEmits(['open-assistant', 'open-document'])
|
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-28 09:30:34 +08:00
|
|
|
|
const assistantInputRef = ref(null)
|
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-30 15:46:51 +08:00
|
|
|
|
let pendingActionTimer = 0
|
|
|
|
|
|
const {
|
|
|
|
|
|
workbenchDatePickerOpen,
|
|
|
|
|
|
workbenchDateMode,
|
|
|
|
|
|
workbenchSingleDate,
|
|
|
|
|
|
workbenchRangeStartDate,
|
|
|
|
|
|
workbenchRangeEndDate,
|
|
|
|
|
|
workbenchDateTagLabel,
|
|
|
|
|
|
workbenchCanApplyDateSelection,
|
|
|
|
|
|
clearWorkbenchDateSelection,
|
|
|
|
|
|
toggleWorkbenchDatePicker,
|
|
|
|
|
|
closeWorkbenchDatePicker,
|
|
|
|
|
|
setWorkbenchDateMode,
|
|
|
|
|
|
handleWorkbenchDatePickerOutside,
|
|
|
|
|
|
handleWorkbenchDateInputChange,
|
|
|
|
|
|
removeWorkbenchDateTag,
|
|
|
|
|
|
buildWorkbenchPromptText
|
|
|
|
|
|
} = useWorkbenchComposerDate({
|
|
|
|
|
|
draft: assistantDraft,
|
|
|
|
|
|
focusInput: focusAssistantInput
|
|
|
|
|
|
})
|
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-28 12:09:49 +08:00
|
|
|
|
const expenseProfileModalOpen = ref(false)
|
2026-05-28 16:24:59 +08:00
|
|
|
|
const employeeProfile = ref(null)
|
|
|
|
|
|
const employeeProfileRuns = ref([])
|
|
|
|
|
|
const employeeProfileLoading = ref(false)
|
|
|
|
|
|
const employeeProfileError = ref('')
|
|
|
|
|
|
let employeeProfileLoadSeq = 0
|
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-28 12:09:49 +08:00
|
|
|
|
const displayUserName = computed(() => {
|
|
|
|
|
|
const user = currentUser.value || {}
|
|
|
|
|
|
return String(user.name || user.username || '同事').trim() || '同事'
|
|
|
|
|
|
})
|
2026-05-13 13:12:28 +00:00
|
|
|
|
const expenseActionLabel = computed(() => (hasExpenseConversation.value ? '继续报销' : '新建报销'))
|
2026-05-30 15:46:51 +08:00
|
|
|
|
const isComposerPending = computed(() => Boolean(pendingAction.value))
|
|
|
|
|
|
const composerPendingLabel = computed(() => {
|
|
|
|
|
|
if (pendingAction.value === 'intent') {
|
|
|
|
|
|
return '正在识别意图,准备进入对应助手...'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pendingAction.value === 'expense') {
|
|
|
|
|
|
return '正在恢复最近报销会话...'
|
|
|
|
|
|
}
|
|
|
|
|
|
return ''
|
|
|
|
|
|
})
|
2026-06-02 14:01:51 +08:00
|
|
|
|
const visibleAssistantCapabilities = computed(() => filterAssistantCapabilitiesForUser(currentUser.value))
|
|
|
|
|
|
const capabilityGridClass = computed(() => resolveWorkbenchCapabilityGridClass(currentUser.value))
|
2026-05-28 09:30:34 +08:00
|
|
|
|
const expenseStatItems = computed(() => buildExpenseStatItems(props.workbenchSummary))
|
|
|
|
|
|
const visibleExpenseStatItems = computed(() => {
|
|
|
|
|
|
const preferredKeys = ['monthly-amount', 'monthly-count', 'in-review', 'pending-payment']
|
|
|
|
|
|
return preferredKeys
|
|
|
|
|
|
.map((key) => expenseStatItems.value.find((item) => item.key === key))
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
})
|
|
|
|
|
|
const visibleUsageProfileMetrics = computed(() => {
|
2026-05-28 16:24:59 +08:00
|
|
|
|
return buildUserProfileMetricCards(
|
|
|
|
|
|
employeeProfile.value,
|
|
|
|
|
|
employeeProfileRuns.value,
|
|
|
|
|
|
currentUser.value
|
|
|
|
|
|
).slice(0, 4)
|
2026-05-20 21:00:47 +08:00
|
|
|
|
})
|
2026-05-28 12:09:49 +08:00
|
|
|
|
const expenseProfileModalMetrics = computed(() => {
|
2026-05-28 16:24:59 +08:00
|
|
|
|
return buildUserProfileSummaryMetrics(
|
|
|
|
|
|
employeeProfile.value,
|
|
|
|
|
|
employeeProfileRuns.value,
|
|
|
|
|
|
currentUser.value
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
const expenseProfileTags = computed(() => normalizeUserProfileTags(employeeProfile.value))
|
|
|
|
|
|
const expenseProfileRadarDimensions = computed(() => normalizeUserProfileRadarDimensions(employeeProfile.value))
|
2026-05-30 15:46:51 +08:00
|
|
|
|
const expenseProfileRadarDefaultView = computed(() => resolveUserProfileDefaultRadarView(employeeProfile.value))
|
2026-05-28 16:24:59 +08:00
|
|
|
|
const expenseProfileOperations = computed(() =>
|
|
|
|
|
|
buildProfileOperationsFromAgentRuns(employeeProfileRuns.value, currentUser.value)
|
|
|
|
|
|
)
|
|
|
|
|
|
const expenseProfileEmptyReason = computed(() => String(employeeProfile.value?.empty_reason || '').trim())
|
|
|
|
|
|
const currentUserProfileKey = computed(() => {
|
|
|
|
|
|
const user = currentUser.value || {}
|
|
|
|
|
|
return [
|
|
|
|
|
|
user.username,
|
|
|
|
|
|
user.email,
|
|
|
|
|
|
user.name,
|
|
|
|
|
|
user.employeeNo,
|
|
|
|
|
|
user.employee_no
|
|
|
|
|
|
].map((item) => String(item || '').trim()).filter(Boolean).join('|')
|
2026-05-28 12:09:49 +08:00
|
|
|
|
})
|
2026-06-03 09:25:23 +08:00
|
|
|
|
const visibleProgressItems = computed(() => {
|
|
|
|
|
|
const rows = Array.isArray(props.workbenchSummary.progressItems)
|
|
|
|
|
|
? props.workbenchSummary.progressItems
|
|
|
|
|
|
: []
|
2026-06-03 12:38:17 +08:00
|
|
|
|
const progressRows = rows.slice(0, 5).map((item) => ({
|
2026-06-03 12:28:21 +08:00
|
|
|
|
...item,
|
2026-06-03 12:38:17 +08:00
|
|
|
|
displayTime: formatProgressTime(item?.updatedAt),
|
|
|
|
|
|
isLongDuration: isLongDurationProgress(item?.updatedAt)
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
return progressRows.map((item, index) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
hasLongDurationDivider: item.isLongDuration && !progressRows[index - 1]?.isLongDuration
|
2026-06-03 12:28:21 +08:00
|
|
|
|
}))
|
2026-06-03 09:25:23 +08:00
|
|
|
|
})
|
2026-05-13 13:12:28 +00:00
|
|
|
|
|
2026-06-03 12:38:17 +08:00
|
|
|
|
const LONG_DURATION_DAYS = 10
|
|
|
|
|
|
const DAY_MS = 24 * 60 * 60 * 1000
|
|
|
|
|
|
|
2026-06-03 12:28:21 +08:00
|
|
|
|
function formatProgressTime(value) {
|
|
|
|
|
|
const text = String(value || '').trim()
|
|
|
|
|
|
if (!text) {
|
|
|
|
|
|
return '最近更新'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const match = /^(\d{4})-(\d{2})-(\d{2})(?:[T\s](\d{2}):(\d{2}))?/.exec(text)
|
|
|
|
|
|
if (match) {
|
|
|
|
|
|
return match[4] ? `${match[2]}-${match[3]} ${match[4]}:${match[5]}` : `${match[2]}-${match[3]}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return text
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 12:38:17 +08:00
|
|
|
|
function parseProgressDate(value) {
|
|
|
|
|
|
const text = String(value || '').trim()
|
|
|
|
|
|
if (!text) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const localDateMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text)
|
|
|
|
|
|
if (localDateMatch) {
|
|
|
|
|
|
return new Date(
|
|
|
|
|
|
Number(localDateMatch[1]),
|
|
|
|
|
|
Number(localDateMatch[2]) - 1,
|
|
|
|
|
|
Number(localDateMatch[3])
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const date = new Date(text)
|
|
|
|
|
|
return Number.isNaN(date.getTime()) ? null : date
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isLongDurationProgress(value) {
|
|
|
|
|
|
const date = parseProgressDate(value)
|
|
|
|
|
|
if (!date) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (Date.now() - date.getTime()) / DAY_MS >= LONG_DURATION_DAYS
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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-30 15:46:51 +08:00
|
|
|
|
prompt: buildWorkbenchPromptText(),
|
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()
|
2026-05-30 15:46:51 +08:00
|
|
|
|
clearWorkbenchDateSelection()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearPendingAction() {
|
|
|
|
|
|
pendingAction.value = ''
|
|
|
|
|
|
if (pendingActionTimer) {
|
|
|
|
|
|
window.clearTimeout(pendingActionTimer)
|
|
|
|
|
|
pendingActionTimer = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function startPendingAction(action) {
|
|
|
|
|
|
clearPendingAction()
|
|
|
|
|
|
pendingAction.value = action
|
|
|
|
|
|
pendingActionTimer = window.setTimeout(() => {
|
|
|
|
|
|
if (pendingAction.value !== action) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
clearPendingAction()
|
|
|
|
|
|
toast('进入助手耗时较长,请稍后重试。')
|
|
|
|
|
|
}, 16000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function shouldShowIntentPending(payload = {}) {
|
|
|
|
|
|
return !props.assistantModalOpen
|
|
|
|
|
|
&& String(payload.prompt || '').trim()
|
|
|
|
|
|
&& String(payload.source || 'workbench').trim() === 'workbench'
|
|
|
|
|
|
&& !String(payload.sessionType || '').trim()
|
2026-05-12 06:39:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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-28 09:30:34 +08:00
|
|
|
|
function focusAssistantInput() {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
assistantInputRef.value?.focus()
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyQuickPrompt(prompt) {
|
|
|
|
|
|
assistantDraft.value = String(prompt || '').trim()
|
|
|
|
|
|
focusAssistantInput()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openPromptAssistant(prompt) {
|
|
|
|
|
|
if (pendingAction.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-30 15:46:51 +08:00
|
|
|
|
const payload = {
|
|
|
|
|
|
prompt: buildWorkbenchPromptText(prompt),
|
2026-05-28 09:30:34 +08:00
|
|
|
|
source: 'workbench',
|
|
|
|
|
|
files: Array.from(selectedFiles.value),
|
|
|
|
|
|
conversation: null
|
2026-05-30 15:46:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (shouldShowIntentPending(payload)) {
|
|
|
|
|
|
startPendingAction('intent')
|
|
|
|
|
|
}
|
|
|
|
|
|
emitAssistant(payload)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 09:25:23 +08:00
|
|
|
|
function openWorkbenchTarget(item) {
|
|
|
|
|
|
const target = item?.target || {}
|
|
|
|
|
|
if (target.type === 'document' && (target.id || target.claimNo)) {
|
|
|
|
|
|
emit('open-document', {
|
|
|
|
|
|
claimId: target.id,
|
|
|
|
|
|
id: target.id || target.claimNo,
|
|
|
|
|
|
claimNo: target.claimNo
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
openPromptAssistant(item?.prompt || `查询 ${item?.id || ''} 的费用进度`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-30 15:46:51 +08:00
|
|
|
|
function openCapabilityAssistant(item) {
|
|
|
|
|
|
if (pendingAction.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
emitAssistant(buildWorkbenchCapabilityAssistantPayload(item, buildAssistantPayload()))
|
2026-05-28 09:30:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 16:24:59 +08:00
|
|
|
|
async function loadCurrentEmployeeProfile() {
|
|
|
|
|
|
const sequence = ++employeeProfileLoadSeq
|
|
|
|
|
|
employeeProfileLoading.value = true
|
|
|
|
|
|
employeeProfileError.value = ''
|
|
|
|
|
|
|
|
|
|
|
|
const [profileResult, runsResult] = await Promise.allSettled([
|
|
|
|
|
|
fetchCurrentEmployeeLatestProfile({
|
|
|
|
|
|
scene: 'operations',
|
|
|
|
|
|
window_days: 90,
|
|
|
|
|
|
expense_type_scope: 'overall'
|
|
|
|
|
|
}),
|
|
|
|
|
|
fetchAgentRuns({ limit: 100 })
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
if (sequence !== employeeProfileLoadSeq) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (profileResult.status === 'fulfilled') {
|
|
|
|
|
|
employeeProfile.value = profileResult.value || null
|
|
|
|
|
|
} else {
|
|
|
|
|
|
employeeProfile.value = null
|
|
|
|
|
|
employeeProfileError.value = resolveCurrentUserProfileError(profileResult.reason)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
employeeProfileRuns.value = runsResult.status === 'fulfilled' ? runsResult.value || [] : []
|
|
|
|
|
|
employeeProfileLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 12:09:49 +08:00
|
|
|
|
function openExpenseProfileModal() {
|
|
|
|
|
|
expenseProfileModalOpen.value = true
|
2026-05-28 16:24:59 +08:00
|
|
|
|
if (!employeeProfile.value && !employeeProfileLoading.value) {
|
|
|
|
|
|
void loadCurrentEmployeeProfile()
|
|
|
|
|
|
}
|
2026-05-28 12:09:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeExpenseProfileModal() {
|
|
|
|
|
|
expenseProfileModalOpen.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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) {
|
2026-05-30 15:46:51 +08:00
|
|
|
|
if (shouldShowIntentPending(nextPayload)) {
|
|
|
|
|
|
startPendingAction('intent')
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
emitAssistant({
|
|
|
|
|
|
...nextPayload,
|
|
|
|
|
|
conversation: null
|
|
|
|
|
|
})
|
|
|
|
|
|
void clearKnowledgeHistoryBeforeExpense().catch((error) => {
|
|
|
|
|
|
console.warn('Failed to clear knowledge history before expense:', error)
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-30 15:46:51 +08:00
|
|
|
|
startPendingAction('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-30 15:46:51 +08:00
|
|
|
|
clearPendingAction()
|
2026-05-12 06:39:26 +00:00
|
|
|
|
}
|
2026-05-12 01:27:49 +00: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-28 16:24:59 +08:00
|
|
|
|
loadCurrentEmployeeProfile()
|
2026-05-30 15:46:51 +08:00
|
|
|
|
document.addEventListener('click', handleWorkbenchDatePickerOutside)
|
2026-05-21 16:09:47 +08:00
|
|
|
|
window.addEventListener(ASSISTANT_SESSION_SNAPSHOT_EVENT, handleAssistantSessionSnapshotChange)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
2026-05-30 15:46:51 +08:00
|
|
|
|
clearPendingAction()
|
|
|
|
|
|
document.removeEventListener('click', handleWorkbenchDatePickerOutside)
|
2026-05-21 16:09:47 +08:00
|
|
|
|
window.removeEventListener(ASSISTANT_SESSION_SNAPSHOT_EVENT, handleAssistantSessionSnapshotChange)
|
2026-05-13 13:12:28 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.assistantModalOpen,
|
|
|
|
|
|
(open, previous) => {
|
2026-05-30 15:46:51 +08:00
|
|
|
|
if (open) {
|
|
|
|
|
|
clearPendingAction()
|
|
|
|
|
|
}
|
2026-05-13 13:12:28 +00:00
|
|
|
|
if (previous && !open) {
|
|
|
|
|
|
refreshLatestExpenseConversation()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
2026-05-28 16:24:59 +08:00
|
|
|
|
|
|
|
|
|
|
watch(currentUserProfileKey, (nextKey, previousKey) => {
|
|
|
|
|
|
if (nextKey && nextKey !== previousKey) {
|
|
|
|
|
|
loadCurrentEmployeeProfile()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
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>
|
2026-06-02 14:01:51 +08:00
|
|
|
|
<style scoped src="../../assets/styles/components/personal-workbench-glass.css"></style>
|
2026-05-30 15:46:51 +08:00
|
|
|
|
<style scoped src="../../assets/styles/components/personal-workbench-composer-date.css"></style>
|
2026-05-28 09:30:34 +08:00
|
|
|
|
<style scoped src="../../assets/styles/components/personal-workbench-insights.css"></style>
|
|
|
|
|
|
<style scoped src="../../assets/styles/components/personal-workbench-responsive.css"></style>
|