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

@@ -34,12 +34,15 @@ export function useAppShell() {
conversation: null,
scope: null,
sessionType: '',
budgetContext: null
budgetContext: null,
initialPromptAutoSubmit: true,
initialApplicationPreview: null
})
const smartEntrySessionId = ref(0)
const smartEntryRevealToken = ref(0)
const smartEntryInvalidatedDraftClaimId = ref('')
const selectedRequestSnapshot = ref(null)
const selectedRequestSnapshot = ref(null)
const documentCenterRefreshToken = ref(0)
const { activeView, currentView, setView } = useNavigation()
const {
@@ -98,12 +101,19 @@ export function useAppShell() {
: []
))
const requestsNeeded = computed(() => ['documents', 'workbench'].includes(activeView.value))
async function reloadDocumentCenterRequests() {
documentCenterRefreshToken.value += 1
return reloadRequests()
}
watch(
requestsNeeded,
(isNeeded) => {
if (isNeeded) {
() => [activeView.value, route.name],
([view]) => {
if (view === 'documents') {
void reloadDocumentCenterRequests()
return
}
if (view === 'workbench') {
void ensureRequestsLoaded()
}
},
@@ -166,10 +176,17 @@ export function useAppShell() {
toast(message)
}
function handleNavigate(view) {
smartEntryOpen.value = false
setView(view)
}
function handleNavigate(view) {
smartEntryOpen.value = false
const shouldRefreshCurrentDocumentCenter =
view === 'documents'
&& activeView.value === 'documents'
&& route.name === 'app-documents'
setView(view)
if (shouldRefreshCurrentDocumentCenter) {
void reloadDocumentCenterRequests()
}
}
function openFinancialAssistantCreate(source) {
if (smartEntryOpen.value) {
@@ -185,7 +202,9 @@ export function useAppShell() {
conversation: null,
scope: null,
sessionType: '',
budgetContext: null
budgetContext: null,
initialPromptAutoSubmit: true,
initialApplicationPreview: null
}
smartEntrySessionId.value += 1
}
@@ -320,6 +339,7 @@ export function useAppShell() {
|| String(payload?.prompt || '').trim()
|| (Array.isArray(payload?.files) && payload.files.length)
|| payload?.conversation
|| payload?.applicationPreview
)
if (smartEntryOpen.value && !shouldReplaceOpenEntry) {
smartEntryRevealToken.value += 1
@@ -342,6 +362,10 @@ export function useAppShell() {
sessionType,
budgetContext: payload.budgetContext && typeof payload.budgetContext === 'object'
? payload.budgetContext
: null,
initialPromptAutoSubmit: payload.initialPromptAutoSubmit !== false,
initialApplicationPreview: payload.applicationPreview && typeof payload.applicationPreview === 'object'
? payload.applicationPreview
: null
}
smartEntrySessionId.value += 1
@@ -410,6 +434,7 @@ export function useAppShell() {
currentView,
customRange,
detailMode,
documentCenterRefreshToken,
filteredRequests,
filters,
handleApprove,
@@ -429,6 +454,7 @@ export function useAppShell() {
requestsError,
requestsLoading,
reloadRequests,
reloadDocumentCenterRequests,
requests,
search,
selectedRequest,

View File

@@ -1,4 +1,4 @@
import { computed, onMounted, ref, watch } from 'vue'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import {
fetchDigitalEmployeeDashboard,
@@ -92,6 +92,9 @@ export function useOverviewView(options = {}) {
const riskDashboardPayload = ref(null)
const riskDashboardLoading = ref(false)
const riskDashboardError = ref(null)
const riskDashboardLastUpdatedAt = ref('')
let riskDashboardRefreshTimer = 0
let riskDashboardRequestSeq = 0
const digitalEmployeeDashboardPayload = ref(null)
const digitalEmployeeDashboardLoading = ref(false)
const digitalEmployeeDashboardError = ref(null)
@@ -178,22 +181,53 @@ export function useOverviewView(options = {}) {
}
const loadRiskDashboard = async () => {
const requestSeq = ++riskDashboardRequestSeq
riskDashboardLoading.value = true
riskDashboardError.value = null
try {
riskDashboardPayload.value = await fetchRiskObservationDashboard({
const payload = await fetchRiskObservationDashboard({
windowDays: activeRiskWindowDays.value,
limit: 500
})
if (requestSeq !== riskDashboardRequestSeq) {
return
}
riskDashboardPayload.value = payload
riskDashboardLastUpdatedAt.value = new Date().toISOString()
} catch (error) {
if (requestSeq !== riskDashboardRequestSeq) {
return
}
riskDashboardPayload.value = null
riskDashboardError.value = error
} finally {
riskDashboardLoading.value = false
if (requestSeq === riskDashboardRequestSeq) {
riskDashboardLoading.value = false
}
}
}
const startRiskDashboardRealtimeRefresh = () => {
if (riskDashboardRefreshTimer) {
window.clearInterval(riskDashboardRefreshTimer)
}
riskDashboardRefreshTimer = window.setInterval(() => {
if (document.visibilityState === 'hidden' || riskDashboardLoading.value) {
return
}
void loadRiskDashboard()
}, 30_000)
}
const stopRiskDashboardRealtimeRefresh = () => {
if (!riskDashboardRefreshTimer) {
return
}
window.clearInterval(riskDashboardRefreshTimer)
riskDashboardRefreshTimer = 0
}
const loadDigitalEmployeeDashboard = async () => {
digitalEmployeeDashboardLoading.value = true
digitalEmployeeDashboardError.value = null
@@ -222,6 +256,11 @@ export function useOverviewView(options = {}) {
void loadSystemDashboard()
void loadRiskDashboard()
void loadDigitalEmployeeDashboard()
startRiskDashboardRealtimeRefresh()
})
onBeforeUnmount(() => {
stopRiskDashboardRealtimeRefresh()
})
watch(
@@ -323,6 +362,9 @@ export function useOverviewView(options = {}) {
const financeDepartmentRanking = computed(() => (
financeDashboardPayload.value?.departmentRanking || []
))
const financeDepartmentEmployeeMix = computed(() => (
financeDashboardPayload.value?.departmentEmployeeMix || emptyFinanceDonut
))
const financeEmployeeRanking = computed(() => (
financeDashboardPayload.value?.employeeRanking || []
))
@@ -501,7 +543,11 @@ export function useOverviewView(options = {}) {
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))
const departmentEmployeeTotal = computed(() => (
financeDepartmentEmployeeMix.value.reduce((sum, item) => sum + Number(item.value || item.amount || 0), 0)
))
const spendCenterValue = computed(() => formatCurrency(Math.round(spendTotal.value)))
const departmentEmployeeCenterValue = computed(() => formatCurrency(Math.round(departmentEmployeeTotal.value)))
const spendLegend = computed(() => financeSpendByCategory.value.map((item) => ({
...item,
@@ -513,6 +559,14 @@ export function useOverviewView(options = {}) {
display: `${item.value}`
})))
const departmentEmployeeLegend = computed(() => financeDepartmentEmployeeMix.value.map((item) => ({
...item,
value: Number(item.value || item.amount || 0),
display: departmentEmployeeTotal.value
? `${Math.round((Number(item.value || item.amount || 0) / departmentEmployeeTotal.value) * 100)}%`
: '0%'
})))
const systemToolTotal = computed(() =>
systemToolCallMix.reduce((sum, item) => sum + item.value, 0)
)
@@ -542,6 +596,7 @@ export function useOverviewView(options = {}) {
rank: index + 1,
shortName: item.name,
amountLabel: formatCurrency(item.amount),
meta: `${Number(item.employeeCount || 0)} 人 / ${Number(item.count || 0)}`,
width: `${Math.max((item.amount / max) * 100, 18)}%`,
color: item.color
}))
@@ -561,6 +616,7 @@ export function useOverviewView(options = {}) {
rank: index + 1,
shortName: item.name,
amountLabel: formatCurrency(item.amount),
meta: `${item.department || '未归属部门'} / ${Number(item.count || 0)}`,
width: `${Math.max((item.amount / max) * 100, 18)}%`,
color: item.color
}))
@@ -738,6 +794,8 @@ export function useOverviewView(options = {}) {
bottlenecks,
budgetMetrics,
budgetSummary,
departmentEmployeeCenterValue,
departmentEmployeeLegend,
departmentRangeOptions,
digitalEmployeeCategoryRows,
digitalEmployeeDashboard,
@@ -760,6 +818,7 @@ export function useOverviewView(options = {}) {
rankedEmployees,
riskDashboard,
riskDashboardError,
riskDashboardLastUpdatedAt,
riskDashboardLoading,
riskDailyTrendRows,
riskLegend,