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 +49 -27
- package/components/templates/Welcome.vue +16 -8
- package/components/uiInterface/IButton.vue +2 -2
- package/composables/useFeatureFlag.ts +5 -2
- package/core/config/theme-tokens.ts +0 -2
- package/layouts/default.vue +23 -22
- package/nuxt.config.ts +18 -1
- package/package.json +2 -2
- package/scripts/product-loader.ts +86 -13
- package/stores/app.ts +25 -2
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 = `<
|
|
154
|
-
|
|
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 =
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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,
|
|
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.
|
|
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
|
|
167
|
-
const productConfig =
|
|
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-
|
|
193
|
-
'hover:shadow-[0_10px_25px_-
|
|
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'
|
package/layouts/default.vue
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="layout-default">
|
|
3
3
|
<!-- Header: 頂部導覽列 -->
|
|
4
|
-
<header v-if="layout
|
|
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
|
|
8
|
-
<img v-if="layout
|
|
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
|
|
11
|
-
<p v-if="layout
|
|
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
|
|
18
|
-
<input type="text" :placeholder="layout
|
|
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
|
|
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
|
|
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
|
|
38
|
+
<footer v-if="layout?.footer?.visible" class="footer">
|
|
39
39
|
<div class="container">
|
|
40
|
-
<div class="footer-content" v-html="layout
|
|
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-
|
|
65
|
-
--color-
|
|
66
|
-
--color-
|
|
67
|
-
--color-
|
|
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": "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|