feat(web): AI 文档查询卡片重构与单号判定统一

- documentClassification 抽出 isApplicationDocumentNo,统一兼容 AP-/APP- 旧格式与 A+8 新格式,aiDocumentQueryModel 复用
- aiDocumentQueryModel 文档卡片改为结构化字段布局(单据类型/金额/申请人/编号/操作),新增查询范围摘要区,渲染走 HTML 信任块
- AppShellRouteView/useAppShell/useRequests/detailAlerts/riskVisibility 等差旅详情模型适配单号判定
- 同步更新 ai-document-query-model/workbench-ai-mode-switch 测试,新增 document-classification 测试
This commit is contained in:
caoxiaozhu
2026-06-20 22:04:37 +08:00
parent 8158716e23
commit 3b74a330a3
17 changed files with 348 additions and 209 deletions

View File

@@ -273,7 +273,27 @@ const sidebarCollapsed = ref(false)
const sidebarCollapsedBeforeAiMode = ref(false)
const mobileSidebarOpen = ref(false)
const overviewDashboard = ref('finance')
const workbenchMode = ref('traditional')
const { companyProfile, currentUser, logout } = useSystemState()
function resolveDefaultWorkbenchMode(user) {
return isPlatformAdminUser(user) ? 'traditional' : 'ai'
}
function resolveWorkbenchUserKey(user = {}) {
const roleCodes = Array.isArray(user?.roleCodes) ? user.roleCodes.join(',') : ''
return [
user?.id,
user?.userId,
user?.username,
user?.account,
user?.name,
user?.role,
roleCodes,
user?.isAdmin ? 'admin' : 'user'
].map((item) => String(item || '').trim()).join('|')
}
const workbenchMode = ref(resolveDefaultWorkbenchMode(currentUser.value))
const aiSidebarCommandSeq = ref(0)
const aiSidebarCommand = ref({ seq: 0, type: '', payload: null })
const aiActiveConversationId = ref('')
@@ -343,7 +363,6 @@ const {
topBarView
} = useAppShell()
const { companyProfile, currentUser, logout } = useSystemState()
const PRODUCT_DISPLAY_NAME = '易财费控'
const ENTERPRISE_DISPLAY_NAME = '远光软件股份有限公司'
const filteredNavItems = computed(() => filterNavItemsByAccess(navItems, currentUser.value))
@@ -496,7 +515,14 @@ function handleLogout() {
watch(
() => currentUser.value,
(user) => {
(user, previousUser) => {
if (resolveWorkbenchUserKey(user) !== resolveWorkbenchUserKey(previousUser)) {
const nextMode = resolveDefaultWorkbenchMode(user)
workbenchMode.value = nextMode
if (nextMode === 'ai') {
sidebarCollapsed.value = false
}
}
aiConversationHistory.value = loadAiWorkbenchConversationHistory(user || {})
},
{ immediate: true }

View File

@@ -1,3 +1,5 @@
import { isApplicationDocumentNo } from '../../utils/documentClassification.js'
const REQUIRED_APPLICATION_EXPENSE_TYPES = new Set(['travel', 'meal'])
const APPLICATION_TYPE_ALIASES = {
@@ -302,8 +304,7 @@ export function isExpenseApplicationClaim(claim) {
return documentType === 'application'
|| documentType === 'expense_application'
|| claimNo.startsWith('AP-')
|| claimNo.startsWith('APP-')
|| isApplicationDocumentNo(claimNo)
|| expenseType === 'application'
|| expenseType.endsWith('_application')
}

View File

@@ -1,3 +1,5 @@
import { isApplicationDocumentNo } from '../../utils/documentClassification.js'
function normalizeText(value) {
return String(value || '').trim()
}
@@ -22,7 +24,7 @@ function isApplicationDocumentRequest(requestModel) {
|| requestModel?.document_type
).toLowerCase()
const claimNo = normalizeText(requestModel?.claimNo || requestModel?.claim_no || requestModel?.documentNo).toUpperCase()
return documentType === 'application' || claimNo.startsWith('AP-') || claimNo.startsWith('APP-')
return documentType === 'application' || isApplicationDocumentNo(claimNo)
}
function isHotelExpenseItem(item) {

View File

@@ -1,3 +1,5 @@
import { isApplicationDocumentNo } from '../../utils/documentClassification.js'
function normalizeText(value) {
return String(value || '').trim()
}
@@ -112,7 +114,7 @@ export function resolveRequestBusinessStage(request = {}) {
|| request?.document_no
|| request?.id
).toUpperCase()
if (claimNo.startsWith('AP-') || claimNo.startsWith('APP-')) {
if (isApplicationDocumentNo(claimNo)) {
return 'expense_application'
}

View File

@@ -1,3 +1,5 @@
import { isApplicationDocumentNo } from '../../utils/documentClassification.js'
export const EXPENSE_TYPE_OPTIONS = [
{ value: 'travel', label: '差旅费' },
{ value: 'train_ticket', label: '火车票' },
@@ -83,8 +85,7 @@ export function isApplicationDocumentRequest(request) {
return (
documentType === 'application'
|| documentType === 'expense_application'
|| claimNo.startsWith('AP-')
|| claimNo.startsWith('APP-')
|| isApplicationDocumentNo(claimNo)
|| typeCode === 'application'
|| typeCode.endsWith('_application')
)

View File

@@ -718,7 +718,7 @@ export function useTravelReimbursementFlow({
function buildApplicationDuplicateDetail(payload) {
const result = payload?.result && typeof payload.result === 'object' ? payload.result : {}
const answer = String(result.answer || result.message || '').trim()
const claimNo = answer.match(/AP-\d{14}-[A-HJ-NP-Z2-9]{8}/)?.[0] || ''
const claimNo = answer.match(/A[A-HJ-NP-Z2-9]{8}|AP-\d{14}-[A-HJ-NP-Z2-9]{8}|APP-\d{8}-[A-Z0-9]{6}/)?.[0] || ''
return claimNo
? `已拦截重复申请,已有申请单:${claimNo}`
: '已拦截重复申请,未创建新申请单'

View File

@@ -5,6 +5,7 @@ import {
collectReceiptFiles
} from './travelReimbursementAttachmentModel.js'
import { resolveAssistantScopeGuard } from '../../utils/assistantSessionScope.js'
import { isApplicationDocumentNo } from '../../utils/documentClassification.js'
import {
APPLICATION_TRANSPORT_MODE_OPTIONS,
applyApplicationBusinessTimeContext,
@@ -148,8 +149,7 @@ function isApplicationClaimRecord(claim) {
expenseType === 'application' ||
expenseType === 'expense_application' ||
expenseType.endsWith('_application') ||
claimNo.startsWith('AP-') ||
claimNo.startsWith('APP-') ||
isApplicationDocumentNo(claimNo) ||
Boolean(extractApplicationDetailFromClaim(claim))
)
}