feat: 新增数字员工管理页面与工作台首页重构
后端优化 agent 资产种子初始化和常量配置,前端新增数字员工 视图和调度对话框组件,重构个人工作台首页布局和洞察面板, 完善审计页面数字员工详情和运行时模型,优化侧边栏导航和图 标配置,新增工作台摘要和工作台数据模块,补充单元测试。
This commit is contained in:
@@ -1,37 +1,71 @@
|
||||
<template>
|
||||
<section class="workbench">
|
||||
<template>
|
||||
<section class="workbench" aria-label="个人工作台">
|
||||
<PanelHead
|
||||
v-if="showHeader"
|
||||
eyebrow="Personal Workspace"
|
||||
title="个人工作台"
|
||||
note="把今天要处理的待办、报销进度和制度更新集中到一个入口。"
|
||||
note="把费用申请、报销进度、制度问答和待办处理集中到一个入口。"
|
||||
/>
|
||||
|
||||
<article class="panel assistant-hero">
|
||||
<div class="assistant-visual" aria-hidden="true">
|
||||
<span class="assistant-glow"></span>
|
||||
<img class="assistant-image" :src="robotAssistant" alt="" />
|
||||
</div>
|
||||
|
||||
<article class="panel assistant-hero" :style="{ '--assistant-bg-image': `url(${homepageBackground})` }">
|
||||
<div class="assistant-copy">
|
||||
<h3>嗨,{{ assistantGreetingName }},描述您想做的事,AI 会直接帮您处理</h3>
|
||||
<p>我会自动识别您的意图,协助完成费用申请、报销、查询和制度问答等业务工作,耐心把事情推进到可执行的下一步。</p>
|
||||
<h1>你的专属 <span>AI 财务助手</span></h1>
|
||||
<p>智能理解财务业务,提供数据洞察与方案建议,高效处理日常事务</p>
|
||||
|
||||
<div class="assistant-input">
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
class="assistant-file-input"
|
||||
type="file"
|
||||
multiple
|
||||
accept=".pdf,.jpg,.jpeg,.png,.webp,.doc,.docx,.xls,.xlsx"
|
||||
@change="handleWorkbenchFilesChange"
|
||||
/>
|
||||
<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">
|
||||
<textarea
|
||||
ref="assistantInputRef"
|
||||
v-model="assistantDraft"
|
||||
rows="1"
|
||||
placeholder="例如:我昨天请客户吃饭花了 860 元,还打车去了客户公司"
|
||||
maxlength="1000"
|
||||
rows="2"
|
||||
placeholder="请输入费用申请、报销问题、预算查询或制度问答..."
|
||||
@keydown.enter.prevent="handleWorkbenchEnter"
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="composer-related-button"
|
||||
:disabled="Boolean(pendingAction)"
|
||||
@click="triggerFileUpload"
|
||||
>
|
||||
<i class="mdi mdi-source-branch"></i>
|
||||
<span>关联单据</span>
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</button>
|
||||
|
||||
<span class="composer-count">{{ assistantDraft.length }}/1000</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="composer-send-button"
|
||||
:disabled="Boolean(pendingAction)"
|
||||
:aria-label="pendingAction === 'expense' ? '处理中' : expenseActionLabel"
|
||||
@click="handleExpenseConversationAction"
|
||||
>
|
||||
<i :class="pendingAction === 'expense' ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-send'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedFiles.length" class="assistant-file-strip">
|
||||
@@ -40,115 +74,206 @@
|
||||
<button type="button" class="assistant-file-clear" @click="clearSelectedFiles">清空</button>
|
||||
</div>
|
||||
|
||||
<div class="assistant-tools">
|
||||
<button type="button" class="ghost-action" :disabled="Boolean(pendingAction)" @click="triggerFileUpload">
|
||||
<i class="mdi mdi-upload-outline"></i>
|
||||
<span>上传票据</span>
|
||||
</button>
|
||||
<div class="quick-prompts" aria-label="常用提问">
|
||||
<span>常用提问:</span>
|
||||
<button
|
||||
v-for="prompt in quickPromptItems"
|
||||
:key="prompt"
|
||||
type="button"
|
||||
class="hero-action"
|
||||
:disabled="Boolean(pendingAction)"
|
||||
@click="handleExpenseConversationAction"
|
||||
@click="applyQuickPrompt(prompt)"
|
||||
>
|
||||
<i :class="pendingAction === 'expense' ? 'mdi mdi-loading mdi-spin' : expenseActionIcon"></i>
|
||||
<span>{{ pendingAction === 'expense' ? '处理中...' : expenseActionLabel }}</span>
|
||||
{{ prompt }}
|
||||
</button>
|
||||
<button type="button" class="quick-more" @click="emit('open-assistant')">
|
||||
更多
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="workbench-grid">
|
||||
<article class="panel list-panel">
|
||||
<div class="section-head">
|
||||
<div class="title-with-badge">
|
||||
<h3>报销待办</h3>
|
||||
<span class="alert-badge">{{ todoAlertCount }}</span>
|
||||
</div>
|
||||
<button type="button" class="link-action">查看全部 <i class="mdi mdi-chevron-right"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="list-body">
|
||||
<div v-for="item in todoItems" :key="item.title" class="todo-row">
|
||||
<WorkbenchListIcon
|
||||
:icon-key="item.iconKey"
|
||||
:color="item.color"
|
||||
:accent="item.accent"
|
||||
/>
|
||||
|
||||
<div class="todo-copy">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<p class="todo-advice">
|
||||
<span class="todo-advice-label">{{ item.tipLabel }}</span>
|
||||
<span class="todo-advice-text">{{ item.suggestion }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button type="button" class="row-action" @click="emit('open-assistant')">{{ item.action }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel list-panel">
|
||||
<div class="section-head">
|
||||
<div class="title-with-badge">
|
||||
<h3>报销进度</h3>
|
||||
<span class="alert-badge">{{ progressAlertCount }}</span>
|
||||
</div>
|
||||
<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">
|
||||
<WorkbenchListIcon
|
||||
:icon-key="item.iconKey"
|
||||
:color="item.color"
|
||||
:accent="item.accent"
|
||||
/>
|
||||
|
||||
<div class="todo-copy progress-copy">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<p>提交时间:{{ item.date }}</p>
|
||||
</div>
|
||||
|
||||
<strong class="progress-amount">{{ item.amount }}</strong>
|
||||
<span class="progress-status" :class="item.tone">{{ item.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<div class="capability-grid" aria-label="AI 财务助手能力">
|
||||
<button
|
||||
v-for="item in assistantCapabilities"
|
||||
:key="item.title"
|
||||
type="button"
|
||||
class="capability-card panel"
|
||||
:class="`capability-card--${item.tone}`"
|
||||
@click="openPromptAssistant(item.prompt)"
|
||||
>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<span class="policy-title-cell">制度名称</span>
|
||||
<span class="policy-summary-cell">摘要</span>
|
||||
<span class="policy-date-cell">发布日期</span>
|
||||
<div class="workbench-content-grid">
|
||||
<article class="panel workbench-card todo-panel">
|
||||
<div class="section-head">
|
||||
<div class="title-with-badge">
|
||||
<h2>我的待办</h2>
|
||||
<span class="soft-badge">{{ todoAlertCount }}</span>
|
||||
</div>
|
||||
<button type="button" class="link-action">全部待办 <i class="mdi mdi-chevron-right"></i></button>
|
||||
</div>
|
||||
|
||||
<div v-for="item in policyItems" :key="item.name" class="policy-row">
|
||||
<strong class="policy-title-cell">{{ item.name }}</strong>
|
||||
<span class="policy-summary-cell">{{ item.summary }}</span>
|
||||
<span class="policy-date-cell">{{ item.date }}</span>
|
||||
<div class="todo-list">
|
||||
<button
|
||||
v-for="item in visibleTodoItems"
|
||||
:key="item.title"
|
||||
type="button"
|
||||
class="todo-row"
|
||||
@click="openPromptAssistant(`帮我处理:${item.title},${item.description}`)"
|
||||
>
|
||||
<WorkbenchListIcon
|
||||
:icon-key="item.iconKey"
|
||||
:color="item.color"
|
||||
:accent="item.accent"
|
||||
/>
|
||||
<span class="todo-copy">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<small>{{ item.description }}</small>
|
||||
</span>
|
||||
<span class="todo-meta">
|
||||
<span class="todo-status" :class="`todo-status--${item.statusTone}`">{{ item.status }}</span>
|
||||
<small>{{ item.due }}</small>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</article>
|
||||
|
||||
<article class="panel workbench-card progress-panel">
|
||||
<div class="section-head">
|
||||
<h2>费用进度</h2>
|
||||
<button type="button" class="link-action">全部进度 <i class="mdi mdi-chevron-right"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="progress-list">
|
||||
<button
|
||||
v-for="item in visibleProgressItems"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="progress-row"
|
||||
@click="openPromptAssistant(`查询 ${item.id} 的费用进度`)"
|
||||
>
|
||||
<span class="progress-identity">
|
||||
<strong>{{ item.id }}</strong>
|
||||
<small>{{ item.title }}</small>
|
||||
</span>
|
||||
|
||||
<span class="progress-steps" aria-hidden="true">
|
||||
<span
|
||||
v-for="(step, index) in progressSteps"
|
||||
:key="step"
|
||||
class="progress-step"
|
||||
:class="{
|
||||
'is-done': index < item.activeStep,
|
||||
'is-current': index === item.activeStep,
|
||||
'is-future': index > item.activeStep
|
||||
}"
|
||||
>
|
||||
<i :class="index <= item.activeStep ? 'mdi mdi-check' : 'mdi mdi-minus'"></i>
|
||||
<small>{{ step }}</small>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="progress-result">
|
||||
<span class="progress-status" :class="`progress-status--${item.statusTone}`">{{ item.status }}</span>
|
||||
<strong>{{ item.amount }}</strong>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<h2>费用画像</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="detail-action"
|
||||
@click="openPromptAssistant('查看我的费用画像详情,并总结 AI 使用、提单效率和预审通过率。')"
|
||||
>
|
||||
<span>查看详情</span>
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="insight-profile-list" aria-label="费用画像">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import PanelHead from '../shared/PanelHead.vue'
|
||||
import WorkbenchListIcon from '../shared/WorkbenchListIcon.vue'
|
||||
import robotAssistant from '../../assets/robot-helper.png'
|
||||
import homepageBackground from '../../assets/homepage_backgraound.png'
|
||||
import { useSystemState } from '../../composables/useSystemState.js'
|
||||
import { useToast } from '../../composables/useToast.js'
|
||||
import {
|
||||
assistantCapabilities,
|
||||
buildExpenseStatItems,
|
||||
progressItems,
|
||||
progressSteps,
|
||||
quickPromptItems,
|
||||
todoItems,
|
||||
usageProfileMetrics
|
||||
} from '../../data/personalWorkbench.js'
|
||||
import { clearUserConversations, fetchLatestConversation } from '../../services/orchestrator.js'
|
||||
import {
|
||||
ASSISTANT_SESSION_SNAPSHOT_EVENT,
|
||||
@@ -157,13 +282,15 @@ import {
|
||||
|
||||
const props = defineProps({
|
||||
showHeader: { type: Boolean, default: true },
|
||||
assistantModalOpen: { type: Boolean, default: false }
|
||||
assistantModalOpen: { type: Boolean, default: false },
|
||||
workbenchSummary: { type: Object, default: () => ({}) }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['open-assistant'])
|
||||
const { currentUser } = useSystemState()
|
||||
const { toast } = useToast()
|
||||
const assistantDraft = ref('')
|
||||
const assistantInputRef = ref(null)
|
||||
const fileInputRef = ref(null)
|
||||
const selectedFiles = ref([])
|
||||
const pendingAction = ref('')
|
||||
@@ -178,11 +305,22 @@ const hasExpenseConversation = computed(() =>
|
||||
|| hasLocalExpenseSnapshot.value
|
||||
)
|
||||
const expenseActionLabel = computed(() => (hasExpenseConversation.value ? '继续报销' : '新建报销'))
|
||||
const expenseActionIcon = computed(() => (hasExpenseConversation.value ? 'mdi mdi-history' : 'mdi mdi-magnify-scan'))
|
||||
const assistantGreetingName = computed(() => {
|
||||
const user = currentUser.value || {}
|
||||
return String(user.name || user.username || '同事').trim() || '同事'
|
||||
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(() => {
|
||||
const preferredKeys = ['ai-usage', 'submit-efficiency', 'auto-pass-rate', 'audit-duration']
|
||||
return preferredKeys
|
||||
.map((key) => usageProfileMetrics.find((item) => item.key === key))
|
||||
.filter(Boolean)
|
||||
})
|
||||
const visibleTodoItems = computed(() => todoItems.slice(0, 5))
|
||||
const visibleProgressItems = computed(() => progressItems.slice(0, 5))
|
||||
const todoAlertCount = computed(() => visibleTodoItems.value.length)
|
||||
|
||||
function buildSelectedFileKey(file) {
|
||||
return [file?.name, file?.size, file?.lastModified, file?.type].join('__')
|
||||
@@ -255,6 +393,30 @@ async function loadLatestConversation() {
|
||||
return payload?.found ? payload.conversation || null : null
|
||||
}
|
||||
|
||||
function focusAssistantInput() {
|
||||
nextTick(() => {
|
||||
assistantInputRef.value?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
function applyQuickPrompt(prompt) {
|
||||
assistantDraft.value = String(prompt || '').trim()
|
||||
focusAssistantInput()
|
||||
}
|
||||
|
||||
function openPromptAssistant(prompt) {
|
||||
if (pendingAction.value) {
|
||||
return
|
||||
}
|
||||
|
||||
emitAssistant({
|
||||
prompt: String(prompt || '').trim(),
|
||||
source: 'workbench',
|
||||
files: Array.from(selectedFiles.value),
|
||||
conversation: null
|
||||
})
|
||||
}
|
||||
|
||||
function handleWorkbenchEnter(event) {
|
||||
if (event.isComposing) {
|
||||
return
|
||||
@@ -340,99 +502,6 @@ async function handleExpenseConversationAction() {
|
||||
}
|
||||
}
|
||||
|
||||
const todoItems = [
|
||||
{
|
||||
title: '业务招待报销建议补参与人员',
|
||||
tipLabel: 'AI 建议',
|
||||
suggestion: '补充客户单位、客户人数、我方陪同人员',
|
||||
action: '去补充',
|
||||
iconKey: 'hospitality',
|
||||
color: 'var(--theme-primary-active)',
|
||||
accent: 'var(--theme-primary-soft-strong)'
|
||||
},
|
||||
{
|
||||
title: '差旅报销单待提交',
|
||||
tipLabel: 'AI 建议',
|
||||
suggestion: '补齐出发交通,可直接生成报销单',
|
||||
action: '继续填写',
|
||||
iconKey: 'travelDraft',
|
||||
color: 'var(--success-hover)',
|
||||
accent: 'var(--success-line)'
|
||||
},
|
||||
{
|
||||
title: '有 5 张票据未关联报销单',
|
||||
tipLabel: 'AI 建议',
|
||||
suggestion: '其中 3 张疑似交通费,可合并生成交通报销',
|
||||
action: '去整理',
|
||||
iconKey: 'receipts',
|
||||
color: 'var(--chart-blue)',
|
||||
accent: 'var(--theme-primary-soft-strong)'
|
||||
}
|
||||
]
|
||||
|
||||
const todoAlertCount = todoItems.length
|
||||
|
||||
const progressItems = [
|
||||
{
|
||||
id: 'travel',
|
||||
title: '差旅报销',
|
||||
amount: '¥3,280',
|
||||
date: '2026-05-03',
|
||||
status: '主管审批中',
|
||||
tone: 'success',
|
||||
iconKey: 'flight',
|
||||
color: 'var(--theme-primary-active)',
|
||||
accent: 'var(--theme-primary-soft-strong)'
|
||||
},
|
||||
{
|
||||
id: 'transport',
|
||||
title: '交通报销',
|
||||
amount: '¥126',
|
||||
date: '2026-05-02',
|
||||
status: '财务复核中',
|
||||
tone: 'info',
|
||||
iconKey: 'transport',
|
||||
color: 'var(--chart-blue)',
|
||||
accent: 'var(--theme-primary-soft-strong)'
|
||||
},
|
||||
{
|
||||
id: 'office',
|
||||
title: '办公采购',
|
||||
amount: '¥458',
|
||||
date: '2026-05-01',
|
||||
status: '已到账',
|
||||
tone: 'mint',
|
||||
iconKey: 'procurement',
|
||||
color: 'var(--success)',
|
||||
accent: 'var(--success-line)'
|
||||
}
|
||||
]
|
||||
|
||||
const progressAlertCount = progressItems.filter((item) => item.status !== '已到账').length
|
||||
|
||||
const policyItems = [
|
||||
{
|
||||
name: '差旅报销管理办法(2026版)',
|
||||
summary: '更新住宿标准与交通等级规则',
|
||||
date: '2026-05-04'
|
||||
},
|
||||
{
|
||||
name: '业务招待费用报销规范',
|
||||
summary: '明确参与人员与事由填写要求',
|
||||
date: '2026-05-02'
|
||||
},
|
||||
{
|
||||
name: '交通费用报销细则',
|
||||
summary: '补充网约车与停车费报销说明',
|
||||
date: '2026-04-28'
|
||||
},
|
||||
{
|
||||
name: '票据与附件提交规范通知',
|
||||
summary: '统一附件命名与上传要求',
|
||||
date: '2026-04-25'
|
||||
}
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
refreshLocalExpenseSnapshot()
|
||||
refreshLatestExpenseConversation()
|
||||
@@ -454,3 +523,5 @@ watch(
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/components/personal-workbench.css"></style>
|
||||
<style scoped src="../../assets/styles/components/personal-workbench-insights.css"></style>
|
||||
<style scoped src="../../assets/styles/components/personal-workbench-responsive.css"></style>
|
||||
|
||||
Reference in New Issue
Block a user