softleader-nuxt-core 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/init.mjs CHANGED
@@ -88,8 +88,8 @@ const writeFile = (filePath, content, overwrite = true) => {
88
88
  // --- Main Execution ---
89
89
 
90
90
  if (isInit) {
91
- if (fs.existsSync(targetDir)) {
92
- log.error(`錯誤: 目錄 "${targetPath}" 已經存在。初始化必須使用新目錄。`)
91
+ if (fs.existsSync(targetDir) && fs.existsSync(path.join(targetDir, 'package.json'))) {
92
+ log.error(`錯誤: 目錄 "${targetPath}" 已經存在且包含 package.json。初始化失敗。`)
93
93
  process.exit(1)
94
94
  }
95
95
  log.step('Initializing New Project...')
@@ -150,23 +150,8 @@ writeFile('app.vue', appVue, false)
150
150
 
151
151
  // 4. 首頁 (Init 模式才產生)
152
152
  if (isInit) {
153
- const indexVue = `<script setup lang="ts">
154
- const { formatDateTime } = useDateTime()
155
- const notify = useNotify()
156
- const now = ref(formatDateTime(new Date()))
157
- const handleClick = () => notify.success('連動成功!')
158
- </script>
159
-
160
- <template>
161
- <div class="pa-10">
162
- <IAlert type="info" title="專案已就緒" text="這是繼承自 softleader-nuxt-core 的新專案。" class="mb-6" />
163
- <ICard title="功能示範">
164
- <div class="d-flex align-center gap-4">
165
- <div>時間: {{ now }}</div>
166
- <IButton variant="primary" @click="handleClick">測試通知</IButton>
167
- </div>
168
- </ICard>
169
- </div>
153
+ const indexVue = `<template>
154
+ <Welcome />
170
155
  </template>
171
156
  `
172
157
  writeFile('pages/index.vue', indexVue, false)
@@ -180,15 +165,19 @@ const useAppInfoTs = `export const useAppInfo = () => {
180
165
  `
181
166
  writeFile('composables/useAppInfo.ts', useAppInfoTs, false)
182
167
 
183
- const exampleRepoTs = `export const exampleRepository = () => {
184
- const api = useApi()
185
- return {
186
- getUsers: () => api.get('/users'),
187
- createUser: (data: any) => api.post('/users', data)
188
- }
168
+ const exampleRepoTs = `/**
169
+ * 示範:Repository 模式與 useApi 的整合
170
+ */
171
+ const exampleRepository = {
172
+ /** 獲取使用者列表 */
173
+ getUsers: () => useApi().get('/users'),
174
+ /** 建立使用者 */
175
+ createUser: (data: any) => useApi().post('/users', data)
189
176
  }
177
+
178
+ export default exampleRepository
190
179
  `
191
- writeFile('repositories/exampleRepository.ts', exampleRepoTs, false)
180
+ writeFile('repositories/exampleRepository.ts', exampleRepoTs, true)
192
181
 
193
182
  // 6. Config Blueprint
194
183
  const defaultConfig = `{
@@ -211,7 +200,40 @@ if (fs.existsSync(schemaPath)) {
211
200
  log.info(' [更新] Config Schema 指引')
212
201
  }
213
202
 
214
- // 8. README (僅在 Init 或檔案不存在時產生)
203
+ // 8. package.json (僅在 Init 或檔案不存在時產生)
204
+ if (isInit) {
205
+ // 讀取當前 core 的版本
206
+ let coreVersion = 'latest'
207
+ try {
208
+ const corePkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'))
209
+ coreVersion = corePkg.version
210
+ } catch (e) {}
211
+
212
+ const packageJson = {
213
+ name: packageName,
214
+ private: true,
215
+ type: 'module',
216
+ scripts: {
217
+ dev: 'nuxt dev',
218
+ build: 'nuxt build',
219
+ generate: 'nuxt generate',
220
+ preview: 'nuxt preview',
221
+ postinstall: 'nuxt prepare',
222
+ typecheck: 'nuxi typecheck'
223
+ },
224
+ dependencies: {
225
+ 'softleader-nuxt-core': `^${coreVersion}`,
226
+ 'nuxt': '^3.15.4'
227
+ },
228
+ devDependencies: {
229
+ 'vue-tsc': '^2.0.0',
230
+ 'typescript': '^5.0.0'
231
+ }
232
+ }
233
+ writeFile('package.json', JSON.stringify(packageJson, null, 2), false)
234
+ }
235
+
236
+ // 9. README (僅在 Init 或檔案不存在時產生)
215
237
  const readmeMd = `# ${projectName}\n\n基於 \`softleader-nuxt-core\` 核心架構。\n\n## 🚀 快速開始\n1. \`npm install\`\n2. \`npm run dev\`\n`
216
238
  writeFile('README.md', readmeMd, false)
217
239
 
@@ -163,10 +163,17 @@ import { computed } from 'vue'
163
163
  import IIcon from '../uiInterface/IIcon.vue'
164
164
  import IButton from '../uiInterface/IButton.vue'
165
165
 
166
- const runtimeConfig = useRuntimeConfig()
167
- const productConfig = runtimeConfig.public.app.productConfig as any
166
+ const appConfig = useAppConfig()
167
+ const productConfig = appConfig.core as any
168
168
 
169
169
  const projectName = computed(() => productConfig?.meta?.title || 'Softleader Project')
170
+
171
+ // 診斷日誌
172
+ onMounted(() => {
173
+ console.log('🎨 Welcome Page mounted with productConfig:', productConfig)
174
+ console.log('🖌️ Current Primary Color:', productConfig?.theme?.primaryColor)
175
+ })
176
+
170
177
  const handleStart = () => {
171
178
  window.scrollTo({ top: window.innerHeight, behavior: 'smooth' })
172
179
  }
@@ -219,7 +226,8 @@ const handleStart = () => {
219
226
  right: -5%;
220
227
  width: 50vw;
221
228
  height: 50vw;
222
- background: rgba(59, 130, 246, 0.1);
229
+ background: var(--color-primary, rgba(59, 130, 246, 0.1));
230
+ opacity: 0.1;
223
231
  }
224
232
  .glow-2 {
225
233
  bottom: -10%;
@@ -274,7 +282,7 @@ const handleStart = () => {
274
282
  }
275
283
 
276
284
  .gradient-text {
277
- background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
285
+ background: linear-gradient(135deg, var(--color-primary, #2563eb) 0%, #7c3aed 100%);
278
286
  -webkit-background-clip: text;
279
287
  background-clip: text;
280
288
  -webkit-text-fill-color: transparent;
@@ -336,7 +344,7 @@ const handleStart = () => {
336
344
  }
337
345
 
338
346
  .prime-cta {
339
- background: #2563eb !important;
347
+ background: var(--color-primary, #2563eb) !important;
340
348
  color: white !important;
341
349
  width: fit-content !important;
342
350
  height: 68px !important;
@@ -344,13 +352,13 @@ const handleStart = () => {
344
352
  border-radius: 1.25rem !important;
345
353
  font-weight: 900 !important;
346
354
  font-size: 1.25rem !important;
347
- box-shadow: 0 25px 50px -12px rgba(37, 99, 235, 0.4) !important;
355
+ box-shadow: 0 25px 50px -12px var(--color-primary-alpha, rgba(37, 99, 235, 0.4)) !important;
348
356
  transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
349
357
  }
350
358
 
351
359
  .prime-cta:hover {
352
360
  transform: translateY(-4px) scale(1.04) !important;
353
- box-shadow: 0 35px 60px -15px rgba(37, 99, 235, 0.5) !important;
361
+ box-shadow: 0 35px 60px -15px var(--color-primary-alpha, rgba(37, 99, 235, 0.5)) !important;
354
362
  }
355
363
  .prime-cta:active {
356
364
  transform: scale(0.96) !important;
@@ -485,7 +493,7 @@ const handleStart = () => {
485
493
  position: absolute;
486
494
  width: 100%;
487
495
  height: 2px;
488
- background: linear-gradient(90deg, transparent, rgba(37, 99, 235, 0.5), transparent);
496
+ background: linear-gradient(90deg, transparent, var(--color-primary-alpha, rgba(37, 99, 235, 0.5)), transparent);
489
497
  top: 0;
490
498
  animation: scan 3s linear infinite;
491
499
  z-index: 20;
@@ -189,8 +189,8 @@ const buttonClasses = computed(() => {
189
189
  // Design System: Primary Button Gradients & Shadows
190
190
  if (props.variant === 'primary' && !props.disabled && !props.loading && !props.color) {
191
191
  classes.push(
192
- 'bg-gradient-to-r from-indigo-600 to-violet-600 text-white',
193
- 'hover:shadow-[0_10px_25px_-5px_rgba(79,70,229,0.4)]', // Enhanced Colored Shadow
192
+ 'bg-[var(--color-primary,#4f46e5)] text-white',
193
+ 'hover:shadow-[0_10px_25px_-5px_var(--color-primary-alpha,rgba(79,70,229,0.4))]', // Enhanced Colored Shadow
194
194
  'hover:-translate-y-0.5', // Lift effect
195
195
  'transition-all duration-200 ease-out'
196
196
  )
@@ -28,8 +28,11 @@ type FeaturePath =
28
28
  * @returns 功能是否啟用
29
29
  */
30
30
  function getFeatureValue(config: FeatureConfig): boolean {
31
+ // 安全檢查:確保是在 Node 環境或有 process polyfill
32
+ const hasProcess = typeof process !== 'undefined' && process.env
33
+
31
34
  // 優先使用環境變數
32
- if (config.envKey) {
35
+ if (config.envKey && hasProcess) {
33
36
  const envValue = process.env[config.envKey]
34
37
  if (envValue !== undefined) {
35
38
  return envValue === 'true' || envValue === '1'
@@ -38,7 +41,7 @@ function getFeatureValue(config: FeatureConfig): boolean {
38
41
 
39
42
  // 檢查環境限制
40
43
  if (config.environments && config.environments.length > 0) {
41
- const currentEnv = process.env.NODE_ENV || 'development'
44
+ const currentEnv = (hasProcess ? process.env.NODE_ENV : 'development') || 'development'
42
45
  const isAllEnvironments = config.environments.includes('all')
43
46
  const isCurrentEnvironment = config.environments.includes(
44
47
  currentEnv as 'development' | 'staging' | 'production'
@@ -1,7 +1,5 @@
1
1
  import { productConfig } from '../../scripts/product-loader'
2
2
 
3
- import { productConfig } from '../../scripts/product-loader'
4
-
5
3
  /**
6
4
  * 全域主題設計變數 (Design Tokens)
7
5
  * 供 Vuetify、Tailwind 或其他 UI 框架共用的基礎變數
@@ -1,43 +1,43 @@
1
1
  <template>
2
2
  <div class="layout-default">
3
3
  <!-- Header: 頂部導覽列 -->
4
- <header v-if="layout.header.visible" class="header" :style="{ height: `${layout.header.height}px` }">
4
+ <header v-if="layout?.header?.visible" class="header" :style="{ height: `${layout?.header?.height || 64}px` }">
5
5
  <div class="container d-flex align-center justify-space-between h-100">
6
6
  <div class="branding d-flex align-center gap-2">
7
- <IIcon v-if="layout.branding.logo.icon && !layout.branding.logo.image" :name="layout.branding.logo.icon" size="28" color="primary" />
8
- <img v-if="layout.branding.logo.image" :src="layout.branding.logo.image" class="logo-img" alt="logo" />
7
+ <IIcon v-if="layout?.branding?.logo?.icon && !layout?.branding?.logo?.image" :name="layout.branding.logo.icon" size="28" color="primary" />
8
+ <img v-if="layout?.branding?.logo?.image" :src="layout.branding.logo.image" class="logo-img" alt="logo" />
9
9
  <div class="branding-text">
10
- <h1 class="logo-title">{{ layout.branding.title }}</h1>
11
- <p v-if="layout.branding.subtitle" class="logo-subtitle">{{ layout.branding.subtitle }}</p>
10
+ <h1 class="logo-title">{{ layout?.branding?.title }}</h1>
11
+ <p v-if="layout?.branding?.subtitle" class="logo-subtitle">{{ layout.branding.subtitle }}</p>
12
12
  </div>
13
13
  </div>
14
14
 
15
15
  <div class="actions d-flex align-center gap-4">
16
16
  <!-- 搜尋框 -->
17
- <div v-if="layout.header.search" class="search-bar">
18
- <input type="text" :placeholder="layout.header.searchPlaceholder" />
17
+ <div v-if="layout?.header?.search" class="search-bar">
18
+ <input type="text" :placeholder="layout?.header?.searchPlaceholder" />
19
19
  </div>
20
20
 
21
21
  <slot name="header-actions" />
22
22
 
23
23
  <!-- 使用者選單按鈕 (示意) -->
24
- <div v-if="layout.header.userMenu.visible" class="user-action">
24
+ <div v-if="layout?.header?.userMenu?.visible" class="user-action">
25
25
  <IIcon name="mdi-account-circle" size="32" class="cursor-pointer" />
26
26
  </div>
27
27
  </div>
28
28
  </div>
29
29
  </header>
30
30
 
31
- <main class="main" :class="{ 'has-header': layout.header.visible }">
31
+ <main class="main" :class="{ 'has-header': layout?.header?.visible }">
32
32
  <div class="container py-10">
33
33
  <slot />
34
34
  </div>
35
35
  </main>
36
36
 
37
37
  <!-- Footer: 頁尾 -->
38
- <footer v-if="layout.footer.visible" class="footer">
38
+ <footer v-if="layout?.footer?.visible" class="footer">
39
39
  <div class="container">
40
- <div class="footer-content" v-html="layout.footer.content"></div>
40
+ <div class="footer-content" v-html="layout?.footer?.content"></div>
41
41
  </div>
42
42
  </footer>
43
43
  </div>
@@ -57,24 +57,25 @@ const layout = computed(() => core.layout)
57
57
  useHead({
58
58
  style: [
59
59
  {
60
- innerHTML: `
60
+ innerHTML: computed(() => `
61
61
  :root {
62
62
  /* 基礎語意色彩 */
63
- --color-primary: ${themeConfig.colors.primary};
64
- --color-success: ${themeConfig.colors.success};
65
- --color-warning: ${themeConfig.colors.warning};
66
- --color-error: ${themeConfig.colors.error};
67
- --color-info: ${themeConfig.colors.info};
63
+ --color-primary: ${layout.value?.theme?.primaryColor || core.theme?.primaryColor || themeConfig.colors.primary};
64
+ --color-primary-alpha: ${layout.value?.theme?.primaryColor || core.theme?.primaryColor || themeConfig.colors.primary}40;
65
+ --color-success: ${core.theme?.successColor || themeConfig.colors.success};
66
+ --color-warning: ${core.theme?.warningColor || themeConfig.colors.warning};
67
+ --color-error: ${core.theme?.errorColor || themeConfig.colors.error};
68
+ --color-info: ${core.theme?.infoColor || themeConfig.colors.info};
68
69
 
69
70
  /* 覆寫 main.css 中的特定變數以達成 100% 連動 */
70
- --color-primary-600: ${themeConfig.colors.primary};
71
- --color-primary-500: ${themeConfig.colors.primary}e6; /* 加入透明度模擬 */
71
+ --color-primary-600: ${layout.value?.theme?.primaryColor || core.theme?.primaryColor || themeConfig.colors.primary};
72
+ --color-primary-500: ${layout.value?.theme?.primaryColor || core.theme?.primaryColor || themeConfig.colors.primary}e6; /* 加入透明度模擬 */
72
73
 
73
74
  /* 基礎圓角 */
74
- --radius-md: ${themeConfig.shape.borderRadius}px;
75
- --radius-lg: ${themeConfig.shape.borderRadius + 4}px;
75
+ --radius-md: ${core.theme?.borderRadius || themeConfig.shape.borderRadius}px;
76
+ --radius-lg: ${(core.theme?.borderRadius || themeConfig.shape.borderRadius) + 4}px;
76
77
  }
77
- `
78
+ `)
78
79
  }
79
80
  ]
80
81
  })
package/nuxt.config.ts CHANGED
@@ -23,6 +23,22 @@ const { resolve } = createResolver(import.meta.url)
23
23
  export default defineNuxtConfig({
24
24
  ssr: false,
25
25
 
26
+ // Server-side Log
27
+ hooks: {
28
+ ready: () => {
29
+ console.log('🚀 Nuxt Core started with product:', productConfig.meta?.title, 'Color:', productConfig.theme?.primaryColor)
30
+ }
31
+ },
32
+
33
+ /** 支援配置驅動:監控 JSON 變動並自動重啟伺服器 (即改即變) */
34
+ watch: [
35
+ 'configs/*.json',
36
+ 'configs/**/*.json',
37
+ // 使用絕對路徑確保在不同 CWD 下都能監控到目前專案的 configs
38
+ `${process.cwd()}/configs/*.json`,
39
+ `${process.cwd()}/configs/**/*.json`
40
+ ],
41
+
26
42
  compatibilityDate: '2024-04-03',
27
43
 
28
44
  /** 編譯與打包設定 */
@@ -61,7 +77,8 @@ export default defineNuxtConfig({
61
77
  app: {
62
78
  ...projectRuntimeConfig.public.app,
63
79
  uaIdentifier:
64
- projectRuntimeConfig.public.app.uaIdentifier || productConfig.branding?.uaIdentifier
80
+ projectRuntimeConfig.public.app.uaIdentifier || productConfig.branding?.uaIdentifier,
81
+ productConfig
65
82
  },
66
83
  // 將 JSON 裡的網路設定同步到 runtimeConfig (環境變數優先)
67
84
  api: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "softleader-nuxt-core",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "Nuxt 3 Core Layer - 可重用的基礎架構",
5
5
  "type": "module",
6
6
  "files": [
@@ -53,7 +53,7 @@
53
53
  "typecheck": "nuxi typecheck",
54
54
  "sync-configs": "node scripts/sync-configs.mjs",
55
55
  "release": "node scripts/release.mjs",
56
- "test:init": "npm pack && node bin/init.mjs init tmp-test-project && rm *.tgz"
56
+ "test:init": "npm pack && node bin/init.mjs init ${1:-tmp-test-project} && rm *.tgz"
57
57
  },
58
58
  "dependencies": {
59
59
  "@nuxt/eslint-config": "^0.5.7",
@@ -121,34 +121,107 @@ export interface ProductConfig {
121
121
  * @param rootDir 專案根目錄 (預設抓目前位置)
122
122
  * @returns 讀到的設定內容
123
123
  */
124
- export function getProductConfig(rootDir: string = process.cwd()): ProductConfig {
124
+ export function getProductConfig(rootDir?: string): ProductConfig {
125
+ // 核心預設結構,確保不會噴 undefined 錯誤 (visible, enabled 等)
126
+ let config: ProductConfig = {
127
+ branding: {},
128
+ meta: {},
129
+ layout: {
130
+ header: { visible: true },
131
+ footer: { visible: true },
132
+ sidebar: { visible: true }
133
+ },
134
+ theme: {},
135
+ network: {},
136
+ features: {},
137
+ auth: {},
138
+ modules: {}
139
+ }
140
+
141
+ // 如果是在瀏覽器環境,直接回傳預設結構 (瀏覽器應該透過 useAppConfig 取得)
142
+ if (typeof process === 'undefined' || !process.cwd) {
143
+ return config
144
+ }
145
+
146
+ // 診斷日誌:寫入檔案以便查閱 (因為看見不到終端機)
147
+ const debugLog = (msg: string) => {
148
+ try {
149
+ fs.appendFileSync('c:\\Users\\gino.huang\\Desktop\\nuxt-layer-core-playground\\product-loader-debug.log', `[${new Date().toISOString()}] ${msg}\n`)
150
+ } catch (e) {}
151
+ console.log(msg)
152
+ }
153
+ debugLog(`>>> getProductConfig start. CWD: ${process.cwd()}, ENV: Ref=${process.env.npm_package_name}`)
154
+
125
155
  // 1. 決定要讀哪個設定檔
126
- // 可以在 package.json 裡面用 cross-env PRODUCT_CONFIG=xxx 來指定
127
156
  const productConfigName = process.env.PRODUCT_CONFIG || 'default'
128
157
 
129
- // 2. 算出檔案在哪裡
130
- const configPath = path.resolve(rootDir, 'configs', `${productConfigName}.json`)
158
+ // 2. 智慧路徑搜尋:找出最近的 configs/ 資料夾
159
+ // 我們會檢查:
160
+ // A. 目前執行目錄 (CWD) 下的 configs/
161
+ // B. 往上找 5 層 (適用於從根目錄跑 workspace 指令的情況)
162
+ let currentSearchDir = process.cwd()
163
+ let configPath = ''
164
+
165
+ console.log(`[Config Debug] CWD: ${currentSearchDir}`)
166
+
167
+ for (let i = 0; i < 5; i++) {
168
+ const targetPath = path.resolve(currentSearchDir, 'configs', `${productConfigName}.json`)
169
+ debugLog(`[Config Path Check ${i}] ${targetPath}`)
170
+
171
+ if (fs.existsSync(targetPath)) {
172
+ configPath = targetPath
173
+ break
174
+ }
175
+
176
+ // 額外支援:如果在根目錄跑,嘗試找 apps/${npm_package_name}/configs
177
+ if (i === 0) {
178
+ const activePackage = process.env.npm_package_name
179
+ const appsDir = path.resolve(currentSearchDir, 'apps')
180
+ debugLog(`[Config Context Check] Package: ${activePackage}, appsDir: ${appsDir}`)
181
+
182
+ if (activePackage && fs.existsSync(path.resolve(appsDir, activePackage))) {
183
+ const appConfigPath = path.resolve(appsDir, activePackage, 'configs', `${productConfigName}.json`)
184
+ debugLog(`[Config Workspace Check] ${appConfigPath}`)
185
+ if (fs.existsSync(appConfigPath)) {
186
+ configPath = appConfigPath
187
+ break
188
+ }
189
+ }
131
190
 
132
- // 預設給一個空的,免得找不到檔案時出錯
133
- let config: ProductConfig = { modules: {} }
191
+ if (!configPath && fs.existsSync(appsDir)) {
192
+ const apps = fs.readdirSync(appsDir)
193
+ for (const app of apps) {
194
+ const appConfigPath = path.resolve(appsDir, app, 'configs', `${productConfigName}.json`)
195
+ if (fs.existsSync(appConfigPath)) {
196
+ configPath = appConfigPath
197
+ break
198
+ }
199
+ }
200
+ }
201
+ }
202
+
203
+ if (configPath) break
204
+
205
+ const nextDir = path.dirname(currentSearchDir)
206
+ if (nextDir === currentSearchDir) break
207
+ currentSearchDir = nextDir
208
+ }
134
209
 
135
- // 3. 真的去讀檔案
136
- if (fs.existsSync(configPath)) {
210
+ if (configPath) {
211
+ debugLog(`[Config Success] 最終確認讀取路徑: ${configPath}`)
137
212
  try {
138
213
  const fileContents = fs.readFileSync(configPath, 'utf8')
139
214
  const loaded = JSON.parse(fileContents)
140
215
 
141
- // 有讀到東西就合併進去
142
216
  if (loaded) {
143
217
  config = { ...config, ...loaded }
218
+ debugLog(`[Config Success] 載入內容: title=${config.meta?.title}, color=${config.theme?.primaryColor}`)
144
219
  }
145
- console.log(`[Config] 成功載入設定: ${productConfigName}`)
146
220
  } catch (e) {
147
- console.error(`[Config] 讀取設定失敗 ${configPath}:`, e)
221
+ debugLog(`[Config Error] 讀取 JSON 失敗 ${configPath}: ${e}`)
148
222
  }
149
223
  } else {
150
- // 找不到檔案,提醒一下
151
- console.warn(`[Config] 找不到設定檔: ${configPath},將不載入任何模組。`)
224
+ debugLog(`[Config Warning] 在任何路徑中都找不到 ${productConfigName}.json`)
152
225
  }
153
226
 
154
227
  return config
package/stores/app.ts CHANGED
@@ -2,8 +2,31 @@ import { defineStore } from 'pinia'
2
2
  import { defaultLayoutConfig, type LayoutConfig } from '../core/config/layout'
3
3
 
4
4
  export const useAppStore = defineStore('app', () => {
5
- // 狀態
6
- const config = ref<LayoutConfig>({ ...defaultLayoutConfig })
5
+ const appConfig = useAppConfig()
6
+ const coreConfig = appConfig.core as any
7
+
8
+ // 狀態:從 appConfig.core 獲取初始值,若無則回退到 defaultLayoutConfig
9
+ const config = ref<LayoutConfig>({
10
+ ...defaultLayoutConfig,
11
+ ...(coreConfig?.layout || {}),
12
+ // 深度處理特定區塊
13
+ theme: {
14
+ ...defaultLayoutConfig.theme,
15
+ ...(coreConfig?.theme || {})
16
+ },
17
+ header: {
18
+ ...defaultLayoutConfig.header,
19
+ ...(coreConfig?.layout?.header || {})
20
+ },
21
+ footer: {
22
+ ...defaultLayoutConfig.footer,
23
+ ...(coreConfig?.layout?.footer || {})
24
+ },
25
+ sidebar: {
26
+ ...defaultLayoutConfig.sidebar,
27
+ ...(coreConfig?.layout?.sidebar || {})
28
+ }
29
+ })
7
30
  const drawer = ref(true)
8
31
  const loading = ref(false)
9
32