feat: 风险可见性控制与差旅详情页交互优化
- 新增风险可见性工具函数与风险日趋势图表组件 - 优化差旅请求详情页费用模型与视图交互 - 完善顶部导航栏样式与应用壳路由逻辑 - 补充风险可见性、风险看板与差旅详情测试覆盖
This commit is contained in:
@@ -32,6 +32,12 @@ const totals = computed(() => props.rows.map((item) => Number(item.total || 0)))
|
||||
const highValues = computed(() => props.rows.map((item) => Number(item.highOrAbove || 0)))
|
||||
const maxValue = computed(() => Math.max(...totals.value, ...highValues.value, 1))
|
||||
const axisMax = computed(() => Math.max(5, Math.ceil(maxValue.value * 1.2)))
|
||||
const barWidth = computed(() => {
|
||||
if (labels.value.length >= 12) return 9
|
||||
if (labels.value.length >= 8) return 11
|
||||
return 14
|
||||
})
|
||||
const bottomGridSize = computed(() => (labels.value.some((label) => String(label).includes('~')) ? 38 : 28))
|
||||
|
||||
const ariaLabel = computed(() =>
|
||||
props.rows.map((item) => (
|
||||
@@ -47,7 +53,7 @@ const chartOptions = computed(() => ({
|
||||
grid: {
|
||||
top: 34,
|
||||
right: 16,
|
||||
bottom: 24,
|
||||
bottom: bottomGridSize.value,
|
||||
left: 28,
|
||||
containLabel: true
|
||||
},
|
||||
@@ -86,7 +92,10 @@ const chartOptions = computed(() => ({
|
||||
axisLabel: {
|
||||
color: '#64748b',
|
||||
fontSize: 11,
|
||||
fontWeight: 700
|
||||
fontWeight: 700,
|
||||
interval: 0,
|
||||
lineHeight: 14,
|
||||
formatter: (value) => String(value || '').replace('~', '\n~')
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
@@ -106,7 +115,8 @@ const chartOptions = computed(() => ({
|
||||
name: '风险观察',
|
||||
type: 'bar',
|
||||
data: totals.value,
|
||||
barWidth: 14,
|
||||
barWidth: barWidth.value,
|
||||
barMaxWidth: 14,
|
||||
itemStyle: {
|
||||
color: themeColors.value.chartPrimary,
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
|
||||
@@ -123,14 +123,14 @@
|
||||
|
||||
<template v-else-if="isWorkbench">
|
||||
<div class="topbar-toolset" aria-label="工作台快捷工具">
|
||||
<div class="notification-wrap" :class="{ 'is-open': notificationOpen }">
|
||||
<div ref="notificationWrapRef" class="notification-wrap" :class="{ 'is-open': notificationOpen }">
|
||||
<button
|
||||
class="topbar-icon-btn notification-btn"
|
||||
type="button"
|
||||
aria-label="通知"
|
||||
:aria-expanded="notificationOpen"
|
||||
aria-haspopup="dialog"
|
||||
@click="notificationOpen = !notificationOpen"
|
||||
@click="toggleNotification"
|
||||
>
|
||||
<i class="mdi mdi-bell-outline"></i>
|
||||
<span v-if="topbarNotificationCount" class="notification-badge">{{ topbarNotificationCount }}</span>
|
||||
@@ -161,7 +161,7 @@
|
||||
class="notification-close-btn"
|
||||
type="button"
|
||||
aria-label="关闭通知"
|
||||
@click="notificationOpen = false"
|
||||
@click="closeNotification"
|
||||
>
|
||||
<i class="mdi mdi-close"></i>
|
||||
</button>
|
||||
@@ -228,9 +228,55 @@
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<button class="topbar-icon-btn" type="button" aria-label="帮助">
|
||||
<i class="mdi mdi-help-circle-outline"></i>
|
||||
</button>
|
||||
<div ref="helpWrapRef" class="help-wrap" :class="{ 'is-open': helpOpen }">
|
||||
<button
|
||||
class="topbar-icon-btn help-btn"
|
||||
type="button"
|
||||
aria-label="帮助"
|
||||
:aria-expanded="helpOpen"
|
||||
aria-haspopup="dialog"
|
||||
@click="toggleHelp"
|
||||
>
|
||||
<i class="mdi mdi-help-circle-outline"></i>
|
||||
</button>
|
||||
|
||||
<Transition name="help-panel">
|
||||
<div v-if="helpOpen" class="help-popover" role="dialog" aria-label="产品信息">
|
||||
<header class="help-head">
|
||||
<span class="help-head-icon" aria-hidden="true">
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
</span>
|
||||
<span class="help-head-copy">
|
||||
<strong>{{ topbarHelpDemo.productName }}</strong>
|
||||
<small>产品版本与版权信息</small>
|
||||
</span>
|
||||
<button
|
||||
class="help-close-btn"
|
||||
type="button"
|
||||
aria-label="关闭帮助"
|
||||
@click="closeHelp"
|
||||
>
|
||||
<i class="mdi mdi-close"></i>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<dl class="help-meta">
|
||||
<div class="help-meta-row">
|
||||
<dt>版本号</dt>
|
||||
<dd>{{ topbarHelpDemo.version }}</dd>
|
||||
</div>
|
||||
<div class="help-meta-row">
|
||||
<dt>更新时间</dt>
|
||||
<dd>{{ topbarHelpDemo.updatedAt }}</dd>
|
||||
</div>
|
||||
<div class="help-meta-row help-meta-row--block">
|
||||
<dt>Copyright</dt>
|
||||
<dd>{{ topbarHelpDemo.copyright }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<button class="company-switcher" type="button" aria-label="切换公司">
|
||||
<span>{{ displayCompanyName }}</span>
|
||||
@@ -315,8 +361,10 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useDocumentCenterInbox } from '../../composables/useDocumentCenterInbox.js'
|
||||
import { useTopBarNotificationStates } from '../../composables/useTopBarNotificationStates.js'
|
||||
import { useDocumentCenterInbox } from '../../composables/useDocumentCenterInbox.js'
|
||||
import { useTopBarNotificationStates } from '../../composables/useTopBarNotificationStates.js'
|
||||
import { useTopBarWorkbenchPopovers } from '../../composables/useTopBarWorkbenchPopovers.js'
|
||||
import { createCurrentYearDateRange, formatDateValue } from '../../utils/dateRangeDefaults.js'
|
||||
import EnterpriseSelect from '../shared/EnterpriseSelect.vue'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -367,7 +415,7 @@ const props = defineProps({
|
||||
},
|
||||
customRange: {
|
||||
type: Object,
|
||||
default: () => ({ start: '2024-07-06', end: '2024-07-12' })
|
||||
default: createCurrentYearDateRange
|
||||
},
|
||||
overviewDashboard: {
|
||||
type: String,
|
||||
@@ -396,12 +444,12 @@ const isDigitalEmployees = computed(() => props.activeView === 'digitalEmployees
|
||||
const isApproval = computed(() => props.activeView === 'approval')
|
||||
const isPolicies = computed(() => props.activeView === 'policies')
|
||||
const isEmployees = computed(() => props.activeView === 'employees')
|
||||
const eyebrowLabel = computed(() => (
|
||||
String(props.currentView?.eyebrow || '').trim()
|
||||
|| (isChat.value ? 'Smart Finance Q&A' : 'Smart Expense Operations')
|
||||
))
|
||||
const displayCompanyName = computed(() => String(props.companyName || '远光软件股份有限公司').trim() || '远光软件股份有限公司')
|
||||
const MAX_NOTIFICATION_ITEMS = 30
|
||||
const eyebrowLabel = computed(() => (
|
||||
String(props.currentView?.eyebrow || '').trim()
|
||||
|| (isChat.value ? 'Smart Finance Q&A' : 'Smart Expense Operations')
|
||||
))
|
||||
const displayCompanyName = computed(() => String(props.companyName || '远光软件股份有限公司').trim() || '远光软件股份有限公司')
|
||||
const MAX_NOTIFICATION_ITEMS = 30
|
||||
const {
|
||||
markDocumentInboxRowRead,
|
||||
markDocumentInboxRowsRead,
|
||||
@@ -409,22 +457,32 @@ const {
|
||||
refreshDocumentInbox,
|
||||
startDocumentInboxPolling,
|
||||
stopDocumentInboxPolling
|
||||
} = useDocumentCenterInbox()
|
||||
let documentInboxInitialRefreshTimer = null
|
||||
const notificationOpen = ref(false)
|
||||
const {
|
||||
readNotificationIds,
|
||||
hideNotificationStates,
|
||||
isNotificationHidden,
|
||||
isNotificationRead,
|
||||
loadNotificationStates,
|
||||
markNotificationStateRead
|
||||
} = useTopBarNotificationStates()
|
||||
const notificationTab = ref('unread')
|
||||
|
||||
function normalizeNotificationId(value) {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
} = useDocumentCenterInbox()
|
||||
let documentInboxInitialRefreshTimer = null
|
||||
const {
|
||||
notificationOpen,
|
||||
helpOpen,
|
||||
notificationWrapRef,
|
||||
helpWrapRef,
|
||||
topbarHelpDemo,
|
||||
toggleNotification,
|
||||
toggleHelp,
|
||||
closeNotification,
|
||||
closeHelp
|
||||
} = useTopBarWorkbenchPopovers()
|
||||
const {
|
||||
readNotificationIds,
|
||||
hideNotificationStates,
|
||||
isNotificationHidden,
|
||||
isNotificationRead,
|
||||
loadNotificationStates,
|
||||
markNotificationStateRead
|
||||
} = useTopBarNotificationStates()
|
||||
const notificationTab = ref('unread')
|
||||
|
||||
function normalizeNotificationId(value) {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
function formatNotificationTime(value) {
|
||||
const date = new Date(value)
|
||||
@@ -459,28 +517,28 @@ function resolveWorkbenchNotificationId(item, index) {
|
||||
return normalizeNotificationId(`workbench:${item?.id || [item?.title, item?.description, item?.time, index].join('|')}`)
|
||||
}
|
||||
|
||||
const documentNotificationItems = computed(() =>
|
||||
documentInboxNotificationRows.value
|
||||
.map((row) => {
|
||||
const id = normalizeNotificationId(`document:${row.documentKey || row.claimId || row.documentNo}`)
|
||||
if (!id || isNotificationHidden(id)) {
|
||||
return null
|
||||
}
|
||||
const unread = Boolean(row.isUnread) && !isNotificationRead(id)
|
||||
|
||||
return {
|
||||
id,
|
||||
const documentNotificationItems = computed(() =>
|
||||
documentInboxNotificationRows.value
|
||||
.map((row) => {
|
||||
const id = normalizeNotificationId(`document:${row.documentKey || row.claimId || row.documentNo}`)
|
||||
if (!id || isNotificationHidden(id)) {
|
||||
return null
|
||||
}
|
||||
const unread = Boolean(row.isUnread) && !isNotificationRead(id)
|
||||
|
||||
return {
|
||||
id,
|
||||
kind: 'document',
|
||||
title: `${row.documentTypeLabel || '单据'} ${row.documentNo || row.claimId || '待生成'}`,
|
||||
description: resolveDocumentNotificationDescription(row),
|
||||
time: formatNotificationTime(row.updatedAt || row.createdAt),
|
||||
category: row.sourceLabel || '单据中心',
|
||||
tone: resolveDocumentNotificationTone({ ...row, isUnread: unread }),
|
||||
unread,
|
||||
icon: row.source === 'approval' ? 'mdi mdi-clipboard-text-clock-outline' : 'mdi mdi-file-document-outline',
|
||||
badge: unread ? '新' : '',
|
||||
target: {
|
||||
type: 'document',
|
||||
time: formatNotificationTime(row.updatedAt || row.createdAt),
|
||||
category: row.sourceLabel || '单据中心',
|
||||
tone: resolveDocumentNotificationTone({ ...row, isUnread: unread }),
|
||||
unread,
|
||||
icon: row.source === 'approval' ? 'mdi mdi-clipboard-text-clock-outline' : 'mdi mdi-file-document-outline',
|
||||
badge: unread ? '新' : '',
|
||||
target: {
|
||||
type: 'document',
|
||||
id: row.claimId,
|
||||
claimNo: row.documentNo
|
||||
},
|
||||
@@ -561,17 +619,17 @@ function resolveNotificationIcon(item) {
|
||||
return 'mdi mdi-bell-outline'
|
||||
}
|
||||
|
||||
function markNotificationRead(item) {
|
||||
if (!item?.id || !item.unread) {
|
||||
return
|
||||
}
|
||||
|
||||
if (item.kind === 'document' && item.documentRow) {
|
||||
markDocumentInboxRowRead(item.documentRow)
|
||||
}
|
||||
|
||||
void markNotificationStateRead(item)
|
||||
}
|
||||
function markNotificationRead(item) {
|
||||
if (!item?.id || !item.unread) {
|
||||
return
|
||||
}
|
||||
|
||||
if (item.kind === 'document' && item.documentRow) {
|
||||
markDocumentInboxRowRead(item.documentRow)
|
||||
}
|
||||
|
||||
void markNotificationStateRead(item)
|
||||
}
|
||||
|
||||
function clearAllNotifications() {
|
||||
const currentItems = notificationItems.value
|
||||
@@ -587,13 +645,13 @@ function clearAllNotifications() {
|
||||
markDocumentInboxRowsRead(documentRows)
|
||||
}
|
||||
|
||||
void hideNotificationStates(currentItems)
|
||||
notificationTab.value = 'unread'
|
||||
}
|
||||
void hideNotificationStates(currentItems)
|
||||
notificationTab.value = 'unread'
|
||||
}
|
||||
|
||||
function openNotification(item) {
|
||||
markNotificationRead(item)
|
||||
notificationOpen.value = false
|
||||
closeNotification()
|
||||
const target = item?.target || {}
|
||||
if (target.type === 'document' && (target.id || target.claimNo)) {
|
||||
emit('openDocument', {
|
||||
@@ -793,25 +851,29 @@ watch(
|
||||
watch(
|
||||
() => props.activeView,
|
||||
(activeView, previousView) => {
|
||||
if (activeView === 'workbench' && previousView !== 'workbench') {
|
||||
clearDocumentInboxInitialRefreshTimer()
|
||||
void loadNotificationStates()
|
||||
void refreshDocumentInbox({ force: true })
|
||||
if (activeView === 'workbench' && previousView !== 'workbench') {
|
||||
clearDocumentInboxInitialRefreshTimer()
|
||||
void loadNotificationStates()
|
||||
void refreshDocumentInbox({ force: true })
|
||||
}
|
||||
if (activeView !== 'workbench') {
|
||||
closeNotification()
|
||||
closeHelp()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(notificationOpen, (open) => {
|
||||
if (open) {
|
||||
void loadNotificationStates()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void loadNotificationStates()
|
||||
scheduleDocumentInboxInitialRefresh()
|
||||
startDocumentInboxPolling()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
watch(notificationOpen, (open) => {
|
||||
if (open) {
|
||||
void loadNotificationStates()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void loadNotificationStates()
|
||||
scheduleDocumentInboxInitialRefresh()
|
||||
startDocumentInboxPolling()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearDocumentInboxInitialRefreshTimer()
|
||||
@@ -836,16 +898,9 @@ function formatRangeLabel(start, end) {
|
||||
return `${start} ~ ${end}`
|
||||
}
|
||||
|
||||
function toDateLabel(date) {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
function buildPresetRangeLabel(label) {
|
||||
const now = new Date()
|
||||
const today = toDateLabel(now)
|
||||
const today = formatDateValue(now)
|
||||
|
||||
if (label === '今日') {
|
||||
return today
|
||||
@@ -855,7 +910,7 @@ function buildPresetRangeLabel(label) {
|
||||
const start = new Date(now)
|
||||
start.setHours(0, 0, 0, 0)
|
||||
start.setDate(start.getDate() - 9)
|
||||
return `${toDateLabel(start)} ~ ${today}`
|
||||
return `${formatDateValue(start)} ~ ${today}`
|
||||
}
|
||||
|
||||
if (label === '本周') {
|
||||
@@ -863,7 +918,7 @@ function buildPresetRangeLabel(label) {
|
||||
const day = start.getDay() || 7
|
||||
start.setHours(0, 0, 0, 0)
|
||||
start.setDate(start.getDate() - day + 1)
|
||||
return `${toDateLabel(start)} ~ ${today}`
|
||||
return `${formatDateValue(start)} ~ ${today}`
|
||||
}
|
||||
|
||||
if (label === '本月') {
|
||||
|
||||
Reference in New Issue
Block a user