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,269 @@
1
+ <template>
2
+ <div class="role-container">
3
+ <el-card shadow="never">
4
+ <div class="toolbar">
5
+ <el-input
6
+ v-model="keyword"
7
+ :placeholder="$t('role.searchPlaceholder')"
8
+ clearable
9
+ style="width: 300px"
10
+ @change="handleSearch"
11
+ >
12
+ <template #prefix>
13
+ <el-icon><Search /></el-icon>
14
+ </template>
15
+ </el-input>
16
+ <div class="toolbar-right">
17
+ <el-button v-if="selectedIds.length" type="danger" @click="handleBatchDelete">
18
+ <el-icon><Delete /></el-icon>
19
+ {{ $t('common.batchDelete') }} ({{ selectedIds.length }})
20
+ </el-button>
21
+ <el-button type="primary" @click="openAdd">
22
+ <el-icon><Plus /></el-icon>
23
+ {{ $t('role.addRole') }}
24
+ </el-button>
25
+ </div>
26
+ </div>
27
+
28
+ <el-table
29
+ :data="list"
30
+ v-loading="loading"
31
+ stripe
32
+ style="width: 100%"
33
+ @selection-change="onSelectionChange"
34
+ >
35
+ <el-table-column type="selection" width="50" />
36
+ <el-table-column prop="name" :label="$t('role.name')" min-width="130" />
37
+ <el-table-column prop="code" :label="$t('role.code')" min-width="130" />
38
+ <el-table-column prop="description" :label="$t('role.description')" min-width="200" />
39
+ <el-table-column prop="permissions" :label="$t('role.permissions')" min-width="280">
40
+ <template #default="{ row }">
41
+ <el-tag
42
+ v-for="perm in row.permissions"
43
+ :key="perm"
44
+ size="small"
45
+ style="margin-right: 5px; margin-bottom: 5px"
46
+ >
47
+ {{ getPermissionName(perm) }}
48
+ </el-tag>
49
+ </template>
50
+ </el-table-column>
51
+ <el-table-column prop="createTime" :label="$t('role.createTime')" min-width="160" />
52
+ <el-table-column :label="$t('common.actions')" width="200" fixed="right">
53
+ <template #default="{ row }">
54
+ <el-button type="primary" link @click="openEdit(row)">{{ $t('common.edit') }}</el-button>
55
+ <el-button type="danger" link @click="handleDelete(row)">{{ $t('common.delete') }}</el-button>
56
+ </template>
57
+ </el-table-column>
58
+ </el-table>
59
+
60
+ <div v-if="!loading && list.length === 0" style="padding: 40px 0">
61
+ <el-empty :description="$t('common.noData')" />
62
+ </div>
63
+
64
+ <el-pagination
65
+ v-model:current-page="page"
66
+ v-model:page-size="pageSize"
67
+ :page-sizes="[10, 20, 50, 100]"
68
+ :total="total"
69
+ layout="total, sizes, prev, pager, next, jumper"
70
+ @size-change="handleSizeChange"
71
+ @current-change="handleCurrentChange"
72
+ style="margin-top: 20px; justify-content: flex-end"
73
+ />
74
+ </el-card>
75
+
76
+ <el-dialog v-model="dialogVisible" :title="isEdit ? $t('role.editRole') : $t('role.addRole')" width="600px">
77
+ <el-form :model="form" :rules="formRules" ref="formRef" label-width="80px">
78
+ <el-form-item :label="$t('role.name')" prop="name">
79
+ <el-input v-model="form.name" />
80
+ </el-form-item>
81
+ <el-form-item :label="$t('role.code')" prop="code">
82
+ <el-input v-model="form.code" :disabled="isEdit" />
83
+ </el-form-item>
84
+ <el-form-item :label="$t('role.description')" prop="description">
85
+ <el-input v-model="form.description" type="textarea" :rows="3" />
86
+ </el-form-item>
87
+ <el-form-item :label="$t('role.permissions')" prop="permissions">
88
+ <el-checkbox-group v-model="form.permissions">
89
+ <el-checkbox v-for="perm in allPermissions" :key="perm.code" :value="perm.code">
90
+ {{ perm.name }}
91
+ </el-checkbox>
92
+ </el-checkbox-group>
93
+ </el-form-item>
94
+ </el-form>
95
+ <template #footer>
96
+ <el-button @click="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
97
+ <el-button type="primary" :loading="loading" @click="handleSubmit">{{ $t('common.confirm') }}</el-button>
98
+ </template>
99
+ </el-dialog>
100
+ </div>
101
+ </template>
102
+
103
+ <script setup lang="ts">
104
+ import { onMounted, computed } from 'vue';
105
+ import { useI18n } from 'vue-i18n';
106
+ import { Search, Plus, Delete } from '@element-plus/icons-vue';
107
+ import { useCrud } from '@/composables/useCrud';
108
+
109
+ interface RoleForm {
110
+ id: string;
111
+ name: string;
112
+ code: string;
113
+ description: string;
114
+ permissions: string[];
115
+ }
116
+
117
+ const { t } = useI18n();
118
+
119
+ const formRules = computed(() => ({
120
+ name: [{ required: true, message: t('role.requiredName'), trigger: 'blur' }],
121
+ code: [{ required: true, message: t('role.requiredCode'), trigger: 'blur' }],
122
+ }));
123
+
124
+ const allPermissions = computed(() => [
125
+ { name: t('role.permUserView'), code: 'user:view' },
126
+ { name: t('role.permUserAdd'), code: 'user:add' },
127
+ { name: t('role.permUserEdit'), code: 'user:edit' },
128
+ { name: t('role.permUserDelete'), code: 'user:delete' },
129
+ { name: t('role.permRoleView'), code: 'role:view' },
130
+ { name: t('role.permRoleAdd'), code: 'role:add' },
131
+ { name: t('role.permRoleEdit'), code: 'role:edit' },
132
+ { name: t('role.permRoleDelete'), code: 'role:delete' },
133
+ ]);
134
+
135
+ const crud = useCrud<RoleForm>({
136
+ url: '/roles',
137
+ name: 'role',
138
+ formDefaults: {
139
+ id: '',
140
+ name: '',
141
+ code: '',
142
+ description: '',
143
+ permissions: [],
144
+ },
145
+ rules: formRules.value,
146
+ });
147
+
148
+ const {
149
+ list,
150
+ loading,
151
+ page,
152
+ pageSize,
153
+ total,
154
+ keyword,
155
+ dialogVisible,
156
+ isEdit,
157
+ selectedIds,
158
+ formRef,
159
+ form,
160
+ fetchData,
161
+ handleSearch,
162
+ handleSizeChange,
163
+ handleCurrentChange,
164
+ openAdd,
165
+ openEdit,
166
+ handleDelete,
167
+ handleBatchDelete,
168
+ handleSubmit,
169
+ } = crud;
170
+
171
+ const getPermissionName = (code: string) => {
172
+ const perm = allPermissions.value.find((p) => p.code === code);
173
+ return perm ? perm.name : code;
174
+ };
175
+
176
+ const onSelectionChange = (rows: any[]) => {
177
+ selectedIds.value = rows.map((r: any) => r.id);
178
+ };
179
+
180
+ onMounted(() => fetchData());
181
+ </script>
182
+
183
+ <style scoped>
184
+ .role-container {
185
+ flex: 1;
186
+ display: flex;
187
+ flex-direction: column;
188
+ }
189
+
190
+ .toolbar {
191
+ display: flex;
192
+ justify-content: space-between;
193
+ align-items: center;
194
+ margin-bottom: 16px;
195
+ flex-wrap: wrap;
196
+ gap: 12px;
197
+ }
198
+
199
+ .toolbar-right {
200
+ display: flex;
201
+ align-items: center;
202
+ gap: 12px;
203
+ }
204
+
205
+ :deep(.el-card) {
206
+ border-radius: 12px;
207
+ width: 100%;
208
+ }
209
+
210
+ :deep(.el-table) {
211
+ width: 100%;
212
+ }
213
+
214
+ :deep(.el-table th.el-table__cell) {
215
+ font-weight: 600;
216
+ }
217
+
218
+ :deep(.el-table__body tr) {
219
+ transition: background 0.2s ease;
220
+ }
221
+
222
+ :deep(.el-table__body tr:hover) {
223
+ background: rgba(64, 158, 255, 0.04) !important;
224
+ }
225
+
226
+ :deep(.el-table--striped .el-table__body tr.el-table__row--striped:hover) {
227
+ background: rgba(64, 158, 255, 0.06) !important;
228
+ }
229
+
230
+ :deep(.el-pagination) {
231
+ padding: 16px 0 4px;
232
+ }
233
+
234
+ :deep(.el-dialog) {
235
+ border-radius: 12px;
236
+ }
237
+
238
+ :deep(.el-dialog__header) {
239
+ margin-right: 0;
240
+ padding: 20px 24px 0;
241
+ }
242
+
243
+ :deep(.el-dialog__body) {
244
+ padding: 20px 24px;
245
+ }
246
+
247
+ :deep(.el-dialog__footer) {
248
+ padding: 0 24px 20px;
249
+ border-top: none;
250
+ }
251
+
252
+ :deep(.el-checkbox-group) {
253
+ display: flex;
254
+ flex-wrap: wrap;
255
+ gap: 8px;
256
+ }
257
+
258
+ html.dark .toolbar :deep(.el-input__wrapper) {
259
+ background: #161b22;
260
+ box-shadow: 0 0 0 1px #30363d inset;
261
+ }
262
+
263
+
264
+
265
+ html.dark :deep(.el-dialog .el-input__wrapper) {
266
+ background: #0d1117;
267
+ box-shadow: 0 0 0 1px #30363d inset;
268
+ }
269
+ </style>
@@ -0,0 +1,332 @@
1
+ <template>
2
+ <div class="user-container">
3
+ <el-card shadow="never">
4
+ <div class="toolbar">
5
+ <el-input
6
+ v-model="keyword"
7
+ :placeholder="$t('user.searchPlaceholder')"
8
+ clearable
9
+ style="width: 300px"
10
+ @change="handleSearch"
11
+ >
12
+ <template #prefix>
13
+ <el-icon><Search /></el-icon>
14
+ </template>
15
+ </el-input>
16
+ <div class="toolbar-right">
17
+ <el-button v-if="selectedIds.length" type="danger" @click="handleBatchDelete">
18
+ <el-icon><Delete /></el-icon>
19
+ {{ $t('common.batchDelete') }} ({{ selectedIds.length }})
20
+ </el-button>
21
+ <el-button @click="exportData">
22
+ <el-icon><Download /></el-icon>
23
+ {{ $t('common.export') }}
24
+ </el-button>
25
+ <el-button type="primary" @click="openAdd">
26
+ <el-icon><Plus /></el-icon>
27
+ {{ $t('user.addUser') }}
28
+ </el-button>
29
+ </div>
30
+ </div>
31
+
32
+ <el-table
33
+ :data="list"
34
+ v-loading="loading"
35
+ stripe
36
+ style="width: 100%"
37
+ @selection-change="onSelectionChange"
38
+ >
39
+ <el-table-column type="selection" width="50" />
40
+ <el-table-column prop="username" :label="$t('user.username')" min-width="120" />
41
+ <el-table-column prop="name" :label="$t('user.name')" min-width="120" />
42
+ <el-table-column prop="email" :label="$t('user.email')" min-width="160" />
43
+ <el-table-column prop="phone" :label="$t('user.phone')" min-width="120" />
44
+ <el-table-column prop="role" :label="$t('user.role')" min-width="90">
45
+ <template #default="{ row }">
46
+ <el-tag :type="getRoleTagType(row.role)">
47
+ {{ getRoleName(row.role) }}
48
+ </el-tag>
49
+ </template>
50
+ </el-table-column>
51
+ <el-table-column prop="status" :label="$t('user.status')" min-width="70">
52
+ <template #default="{ row }">
53
+ <el-tag :type="row.status === 1 ? 'success' : 'danger'">
54
+ {{ row.status === 1 ? $t('common.enable') : $t('common.disable') }}
55
+ </el-tag>
56
+ </template>
57
+ </el-table-column>
58
+ <el-table-column prop="createTime" :label="$t('user.createTime')" min-width="160" />
59
+ <el-table-column :label="$t('common.actions')" width="200" fixed="right">
60
+ <template #default="{ row }">
61
+ <el-button type="primary" link @click="openEdit(row)">{{ $t('common.edit') }}</el-button>
62
+ <el-button type="danger" link @click="handleDelete(row)">{{ $t('common.delete') }}</el-button>
63
+ </template>
64
+ </el-table-column>
65
+ </el-table>
66
+
67
+ <div v-if="!loading && list.length === 0" style="padding: 40px 0">
68
+ <el-empty :description="$t('common.noData')" />
69
+ </div>
70
+
71
+ <el-pagination
72
+ v-model:current-page="page"
73
+ v-model:page-size="pageSize"
74
+ :page-sizes="[10, 20, 50, 100]"
75
+ :total="total"
76
+ layout="total, sizes, prev, pager, next, jumper"
77
+ @size-change="handleSizeChange"
78
+ @current-change="handleCurrentChange"
79
+ style="margin-top: 20px; justify-content: flex-end"
80
+ />
81
+ </el-card>
82
+
83
+ <el-dialog v-model="dialogVisible" :title="isEdit ? $t('user.editUser') : $t('user.addUser')" width="500px">
84
+ <el-form :model="form" :rules="formRules" ref="formRef" label-width="80px">
85
+ <el-form-item :label="$t('user.username')" prop="username">
86
+ <el-input v-model="form.username" :disabled="isEdit" />
87
+ </el-form-item>
88
+ <el-form-item :label="$t('user.name')" prop="name">
89
+ <el-input v-model="form.name" />
90
+ </el-form-item>
91
+ <el-form-item :label="$t('user.email')" prop="email">
92
+ <el-input v-model="form.email" />
93
+ </el-form-item>
94
+ <el-form-item :label="$t('user.phone')" prop="phone">
95
+ <el-input v-model="form.phone" />
96
+ </el-form-item>
97
+ <el-form-item :label="$t('user.role')" prop="role">
98
+ <el-select v-model="form.role" style="width: 100%">
99
+ <el-option v-for="r in roleOptions" :key="r.code" :label="r.name" :value="r.code" />
100
+ </el-select>
101
+ </el-form-item>
102
+ <el-form-item :label="$t('user.status')" prop="status">
103
+ <el-radio-group v-model="form.status">
104
+ <el-radio :value="1">{{ $t('common.enable') }}</el-radio>
105
+ <el-radio :value="0">{{ $t('common.disable') }}</el-radio>
106
+ </el-radio-group>
107
+ </el-form-item>
108
+ </el-form>
109
+ <template #footer>
110
+ <el-button @click="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
111
+ <el-button type="primary" :loading="loading" @click="handleSubmit">{{ $t('common.confirm') }}</el-button>
112
+ </template>
113
+ </el-dialog>
114
+ </div>
115
+ </template>
116
+
117
+ <script setup lang="ts">
118
+ import { ref, computed, onMounted } from 'vue';
119
+ import { useI18n } from 'vue-i18n';
120
+ import { Search, Plus, Delete, Download } from '@element-plus/icons-vue';
121
+ import { useCrud } from '@/composables/useCrud';
122
+ import http from '@/services/http';
123
+
124
+ interface UserForm {
125
+ id: string;
126
+ username: string;
127
+ name: string;
128
+ email: string;
129
+ phone: string;
130
+ role: string;
131
+ status: number;
132
+ }
133
+
134
+ const { t } = useI18n();
135
+
136
+ const formRules = computed(() => ({
137
+ username: [{ required: true, message: t('user.requiredUsername'), trigger: 'blur' }],
138
+ name: [{ required: true, message: t('user.requiredName'), trigger: 'blur' }],
139
+ email: [
140
+ { required: true, message: t('user.requiredEmail'), trigger: 'blur' },
141
+ { type: 'email', message: t('user.invalidEmail'), trigger: 'blur' },
142
+ ],
143
+ }));
144
+
145
+ const crud = useCrud<UserForm>({
146
+ url: '/users',
147
+ name: 'user',
148
+ formDefaults: {
149
+ id: '',
150
+ username: '',
151
+ name: '',
152
+ email: '',
153
+ phone: '',
154
+ role: 'user',
155
+ status: 1,
156
+ },
157
+ rules: formRules.value,
158
+ });
159
+
160
+ const {
161
+ list,
162
+ loading,
163
+ page,
164
+ pageSize,
165
+ total,
166
+ keyword,
167
+ dialogVisible,
168
+ isEdit,
169
+ selectedIds,
170
+ formRef,
171
+ form,
172
+ fetchData,
173
+ handleSearch,
174
+ handleSizeChange,
175
+ handleCurrentChange,
176
+ openAdd,
177
+ openEdit,
178
+ handleDelete,
179
+ handleBatchDelete,
180
+ handleSubmit,
181
+ } = crud;
182
+
183
+ interface RoleOption {
184
+ code: string;
185
+ name: string;
186
+ }
187
+
188
+ const roleOptions = ref<RoleOption[]>([]);
189
+
190
+ const fetchRoles = async () => {
191
+ try {
192
+ const res: any = await http.get('/roles', { params: { all: true } });
193
+ if (res.code === 0 || res.code === 200) {
194
+ roleOptions.value = res.data;
195
+ }
196
+ } catch {
197
+ // fallback
198
+ }
199
+ };
200
+
201
+ const getRoleName = (roleCode: string) => {
202
+ const found = roleOptions.value.find((r: any) => r.code === roleCode);
203
+ return found ? found.name : roleCode;
204
+ };
205
+
206
+ const getRoleTagType = (roleCode: string) => {
207
+ if (roleCode === 'admin') return 'danger';
208
+ if (roleCode === 'guest') return 'info';
209
+ return 'primary';
210
+ };
211
+
212
+ const onSelectionChange = (rows: any[]) => {
213
+ selectedIds.value = rows.map((r: any) => r.id);
214
+ };
215
+
216
+ const csvEscape = (val: any) => {
217
+ const s = String(val ?? '');
218
+ if (s.includes(',') || s.includes('"') || s.includes('\n')) {
219
+ return `"${s.replace(/"/g, '""')}"`;
220
+ }
221
+ return s;
222
+ };
223
+
224
+ const exportData = async () => {
225
+ try {
226
+ const res: any = await http.get('/users/export');
227
+ if (res.code === 0 || res.code === 200) {
228
+ const exportList = res.data.list;
229
+ const headers = ['username', 'name', 'email', 'phone', 'roleName', 'status', 'createTime'];
230
+ const labels = headers.map((h) => t(`user.${h === 'roleName' ? 'role' : h}`));
231
+ const rows = exportList.map((item: any) => headers.map((h) => csvEscape(item[h])));
232
+ const csv = [labels.join(','), ...rows.map((r: any[]) => r.join(','))].join('\n');
233
+ const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
234
+ const url = URL.createObjectURL(blob);
235
+ const a = document.createElement('a');
236
+ a.href = url;
237
+ a.download = `users_${new Date().toISOString().slice(0, 10)}.csv`;
238
+ a.click();
239
+ URL.revokeObjectURL(url);
240
+ }
241
+ } catch {
242
+ ElMessage.error(t('common.error'));
243
+ }
244
+ };
245
+
246
+ onMounted(() => {
247
+ fetchData();
248
+ fetchRoles();
249
+ });
250
+ </script>
251
+
252
+ <style scoped>
253
+ .user-container {
254
+ flex: 1;
255
+ display: flex;
256
+ flex-direction: column;
257
+ }
258
+
259
+ .toolbar {
260
+ display: flex;
261
+ justify-content: space-between;
262
+ align-items: center;
263
+ margin-bottom: 16px;
264
+ flex-wrap: wrap;
265
+ gap: 12px;
266
+ }
267
+
268
+ .toolbar-right {
269
+ display: flex;
270
+ align-items: center;
271
+ gap: 12px;
272
+ }
273
+
274
+ :deep(.el-card) {
275
+ border-radius: 12px;
276
+ width: 100%;
277
+ }
278
+
279
+ :deep(.el-table) {
280
+ width: 100%;
281
+ }
282
+
283
+ :deep(.el-table th.el-table__cell) {
284
+ font-weight: 600;
285
+ }
286
+
287
+ :deep(.el-table__body tr) {
288
+ transition: background 0.2s ease;
289
+ }
290
+
291
+ :deep(.el-table__body tr:hover) {
292
+ background: rgba(64, 158, 255, 0.04) !important;
293
+ }
294
+
295
+ :deep(.el-table--striped .el-table__body tr.el-table__row--striped:hover) {
296
+ background: rgba(64, 158, 255, 0.06) !important;
297
+ }
298
+
299
+ :deep(.el-pagination) {
300
+ padding: 16px 0 4px;
301
+ }
302
+
303
+ :deep(.el-dialog) {
304
+ border-radius: 12px;
305
+ }
306
+
307
+ :deep(.el-dialog__header) {
308
+ margin-right: 0;
309
+ padding: 20px 24px 0;
310
+ }
311
+
312
+ :deep(.el-dialog__body) {
313
+ padding: 20px 24px;
314
+ }
315
+
316
+ :deep(.el-dialog__footer) {
317
+ padding: 0 24px 20px;
318
+ border-top: none;
319
+ }
320
+
321
+ html.dark .toolbar :deep(.el-input__wrapper) {
322
+ background: #161b22;
323
+ box-shadow: 0 0 0 1px #30363d inset;
324
+ }
325
+
326
+
327
+
328
+ html.dark :deep(.el-dialog .el-input__wrapper) {
329
+ background: #0d1117;
330
+ box-shadow: 0 0 0 1px #30363d inset;
331
+ }
332
+ </style>
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <div class="error-page">
3
+ <div class="error-content">
4
+ <div class="error-icon">
5
+ <svg viewBox="0 0 80 80" width="80" height="80">
6
+ <rect x="10" y="30" width="60" height="40" rx="4" fill="none" stroke="#e6a23c" stroke-width="3" opacity="0.2" />
7
+ <rect x="10" y="30" width="60" height="40" rx="4" fill="none" stroke="#e6a23c" stroke-width="3" />
8
+ <line x1="22" y1="42" x2="58" y2="42" stroke="#e6a23c" stroke-width="2" />
9
+ <line x1="22" y1="50" x2="48" y2="50" stroke="#e6a23c" stroke-width="2" />
10
+ <line x1="22" y1="58" x2="38" y2="58" stroke="#e6a23c" stroke-width="2" />
11
+ <text x="65" y="24" text-anchor="middle" font-size="18" font-weight="700" fill="#e6a23c">403</text>
12
+ </svg>
13
+ </div>
14
+ <h1 class="error-title">{{ $t('error.forbiddenTitle') }}</h1>
15
+ <p class="error-desc">{{ $t('error.forbiddenDesc') }}</p>
16
+ <el-button type="primary" round @click="$router.push('/')">
17
+ {{ $t('error.backHome') }}
18
+ </el-button>
19
+ </div>
20
+ </div>
21
+ </template>
22
+
23
+ <style scoped>
24
+ .error-page {
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ min-height: 100%;
29
+ padding: 40px;
30
+ }
31
+
32
+ .error-content {
33
+ text-align: center;
34
+ max-width: 420px;
35
+ }
36
+
37
+ .error-icon {
38
+ margin-bottom: 24px;
39
+ }
40
+
41
+ .error-title {
42
+ font-size: 24px;
43
+ font-weight: 600;
44
+ color: #1a1a2e;
45
+ margin: 0 0 12px;
46
+ }
47
+
48
+ .error-desc {
49
+ font-size: 14px;
50
+ color: #909399;
51
+ margin: 0 0 32px;
52
+ line-height: 1.6;
53
+ }
54
+
55
+ html.dark .error-title {
56
+ color: #c9d1d9;
57
+ }
58
+
59
+ html.dark .error-desc {
60
+ color: #8b949e;
61
+ }
62
+ </style>