feat: 报销审批流重构与管家计划全链路贯通
- 重构报销状态注册表、审批流路由与平台风险标记 - 完善管家意图规划器与模型计划构建器全链路 - 新增 OCR Worker 脚本、数据库会话管理与通知状态 - 优化文档中心、日志视图、预算中心与员工管理交互 - 增强工作台摘要、图标资源与全局主题样式 - 补充审批路由、状态注册、OCR 服务与管家规划器测试覆盖
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)}`)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user