feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造
- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制 - 引入费用审批动态路由、平台风险分级、预审与风险阶段管理 - 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板 - 新增 Hermes 风险线索收集器、Agent 链路追踪中心 - 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估 - 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
@@ -91,7 +91,11 @@ export function useAppShell() {
|
||||
})
|
||||
|
||||
const detailMode = computed(() => route.name === 'app-document-detail')
|
||||
const detailAlerts = computed(() => (detailMode.value ? buildDetailAlerts(selectedRequest.value) : []))
|
||||
const detailAlerts = computed(() => (
|
||||
detailMode.value
|
||||
? buildDetailAlerts(selectedRequest.value, { currentUser: currentUser.value })
|
||||
: []
|
||||
))
|
||||
|
||||
const requestsNeeded = computed(() => ['documents', 'workbench'].includes(activeView.value))
|
||||
|
||||
|
||||
@@ -127,8 +127,12 @@ export function useChat(activeView) {
|
||||
is_admin: Boolean(user.isAdmin),
|
||||
name: user.name || '',
|
||||
role: user.role || '',
|
||||
department: user.department || user.departmentName || '',
|
||||
department_name: user.departmentName || user.department || '',
|
||||
position: user.position || '',
|
||||
grade: user.grade || '',
|
||||
employee_no: user.employeeNo || '',
|
||||
manager_name: user.managerName || user.manager_name || '',
|
||||
active_case_id: activeCase.value?.id || ''
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { fetchFinanceDashboard, fetchSystemDashboard } from '../services/analytics.js'
|
||||
import {
|
||||
fetchDigitalEmployeeDashboard,
|
||||
fetchFinanceDashboard,
|
||||
fetchSystemDashboard
|
||||
} from '../services/analytics.js'
|
||||
import { fetchRiskObservationDashboard } from '../services/riskObservations.js'
|
||||
import {
|
||||
buildDigitalEmployeeCategoryRows,
|
||||
buildDigitalEmployeeDailyRows,
|
||||
buildDigitalEmployeeKpiMetrics,
|
||||
buildDigitalEmployeeTaskRanking,
|
||||
emptyDigitalEmployeeDashboard
|
||||
} from '../views/scripts/overviewDigitalEmployeeDashboardModel.js'
|
||||
|
||||
import {
|
||||
metricBlueprints,
|
||||
systemMetricBlueprints,
|
||||
trendRanges,
|
||||
trendSeries,
|
||||
spendByCategory as fallbackSpendByCategory,
|
||||
exceptionMix as fallbackExceptionMix,
|
||||
departmentRangeOptions,
|
||||
bottlenecks as fallbackBottlenecks,
|
||||
budgetSummary as fallbackBudgetSummary,
|
||||
systemDashboardTotals as fallbackSystemDashboardTotals,
|
||||
systemAgentDailyRatio as fallbackSystemAgentDailyRatio,
|
||||
systemLoginWave as fallbackSystemLoginWave,
|
||||
@@ -30,6 +36,33 @@ import {
|
||||
systemToolDetailRows as fallbackSystemToolDetailRows
|
||||
} from '../data/metrics.js'
|
||||
|
||||
const emptyFinanceTotals = {
|
||||
pendingCount: 0,
|
||||
pendingAmount: 0,
|
||||
avgSla: 0,
|
||||
autoPassRate: 0,
|
||||
riskCount: 0,
|
||||
slaRate: 0
|
||||
}
|
||||
|
||||
const emptyFinanceTrend = {
|
||||
labels: [],
|
||||
applications: [],
|
||||
approved: [],
|
||||
avgHours: []
|
||||
}
|
||||
|
||||
const emptyFinanceDonut = [
|
||||
{ name: '暂无数据', value: 0, color: '#cbd5e1' }
|
||||
]
|
||||
|
||||
const emptyFinanceBudgetSummary = {
|
||||
ratio: 0,
|
||||
total: '¥0',
|
||||
used: '¥0',
|
||||
left: '¥0'
|
||||
}
|
||||
|
||||
export function useOverviewView(options = {}) {
|
||||
const activeTrendRange = ref(trendRanges[0])
|
||||
const activeDepartmentRange = ref(departmentRangeOptions[0])
|
||||
@@ -48,23 +81,9 @@ export function useOverviewView(options = {}) {
|
||||
const riskDashboardPayload = ref(null)
|
||||
const riskDashboardLoading = ref(false)
|
||||
const riskDashboardError = ref(null)
|
||||
|
||||
const demoTotals = {
|
||||
pendingCount: 128,
|
||||
pendingAmount: 361600,
|
||||
avgSla: 6.8,
|
||||
autoPassRate: 78,
|
||||
riskCount: 14,
|
||||
slaRate: 96
|
||||
}
|
||||
|
||||
const demoDepartments = [
|
||||
{ name: '销售部', amount: 182000, color: 'var(--theme-primary)' },
|
||||
{ name: '研发中心', amount: 146000, color: 'var(--chart-blue)' },
|
||||
{ name: '市场部', amount: 96000, color: 'var(--chart-amber)' },
|
||||
{ name: '运营部', amount: 68600, color: 'var(--chart-purple)' },
|
||||
{ name: '行政部', amount: 48300, color: 'var(--chart-blue)' }
|
||||
]
|
||||
const digitalEmployeeDashboardPayload = ref(null)
|
||||
const digitalEmployeeDashboardLoading = ref(false)
|
||||
const digitalEmployeeDashboardError = ref(null)
|
||||
|
||||
const formatCompact = (value) => {
|
||||
if (value >= 1_000_000) return `¥${(value / 1_000_000).toFixed(1)}M`
|
||||
@@ -163,6 +182,23 @@ export function useOverviewView(options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
const loadDigitalEmployeeDashboard = async () => {
|
||||
digitalEmployeeDashboardLoading.value = true
|
||||
digitalEmployeeDashboardError.value = null
|
||||
|
||||
try {
|
||||
digitalEmployeeDashboardPayload.value = await fetchDigitalEmployeeDashboard({
|
||||
days: 7,
|
||||
limit: 300
|
||||
})
|
||||
} catch (error) {
|
||||
digitalEmployeeDashboardPayload.value = null
|
||||
digitalEmployeeDashboardError.value = error
|
||||
} finally {
|
||||
digitalEmployeeDashboardLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const setRiskWindowDays = (value) => {
|
||||
const days = Number(value || 30)
|
||||
const matched = riskWindowOptions.some((item) => Number(item.value) === days)
|
||||
@@ -173,6 +209,7 @@ export function useOverviewView(options = {}) {
|
||||
void loadFinanceDashboard()
|
||||
void loadSystemDashboard()
|
||||
void loadRiskDashboard()
|
||||
void loadDigitalEmployeeDashboard()
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -224,14 +261,15 @@ export function useOverviewView(options = {}) {
|
||||
windowDays: activeRiskWindowDays.value,
|
||||
totalObservations: 0,
|
||||
pendingCount: 0,
|
||||
riskClueCount: 0,
|
||||
highOrAboveCount: 0,
|
||||
confirmedCount: 0,
|
||||
falsePositiveCount: 0,
|
||||
feedbackSampleCount: 0,
|
||||
totalAmount: 0,
|
||||
averageScore: 0,
|
||||
confirmationRate: 0,
|
||||
falsePositiveRate: 0,
|
||||
candidateRuleCount: 0,
|
||||
levelDistribution: {},
|
||||
statusDistribution: {},
|
||||
signalDistribution: {},
|
||||
@@ -252,29 +290,32 @@ export function useOverviewView(options = {}) {
|
||||
recentHighObservations: []
|
||||
}
|
||||
))
|
||||
const digitalEmployeeDashboard = computed(() => (
|
||||
digitalEmployeeDashboardPayload.value || emptyDigitalEmployeeDashboard
|
||||
))
|
||||
const financeDashboardTotals = computed(() => (
|
||||
financeDashboardPayload.value?.totals || demoTotals
|
||||
financeDashboardPayload.value?.totals || emptyFinanceTotals
|
||||
))
|
||||
const financeMetricMeta = computed(() => (
|
||||
financeDashboardPayload.value?.metricMeta || {}
|
||||
))
|
||||
const financeTrend = computed(() => (
|
||||
financeDashboardPayload.value?.trend || trendSeries[activeTrendRange.value]
|
||||
financeDashboardPayload.value?.trend || emptyFinanceTrend
|
||||
))
|
||||
const financeSpendByCategory = computed(() => (
|
||||
financeDashboardPayload.value?.spendByCategory || fallbackSpendByCategory
|
||||
financeDashboardPayload.value?.spendByCategory || emptyFinanceDonut
|
||||
))
|
||||
const financeExceptionMix = computed(() => (
|
||||
financeDashboardPayload.value?.exceptionMix || fallbackExceptionMix
|
||||
financeDashboardPayload.value?.exceptionMix || emptyFinanceDonut
|
||||
))
|
||||
const financeDepartmentRanking = computed(() => (
|
||||
financeDashboardPayload.value?.departmentRanking || demoDepartments
|
||||
financeDashboardPayload.value?.departmentRanking || []
|
||||
))
|
||||
const financeBottlenecks = computed(() => (
|
||||
financeDashboardPayload.value?.bottlenecks || fallbackBottlenecks
|
||||
financeDashboardPayload.value?.bottlenecks || []
|
||||
))
|
||||
const financeBudgetSummary = computed(() => (
|
||||
financeDashboardPayload.value?.budgetSummary || fallbackBudgetSummary
|
||||
financeDashboardPayload.value?.budgetSummary || emptyFinanceBudgetSummary
|
||||
))
|
||||
|
||||
const resolveSystemMetricMeta = (metric) => {
|
||||
@@ -327,8 +368,8 @@ export function useOverviewView(options = {}) {
|
||||
|
||||
if (!financeDashboardPayload.value || !meta) {
|
||||
return {
|
||||
changeText: metric.change,
|
||||
delta: metric.delta,
|
||||
changeText: financeDashboardLoading.value ? '加载中' : '实时',
|
||||
delta: financeDashboardError.value ? '真实数据加载失败' : '等待真实数据',
|
||||
trend: metric.trend
|
||||
}
|
||||
}
|
||||
@@ -432,6 +473,10 @@ export function useOverviewView(options = {}) {
|
||||
}))
|
||||
})
|
||||
|
||||
const digitalEmployeeKpiMetrics = computed(() => (
|
||||
buildDigitalEmployeeKpiMetrics(digitalEmployeeDashboard.value, formatNumberCompact)
|
||||
))
|
||||
|
||||
const activeTrend = computed(() => financeTrend.value)
|
||||
const spendTotal = computed(() => financeSpendByCategory.value.reduce((sum, item) => sum + Number(item.value || 0), 0))
|
||||
const riskTotal = computed(() => financeExceptionMix.value.reduce((sum, item) => sum + Number(item.value || 0), 0))
|
||||
@@ -585,6 +630,9 @@ export function useOverviewView(options = {}) {
|
||||
highWidth: `${Math.max((item.highOrAbove / maxValue) * 100, item.highOrAbove ? 4 : 0)}%`
|
||||
}))
|
||||
})
|
||||
const digitalEmployeeDailyRows = computed(() => buildDigitalEmployeeDailyRows(digitalEmployeeDashboard.value))
|
||||
const digitalEmployeeTaskRanking = computed(() => buildDigitalEmployeeTaskRanking(digitalEmployeeDashboard.value))
|
||||
const digitalEmployeeCategoryRows = computed(() => buildDigitalEmployeeCategoryRows(digitalEmployeeDashboard.value))
|
||||
|
||||
function buildRiskDistributionLegend(distribution, labels, colors) {
|
||||
const entries = Object.entries(distribution || {})
|
||||
@@ -635,6 +683,13 @@ export function useOverviewView(options = {}) {
|
||||
bottlenecks,
|
||||
budgetSummary,
|
||||
departmentRangeOptions,
|
||||
digitalEmployeeCategoryRows,
|
||||
digitalEmployeeDashboard,
|
||||
digitalEmployeeDashboardError,
|
||||
digitalEmployeeDashboardLoading,
|
||||
digitalEmployeeDailyRows,
|
||||
digitalEmployeeKpiMetrics,
|
||||
digitalEmployeeTaskRanking,
|
||||
exceptionMix,
|
||||
financeDashboardError,
|
||||
financeDashboardLoading,
|
||||
@@ -688,7 +743,6 @@ export function useOverviewView(options = {}) {
|
||||
systemToolRankings,
|
||||
systemToolTotal,
|
||||
systemTrendSeries,
|
||||
trendRanges,
|
||||
trendSeries
|
||||
trendRanges
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,12 @@ const APPLICATION_PROGRESS_LABELS = [
|
||||
'审批完成'
|
||||
]
|
||||
|
||||
const APPLICATION_PROGRESS_LABELS_WITHOUT_BUDGET = [
|
||||
'创建申请',
|
||||
'直属领导审批',
|
||||
'审批完成'
|
||||
]
|
||||
|
||||
function parseNumber(value) {
|
||||
const nextValue = Number(value)
|
||||
return Number.isFinite(nextValue) ? nextValue : 0
|
||||
@@ -471,11 +477,13 @@ function resolveApplicationApproverName(claim) {
|
||||
function resolveApplicationBudgetApproverName(claim) {
|
||||
const routeEvent = findApprovalEventForStep(claim, '直属领导审批')
|
||||
return resolveDisplayName(
|
||||
claim?.budget_approver_name,
|
||||
claim?.budgetApproverName,
|
||||
routeEvent?.next_approver_name,
|
||||
routeEvent?.nextApproverName,
|
||||
routeEvent?.budget_approver_name,
|
||||
routeEvent?.budgetApproverName
|
||||
) || 'P8预算监控者'
|
||||
) || '预算管理者'
|
||||
}
|
||||
|
||||
function resolveProgressDisplayLabel(label, documentTypeCode, claim, approvalMeta) {
|
||||
@@ -592,19 +600,36 @@ function findLatestPaymentEvent(claim) {
|
||||
)
|
||||
}
|
||||
|
||||
function findApplicationHandoffEvent(claim) {
|
||||
const handoffEvents = getRiskFlags(claim).filter((flag) => (
|
||||
flag
|
||||
&& typeof flag === 'object'
|
||||
&& normalizeText(flag.source) === 'application_handoff'
|
||||
&& normalizeText(flag.application_claim_no || flag.applicationClaimNo)
|
||||
))
|
||||
return getLatestEvent(handoffEvents) || handoffEvents[handoffEvents.length - 1] || null
|
||||
}
|
||||
|
||||
function normalizeApplicationHandoffDetail(flag = {}) {
|
||||
const detail = flag?.application_detail || flag?.applicationDetail || {}
|
||||
return detail && typeof detail === 'object' ? detail : {}
|
||||
const reviewValues = flag?.review_form_values || flag?.reviewFormValues || {}
|
||||
const sceneSelection = flag?.expense_scene_selection || flag?.expenseSceneSelection || {}
|
||||
return [sceneSelection, reviewValues, detail]
|
||||
.filter((item) => item && typeof item === 'object')
|
||||
.reduce((acc, item) => ({ ...acc, ...item }), {})
|
||||
}
|
||||
|
||||
function resolveApplicationField(flag = {}, detail = {}, snakeKey, camelKey = '') {
|
||||
return normalizeText(
|
||||
flag?.[snakeKey]
|
||||
|| (camelKey ? flag?.[camelKey] : '')
|
||||
|| detail?.[snakeKey]
|
||||
|| (camelKey ? detail?.[camelKey] : '')
|
||||
)
|
||||
}
|
||||
|
||||
function resolveRelatedApplicationClaimNo(flag = {}) {
|
||||
const detail = normalizeApplicationHandoffDetail(flag)
|
||||
return resolveApplicationField(flag, detail, 'application_claim_no', 'applicationClaimNo')
|
||||
}
|
||||
|
||||
function findRelatedApplicationEvent(claim) {
|
||||
const events = getRiskFlags(claim).filter((flag) => (
|
||||
flag
|
||||
&& typeof flag === 'object'
|
||||
&& resolveRelatedApplicationClaimNo(flag)
|
||||
))
|
||||
return getLatestEvent(events) || events[events.length - 1] || null
|
||||
}
|
||||
|
||||
function resolveRelatedApplicationAmountLabel(flag, detail, claim) {
|
||||
@@ -631,53 +656,57 @@ function resolveRelatedApplicationAmountLabel(flag, detail, claim) {
|
||||
}
|
||||
|
||||
function resolveRelatedApplicationInfo(claim, typeLabel = '') {
|
||||
const handoff = findApplicationHandoffEvent(claim)
|
||||
if (!handoff) {
|
||||
const relatedEvent = findRelatedApplicationEvent(claim)
|
||||
if (!relatedEvent) {
|
||||
return null
|
||||
}
|
||||
|
||||
const detail = normalizeApplicationHandoffDetail(handoff)
|
||||
const claimNo = normalizeText(handoff.application_claim_no || handoff.applicationClaimNo)
|
||||
const detail = normalizeApplicationHandoffDetail(relatedEvent)
|
||||
const claimNo = resolveRelatedApplicationClaimNo(relatedEvent)
|
||||
const applicationType = normalizeText(
|
||||
detail.application_type
|
||||
|| detail.applicationType
|
||||
|| handoff.application_type
|
||||
|| handoff.applicationType
|
||||
|| relatedEvent.application_type
|
||||
|| relatedEvent.applicationType
|
||||
|| typeLabel
|
||||
)
|
||||
const location = normalizeText(
|
||||
detail.application_location
|
||||
|| detail.applicationLocation
|
||||
|| detail.location
|
||||
|| handoff.application_location
|
||||
|| handoff.applicationLocation
|
||||
|| relatedEvent.application_location
|
||||
|| relatedEvent.applicationLocation
|
||||
|| claim?.location
|
||||
)
|
||||
const reason = normalizeText(
|
||||
detail.application_reason
|
||||
|| detail.applicationReason
|
||||
|| detail.reason
|
||||
|| handoff.application_reason
|
||||
|| handoff.applicationReason
|
||||
|| relatedEvent.application_reason
|
||||
|| relatedEvent.applicationReason
|
||||
|| claim?.reason
|
||||
)
|
||||
const content = normalizeText(
|
||||
detail.application_content
|
||||
|| detail.applicationContent
|
||||
|| handoff.application_content
|
||||
|| handoff.applicationContent
|
||||
|| relatedEvent.application_content
|
||||
|| relatedEvent.applicationContent
|
||||
) || [applicationType, location].filter(Boolean).join(' / ')
|
||||
const rawTime = normalizeText(
|
||||
detail.application_time
|
||||
|| detail.applicationTime
|
||||
|| detail.time
|
||||
|| handoff.application_time
|
||||
|| handoff.applicationTime
|
||||
|| detail.application_date
|
||||
|| detail.applicationDate
|
||||
|| relatedEvent.application_time
|
||||
|| relatedEvent.applicationTime
|
||||
|| relatedEvent.application_date
|
||||
|| relatedEvent.applicationDate
|
||||
|| claim?.occurred_at
|
||||
)
|
||||
|
||||
return {
|
||||
id: normalizeText(handoff.application_claim_id || handoff.applicationClaimId),
|
||||
id: resolveApplicationField(relatedEvent, detail, 'application_claim_id', 'applicationClaimId'),
|
||||
claimNo,
|
||||
content,
|
||||
reason,
|
||||
@@ -685,19 +714,19 @@ function resolveRelatedApplicationInfo(claim, typeLabel = '') {
|
||||
detail.application_days
|
||||
|| detail.applicationDays
|
||||
|| detail.days
|
||||
|| handoff.application_days
|
||||
|| handoff.applicationDays
|
||||
|| relatedEvent.application_days
|
||||
|| relatedEvent.applicationDays
|
||||
),
|
||||
location,
|
||||
time: formatDate(rawTime) || rawTime,
|
||||
amountLabel: resolveRelatedApplicationAmountLabel(handoff, detail, claim),
|
||||
statusLabel: normalizeText(handoff.application_status_label || handoff.applicationStatusLabel),
|
||||
amountLabel: resolveRelatedApplicationAmountLabel(relatedEvent, detail, claim),
|
||||
statusLabel: resolveApplicationField(relatedEvent, detail, 'application_status_label', 'applicationStatusLabel'),
|
||||
transportMode: normalizeText(
|
||||
detail.application_transport_mode
|
||||
|| detail.applicationTransportMode
|
||||
|| detail.transport_mode
|
||||
|| handoff.application_transport_mode
|
||||
|| handoff.applicationTransportMode
|
||||
|| relatedEvent.application_transport_mode
|
||||
|| relatedEvent.applicationTransportMode
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -743,6 +772,42 @@ function findMergedApplicationBudgetApprovalEvent(claim) {
|
||||
)
|
||||
}
|
||||
|
||||
function applicationRequiresBudgetReviewStep(claim, workflowNode) {
|
||||
const node = normalizeText(workflowNode || claim?.approval_stage || claim?.workflowNode)
|
||||
if (node.includes('预算')) {
|
||||
return true
|
||||
}
|
||||
|
||||
return getRiskFlags(claim).some((flag) => {
|
||||
if (!flag || typeof flag !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
const source = normalizeText(flag.source)
|
||||
const eventType = normalizeText(flag.event_type || flag.eventType)
|
||||
const previousStage = normalizeText(flag.previous_approval_stage || flag.previousApprovalStage)
|
||||
const nextStage = normalizeText(flag.next_approval_stage || flag.nextApprovalStage)
|
||||
const routeDecision = flag.route_decision || flag.routeDecision || {}
|
||||
|
||||
if (source === 'approval_routing' && flag.requires_budget_review === true) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
routeDecision
|
||||
&& typeof routeDecision === 'object'
|
||||
&& routeDecision.requires_budget_review === true
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return (
|
||||
source === 'budget_approval'
|
||||
|| eventType === 'expense_application_budget_approval'
|
||||
|| previousStage.includes('预算')
|
||||
|| nextStage.includes('预算')
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function buildProgressStepMeta(time, detail = '', title = '') {
|
||||
return {
|
||||
time,
|
||||
@@ -788,7 +853,7 @@ function buildCompletedStepMeta(claim, label) {
|
||||
approvalEvent.operatorName,
|
||||
stepLabel === '直属领导审批' ? claim?.manager_name : '',
|
||||
stepLabel === '预算管理者审批' ? approvalEvent.next_approver_name : ''
|
||||
) || (stepLabel === '财务审批' ? '财务' : stepLabel === '预算管理者审批' ? '预算监控者' : '直属领导')
|
||||
) || (stepLabel === '财务审批' ? '财务' : stepLabel === '预算管理者审批' ? '预算管理者' : '直属领导')
|
||||
const approvedAt = formatDateTime(approvalEvent.created_at || approvalEvent.createdAt)
|
||||
return buildProgressStepMeta(`${operator}通过`, approvedAt, `${operator}审批通过 ${approvedAt}`.trim())
|
||||
}
|
||||
@@ -899,13 +964,20 @@ function buildProgressSteps(approvalMeta, workflowNode, claim = {}, options = {}
|
||||
documentTypeCode === DOCUMENT_TYPE_APPLICATION
|
||||
&& Boolean(findMergedApplicationBudgetApprovalEvent(claim))
|
||||
)
|
||||
const shouldShowApplicationBudgetStep = (
|
||||
documentTypeCode === DOCUMENT_TYPE_APPLICATION
|
||||
&& !hasMergedApplicationBudgetApproval
|
||||
&& applicationRequiresBudgetReviewStep(claim, workflowNode)
|
||||
)
|
||||
const progressLabels =
|
||||
documentTypeCode === DOCUMENT_TYPE_APPLICATION
|
||||
? hasApplicationReturnStep
|
||||
? ['创建申请', '直属领导审批', '退回', '待提交']
|
||||
: hasMergedApplicationBudgetApproval
|
||||
? ['创建申请', '直属领导审批', '审批完成']
|
||||
: APPLICATION_PROGRESS_LABELS
|
||||
: shouldShowApplicationBudgetStep
|
||||
? APPLICATION_PROGRESS_LABELS
|
||||
: APPLICATION_PROGRESS_LABELS_WITHOUT_BUDGET
|
||||
: REIMBURSEMENT_PROGRESS_LABELS
|
||||
const currentIndex =
|
||||
documentTypeCode === DOCUMENT_TYPE_APPLICATION
|
||||
@@ -1061,12 +1133,17 @@ export function mapExpenseClaimToRequest(claim) {
|
||||
const expenseItems = buildExpenseItems(claim, riskSummary)
|
||||
const applyDateTime = claim?.submitted_at || claim?.created_at
|
||||
const relatedApplication = isApplicationDocument ? null : resolveRelatedApplicationInfo(claim, typeLabel)
|
||||
const employeeId = String(claim?.employee_id || claim?.employeeId || '').trim()
|
||||
const employeeName = String(claim?.employee_name || claim?.employeeName || '').trim()
|
||||
|
||||
return {
|
||||
id: String(claim?.claim_no || claim?.id || '').trim(),
|
||||
claimNo: String(claim?.claim_no || claim?.id || '').trim(),
|
||||
claimId: String(claim?.id || '').trim(),
|
||||
status: String(claim?.status || '').trim(),
|
||||
employeeId,
|
||||
employee_id: employeeId,
|
||||
profileEmployeeId: employeeId || employeeName,
|
||||
person: String(claim?.employee_name || '').trim() || '待补充',
|
||||
dept: String(claim?.department_name || '').trim() || '待补充',
|
||||
departmentName: String(claim?.department_name || '').trim() || '待补充',
|
||||
@@ -1074,6 +1151,9 @@ export function mapExpenseClaimToRequest(claim) {
|
||||
employeePosition: String(claim?.employee_position || '').trim(),
|
||||
employeeGrade: String(claim?.employee_grade || '').trim(),
|
||||
managerName: resolveDisplayName(claim?.manager_name),
|
||||
budgetApproverName: resolveDisplayName(claim?.budget_approver_name, claim?.budgetApproverName),
|
||||
budgetApproverGrade: String(claim?.budget_approver_grade || claim?.budgetApproverGrade || '').trim(),
|
||||
budgetApproverRoleCode: String(claim?.budget_approver_role_code || claim?.budgetApproverRoleCode || '').trim(),
|
||||
roleLabels: Array.isArray(claim?.role_labels) ? claim.role_labels.filter(Boolean) : [],
|
||||
entity: '',
|
||||
typeCode,
|
||||
|
||||
@@ -8,13 +8,14 @@ import {
|
||||
testBootstrapDatabase,
|
||||
testBootstrapRuntime
|
||||
} from '../services/bootstrap.js'
|
||||
import { login as loginByAccount } from '../services/auth.js'
|
||||
import { setRuntimeApiBaseUrl } from '../services/api.js'
|
||||
import { checkBackendHealth } from './useBackendHealth.js'
|
||||
import { resolveDefaultAuthorizedRoute } from '../utils/accessControl.js'
|
||||
import { useToast } from './useToast.js'
|
||||
import { fetchCurrentAuthUser, login as loginByAccount } from '../services/auth.js'
|
||||
import { setRuntimeApiBaseUrl } from '../services/api.js'
|
||||
import { checkBackendHealth } from './useBackendHealth.js'
|
||||
import { resolveDefaultAuthorizedRoute } from '../utils/accessControl.js'
|
||||
import { useToast } from './useToast.js'
|
||||
import { fetchSettings } from '../services/settings.js'
|
||||
import { setThemeSkin } from './useThemeSkin.js'
|
||||
import { normalizeAuthUserSnapshot } from '../utils/authUser.js'
|
||||
import {
|
||||
clearAuthSessionMetrics,
|
||||
finalizeAuthSession,
|
||||
@@ -140,10 +141,10 @@ function buildLegacyAdminUser(username = '') {
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePlatformAdminFlag(payload, roleCodes = []) {
|
||||
const username = String(payload?.username || payload?.account || '').trim().toLowerCase()
|
||||
const role = String(payload?.role || '').trim().toLowerCase()
|
||||
const normalizedRoleCodes = roleCodes.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean)
|
||||
function resolvePlatformAdminFlag(payload, roleCodes = []) {
|
||||
const username = String(payload?.username || payload?.account || '').trim().toLowerCase()
|
||||
const role = String(payload?.role || '').trim().toLowerCase()
|
||||
const normalizedRoleCodes = roleCodes.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean)
|
||||
|
||||
return (
|
||||
Boolean(payload?.isAdmin)
|
||||
@@ -152,46 +153,36 @@ function resolvePlatformAdminFlag(payload, roleCodes = []) {
|
||||
|| role === '管理员'
|
||||
|| role === '系统管理员'
|
||||
|| normalizedRoleCodes.includes('admin')
|
||||
)
|
||||
}
|
||||
|
||||
function readStoredUser() {
|
||||
if (typeof window === 'undefined') {
|
||||
return buildAnonymousUser()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeStoredAuthUser(payload = {}) {
|
||||
const user = normalizeAuthUserSnapshot(payload, {
|
||||
defaultName: DEFAULT_USER_NAME,
|
||||
defaultRole: DEFAULT_USER_ROLE
|
||||
})
|
||||
|
||||
return {
|
||||
...user,
|
||||
isAdmin: resolvePlatformAdminFlag(payload, user.roleCodes)
|
||||
}
|
||||
}
|
||||
|
||||
function readStoredUser() {
|
||||
if (typeof window === 'undefined') {
|
||||
return buildAnonymousUser()
|
||||
}
|
||||
|
||||
const raw = window.sessionStorage.getItem(AUTH_USER_KEY)
|
||||
|
||||
if (raw) {
|
||||
try {
|
||||
const payload = JSON.parse(raw)
|
||||
if (payload && typeof payload === 'object') {
|
||||
const username = String(payload.username || '').trim()
|
||||
const name = String(payload.name || username || DEFAULT_USER_NAME).trim()
|
||||
const roleCodes = Array.isArray(payload.roleCodes) ? payload.roleCodes.filter(Boolean) : []
|
||||
|
||||
return {
|
||||
username,
|
||||
name,
|
||||
role: String(payload.role || DEFAULT_USER_ROLE),
|
||||
department: String(payload.department || payload.departmentName || ''),
|
||||
departmentName: String(payload.departmentName || payload.department || ''),
|
||||
position: String(payload.position || ''),
|
||||
grade: String(payload.grade || ''),
|
||||
employeeNo: String(payload.employeeNo || payload.employee_no || ''),
|
||||
managerName: String(payload.managerName || payload.manager_name || ''),
|
||||
location: String(payload.location || ''),
|
||||
costCenter: String(payload.costCenter || payload.cost_center || ''),
|
||||
financeOwnerName: String(payload.financeOwnerName || payload.finance_owner_name || ''),
|
||||
riskProfile: payload.riskProfile && typeof payload.riskProfile === 'object' ? payload.riskProfile : {},
|
||||
roleCodes,
|
||||
email: String(payload.email || ''),
|
||||
avatar: String(payload.avatar || name.slice(0, 1).toUpperCase()),
|
||||
isAdmin: resolvePlatformAdminFlag(payload, roleCodes)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return buildLegacyAdminUser(readStoredUsername())
|
||||
try {
|
||||
const payload = JSON.parse(raw)
|
||||
if (payload && typeof payload === 'object') {
|
||||
return normalizeStoredAuthUser(payload)
|
||||
}
|
||||
} catch {
|
||||
return buildLegacyAdminUser(readStoredUsername())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,8 +232,18 @@ function persistAuthState(value, user = null, sessionId = '') {
|
||||
window.sessionStorage.removeItem(AUTH_LAST_ACTIVITY_KEY)
|
||||
clearAuthSessionMetrics()
|
||||
}
|
||||
|
||||
function clearSessionTimeout() {
|
||||
|
||||
function persistAuthUserSnapshot(user = {}) {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedUser = user || buildAnonymousUser()
|
||||
window.sessionStorage.setItem(AUTH_USERNAME_KEY, String(normalizedUser.username || '').trim())
|
||||
window.sessionStorage.setItem(AUTH_USER_KEY, JSON.stringify(normalizedUser))
|
||||
}
|
||||
|
||||
function clearSessionTimeout() {
|
||||
if (typeof window === 'undefined' || !sessionTimeoutHandle) {
|
||||
return
|
||||
}
|
||||
@@ -337,10 +338,10 @@ function installSessionMonitoring() {
|
||||
window.addEventListener('beforeunload', handleSessionUnload, { passive: true })
|
||||
}
|
||||
|
||||
function syncAuthSession(options = {}) {
|
||||
const shouldNotify = Boolean(options.notify)
|
||||
|
||||
if (!readAuthState()) {
|
||||
function syncAuthSession(options = {}) {
|
||||
const shouldNotify = Boolean(options.notify)
|
||||
|
||||
if (!readAuthState()) {
|
||||
loggedIn.value = false
|
||||
currentUser.value = buildAnonymousUser()
|
||||
clearSessionTimeout()
|
||||
@@ -354,11 +355,31 @@ function syncAuthSession(options = {}) {
|
||||
|
||||
loggedIn.value = true
|
||||
currentUser.value = readStoredUser()
|
||||
scheduleSessionTimeout()
|
||||
return true
|
||||
}
|
||||
|
||||
function reconcileEntryRoute(router) {
|
||||
scheduleSessionTimeout()
|
||||
return true
|
||||
}
|
||||
|
||||
async function refreshCurrentUserFromBackend(options = {}) {
|
||||
if (!readAuthState()) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await fetchCurrentAuthUser()
|
||||
const user = normalizeStoredAuthUser(payload)
|
||||
currentUser.value = user
|
||||
persistAuthUserSnapshot(user)
|
||||
return true
|
||||
} catch (error) {
|
||||
if (!options.silent) {
|
||||
toast(error.message || '当前用户信息刷新失败,请重新登录后再试。')
|
||||
}
|
||||
console.warn('Failed to refresh current user snapshot:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function reconcileEntryRoute(router) {
|
||||
const target = resolveEntryRoute()
|
||||
const current = router.currentRoute.value
|
||||
|
||||
@@ -376,10 +397,13 @@ export function installSessionNavigation(router) {
|
||||
}
|
||||
|
||||
fetchBootstrapState()
|
||||
.then((state) => {
|
||||
applyBootstrapState(state)
|
||||
setRuntimeApiBaseUrl(resolveBrowserApiBaseUrl(state))
|
||||
fetchSettings()
|
||||
.then((state) => {
|
||||
applyBootstrapState(state)
|
||||
setRuntimeApiBaseUrl(resolveBrowserApiBaseUrl(state))
|
||||
if (loggedIn.value) {
|
||||
refreshCurrentUserFromBackend({ silent: true })
|
||||
}
|
||||
fetchSettings()
|
||||
.then((snapshot) => {
|
||||
if (snapshot?.appearanceForm?.themeSkin) {
|
||||
setThemeSkin(snapshot.appearanceForm.themeSkin)
|
||||
@@ -653,11 +677,11 @@ async function handleLogin(credentials) {
|
||||
password: credentials.password
|
||||
})
|
||||
|
||||
const responseUser = response?.user || buildAnonymousUser()
|
||||
const responseRoleCodes = Array.isArray(responseUser.roleCodes) ? responseUser.roleCodes.filter(Boolean) : []
|
||||
const user = {
|
||||
...responseUser,
|
||||
roleCodes: responseRoleCodes,
|
||||
const responseUser = normalizeStoredAuthUser(response?.user || buildAnonymousUser())
|
||||
const responseRoleCodes = responseUser.roleCodes
|
||||
const user = {
|
||||
...responseUser,
|
||||
roleCodes: responseRoleCodes,
|
||||
isAdmin: resolvePlatformAdminFlag(responseUser, responseRoleCodes)
|
||||
}
|
||||
loggedIn.value = true
|
||||
@@ -738,8 +762,9 @@ export function useSystemState() {
|
||||
loginError,
|
||||
loginSubmitting,
|
||||
logout,
|
||||
resetFromClientEnv,
|
||||
resolveEntryRoute,
|
||||
resetFromClientEnv,
|
||||
refreshCurrentUserFromBackend,
|
||||
resolveEntryRoute,
|
||||
runtimeTestMessage,
|
||||
runtimeTestPassed,
|
||||
runtimeTesting,
|
||||
|
||||
Reference in New Issue
Block a user