feat: 数字员工财务报告体系与定时提醒及看板快照调度

- 新增数字员工财务报告生成、邮件投递与渲染调度器
- 引入员工画像扫描调度与定时提醒任务
- 完善财务看板快照、排行口径与部门人员占比计算
- 优化数字员工工作看板仪表盘与技能目录
- 增强前端总览页图表、工作台摘要与顶部导航栏交互
- 新增差旅申请规划推动提醒与报销创建会话状态管理
- 补充财务报告、看板调度、数字员工工作记录测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-03 09:25:23 +08:00
parent 0c74b4ab4a
commit 15006a05a7
114 changed files with 7356 additions and 650 deletions

View File

@@ -2,9 +2,13 @@ import { ref } from 'vue'
import {
APPLICATION_TRANSPORT_MODE_OPTIONS,
applyApplicationPolicyEstimateError,
applyApplicationPolicyEstimateResult,
buildApplicationPreviewRows,
buildApplicationPolicyEstimateRequest,
buildLocalApplicationPreviewMessage,
normalizeApplicationPreview,
resolveApplicationDaysFromDateRange,
refreshApplicationPreviewTransportEstimate
} from '../../utils/expenseApplicationPreview.js'
import { waitForMockApplicationTransportQuote } from '../../utils/expenseApplicationEstimate.js'
@@ -44,6 +48,27 @@ function shouldRefreshTransportEstimate(fieldKey) {
return ['transportMode', 'time', 'location', 'days'].includes(fieldKey)
}
function resolveEditorCurrentUser(currentUser) {
if (currentUser && typeof currentUser === 'object' && 'value' in currentUser) {
return currentUser.value || {}
}
return currentUser || {}
}
function buildEditedApplicationPreviewFields(fields = {}, editor = {}, nextValue = '') {
const nextFields = {
...fields,
[editor.fieldKey]: nextValue
}
if (editor.fieldKey === 'time') {
const resolvedDays = resolveApplicationDaysFromDateRange(nextValue)
if (resolvedDays) {
nextFields.days = resolvedDays
}
}
return nextFields
}
function buildTransportEstimatePendingPreview(preview = {}) {
const fields = preview?.fields || {}
return normalizeApplicationPreview({
@@ -57,9 +82,29 @@ function buildTransportEstimatePendingPreview(preview = {}) {
})
}
export function useApplicationPreviewEditor({ persistSessionState, toast } = {}) {
export function useApplicationPreviewEditor({
persistSessionState,
toast,
calculateTravelReimbursement,
currentUser
} = {}) {
const applicationPreviewEditor = ref(buildEmptyEditor())
async function refreshApplicationPreviewEstimate(preview = {}) {
const user = resolveEditorCurrentUser(currentUser)
const estimateRequest = buildApplicationPolicyEstimateRequest(preview, user)
if (estimateRequest.canCalculate && typeof calculateTravelReimbursement === 'function') {
try {
const result = await calculateTravelReimbursement(estimateRequest.payload)
return applyApplicationPolicyEstimateResult(preview, result, user)
} catch (error) {
console.warn('Application preview estimate refresh failed:', error)
return applyApplicationPolicyEstimateError(preview, error, user)
}
}
return refreshApplicationPreviewTransportEstimate(preview)
}
function resolveApplicationPreviewRows(message) {
return buildApplicationPreviewRows(message?.applicationPreview || {})
}
@@ -158,25 +203,29 @@ export function useApplicationPreviewEditor({ persistSessionState, toast } = {})
}
const nextPreview = normalizeApplicationPreview({
...message.applicationPreview,
fields: {
...(message.applicationPreview.fields || {}),
[editor.fieldKey]: nextValue
}
fields: buildEditedApplicationPreviewFields(
message.applicationPreview.fields || {},
editor,
nextValue
)
})
const needRefreshTransport = shouldRefreshTransportEstimate(editor.fieldKey) && String(nextPreview.fields?.transportMode || '').trim()
message.applicationPreview = needRefreshTransport
const needRefreshEstimate = shouldRefreshTransportEstimate(editor.fieldKey)
const transportMode = String(nextPreview.fields?.transportMode || '').trim()
message.applicationPreview = needRefreshEstimate
? buildTransportEstimatePendingPreview(nextPreview)
: nextPreview
message.text = buildLocalApplicationPreviewMessage(message.applicationPreview)
cancelApplicationPreviewEditor()
persistSessionState?.()
if (needRefreshTransport) {
await waitForMockApplicationTransportQuote({
transportMode: nextPreview.fields.transportMode,
location: nextPreview.fields.matchedCity || nextPreview.fields.location,
time: nextPreview.fields.time
})
const refreshedPreview = refreshApplicationPreviewTransportEstimate(nextPreview)
if (needRefreshEstimate) {
if (transportMode) {
await waitForMockApplicationTransportQuote({
transportMode,
location: nextPreview.fields.matchedCity || nextPreview.fields.location,
time: nextPreview.fields.time
})
}
const refreshedPreview = await refreshApplicationPreviewEstimate(nextPreview)
message.applicationPreview = refreshedPreview
message.text = buildLocalApplicationPreviewMessage(refreshedPreview)
persistSessionState?.()