feat(web): 工作台 AI 模式与差旅/风险建议交互优化
- 新增 PersonalWorkbenchAiMode 组件、AI 侧边栏与 orb 机器人视觉资源 - 新增 aiApplicationDraftModel / aiExpenseDraftModel / aiWorkbenchConversationStore 及业务准入 aiSidebarBusinessAccess,支撑 AI 模式下的申请与报销草稿 - 顶栏、侧边栏、工作台样式重构,适配 AI 模式切换与响应式布局 - 同步 steward plan/off_topic、差旅报销引导流、风险建议卡片等测试
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user