gridsum-vue3-pc 1.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +88 -0
  4. package/bin/create-vue3-pc.mjs +545 -0
  5. package/package.json +68 -0
  6. package/template/base/.dockerignore +12 -0
  7. package/template/base/.env +5 -0
  8. package/template/base/.env.production +5 -0
  9. package/template/base/.eslintrc.cjs +22 -0
  10. package/template/base/.husky/commit-msg +1 -0
  11. package/template/base/.husky/pre-commit +1 -0
  12. package/template/base/.lintstagedrc +7 -0
  13. package/template/base/.prettierrc +5 -0
  14. package/template/base/.stylelintrc.cjs +6 -0
  15. package/template/base/.vscode/settings.json +26 -0
  16. package/template/base/CHANGELOG.md +6 -0
  17. package/template/base/Dockerfile +19 -0
  18. package/template/base/README.md +87 -0
  19. package/template/base/commitlint.config.cjs +1 -0
  20. package/template/base/index.html +15 -0
  21. package/template/base/mock/user.js +393 -0
  22. package/template/base/nginx.conf +27 -0
  23. package/template/base/package.json +47 -0
  24. package/template/base/public/favicon.svg +9 -0
  25. package/template/base/public/logo.svg +9 -0
  26. package/template/base/src/App.vue +20 -0
  27. package/template/base/src/assets/index.css +83 -0
  28. package/template/base/src/assets/logo.png +0 -0
  29. package/template/base/src/components/LanguageSwitch.vue +65 -0
  30. package/template/base/src/components/basic-layout.vue +484 -0
  31. package/template/base/src/composables/useCrud.ts +172 -0
  32. package/template/base/src/env.d.ts +28 -0
  33. package/template/base/src/env.ts +24 -0
  34. package/template/base/src/locales/en.json +153 -0
  35. package/template/base/src/locales/index.ts +32 -0
  36. package/template/base/src/locales/zh.json +153 -0
  37. package/template/base/src/main.ts +27 -0
  38. package/template/base/src/router/index.ts +91 -0
  39. package/template/base/src/services/http.ts +64 -0
  40. package/template/base/src/services/user.ts +23 -0
  41. package/template/base/src/store/modules/user.ts +45 -0
  42. package/template/base/src/views/Admin.vue +326 -0
  43. package/template/base/src/views/Home.vue +382 -0
  44. package/template/base/src/views/Login.vue +1252 -0
  45. package/template/base/src/views/Role.vue +269 -0
  46. package/template/base/src/views/User.vue +332 -0
  47. package/template/base/src/views/error/Forbidden.vue +62 -0
  48. package/template/base/src/views/error/NotFound.vue +60 -0
  49. package/template/base/src/views/error/ServerError.vue +62 -0
  50. package/template/base/tests/e2e/example.spec.ts +7 -0
  51. package/template/base/tests/unit/user.test.ts +15 -0
  52. package/template/base/vite.config.ts +52 -0
  53. package/template/cicd-github/.github/workflows/ci.yml +123 -0
  54. package/template/cicd-gitlab/.gitlab-ci.yml +103 -0
  55. package/template/cicd-jenkins/Jenkinsfile +107 -0
  56. package/template/ts/shims-vue.d.ts +5 -0
  57. package/template/ts/tsconfig.json +23 -0
@@ -0,0 +1,484 @@
1
+ <template>
2
+ <el-container class="app-container">
3
+ <el-header class="app-header">
4
+ <div class="app-header-left">
5
+ <img src="/logo.svg" width="22" height="22" class="header-logo" alt="logo" />
6
+ <div class="app-header-title">{{ title }}</div>
7
+ </div>
8
+ <div class="app-header-actions">
9
+ <LanguageSwitch />
10
+ <el-tooltip :content="isDark ? $t('layout.lightMode') : $t('layout.darkMode')" placement="bottom">
11
+ <el-button class="header-btn" text @click="toggleDark">
12
+ <el-icon><Moon v-if="!isDark" /><Sunny v-else /></el-icon>
13
+ </el-button>
14
+ </el-tooltip>
15
+ <el-tooltip :content="isFullscreen ? $t('layout.exitFullscreen') : $t('layout.fullscreen')" placement="bottom">
16
+ <el-button class="header-btn" text @click="toggleFullscreen">
17
+ <el-icon><FullScreen v-if="!isFullscreen" /><Aim v-else /></el-icon>
18
+ </el-button>
19
+ </el-tooltip>
20
+ <el-dropdown trigger="click" class="notification-dropdown">
21
+ <el-badge :value="unreadCount" :hidden="unreadCount === 0" class="notification-badge">
22
+ <el-button class="header-btn" text>
23
+ <el-icon><Bell /></el-icon>
24
+ </el-button>
25
+ </el-badge>
26
+ <template #dropdown>
27
+ <el-dropdown-menu class="notification-menu">
28
+ <el-dropdown-item v-for="notif in notifications" :key="notif.id" :divided="notif !== notifications[0]">
29
+ <div class="notification-item" @click="markRead(notif.id)">
30
+ <div class="notif-dot" :class="{ unread: !notif.read }" />
31
+ <div class="notif-content">
32
+ <div class="notif-title">{{ notif.title }}</div>
33
+ <div class="notif-time">{{ notif.time }}</div>
34
+ </div>
35
+ </div>
36
+ </el-dropdown-item>
37
+ <el-dropdown-item v-if="notifications.length === 0" disabled>
38
+ <span class="no-notif">{{ $t('layout.noNotifications') }}</span>
39
+ </el-dropdown-item>
40
+ </el-dropdown-menu>
41
+ </template>
42
+ </el-dropdown>
43
+ <el-dropdown @command="handleCommand">
44
+ <span class="user-dropdown-link">
45
+ {{ userInfo?.displayName || userInfo?.username || $t('user.notLoggedIn') }}
46
+ <el-icon><ArrowDown /></el-icon>
47
+ </span>
48
+ <template #dropdown>
49
+ <el-dropdown-menu>
50
+ <el-dropdown-item command="logout">{{ $t('user.logout') }}</el-dropdown-item>
51
+ </el-dropdown-menu>
52
+ </template>
53
+ </el-dropdown>
54
+ </div>
55
+ </el-header>
56
+ <el-container>
57
+ <el-aside :width="isCollapse ? '64px' : '200px'" class="app-sidebar">
58
+ <div class="collapse-btn" @click="isCollapse = !isCollapse">
59
+ <el-icon>
60
+ <Fold v-if="!isCollapse" />
61
+ <Expand v-else />
62
+ </el-icon>
63
+ </div>
64
+ <el-menu
65
+ :default-active="activeMenu"
66
+ :collapse="isCollapse"
67
+ router
68
+ class="app-menu"
69
+ >
70
+ <el-menu-item index="/">
71
+ <el-icon><HomeFilled /></el-icon>
72
+ <template #title>{{ $t('menu.home') }}</template>
73
+ </el-menu-item>
74
+ <el-sub-menu index="/system" v-if="isAdmin">
75
+ <template #title>
76
+ <el-icon><Setting /></el-icon>
77
+ <span>{{ $t('menu.system') }}</span>
78
+ </template>
79
+ <el-menu-item index="/user">
80
+ <el-icon><User /></el-icon>
81
+ <template #title>{{ $t('menu.user') }}</template>
82
+ </el-menu-item>
83
+ <el-menu-item index="/role">
84
+ <el-icon><Key /></el-icon>
85
+ <template #title>{{ $t('menu.role') }}</template>
86
+ </el-menu-item>
87
+ <el-menu-item index="/admin">
88
+ <el-icon><Setting /></el-icon>
89
+ <template #title>{{ $t('menu.admin') }}</template>
90
+ </el-menu-item>
91
+ </el-sub-menu>
92
+ </el-menu>
93
+ </el-aside>
94
+ <el-main class="app-main">
95
+ <div class="breadcrumb-bar">
96
+ <el-breadcrumb separator="/">
97
+ <el-breadcrumb-item :to="{ path: '/' }">{{ $t('menu.home') }}</el-breadcrumb-item>
98
+ <el-breadcrumb-item v-for="b in breadcrumbs" :key="b.path" :to="b.path !== route.path ? { path: b.path } : undefined">
99
+ {{ b.title }}
100
+ </el-breadcrumb-item>
101
+ </el-breadcrumb>
102
+ </div>
103
+ <div class="app-content">
104
+ <router-view v-slot="{ Component }">
105
+ <keep-alive :max="10">
106
+ <transition name="page-fade" mode="out-in">
107
+ <component :is="Component" />
108
+ </transition>
109
+ </keep-alive>
110
+ </router-view>
111
+ </div>
112
+ </el-main>
113
+ </el-container>
114
+ </el-container>
115
+ </template>
116
+
117
+ <script setup lang="ts">
118
+ import { ref, computed, onMounted } from 'vue';
119
+ import { useRoute } from 'vue-router';
120
+ import {
121
+ ArrowDown, HomeFilled, Setting, User, Key, Fold, Expand,
122
+ Moon, Sunny, FullScreen, Aim, Bell,
123
+ } from '@element-plus/icons-vue';
124
+ import { useUserStore } from '@/store/modules/user';
125
+ import { storeToRefs } from 'pinia';
126
+ import { useI18n } from 'vue-i18n';
127
+ import LanguageSwitch from './LanguageSwitch.vue';
128
+
129
+ const { t } = useI18n();
130
+ const route = useRoute();
131
+ const userStore = useUserStore();
132
+ const { userInfo } = storeToRefs(userStore);
133
+
134
+ const title = import.meta.env.VITE_APP_TITLE || 'Vue3 PC Template';
135
+
136
+ const isCollapse = ref(false);
137
+ const activeMenu = computed(() => route.path);
138
+ const isFullscreen = ref(false);
139
+
140
+ const isAdmin = computed(() => userStore.isAdmin());
141
+
142
+ const isDark = ref(false);
143
+
144
+ const breadcrumbs = computed(() => {
145
+ const paths = route.path.split('/').filter(Boolean);
146
+ const result: { path: string; title: string }[] = [];
147
+ let accumulator = '';
148
+ for (const p of paths) {
149
+ accumulator += `/${p}`;
150
+ const meta = route.matched.find(r => r.path === accumulator)?.meta;
151
+ const titleKey = meta?.titleKey as string | undefined;
152
+ const title = titleKey ? t(titleKey) : p;
153
+ result.push({ path: accumulator, title });
154
+ }
155
+ return result;
156
+ });
157
+
158
+ interface Notification {
159
+ id: number;
160
+ title: string;
161
+ time: string;
162
+ read: boolean;
163
+ }
164
+
165
+ const notifications = ref<Notification[]>([
166
+ { id: 1, title: t('admin.logLogin', { user: userInfo.value?.username || 'admin' }), time: '10:32', read: false },
167
+ { id: 2, title: t('admin.logCreateUser', { user: 'admin', target: 'zhangsan' }), time: '10:15', read: false },
168
+ { id: 3, title: t('admin.logUpdateRole', { user: 'admin', target: 'user' }), time: '09:50', read: true },
169
+ ]);
170
+
171
+ const unreadCount = computed(() => notifications.value.filter(n => !n.read).length);
172
+
173
+ const markRead = (id: number) => {
174
+ const n = notifications.value.find(item => item.id === id);
175
+ if (n) n.read = true;
176
+ };
177
+
178
+ const toggleDark = () => {
179
+ isDark.value = !isDark.value;
180
+ document.documentElement.classList.toggle('dark', isDark.value);
181
+ localStorage.setItem('theme', isDark.value ? 'dark' : 'light');
182
+ };
183
+
184
+ const toggleFullscreen = () => {
185
+ if (!document.fullscreenElement) {
186
+ document.documentElement.requestFullscreen();
187
+ isFullscreen.value = true;
188
+ } else {
189
+ document.exitFullscreen();
190
+ isFullscreen.value = false;
191
+ }
192
+ };
193
+
194
+ const handleCommand = (command: string) => {
195
+ if (command === 'logout') {
196
+ userStore.logout();
197
+ }
198
+ };
199
+
200
+ onMounted(() => {
201
+ const saved = localStorage.getItem('theme');
202
+ if (saved === 'dark') {
203
+ isDark.value = true;
204
+ document.documentElement.classList.add('dark');
205
+ }
206
+
207
+ document.addEventListener('fullscreenchange', () => {
208
+ isFullscreen.value = !!document.fullscreenElement;
209
+ });
210
+ });
211
+ </script>
212
+
213
+ <style scoped lang="scss">
214
+ .app-container {
215
+ height: 100vh;
216
+ }
217
+
218
+ .app-header {
219
+ display: flex;
220
+ align-items: center;
221
+ justify-content: space-between;
222
+ background: #fff;
223
+ color: #1a1a2e;
224
+ padding: 0 24px;
225
+ z-index: 100;
226
+ position: relative;
227
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
228
+ height: 56px !important;
229
+
230
+ .app-header-left {
231
+ display: flex;
232
+ align-items: center;
233
+ gap: 12px;
234
+ }
235
+
236
+ .header-logo {
237
+ flex-shrink: 0;
238
+ }
239
+
240
+ .app-header-title {
241
+ font-size: 17px;
242
+ font-weight: 700;
243
+ letter-spacing: 0.5px;
244
+ background: linear-gradient(135deg, #667eea, #764ba2);
245
+ -webkit-background-clip: text;
246
+ -webkit-text-fill-color: transparent;
247
+ background-clip: text;
248
+ }
249
+
250
+ .app-header-actions {
251
+ display: flex;
252
+ align-items: center;
253
+ gap: 8px;
254
+ }
255
+
256
+ .header-btn {
257
+ color: #606266;
258
+ font-size: 18px;
259
+ padding: 6px;
260
+ border-radius: 8px;
261
+ transition: all 0.2s;
262
+
263
+ &:hover {
264
+ color: #409eff;
265
+ background: rgba(64, 158, 255, 0.06);
266
+ }
267
+ }
268
+
269
+ .user-dropdown-link {
270
+ cursor: pointer;
271
+ display: flex;
272
+ align-items: center;
273
+ gap: 6px;
274
+ padding: 4px 8px;
275
+ border-radius: 8px;
276
+ font-size: 14px;
277
+ color: #303133;
278
+ transition: all 0.2s;
279
+
280
+ &:hover {
281
+ background: rgba(64, 158, 255, 0.06);
282
+ }
283
+ }
284
+ }
285
+
286
+ .app-sidebar {
287
+ background: #fff;
288
+ border-right: 1px solid #e8e8e8;
289
+ transition: width 0.3s;
290
+ position: relative;
291
+ display: flex;
292
+ flex-direction: column;
293
+ overflow: hidden;
294
+
295
+ .collapse-btn {
296
+ display: flex;
297
+ align-items: center;
298
+ justify-content: center;
299
+ height: 44px;
300
+ cursor: pointer;
301
+ border-bottom: 1px solid #e8e8e8;
302
+ color: #909399;
303
+ transition: all 0.2s;
304
+
305
+ &:hover {
306
+ background: #f5f7fa;
307
+ color: #409eff;
308
+ }
309
+ }
310
+
311
+ .app-menu {
312
+ flex: 1;
313
+ overflow-y: auto;
314
+ overflow-x: hidden;
315
+ border-right: none;
316
+ }
317
+ }
318
+
319
+ .app-main {
320
+ background: #f0f2f5;
321
+ padding: 0;
322
+ display: flex;
323
+ flex-direction: column;
324
+ overflow: auto;
325
+ }
326
+
327
+ .app-content {
328
+ padding: 16px;
329
+ flex: 1;
330
+ display: flex;
331
+ flex-direction: column;
332
+ }
333
+
334
+ .breadcrumb-bar {
335
+ display: flex;
336
+ align-items: center;
337
+ height: 44px;
338
+ padding: 0 24px;
339
+ background: #fff;
340
+ border-bottom: 1px solid #e8e8e8;
341
+ flex-shrink: 0;
342
+ }
343
+
344
+ .notification-badge {
345
+ :deep(.el-badge__content) {
346
+ top: 4px;
347
+ right: 4px;
348
+ }
349
+ }
350
+
351
+ .notification-menu {
352
+ max-height: 300px;
353
+ overflow-y: auto;
354
+ min-width: 280px;
355
+ }
356
+
357
+ .notification-item {
358
+ display: flex;
359
+ gap: 10px;
360
+ padding: 4px 0;
361
+ cursor: pointer;
362
+ width: 100%;
363
+ }
364
+
365
+ .notif-dot {
366
+ width: 8px;
367
+ height: 8px;
368
+ border-radius: 50%;
369
+ background: transparent;
370
+ flex-shrink: 0;
371
+ margin-top: 6px;
372
+
373
+ &.unread {
374
+ background: #409eff;
375
+ }
376
+ }
377
+
378
+ .notif-content {
379
+ flex: 1;
380
+ min-width: 0;
381
+ }
382
+
383
+ .notif-title {
384
+ font-size: 13px;
385
+ color: #303133;
386
+ white-space: nowrap;
387
+ overflow: hidden;
388
+ text-overflow: ellipsis;
389
+ }
390
+
391
+ .notif-time {
392
+ font-size: 11px;
393
+ color: #909399;
394
+ margin-top: 2px;
395
+ }
396
+
397
+ .no-notif {
398
+ color: #909399;
399
+ font-size: 13px;
400
+ }
401
+
402
+ .page-fade-enter-active,
403
+ .page-fade-leave-active {
404
+ transition: opacity 0.25s ease, transform 0.25s ease;
405
+ }
406
+
407
+ .page-fade-enter-from {
408
+ opacity: 0;
409
+ transform: translateY(8px);
410
+ }
411
+
412
+ .page-fade-leave-to {
413
+ opacity: 0;
414
+ transform: translateY(-8px);
415
+ }
416
+
417
+ html.dark .app-header {
418
+ background: #161b22;
419
+ color: #c9d1d9;
420
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
421
+
422
+ .header-btn {
423
+ color: #8b949e;
424
+
425
+ &:hover {
426
+ color: #58a6ff;
427
+ background: rgba(88, 166, 255, 0.1);
428
+ }
429
+ }
430
+
431
+ .user-dropdown-link {
432
+ color: #c9d1d9;
433
+
434
+ &:hover {
435
+ background: rgba(88, 166, 255, 0.1);
436
+ }
437
+ }
438
+
439
+ .notif-title {
440
+ color: #c9d1d9;
441
+ }
442
+
443
+ .notif-time,
444
+ .no-notif {
445
+ color: #8b949e;
446
+ }
447
+ }
448
+
449
+ html.dark .app-sidebar {
450
+ background: #161b22;
451
+ border-right-color: #21262d;
452
+
453
+ .collapse-btn {
454
+ border-bottom-color: #21262d;
455
+ color: #8b949e;
456
+
457
+ &:hover {
458
+ background: #1c2128;
459
+ color: #58a6ff;
460
+ }
461
+ }
462
+
463
+ .app-menu {
464
+ border-right: none;
465
+ }
466
+ }
467
+
468
+ html.dark .app-main {
469
+ background: #0d1117;
470
+ }
471
+
472
+ html.dark .breadcrumb-bar {
473
+ background: #161b22;
474
+ border-bottom-color: #21262d;
475
+
476
+ :deep(.el-breadcrumb__inner) {
477
+ color: #8b949e !important;
478
+ }
479
+
480
+ :deep(.el-breadcrumb__inner.is-link:hover) {
481
+ color: #58a6ff !important;
482
+ }
483
+ }
484
+ </style>
@@ -0,0 +1,172 @@
1
+ import { ref, reactive } from 'vue';
2
+ import { ElMessage, ElMessageBox } from 'element-plus';
3
+ import { useI18n } from 'vue-i18n';
4
+ import http from '@/services/http';
5
+ import type { ResponseData } from '@/services/http';
6
+ import type { PaginatedData } from '@/services/user';
7
+ import type { FormInstance, FormRules } from 'element-plus';
8
+
9
+ export interface CrudOptions<T extends { id?: string | number }> {
10
+ url: string;
11
+ formDefaults: T;
12
+ rules?: FormRules<T>;
13
+ name?: string;
14
+ }
15
+
16
+ export function useCrud<T extends { id?: string | number }>(options: CrudOptions<T>) {
17
+ const { url, formDefaults, name, rules } = options;
18
+ const { t } = useI18n();
19
+
20
+ const list = ref<T[]>([]);
21
+ const loading = ref(false);
22
+ const page = ref(1);
23
+ const pageSize = ref(10);
24
+ const total = ref(0);
25
+ const keyword = ref('');
26
+
27
+ const dialogVisible = ref(false);
28
+ const isEdit = ref(false);
29
+ const selectedIds = ref<(string | number)[]>([]);
30
+
31
+ const formRef = ref<FormInstance>();
32
+ const form = reactive({ ...formDefaults }) as T;
33
+
34
+ const fetchData = async () => {
35
+ loading.value = true;
36
+ try {
37
+ const res: ResponseData<PaginatedData<T>> = await http.get(url, {
38
+ params: { page: page.value, pageSize: pageSize.value, keyword: keyword.value },
39
+ });
40
+ if (res.code === 0 || res.code === 200) {
41
+ list.value = res.data.list;
42
+ total.value = res.data.total;
43
+ }
44
+ } catch {
45
+ ElMessage.error(t('common.error'));
46
+ } finally {
47
+ loading.value = false;
48
+ }
49
+ };
50
+
51
+ const handleSearch = () => {
52
+ page.value = 1;
53
+ fetchData();
54
+ };
55
+
56
+ const handleSizeChange = (size: number) => {
57
+ pageSize.value = size;
58
+ page.value = 1;
59
+ fetchData();
60
+ };
61
+
62
+ const handleCurrentChange = (p: number) => {
63
+ page.value = p;
64
+ fetchData();
65
+ };
66
+
67
+ const openAdd = () => {
68
+ isEdit.value = false;
69
+ Object.assign(form, { ...formDefaults });
70
+ dialogVisible.value = true;
71
+ };
72
+
73
+ const openEdit = (row: T) => {
74
+ isEdit.value = true;
75
+ Object.assign(form, { ...row });
76
+ dialogVisible.value = true;
77
+ };
78
+
79
+ const handleDelete = async (row: T) => {
80
+ try {
81
+ await ElMessageBox.confirm(
82
+ t(`${name || 'common'}.deleteConfirm`),
83
+ t('common.warning'),
84
+ { type: 'warning' },
85
+ );
86
+ await http.delete(`${url}/${row.id}`);
87
+ ElMessage.success(t(`${name || 'common'}.deleteSuccess`));
88
+ fetchData();
89
+ } catch (e: any) {
90
+ if (e !== 'cancel') {
91
+ ElMessage.error(t('common.error'));
92
+ }
93
+ }
94
+ };
95
+
96
+ const handleBatchDelete = async () => {
97
+ if (selectedIds.value.length === 0) {
98
+ ElMessage.warning(t('common.selectItems'));
99
+ return;
100
+ }
101
+ try {
102
+ await ElMessageBox.confirm(
103
+ t('common.batchDeleteConfirm'),
104
+ t('common.warning'),
105
+ { type: 'warning' },
106
+ );
107
+ const res: ResponseData<any> = await http.delete(`${url}/batch`, { data: { ids: selectedIds.value } });
108
+ if (res.code !== 0 && res.code !== 200) {
109
+ ElMessage.error(res.message || t('common.error'));
110
+ return;
111
+ }
112
+ if (res.message && res.message.length > 0) {
113
+ ElMessage.warning(res.message);
114
+ } else {
115
+ ElMessage.success(t(`${name || 'common'}.deleteSuccess`));
116
+ }
117
+ selectedIds.value = [];
118
+ fetchData();
119
+ } catch (e: any) {
120
+ if (e !== 'cancel') {
121
+ ElMessage.error(t('common.error'));
122
+ }
123
+ }
124
+ };
125
+
126
+ const handleSubmit = async () => {
127
+ const valid = await formRef.value?.validate().catch(() => false);
128
+ if (!valid) return;
129
+
130
+ try {
131
+ let res: ResponseData<T>;
132
+ if (isEdit.value) {
133
+ res = await http.put(`${url}/${form.id}`, form);
134
+ } else {
135
+ res = await http.post(url, form);
136
+ }
137
+ if (res.code === 0 || res.code === 200) {
138
+ ElMessage.success(t(`${name || 'common'}.${isEdit.value ? 'editSuccess' : 'addSuccess'}`));
139
+ dialogVisible.value = false;
140
+ fetchData();
141
+ } else {
142
+ ElMessage.error(res.message || t('common.error'));
143
+ }
144
+ } catch {
145
+ ElMessage.error(t('common.error'));
146
+ }
147
+ };
148
+
149
+ return {
150
+ list,
151
+ loading,
152
+ page,
153
+ pageSize,
154
+ total,
155
+ keyword,
156
+ dialogVisible,
157
+ isEdit,
158
+ selectedIds,
159
+ formRef,
160
+ form,
161
+ rules,
162
+ fetchData,
163
+ handleSearch,
164
+ handleSizeChange,
165
+ handleCurrentChange,
166
+ openAdd,
167
+ openEdit,
168
+ handleDelete,
169
+ handleBatchDelete,
170
+ handleSubmit,
171
+ };
172
+ }
@@ -0,0 +1,28 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_APP_TITLE: string;
5
+ readonly VITE_APP_LOCALE: string;
6
+ readonly VITE_APP_MOCK: string;
7
+ readonly VITE_APP_PORT: string;
8
+ readonly VITE_APP_API_BASE_URL: string;
9
+ }
10
+
11
+ interface ImportMeta {
12
+ readonly env: ImportMetaEnv;
13
+ }
14
+
15
+ declare module '*.svg' {
16
+ const content: string;
17
+ export default content;
18
+ }
19
+
20
+ declare module '*.png' {
21
+ const content: string;
22
+ export default content;
23
+ }
24
+
25
+ declare module '*.jpg' {
26
+ const content: string;
27
+ export default content;
28
+ }
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+
3
+ const envSchema = z.object({
4
+ VITE_APP_TITLE: z.string().default('Vue3 PC Template'),
5
+ VITE_APP_LOCALE: z.enum(['zh-CN', 'en']).default('zh-CN'),
6
+ VITE_APP_MOCK: z
7
+ .string()
8
+ .transform((v) => v === 'true')
9
+ .default('true'),
10
+ VITE_APP_PORT: z.coerce.number().int().positive().default(3000),
11
+ VITE_APP_API_BASE_URL: z.string().default('http://localhost:8080'),
12
+ });
13
+
14
+ const parsed = envSchema.safeParse(import.meta.env);
15
+
16
+ if (!parsed.success) {
17
+ console.error('[env] Invalid environment variables:');
18
+ for (const issue of parsed.error.issues) {
19
+ console.error(` - ${issue.path.join('.')}: ${issue.message}`);
20
+ }
21
+ throw new Error('Environment variable validation failed');
22
+ }
23
+
24
+