-
-
+
-
+
diff --git a/src/main.ts b/src/main.ts
index 7a70045..313bfeb 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,4 +1,5 @@
import { createApp } from 'vue'
+import 'element-plus/theme-chalk/dark/css-vars.css'
import App from './App.vue'
import { installRouter } from './router'
diff --git a/src/stores/modules/app.ts b/src/stores/modules/app.ts
new file mode 100644
index 0000000..d91d559
--- /dev/null
+++ b/src/stores/modules/app.ts
@@ -0,0 +1,53 @@
+import { computed, ref } from 'vue'
+import { defineStore } from 'pinia'
+import { defaultLayout, type LayoutType } from '@/config/layout'
+
+/**
+ * 布局/主题偏好。默认值来自 src/config/layout.ts(集中配置),
+ * 用户在页面上的修改会覆盖默认值并持久化到 localStorage。
+ */
+export const useAppStore = defineStore(
+ 'appStore',
+ () => {
+ const layout = ref
(defaultLayout.layout)
+ const mixbar = ref(defaultLayout.mixbar)
+ const themeColor = ref(defaultLayout.themeColor)
+ const isDark = ref(defaultLayout.isDark)
+ const headerFixed = ref(defaultLayout.headerFixed)
+ const sidebarWidth = ref(defaultLayout.sidebarWidth)
+ const footerFixed = ref(defaultLayout.footerFixed)
+ const sidebarCollapsed = ref(defaultLayout.sidebarCollapsed)
+ const showBreadcrumb = ref(defaultLayout.showBreadcrumb)
+ const showTabs = ref(defaultLayout.showTabs)
+ const showFullscreen = ref(defaultLayout.showFullscreen)
+ const menuAccordion = ref(defaultLayout.menuAccordion)
+
+ /** 是否有顶栏:horizontal / top 有,sidebar(左右)无。 */
+ const hasHeader = computed(() => layout.value !== 'sidebar')
+
+ /** 是否渲染侧边栏:horizontal / sidebar 有,top 无。 */
+ const hasSidebar = computed(() => layout.value !== 'top')
+
+ /** 是否渲染 MixBar。 */
+ const hasMixbar = computed(() => mixbar.value && hasSidebar.value)
+
+ return {
+ layout,
+ mixbar,
+ themeColor,
+ isDark,
+ headerFixed,
+ sidebarWidth,
+ footerFixed,
+ sidebarCollapsed,
+ showBreadcrumb,
+ showTabs,
+ showFullscreen,
+ menuAccordion,
+ hasHeader,
+ hasSidebar,
+ hasMixbar,
+ }
+ },
+ { persist: true },
+)
diff --git a/src/stores/modules/tabs.ts b/src/stores/modules/tabs.ts
new file mode 100644
index 0000000..72c5f79
--- /dev/null
+++ b/src/stores/modules/tabs.ts
@@ -0,0 +1,25 @@
+import { ref } from 'vue'
+import { defineStore } from 'pinia'
+
+export interface TabItem {
+ path: string
+ title: string
+ closable: boolean
+}
+
+/** 多页签状态(会话级,不持久化)。 */
+export const useTabsStore = defineStore('tabsStore', () => {
+ const tabs = ref([])
+
+ function addTab(tab: TabItem): void {
+ if (!tabs.value.some((t) => t.path === tab.path)) {
+ tabs.value.push(tab)
+ }
+ }
+
+ function removeTab(path: string): void {
+ tabs.value = tabs.value.filter((t) => t.path !== path)
+ }
+
+ return { tabs, addTab, removeTab }
+})
diff --git a/src/types/resource.ts b/src/types/resource.ts
new file mode 100644
index 0000000..6de5038
--- /dev/null
+++ b/src/types/resource.ts
@@ -0,0 +1,20 @@
+/** 资源类型(对齐后端 sys_resource.type)。 */
+export type ResourceType = 'menu' | 'button' | 'api'
+
+/**
+ * 资源/菜单节点,字段严格对齐后端 Resource 模型
+ * (id/code/name/parentCode/type/path/component/icon/sort)。
+ * `children` 为前端按 parentCode 组装的渲染字段。
+ */
+export interface ResourceNode {
+ id: string
+ code: string
+ name: string
+ parentCode: string | null
+ type: ResourceType
+ path: string | null
+ component: string | null
+ icon: string | null
+ sort: number
+ children?: ResourceNode[]
+}
diff --git a/src/utils/theme.ts b/src/utils/theme.ts
new file mode 100644
index 0000000..e04bc7e
--- /dev/null
+++ b/src/utils/theme.ts
@@ -0,0 +1,54 @@
+/** 十六进制颜色 → RGB 三元组。 */
+function hexToRgb(hex: string): [number, number, number] {
+ let h = hex.replace('#', '')
+ if (h.length === 3) h = h.split('').map((c) => c + c).join('')
+ const num = parseInt(h, 16)
+ return [(num >> 16) & 255, (num >> 8) & 255, num & 255]
+}
+
+/** RGB → 十六进制颜色。 */
+function rgbToHex(r: number, g: number, b: number): string {
+ return `#${[r, g, b].map((v) => Math.round(v).toString(16).padStart(2, '0')).join('')}`
+}
+
+/** 颜色混合:weight 为 target 占比(0=纯 base,1=纯 target)。 */
+function mix(base: string, target: string, weight: number): string {
+ const [r1, g1, b1] = hexToRgb(base)
+ const [r2, g2, b2] = hexToRgb(target)
+ return rgbToHex(r1 + (r2 - r1) * weight, g1 + (g2 - g1) * weight, b1 + (b2 - b1) * weight)
+}
+
+/** 应用主题色:写入 Element Plus 主色派生变量与 --wm-color-* 令牌(内联样式优先级最高,明暗皆生效)。 */
+export function applyThemeColor(primary: string): void {
+ const root = document.documentElement
+ const set = (name: string, value: string) => root.style.setProperty(name, value)
+
+ set('--el-color-primary', primary)
+ set('--el-color-primary-light-3', mix(primary, '#ffffff', 0.3))
+ set('--el-color-primary-light-5', mix(primary, '#ffffff', 0.5))
+ set('--el-color-primary-light-7', mix(primary, '#ffffff', 0.7))
+ set('--el-color-primary-light-8', mix(primary, '#ffffff', 0.8))
+ set('--el-color-primary-light-9', mix(primary, '#ffffff', 0.9))
+ set('--el-color-primary-dark-2', mix(primary, '#000000', 0.2))
+
+ // 同步 --wm-color-primary 及色阶,保证 UnoCSS(text-primary 等)一致
+ set('--wm-color-primary', primary)
+ ;[1, 2, 3, 5, 7, 8, 9].forEach((i) => {
+ set(`--wm-color-primary-light-${i}`, mix(primary, '#ffffff', i / 10))
+ })
+ set('--wm-color-primary-dark-1', mix(primary, '#000000', 0.1))
+ set('--wm-color-primary-dark-2', mix(primary, '#000000', 0.2))
+}
+
+/** 应用暗黑模式:同时切换 Element Plus 的 html.dark 与现有 data-theme 令牌。 */
+export function applyDarkMode(isDark: boolean): void {
+ const root = document.documentElement
+ root.classList.toggle('dark', isDark)
+ root.setAttribute('data-theme', isDark ? 'dark' : 'light')
+}
+
+/** 统一应用主题(颜色 + 暗黑)。 */
+export function applyTheme(themeColor: string, isDark: boolean): void {
+ applyThemeColor(themeColor)
+ applyDarkMode(isDark)
+}
diff --git a/stats.html b/stats.html
index b947902..20cd71f 100644
--- a/stats.html
+++ b/stats.html
@@ -4930,7 +4930,7 @@ var drawChart = (function (exports) {