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

@@ -3,6 +3,7 @@
class="app"
:class="{
'sidebar-collapsed': sidebarCollapsed,
'workbench-ai-sidebar-active': isAiShellMode,
'mobile-sidebar-open': mobileSidebarOpen,
'login-entry-active': loginEntryAnimating
}"
@@ -29,18 +30,39 @@
</div>
</Transition>
<div class="app-sidebar">
<SidebarRail
:nav-items="filteredNavItems"
:active-view="activeView"
:company-name="PRODUCT_DISPLAY_NAME"
:company-logo="companyProfile.logo"
:current-user="currentUser"
:collapsed="sidebarCollapsed"
@open-chat="openSmartEntry"
@logout="handleLogout"
@toggle-collapse="toggleSidebarCollapsed"
@navigate="handleNavigateWithMobileClose"
/>
<Transition name="sidebar-mode-fade" mode="out-in">
<AiSidebarRail
v-if="isAiShellMode"
key="ai-sidebar"
:nav-items="filteredNavItems"
:active-view="activeView"
:active-conversation-id="aiActiveConversationId"
:conversation-history="aiConversationHistory"
:current-user="currentUser"
:brand-name="PRODUCT_DISPLAY_NAME"
:brand-logo="companyProfile.logo"
:collapsed="sidebarCollapsed"
@navigate="handleNavigateWithMobileClose"
@new-chat="openAiSidebarNewChat"
@open-recent="openAiSidebarRecent"
@rename-conversation="handleAiConversationRename"
@logout="handleLogout"
/>
<SidebarRail
v-else
key="standard-sidebar"
:nav-items="filteredNavItems"
:active-view="activeView"
:company-name="PRODUCT_DISPLAY_NAME"
:company-logo="companyProfile.logo"
:current-user="currentUser"
:collapsed="sidebarCollapsed"
@open-chat="openSmartEntry"
@logout="handleLogout"
@toggle-collapse="toggleSidebarCollapsed"
@navigate="handleNavigateWithMobileClose"
/>
</Transition>
</div>
<main
@@ -72,6 +94,7 @@
:request-summary="requestSummary"
:document-summary="documentSummary"
:workbench-summary="workbenchSummary"
:workbench-mode="workbenchMode"
:digital-employee-summary="digitalEmployeeSummary"
:company-name="ENTERPRISE_DISPLAY_NAME"
:detail-mode="resolvedDetailMode"
@@ -87,6 +110,7 @@
@new-application="openExpenseApplicationCreate"
@open-document="openWorkbenchDocument"
@navigate="handleNavigate"
@toggle-workbench-mode="toggleWorkbenchMode"
/>
<FilterBar
@@ -104,6 +128,7 @@
'documents-workarea': activeView === 'documents',
'receipt-folder-workarea': activeView === 'receiptFolder',
'workbench-workarea': activeView === 'workbench',
'workbench-workarea-ai-mode': isWorkbenchAiMode,
'budget-workarea': activeView === 'budget',
'policies-workarea': activeView === 'policies',
'audit-workarea': activeView === 'audit',
@@ -126,6 +151,10 @@
v-else-if="activeView === 'workbench'"
:assistant-modal-open="smartEntryOpen"
:workbench-summary="workbenchSummary"
:workbench-mode="workbenchMode"
:ai-sidebar-command="aiSidebarCommand"
@ai-conversation-change="handleAiConversationChange"
@ai-conversation-history-change="handleAiConversationHistoryChange"
@open-assistant="openSmartEntry"
@open-document="openWorkbenchDocument"
/>
@@ -207,8 +236,9 @@
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import AiSidebarRail from '../components/layout/AiSidebarRail.vue'
import SidebarRail from '../components/layout/SidebarRail.vue'
import TopBar from '../components/layout/TopBar.vue'
import FilterBar from '../components/layout/FilterBar.vue'
@@ -229,6 +259,7 @@ import TravelRequestDetailView from './TravelRequestDetailView.vue'
import { useAppShell } from '../composables/useAppShell.js'
import { useSystemState } from '../composables/useSystemState.js'
import { filterNavItemsByAccess } from '../utils/accessControl.js'
import { loadAiWorkbenchConversationHistory, saveAiWorkbenchConversation } from '../utils/aiWorkbenchConversationStore.js'
import { consumeLoginEntryTransition } from '../utils/loginEntryTransition.js'
const employeeSummary = ref(null)
@@ -241,9 +272,15 @@ const digitalEmployeeDetailOpen = ref(false)
const receiptFolderDetailOpen = ref(false)
const budgetDetailOpen = ref(false)
const loginEntryAnimating = ref(false)
const sidebarCollapsed = ref(true)
const sidebarCollapsed = ref(false)
const sidebarCollapsedBeforeAiMode = ref(false)
const mobileSidebarOpen = ref(false)
const overviewDashboard = ref('finance')
const workbenchMode = ref('traditional')
const aiSidebarCommandSeq = ref(0)
const aiSidebarCommand = ref({ seq: 0, type: '', payload: null })
const aiActiveConversationId = ref('')
const aiConversationHistory = ref([])
let loginEntryTimer = null
function stopLoginEntryAnimation() {
@@ -269,10 +306,23 @@ function toggleSidebarCollapsed() {
}
function handleNavigateWithMobileClose(viewId) {
handleNavigate(viewId)
void handleNavigate(viewId)
mobileSidebarOpen.value = false
}
function toggleWorkbenchMode() {
const nextMode = workbenchMode.value === 'ai' ? 'traditional' : 'ai'
if (nextMode === 'ai') {
sidebarCollapsedBeforeAiMode.value = sidebarCollapsed.value
workbenchMode.value = nextMode
sidebarCollapsed.value = false
return
}
workbenchMode.value = nextMode
sidebarCollapsed.value = sidebarCollapsedBeforeAiMode.value
}
const {
activeRange,
activeView,
@@ -319,6 +369,8 @@ const { companyProfile, currentUser, logout } = useSystemState()
const PRODUCT_DISPLAY_NAME = '易财费控'
const ENTERPRISE_DISPLAY_NAME = '远光软件股份有限公司'
const filteredNavItems = computed(() => filterNavItemsByAccess(navItems, currentUser.value))
const isAiShellMode = computed(() => workbenchMode.value === 'ai')
const isWorkbenchAiMode = computed(() => activeView.value === 'workbench' && workbenchMode.value === 'ai')
const DETAIL_TOPBAR_FALLBACKS = {
audit: {
title: '规则中心详情',
@@ -382,10 +434,82 @@ function openWorkbenchDocument(payload = {}) {
openRequestDetail(request || payload, { returnTo })
}
function dispatchAiSidebarCommand(type, payload = null) {
aiSidebarCommandSeq.value += 1
aiSidebarCommand.value = {
seq: aiSidebarCommandSeq.value,
type,
payload
}
}
async function openAiConversationWorkspace(type, payload = null) {
if (activeView.value !== 'workbench') {
const navigation = handleNavigate('workbench')
if (navigation && typeof navigation.then === 'function') {
await navigation
}
await nextTick()
}
dispatchAiSidebarCommand(type, payload)
}
function openAiSidebarNewChat() {
aiActiveConversationId.value = ''
void openAiConversationWorkspace('new-chat')
}
function openAiSidebarRecent(item = {}) {
aiActiveConversationId.value = String(item.id || '').trim()
void openAiConversationWorkspace('open-recent', item)
}
function handleAiConversationChange(payload = {}) {
aiActiveConversationId.value = String(payload.id || '').trim()
}
function handleAiConversationHistoryChange(payload = []) {
aiConversationHistory.value = Array.isArray(payload) ? payload : []
}
function handleAiConversationRename(payload = {}) {
const conversationId = String(payload.id || '').trim()
const title = String(payload.title || '').trim()
if (!conversationId || !title) {
return
}
const target = aiConversationHistory.value.find((item) => String(item.id || '').trim() === conversationId)
if (!target) {
return
}
aiConversationHistory.value = saveAiWorkbenchConversation(currentUser.value || {}, {
...target,
title
})
if (aiActiveConversationId.value === conversationId) {
dispatchAiSidebarCommand('open-recent', {
...target,
title
})
}
}
function handleLogout() {
logout('manual')
}
watch(
() => currentUser.value,
(user) => {
aiConversationHistory.value = loadAiWorkbenchConversationHistory(user || {})
},
{ immediate: true }
)
onMounted(() => {
playLoginEntryAnimation()
})
@@ -393,10 +517,4 @@ onMounted(() => {
onBeforeUnmount(() => {
stopLoginEntryAnimation()
})
watch(activeView, (newView) => {
if (newView === 'workbench') {
sidebarCollapsed.value = true
}
}, { immediate: true })
</script>