+
新增
@@ -142,8 +142,8 @@
@@ -203,8 +203,8 @@
@@ -212,11 +212,12 @@
+
+
diff --git a/src/views/system/user/components/DeptTree.vue b/src/views/system/user/components/UserDeptTree.vue
similarity index 95%
rename from src/views/system/user/components/DeptTree.vue
rename to src/views/system/user/components/UserDeptTree.vue
index 19e41dbf..052ffe20 100644
--- a/src/views/system/user/components/DeptTree.vue
+++ b/src/views/system/user/components/UserDeptTree.vue
@@ -1,5 +1,4 @@
-
-
+
@@ -21,7 +20,7 @@
+
+
diff --git a/tests/setup.ts b/tests/setup.ts
new file mode 100644
index 00000000..2f1d5596
--- /dev/null
+++ b/tests/setup.ts
@@ -0,0 +1,52 @@
+/**
+ * 测试环境全局配置
+ */
+
+import { vi } from "vitest";
+
+// Mock window.matchMedia
+Object.defineProperty(window, "matchMedia", {
+ writable: true,
+ value: vi.fn().mockImplementation((query) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+});
+
+// Mock IntersectionObserver
+global.IntersectionObserver = class IntersectionObserver {
+ constructor() {}
+ disconnect() {}
+ observe() {}
+ takeRecords() {
+ return [];
+ }
+ unobserve() {}
+} as any;
+
+// Mock ResizeObserver
+global.ResizeObserver = class ResizeObserver {
+ constructor() {}
+ disconnect() {}
+ observe() {}
+ unobserve() {}
+} as any;
+
+// Mock Element.scrollIntoView
+Element.prototype.scrollIntoView = vi.fn();
+
+// Mock console methods to reduce noise in tests
+global.console = {
+ ...console,
+ log: vi.fn(),
+ debug: vi.fn(),
+ info: vi.fn(),
+ warn: vi.fn(),
+ error: vi.fn(),
+};
diff --git a/tests/unit/components/Pagination.test.ts b/tests/unit/components/Pagination.test.ts
new file mode 100644
index 00000000..440ff485
--- /dev/null
+++ b/tests/unit/components/Pagination.test.ts
@@ -0,0 +1,114 @@
+import { describe, it, expect, beforeEach } from "vitest";
+import { mount } from "@vue/test-utils";
+import Pagination from "@/components/Pagination/index.vue";
+import ElementPlus from "element-plus";
+
+describe("Pagination 组件", () => {
+ const createWrapper = (props = {}) => {
+ return mount(Pagination, {
+ props: {
+ page: 1,
+ limit: 10,
+ total: 100,
+ ...props,
+ },
+ global: {
+ plugins: [ElementPlus],
+ },
+ });
+ };
+
+ describe("渲染", () => {
+ it("应该正确渲染分页组件", () => {
+ const wrapper = createWrapper();
+ expect(wrapper.exists()).toBe(true);
+ expect(wrapper.find(".pagination").exists()).toBe(true);
+ });
+
+ it("应该在 hidden 为 true 时隐藏分页", () => {
+ const wrapper = createWrapper({ hidden: true });
+ expect(wrapper.find(".pagination").classes()).toContain("hidden");
+ });
+
+ it("应该在 hidden 为 false 时显示分页", () => {
+ const wrapper = createWrapper({ hidden: false });
+ expect(wrapper.find(".pagination").classes()).not.toContain("hidden");
+ });
+ });
+
+ describe("Props", () => {
+ it("应该接收 total 属性", () => {
+ const wrapper = createWrapper({ total: 200 });
+ expect(wrapper.props("total")).toBe(200);
+ });
+
+ it("应该接收 pageSizes 属性", () => {
+ const pageSizes = [5, 10, 20, 50];
+ const wrapper = createWrapper({ pageSizes });
+ expect(wrapper.props("pageSizes")).toEqual(pageSizes);
+ });
+
+ it("应该使用默认的 pageSizes", () => {
+ const wrapper = createWrapper();
+ expect(wrapper.props("pageSizes")).toEqual([10, 20, 30, 50]);
+ });
+
+ it("应该接收 layout 属性", () => {
+ const layout = "prev, pager, next";
+ const wrapper = createWrapper({ layout });
+ expect(wrapper.props("layout")).toBe(layout);
+ });
+
+ it("应该接收 background 属性", () => {
+ const wrapper = createWrapper({ background: false });
+ expect(wrapper.props("background")).toBe(false);
+ });
+ });
+
+ describe("事件", () => {
+ it("应该在页码改变时触发 pagination 事件", async () => {
+ const wrapper = createWrapper();
+
+ // 模拟页码改变
+ await wrapper.vm.handleCurrentChange(2);
+
+ expect(wrapper.emitted("pagination")).toBeTruthy();
+ expect(wrapper.emitted("pagination")?.[0]).toEqual([{ page: 2, limit: 10 }]);
+ });
+
+ it("应该在每页条数改变时触发 pagination 事件", async () => {
+ const wrapper = createWrapper();
+
+ // 模拟每页条数改变
+ await wrapper.vm.handleSizeChange(20);
+
+ expect(wrapper.emitted("pagination")).toBeTruthy();
+ expect(wrapper.emitted("pagination")?.[0]).toEqual([{ page: 1, limit: 20 }]);
+ });
+
+ it("应该在每页条数改变时重置页码为 1", async () => {
+ const wrapper = createWrapper({ page: 3 });
+
+ await wrapper.vm.handleSizeChange(20);
+
+ expect(wrapper.emitted("pagination")?.[0]).toEqual([{ page: 1, limit: 20 }]);
+ });
+ });
+
+ describe("边界情况", () => {
+ it("应该在 total 为 0 时正常工作", () => {
+ const wrapper = createWrapper({ total: 0 });
+ expect(wrapper.exists()).toBe(true);
+ });
+
+ it("应该在当前页超出范围时自动调整", async () => {
+ const wrapper = createWrapper({ page: 5, limit: 10, total: 100 });
+
+ // 修改 total 使当前页超出范围
+ await wrapper.setProps({ total: 20 });
+
+ // 应该触发 pagination 事件,页码调整为最后一页
+ expect(wrapper.emitted("pagination")).toBeTruthy();
+ });
+ });
+});
diff --git a/tests/unit/composables/useDictSync.test.ts b/tests/unit/composables/useDictSync.test.ts
new file mode 100644
index 00000000..cd4919ca
--- /dev/null
+++ b/tests/unit/composables/useDictSync.test.ts
@@ -0,0 +1,62 @@
+import { describe, it, expect, beforeEach, vi } from "vitest";
+import { useDictSync } from "@/composables/websocket/useDictSync";
+import { useDictStore } from "@/store/modules/dict";
+import { setActivePinia, createPinia } from "pinia";
+
+// Mock useStomp
+vi.mock("@/composables/websocket/useStomp", () => ({
+ useStomp: vi.fn(() => ({
+ isConnected: ref(false),
+ connect: vi.fn(),
+ disconnect: vi.fn(),
+ subscribe: vi.fn(() => "sub-id"),
+ unsubscribe: vi.fn(),
+ })),
+}));
+
+describe("useDictSync", () => {
+ beforeEach(() => {
+ setActivePinia(createPinia());
+ localStorage.clear();
+ vi.clearAllMocks();
+ });
+
+ describe("初始化", () => {
+ it("应该创建字典同步实例", () => {
+ const dictSync = useDictSync();
+ expect(dictSync).toBeDefined();
+ expect(dictSync.initialize).toBeDefined();
+ expect(dictSync.cleanup).toBeDefined();
+ });
+
+ it("应该初始化 WebSocket 连接", () => {
+ const dictSync = useDictSync();
+ dictSync.initialize();
+ // 验证初始化逻辑(具体实现取决于 useDictSync 的内部逻辑)
+ expect(dictSync).toBeDefined();
+ });
+ });
+
+ describe("清理", () => {
+ it("应该清理 WebSocket 连接", () => {
+ const dictSync = useDictSync();
+ dictSync.initialize();
+ dictSync.cleanup();
+ // 验证清理逻辑
+ expect(dictSync).toBeDefined();
+ });
+ });
+
+ describe("字典同步", () => {
+ it("应该处理字典更新消息", () => {
+ const dictSync = useDictSync();
+ const dictStore = useDictStore();
+
+ dictSync.initialize();
+
+ // 模拟接收字典更新消息
+ // 注意:这里需要根据实际的消息格式来测试
+ expect(dictStore).toBeDefined();
+ });
+ });
+});
diff --git a/tests/unit/composables/useTableSelection.test.ts b/tests/unit/composables/useTableSelection.test.ts
new file mode 100644
index 00000000..d0cef0f0
--- /dev/null
+++ b/tests/unit/composables/useTableSelection.test.ts
@@ -0,0 +1,88 @@
+import { describe, it, expect, beforeEach } from "vitest";
+import { useTableSelection } from "@/composables/table/useTableSelection";
+
+describe("useTableSelection", () => {
+ let selection: ReturnType;
+
+ beforeEach(() => {
+ selection = useTableSelection();
+ });
+
+ it("应该初始化为空数组", () => {
+ expect(selection.selectedIds.value).toEqual([]);
+ expect(selection.selectedRows.value).toEqual([]);
+ });
+
+ describe("handleSelectionChange()", () => {
+ it("应该更新选中的行", () => {
+ const rows = [
+ { id: 1, name: "张三" },
+ { id: 2, name: "李四" },
+ ];
+
+ selection.handleSelectionChange(rows);
+
+ expect(selection.selectedRows.value).toEqual(rows);
+ expect(selection.selectedIds.value).toEqual([1, 2]);
+ });
+
+ it("应该处理空数组", () => {
+ selection.handleSelectionChange([]);
+
+ expect(selection.selectedRows.value).toEqual([]);
+ expect(selection.selectedIds.value).toEqual([]);
+ });
+
+ it("应该支持自定义 ID 字段", () => {
+ const customSelection = useTableSelection("userId");
+ const rows = [
+ { userId: "u1", name: "张三" },
+ { userId: "u2", name: "李四" },
+ ];
+
+ customSelection.handleSelectionChange(rows);
+
+ expect(customSelection.selectedIds.value).toEqual(["u1", "u2"]);
+ });
+ });
+
+ describe("clearSelection()", () => {
+ it("应该清空选中项", () => {
+ const rows = [{ id: 1, name: "张三" }];
+ selection.handleSelectionChange(rows);
+
+ selection.clearSelection();
+
+ expect(selection.selectedIds.value).toEqual([]);
+ expect(selection.selectedRows.value).toEqual([]);
+ });
+ });
+
+ describe("hasSelection", () => {
+ it("有选中项时应该返回 true", () => {
+ const rows = [{ id: 1, name: "张三" }];
+ selection.handleSelectionChange(rows);
+
+ expect(selection.hasSelection.value).toBe(true);
+ });
+
+ it("无选中项时应该返回 false", () => {
+ expect(selection.hasSelection.value).toBe(false);
+ });
+ });
+
+ describe("selectionCount", () => {
+ it("应该返回选中项数量", () => {
+ expect(selection.selectionCount.value).toBe(0);
+
+ const rows = [
+ { id: 1, name: "张三" },
+ { id: 2, name: "李四" },
+ { id: 3, name: "王五" },
+ ];
+ selection.handleSelectionChange(rows);
+
+ expect(selection.selectionCount.value).toBe(3);
+ });
+ });
+});
diff --git a/tests/unit/store/app.test.ts b/tests/unit/store/app.test.ts
new file mode 100644
index 00000000..3edd7d04
--- /dev/null
+++ b/tests/unit/store/app.test.ts
@@ -0,0 +1,141 @@
+import { describe, it, expect, beforeEach } from "vitest";
+import { setActivePinia, createPinia } from "pinia";
+import { useAppStore } from "@/store/modules/app";
+import { DeviceEnum, SidebarStatus } from "@/enums";
+
+describe("useAppStore", () => {
+ beforeEach(() => {
+ setActivePinia(createPinia());
+ localStorage.clear();
+ });
+
+ describe("侧边栏状态", () => {
+ it("应该切换侧边栏状态", () => {
+ const store = useAppStore();
+ const initialState = store.sidebar.opened;
+
+ store.toggleSidebar();
+ expect(store.sidebar.opened).toBe(!initialState);
+
+ store.toggleSidebar();
+ expect(store.sidebar.opened).toBe(initialState);
+ });
+
+ it("应该关闭侧边栏", () => {
+ const store = useAppStore();
+ store.openSideBar();
+ expect(store.sidebar.opened).toBe(true);
+
+ store.closeSideBar();
+ expect(store.sidebar.opened).toBe(false);
+ });
+
+ it("应该打开侧边栏", () => {
+ const store = useAppStore();
+ store.closeSideBar();
+ expect(store.sidebar.opened).toBe(false);
+
+ store.openSideBar();
+ expect(store.sidebar.opened).toBe(true);
+ });
+
+ it("应该持久化侧边栏状态", () => {
+ const store = useAppStore();
+ store.openSideBar();
+
+ // 创建新的 store 实例模拟页面刷新
+ const newStore = useAppStore();
+ expect(newStore.sidebar.opened).toBe(true);
+ });
+ });
+
+ describe("设备类型", () => {
+ it("应该切换设备类型", () => {
+ const store = useAppStore();
+
+ store.toggleDevice(DeviceEnum.MOBILE);
+ expect(store.device).toBe(DeviceEnum.MOBILE);
+
+ store.toggleDevice(DeviceEnum.DESKTOP);
+ expect(store.device).toBe(DeviceEnum.DESKTOP);
+ });
+
+ it("应该持久化设备类型", () => {
+ const store = useAppStore();
+ store.toggleDevice(DeviceEnum.MOBILE);
+
+ const newStore = useAppStore();
+ expect(newStore.device).toBe(DeviceEnum.MOBILE);
+ });
+ });
+
+ describe("组件尺寸", () => {
+ it("应该修改组件尺寸", () => {
+ const store = useAppStore();
+
+ store.changeSize("large");
+ expect(store.size).toBe("large");
+
+ store.changeSize("small");
+ expect(store.size).toBe("small");
+ });
+
+ it("应该持久化组件尺寸", () => {
+ const store = useAppStore();
+ store.changeSize("large");
+
+ const newStore = useAppStore();
+ expect(newStore.size).toBe("large");
+ });
+ });
+
+ describe("语言设置", () => {
+ it("应该修改语言", () => {
+ const store = useAppStore();
+
+ store.changeLanguage("en");
+ expect(store.language).toBe("en");
+
+ store.changeLanguage("zh-cn");
+ expect(store.language).toBe("zh-cn");
+ });
+
+ it("应该根据语言返回正确的 locale", () => {
+ const store = useAppStore();
+
+ store.changeLanguage("en");
+ expect(store.locale).toBeDefined();
+
+ store.changeLanguage("zh-cn");
+ expect(store.locale).toBeDefined();
+ });
+
+ it("应该持久化语言设置", () => {
+ const store = useAppStore();
+ store.changeLanguage("en");
+
+ const newStore = useAppStore();
+ expect(newStore.language).toBe("en");
+ });
+ });
+
+ describe("顶部菜单", () => {
+ it("应该激活顶部菜单", () => {
+ const store = useAppStore();
+
+ store.activeTopMenu("/dashboard");
+ expect(store.activeTopMenuPath).toBe("/dashboard");
+
+ store.activeTopMenu("/system");
+ expect(store.activeTopMenuPath).toBe("/system");
+ });
+
+ it("应该持久化顶部菜单路径", () => {
+ const store = useAppStore();
+ store.activeTopMenu("/dashboard");
+
+ const newStore = useAppStore();
+ expect(newStore.activeTopMenuPath).toBe("/dashboard");
+ });
+ });
+});
diff --git a/tests/unit/store/dict.test.ts b/tests/unit/store/dict.test.ts
new file mode 100644
index 00000000..1e46610f
--- /dev/null
+++ b/tests/unit/store/dict.test.ts
@@ -0,0 +1,134 @@
+import { describe, it, expect, beforeEach, vi } from "vitest";
+import { setActivePinia, createPinia } from "pinia";
+import { useDictStore } from "@/store/modules/dict";
+import DictAPI from "@/api/system/dict";
+import type { DictItemOption } from "@/types/api";
+
+// Mock DictAPI
+vi.mock("@/api/system/dict", () => ({
+ default: {
+ getDictItems: vi.fn(),
+ },
+}));
+
+describe("useDictStore", () => {
+ beforeEach(() => {
+ // 创建新的 Pinia 实例
+ setActivePinia(createPinia());
+ // 清理 localStorage
+ localStorage.clear();
+ // 重置所有 mock
+ vi.clearAllMocks();
+ });
+
+ describe("字典缓存", () => {
+ it("应该缓存字典数据", async () => {
+ const store = useDictStore();
+ const mockData: DictItemOption[] = [
+ { value: "1", label: "选项1" },
+ { value: "2", label: "选项2" },
+ ];
+
+ vi.mocked(DictAPI.getDictItems).mockResolvedValue(mockData);
+
+ await store.loadDictItems("test_dict");
+
+ expect(store.getDictItems("test_dict")).toEqual(mockData);
+ expect(DictAPI.getDictItems).toHaveBeenCalledWith("test_dict");
+ expect(DictAPI.getDictItems).toHaveBeenCalledTimes(1);
+ });
+
+ it("应该从缓存中获取字典数据,不重复请求", async () => {
+ const store = useDictStore();
+ const mockData: DictItemOption[] = [{ value: "1", label: "选项1" }];
+
+ vi.mocked(DictAPI.getDictItems).mockResolvedValue(mockData);
+
+ // 第一次加载
+ await store.loadDictItems("test_dict");
+ // 第二次加载(应该从缓存获取)
+ await store.loadDictItems("test_dict");
+
+ expect(DictAPI.getDictItems).toHaveBeenCalledTimes(1);
+ expect(store.getDictItems("test_dict")).toEqual(mockData);
+ });
+
+ it("应该防止并发重复请求", async () => {
+ const store = useDictStore();
+ const mockData: DictItemOption[] = [{ value: "1", label: "选项1" }];
+
+ vi.mocked(DictAPI.getDictItems).mockResolvedValue(mockData);
+
+ // 同时发起多个请求
+ const promises = [
+ store.loadDictItems("test_dict"),
+ store.loadDictItems("test_dict"),
+ store.loadDictItems("test_dict"),
+ ];
+
+ await Promise.all(promises);
+
+ // 应该只请求一次
+ expect(DictAPI.getDictItems).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("字典操作", () => {
+ it("应该返回空数组当字典不存在时", () => {
+ const store = useDictStore();
+ expect(store.getDictItems("non_exist")).toEqual([]);
+ });
+
+ it("应该移除指定字典项", async () => {
+ const store = useDictStore();
+ const mockData: DictItemOption[] = [{ value: "1", label: "选项1" }];
+
+ vi.mocked(DictAPI.getDictItems).mockResolvedValue(mockData);
+
+ await store.loadDictItems("test_dict");
+ expect(store.getDictItems("test_dict")).toEqual(mockData);
+
+ store.removeDictItem("test_dict");
+ expect(store.getDictItems("test_dict")).toEqual([]);
+ });
+
+ it("应该清空所有字典缓存", async () => {
+ const store = useDictStore();
+ const mockData1: DictItemOption[] = [{ value: "1", label: "选项1" }];
+ const mockData2: DictItemOption[] = [{ value: "2", label: "选项2" }];
+
+ vi.mocked(DictAPI.getDictItems)
+ .mockResolvedValueOnce(mockData1)
+ .mockResolvedValueOnce(mockData2);
+
+ await store.loadDictItems("dict1");
+ await store.loadDictItems("dict2");
+
+ expect(store.getDictItems("dict1")).toEqual(mockData1);
+ expect(store.getDictItems("dict2")).toEqual(mockData2);
+
+ store.clearDictCache();
+
+ expect(store.getDictItems("dict1")).toEqual([]);
+ expect(store.getDictItems("dict2")).toEqual([]);
+ });
+ });
+
+ describe("错误处理", () => {
+ it("应该处理请求失败的情况", async () => {
+ const store = useDictStore();
+ const error = new Error("Network error");
+
+ vi.mocked(DictAPI.getDictItems).mockRejectedValue(error);
+
+ await expect(store.loadDictItems("test_dict")).rejects.toThrow("Network error");
+
+ // 失败后应该允许重试
+ const mockData: DictItemOption[] = [{ value: "1", label: "选项1" }];
+ vi.mocked(DictAPI.getDictItems).mockResolvedValue(mockData);
+
+ await store.loadDictItems("test_dict");
+ expect(store.getDictItems("test_dict")).toEqual(mockData);
+ });
+ });
+});
diff --git a/tests/unit/store/settings.test.ts b/tests/unit/store/settings.test.ts
new file mode 100644
index 00000000..269318c1
--- /dev/null
+++ b/tests/unit/store/settings.test.ts
@@ -0,0 +1,186 @@
+import { describe, it, expect, beforeEach, vi } from "vitest";
+import { setActivePinia, createPinia } from "pinia";
+import { useSettingsStore } from "@/store/modules/settings";
+import { ThemeMode, SidebarColor } from "@/enums";
+
+// Mock theme utils
+vi.mock("@/utils/theme", () => ({
+ applyTheme: vi.fn(),
+ generateThemeColors: vi.fn(() => ({})),
+ toggleDarkMode: vi.fn(),
+ toggleSidebarColor: vi.fn(),
+}));
+
+describe("useSettingsStore", () => {
+ beforeEach(() => {
+ setActivePinia(createPinia());
+ localStorage.clear();
+ vi.clearAllMocks();
+ });
+
+ describe("界面显示设置", () => {
+ it("应该切换标签页显示", () => {
+ const store = useSettingsStore();
+ const initial = store.showTagsView;
+
+ store.showTagsView = !initial;
+ expect(store.showTagsView).toBe(!initial);
+ });
+
+ it("应该切换 Logo 显示", () => {
+ const store = useSettingsStore();
+ const initial = store.showAppLogo;
+
+ store.showAppLogo = !initial;
+ expect(store.showAppLogo).toBe(!initial);
+ });
+
+ it("应该切换水印显示", () => {
+ const store = useSettingsStore();
+ const initial = store.showWatermark;
+
+ store.showWatermark = !initial;
+ expect(store.showWatermark).toBe(!initial);
+ });
+
+ it("应该持久化界面显示设置", () => {
+ const store = useSettingsStore();
+ store.showTagsView = false;
+ store.showAppLogo = false;
+ store.showWatermark = true;
+
+ const newStore = useSettingsStore();
+ expect(newStore.showTagsView).toBe(false);
+ expect(newStore.showAppLogo).toBe(false);
+ expect(newStore.showWatermark).toBe(true);
+ });
+ });
+
+ describe("主题设置", () => {
+ it("应该切换主题模式", () => {
+ const store = useSettingsStore();
+
+ store.theme = ThemeMode.DARK;
+ expect(store.theme).toBe(ThemeMode.DARK);
+
+ store.theme = ThemeMode.LIGHT;
+ expect(store.theme).toBe(ThemeMode.LIGHT);
+ });
+
+ it("应该修改主题颜色", () => {
+ const store = useSettingsStore();
+
+ store.themeColor = "#409EFF";
+ expect(store.themeColor).toBe("#409EFF");
+
+ store.themeColor = "#67C23A";
+ expect(store.themeColor).toBe("#67C23A");
+ });
+
+ it("应该持久化主题设置", () => {
+ const store = useSettingsStore();
+ store.theme = ThemeMode.DARK;
+ store.themeColor = "#409EFF";
+
+ const newStore = useSettingsStore();
+ expect(newStore.theme).toBe(ThemeMode.DARK);
+ expect(newStore.themeColor).toBe("#409EFF");
+ });
+ });
+
+ describe("侧边栏配色", () => {
+ it("应该切换侧边栏配色方案", () => {
+ const store = useSettingsStore();
+
+ store.sidebarColorScheme = SidebarColor.CLASSIC_BLUE;
+ expect(store.sidebarColorScheme).toBe(SidebarColor.CLASSIC_BLUE);
+
+ store.sidebarColorScheme = SidebarColor.THEME_COLOR;
+ expect(store.sidebarColorScheme).toBe(SidebarColor.THEME_COLOR);
+ });
+
+ it("应该持久化侧边栏配色", () => {
+ const store = useSettingsStore();
+ store.sidebarColorScheme = SidebarColor.CLASSIC_BLUE;
+
+ const newStore = useSettingsStore();
+ expect(newStore.sidebarColorScheme).toBe(SidebarColor.CLASSIC_BLUE);
+ });
+ });
+
+ describe("特殊模式", () => {
+ it("应该切换灰色模式", () => {
+ const store = useSettingsStore();
+
+ store.grayMode = true;
+ expect(store.grayMode).toBe(true);
+
+ store.grayMode = false;
+ expect(store.grayMode).toBe(false);
+ });
+
+ it("应该切换色弱模式", () => {
+ const store = useSettingsStore();
+
+ store.colorWeak = true;
+ expect(store.colorWeak).toBe(true);
+
+ store.colorWeak = false;
+ expect(store.colorWeak).toBe(false);
+ });
+
+ it("应该持久化特殊模式", () => {
+ const store = useSettingsStore();
+ store.grayMode = true;
+ store.colorWeak = true;
+
+ const newStore = useSettingsStore();
+ expect(newStore.grayMode).toBe(true);
+ expect(newStore.colorWeak).toBe(true);
+ });
+ });
+
+ describe("AI 助手", () => {
+ it("应该切换 AI 助手", () => {
+ const store = useSettingsStore();
+
+ store.userEnableAi = true;
+ expect(store.userEnableAi).toBe(true);
+
+ store.userEnableAi = false;
+ expect(store.userEnableAi).toBe(false);
+ });
+
+ it("应该持久化 AI 助手设置", () => {
+ const store = useSettingsStore();
+ store.userEnableAi = true;
+
+ const newStore = useSettingsStore();
+ expect(newStore.userEnableAi).toBe(true);
+ });
+ });
+
+ describe("重置设置", () => {
+ it("应该重置所有设置为默认值", () => {
+ const store = useSettingsStore();
+
+ // 修改所有设置
+ store.showTagsView = false;
+ store.showAppLogo = false;
+ store.showWatermark = false;
+ store.userEnableAi = true;
+ store.grayMode = true;
+ store.colorWeak = true;
+ store.theme = ThemeMode.DARK;
+ store.themeColor = "#409EFF";
+
+ // 重置
+ store.resetSettings();
+
+ // 验证已重置(具体值取决于 defaults)
+ expect(store.userEnableAi).toBe(false);
+ expect(store.grayMode).toBe(false);
+ expect(store.colorWeak).toBe(false);
+ });
+ });
+});
diff --git a/tests/unit/utils/auth.test.ts b/tests/unit/utils/auth.test.ts
new file mode 100644
index 00000000..6ee9c5fb
--- /dev/null
+++ b/tests/unit/utils/auth.test.ts
@@ -0,0 +1,157 @@
+import { describe, it, expect, beforeEach, vi } from "vitest";
+import { AuthStorage, hasPerm } from "@/utils/auth";
+import { Storage } from "@/utils/storage";
+import { STORAGE_KEYS, ROLE_ROOT } from "@/constants";
+
+// Mock Storage
+vi.mock("@/utils/storage", () => ({
+ Storage: {
+ get: vi.fn(),
+ set: vi.fn(),
+ remove: vi.fn(),
+ sessionGet: vi.fn(),
+ sessionSet: vi.fn(),
+ sessionRemove: vi.fn(),
+ },
+}));
+
+// Mock useUserStoreHook
+vi.mock("@/store/modules/user", () => ({
+ useUserStoreHook: vi.fn(() => ({
+ userInfo: {
+ roles: ["admin"],
+ perms: ["sys:user:create", "sys:user:update"],
+ },
+ })),
+}));
+
+describe("Auth 工具函数", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("AuthStorage", () => {
+ describe("getAccessToken()", () => {
+ it("记住我为 true 时应该从 localStorage 获取", () => {
+ vi.mocked(Storage.get).mockReturnValueOnce(true); // rememberMe
+ vi.mocked(Storage.get).mockReturnValueOnce("token123"); // accessToken
+
+ const token = AuthStorage.getAccessToken();
+
+ expect(Storage.get).toHaveBeenCalledWith(STORAGE_KEYS.REMEMBER_ME, false);
+ expect(Storage.get).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN, "");
+ expect(token).toBe("token123");
+ });
+
+ it("记住我为 false 时应该从 sessionStorage 获取", () => {
+ vi.mocked(Storage.get).mockReturnValueOnce(false); // rememberMe
+ vi.mocked(Storage.sessionGet).mockReturnValueOnce("session-token");
+
+ const token = AuthStorage.getAccessToken();
+
+ expect(Storage.sessionGet).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN, "");
+ expect(token).toBe("session-token");
+ });
+ });
+
+ describe("getRefreshToken()", () => {
+ it("记住我为 true 时应该从 localStorage 获取", () => {
+ vi.mocked(Storage.get).mockReturnValueOnce(true);
+ vi.mocked(Storage.get).mockReturnValueOnce("refresh123");
+
+ const token = AuthStorage.getRefreshToken();
+
+ expect(token).toBe("refresh123");
+ });
+
+ it("记住我为 false 时应该从 sessionStorage 获取", () => {
+ vi.mocked(Storage.get).mockReturnValueOnce(false);
+ vi.mocked(Storage.sessionGet).mockReturnValueOnce("session-refresh");
+
+ const token = AuthStorage.getRefreshToken();
+
+ expect(token).toBe("session-refresh");
+ });
+ });
+
+ describe("setTokens()", () => {
+ it("记住我为 true 时应该存储到 localStorage", () => {
+ AuthStorage.setTokens("access123", "refresh123", true);
+
+ expect(Storage.set).toHaveBeenCalledWith(STORAGE_KEYS.REMEMBER_ME, true);
+ expect(Storage.set).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN, "access123");
+ expect(Storage.set).toHaveBeenCalledWith(STORAGE_KEYS.REFRESH_TOKEN, "refresh123");
+ });
+
+ it("记住我为 false 时应该存储到 sessionStorage", () => {
+ AuthStorage.setTokens("access123", "refresh123", false);
+
+ expect(Storage.set).toHaveBeenCalledWith(STORAGE_KEYS.REMEMBER_ME, false);
+ expect(Storage.sessionSet).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN, "access123");
+ expect(Storage.sessionSet).toHaveBeenCalledWith(STORAGE_KEYS.REFRESH_TOKEN, "refresh123");
+ expect(Storage.remove).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN);
+ expect(Storage.remove).toHaveBeenCalledWith(STORAGE_KEYS.REFRESH_TOKEN);
+ });
+ });
+
+ describe("clearAuth()", () => {
+ it("应该清理所有认证信息", () => {
+ AuthStorage.clearAuth();
+
+ expect(Storage.remove).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN);
+ expect(Storage.remove).toHaveBeenCalledWith(STORAGE_KEYS.REFRESH_TOKEN);
+ expect(Storage.sessionRemove).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN);
+ expect(Storage.sessionRemove).toHaveBeenCalledWith(STORAGE_KEYS.REFRESH_TOKEN);
+ });
+ });
+
+ describe("getRememberMe()", () => {
+ it("应该获取记住我状态", () => {
+ vi.mocked(Storage.get).mockReturnValueOnce(true);
+
+ const rememberMe = AuthStorage.getRememberMe();
+
+ expect(Storage.get).toHaveBeenCalledWith(STORAGE_KEYS.REMEMBER_ME, false);
+ expect(rememberMe).toBe(true);
+ });
+ });
+ });
+
+ describe("hasPerm()", () => {
+ it("超级管理员应该拥有所有按钮权限", () => {
+ const { useUserStoreHook } = await import("@/store/modules/user");
+ vi.mocked(useUserStoreHook).mockReturnValueOnce({
+ userInfo: {
+ roles: [ROLE_ROOT],
+ perms: [],
+ },
+ } as any);
+
+ expect(hasPerm("any:permission", "button")).toBe(true);
+ });
+
+ it("应该验证单个按钮权限", () => {
+ expect(hasPerm("sys:user:create", "button")).toBe(true);
+ expect(hasPerm("sys:user:delete", "button")).toBe(false);
+ });
+
+ it("应该验证多个按钮权限(或关系)", () => {
+ expect(hasPerm(["sys:user:create", "sys:user:delete"], "button")).toBe(true);
+ expect(hasPerm(["sys:user:delete", "sys:user:export"], "button")).toBe(false);
+ });
+
+ it("应该验证角色权限", () => {
+ expect(hasPerm("admin", "role")).toBe(true);
+ expect(hasPerm("user", "role")).toBe(false);
+ });
+
+ it("用户信息不完整时应该返回 false", () => {
+ const { useUserStoreHook } = await import("@/store/modules/user");
+ vi.mocked(useUserStoreHook).mockReturnValueOnce({
+ userInfo: {},
+ } as any);
+
+ expect(hasPerm("any:permission")).toBe(false);
+ });
+ });
+});
diff --git a/tests/unit/utils/format.test.ts b/tests/unit/utils/format.test.ts
new file mode 100644
index 00000000..f3c19a69
--- /dev/null
+++ b/tests/unit/utils/format.test.ts
@@ -0,0 +1,136 @@
+import { describe, it, expect } from "vitest";
+import { formatGrowthRate, formatFileSize, formatNumber, formatCurrency } from "@/utils/format";
+
+describe("Format 工具函数", () => {
+ describe("formatGrowthRate()", () => {
+ it("应该格式化正增长率", () => {
+ expect(formatGrowthRate(0.25)).toBe("+25.00%");
+ expect(formatGrowthRate(0.5)).toBe("+50.00%");
+ expect(formatGrowthRate(1.5)).toBe("+150.00%");
+ });
+
+ it("应该格式化负增长率", () => {
+ expect(formatGrowthRate(-0.25)).toBe("-25.00%");
+ expect(formatGrowthRate(-0.5)).toBe("-50.00%");
+ });
+
+ it("应该格式化零增长率", () => {
+ expect(formatGrowthRate(0)).toBe("0.00%");
+ });
+
+ it("应该处理小数精度", () => {
+ expect(formatGrowthRate(0.12345)).toBe("+12.35%");
+ expect(formatGrowthRate(0.12344)).toBe("+12.34%");
+ });
+
+ it("应该处理 null 和 undefined", () => {
+ expect(formatGrowthRate(null as any)).toBe("0.00%");
+ expect(formatGrowthRate(undefined as any)).toBe("0.00%");
+ });
+ });
+
+ describe("formatFileSize()", () => {
+ it("应该格式化字节", () => {
+ expect(formatFileSize(0)).toBe("0 B");
+ expect(formatFileSize(100)).toBe("100 B");
+ expect(formatFileSize(1023)).toBe("1023 B");
+ });
+
+ it("应该格式化 KB", () => {
+ expect(formatFileSize(1024)).toBe("1.00 KB");
+ expect(formatFileSize(1536)).toBe("1.50 KB");
+ expect(formatFileSize(10240)).toBe("10.00 KB");
+ });
+
+ it("应该格式化 MB", () => {
+ expect(formatFileSize(1048576)).toBe("1.00 MB");
+ expect(formatFileSize(5242880)).toBe("5.00 MB");
+ });
+
+ it("应该格式化 GB", () => {
+ expect(formatFileSize(1073741824)).toBe("1.00 GB");
+ expect(formatFileSize(5368709120)).toBe("5.00 GB");
+ });
+
+ it("应该格式化 TB", () => {
+ expect(formatFileSize(1099511627776)).toBe("1.00 TB");
+ });
+
+ it("应该处理负数", () => {
+ expect(formatFileSize(-1024)).toBe("0 B");
+ });
+
+ it("应该处理 null 和 undefined", () => {
+ expect(formatFileSize(null as any)).toBe("0 B");
+ expect(formatFileSize(undefined as any)).toBe("0 B");
+ });
+ });
+
+ describe("formatNumber()", () => {
+ it("应该格式化整数", () => {
+ expect(formatNumber(1000)).toBe("1,000");
+ expect(formatNumber(1000000)).toBe("1,000,000");
+ expect(formatNumber(123456789)).toBe("123,456,789");
+ });
+
+ it("应该格式化小数", () => {
+ expect(formatNumber(1000.5)).toBe("1,000.5");
+ expect(formatNumber(1234.56)).toBe("1,234.56");
+ });
+
+ it("应该处理小数位数", () => {
+ expect(formatNumber(1234.5678, 2)).toBe("1,234.57");
+ expect(formatNumber(1234.5, 2)).toBe("1,234.50");
+ expect(formatNumber(1234, 2)).toBe("1,234.00");
+ });
+
+ it("应该处理零", () => {
+ expect(formatNumber(0)).toBe("0");
+ expect(formatNumber(0, 2)).toBe("0.00");
+ });
+
+ it("应该处理负数", () => {
+ expect(formatNumber(-1000)).toBe("-1,000");
+ expect(formatNumber(-1234.56, 2)).toBe("-1,234.56");
+ });
+
+ it("应该处理 null 和 undefined", () => {
+ expect(formatNumber(null as any)).toBe("0");
+ expect(formatNumber(undefined as any)).toBe("0");
+ });
+ });
+
+ describe("formatCurrency()", () => {
+ it("应该格式化货币(默认人民币)", () => {
+ expect(formatCurrency(1000)).toBe("¥1,000.00");
+ expect(formatCurrency(1234.56)).toBe("¥1,234.56");
+ expect(formatCurrency(1000000)).toBe("¥1,000,000.00");
+ });
+
+ it("应该格式化美元", () => {
+ expect(formatCurrency(1000, "USD")).toBe("$1,000.00");
+ expect(formatCurrency(1234.56, "USD")).toBe("$1,234.56");
+ });
+
+ it("应该格式化欧元", () => {
+ expect(formatCurrency(1000, "EUR")).toBe("€1,000.00");
+ });
+
+ it("应该格式化日元", () => {
+ expect(formatCurrency(1000, "JPY")).toBe("¥1,000");
+ });
+
+ it("应该处理零", () => {
+ expect(formatCurrency(0)).toBe("¥0.00");
+ });
+
+ it("应该处理负数", () => {
+ expect(formatCurrency(-1000)).toBe("-¥1,000.00");
+ });
+
+ it("应该处理 null 和 undefined", () => {
+ expect(formatCurrency(null as any)).toBe("¥0.00");
+ expect(formatCurrency(undefined as any)).toBe("¥0.00");
+ });
+ });
+});
diff --git a/tests/unit/utils/storage.test.ts b/tests/unit/utils/storage.test.ts
new file mode 100644
index 00000000..040cfc88
--- /dev/null
+++ b/tests/unit/utils/storage.test.ts
@@ -0,0 +1,150 @@
+import { describe, it, expect, beforeEach, afterEach } from "vitest";
+import { Storage } from "@/utils/storage";
+import { STORAGE_KEYS, APP_PREFIX } from "@/constants";
+
+describe("Storage 工具类", () => {
+ // 每个测试前清理存储
+ beforeEach(() => {
+ localStorage.clear();
+ sessionStorage.clear();
+ });
+
+ // 每个测试后清理存储
+ afterEach(() => {
+ localStorage.clear();
+ sessionStorage.clear();
+ });
+
+ describe("localStorage 操作", () => {
+ it("应该能够存储和获取字符串", () => {
+ Storage.set("test-key", "test-value");
+ expect(Storage.get("test-key")).toBe("test-value");
+ });
+
+ it("应该能够存储和获取对象", () => {
+ const testObj = { name: "张三", age: 25 };
+ Storage.set("test-obj", testObj);
+ expect(Storage.get("test-obj")).toEqual(testObj);
+ });
+
+ it("应该能够存储和获取数组", () => {
+ const testArr = [1, 2, 3, 4, 5];
+ Storage.set("test-arr", testArr);
+ expect(Storage.get("test-arr")).toEqual(testArr);
+ });
+
+ it("应该能够存储和获取布尔值", () => {
+ Storage.set("test-bool", true);
+ expect(Storage.get("test-bool")).toBe(true);
+ });
+
+ it("获取不存在的键应该返回 undefined", () => {
+ expect(Storage.get("non-existent")).toBeUndefined();
+ });
+
+ it("获取不存在的键应该返回默认值", () => {
+ expect(Storage.get("non-existent", "default")).toBe("default");
+ });
+
+ it("应该能够删除存储项", () => {
+ Storage.set("test-key", "test-value");
+ Storage.remove("test-key");
+ expect(Storage.get("test-key")).toBeUndefined();
+ });
+ });
+
+ describe("sessionStorage 操作", () => {
+ it("应该能够存储和获取字符串", () => {
+ Storage.sessionSet("test-key", "test-value");
+ expect(Storage.sessionGet("test-key")).toBe("test-value");
+ });
+
+ it("应该能够存储和获取对象", () => {
+ const testObj = { name: "李四", age: 30 };
+ Storage.sessionSet("test-obj", testObj);
+ expect(Storage.sessionGet("test-obj")).toEqual(testObj);
+ });
+
+ it("获取不存在的键应该返回默认值", () => {
+ expect(Storage.sessionGet("non-existent", "default")).toBe("default");
+ });
+
+ it("应该能够删除存储项", () => {
+ Storage.sessionSet("test-key", "test-value");
+ Storage.sessionRemove("test-key");
+ expect(Storage.sessionGet("test-key")).toBeUndefined();
+ });
+ });
+
+ describe("批量清理操作", () => {
+ it("clear() 应该同时清理 localStorage 和 sessionStorage", () => {
+ Storage.set("test-key", "local-value");
+ Storage.sessionSet("test-key", "session-value");
+
+ Storage.clear("test-key");
+
+ expect(Storage.get("test-key")).toBeUndefined();
+ expect(Storage.sessionGet("test-key")).toBeUndefined();
+ });
+
+ it("clearMultiple() 应该批量清理多个键", () => {
+ Storage.set("key1", "value1");
+ Storage.set("key2", "value2");
+ Storage.sessionSet("key1", "session1");
+
+ Storage.clearMultiple(["key1", "key2"]);
+
+ expect(Storage.get("key1")).toBeUndefined();
+ expect(Storage.get("key2")).toBeUndefined();
+ expect(Storage.sessionGet("key1")).toBeUndefined();
+ });
+
+ it("clearByPrefix() 应该清理指定前缀的所有存储项", () => {
+ Storage.set(`${APP_PREFIX}:auth:token`, "token123");
+ Storage.set(`${APP_PREFIX}:auth:user`, "user123");
+ Storage.set(`${APP_PREFIX}:ui:theme`, "dark");
+ Storage.set("other:key", "other-value");
+
+ Storage.clearByPrefix(`${APP_PREFIX}:auth:`);
+
+ expect(Storage.get(`${APP_PREFIX}:auth:token`)).toBeUndefined();
+ expect(Storage.get(`${APP_PREFIX}:auth:user`)).toBeUndefined();
+ expect(Storage.get(`${APP_PREFIX}:ui:theme`)).toBe("dark");
+ expect(Storage.get("other:key")).toBe("other-value");
+ });
+
+ it("clearAllProject() 应该清理所有项目相关的存储", () => {
+ Storage.set(STORAGE_KEYS.ACCESS_TOKEN, "token123");
+ Storage.set(STORAGE_KEYS.THEME, "dark");
+ Storage.set("other:key", "other-value");
+
+ Storage.clearAllProject();
+
+ expect(Storage.get(STORAGE_KEYS.ACCESS_TOKEN)).toBeUndefined();
+ expect(Storage.get(STORAGE_KEYS.THEME)).toBeUndefined();
+ expect(Storage.get("other:key")).toBe("other-value");
+ });
+ });
+
+ describe("边界情况", () => {
+ it("应该能够处理 null 值", () => {
+ Storage.set("test-null", null);
+ expect(Storage.get("test-null")).toBeNull();
+ });
+
+ it("应该能够处理空字符串", () => {
+ Storage.set("test-empty", "");
+ expect(Storage.get("test-empty")).toBe("");
+ });
+
+ it("应该能够处理数字 0", () => {
+ Storage.set("test-zero", 0);
+ expect(Storage.get("test-zero")).toBe(0);
+ });
+
+ it("应该能够处理 false", () => {
+ Storage.set("test-false", false);
+ expect(Storage.get("test-false")).toBe(false);
+ });
+ });
+});
diff --git a/tests/unit/utils/validate.test.ts b/tests/unit/utils/validate.test.ts
new file mode 100644
index 00000000..beb9b5c5
--- /dev/null
+++ b/tests/unit/utils/validate.test.ts
@@ -0,0 +1,177 @@
+import { describe, it, expect } from "vitest";
+import { isExternal, isValidURL, isEmail, isMobile, VALIDATORS } from "@/utils/validate";
+
+describe("Validate 工具函数", () => {
+ describe("isExternal()", () => {
+ it("应该识别外部链接", () => {
+ expect(isExternal("https://www.example.com")).toBe(true);
+ expect(isExternal("http://example.com")).toBe(true);
+ expect(isExternal("//example.com")).toBe(true);
+ expect(isExternal("mailto:test@example.com")).toBe(true);
+ expect(isExternal("tel:1234567890")).toBe(true);
+ });
+
+ it("应该识别内部链接", () => {
+ expect(isExternal("/dashboard")).toBe(false);
+ expect(isExternal("dashboard")).toBe(false);
+ expect(isExternal("./dashboard")).toBe(false);
+ expect(isExternal("../dashboard")).toBe(false);
+ });
+
+ it("应该处理空字符串", () => {
+ expect(isExternal("")).toBe(false);
+ });
+ });
+
+ describe("isValidURL()", () => {
+ it("应该验证有效的 URL", () => {
+ expect(isValidURL("https://www.example.com")).toBe(true);
+ expect(isValidURL("http://example.com")).toBe(true);
+ expect(isValidURL("https://example.com/path?query=1")).toBe(true);
+ expect(isValidURL("http://localhost:3000")).toBe(true);
+ });
+
+ it("应该拒绝无效的 URL", () => {
+ expect(isValidURL("not-a-url")).toBe(false);
+ expect(isValidURL("//example.com")).toBe(false);
+ expect(isValidURL("/path")).toBe(false);
+ expect(isValidURL("")).toBe(false);
+ });
+ });
+
+ describe("isEmail()", () => {
+ it("应该验证有效的邮箱", () => {
+ expect(isEmail("test@example.com")).toBe(true);
+ expect(isEmail("user.name@example.com")).toBe(true);
+ expect(isEmail("user+tag@example.co.uk")).toBe(true);
+ expect(isEmail("123@example.com")).toBe(true);
+ });
+
+ it("应该拒绝无效的邮箱", () => {
+ expect(isEmail("invalid")).toBe(false);
+ expect(isEmail("@example.com")).toBe(false);
+ expect(isEmail("user@")).toBe(false);
+ expect(isEmail("user @example.com")).toBe(false);
+ expect(isEmail("")).toBe(false);
+ });
+ });
+
+ describe("isMobile()", () => {
+ it("应该验证有效的手机号", () => {
+ expect(isMobile("13800138000")).toBe(true);
+ expect(isMobile("15912345678")).toBe(true);
+ expect(isMobile("18612345678")).toBe(true);
+ expect(isMobile("19912345678")).toBe(true);
+ });
+
+ it("应该拒绝无效的手机号", () => {
+ expect(isMobile("12345678901")).toBe(false); // 不是 1 开头的有效号段
+ expect(isMobile("1381234567")).toBe(false); // 少于 11 位
+ expect(isMobile("138123456789")).toBe(false); // 多于 11 位
+ expect(isMobile("abcdefghijk")).toBe(false); // 非数字
+ expect(isMobile("")).toBe(false);
+ });
+ });
+
+ describe("VALIDATORS 对象", () => {
+ describe("required 验证器", () => {
+ it("应该验证必填项", () => {
+ const callback = vi.fn();
+
+ VALIDATORS.required({}, "test", callback);
+ expect(callback).toHaveBeenCalledWith(new Error("此项为必填项"));
+
+ callback.mockClear();
+ VALIDATORS.required({}, "", callback);
+ expect(callback).toHaveBeenCalledWith(new Error("此项为必填项"));
+
+ callback.mockClear();
+ VALIDATORS.required({}, null, callback);
+ expect(callback).toHaveBeenCalledWith(new Error("此项为必填项"));
+
+ callback.mockClear();
+ VALIDATORS.required({}, undefined, callback);
+ expect(callback).toHaveBeenCalledWith(new Error("此项为必填项"));
+ });
+
+ it("应该通过有效值", () => {
+ const callback = vi.fn();
+
+ VALIDATORS.required({}, "value", callback);
+ expect(callback).toHaveBeenCalledWith();
+
+ callback.mockClear();
+ VALIDATORS.required({}, 0, callback);
+ expect(callback).toHaveBeenCalledWith();
+
+ callback.mockClear();
+ VALIDATORS.required({}, false, callback);
+ expect(callback).toHaveBeenCalledWith();
+ });
+ });
+
+ describe("email 验证器", () => {
+ it("应该验证邮箱格式", () => {
+ const callback = vi.fn();
+
+ VALIDATORS.email({}, "invalid", callback);
+ expect(callback).toHaveBeenCalledWith(new Error("请输入正确的邮箱地址"));
+
+ callback.mockClear();
+ VALIDATORS.email({}, "test@example.com", callback);
+ expect(callback).toHaveBeenCalledWith();
+ });
+
+ it("应该允许空值", () => {
+ const callback = vi.fn();
+
+ VALIDATORS.email({}, "", callback);
+ expect(callback).toHaveBeenCalledWith();
+
+ callback.mockClear();
+ VALIDATORS.email({}, null, callback);
+ expect(callback).toHaveBeenCalledWith();
+ });
+ });
+
+ describe("mobile 验证器", () => {
+ it("应该验证手机号格式", () => {
+ const callback = vi.fn();
+
+ VALIDATORS.mobile({}, "12345678901", callback);
+ expect(callback).toHaveBeenCalledWith(new Error("请输入正确的手机号码"));
+
+ callback.mockClear();
+ VALIDATORS.mobile({}, "13800138000", callback);
+ expect(callback).toHaveBeenCalledWith();
+ });
+
+ it("应该允许空值", () => {
+ const callback = vi.fn();
+
+ VALIDATORS.mobile({}, "", callback);
+ expect(callback).toHaveBeenCalledWith();
+ });
+ });
+
+ describe("url 验证器", () => {
+ it("应该验证 URL 格式", () => {
+ const callback = vi.fn();
+
+ VALIDATORS.url({}, "not-a-url", callback);
+ expect(callback).toHaveBeenCalledWith(new Error("请输入正确的 URL 地址"));
+
+ callback.mockClear();
+ VALIDATORS.url({}, "https://example.com", callback);
+ expect(callback).toHaveBeenCalledWith();
+ });
+
+ it("应该允许空值", () => {
+ const callback = vi.fn();
+
+ VALIDATORS.url({}, "", callback);
+ expect(callback).toHaveBeenCalledWith();
+ });
+ });
+ });
+});
diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json
new file mode 100644
index 00000000..7109adef
--- /dev/null
+++ b/tsconfig.eslint.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "include": [
+ "src/**/*",
+ "mock/**/*",
+ "tests/**/*",
+ "vitest.config.ts",
+ "vite.config.ts",
+ "eslint.config.ts"
+ ]
+}
diff --git a/tsconfig.json b/tsconfig.json
index 4b4ebba7..e314cd1b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -29,6 +29,7 @@
},
"include": [
+ "types/**/*.d.ts",
"mock/**/*.ts",
"src/**/*.ts",
"src/**/*.vue",
diff --git a/types/env.d.ts b/types/env.d.ts
new file mode 100644
index 00000000..67b813a3
--- /dev/null
+++ b/types/env.d.ts
@@ -0,0 +1,32 @@
+///
+
+/**
+ * Vite 环境变量类型定义
+ */
+interface ImportMetaEnv {
+ readonly VITE_APP_PORT: number;
+ readonly VITE_APP_NAME: string;
+ readonly VITE_APP_BASE_API: string;
+ readonly VITE_APP_API_URL: string;
+ readonly VITE_APP_TITLE?: string;
+ readonly VITE_APP_TENANT_ENABLED?: string;
+ readonly VITE_ENABLE_AI_ASSISTANT?: string;
+ readonly VITE_MOCK_DEV_SERVER: boolean;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
+
+declare const __APP_INFO__: {
+ pkg: {
+ name: string;
+ version: string;
+ engines: {
+ node: string;
+ };
+ dependencies: Record;
+ devDependencies: Record;
+ };
+ buildTimestamp: number;
+};
diff --git a/types/modules.d.ts b/types/modules.d.ts
new file mode 100644
index 00000000..a3ef2268
--- /dev/null
+++ b/types/modules.d.ts
@@ -0,0 +1,14 @@
+/**
+ * 第三方模块类型声明
+ */
+
+declare module "*.vue" {
+ import type { DefineComponent } from "vue";
+ const component: DefineComponent<{}, {}, any>;
+ export default component;
+}
+
+declare module "sockjs-client/dist/sockjs.min.js" {
+ import Client from "sockjs-client";
+ export default Client;
+}
diff --git a/types/router.d.ts b/types/router.d.ts
new file mode 100644
index 00000000..1effabc0
--- /dev/null
+++ b/types/router.d.ts
@@ -0,0 +1,17 @@
+/**
+ * Vue Router 类型扩展
+ */
+import "vue-router";
+
+declare module "vue-router" {
+ interface RouteMeta {
+ title?: string;
+ icon?: string;
+ hidden?: boolean;
+ alwaysShow?: boolean;
+ affix?: boolean;
+ keepAlive?: boolean;
+ breadcrumb?: boolean;
+ activeMenu?: string;
+ }
+}
diff --git a/vite.config.ts b/vite.config.ts
index 15bca5f8..c622774e 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -83,8 +83,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
// 指定自定义组件位置(默认:src/components)
dirs: ["src/components", "src/**/components"],
// 导入组件类型声明文件路径 (false:关闭自动生成)
- dts: false,
- // dts: "src/types/components.d.ts",
+ dts: "src/types/components.d.ts",
}),
] as PluginOption[],
// 预加载项目必需的组件
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 00000000..5963d8c9
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,43 @@
+import { defineConfig } from "vitest/config";
+import vue from "@vitejs/plugin-vue";
+import { resolve } from "path";
+
+export default defineConfig({
+ plugins: [vue()],
+ test: {
+ // 使用 happy-dom 作为测试环境(比 jsdom 快)
+ environment: "happy-dom",
+
+ // 全局测试 API(describe, it, expect 等)
+ globals: true,
+
+ // 测试环境设置文件
+ setupFiles: ["./tests/setup.ts"],
+
+ // 覆盖率配置
+ coverage: {
+ provider: "v8",
+ reporter: ["text", "json", "html"],
+ exclude: [
+ "node_modules/",
+ "tests/",
+ "**/*.d.ts",
+ "**/*.config.*",
+ "**/mockData",
+ "**/.{idea,git,cache,output,temp}",
+ ],
+ },
+
+ // 测试文件匹配规则
+ include: ["tests/**/*.{test,spec}.{js,ts}"],
+
+ // 测试超时时间
+ testTimeout: 10000,
+ },
+
+ resolve: {
+ alias: {
+ "@": resolve(__dirname, "./src"),
+ },
+ },
+});