feat: 报销审批流重构与管家计划全链路贯通

- 重构报销状态注册表、审批流路由与平台风险标记
- 完善管家意图规划器与模型计划构建器全链路
- 新增 OCR Worker 脚本、数据库会话管理与通知状态
- 优化文档中心、日志视图、预算中心与员工管理交互
- 增强工作台摘要、图标资源与全局主题样式
- 补充审批路由、状态注册、OCR 服务与管家规划器测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-06 17:19:07 +08:00
parent f60cebadb8
commit e124e4bbcb
162 changed files with 9161 additions and 1941 deletions

View File

@@ -1,5 +1,7 @@
import { apiRequest } from './api.js'
export const NOTIFICATION_STATE_BATCH_SIZE = 100
function normalizeStateItem(item) {
const notificationId = String(item?.notification_id || item?.notificationId || '').trim()
if (!notificationId) {
@@ -16,6 +18,17 @@ function normalizeStateItem(item) {
}
}
export function chunkNotificationStatePatches(states = [], batchSize = NOTIFICATION_STATE_BATCH_SIZE) {
const size = Math.max(1, Number(batchSize) || NOTIFICATION_STATE_BATCH_SIZE)
const chunks = []
for (let index = 0; index < states.length; index += size) {
chunks.push(states.slice(index, index + size))
}
return chunks
}
export async function fetchNotificationStates() {
const payload = await apiRequest('/notification-states')
return Array.isArray(payload?.states) ? payload.states : []
@@ -30,9 +43,14 @@ export async function patchNotificationStates(states = []) {
return []
}
const payload = await apiRequest('/notification-states', {
method: 'POST',
body: JSON.stringify({ states: normalizedStates })
})
return Array.isArray(payload?.states) ? payload.states : []
let latestStates = []
for (const batch of chunkNotificationStatePatches(normalizedStates)) {
const payload = await apiRequest('/notification-states', {
method: 'POST',
body: JSON.stringify({ states: batch })
})
latestStates = Array.isArray(payload?.states) ? payload.states : latestStates
}
return latestStates
}

View File

@@ -1,16 +1,44 @@
import { apiRequest } from './api.js'
const inflightOcrRequests = new Map()
function buildOcrRequestKey(files = []) {
return files
.map((file) => [
String(file?.name || ''),
String(file?.size || 0),
String(file?.lastModified || 0),
String(file?.receiptId || '')
].join(':'))
.join('|')
}
export function recognizeOcrFiles(files, options = {}) {
const requestKey = buildOcrRequestKey(files)
if (requestKey && inflightOcrRequests.has(requestKey)) {
return inflightOcrRequests.get(requestKey)
}
const formData = new FormData()
for (const file of files) {
formData.append('files', file)
formData.append('receipt_ids', String(file?.receiptId || ''))
}
return apiRequest('/ocr/recognize', {
const request = apiRequest('/ocr/recognize', {
method: 'POST',
body: formData,
contentType: null,
...options
})
if (!requestKey) {
return request
}
inflightOcrRequests.set(requestKey, request)
request.then(
() => inflightOcrRequests.delete(requestKey),
() => inflightOcrRequests.delete(requestKey)
)
return request
}

View File

@@ -1,5 +1,7 @@
import { apiRequest } from './api.js'
export const REIMBURSEMENT_LIST_PREVIEW_PARAMS = Object.freeze({ page: 1, pageSize: 100 })
function buildListQuery(params = {}) {
const search = new URLSearchParams()
const page = params.page
@@ -17,6 +19,14 @@ function buildListQuery(params = {}) {
return query ? `?${query}` : ''
}
export function extractExpenseClaimItems(payload) {
if (Array.isArray(payload)) {
return payload
}
return Array.isArray(payload?.items) ? payload.items : []
}
export function fetchExpenseClaims(params = {}) {
return apiRequest(`/reimbursements/claims${buildListQuery(params)}`)
}

View File

@@ -8,6 +8,22 @@ export function fetchStewardPlan(payload, options = {}) {
})
}
export function fetchStewardSlotDecision(payload, options = {}) {
return apiRequest('/steward/slot-decisions', {
method: 'POST',
body: JSON.stringify(payload),
...options
})
}
export function fetchStewardRuntimeDecision(payload, options = {}) {
return apiRequest('/steward/runtime-decisions', {
method: 'POST',
body: JSON.stringify(payload),
...options
})
}
export async function fetchStewardPlanStream(payload, handlers = {}, options = {}) {
const {
timeoutMs = 0,