refactor: 个人中心页面重构和布局代码优化
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user