feat(web): 工作台 AI 模式与差旅/风险建议交互优化

- 新增 PersonalWorkbenchAiMode 组件、AI 侧边栏与 orb 机器人视觉资源
- 新增 aiApplicationDraftModel / aiExpenseDraftModel / aiWorkbenchConversationStore
  及业务准入 aiSidebarBusinessAccess,支撑 AI 模式下的申请与报销草稿
- 顶栏、侧边栏、工作台样式重构,适配 AI 模式切换与响应式布局
- 同步 steward plan/off_topic、差旅报销引导流、风险建议卡片等测试
This commit is contained in:
caoxiaozhu
2026-06-18 22:12:24 +08:00
parent a6674a1e76
commit 0cde1f8990
65 changed files with 8011 additions and 1608 deletions

View File

@@ -1,10 +1,11 @@
<template>
<header class="topbar" :class="{ 'chat-mode': isChat, 'detail-mode': isRequestDetail }">
<div class="title-group">
<div v-if="!isWorkbenchAiHome" class="title-group">
<div class="eyebrow">{{ eyebrowLabel }}</div>
<h1>{{ currentView.title }}</h1>
<p>{{ currentView.desc }}</p>
</div>
<div v-else class="title-group" aria-hidden="true"></div>
<div class="top-actions">
<template v-if="isChat">
@@ -278,12 +279,23 @@
</Transition>
</div>
<button class="company-switcher" type="button" aria-label="切换公司">
<span>{{ displayCompanyName }}</span>
<i class="mdi mdi-chevron-down"></i>
</button>
</div>
</template>
<button class="company-switcher" type="button" aria-label="切换公司">
<span>{{ displayCompanyName }}</span>
<i class="mdi mdi-chevron-down"></i>
</button>
<button
type="button"
class="topbar-ai-mode-toggle"
:class="{ active: isTopbarAiMode }"
:aria-pressed="isTopbarAiMode"
:aria-label="topbarWorkbenchModeTitle"
:title="topbarWorkbenchModeTitle"
@click="toggleTopbarWorkbenchMode"
>
<span class="topbar-ai-mode-toggle__glyph">AI</span>
</button>
</div>
</template>
<template v-else-if="isDocuments">
<div class="kpi-chips">
@@ -345,18 +357,36 @@
</div>
</template>
<template v-else-if="isEmployees">
<div class="kpi-chips">
<div v-for="kpi in employeeKpis" :key="kpi.label" class="kpi-chip" :style="{ '--chip-color': kpi.color }">
<span class="chip-value">{{ kpi.value }}<small>{{ kpi.unit }}</small></span>
<span class="chip-label">{{ kpi.label }}</span>
<span class="chip-delta" :class="kpi.trend">{{ kpi.meta }}</span>
</div>
</div>
</template>
</div>
</header>
</template>
<template v-else-if="isEmployees">
<div class="kpi-chips">
<div v-for="kpi in employeeKpis" :key="kpi.label" class="kpi-chip" :style="{ '--chip-color': kpi.color }">
<span class="chip-value">{{ kpi.value }}<small>{{ kpi.unit }}</small></span>
<span class="chip-label">{{ kpi.label }}</span>
<span class="chip-delta" :class="kpi.trend">{{ kpi.meta }}</span>
</div>
</div>
</template>
<div v-if="showAiModeUtilityActions" class="topbar-utility-actions" aria-label="AI模式快捷操作">
<button class="company-switcher" type="button" aria-label="切换公司">
<span>{{ displayCompanyName }}</span>
<i class="mdi mdi-chevron-down"></i>
</button>
<button
type="button"
class="topbar-ai-mode-toggle"
:class="{ active: isTopbarAiMode }"
:aria-pressed="isTopbarAiMode"
:aria-label="topbarWorkbenchModeTitle"
:title="topbarWorkbenchModeTitle"
@click="toggleTopbarWorkbenchMode"
>
<span class="topbar-ai-mode-toggle__glyph">AI</span>
</button>
</div>
</div>
</header>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
@@ -394,14 +424,18 @@ const props = defineProps({
type: Object,
default: () => null
},
workbenchSummary: {
type: Object,
default: () => null
},
companyName: {
type: String,
default: ''
},
workbenchSummary: {
type: Object,
default: () => null
},
workbenchMode: {
type: String,
default: 'traditional'
},
companyName: {
type: String,
default: ''
},
detailMode: {
type: Boolean,
default: false
@@ -431,10 +465,11 @@ const emit = defineEmits([
'update:overviewDashboard',
'batchApprove',
'openChat',
'newApplication',
'openDocument',
'navigate'
])
'newApplication',
'openDocument',
'navigate',
'toggleWorkbenchMode'
])
const isChat = computed(() => props.activeView === 'chat')
const isOverview = computed(() => props.activeView === 'overview')
const isWorkbench = computed(() => props.activeView === 'workbench')
@@ -444,12 +479,16 @@ const isRequests = computed(() => props.activeView === 'requests')
const isDigitalEmployees = computed(() => props.activeView === 'digitalEmployees')
const isApproval = computed(() => props.activeView === 'approval')
const isPolicies = computed(() => props.activeView === 'policies')
const isEmployees = computed(() => props.activeView === 'employees')
const isEmployees = computed(() => props.activeView === 'employees')
const eyebrowLabel = computed(() => (
String(props.currentView?.eyebrow || '').trim()
|| (isChat.value ? 'Smart Finance Q&A' : 'Smart Expense Operations')
))
const displayCompanyName = computed(() => String(props.companyName || '远光软件股份有限公司').trim() || '远光软件股份有限公司')
const isTopbarAiMode = computed(() => props.workbenchMode === 'ai')
const topbarWorkbenchModeTitle = computed(() => (isTopbarAiMode.value ? 'AI 模式,点击切换传统模式' : '传统模式,点击切换 AI 模式'))
const isWorkbenchAiHome = computed(() => isWorkbench.value && isTopbarAiMode.value)
const showAiModeUtilityActions = computed(() => isTopbarAiMode.value && !isWorkbench.value)
const MAX_NOTIFICATION_ITEMS = 30
const {
markDocumentInboxRowRead,
@@ -576,12 +615,16 @@ const readNotifications = computed(() => notificationItems.value.filter((item) =
const activeNotifications = computed(() => (
notificationTab.value === 'unread' ? unreadNotifications.value : readNotifications.value
))
const topbarNotificationCount = computed(() => {
const count = unreadNotifications.value.length
return count > 0 ? Math.min(count, 99) : 0
})
function clearDocumentInboxInitialRefreshTimer() {
const topbarNotificationCount = computed(() => {
const count = unreadNotifications.value.length
return count > 0 ? Math.min(count, 99) : 0
})
function toggleTopbarWorkbenchMode() {
emit('toggleWorkbenchMode')
}
function clearDocumentInboxInitialRefreshTimer() {
if (documentInboxInitialRefreshTimer && typeof window !== 'undefined') {
window.clearTimeout(documentInboxInitialRefreshTimer)
documentInboxInitialRefreshTimer = null