refactor(ui): 界面升级与代码规范优化

This commit is contained in:
Ray.Hao
2026-06-18 18:38:12 +08:00
parent 288ea9b43d
commit 14035cfa4d
130 changed files with 7813 additions and 5893 deletions

View File

@@ -1,51 +1,68 @@
<template>
<el-dropdown class="notice__dropdown" trigger="click">
<el-dropdown class="notice__dropdown" trigger="click" @visible-change="handleVisibleChange">
<div class="notice__trigger">
<el-badge v-if="unreadTotal > 0" :value="unreadTotal" :max="99">
<div class="i-svg:bell" />
</el-badge>
<div v-else class="i-svg:bell" />
<div class="notice__icon">
<el-icon class="notice__bell" :size="16">
<Bell />
</el-icon>
<span v-if="unreadTotal > 0" class="notice__pulse" />
</div>
</div>
<template #dropdown>
<div class="p-5">
<template v-if="list.length > 0">
<div v-for="item in list" :key="item.id" class="w-500px py-3">
<div class="flex-y-center">
<DictTag v-model="item.type" code="notice_type" size="small" />
<el-text
size="small"
class="w-200px cursor-pointer !ml-2 !flex-1"
truncated
@click="read(item.id)"
>
{{ item.title }}
</el-text>
<div class="notice__panel">
<div class="notice__tabs">
<button
type="button"
:class="['notice__tab', { 'is-active': activeStatus === 0 }]"
@click.stop="switchStatus(0)"
>
<span>未读</span>
<span v-if="unreadTotal > 0" class="notice__count">
{{ unreadTotal > 99 ? "99+" : unreadTotal }}
</span>
</button>
<button
type="button"
:class="['notice__tab', { 'is-active': activeStatus === 1 }]"
@click.stop="switchStatus(1)"
>
已读
</button>
</div>
<div class="text-xs text-gray">
{{ item.publishTime }}
</div>
<div v-if="list.length > 0" class="notice__list">
<div
v-for="item in list"
:key="item.id"
:class="['notice__item', { 'is-read': item.isRead === 1 }]"
>
<DictTag v-model="item.type" code="notice_type" size="small" />
<el-text size="small" class="notice__title" truncated @click="read(item.id)">
{{ item.title }}
</el-text>
<div class="notice__time">
{{ item.publishTime }}
</div>
</div>
<el-divider />
<div class="flex-x-between">
<el-link type="primary" underline="never" @click="goMore">
<span class="text-xs">查看更多</span>
<el-icon class="text-xs">
<ArrowRight />
</el-icon>
</el-link>
<el-link v-if="list.length > 0" type="primary" underline="never" @click="readAll">
<span class="text-xs">全部已读</span>
</el-link>
</div>
</template>
<template v-else>
<div class="flex-center h-150px w-350px">
<el-empty :image-size="50" description="暂无消息" />
</div>
</template>
</div>
<div v-else class="notice__empty">
<el-empty :image-size="54" :description="emptyText" />
</div>
<el-divider class="notice__divider" />
<div class="notice__footer">
<el-link type="primary" underline="never" @click="goMore">
<span class="text-xs">查看更多</span>
<el-icon class="text-xs">
<ArrowRight />
</el-icon>
</el-link>
<el-link v-if="unreadTotal > 0" type="primary" underline="never" @click="readAll">
<span class="text-xs">全部已读</span>
</el-link>
</div>
</div>
</template>
</el-dropdown>
@@ -78,7 +95,23 @@
<script setup lang="ts">
import { useNotice } from "./useNotice";
const { list, unreadTotal, detail, dialogVisible, read, readAll, goMore } = useNotice();
const {
list,
unreadTotal,
activeStatus,
emptyText,
detail,
dialogVisible,
switchStatus,
refresh,
read,
readAll,
goMore,
} = useNotice();
function handleVisibleChange(visible: boolean) {
if (visible) refresh();
}
</script>
<style lang="scss" scoped>
@@ -98,5 +131,155 @@ const { list, unreadTotal, detail, dialogVisible, read, readAll, goMore } = useN
width: 100%;
height: 100%;
}
&__icon {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 6px;
}
&__bell {
color: var(--el-text-color-regular);
}
&__pulse {
position: absolute;
top: 4px;
right: 5px;
width: 6px;
height: 6px;
background: var(--el-color-danger);
border: 1px solid var(--el-bg-color);
border-radius: 50%;
&::after {
position: absolute;
inset: -1px;
content: "";
background: var(--el-color-danger);
border-radius: inherit;
animation: notice-pulse 1.8s ease-out infinite;
}
}
&__panel {
width: 390px;
padding: 12px;
}
&__tabs {
display: flex;
gap: 4px;
padding: 3px;
margin-bottom: 8px;
background: var(--el-fill-color-light);
border-radius: 6px;
}
&__tab {
display: inline-flex;
flex: 1;
gap: 6px;
align-items: center;
justify-content: center;
height: 28px;
padding: 0 10px;
font-size: 12px;
color: var(--el-text-color-secondary);
cursor: pointer;
background: transparent;
border: 0;
border-radius: 5px;
&.is-active {
font-weight: 500;
color: var(--el-color-primary);
background: var(--el-bg-color-overlay);
box-shadow: 0 1px 2px rgb(15 23 42 / 5%);
}
}
&__count {
min-width: 18px;
height: 16px;
padding: 0 5px;
font-size: 10px;
line-height: 16px;
color: #fff;
text-align: center;
background: var(--el-color-danger);
border-radius: 999px;
}
&__list {
max-height: 280px;
overflow-y: auto;
}
&__item {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
padding: 10px 2px;
& + & {
border-top: 1px solid var(--el-border-color-lighter);
}
&.is-read {
opacity: 0.72;
}
}
&__title {
min-width: 0;
cursor: pointer;
}
&__time {
flex-shrink: 0;
font-size: 12px;
color: var(--el-text-color-secondary);
white-space: nowrap;
}
&__empty {
display: flex;
align-items: center;
justify-content: center;
height: 150px;
}
&__divider {
margin: 10px 0;
}
&__footer {
display: flex;
align-items: center;
justify-content: space-between;
}
}
@keyframes notice-pulse {
0% {
opacity: 0.45;
transform: scale(1);
}
70% {
opacity: 0;
transform: scale(2.8);
}
100% {
opacity: 0;
transform: scale(2.8);
}
}
</style>

View File

@@ -1,7 +1,7 @@
/**
* 通知中心逻辑
*/
import { ref, onMounted, onBeforeUnmount } from "vue";
import { computed, ref, onMounted, onBeforeUnmount } from "vue";
import type { NoticeItem, NoticeDetail, NoticeQueryParams } from "@/api/system/notice";
import NoticeAPI from "@/api/system/notice";
import { useSse } from "@/composables";
@@ -10,6 +10,7 @@ import router from "@/router";
const PAGE_SIZE = 5;
const NOTICE_EVENT = "notice";
type NoticeStatus = 0 | 1;
export function useNotice() {
const { on } = useSse();
@@ -17,8 +18,10 @@ export function useNotice() {
// 状态
const list = ref<NoticeItem[]>([]);
const unreadTotal = ref(0);
const activeStatus = ref<NoticeStatus>(0);
const detail = ref<NoticeDetail | null>(null);
const dialogVisible = ref(false);
const emptyText = computed(() => (activeStatus.value === 0 ? "暂无未读消息" : "暂无已读消息"));
let unsubscribe: (() => void) | null = null;
@@ -30,29 +33,64 @@ export function useNotice() {
const query: NoticeQueryParams = {
pageNum: 1,
pageSize: PAGE_SIZE,
isRead: 0,
isRead: activeStatus.value,
...params,
};
const page = await NoticeAPI.getMyNoticePage(query);
list.value = page.list || [];
if (query.isRead === 0) {
unreadTotal.value = page.total ?? 0;
}
}
async function fetchUnreadTotal() {
const page = await NoticeAPI.getMyNoticePage({
pageNum: 1,
pageSize: 1,
isRead: 0,
});
unreadTotal.value = page.total ?? 0;
}
async function switchStatus(status: NoticeStatus) {
if (activeStatus.value === status) return;
activeStatus.value = status;
await fetchList();
}
async function refresh() {
await Promise.all([
fetchList(),
activeStatus.value === 0 ? Promise.resolve() : fetchUnreadTotal(),
]);
}
async function read(id: string) {
const item = list.value.find((notice: NoticeItem) => notice.id === id);
const wasUnread = item?.isRead !== 1;
detail.value = await NoticeAPI.getDetail(id);
dialogVisible.value = true;
const idx = list.value.findIndex((item: NoticeItem) => item.id === id);
if (idx >= 0) list.value.splice(idx, 1);
if (unreadTotal.value > 0) unreadTotal.value -= 1;
if (wasUnread && unreadTotal.value > 0) unreadTotal.value -= 1;
await fetchList();
await refresh();
}
async function readAll() {
if (unreadTotal.value <= 0) return;
await NoticeAPI.readAll();
list.value = [];
unreadTotal.value = 0;
if (activeStatus.value === 0) {
list.value = [];
} else {
await fetchList();
}
ElMessage.success("已全部标记为已读");
}
@@ -71,15 +109,16 @@ export function useNotice() {
try {
if (!data.id) return;
if (list.value.some((item: NoticeItem) => item.id === data.id)) return;
unreadTotal.value += 1;
if (activeStatus.value !== 0) return;
if (list.value.some((item: NoticeItem) => item.id === data.id)) return;
list.value.unshift({
id: data.id,
title: data.title,
type: data.type,
publishTime: data.publishTime,
isRead: 0,
} as NoticeItem);
if (list.value.length > PAGE_SIZE) {
@@ -103,8 +142,9 @@ export function useNotice() {
const idx = list.value.findIndex((item: NoticeItem) => item.id === data.id);
if (idx >= 0) {
const wasUnread = list.value[idx].isRead !== 1;
list.value.splice(idx, 1);
if (unreadTotal.value > 0) unreadTotal.value -= 1;
if (wasUnread && unreadTotal.value > 0) unreadTotal.value -= 1;
}
} catch (e) {
console.error("处理撤回通知失败", e);
@@ -117,7 +157,7 @@ export function useNotice() {
// ============================================
onMounted(() => {
fetchList();
refresh();
setupSubscription();
});
@@ -131,9 +171,13 @@ export function useNotice() {
return {
list,
unreadTotal,
activeStatus,
emptyText,
detail,
dialogVisible,
fetchList,
switchStatus,
refresh,
read,
readAll,
goMore,