softleader-nuxt-core 1.2.0 → 2.2.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 +266 -244
- package/components/templates/Welcome.vue +16 -8
- package/components/uiInterface/IButton.vue +466 -466
- package/composables/useFeatureFlag.ts +5 -2
- package/composables/useModules.ts +18 -18
- package/core/config/theme-tokens.ts +0 -2
- package/layouts/default.vue +23 -22
- package/nuxt.config.ts +18 -1
- package/package.json +3 -2
- package/repositories/index.ts +18 -0
- package/repositories/modules/auth.ts +29 -0
- package/repositories/modules/user.ts +100 -0
- package/scripts/product-loader.ts +86 -13
- package/scripts/release.mjs +24 -24
- package/stores/app.ts +25 -2
|
@@ -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,18 +1,18 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 核心邏輯模組入口 (Centralized Logic Modules Entry)
|
|
3
|
-
* 提供專案規範中要求的核心 Composable。
|
|
4
|
-
*/
|
|
5
|
-
export function useModules() {
|
|
6
|
-
return {
|
|
7
|
-
/** 分頁管理 */
|
|
8
|
-
usePagination,
|
|
9
|
-
/** API 請求 */
|
|
10
|
-
useApi,
|
|
11
|
-
/** 檔案下載 */
|
|
12
|
-
useFileDownload,
|
|
13
|
-
/** 檔案上傳 */
|
|
14
|
-
useFileUpload,
|
|
15
|
-
/** 資料驗證 */
|
|
16
|
-
useValidation
|
|
17
|
-
}
|
|
18
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 核心邏輯模組入口 (Centralized Logic Modules Entry)
|
|
3
|
+
* 提供專案規範中要求的核心 Composable。
|
|
4
|
+
*/
|
|
5
|
+
export function useModules() {
|
|
6
|
+
return {
|
|
7
|
+
/** 分頁管理 */
|
|
8
|
+
usePagination,
|
|
9
|
+
/** API 請求 */
|
|
10
|
+
useApi,
|
|
11
|
+
/** 檔案下載 */
|
|
12
|
+
useFileDownload,
|
|
13
|
+
/** 檔案上傳 */
|
|
14
|
+
useFileUpload,
|
|
15
|
+
/** 資料驗證 */
|
|
16
|
+
useValidation
|
|
17
|
+
}
|
|
18
|
+
}
|
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.2.0",
|
|
4
4
|
"description": "Nuxt 3 Core Layer - 可重用的基礎架構",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"utils",
|
|
20
20
|
"stores",
|
|
21
21
|
"types",
|
|
22
|
+
"repositories",
|
|
22
23
|
"bin",
|
|
23
24
|
"schemas",
|
|
24
25
|
"nuxt.config.ts",
|
|
@@ -53,7 +54,7 @@
|
|
|
53
54
|
"typecheck": "nuxi typecheck",
|
|
54
55
|
"sync-configs": "node scripts/sync-configs.mjs",
|
|
55
56
|
"release": "node scripts/release.mjs",
|
|
56
|
-
"test:init": "npm pack && node bin/init.mjs init tmp-test-project && rm *.tgz"
|
|
57
|
+
"test:init": "npm pack && node bin/init.mjs init ${1:-tmp-test-project} && rm *.tgz"
|
|
57
58
|
},
|
|
58
59
|
"dependencies": {
|
|
59
60
|
"@nuxt/eslint-config": "^0.5.7",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import user from './modules/user'
|
|
2
|
+
import auth from './modules/auth'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Repository Index (資料層)
|
|
6
|
+
*
|
|
7
|
+
* 職責:
|
|
8
|
+
* 1. 自動掃描 modules 資料夾下的所有檔案
|
|
9
|
+
* 2. 將它們打包成一個大物件匯出
|
|
10
|
+
* 3. 不依賴 Nuxt,純粹的資料定義
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const repositories = {
|
|
14
|
+
user,
|
|
15
|
+
auth
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default repositories
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { UseFetchOptions } from 'nuxt/app'
|
|
2
|
+
|
|
3
|
+
// 定義 Client,指向 /auth (假設後端路由是 /api/auth)
|
|
4
|
+
const api = useClient('/auth')
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
/**
|
|
8
|
+
* 使用者登入
|
|
9
|
+
* @param credentials { username, password }
|
|
10
|
+
* @param options
|
|
11
|
+
*/
|
|
12
|
+
login(credentials: any, options: UseFetchOptions<any> = {}) {
|
|
13
|
+
return api.post('/login', credentials, options)
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 使用者登出 (如果有的話)
|
|
18
|
+
*/
|
|
19
|
+
logout() {
|
|
20
|
+
return api.post('/logout')
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 取得當前使用者資訊 (Session/Me)
|
|
25
|
+
*/
|
|
26
|
+
getProfile() {
|
|
27
|
+
return api.get('/me')
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { UseFetchOptions } from 'nuxt/app'
|
|
2
|
+
import type { UserListResponse } from '../../types/api'
|
|
3
|
+
|
|
4
|
+
type MaybeRef<T> = T | Ref<T>
|
|
5
|
+
|
|
6
|
+
interface UserQueryParams {
|
|
7
|
+
page?: MaybeRef<number>
|
|
8
|
+
itemsPerPage?: MaybeRef<number>
|
|
9
|
+
q?: MaybeRef<string>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// 1. 定義 Client
|
|
13
|
+
const api = useClient('/users')
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
/**
|
|
17
|
+
* 取得使用者列表
|
|
18
|
+
* @param params - 查詢參數 (如分頁、搜尋關鍵字)
|
|
19
|
+
* @param options - 其他 Fetch 選項
|
|
20
|
+
* @returns List of users
|
|
21
|
+
*/
|
|
22
|
+
getUsers(params: UserQueryParams = {}, options: UseFetchOptions<UserListResponse> = {}) {
|
|
23
|
+
return api.get<UserListResponse>('/', {
|
|
24
|
+
query: params,
|
|
25
|
+
...options
|
|
26
|
+
})
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 根據 ID 取得單一使用者
|
|
31
|
+
* @param id - 使用者 ID
|
|
32
|
+
* @returns User detail
|
|
33
|
+
*/
|
|
34
|
+
getUserById(id: MaybeRef<number>) {
|
|
35
|
+
return api.get<any>(`/${unref(id)}`)
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* [範例] 建立使用者
|
|
40
|
+
* @param userData - 使用者資料物件
|
|
41
|
+
* @returns Created user data
|
|
42
|
+
*/
|
|
43
|
+
createUser(userData: any) {
|
|
44
|
+
return api.post('/', userData)
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* [範例] 搜尋使用者
|
|
49
|
+
* @param keyword - 搜尋關鍵字
|
|
50
|
+
* @returns Search results
|
|
51
|
+
*/
|
|
52
|
+
searchUsers(keyword: Ref<string>) {
|
|
53
|
+
return api.get('/search', {
|
|
54
|
+
query: { q: keyword },
|
|
55
|
+
watch: [keyword]
|
|
56
|
+
})
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* [範例] 更新使用者
|
|
61
|
+
* @param id - 使用者 ID
|
|
62
|
+
* @param userData - 更新的使用者資料
|
|
63
|
+
* @returns Updated user data
|
|
64
|
+
*/
|
|
65
|
+
updateUser(id: number, userData: any) {
|
|
66
|
+
return api.put(`/${id}`, userData)
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* [範例] 刪除使用者
|
|
71
|
+
* @param id - 使用者 ID
|
|
72
|
+
* @returns Deletion result
|
|
73
|
+
*/
|
|
74
|
+
deleteUser(id: number) {
|
|
75
|
+
return api.delete(`/${id}`)
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* [範例] 取得大量資料 (延遲載入)
|
|
80
|
+
* @returns Heavy data report
|
|
81
|
+
*/
|
|
82
|
+
getHeavyData() {
|
|
83
|
+
// 注意:這裡是 /heavy-report,因為 api base 是 /users
|
|
84
|
+
// 假設 heavy-report 也是在 /users 下,否則需另外處理
|
|
85
|
+
// 原始碼是 useApi('/heavy-report'),這可能不在 /users 下
|
|
86
|
+
// 為了安全,這裡假設它是獨立的
|
|
87
|
+
// 如果是獨立的,應該用 useApi 或另外一個 useClient
|
|
88
|
+
// 這裡我判斷它可能是 /users/heavy-report 的筆誤,或者真的是 root level
|
|
89
|
+
// 為了保持功能,我這裡用 root client 處理 (假設它不是 users/*)
|
|
90
|
+
// 但原檔放在 user.ts 卻叫 /heavy-report 有點怪
|
|
91
|
+
// 我先維持原路徑:/heavy-report (不接在 /users 後面)
|
|
92
|
+
|
|
93
|
+
// 這裡特別展示:如何在 policy 模式下呼叫「外面」的 API
|
|
94
|
+
// 方法 1: 使用 useApi
|
|
95
|
+
// return useApi('/heavy-report', { lazy: true })
|
|
96
|
+
|
|
97
|
+
// 方法 2 (如果它其實是 user 報表):
|
|
98
|
+
return useApi('/heavy-report', { lazy: true })
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -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/scripts/release.mjs
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import { execSync } from 'child_process'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 自動化發布腳本
|
|
5
|
-
* 用法:
|
|
6
|
-
* npm run release (預設 patch: 1.1.0 -> 1.1.1)
|
|
7
|
-
* npm run release minor (1.1.0 -> 1.2.0)
|
|
8
|
-
* npm run release major (1.1.0 -> 2.0.0)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const type = process.argv[2] || 'patch'
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
console.log(`\n\x1b[36m[Release] Step 1: Bumping version (${type})...\x1b[0m`)
|
|
15
|
-
execSync(`npm version ${type} --no-git-tag-version`, { stdio: 'inherit' })
|
|
16
|
-
|
|
17
|
-
console.log(`\n\x1b[36m[Release] Step 2: Publishing to npm...\x1b[0m`)
|
|
18
|
-
execSync('npm publish', { stdio: 'inherit' })
|
|
19
|
-
|
|
20
|
-
console.log('\n\x1b[32m[Release] Successfully published! 🎉\x1b[0m\n')
|
|
21
|
-
} catch (error) {
|
|
22
|
-
console.error('\n\x1b[31m[Release] Failed to publish:\x1b[0m', error.message)
|
|
23
|
-
process.exit(1)
|
|
24
|
-
}
|
|
1
|
+
import { execSync } from 'child_process'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 自動化發布腳本
|
|
5
|
+
* 用法:
|
|
6
|
+
* npm run release (預設 patch: 1.1.0 -> 1.1.1)
|
|
7
|
+
* npm run release minor (1.1.0 -> 1.2.0)
|
|
8
|
+
* npm run release major (1.1.0 -> 2.0.0)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const type = process.argv[2] || 'patch'
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
console.log(`\n\x1b[36m[Release] Step 1: Bumping version (${type})...\x1b[0m`)
|
|
15
|
+
execSync(`npm version ${type} --no-git-tag-version`, { stdio: 'inherit' })
|
|
16
|
+
|
|
17
|
+
console.log(`\n\x1b[36m[Release] Step 2: Publishing to npm...\x1b[0m`)
|
|
18
|
+
execSync('npm publish', { stdio: 'inherit' })
|
|
19
|
+
|
|
20
|
+
console.log('\n\x1b[32m[Release] Successfully published! 🎉\x1b[0m\n')
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('\n\x1b[31m[Release] Failed to publish:\x1b[0m', error.message)
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
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
|
|