refactor(projects): 消息提示增加等级区分
This commit is contained in:
@@ -119,3 +119,12 @@ export const RDMS_WORKLOG_DIFFICULTY_DICT_CODE = 'rdms_task_item_worklog_difficu
|
|||||||
* 来源口径:`overtime-application-design.md` 明确时长下拉字典为 rdms_overtime_duration
|
* 来源口径:`overtime-application-design.md` 明确时长下拉字典为 rdms_overtime_duration
|
||||||
*/
|
*/
|
||||||
export const RDMS_OVERTIME_DURATION_DICT_CODE = 'rdms_overtime_duration';
|
export const RDMS_OVERTIME_DURATION_DICT_CODE = 'rdms_overtime_duration';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 站内信消息等级字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:站内信 NotifyMessage.level(1=普通 2=提醒 3=警告 4=严重,数字越大越紧急)
|
||||||
|
* 来源口径:`2026-06-13-站内信消息等级-前端对接.html` 明确等级字典为 notify_message_level,
|
||||||
|
* 显示名与颜色(hex)均走字典,前端按 level 取色不硬编码。
|
||||||
|
*/
|
||||||
|
export const NOTIFY_MESSAGE_LEVEL_DICT_CODE = 'notify_message_level';
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import { useDebounceFn, useInfiniteScroll } from '@vueuse/core';
|
import { useDebounceFn, useInfiniteScroll } from '@vueuse/core';
|
||||||
|
import { NOTIFY_MESSAGE_LEVEL_DICT_CODE } from '@/constants/dict';
|
||||||
import {
|
import {
|
||||||
fetchGetMyNotifyMessagePage,
|
fetchGetMyNotifyMessagePage,
|
||||||
fetchGetUnreadNotifyCount,
|
fetchGetUnreadNotifyCount,
|
||||||
fetchUpdateAllNotifyMessageRead,
|
fetchUpdateAllNotifyMessageRead,
|
||||||
fetchUpdateNotifyMessageRead
|
fetchUpdateNotifyMessageRead
|
||||||
} from '@/service/api';
|
} from '@/service/api';
|
||||||
import { formatRelativeTime } from '@/utils/datetime';
|
import { useDictStore } from '@/store/modules/dict';
|
||||||
|
import { formatDateTime, formatRelativeTime } from '@/utils/datetime';
|
||||||
|
|
||||||
defineOptions({ name: 'NotificationBell' });
|
defineOptions({ name: 'NotificationBell' });
|
||||||
|
|
||||||
|
const dictStore = useDictStore();
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
const UNREAD_COUNT_POLL_INTERVAL = 30 * 1000;
|
const UNREAD_COUNT_POLL_INTERVAL = 15 * 1000;
|
||||||
|
|
||||||
type TabKey = 'unread' | 'read';
|
type TabKey = 'unread' | 'read';
|
||||||
|
|
||||||
@@ -43,10 +47,18 @@ const drawerOpen = ref(false);
|
|||||||
const activeTab = ref<TabKey>('unread');
|
const activeTab = ref<TabKey>('unread');
|
||||||
const searchKeyword = ref('');
|
const searchKeyword = ref('');
|
||||||
|
|
||||||
|
const detailVisible = ref(false);
|
||||||
|
const detailMessage = ref<Api.NotifyMessage.NotifyMessage | null>(null);
|
||||||
|
|
||||||
function keywordParam() {
|
function keywordParam() {
|
||||||
return searchKeyword.value.trim() || undefined;
|
return searchKeyword.value.trim() || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 列表圆点颜色:跟随消息等级(与等级徽标同一字典色源);取不到时回 undefined,由 CSS 兜底 */
|
||||||
|
function levelDotColor(level: number) {
|
||||||
|
return dictStore.getDictItem(NOTIFY_MESSAGE_LEVEL_DICT_CODE, level)?.colorType ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshUnreadCount() {
|
async function refreshUnreadCount() {
|
||||||
const { data, error } = await fetchGetUnreadNotifyCount();
|
const { data, error } = await fetchGetUnreadNotifyCount();
|
||||||
if (!error && typeof data === 'number') {
|
if (!error && typeof data === 'number') {
|
||||||
@@ -180,6 +192,16 @@ async function markRead(item: Api.NotifyMessage.NotifyMessage) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openDetail(row: Api.NotifyMessage.NotifyMessage) {
|
||||||
|
// 弹框持有该行引用,正文不随未读列表移除而消失
|
||||||
|
detailMessage.value = row;
|
||||||
|
detailVisible.value = true;
|
||||||
|
// 未读消息「打开即已读」:后台静默标记,避免"看一半就跑到已读"
|
||||||
|
if (!row.readStatus) {
|
||||||
|
markRead(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function markAllRead() {
|
async function markAllRead() {
|
||||||
const { error } = await fetchUpdateAllNotifyMessageRead();
|
const { error } = await fetchUpdateAllNotifyMessageRead();
|
||||||
if (error) return;
|
if (error) return;
|
||||||
@@ -193,6 +215,8 @@ async function markAllRead() {
|
|||||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 等级徽标颜色/文案走字典:若未在登录缓存内则按编码补拉一次(已缓存时不发请求)
|
||||||
|
dictStore.ensureDictData(NOTIFY_MESSAGE_LEVEL_DICT_CODE);
|
||||||
refreshUnreadCount();
|
refreshUnreadCount();
|
||||||
pollTimer = setInterval(() => {
|
pollTimer = setInterval(() => {
|
||||||
if (document.hidden) return;
|
if (document.hidden) return;
|
||||||
@@ -253,12 +277,15 @@ onBeforeUnmount(() => {
|
|||||||
v-for="row in listStates.unread.items"
|
v-for="row in listStates.unread.items"
|
||||||
:key="row.id"
|
:key="row.id"
|
||||||
class="notification-bell__row is-unread"
|
class="notification-bell__row is-unread"
|
||||||
@click="markRead(row)"
|
@click="openDetail(row)"
|
||||||
>
|
>
|
||||||
<span class="notification-bell__row-dot" />
|
<span class="notification-bell__row-dot" :style="{ backgroundColor: levelDotColor(row.level) }" />
|
||||||
<div class="notification-bell__row-body">
|
<div class="notification-bell__row-body">
|
||||||
<div class="notification-bell__row-title">{{ row.templateContent }}</div>
|
<div class="notification-bell__row-title">{{ row.templateContent }}</div>
|
||||||
<div class="notification-bell__row-time">{{ formatRelativeTime(row.createTime) }}</div>
|
<div class="notification-bell__row-meta">
|
||||||
|
<DictTag :dict-code="NOTIFY_MESSAGE_LEVEL_DICT_CODE" :value="row.level" size="small" round />
|
||||||
|
<span class="notification-bell__row-time">{{ formatRelativeTime(row.createTime) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -273,18 +300,23 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<ElTabPane name="read">
|
<ElTabPane name="read">
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="notification-bell__tab-label">
|
<span class="notification-bell__tab-label">已读</span>
|
||||||
已读
|
|
||||||
<span class="notification-bell__tab-count">{{ listStates.read.total }}</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
<ElScrollbar ref="readScrollbar" class="notification-bell__scroll">
|
<ElScrollbar ref="readScrollbar" class="notification-bell__scroll">
|
||||||
<ul v-if="listStates.read.items.length > 0" class="notification-bell__list">
|
<ul v-if="listStates.read.items.length > 0" class="notification-bell__list">
|
||||||
<li v-for="row in listStates.read.items" :key="row.id" class="notification-bell__row">
|
<li
|
||||||
<span class="notification-bell__row-dot" />
|
v-for="row in listStates.read.items"
|
||||||
|
:key="row.id"
|
||||||
|
class="notification-bell__row"
|
||||||
|
@click="openDetail(row)"
|
||||||
|
>
|
||||||
|
<span class="notification-bell__row-dot" :style="{ backgroundColor: levelDotColor(row.level) }" />
|
||||||
<div class="notification-bell__row-body">
|
<div class="notification-bell__row-body">
|
||||||
<div class="notification-bell__row-title">{{ row.templateContent }}</div>
|
<div class="notification-bell__row-title">{{ row.templateContent }}</div>
|
||||||
<div class="notification-bell__row-time">{{ formatRelativeTime(row.createTime) }}</div>
|
<div class="notification-bell__row-meta">
|
||||||
|
<DictTag :dict-code="NOTIFY_MESSAGE_LEVEL_DICT_CODE" :value="row.level" size="small" round />
|
||||||
|
<span class="notification-bell__row-time">{{ formatRelativeTime(row.createTime) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -303,6 +335,28 @@ onBeforeUnmount(() => {
|
|||||||
<ElButton @click="closeDrawer">关闭</ElButton>
|
<ElButton @click="closeDrawer">关闭</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ElDrawer>
|
</ElDrawer>
|
||||||
|
|
||||||
|
<ElDialog v-model="detailVisible" width="520px" align-center class="notification-bell__detail">
|
||||||
|
<template #header>
|
||||||
|
<div class="notification-bell__detail-head">
|
||||||
|
<span class="notification-bell__detail-sender">{{ detailMessage?.templateNickname || '系统通知' }}</span>
|
||||||
|
<DictTag
|
||||||
|
v-if="detailMessage"
|
||||||
|
:dict-code="NOTIFY_MESSAGE_LEVEL_DICT_CODE"
|
||||||
|
:value="detailMessage.level"
|
||||||
|
size="small"
|
||||||
|
round
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-if="detailMessage" class="notification-bell__detail-body">
|
||||||
|
<div class="notification-bell__detail-content">{{ detailMessage.templateContent }}</div>
|
||||||
|
<div class="notification-bell__detail-time">收到于 {{ formatDateTime(detailMessage.createTime) }}</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="detailVisible = false">关闭</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -484,18 +538,15 @@ onBeforeUnmount(() => {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 12px 4px;
|
padding: 12px 4px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-bell__row + .notification-bell__row {
|
.notification-bell__row + .notification-bell__row {
|
||||||
border-top: 1px dashed var(--el-border-color-lighter);
|
border-top: 1px dashed var(--el-border-color-lighter);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-bell__row.is-unread {
|
.notification-bell__row:hover {
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 120ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-bell__row.is-unread:hover {
|
|
||||||
background-color: var(--el-fill-color-light);
|
background-color: var(--el-fill-color-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,8 +578,14 @@ onBeforeUnmount(() => {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-bell__row-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.notification-bell__row-time {
|
.notification-bell__row-time {
|
||||||
margin-top: 4px;
|
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
@@ -547,4 +604,41 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-bell__detail-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__detail-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding-right: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__detail-sender {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__detail-content {
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__detail-time {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const { selectedKeyDummy, handleSelect } = useMenu();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
||||||
<ElMenu
|
<ElMenu
|
||||||
ellipsis
|
ellipsis
|
||||||
class="w-full"
|
class="w-full"
|
||||||
|
|||||||
@@ -93,7 +93,8 @@ function isMenuActive(menu: App.Global.Menu | App.ObjectContext.Menu): boolean {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
<!-- defer:BaseLayout 二次挂载时 GlobalMenu 已缓存为同步挂载,目标 div 还未插入 document,不延迟解析会静默失败且不重试 -->
|
||||||
|
<Teleport defer :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
||||||
<div class="mix-header-nav size-full min-w-0 flex-y-center">
|
<div class="mix-header-nav size-full min-w-0 flex-y-center">
|
||||||
<button
|
<button
|
||||||
v-if="activeFirstLevelMenu"
|
v-if="activeFirstLevelMenu"
|
||||||
@@ -161,7 +162,7 @@ function isMenuActive(menu: App.Global.Menu | App.ObjectContext.Menu): boolean {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<FirstLevelMenu
|
<FirstLevelMenu
|
||||||
:menus="allMenus"
|
:menus="allMenus"
|
||||||
:active-menu-key="activeFirstLevelMenuKey"
|
:active-menu-key="activeFirstLevelMenuKey"
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
||||||
<ElMenu
|
<ElMenu
|
||||||
ellipsis
|
ellipsis
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@@ -66,7 +66,7 @@ watch(
|
|||||||
<MenuItem v-for="item in firstLevelMenus" :key="item.key" :item="item" :index="item.key" />
|
<MenuItem v-for="item in firstLevelMenus" :key="item.key" :item="item" :index="item.key" />
|
||||||
</ElMenu>
|
</ElMenu>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<SimpleScrollbar>
|
<SimpleScrollbar>
|
||||||
<ElMenu
|
<ElMenu
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<SimpleScrollbar>
|
<SimpleScrollbar>
|
||||||
<ElMenu
|
<ElMenu
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
|
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
|
||||||
<FirstLevelMenu
|
<FirstLevelMenu
|
||||||
:menus="allMenus"
|
:menus="allMenus"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ function createBatchDeleteQuery(ids: number[]) {
|
|||||||
type DictDataResponse = Omit<Api.Dict.DictData, 'colorType'> & {
|
type DictDataResponse = Omit<Api.Dict.DictData, 'colorType'> & {
|
||||||
colorType?: string | null;
|
colorType?: string | null;
|
||||||
color_type?: string | null;
|
color_type?: string | null;
|
||||||
|
css_class?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DictDataPageResponse = Omit<Api.Dict.PageResult<Api.Dict.DictData>, 'list'> & {
|
type DictDataPageResponse = Omit<Api.Dict.PageResult<Api.Dict.DictData>, 'list'> & {
|
||||||
@@ -28,6 +29,7 @@ type DictDataPageResponse = Omit<Api.Dict.PageResult<Api.Dict.DictData>, 'list'>
|
|||||||
type FrontendDictDataResponse = Omit<Api.Dict.FrontendDictData, 'colorType'> & {
|
type FrontendDictDataResponse = Omit<Api.Dict.FrontendDictData, 'colorType'> & {
|
||||||
colorType?: string | null;
|
colorType?: string | null;
|
||||||
color_type?: string | null;
|
color_type?: string | null;
|
||||||
|
css_class?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FrontendDictCacheResponse = Record<string, FrontendDictDataResponse[]>;
|
type FrontendDictCacheResponse = Record<string, FrontendDictDataResponse[]>;
|
||||||
@@ -37,20 +39,22 @@ function normalizeColorType(value?: string | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDictData(data: DictDataResponse): Api.Dict.DictData {
|
function normalizeDictData(data: DictDataResponse): Api.Dict.DictData {
|
||||||
const { color_type: colorTypeFromSnakeCase, ...rest } = data;
|
const { color_type: colorTypeFromSnakeCase, css_class: cssClassFromSnakeCase, ...rest } = data;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
colorType: normalizeColorType(data.colorType ?? colorTypeFromSnakeCase)
|
colorType: normalizeColorType(data.colorType ?? colorTypeFromSnakeCase),
|
||||||
|
cssClass: normalizeColorType(data.cssClass ?? cssClassFromSnakeCase)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeFrontendDictData(data: FrontendDictDataResponse): Api.Dict.FrontendDictData {
|
function normalizeFrontendDictData(data: FrontendDictDataResponse): Api.Dict.FrontendDictData {
|
||||||
const { color_type: colorTypeFromSnakeCase, ...rest } = data;
|
const { color_type: colorTypeFromSnakeCase, css_class: cssClassFromSnakeCase, ...rest } = data;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
colorType: normalizeColorType(data.colorType ?? colorTypeFromSnakeCase)
|
colorType: normalizeColorType(data.colorType ?? colorTypeFromSnakeCase),
|
||||||
|
cssClass: normalizeColorType(data.cssClass ?? cssClassFromSnakeCase)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import { type ServiceRequestResult, mapServiceResult, normalizeStringId, safeJso
|
|||||||
|
|
||||||
const NOTIFY_MESSAGE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/notify-message`;
|
const NOTIFY_MESSAGE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/notify-message`;
|
||||||
|
|
||||||
type NotifyMessageResponse = Omit<Api.NotifyMessage.NotifyMessage, 'id'> & {
|
type NotifyMessageResponse = Omit<Api.NotifyMessage.NotifyMessage, 'id' | 'level'> & {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
|
/** 后端老消息可能不带 level,按可空接收,normalize 时回落普通(1) */
|
||||||
|
level?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MyNotifyMessagePageResponse = Omit<Api.NotifyMessage.PageResult<Api.NotifyMessage.NotifyMessage>, 'list'> & {
|
type MyNotifyMessagePageResponse = Omit<Api.NotifyMessage.PageResult<Api.NotifyMessage.NotifyMessage>, 'list'> & {
|
||||||
@@ -15,7 +17,8 @@ type MyNotifyMessagePageResponse = Omit<Api.NotifyMessage.PageResult<Api.NotifyM
|
|||||||
function normalizeNotifyMessage(data: NotifyMessageResponse): Api.NotifyMessage.NotifyMessage {
|
function normalizeNotifyMessage(data: NotifyMessageResponse): Api.NotifyMessage.NotifyMessage {
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
id: normalizeStringId(data.id)
|
id: normalizeStringId(data.id),
|
||||||
|
level: data.level ?? 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,15 @@ function normalizeColorType(raw: unknown): string | null {
|
|||||||
return HEX_COLOR_PATTERN.test(trimmed) ? trimmed : null;
|
return HEX_COLOR_PATTERN.test(trimmed) ? trimmed : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析字典项最终展示色(hex)。
|
||||||
|
* 精确色 cssClass 优先(覆盖 colorType 落到语义色无法区分黄/橙等场景),其次 colorType;
|
||||||
|
* 两者都不是合法 hex 时回落 null(默认渲染)。
|
||||||
|
*/
|
||||||
|
function resolveDisplayColor(colorType: unknown, cssClass: unknown): string | null {
|
||||||
|
return normalizeColorType(cssClass) ?? normalizeColorType(colorType);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeFrontendDictData(
|
function normalizeFrontendDictData(
|
||||||
dictType: string,
|
dictType: string,
|
||||||
list: Api.Dict.FrontendDictData[],
|
list: Api.Dict.FrontendDictData[],
|
||||||
@@ -40,7 +49,7 @@ function normalizeFrontendDictData(
|
|||||||
dictType: item.dictType || dictType,
|
dictType: item.dictType || dictType,
|
||||||
sort: item.sort,
|
sort: item.sort,
|
||||||
status: item.status ?? 0,
|
status: item.status ?? 0,
|
||||||
colorType: normalizeColorType(item.colorType),
|
colorType: resolveDisplayColor(item.colorType, item.cssClass),
|
||||||
remark: item.remark ?? null,
|
remark: item.remark ?? null,
|
||||||
createTime: 0
|
createTime: 0
|
||||||
}));
|
}));
|
||||||
@@ -54,7 +63,7 @@ function normalizeDictDataItem(item: Api.Dict.DictData, dictType: string): Api.D
|
|||||||
value: String(item.value),
|
value: String(item.value),
|
||||||
dictType: item.dictType || dictType,
|
dictType: item.dictType || dictType,
|
||||||
status: item.status ?? 0,
|
status: item.status ?? 0,
|
||||||
colorType: normalizeColorType(item.colorType),
|
colorType: resolveDisplayColor(item.colorType, item.cssClass),
|
||||||
remark: item.remark ?? null
|
remark: item.remark ?? null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/typings/api/dict.d.ts
vendored
4
src/typings/api/dict.d.ts
vendored
@@ -57,6 +57,8 @@ declare namespace Api {
|
|||||||
status: DictStatus;
|
status: DictStatus;
|
||||||
/** 颜色(hex,#xxxxxx);nullable,无值时前端按默认渲染 */
|
/** 颜色(hex,#xxxxxx);nullable,无值时前端按默认渲染 */
|
||||||
colorType?: string | null;
|
colorType?: string | null;
|
||||||
|
/** 精确颜色(hex,#xxxxxx);存在时优先于 colorType,用于 colorType 落到语义色无法区分的场景 */
|
||||||
|
cssClass?: string | null;
|
||||||
/** remark */
|
/** remark */
|
||||||
remark?: string | null;
|
remark?: string | null;
|
||||||
/** create time */
|
/** create time */
|
||||||
@@ -77,6 +79,8 @@ declare namespace Api {
|
|||||||
status?: DictStatus;
|
status?: DictStatus;
|
||||||
/** 颜色(hex,#xxxxxx);nullable,无值时前端按默认渲染 */
|
/** 颜色(hex,#xxxxxx);nullable,无值时前端按默认渲染 */
|
||||||
colorType?: string | null;
|
colorType?: string | null;
|
||||||
|
/** 精确颜色(hex,#xxxxxx);存在时优先于 colorType */
|
||||||
|
cssClass?: string | null;
|
||||||
/** 备注,可用于下拉中文释义展示 */
|
/** 备注,可用于下拉中文释义展示 */
|
||||||
remark?: string | null;
|
remark?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
2
src/typings/api/notify-message.d.ts
vendored
2
src/typings/api/notify-message.d.ts
vendored
@@ -25,6 +25,8 @@ declare namespace Api {
|
|||||||
templateContent: string;
|
templateContent: string;
|
||||||
/** 消息类型,字典 system_notify_template_type */
|
/** 消息类型,字典 system_notify_template_type */
|
||||||
templateType: number;
|
templateType: number;
|
||||||
|
/** 消息等级(字典 notify_message_level,1=普通 2=提醒 3=警告 4=严重,数字越大越紧急);老消息缺省为普通(1) */
|
||||||
|
level: number;
|
||||||
/** 是否已读 */
|
/** 是否已读 */
|
||||||
readStatus: boolean;
|
readStatus: boolean;
|
||||||
/** 阅读时间;未读为 null */
|
/** 阅读时间;未读为 null */
|
||||||
|
|||||||
@@ -18,3 +18,10 @@ export function formatRelativeTime(value: string | number) {
|
|||||||
|
|
||||||
return time.format('YYYY-MM-DD HH:mm');
|
return time.format('YYYY-MM-DD HH:mm');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 绝对时间展示:YYYY-MM-DD HH:mm;空值或非法值回空串 */
|
||||||
|
export function formatDateTime(value: string | number | null | undefined) {
|
||||||
|
if (value === null || value === undefined || value === '') return '';
|
||||||
|
const time = dayjs(value);
|
||||||
|
return time.isValid() ? time.format('YYYY-MM-DD HH:mm') : '';
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user