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

@@ -1,5 +1,5 @@
<template>
<header class="topbar" :class="{ 'chat-mode': isChat }">
<header class="topbar" :class="{ 'chat-mode': isChat, 'detail-mode': isRequestDetail }">
<div class="title-group">
<div class="eyebrow">{{ eyebrowLabel }}</div>
<h1>{{ currentView.title }}</h1>
@@ -121,12 +121,73 @@
</div>
</template>
<template v-else-if="isWorkbench">
<template v-else-if="isWorkbench">
<div class="topbar-toolset" aria-label="工作台快捷工具">
<button class="topbar-icon-btn notification-btn" type="button" aria-label="通知">
<i class="mdi mdi-bell-outline"></i>
<span v-if="topbarNotificationCount" class="notification-badge">{{ topbarNotificationCount }}</span>
</button>
<div class="notification-wrap">
<button
class="topbar-icon-btn notification-btn"
type="button"
aria-label="通知"
:aria-expanded="notificationOpen"
aria-haspopup="dialog"
@click="notificationOpen = !notificationOpen"
>
<i class="mdi mdi-bell-outline"></i>
<span v-if="topbarNotificationCount" class="notification-badge">{{ topbarNotificationCount }}</span>
</button>
<div v-if="notificationOpen" class="notification-popover" role="dialog" aria-label="通知中心">
<header class="notification-head">
<strong>通知</strong>
<button type="button" aria-label="关闭通知" @click="notificationOpen = false">
<i class="mdi mdi-close"></i>
</button>
</header>
<div class="notification-tabs" role="tablist" aria-label="通知状态">
<button
type="button"
role="tab"
:aria-selected="notificationTab === 'unread'"
:class="{ active: notificationTab === 'unread' }"
@click="notificationTab = 'unread'"
>
未读 {{ unreadNotifications.length }}
</button>
<button
type="button"
role="tab"
:aria-selected="notificationTab === 'read'"
:class="{ active: notificationTab === 'read' }"
@click="notificationTab = 'read'"
>
已读 {{ readNotifications.length }}
</button>
</div>
<div v-if="activeNotifications.length" class="notification-list">
<button
v-for="item in activeNotifications"
:key="item.id"
type="button"
class="notification-row"
@click="openNotification(item)"
>
<span class="notification-dot" :class="item.tone"></span>
<span class="notification-copy">
<strong>{{ item.title }}</strong>
<small>{{ item.description }}</small>
<em>{{ item.time }}</em>
</span>
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
<div v-else class="notification-empty">
<i class="mdi mdi-bell-check-outline"></i>
<span>{{ notificationTab === 'unread' ? '暂无未读通知' : '暂无已读通知' }}</span>
</div>
</div>
</div>
<button class="topbar-icon-btn" type="button" aria-label="帮助">
<i class="mdi mdi-help-circle-outline"></i>
@@ -243,6 +304,10 @@ const props = defineProps({
type: Object,
default: () => null
},
workbenchSummary: {
type: Object,
default: () => null
},
companyName: {
type: String,
default: ''
@@ -276,7 +341,8 @@ const emit = defineEmits([
'update:overviewDashboard',
'batchApprove',
'openChat',
'newApplication'
'newApplication',
'openDocument'
])
const isChat = computed(() => props.activeView === 'chat')
const isOverview = computed(() => props.activeView === 'overview')
@@ -294,10 +360,34 @@ const eyebrowLabel = computed(() => (
))
const displayCompanyName = computed(() => String(props.companyName || '远光软件股份有限公司').trim() || '远光软件股份有限公司')
const topbarNotificationCount = computed(() => {
const summary = props.documentSummary ?? {}
const count = Number(summary.toProcess ?? summary.toSubmit ?? 8)
const summary = props.workbenchSummary ?? {}
const count = Number(summary.unreadNotificationCount ?? 0)
return Number.isFinite(count) && count > 0 ? Math.min(count, 99) : 0
})
})
const notificationOpen = ref(false)
const notificationTab = ref('unread')
const notificationItems = computed(() => (
Array.isArray(props.workbenchSummary?.notifications)
? props.workbenchSummary.notifications
: []
))
const unreadNotifications = computed(() => notificationItems.value.filter((item) => item.unread))
const readNotifications = computed(() => notificationItems.value.filter((item) => !item.unread))
const activeNotifications = computed(() => (
notificationTab.value === 'unread' ? unreadNotifications.value : readNotifications.value
))
function openNotification(item) {
notificationOpen.value = false
const target = item?.target || {}
if (target.type === 'document' && (target.id || target.claimNo)) {
emit('openDocument', {
claimId: target.id,
id: target.id || target.claimNo,
claimNo: target.claimNo
})
}
}
const requestKpis = computed(() => {
const summary = props.requestSummary ?? {}