refactor: 个人中心页面重构和布局代码优化

This commit is contained in:
Ray.Hao
2026-06-21 14:09:27 +08:00
parent 7ffcf8820e
commit 845d117f08
68 changed files with 4026 additions and 3418 deletions

View File

@@ -1,38 +1,65 @@
<template>
<component :is="linkType" v-bind="linkProps(to)">
<component :is="linkType" v-bind="linkProps(to)" @click="handleClick">
<slot />
</component>
</template>
<script setup lang="ts">
import { ExternalOpenModeEnum } from "@/enums";
import { isExternal } from "@/utils/index";
defineOptions({
name: "AppLink",
inheritAttrs: false,
});
import { isExternal } from "@/utils/index";
interface AppLinkTo {
path: string;
meta?: {
externalUrl?: string;
openMode?: number;
type?: string;
};
query?: Record<string, unknown> | null;
}
const props = defineProps({
to: {
type: Object,
required: true,
},
});
const props = defineProps<{
to: AppLinkTo;
}>();
const isExternalLink = computed(() => {
return isExternal(props.to.path || "");
return Boolean(externalUrl.value);
});
const linkType = computed(() => (isExternalLink.value ? "a" : "router-link"));
const linkProps = (to: any) => {
const externalUrl = computed(() => {
if (props.to.meta?.openMode === ExternalOpenModeEnum.NEW_TAB && props.to.meta.externalUrl) {
return props.to.meta.externalUrl;
}
return isExternal(props.to.path || "") ? props.to.path : "";
});
const linkProps = (to: AppLinkTo) => {
if (isExternalLink.value) {
return {
href: to.path,
href: externalUrl.value,
target: "_blank",
rel: "noopener noreferrer",
};
}
return { to };
const { meta, ...routeTo } = to;
void meta;
return { to: routeTo };
};
function handleClick(event: MouseEvent) {
if (!isExternalLink.value) return;
event.preventDefault();
event.stopPropagation();
window.open(externalUrl.value, "_blank", "noopener,noreferrer");
}
</script>

View File

@@ -1,13 +1,10 @@
/**
* 菜单搜索逻辑
*/
import { ref, onMounted, onBeforeUnmount, toRaw } from "vue";
import { RouteRecordRaw, LocationQueryRaw } from "vue-router";
import { onBeforeUnmount, onMounted, ref, toRaw } from "vue";
import type { LocationQueryRaw, RouteRecordRaw } from "vue-router";
import router from "@/router";
import { usePermissionStore } from "@/stores";
import { isExternal } from "@/utils";
/** 搜索项类型 */
/** 命令面板中的可搜索菜单项。 */
interface SearchItem {
title: string;
path: string;
@@ -19,26 +16,22 @@ interface SearchItem {
const STORAGE_KEY = "menu_search_history";
const MAX_HISTORY = 5;
const EXCLUDED_PATHS = ["/redirect", "/login", "/401", "/404"];
export function useCommandPalette() {
const permissionStore = usePermissionStore();
// 状态
// 面板状态
const visible = ref(false);
const keyword = ref("");
const activeIndex = ref(-1);
const inputRef = ref<HTMLInputElement>();
// 菜单数据
const menuItems = ref<SearchItem[]>([]);
const results = ref<SearchItem[]>([]);
const history = ref<SearchItem[]>([]);
// 排除的路由
const excludedPaths = ["/redirect", "/login", "/401", "/404"];
// ============================================
// 弹窗控制
// ============================================
function open() {
keyword.value = "";
results.value = [];
@@ -51,24 +44,28 @@ export function useCommandPalette() {
visible.value = false;
}
// ============================================
// 搜索逻辑
// ============================================
/**
* 搜索仅匹配菜单标题,避免路径命中过多造成结果噪音。
*/
function onSearch() {
activeIndex.value = -1;
if (!keyword.value.trim()) {
results.value = [];
return;
}
const kw = keyword.value.toLowerCase();
results.value = menuItems.value.filter((item) => item.title.toLowerCase().includes(kw));
const keywordText = keyword.value.toLowerCase();
results.value = menuItems.value.filter((item) =>
item.title.toLowerCase().includes(keywordText)
);
}
function getDisplayList() {
function getDisplayList(): SearchItem[] {
return results.value.length ? results.value : history.value;
}
/**
* 键盘选择当前展示列表,搜索为空时回退历史记录。
*/
function onSelect() {
const list = getDisplayList();
if (list.length === 0) return;
@@ -100,10 +97,6 @@ export function useCommandPalette() {
}
}
// ============================================
// 历史记录
// ============================================
function loadHistory() {
try {
const data = localStorage.getItem(STORAGE_KEY);
@@ -117,15 +110,15 @@ export function useCommandPalette() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(history.value));
}
/**
* 历史记录按最近使用排序,并限制本地缓存数量。
*/
function addHistory(item: SearchItem) {
// 去重
const idx = history.value.findIndex((i) => i.path === item.path);
if (idx !== -1) history.value.splice(idx, 1);
const index = history.value.findIndex((historyItem) => historyItem.path === item.path);
if (index !== -1) history.value.splice(index, 1);
// 添加到开头
history.value.unshift(item);
// 限制数量
if (history.value.length > MAX_HISTORY) {
history.value = history.value.slice(0, MAX_HISTORY);
}
@@ -143,17 +136,16 @@ export function useCommandPalette() {
localStorage.removeItem(STORAGE_KEY);
}
// ============================================
// 路由解析
// ============================================
/**
* 将权限路由拍平成命令面板可搜索的菜单项。
*/
function loadRoutes(routes: RouteRecordRaw[], parentPath = "") {
routes.forEach((route) => {
const path = route.path.startsWith("/")
? route.path
: `${parentPath}${parentPath.endsWith("/") ? "" : "/"}${route.path}`;
if (excludedPaths.includes(route.path) || isExternal(route.path)) return;
if (EXCLUDED_PATHS.includes(route.path) || isExternal(route.path)) return;
if (route.children) {
loadRoutes(route.children, path);
@@ -172,10 +164,9 @@ export function useCommandPalette() {
});
}
// ============================================
// 快捷键
// ============================================
/**
* Ctrl/Cmd + K 打开命令面板,并阻止浏览器默认搜索。
*/
function handleKeydown(e: KeyboardEvent) {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
@@ -183,10 +174,6 @@ export function useCommandPalette() {
}
}
// ============================================
// 生命周期
// ============================================
onMounted(() => {
loadRoutes(permissionStore.routes);
loadHistory();

View File

@@ -18,12 +18,10 @@ const settingsStore = useSettingsStore();
const layout = computed(() => settingsStore.layout);
const hamburgerClass = computed(() => {
// 如果暗黑主题
if (settingsStore.resolvedTheme === ThemeMode.DARK) {
return "hamburger--white";
}
// 如果是混合布局 && 侧边栏配色方案是经典蓝
if (
layout.value === LayoutMode.MIX &&
settingsStore.sidebarColorScheme === SidebarColor.CLASSIC_BLUE
@@ -31,7 +29,6 @@ const hamburgerClass = computed(() => {
return "hamburger--white";
}
// 默认返回空字符串
return "";
});
@@ -45,13 +42,23 @@ function toggleClick() {
display: flex;
align-items: center;
justify-content: center;
padding: 0 15px;
width: 48px;
height: 100%;
padding: 0;
color: var(--el-text-color-regular);
cursor: pointer;
.hamburger {
width: 16px;
height: 16px;
font-size: 16px;
vertical-align: middle;
color: currentcolor;
background-color: currentcolor;
transform: scaleX(-1);
transition: transform 0.3s ease;
transition:
color 0.16s,
transform 0.3s ease;
&--white {
color: #fff;
@@ -61,5 +68,9 @@ function toggleClick() {
transform: scaleX(1);
}
}
&:hover {
color: var(--el-color-primary);
}
}
</style>

View File

@@ -1,21 +1,29 @@
/**
* 通知中心逻辑
*/
import { computed, ref, onMounted, onBeforeUnmount } from "vue";
import type { NoticeItem, NoticeDetail, NoticeQueryParams } from "@/api/system/notice";
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import type { NoticeDetail, NoticeItem, NoticeQueryParams } from "@/api/system/notice";
import NoticeAPI from "@/api/system/notice";
import { useSse } from "@/composables";
import router from "@/router";
const PAGE_SIZE = 5;
const NOTICE_EVENT = "notice";
const NOTICE_REVOKE_EVENT = "notice-revoke";
type NoticeStatus = 0 | 1;
interface NoticeMessage {
id: string;
title: string;
type: number;
publishTime?: Date;
}
interface NoticeRevokeMessage {
id: string;
}
export function useNotice() {
const { on } = useSse();
// 状态
const list = ref<NoticeItem[]>([]);
const unreadTotal = ref(0);
const activeStatus = ref<NoticeStatus>(0);
@@ -23,11 +31,7 @@ export function useNotice() {
const dialogVisible = ref(false);
const emptyText = computed(() => (activeStatus.value === 0 ? "暂无未读消息" : "暂无已读消息"));
let unsubscribe: (() => void) | null = null;
// ============================================
// 数据获取
// ============================================
let stopSubscriptions: (() => void) | null = null;
async function fetchList(params?: Partial<NoticeQueryParams>) {
const query: NoticeQueryParams = {
@@ -98,14 +102,10 @@ export function useNotice() {
router.push({ name: "MyNotice" });
}
// ============================================
// SSE 订阅
// ============================================
function setupSubscription() {
if (unsubscribe) return;
if (stopSubscriptions) return;
unsubscribe = on(NOTICE_EVENT, (data: any) => {
const stopNotice = on<NoticeMessage>(NOTICE_EVENT, (data) => {
try {
if (!data.id) return;
@@ -116,10 +116,13 @@ export function useNotice() {
list.value.unshift({
id: data.id,
title: data.title,
content: "",
type: data.type,
level: "",
publishStatus: 1,
publishTime: data.publishTime,
isRead: 0,
} as NoticeItem);
});
if (list.value.length > PAGE_SIZE) {
list.value.length = PAGE_SIZE;
@@ -136,25 +139,26 @@ export function useNotice() {
}
});
on("notice-revoke", (data: any) => {
const stopRevoke = on<NoticeRevokeMessage>(NOTICE_REVOKE_EVENT, (data) => {
try {
if (!data.id) return;
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);
const index = list.value.findIndex((item: NoticeItem) => item.id === data.id);
if (index >= 0) {
const wasUnread = list.value[index].isRead !== 1;
list.value.splice(index, 1);
if (wasUnread && unreadTotal.value > 0) unreadTotal.value -= 1;
}
} catch (e) {
console.error("处理撤回通知失败", e);
}
});
}
// ============================================
// 生命周期
// ============================================
stopSubscriptions = () => {
stopNotice();
stopRevoke();
};
}
onMounted(() => {
refresh();
@@ -162,9 +166,9 @@ export function useNotice() {
});
onBeforeUnmount(() => {
if (unsubscribe) {
unsubscribe();
unsubscribe = null;
if (stopSubscriptions) {
stopSubscriptions();
stopSubscriptions = null;
}
});