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

@@ -5,7 +5,5 @@ export type { DictMessage, DictChangeMessage, DictChangeCallback } from "./sse";
// 表格相关
export { useTableSelection } from "./useTableSelection";
// 最近访问菜单
export { useRecentMenus } from "./useRecentMenus";
export type { RecentMenuItem } from "./useRecentMenus";
export { usePageTable } from "./usePageTable";
export type { UsePageTableOptions, UsePageTableReturn } from "./usePageTable";

View File

@@ -0,0 +1,114 @@
import { reactive, ref, type Ref } from "vue";
import type { BaseQueryParams, PageResult } from "@/api/common";
/**
* 分页列表页通用状态管理
*
* 只管理请求、分页数据和查询参数,不处理勾选、弹窗、表单
*/
export function usePageTable<T, Q extends BaseQueryParams = BaseQueryParams>(
options: UsePageTableOptions<T, Q>
): UsePageTableReturn<T, Q> {
const { initialParams, request, onBeforeReset } = options;
const loading = ref(false);
const list = ref<T[]>([]) as Ref<T[]>;
const total = ref(0);
const params = reactive({ ...initialParams }) as Q;
/**
* 拉取当前查询参数对应的分页数据
*
* 只负责请求和回填,不处理弹窗、路由或消息提示
*/
async function fetchData(): Promise<void> {
loading.value = true;
try {
const data = await request(params);
list.value = data.list ?? [];
total.value = data.total ?? 0;
} finally {
loading.value = false;
}
}
/**
* 回到第一页并查询
*/
function handleQuery(): void {
params.pageNum = 1;
fetchData();
}
/**
* 恢复初始查询参数
*
* 保持响应式引用不变,不触发查询
*/
function resetParams(): void {
Object.assign(params, initialParams);
}
/**
* 恢复初始查询参数并重新查询
*/
function handleResetQuery(): void {
onBeforeReset?.();
resetParams();
fetchData();
}
return {
loading,
list,
total,
params,
fetchData,
handleQuery,
handleResetQuery,
resetParams,
};
}
export interface UsePageTableOptions<T, Q extends BaseQueryParams> {
/** 初始查询参数,同时作为重置基准 */
initialParams: Q;
/**
* 分页请求函数
*/
request: (params: Q) => Promise<PageResult<T>>;
/**
* 重置查询前的回调
*
* 常用于同步重置搜索表单字段
*/
onBeforeReset?: () => void;
}
export interface UsePageTableReturn<T, Q extends BaseQueryParams> {
/** 加载状态 */
loading: Ref<boolean>;
/** 当前页数据列表 */
list: Ref<T[]>;
/** 数据总条数 */
total: Ref<number>;
/** 当前查询参数 */
params: Q;
/**
* 拉取分页数据
*/
fetchData: () => Promise<void>;
/**
* 回到第一页并查询
*/
handleQuery: () => void;
/**
* 恢复初始参数并重新查询
*/
handleResetQuery: () => void;
/**
* 恢复初始参数但不触发查询
*/
resetParams: () => void;
}

View File

@@ -1,99 +0,0 @@
import { ref } from "vue";
/**
* 最近访问菜单项
*/
export interface RecentMenuItem {
path: string;
title: string;
icon?: string;
visitedAt: number;
}
const STORAGE_KEY = "recent_menus";
const MAX_COUNT = 8;
// 全局状态
const recentMenus = ref<RecentMenuItem[]>([]);
/**
* 从 localStorage 加载数据
*/
function loadFromStorage(): RecentMenuItem[] {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : [];
} catch {
return [];
}
}
/**
* 保存到 localStorage
*/
function saveToStorage(menus: RecentMenuItem[]) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(menus));
}
// 初始化
recentMenus.value = loadFromStorage();
/**
* 最近访问菜单 composable
*/
export function useRecentMenus() {
/**
* 清空所有记录
*/
function clearRecentMenus() {
recentMenus.value = [];
localStorage.removeItem(STORAGE_KEY);
}
/**
* 格式化访问时间
*/
function formatVisitTime(timestamp: number): string {
const now = Date.now();
const diff = now - timestamp;
if (diff < 60000) return "刚刚";
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`;
if (diff < 604800000) return `${Math.floor(diff / 86400000)}天前`;
const date = new Date(timestamp);
return `${date.getMonth() + 1}-${date.getDate()}`;
}
return {
recentMenus,
clearRecentMenus,
formatVisitTime,
};
}
/**
* 添加最近访问记录(全局方法,供路由守卫调用)
*/
export function addRecentMenu(path: string, title: string, icon?: string) {
if (!path || !title) return;
// 过滤掉不需要记录的路径
const excludePaths = ["/dashboard", "/redirect", "/404", "/401", "/login", "/"];
if (excludePaths.some((p) => path === p || path.startsWith(p + "/"))) return;
// 移除已存在的相同路径
const filtered = recentMenus.value.filter((item) => item.path !== path);
// 添加到开头
const newItem: RecentMenuItem = {
path,
title,
icon,
visitedAt: Date.now(),
};
recentMenus.value = [newItem, ...filtered].slice(0, MAX_COUNT);
saveToStorage(recentMenus.value);
}

View File

@@ -1,56 +1,55 @@
import { computed, ref } from "vue";
/**
* 表格行选择 Composable
* 表格行选择 Composable。
*
* @description 提供统一的表格行选择逻辑,包括选中ID管理和清空选择
* @template T 数据项类型,必须包含 id 属性
* @returns 返回选中的ID列表、选择变化处理函数、清空选择函数
* 提供统一的表格行选择逻辑,包括选中 ID 管理和清空选择。
*
* 规则:
*
* - 不和分页状态合并。
* - 不和弹窗状态合并。
* - 不和 API 请求合并。
*
* @template T 数据项类型,必须包含可选 `id` 属性
*
* @example
* ```typescript
* const { selectedIds, handleSelectionChange, clearSelection } = useTableSelection<UserVO>();
* ```ts
* const { selectedIds, hasSelection, handleSelectionChange } = useTableSelection<RoleItem>();
* ```
*/
export function useTableSelection<T extends { id: string | number }>() {
/**
* 选中的数据项ID列表
*/
export function useTableSelection<T extends { id?: string | number }>() {
/** 选中的数据项 ID 列表。 */
const selectedIds = ref<(string | number)[]>([]);
/** 选中数量。 */
const selectedCount = computed(() => selectedIds.value.length);
/** 是否有选中项。 */
const hasSelection = computed(() => selectedIds.value.length > 0);
/**
* 表格选中项变化处理
* 表格选中项变化处理。
*
* @param selection 选中的行数据列表
*/
function handleSelectionChange(selection: T[]): void {
selectedIds.value = selection.map((item) => item.id);
selectedIds.value = selection.flatMap((item) => (item.id ? [item.id] : []));
}
/**
* 清空选择
*/
function clearSelection(): void {
selectedIds.value = [];
}
/**
* 检查指定ID是否被选中
* @param id 要检查的ID
* @returns 是否被选中
* 检查指定 ID 是否被选中。
*
* @param id 要检查的 ID
*/
function isSelected(id: string | number): boolean {
return selectedIds.value.includes(id);
}
/**
* 获取选中的数量
*/
const selectedCount = computed(() => selectedIds.value.length);
/**
* 是否有选中项
*/
const hasSelection = computed(() => selectedIds.value.length > 0);
/** 清空选中 ID。注意:不会清空表格 UI 上的勾选状态,需页面自行调用表格实例的 `clearSelection`。 */
function clearSelection(): void {
selectedIds.value = [];
}
return {
selectedIds,