softleader-nuxt-core 1.0.1 → 1.0.5

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.
Files changed (99) hide show
  1. package/.eslintrc.cjs +25 -0
  2. package/.prettierrc +10 -0
  3. package/components/auth/LoginForm.vue +253 -0
  4. package/components/layout/AppFooter.vue +18 -0
  5. package/components/layout/AppHeader.vue +112 -0
  6. package/components/layout/AppSidebar.vue +136 -0
  7. package/components/layout/AppSidebarItem.vue +125 -0
  8. package/components/layout/PortalHeader.vue +164 -0
  9. package/components/layout/header/HeaderActions.vue +51 -0
  10. package/components/layout/header/HeaderBreadcrumbs.vue +75 -0
  11. package/components/layout/header/HeaderNotifications.vue +147 -0
  12. package/components/layout/header/HeaderSearch.vue +77 -0
  13. package/components/layout/header/HeaderUserMenu.vue +257 -0
  14. package/components/uiBusiness/ApiLoadingButton.vue +53 -0
  15. package/components/uiBusiness/BDataTable.vue +888 -0
  16. package/components/uiBusiness/CitySelect.vue +101 -0
  17. package/components/uiBusiness/CountrySelect.vue +73 -0
  18. package/components/uiBusiness/DateRangePicker.vue +254 -0
  19. package/components/uiBusiness/EmailInput.vue +99 -0
  20. package/components/uiBusiness/GenderRadio.vue +116 -0
  21. package/components/uiBusiness/GlobalLoading.vue +61 -0
  22. package/components/uiBusiness/GlobalModal.vue +97 -0
  23. package/components/uiBusiness/GlobalSnackbar.vue +36 -0
  24. package/components/uiBusiness/OptionSelect.vue +51 -0
  25. package/components/uiBusiness/PasswordInput.vue +205 -0
  26. package/components/uiBusiness/PhoneInput.vue +94 -0
  27. package/components/uiBusiness/PolicyForm.vue +47 -0
  28. package/components/uiBusiness/SmartCard.vue +98 -0
  29. package/components/uiBusiness/SmartComplexWidget.vue +273 -0
  30. package/components/uiBusiness/SmartTable.vue +241 -0
  31. package/components/uiInterface/IAlert.vue +109 -0
  32. package/components/uiInterface/IApp.vue +27 -0
  33. package/components/uiInterface/IAvatar.vue +90 -0
  34. package/components/uiInterface/IBreadcrumbs.vue +88 -0
  35. package/components/uiInterface/IButton.vue +409 -0
  36. package/components/uiInterface/ICard.vue +198 -0
  37. package/components/uiInterface/ICheckbox.vue +286 -0
  38. package/components/uiInterface/IChip.vue +129 -0
  39. package/components/uiInterface/IChipGroup.vue +45 -0
  40. package/components/uiInterface/ICodeBlock.vue +176 -0
  41. package/components/uiInterface/IDataTable.vue +515 -0
  42. package/components/uiInterface/IDatePicker.vue +231 -0
  43. package/components/uiInterface/IDivider.vue +56 -0
  44. package/components/uiInterface/IIcon.vue +254 -0
  45. package/components/uiInterface/IInput.vue +342 -0
  46. package/components/uiInterface/ILoadingButton.vue +37 -0
  47. package/components/uiInterface/ILoadingSpinner.vue +68 -0
  48. package/components/uiInterface/IModal.vue +260 -0
  49. package/components/uiInterface/IRadio.vue +239 -0
  50. package/components/uiInterface/ISelect.vue +296 -0
  51. package/components/uiInterface/ISheet.vue +77 -0
  52. package/components/uiInterface/ISnackbar.vue +173 -0
  53. package/components/uiInterface/IStack.vue +46 -0
  54. package/components/uiInterface/ISwitch.vue +278 -0
  55. package/components/uiInterface/ITabs.vue +77 -0
  56. package/components/uiInterface/ITextField.vue +334 -0
  57. package/components/uiInterface/ITextarea.vue +342 -0
  58. package/components/uiInterface/layout/IAppBar.vue +114 -0
  59. package/components/uiInterface/layout/IBadge.vue +96 -0
  60. package/components/uiInterface/layout/IBreadcrumbs.vue +120 -0
  61. package/components/uiInterface/layout/IDrawer.vue +148 -0
  62. package/components/uiInterface/layout/IMenuItem.vue +189 -0
  63. package/composables/useApi.ts +215 -0
  64. package/composables/useAppDevice.ts +85 -0
  65. package/composables/useCustomIcon.ts +76 -0
  66. package/composables/useDateTime.ts +550 -0
  67. package/composables/useDebounce.ts +173 -0
  68. package/composables/useEncryption.ts +159 -0
  69. package/composables/useErrorHandler.ts +227 -0
  70. package/composables/useFeatureFlag.ts +176 -0
  71. package/composables/useFileDownload.ts +372 -0
  72. package/composables/useFileUpload.ts +495 -0
  73. package/composables/useFormatter.ts +343 -0
  74. package/composables/useIdle.ts +146 -0
  75. package/composables/useLanguage.ts +69 -0
  76. package/composables/useLoading.ts +49 -0
  77. package/composables/useMenuFilter.ts +69 -0
  78. package/composables/useModal.ts +191 -0
  79. package/composables/useNetwork.ts +102 -0
  80. package/composables/useNotify.ts +61 -0
  81. package/composables/useOptions.ts +436 -0
  82. package/composables/usePagination.ts +247 -0
  83. package/composables/usePermission.ts +192 -0
  84. package/composables/useRepository.ts +8 -0
  85. package/composables/useRepositoryHelpers.ts +80 -0
  86. package/composables/useStorage.ts +210 -0
  87. package/composables/useTableData.ts +127 -0
  88. package/composables/useValidation.ts +410 -0
  89. package/composables/useWatermark.ts +165 -0
  90. package/nuxt.config.ts +82 -18
  91. package/package.json +27 -9
  92. package/plugins/README.md +5 -0
  93. package/plugins/api.ts +27 -0
  94. package/plugins/dayjs.global.ts +17 -0
  95. package/plugins/security.client.ts +575 -0
  96. package/plugins/vuetify.ts +37 -0
  97. package/components/Button.vue +0 -108
  98. package/components/Card.vue +0 -81
  99. package/tsconfig.base.json +0 -19
package/.eslintrc.cjs ADDED
@@ -0,0 +1,25 @@
1
+ module.exports = {
2
+ root: true,
3
+ env: {
4
+ browser: true,
5
+ node: true,
6
+ es2022: true
7
+ },
8
+ extends: [
9
+ '@nuxt/eslint-config',
10
+ 'plugin:@typescript-eslint/recommended'
11
+ ],
12
+ parser: '@typescript-eslint/parser',
13
+ parserOptions: {
14
+ ecmaVersion: 2022,
15
+ sourceType: 'module'
16
+ },
17
+ plugins: ['@typescript-eslint'],
18
+ rules: {
19
+ // 自訂規則
20
+ 'vue/multi-word-component-names': 'off',
21
+ '@typescript-eslint/no-explicit-any': 'warn',
22
+ 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
23
+ 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
24
+ }
25
+ }
package/.prettierrc ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "tabWidth": 2,
5
+ "useTabs": false,
6
+ "trailingComma": "none",
7
+ "printWidth": 100,
8
+ "arrowParens": "avoid",
9
+ "endOfLine": "auto"
10
+ }
@@ -0,0 +1,253 @@
1
+ <script setup lang="ts">
2
+ import IButton from '../uiInterface/IButton.vue'
3
+ import ICheckbox from '../uiInterface/ICheckbox.vue'
4
+ import EmailInput from '../uiBusiness/EmailInput.vue'
5
+ import PasswordInput from '../uiBusiness/PasswordInput.vue'
6
+
7
+ const { $api } = useNuxtApp()
8
+ const router = useRouter()
9
+
10
+ defineProps<{
11
+ title?: string
12
+ subtitle?: string
13
+ }>()
14
+
15
+ const form = reactive({
16
+ username: '',
17
+ password: '',
18
+ rememberMe: false
19
+ })
20
+
21
+ const loading = ref(false)
22
+ const errorMsg = ref('')
23
+
24
+ /**
25
+ * 處理登入請求
26
+ */
27
+ async function handleLogin() {
28
+ loading.value = true
29
+ errorMsg.value = ''
30
+
31
+ try {
32
+ const { data, error } = await $api.auth.login({
33
+ username: form.username,
34
+ password: form.password
35
+ })
36
+
37
+ if (error.value) {
38
+ throw new Error(error.value.message || '登入失敗')
39
+ }
40
+
41
+ const config = useRuntimeConfig()
42
+ const token = useCookie(config.public.auth.tokenKey as string, {
43
+ maxAge: Number(config.public.auth.maxAge)
44
+ })
45
+ token.value = (data.value as any)?.accessToken
46
+
47
+ router.push('/')
48
+ } catch (err: any) {
49
+ errorMsg.value = err.message || '發生未知錯誤'
50
+ } finally {
51
+ loading.value = false
52
+ }
53
+ }
54
+ </script>
55
+
56
+ <template>
57
+ <div class="login-wrapper">
58
+ <div class="glass-card">
59
+ <div class="login-header">
60
+ <h2 class="login-title">
61
+ {{ title || '歡迎回來' }}
62
+ </h2>
63
+ <p class="login-subtitle">
64
+ {{ subtitle || '請輸入您的帳號密碼以登入系統' }}
65
+ </p>
66
+ </div>
67
+
68
+ <form
69
+ class="login-form"
70
+ @submit.prevent="handleLogin"
71
+ >
72
+ <div class="input-stack">
73
+ <label class="input-label">帳號 / Email</label>
74
+ <EmailInput
75
+ v-model="form.username"
76
+ placeholder="請輸入電子郵件"
77
+ :disabled="loading"
78
+ required
79
+ />
80
+
81
+ <label class="input-label">密碼</label>
82
+ <PasswordInput
83
+ v-model="form.password"
84
+ placeholder="••••••••"
85
+ :disabled="loading"
86
+ required
87
+ :show-strength="false"
88
+ />
89
+ </div>
90
+
91
+ <div class="form-extras">
92
+ <ICheckbox
93
+ v-model="form.rememberMe"
94
+ label="記住我 30 天"
95
+ />
96
+ <a
97
+ href="#"
98
+ class="forgot-link"
99
+ >
100
+ 忘記密碼?
101
+ </a>
102
+ </div>
103
+
104
+ <div
105
+ v-if="errorMsg"
106
+ class="error-message"
107
+ >
108
+ <span class="error-icon">⚠️</span>
109
+ {{ errorMsg }}
110
+ </div>
111
+
112
+ <IButton
113
+ type="submit"
114
+ block
115
+ color="white"
116
+ size="large"
117
+ :loading="loading"
118
+ class="submit-btn"
119
+ >
120
+ 登入
121
+ </IButton>
122
+ </form>
123
+ </div>
124
+ </div>
125
+ </template>
126
+
127
+ <style scoped>
128
+ .login-wrapper {
129
+ width: 100%;
130
+ max-width: 640px;
131
+ perspective: 1000px;
132
+ }
133
+
134
+ /*
135
+ Refined Glass Card
136
+ Subtle, Premium, Minimalist
137
+ */
138
+ .glass-card {
139
+ background: rgba(30, 41, 59, 0.4); /* Very subtle dark slate */
140
+ backdrop-filter: blur(24px);
141
+ -webkit-backdrop-filter: blur(24px);
142
+ border: 1px solid rgba(255, 255, 255, 0.08);
143
+ border-radius: 24px;
144
+ padding: 3rem 2.5rem;
145
+ box-shadow:
146
+ 0 0 0 1px rgba(0, 0, 0, 0.05),
147
+ 0 24px 48px -12px rgba(0, 0, 0, 0.2);
148
+ transition: transform 0.3s ease;
149
+ }
150
+
151
+ .glass-card:hover {
152
+ border-color: rgba(255, 255, 255, 0.12);
153
+ box-shadow:
154
+ 0 0 0 1px rgba(255, 255, 255, 0.05),
155
+ 0 32px 64px -12px rgba(0, 0, 0, 0.3);
156
+ }
157
+
158
+ .login-header {
159
+ margin-bottom: 2.5rem;
160
+ text-align: center;
161
+ }
162
+
163
+ .login-title {
164
+ font-size: 1.875rem;
165
+ font-weight: 700;
166
+ color: #f8fafc;
167
+ margin: 0 0 0.75rem 0;
168
+ letter-spacing: -0.025em;
169
+ }
170
+
171
+ .login-subtitle {
172
+ color: #94a3b8;
173
+ font-size: 1rem;
174
+ line-height: 1.5;
175
+ }
176
+
177
+ .login-form {
178
+ display: flex;
179
+ flex-direction: column;
180
+ gap: 1.5rem;
181
+ }
182
+
183
+ .input-stack {
184
+ display: flex;
185
+ flex-direction: column;
186
+ gap: 1.25rem; /* Slightly more space for card layout */
187
+ }
188
+
189
+ .input-label {
190
+ display: block;
191
+ font-size: 0.875rem;
192
+ font-weight: 600; /* Bolder label for card */
193
+ color: #cbd5e1;
194
+ margin-bottom: -0.5rem;
195
+ margin-left: 0.25rem; /* Align with input curve */
196
+ }
197
+
198
+ .form-extras {
199
+ display: flex;
200
+ justify-content: space-between;
201
+ align-items: center;
202
+ font-size: 0.875rem;
203
+ padding: 0 0.25rem;
204
+ }
205
+
206
+ /* Checkbox text color override */
207
+ /* Checkbox text color override */
208
+ :deep(.i-checkbox__label) {
209
+ color: #94a3b8 !important;
210
+ font-size: 0.875rem !important;
211
+ font-weight: 500;
212
+ }
213
+
214
+ .forgot-link {
215
+ color: #ffffff;
216
+ text-decoration: none;
217
+ font-weight: 500;
218
+ transition: opacity 0.2s;
219
+ }
220
+
221
+ .forgot-link:hover {
222
+ opacity: 0.8;
223
+ text-decoration: underline;
224
+ }
225
+
226
+ .error-message {
227
+ background: rgba(239, 68, 68, 0.15);
228
+ border: 1px solid rgba(239, 68, 68, 0.25);
229
+ border-radius: 12px;
230
+ padding: 0.75rem 1rem;
231
+ color: #fca5a5;
232
+ font-size: 0.875rem;
233
+ display: flex;
234
+ align-items: center;
235
+ gap: 0.5rem;
236
+ }
237
+
238
+ :deep(.submit-btn) {
239
+ font-weight: 700 !important;
240
+ font-size: 1.05rem !important;
241
+ height: 52px !important;
242
+ border-radius: 12px !important;
243
+ border: none !important;
244
+ margin-top: 0.5rem;
245
+ transition: all 0.2s !important;
246
+ /* Removed fixed background/color to let props control it, or default to primary if needed */
247
+ }
248
+
249
+ :deep(.submit-btn:hover) {
250
+ transform: translateY(-1px);
251
+ box-shadow: 0 4px 12px rgba(255, 255, 255, 0.2);
252
+ }
253
+ </style>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * 應用程式頁尾 - 框架無關
4
+ * 框架無關的頁尾
5
+ */
6
+ import { useAppStore } from '../../stores/app'
7
+
8
+ const appStore = useAppStore()
9
+ </script>
10
+
11
+ <template>
12
+ <footer
13
+ v-if="appStore.config.footer.visible"
14
+ class="app-footer"
15
+ >
16
+ {{ appStore.config.footer.content }}
17
+ </footer>
18
+ </template>
@@ -0,0 +1,112 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * 應用程式頁首 - 使用三層架構
4
+ * 業務層:整合 Store、子元件
5
+ * UI 層:使用 IAppBar (可替換 UI 框架)
6
+ */
7
+ import IAppBar from '../uiInterface/layout/IAppBar.vue'
8
+ import { useAppStore } from '../../stores/app'
9
+ import HeaderBreadcrumbs from './header/HeaderBreadcrumbs.vue'
10
+ import HeaderSearch from './header/HeaderSearch.vue'
11
+ import HeaderNotifications from './header/HeaderNotifications.vue'
12
+ import HeaderUserMenu from './header/HeaderUserMenu.vue'
13
+ import HeaderActions from './header/HeaderActions.vue'
14
+
15
+ const appStore = useAppStore()
16
+ </script>
17
+
18
+ <template>
19
+ <IAppBar
20
+ v-if="appStore.config.header.visible"
21
+ :fixed="true"
22
+ :elevation="2"
23
+ color="white"
24
+ >
25
+ <!-- 手機版選單切換 -->
26
+ <button
27
+ class="header-nav-toggle"
28
+ aria-label="Toggle navigation"
29
+ @click="appStore.toggleDrawer"
30
+ >
31
+ <svg
32
+ width="24"
33
+ height="24"
34
+ viewBox="0 0 24 24"
35
+ fill="none"
36
+ stroke="currentColor"
37
+ stroke-width="2"
38
+ >
39
+ <line
40
+ x1="3"
41
+ y1="12"
42
+ x2="21"
43
+ y2="12"
44
+ />
45
+ <line
46
+ x1="3"
47
+ y1="6"
48
+ x2="21"
49
+ y2="6"
50
+ />
51
+ <line
52
+ x1="3"
53
+ y1="18"
54
+ x2="21"
55
+ y2="18"
56
+ />
57
+ </svg>
58
+ </button>
59
+
60
+ <div class="header-content">
61
+ <!-- 麵包屑導航 -->
62
+ <HeaderBreadcrumbs />
63
+
64
+ <div class="header-spacer" />
65
+
66
+ <!-- 全域搜尋 -->
67
+ <HeaderSearch />
68
+
69
+ <!-- 通用頁首動作 -->
70
+ <HeaderActions />
71
+
72
+ <!-- 通知中心 -->
73
+ <HeaderNotifications />
74
+
75
+ <!-- 使用者選單 -->
76
+ <HeaderUserMenu />
77
+ </div>
78
+ </IAppBar>
79
+ </template>
80
+
81
+ <style scoped>
82
+ .header-nav-toggle {
83
+ display: none;
84
+ background: none;
85
+ border: none;
86
+ cursor: pointer;
87
+ padding: 0.5rem;
88
+ color: #333;
89
+ transition: color 0.2s;
90
+ }
91
+
92
+ .header-nav-toggle:hover {
93
+ color: #3498db;
94
+ }
95
+
96
+ @media (max-width: 1023px) {
97
+ .header-nav-toggle {
98
+ display: block;
99
+ }
100
+ }
101
+
102
+ .header-content {
103
+ flex: 1;
104
+ display: flex;
105
+ align-items: center;
106
+ gap: 1rem;
107
+ }
108
+
109
+ .header-spacer {
110
+ flex: 1;
111
+ }
112
+ </style>
@@ -0,0 +1,136 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * 應用程式側邊欄 - 使用三層架構
4
+ * 業務層:整合 Store、權限控制、導航邏輯
5
+ * UI 層:使用 IDrawer + IMenuItem (可替換 UI 框架)
6
+ */
7
+ import IDrawer from '../uiInterface/layout/IDrawer.vue'
8
+ import AppSidebarItem from './AppSidebarItem.vue'
9
+
10
+ const sidebarStore = useSidebarStore()
11
+ const userStore = useUserStore()
12
+ const appStore = useAppStore()
13
+
14
+ const props = defineProps<{
15
+ modelValue?: boolean
16
+ }>()
17
+
18
+ const emit = defineEmits(['update:modelValue'])
19
+
20
+ const drawer = computed({
21
+ get: () => props.modelValue,
22
+ set: (val) => emit('update:modelValue', val)
23
+ })
24
+
25
+ // 確保 sidebar 已經生成,並傳入使用者權限
26
+ watch(
27
+ () => userStore.permissions,
28
+ (newPermissions) => {
29
+ sidebarStore.generate(newPermissions)
30
+ },
31
+ { immediate: true }
32
+ )
33
+ </script>
34
+
35
+ <template>
36
+ <IDrawer
37
+ v-model="drawer"
38
+ :permanent="true"
39
+ width="256px"
40
+ >
41
+ <!-- 品牌 Logo 區塊 -->
42
+ <div class="sidebar-brand">
43
+ <div class="sidebar-brand-icon">
44
+ <img
45
+ v-if="appStore.config.branding.logo.image"
46
+ :src="appStore.config.branding.logo.image"
47
+ alt="Logo"
48
+ style="width: 100%; height: 100%; object-fit: contain"
49
+ />
50
+ <svg
51
+ v-else
52
+ width="24"
53
+ height="24"
54
+ viewBox="0 0 24 24"
55
+ fill="currentColor"
56
+ >
57
+ <path d="M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z" />
58
+ </svg>
59
+ </div>
60
+ <div class="sidebar-brand-text">
61
+ <h1 class="sidebar-brand-title">
62
+ {{ appStore.config.branding.title }}
63
+ </h1>
64
+ <p class="sidebar-brand-subtitle">
65
+ {{ appStore.config.branding.subtitle }}
66
+ </p>
67
+ </div>
68
+ </div>
69
+
70
+ <!-- 導航選單 -->
71
+ <nav class="sidebar-nav">
72
+ <div class="sidebar-nav-title">
73
+ {{ appStore.config.sidebar.mainMenuTitle }}
74
+ </div>
75
+ <AppSidebarItem
76
+ v-for="item in sidebarStore.items"
77
+ :key="item.label"
78
+ :item="item"
79
+ />
80
+ </nav>
81
+ </IDrawer>
82
+ </template>
83
+
84
+ <style scoped>
85
+ .sidebar-brand {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 1rem;
89
+ padding: 1.5rem 1rem;
90
+ border-bottom: 1px solid #e0e0e0;
91
+ }
92
+
93
+ .sidebar-brand-icon {
94
+ width: 40px;
95
+ height: 40px;
96
+ flex-shrink: 0;
97
+ color: #3498db;
98
+ }
99
+
100
+ .sidebar-brand-text {
101
+ flex: 1;
102
+ min-width: 0;
103
+ }
104
+
105
+ .sidebar-brand-title {
106
+ font-size: 1.1rem;
107
+ font-weight: 600;
108
+ margin: 0;
109
+ color: #2c3e50;
110
+ white-space: nowrap;
111
+ overflow: hidden;
112
+ text-overflow: ellipsis;
113
+ }
114
+
115
+ .sidebar-brand-subtitle {
116
+ font-size: 0.75rem;
117
+ color: #7f8c8d;
118
+ margin: 0.25rem 0 0 0;
119
+ white-space: nowrap;
120
+ overflow: hidden;
121
+ text-overflow: ellipsis;
122
+ }
123
+
124
+ .sidebar-nav {
125
+ padding: 1rem 0;
126
+ }
127
+
128
+ .sidebar-nav-title {
129
+ padding: 0.5rem 1rem;
130
+ font-size: 0.75rem;
131
+ font-weight: 600;
132
+ color: #95a5a6;
133
+ text-transform: uppercase;
134
+ letter-spacing: 0.5px;
135
+ }
136
+ </style>
@@ -0,0 +1,125 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * 應用程式側邊欄項目 - 框架無關
4
+ * 框架無關的側邊欄項目 (支援遞迴巢狀選單)
5
+ */
6
+ import type { SidebarItem } from '../../core/sidebar/buildSidebar'
7
+ import AppSidebarItem from './AppSidebarItem.vue'
8
+
9
+ defineOptions({
10
+ name: 'AppSidebarItem'
11
+ })
12
+
13
+ const props = defineProps<{
14
+ item: SidebarItem
15
+ }>()
16
+
17
+ const route = useRoute()
18
+ const isExpanded = ref(false)
19
+
20
+ // 判斷是否為目前活動項目
21
+ const isActive = computed(() => {
22
+ if (!props.item.to) return false
23
+ return route.path === props.item.to
24
+ })
25
+
26
+ // 如果有子選單,切換展開狀態
27
+ const toggleExpand = () => {
28
+ if (props.item.children && props.item.children.length > 0) {
29
+ isExpanded.value = !isExpanded.value
30
+ }
31
+ }
32
+
33
+ // 處理點擊事件
34
+ const handleClick = () => {
35
+ if (props.item.to) {
36
+ navigateTo(props.item.to)
37
+ } else {
38
+ toggleExpand()
39
+ }
40
+ }
41
+ </script>
42
+
43
+ <template>
44
+ <!-- 如果有子選單 -->
45
+ <div v-if="item.children && item.children.length > 0">
46
+ <div
47
+ class="sidebar-nav-item"
48
+ :class="{ 'is-disabled': item.disabled }"
49
+ @click="handleClick"
50
+ >
51
+ <svg
52
+ v-if="item.icon"
53
+ class="sidebar-nav-icon"
54
+ width="20"
55
+ height="20"
56
+ viewBox="0 0 24 24"
57
+ fill="none"
58
+ stroke="currentColor"
59
+ stroke-width="2"
60
+ >
61
+ <!-- 簡化的圖標,實際應用中可以根據 item.icon 動態渲染 -->
62
+ <circle
63
+ cx="12"
64
+ cy="12"
65
+ r="10"
66
+ />
67
+ </svg>
68
+ <span style="flex: 1">{{ item.label }}</span>
69
+ <svg
70
+ width="16"
71
+ height="16"
72
+ viewBox="0 0 24 24"
73
+ fill="none"
74
+ stroke="currentColor"
75
+ stroke-width="2"
76
+ :style="{
77
+ transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
78
+ transition: 'transform 0.2s'
79
+ }"
80
+ >
81
+ <polyline points="6 9 12 15 18 9" />
82
+ </svg>
83
+ </div>
84
+
85
+ <!-- 子選單 (遞迴) -->
86
+ <div
87
+ v-show="isExpanded"
88
+ style="padding-left: 1rem"
89
+ >
90
+ <AppSidebarItem
91
+ v-for="child in item.children"
92
+ :key="child.label"
93
+ :item="child"
94
+ />
95
+ </div>
96
+ </div>
97
+
98
+ <!-- 如果沒有子選單 -->
99
+ <NuxtLink
100
+ v-else
101
+ :to="item.to || '#'"
102
+ class="sidebar-nav-item"
103
+ :class="{ 'is-active': isActive, 'is-disabled': item.disabled }"
104
+ @click.prevent="handleClick"
105
+ >
106
+ <svg
107
+ v-if="item.icon"
108
+ class="sidebar-nav-icon"
109
+ width="20"
110
+ height="20"
111
+ viewBox="0 0 24 24"
112
+ fill="none"
113
+ stroke="currentColor"
114
+ stroke-width="2"
115
+ >
116
+ <!-- 簡化的圖標,實際應用中可以根據 item.icon 動態渲染 -->
117
+ <circle
118
+ cx="12"
119
+ cy="12"
120
+ r="10"
121
+ />
122
+ </svg>
123
+ <span>{{ item.label }}</span>
124
+ </NuxtLink>
125
+ </template>