fix(documents): move unread notice into bell
This commit is contained in:
@@ -35,27 +35,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.scope-tab-label {
|
.scope-tab-label {
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scope-tab-badge {
|
.scope-tab-badge {
|
||||||
position: absolute;
|
position: static;
|
||||||
top: -8px;
|
flex: 0 0 auto;
|
||||||
right: -15px;
|
min-width: 14px;
|
||||||
min-width: 15px;
|
height: 14px;
|
||||||
height: 15px;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0 4px;
|
margin-top: -5px;
|
||||||
|
padding: 0 3px;
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: #ef4444;
|
background: #ef4444;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 10px;
|
font-size: 9.5px;
|
||||||
font-weight: 850;
|
font-weight: 850;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
|
|||||||
@@ -216,14 +216,12 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: 128px;
|
max-width: 128px;
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
color: currentColor;
|
color: currentColor;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: visible;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition:
|
transition:
|
||||||
max-width var(--rail-motion-duration) var(--rail-motion-ease),
|
max-width var(--rail-motion-duration) var(--rail-motion-ease),
|
||||||
@@ -232,16 +230,6 @@
|
|||||||
will-change: max-width, opacity, transform;
|
will-change: max-width, opacity, transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-label-text {
|
|
||||||
position: relative;
|
|
||||||
min-width: 0;
|
|
||||||
max-width: 100%;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
overflow: visible;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-badge {
|
.nav-badge {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
min-width: 34px;
|
min-width: 34px;
|
||||||
@@ -263,27 +251,6 @@
|
|||||||
will-change: min-width, max-width, padding, opacity;
|
will-change: min-width, max-width, padding, opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-unread-dot {
|
|
||||||
width: 9px;
|
|
||||||
height: 9px;
|
|
||||||
border: 2px solid #fff;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #ef4444;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 3px rgba(239, 68, 68, 0.12),
|
|
||||||
0 6px 14px rgba(239, 68, 68, 0.32);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-unread-dot-label {
|
|
||||||
position: absolute;
|
|
||||||
top: -8px;
|
|
||||||
right: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-unread-dot-collapsed {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rail-user {
|
.rail-user {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -493,19 +460,6 @@
|
|||||||
transition-delay: 0ms;
|
transition-delay: 0ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rail-collapsed .nav-unread-dot-label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rail-collapsed .nav-unread-dot-collapsed {
|
|
||||||
position: absolute;
|
|
||||||
display: block;
|
|
||||||
top: 10px;
|
|
||||||
right: 11px;
|
|
||||||
width: 9px;
|
|
||||||
height: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rail-collapsed {
|
.rail-collapsed {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -408,21 +408,26 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-btn {
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.notification-badge {
|
.notification-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 1px;
|
||||||
right: 1px;
|
right: 0;
|
||||||
min-width: 13px;
|
min-width: 15px;
|
||||||
height: 13px;
|
height: 15px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0 3px;
|
padding: 0 4px;
|
||||||
border: 2px solid #fff;
|
border: 2px solid #fff;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: #ef4444;
|
background: #ef4444;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 8px;
|
font-size: 9px;
|
||||||
font-weight: 850;
|
font-weight: 850;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
box-shadow: 0 5px 10px rgba(239, 68, 68, .22);
|
box-shadow: 0 5px 10px rgba(239, 68, 68, .22);
|
||||||
@@ -433,15 +438,15 @@
|
|||||||
top: calc(100% + 10px);
|
top: calc(100% + 10px);
|
||||||
right: -8px;
|
right: -8px;
|
||||||
z-index: 60;
|
z-index: 60;
|
||||||
width: min(360px, calc(100vw - 32px));
|
width: min(392px, calc(100vw - 32px));
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
padding: 12px;
|
padding: 14px;
|
||||||
border: 1px solid #e5edf5;
|
border: 1px solid #e5edf5;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: rgba(255, 255, 255, 0.98);
|
background: #fff;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 18px 42px rgba(15, 23, 42, 0.14),
|
0 18px 46px rgba(15, 23, 42, 0.14),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.92);
|
inset 0 1px 0 rgba(255, 255, 255, 0.92);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,14 +474,27 @@
|
|||||||
.notification-head {
|
.notification-head {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #edf2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-head-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-head strong {
|
.notification-head strong {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
font-weight: 850;
|
font-weight: 850;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-head small {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
.notification-head button {
|
.notification-head button {
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
@@ -501,89 +519,175 @@
|
|||||||
|
|
||||||
.notification-tabs button {
|
.notification-tabs button {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
height: 28px;
|
height: 30px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-tabs button em {
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.notification-tabs button.active {
|
.notification-tabs button.active {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: var(--theme-primary-active);
|
color: var(--theme-primary-active);
|
||||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-tabs button.active em {
|
||||||
|
background: var(--theme-primary-light-8);
|
||||||
|
color: var(--theme-primary-active);
|
||||||
|
}
|
||||||
|
|
||||||
.notification-list {
|
.notification-list {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-row {
|
.notification-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 8px minmax(0, 1fr) 16px;
|
grid-template-columns: 34px minmax(0, 1fr) 16px;
|
||||||
align-items: center;
|
align-items: start;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 10px 4px;
|
min-height: 72px;
|
||||||
border-top: 1px solid #edf2f7;
|
padding: 10px;
|
||||||
|
border: 1px solid #edf2f7;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
transition:
|
||||||
|
border-color 160ms var(--ease),
|
||||||
.notification-row:first-child {
|
background 160ms var(--ease),
|
||||||
border-top: 0;
|
box-shadow 160ms var(--ease);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-row:hover {
|
.notification-row:hover {
|
||||||
background: #f8fafc;
|
border-color: var(--theme-primary-light-5);
|
||||||
|
background: #f8fbff;
|
||||||
|
box-shadow: 0 8px 18px rgba(37, 99, 235, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-dot {
|
.notification-type-icon {
|
||||||
width: 7px;
|
width: 34px;
|
||||||
height: 7px;
|
height: 34px;
|
||||||
border-radius: 999px;
|
display: grid;
|
||||||
background: var(--theme-primary);
|
place-items: center;
|
||||||
|
border: 1px solid var(--theme-primary-light-6);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--theme-primary-light-9);
|
||||||
|
color: var(--theme-primary-active);
|
||||||
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-dot.danger { background: #ef4444; }
|
.notification-type-icon.danger {
|
||||||
.notification-dot.warning { background: #f59e0b; }
|
border-color: #fecaca;
|
||||||
.notification-dot.success { background: var(--success); }
|
background: #fff5f5;
|
||||||
.notification-dot.info { background: #3b82f6; }
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-type-icon.warning {
|
||||||
|
border-color: #fde68a;
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-type-icon.success {
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-type-icon.info {
|
||||||
|
border-color: #bfdbfe;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
.notification-copy {
|
.notification-copy {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 2px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-copy strong,
|
.notification-copy strong,
|
||||||
.notification-copy small,
|
.notification-copy small {
|
||||||
.notification-copy em {
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-title-line {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.notification-copy strong {
|
.notification-copy strong {
|
||||||
|
min-width: 0;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 850;
|
font-weight: 850;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-title-line b {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #ef4444;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.notification-copy small {
|
.notification-copy small {
|
||||||
color: #475569;
|
color: #475569;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-copy em {
|
.notification-meta {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-meta em,
|
||||||
|
.notification-meta time {
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-row > .mdi {
|
.notification-row > .mdi {
|
||||||
|
align-self: center;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,13 +50,7 @@
|
|||||||
@click="emit('navigate', item.id)"
|
@click="emit('navigate', item.id)"
|
||||||
>
|
>
|
||||||
<span class="nav-icon" v-html="item.icon"></span>
|
<span class="nav-icon" v-html="item.icon"></span>
|
||||||
<span class="nav-label">
|
<span class="nav-label">{{ item.displayLabel }}</span>
|
||||||
<span class="nav-label-text">
|
|
||||||
{{ item.displayLabel }}
|
|
||||||
<span v-if="item.hasNewMessage" class="nav-unread-dot nav-unread-dot-label" aria-hidden="true"></span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span v-if="item.hasNewMessage" class="nav-unread-dot nav-unread-dot-collapsed" aria-hidden="true"></span>
|
|
||||||
<span v-if="item.badge" class="nav-badge">{{ item.badge }}</span>
|
<span v-if="item.badge" class="nav-badge">{{ item.badge }}</span>
|
||||||
</button>
|
</button>
|
||||||
</ElTooltip>
|
</ElTooltip>
|
||||||
@@ -118,9 +112,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ElTooltip } from 'element-plus/es/components/tooltip/index.mjs'
|
import { ElTooltip } from 'element-plus/es/components/tooltip/index.mjs'
|
||||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { useDocumentCenterInbox } from '../../composables/useDocumentCenterInbox.js'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
navItems: { type: Array, required: true },
|
navItems: { type: Array, required: true },
|
||||||
@@ -149,14 +141,6 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['navigate', 'openChat', 'logout', 'toggle-collapse'])
|
const emit = defineEmits(['navigate', 'openChat', 'logout', 'toggle-collapse'])
|
||||||
|
|
||||||
const {
|
|
||||||
hasUnread: documentInboxHasUnread,
|
|
||||||
refreshDocumentInbox,
|
|
||||||
startDocumentInboxPolling,
|
|
||||||
stopDocumentInboxPolling
|
|
||||||
} = useDocumentCenterInbox()
|
|
||||||
let inboxInitialRefreshTimer = null
|
|
||||||
|
|
||||||
const sidebarMeta = {
|
const sidebarMeta = {
|
||||||
overview: { label: '分析看板' },
|
overview: { label: '分析看板' },
|
||||||
workbench: { label: '个人工作台' },
|
workbench: { label: '个人工作台' },
|
||||||
@@ -173,35 +157,10 @@ const decoratedNavItems = computed(() =>
|
|||||||
props.navItems.map((item) => ({
|
props.navItems.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
displayLabel: sidebarMeta[item.id]?.label ?? item.label,
|
displayLabel: sidebarMeta[item.id]?.label ?? item.label,
|
||||||
hasNewMessage: item.id === 'documents' ? documentInboxHasUnread.value : false,
|
|
||||||
badge: sidebarMeta[item.id]?.badge
|
badge: sidebarMeta[item.id]?.badge
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
function clearInboxInitialRefreshTimer() {
|
|
||||||
if (inboxInitialRefreshTimer && typeof window !== 'undefined') {
|
|
||||||
window.clearTimeout(inboxInitialRefreshTimer)
|
|
||||||
inboxInitialRefreshTimer = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleInboxInitialRefresh() {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
clearInboxInitialRefreshTimer()
|
|
||||||
inboxInitialRefreshTimer = window.setTimeout(() => {
|
|
||||||
inboxInitialRefreshTimer = null
|
|
||||||
void refreshDocumentInbox()
|
|
||||||
}, props.activeView === 'documents' ? 1200 : 6000)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
scheduleInboxInitialRefresh()
|
|
||||||
startDocumentInboxPolling()
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const displayUser = computed(() => ({
|
const displayUser = computed(() => ({
|
||||||
name: props.currentUser?.name || '系统管理员',
|
name: props.currentUser?.name || '系统管理员',
|
||||||
@@ -295,19 +254,7 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.activeView,
|
|
||||||
(activeView, previousView) => {
|
|
||||||
if (activeView === 'documents' && previousView !== 'documents') {
|
|
||||||
clearInboxInitialRefreshTimer()
|
|
||||||
void refreshDocumentInbox({ force: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearInboxInitialRefreshTimer()
|
|
||||||
stopDocumentInboxPolling()
|
|
||||||
closeCollapsedUserMenuNow()
|
closeCollapsedUserMenuNow()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -138,7 +138,10 @@
|
|||||||
|
|
||||||
<div v-if="notificationOpen" class="notification-popover" role="dialog" aria-label="通知中心">
|
<div v-if="notificationOpen" class="notification-popover" role="dialog" aria-label="通知中心">
|
||||||
<header class="notification-head">
|
<header class="notification-head">
|
||||||
<strong>通知</strong>
|
<span class="notification-head-copy">
|
||||||
|
<strong>通知中心</strong>
|
||||||
|
<small>{{ unreadNotifications.length ? `${unreadNotifications.length} 条待处理` : '暂无待处理通知' }}</small>
|
||||||
|
</span>
|
||||||
<button type="button" aria-label="关闭通知" @click="notificationOpen = false">
|
<button type="button" aria-label="关闭通知" @click="notificationOpen = false">
|
||||||
<i class="mdi mdi-close"></i>
|
<i class="mdi mdi-close"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -152,7 +155,8 @@
|
|||||||
:class="{ active: notificationTab === 'unread' }"
|
:class="{ active: notificationTab === 'unread' }"
|
||||||
@click="notificationTab = 'unread'"
|
@click="notificationTab = 'unread'"
|
||||||
>
|
>
|
||||||
未读 {{ unreadNotifications.length }}
|
<span>未读</span>
|
||||||
|
<em>{{ unreadNotifications.length }}</em>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -161,7 +165,8 @@
|
|||||||
:class="{ active: notificationTab === 'read' }"
|
:class="{ active: notificationTab === 'read' }"
|
||||||
@click="notificationTab = 'read'"
|
@click="notificationTab = 'read'"
|
||||||
>
|
>
|
||||||
已读 {{ readNotifications.length }}
|
<span>已读</span>
|
||||||
|
<em>{{ readNotifications.length }}</em>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -173,11 +178,19 @@
|
|||||||
class="notification-row"
|
class="notification-row"
|
||||||
@click="openNotification(item)"
|
@click="openNotification(item)"
|
||||||
>
|
>
|
||||||
<span class="notification-dot" :class="item.tone"></span>
|
<span class="notification-type-icon" :class="item.tone">
|
||||||
|
<i :class="resolveNotificationIcon(item)"></i>
|
||||||
|
</span>
|
||||||
<span class="notification-copy">
|
<span class="notification-copy">
|
||||||
<strong>{{ item.title }}</strong>
|
<span class="notification-title-line">
|
||||||
|
<strong>{{ item.title }}</strong>
|
||||||
|
<b v-if="item.badge">{{ item.badge }}</b>
|
||||||
|
</span>
|
||||||
<small>{{ item.description }}</small>
|
<small>{{ item.description }}</small>
|
||||||
<em>{{ item.time }}</em>
|
<span class="notification-meta">
|
||||||
|
<em>{{ item.category || '系统通知' }}</em>
|
||||||
|
<time>{{ item.time }}</time>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<i class="mdi mdi-chevron-right"></i>
|
<i class="mdi mdi-chevron-right"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -274,8 +287,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { useDocumentCenterInbox } from '../../composables/useDocumentCenterInbox.js'
|
||||||
import EnterpriseSelect from '../shared/EnterpriseSelect.vue'
|
import EnterpriseSelect from '../shared/EnterpriseSelect.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -342,7 +356,8 @@ const emit = defineEmits([
|
|||||||
'batchApprove',
|
'batchApprove',
|
||||||
'openChat',
|
'openChat',
|
||||||
'newApplication',
|
'newApplication',
|
||||||
'openDocument'
|
'openDocument',
|
||||||
|
'navigate'
|
||||||
])
|
])
|
||||||
const isChat = computed(() => props.activeView === 'chat')
|
const isChat = computed(() => props.activeView === 'chat')
|
||||||
const isOverview = computed(() => props.activeView === 'overview')
|
const isOverview = computed(() => props.activeView === 'overview')
|
||||||
@@ -359,27 +374,111 @@ const eyebrowLabel = computed(() => (
|
|||||||
|| (isChat.value ? 'Smart Finance Q&A' : 'Smart Expense Operations')
|
|| (isChat.value ? 'Smart Finance Q&A' : 'Smart Expense Operations')
|
||||||
))
|
))
|
||||||
const displayCompanyName = computed(() => String(props.companyName || '远光软件股份有限公司').trim() || '远光软件股份有限公司')
|
const displayCompanyName = computed(() => String(props.companyName || '远光软件股份有限公司').trim() || '远光软件股份有限公司')
|
||||||
const topbarNotificationCount = computed(() => {
|
const {
|
||||||
|
refreshDocumentInbox,
|
||||||
|
startDocumentInboxPolling,
|
||||||
|
stopDocumentInboxPolling,
|
||||||
|
unreadCount: documentInboxUnreadCount
|
||||||
|
} = useDocumentCenterInbox()
|
||||||
|
let documentInboxInitialRefreshTimer = null
|
||||||
|
const workbenchNotificationCount = computed(() => {
|
||||||
const summary = props.workbenchSummary ?? {}
|
const summary = props.workbenchSummary ?? {}
|
||||||
const count = Number(summary.unreadNotificationCount ?? 0)
|
const count = Number(summary.unreadNotificationCount ?? 0)
|
||||||
|
return Number.isFinite(count) && count > 0 ? count : 0
|
||||||
|
})
|
||||||
|
const topbarNotificationCount = computed(() => {
|
||||||
|
const count = workbenchNotificationCount.value + Number(documentInboxUnreadCount.value || 0)
|
||||||
return Number.isFinite(count) && count > 0 ? Math.min(count, 99) : 0
|
return Number.isFinite(count) && count > 0 ? Math.min(count, 99) : 0
|
||||||
})
|
})
|
||||||
const notificationOpen = ref(false)
|
const notificationOpen = ref(false)
|
||||||
const notificationTab = ref('unread')
|
const notificationTab = ref('unread')
|
||||||
const notificationItems = computed(() => (
|
const documentInboxBadgeText = computed(() => {
|
||||||
|
const count = Number(documentInboxUnreadCount.value || 0)
|
||||||
|
return count > 99 ? '99+' : String(count)
|
||||||
|
})
|
||||||
|
const documentInboxNotification = computed(() => {
|
||||||
|
const count = Number(documentInboxUnreadCount.value || 0)
|
||||||
|
if (!Number.isFinite(count) || count <= 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: 'document-center-unread',
|
||||||
|
title: '单据中心有新单据',
|
||||||
|
description: `当前有 ${count} 条新单据待查看`,
|
||||||
|
time: '刚刚更新',
|
||||||
|
category: '单据中心',
|
||||||
|
tone: 'danger',
|
||||||
|
unread: true,
|
||||||
|
icon: 'mdi mdi-file-document-alert-outline',
|
||||||
|
badge: documentInboxBadgeText.value,
|
||||||
|
target: { type: 'documents-center' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const workbenchNotificationItems = computed(() => (
|
||||||
Array.isArray(props.workbenchSummary?.notifications)
|
Array.isArray(props.workbenchSummary?.notifications)
|
||||||
? props.workbenchSummary.notifications
|
? props.workbenchSummary.notifications
|
||||||
: []
|
: []
|
||||||
))
|
))
|
||||||
|
const notificationItems = computed(() => {
|
||||||
|
const inboxNotification = documentInboxNotification.value
|
||||||
|
return inboxNotification
|
||||||
|
? [inboxNotification, ...workbenchNotificationItems.value]
|
||||||
|
: workbenchNotificationItems.value
|
||||||
|
})
|
||||||
const unreadNotifications = computed(() => notificationItems.value.filter((item) => item.unread))
|
const unreadNotifications = computed(() => notificationItems.value.filter((item) => item.unread))
|
||||||
const readNotifications = computed(() => notificationItems.value.filter((item) => !item.unread))
|
const readNotifications = computed(() => notificationItems.value.filter((item) => !item.unread))
|
||||||
const activeNotifications = computed(() => (
|
const activeNotifications = computed(() => (
|
||||||
notificationTab.value === 'unread' ? unreadNotifications.value : readNotifications.value
|
notificationTab.value === 'unread' ? unreadNotifications.value : readNotifications.value
|
||||||
))
|
))
|
||||||
|
|
||||||
|
function clearDocumentInboxInitialRefreshTimer() {
|
||||||
|
if (documentInboxInitialRefreshTimer && typeof window !== 'undefined') {
|
||||||
|
window.clearTimeout(documentInboxInitialRefreshTimer)
|
||||||
|
documentInboxInitialRefreshTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleDocumentInboxInitialRefresh() {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clearDocumentInboxInitialRefreshTimer()
|
||||||
|
documentInboxInitialRefreshTimer = window.setTimeout(() => {
|
||||||
|
documentInboxInitialRefreshTimer = null
|
||||||
|
void refreshDocumentInbox()
|
||||||
|
}, props.activeView === 'workbench' ? 1200 : 6000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveNotificationIcon(item) {
|
||||||
|
if (item?.icon) {
|
||||||
|
return item.icon
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item?.tone === 'danger') {
|
||||||
|
return 'mdi mdi-alert-circle-outline'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item?.tone === 'warning') {
|
||||||
|
return 'mdi mdi-alert-outline'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item?.tone === 'success') {
|
||||||
|
return 'mdi mdi-check-circle-outline'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'mdi mdi-bell-outline'
|
||||||
|
}
|
||||||
|
|
||||||
function openNotification(item) {
|
function openNotification(item) {
|
||||||
notificationOpen.value = false
|
notificationOpen.value = false
|
||||||
const target = item?.target || {}
|
const target = item?.target || {}
|
||||||
|
if (target.type === 'documents-center') {
|
||||||
|
emit('navigate', 'documents')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (target.type === 'document' && (target.id || target.claimNo)) {
|
if (target.type === 'document' && (target.id || target.claimNo)) {
|
||||||
emit('openDocument', {
|
emit('openDocument', {
|
||||||
claimId: target.id,
|
claimId: target.id,
|
||||||
@@ -575,6 +674,26 @@ watch(
|
|||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.activeView,
|
||||||
|
(activeView, previousView) => {
|
||||||
|
if (activeView === 'workbench' && previousView !== 'workbench') {
|
||||||
|
clearDocumentInboxInitialRefreshTimer()
|
||||||
|
void refreshDocumentInbox({ force: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
scheduleDocumentInboxInitialRefresh()
|
||||||
|
startDocumentInboxPolling()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearDocumentInboxInitialRefreshTimer()
|
||||||
|
stopDocumentInboxPolling()
|
||||||
|
})
|
||||||
|
|
||||||
function setRange(range) {
|
function setRange(range) {
|
||||||
emit('update:activeRange', range)
|
emit('update:activeRange', range)
|
||||||
calendarOpen.value = false
|
calendarOpen.value = false
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
@batch-approve="toast('已批量通过 23 条审批任务。')"
|
@batch-approve="toast('已批量通过 23 条审批任务。')"
|
||||||
@new-application="openExpenseApplicationCreate"
|
@new-application="openExpenseApplicationCreate"
|
||||||
@open-document="openWorkbenchDocument"
|
@open-document="openWorkbenchDocument"
|
||||||
|
@navigate="handleNavigate"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FilterBar
|
<FilterBar
|
||||||
|
|||||||
@@ -198,8 +198,12 @@ test('documents center category tabs render bubble counts for new documents', ()
|
|||||||
documentsCenterView,
|
documentsCenterView,
|
||||||
/const scopeTabItems = computed\(\(\) =>[\s\S]*badgeCount: scopeNewCountMap\.value\[tab\] \|\| 0/
|
/const scopeTabItems = computed\(\(\) =>[\s\S]*badgeCount: scopeNewCountMap\.value\[tab\] \|\| 0/
|
||||||
)
|
)
|
||||||
assert.match(documentListSharedStyles, /\.scope-tab-label\s*\{[\s\S]*position:\s*relative;/)
|
const scopeTabBadgeBlock = documentListSharedStyles.match(/\.scope-tab-badge\s*\{[^}]*\}/)?.[0] || ''
|
||||||
assert.match(documentListSharedStyles, /\.scope-tab-badge\s*\{[\s\S]*position:\s*absolute;[\s\S]*top:\s*-8px;[\s\S]*height:\s*15px;/)
|
assert.match(documentListSharedStyles, /\.scope-tab-label\s*\{[\s\S]*align-items:\s*flex-start;[\s\S]*gap:\s*4px;/)
|
||||||
|
assert.match(scopeTabBadgeBlock, /position:\s*static;/)
|
||||||
|
assert.match(scopeTabBadgeBlock, /height:\s*14px;/)
|
||||||
|
assert.match(scopeTabBadgeBlock, /margin-top:\s*-5px;/)
|
||||||
|
assert.doesNotMatch(scopeTabBadgeBlock, /position:\s*absolute;/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('documents center can mark all unread documents as read from toolbar', () => {
|
test('documents center can mark all unread documents as read from toolbar', () => {
|
||||||
|
|||||||
@@ -13,6 +13,21 @@ const sidebarStyles = readFileSync(
|
|||||||
'utf8'
|
'utf8'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const topbar = readFileSync(
|
||||||
|
fileURLToPath(new URL('../src/components/layout/TopBar.vue', import.meta.url)),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
|
||||||
|
const topbarStyles = readFileSync(
|
||||||
|
fileURLToPath(new URL('../src/assets/styles/components/top-bar.css', import.meta.url)),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
|
||||||
|
const appShellRouteView = readFileSync(
|
||||||
|
fileURLToPath(new URL('../src/views/AppShellRouteView.vue', import.meta.url)),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
|
||||||
const documentInbox = readFileSync(
|
const documentInbox = readFileSync(
|
||||||
fileURLToPath(new URL('../src/composables/useDocumentCenterInbox.js', import.meta.url)),
|
fileURLToPath(new URL('../src/composables/useDocumentCenterInbox.js', import.meta.url)),
|
||||||
'utf8'
|
'utf8'
|
||||||
@@ -23,19 +38,37 @@ const documentNewState = readFileSync(
|
|||||||
'utf8'
|
'utf8'
|
||||||
)
|
)
|
||||||
|
|
||||||
test('sidebar renders a red dot for unread document center rows', () => {
|
test('sidebar no longer renders document center unread indicators', () => {
|
||||||
assert.match(sidebar, /useDocumentCenterInbox/)
|
assert.doesNotMatch(sidebar, /useDocumentCenterInbox/)
|
||||||
assert.match(sidebar, /hasUnread: documentInboxHasUnread/)
|
assert.doesNotMatch(sidebar, /hasUnread: documentInboxHasUnread/)
|
||||||
assert.match(sidebar, /class="nav-label-text"[\s\S]*class="nav-unread-dot nav-unread-dot-label"/)
|
assert.doesNotMatch(sidebar, /hasNewMessage/)
|
||||||
assert.match(sidebar, /class="nav-unread-dot nav-unread-dot-collapsed"/)
|
assert.doesNotMatch(sidebar, /nav-label-text/)
|
||||||
assert.match(sidebar, /hasNewMessage: item\.id === 'documents' \? documentInboxHasUnread\.value : false/)
|
assert.doesNotMatch(sidebar, /nav-unread-dot/)
|
||||||
assert.match(sidebar, /void refreshDocumentInbox\(\)/)
|
assert.match(sidebar, /<span class="nav-label">\{\{ item\.displayLabel \}\}<\/span>/)
|
||||||
assert.match(sidebar, /startDocumentInboxPolling\(\)/)
|
assert.doesNotMatch(sidebarStyles, /\.nav-label-text\s*\{/)
|
||||||
assert.match(sidebar, /stopDocumentInboxPolling\(\)/)
|
assert.doesNotMatch(sidebarStyles, /\.nav-unread-dot/)
|
||||||
assert.match(sidebarStyles, /\.nav-label-text\s*\{[\s\S]*position:\s*relative;/)
|
assert.match(sidebarStyles, /\.nav-label\s*\{[\s\S]*overflow:\s*hidden;[\s\S]*text-overflow:\s*ellipsis;/)
|
||||||
assert.match(sidebarStyles, /\.nav-unread-dot\s*\{[\s\S]*background:\s*#ef4444;/)
|
})
|
||||||
assert.match(sidebarStyles, /\.nav-unread-dot-label\s*\{[\s\S]*position:\s*absolute;[\s\S]*top:\s*-8px;/)
|
|
||||||
assert.match(sidebarStyles, /\.rail-collapsed \.nav-unread-dot-collapsed\s*\{[\s\S]*position:\s*absolute;/)
|
test('topbar bell owns document center unread notifications', () => {
|
||||||
|
assert.match(topbar, /useDocumentCenterInbox/)
|
||||||
|
assert.match(topbar, /unreadCount: documentInboxUnreadCount/)
|
||||||
|
assert.match(topbar, /const workbenchNotificationCount = computed/)
|
||||||
|
assert.match(topbar, /const count = workbenchNotificationCount\.value \+ Number\(documentInboxUnreadCount\.value \|\| 0\)/)
|
||||||
|
assert.match(topbar, /const documentInboxNotification = computed/)
|
||||||
|
assert.match(topbar, /id: 'document-center-unread'/)
|
||||||
|
assert.match(topbar, /title: '单据中心有新单据'/)
|
||||||
|
assert.match(topbar, /target: \{ type: 'documents-center' \}/)
|
||||||
|
assert.match(topbar, /emit\('navigate', 'documents'\)/)
|
||||||
|
assert.match(appShellRouteView, /@navigate="handleNavigate"/)
|
||||||
|
assert.match(topbar, /startDocumentInboxPolling\(\)/)
|
||||||
|
assert.match(topbar, /stopDocumentInboxPolling\(\)/)
|
||||||
|
assert.match(topbar, /class="notification-type-icon" :class="item\.tone"/)
|
||||||
|
assert.match(topbarStyles, /\.notification-head-copy\s*\{[\s\S]*display:\s*grid;/)
|
||||||
|
assert.match(topbarStyles, /\.notification-tabs button em\s*\{[\s\S]*border-radius:\s*999px;/)
|
||||||
|
assert.match(topbarStyles, /\.notification-row\s*\{[\s\S]*grid-template-columns:\s*34px minmax\(0,\s*1fr\) 16px;/)
|
||||||
|
assert.match(topbarStyles, /\.notification-type-icon\.danger\s*\{[\s\S]*background:\s*#fff5f5;/)
|
||||||
|
assert.doesNotMatch(topbarStyles, /\.notification-dot/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('document inbox reuses document center viewed-key state', () => {
|
test('document inbox reuses document center viewed-key state', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user