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,326 @@
1
+ <template>
2
+ <div class="admin-container">
3
+ <div class="stats-grid">
4
+ <el-card v-for="item in statCards" :key="item.key" shadow="hover" class="stat-card">
5
+ <div class="stat-content">
6
+ <div class="stat-info">
7
+ <div class="stat-label">{{ item.label }}</div>
8
+ <div class="stat-value">{{ item.value }}</div>
9
+ </div>
10
+ <div class="stat-icon" :style="{ background: item.color }">
11
+ <el-icon :size="24"><component :is="item.icon" /></el-icon>
12
+ </div>
13
+ </div>
14
+ </el-card>
15
+ </div>
16
+
17
+ <div class="admin-grid">
18
+ <el-card shadow="never" class="section-card">
19
+ <template #header>
20
+ <div class="card-header">
21
+ <span>{{ $t('admin.distributionTitle') }}</span>
22
+ </div>
23
+ </template>
24
+ <div class="pie-chart">
25
+ <svg viewBox="0 0 200 200" class="pie-svg">
26
+ <circle cx="100" cy="100" r="80" fill="none" stroke="#e8e8e8" stroke-width="40" />
27
+ <circle
28
+ cx="100" cy="100" r="80"
29
+ fill="none"
30
+ stroke="#409eff"
31
+ stroke-width="40"
32
+ :stroke-dasharray="`${adminPct * 251.2} ${251.2 - adminPct * 251.2}`"
33
+ stroke-dashoffset="62.8"
34
+ transform="rotate(-90 100 100)"
35
+ />
36
+ <circle
37
+ cx="100" cy="100" r="80"
38
+ fill="none"
39
+ stroke="#67c23a"
40
+ stroke-width="40"
41
+ :stroke-dasharray="`${userPct * 251.2} ${251.2 - userPct * 251.2}`"
42
+ :stroke-dashoffset="62.8 + (adminPct * -251.2)"
43
+ transform="rotate(-90 100 100)"
44
+ />
45
+ <circle
46
+ cx="100" cy="100" r="80"
47
+ fill="none"
48
+ stroke="#e6a23c"
49
+ stroke-width="40"
50
+ :stroke-dasharray="`${guestPct * 251.2} ${251.2 - guestPct * 251.2}`"
51
+ :stroke-dashoffset="62.8 + ((adminPct + userPct) * -251.2)"
52
+ transform="rotate(-90 100 100)"
53
+ />
54
+ </svg>
55
+ <div class="pie-legend">
56
+ <div class="legend-item">
57
+ <span class="dot" style="background: #409eff" />
58
+ <span>{{ $t('user.roleAdmin') }} ({{ adminPct }}%)</span>
59
+ </div>
60
+ <div class="legend-item">
61
+ <span class="dot" style="background: #67c23a" />
62
+ <span>{{ $t('user.roleUser') }} ({{ userPct }}%)</span>
63
+ </div>
64
+ <div class="legend-item">
65
+ <span class="dot" style="background: #e6a23c" />
66
+ <span>{{ $t('user.roleGuest') }} ({{ guestPct }}%)</span>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </el-card>
71
+
72
+ <el-card shadow="never" class="section-card">
73
+ <template #header>
74
+ <div class="card-header">
75
+ <span>{{ $t('admin.recentActivity') }}</span>
76
+ </div>
77
+ </template>
78
+ <div class="activity-list">
79
+ <div v-for="(log, i) in activityLogs" :key="i" class="activity-item">
80
+ <div class="activity-dot" :style="{ background: log.color }" />
81
+ <div class="activity-content">
82
+ <span class="activity-text">{{ log.text }}</span>
83
+ <span class="activity-time">{{ log.time }}</span>
84
+ </div>
85
+ </div>
86
+ <el-empty v-if="activityLogs.length === 0" :description="$t('common.noData')" />
87
+ </div>
88
+ </el-card>
89
+ </div>
90
+ </div>
91
+ </template>
92
+
93
+ <script setup lang="ts">
94
+ import { computed } from 'vue';
95
+ import { useI18n } from 'vue-i18n';
96
+ import { User, Key, Setting, Monitor } from '@element-plus/icons-vue';
97
+
98
+ const { t } = useI18n();
99
+
100
+ const statCards = computed(() => [
101
+ { key: 'users', label: t('admin.userTotal'), value: 128, icon: User, color: 'linear-gradient(135deg, #667eea, #764ba2)' },
102
+ { key: 'roles', label: t('admin.roleTotal'), value: 5, icon: Key, color: 'linear-gradient(135deg, #f093fb, #f5576c)' },
103
+ { key: 'perms', label: t('admin.permTotal'), value: 8, icon: Setting, color: 'linear-gradient(135deg, #4facfe, #00f2fe)' },
104
+ { key: 'sessions', label: t('admin.activeSessions'), value: 36, icon: Monitor, color: 'linear-gradient(135deg, #43e97b, #38f9d7)' },
105
+ ]);
106
+
107
+ const adminPct = 20;
108
+ const userPct = 60;
109
+ const guestPct = 20;
110
+
111
+ const activityLogs = computed(() => [
112
+ { text: t('admin.logLogin', { user: 'admin' }), time: '10:32:15', color: '#409eff' },
113
+ { text: t('admin.logCreateUser', { user: 'admin', target: 'zhangsan' }), time: '10:15:00', color: '#67c23a' },
114
+ { text: t('admin.logUpdateRole', { user: 'admin', target: 'user' }), time: '09:50:30', color: '#e6a23c' },
115
+ { text: t('admin.logDeleteUser', { user: 'admin', target: 'wangwu' }), time: '09:20:00', color: '#f56c6c' },
116
+ { text: t('admin.logLogin', { user: 'zhangsan' }), time: '09:00:00', color: '#409eff' },
117
+ ]);
118
+ </script>
119
+
120
+ <style scoped>
121
+ .admin-container {
122
+ flex: 1;
123
+ display: flex;
124
+ flex-direction: column;
125
+ gap: 16px;
126
+ }
127
+
128
+ .stats-grid {
129
+ display: grid;
130
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
131
+ gap: 16px;
132
+ }
133
+
134
+ .stat-card {
135
+ border-radius: 12px;
136
+ border: 1px solid #ebeef5;
137
+ transition: transform 0.25s ease, box-shadow 0.3s ease;
138
+ cursor: default;
139
+ }
140
+
141
+ .stat-card:hover {
142
+ transform: translateY(-4px);
143
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.08) !important;
144
+ }
145
+
146
+ .stat-content {
147
+ display: flex;
148
+ justify-content: space-between;
149
+ align-items: center;
150
+ }
151
+
152
+ .stat-info {
153
+ display: flex;
154
+ flex-direction: column;
155
+ gap: 4px;
156
+ }
157
+
158
+ .stat-label {
159
+ font-size: 13px;
160
+ color: #909399;
161
+ font-weight: 500;
162
+ }
163
+
164
+ .stat-value {
165
+ font-size: 28px;
166
+ font-weight: 700;
167
+ color: #1a1a2e;
168
+ line-height: 1.2;
169
+ }
170
+
171
+ .stat-icon {
172
+ width: 48px;
173
+ height: 48px;
174
+ border-radius: 14px;
175
+ display: flex;
176
+ align-items: center;
177
+ justify-content: center;
178
+ color: #fff;
179
+ flex-shrink: 0;
180
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
181
+ }
182
+
183
+ .admin-grid {
184
+ display: grid;
185
+ grid-template-columns: 1fr 1fr;
186
+ gap: 16px;
187
+ flex: 1;
188
+ }
189
+
190
+ @media (max-width: 800px) {
191
+ .admin-grid {
192
+ grid-template-columns: 1fr;
193
+ }
194
+
195
+ .stats-grid {
196
+ grid-template-columns: repeat(2, 1fr);
197
+ }
198
+ }
199
+
200
+ @media (max-width: 600px) {
201
+ .stats-grid {
202
+ grid-template-columns: 1fr;
203
+ }
204
+ }
205
+
206
+ .pie-chart {
207
+ display: flex;
208
+ align-items: center;
209
+ gap: 32px;
210
+ padding: 8px 0;
211
+ }
212
+
213
+ .pie-svg {
214
+ width: 140px;
215
+ height: 140px;
216
+ }
217
+
218
+ .pie-legend {
219
+ display: flex;
220
+ flex-direction: column;
221
+ gap: 14px;
222
+ }
223
+
224
+ .legend-item {
225
+ display: flex;
226
+ align-items: center;
227
+ gap: 10px;
228
+ font-size: 14px;
229
+ color: #606266;
230
+ transition: transform 0.2s ease;
231
+ }
232
+
233
+ .legend-item:hover {
234
+ transform: translateX(4px);
235
+ }
236
+
237
+ .dot {
238
+ width: 10px;
239
+ height: 10px;
240
+ border-radius: 50%;
241
+ flex-shrink: 0;
242
+ }
243
+
244
+ .activity-list {
245
+ display: flex;
246
+ flex-direction: column;
247
+ }
248
+
249
+ .activity-item {
250
+ display: flex;
251
+ gap: 12px;
252
+ padding: 14px 12px;
253
+ border-bottom: 1px solid #f0f0f0;
254
+ transition: background 0.2s ease;
255
+ border-radius: 8px;
256
+ margin: 0 -12px;
257
+ }
258
+
259
+ .activity-item:last-child {
260
+ border-bottom: none;
261
+ }
262
+
263
+ .activity-item:hover {
264
+ background: rgba(64, 158, 255, 0.04);
265
+ }
266
+
267
+ .activity-dot {
268
+ width: 8px;
269
+ height: 8px;
270
+ border-radius: 50%;
271
+ margin-top: 6px;
272
+ flex-shrink: 0;
273
+ box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.04);
274
+ }
275
+
276
+ .activity-content {
277
+ display: flex;
278
+ flex-direction: column;
279
+ gap: 4px;
280
+ flex: 1;
281
+ }
282
+
283
+ .activity-text {
284
+ font-size: 14px;
285
+ color: #303133;
286
+ }
287
+
288
+ .activity-time {
289
+ font-size: 12px;
290
+ color: #909399;
291
+ }
292
+
293
+ html.dark .stat-card,
294
+ html.dark .section-card {
295
+ background: #161b22;
296
+ border-color: #21262d;
297
+ }
298
+
299
+ html.dark .stat-value {
300
+ color: #c9d1d9;
301
+ }
302
+
303
+ html.dark .activity-item {
304
+ border-bottom-color: #21262d;
305
+ }
306
+
307
+ html.dark .activity-item:hover {
308
+ background: rgba(88, 166, 255, 0.06);
309
+ }
310
+
311
+ html.dark .activity-text {
312
+ color: #c9d1d9;
313
+ }
314
+
315
+ html.dark .activity-time {
316
+ color: #8b949e;
317
+ }
318
+
319
+ html.dark .legend-item {
320
+ color: #8b949e;
321
+ }
322
+
323
+ html.dark .activity-dot {
324
+ box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.04);
325
+ }
326
+ </style>
@@ -0,0 +1,382 @@
1
+ <template>
2
+ <div class="dashboard">
3
+ <div class="welcome-banner">
4
+ <div class="welcome-text">
5
+ <h1>{{ $t('home.greeting', { name: userInfo?.displayName || userInfo?.username || $t('user.notLoggedIn') }) }}</h1>
6
+ <p>{{ $t('home.welcome') }}</p>
7
+ </div>
8
+ <div class="welcome-time">
9
+ <span class="time">{{ currentTime }}</span>
10
+ <span class="date">{{ currentDate }}</span>
11
+ </div>
12
+ </div>
13
+
14
+ <div class="stats-grid">
15
+ <el-card v-for="stat in stats" :key="stat.key" shadow="hover" class="stat-card">
16
+ <div class="stat-content">
17
+ <div class="stat-info">
18
+ <div class="stat-label">{{ stat.label }}</div>
19
+ <div class="stat-value">{{ stat.value }}</div>
20
+ <div class="stat-trend" :class="stat.trend > 0 ? 'up' : 'down'">
21
+ {{ stat.trend > 0 ? '+' : '' }}{{ stat.trend }}%
22
+ <el-icon><Top v-if="stat.trend > 0" /><Bottom v-else /></el-icon>
23
+ </div>
24
+ </div>
25
+ <div class="stat-icon" :style="{ background: stat.color }">
26
+ <el-icon :size="28"><component :is="stat.icon" /></el-icon>
27
+ </div>
28
+ </div>
29
+ </el-card>
30
+ </div>
31
+
32
+ <div class="dashboard-grid">
33
+ <el-card shadow="never" class="chart-card">
34
+ <template #header>
35
+ <span>{{ $t('home.trendTitle') }}</span>
36
+ </template>
37
+ <div class="chart-placeholder">
38
+ <svg viewBox="0 0 400 160" class="trend-svg">
39
+ <polyline
40
+ :points="trendPoints"
41
+ fill="none"
42
+ stroke="#409eff"
43
+ stroke-width="2"
44
+ />
45
+ <path :d="trendArea" fill="rgba(64,158,255,0.1)" />
46
+ </svg>
47
+ </div>
48
+ </el-card>
49
+
50
+ <el-card shadow="never" class="quick-actions-card">
51
+ <template #header>
52
+ <span>{{ $t('home.quickActions') }}</span>
53
+ </template>
54
+ <div class="quick-actions">
55
+ <el-button
56
+ v-for="action in quickActions"
57
+ :key="action.key"
58
+ :type="action.type || 'default'"
59
+ class="action-btn"
60
+ @click="action.handler"
61
+ >
62
+ <el-icon><component :is="action.icon" /></el-icon>
63
+ <span>{{ action.label }}</span>
64
+ </el-button>
65
+ </div>
66
+ </el-card>
67
+ </div>
68
+ </div>
69
+ </template>
70
+
71
+ <script setup lang="ts">
72
+ import { ref, computed, onMounted, onUnmounted } from 'vue';
73
+ import { useRouter } from 'vue-router';
74
+ import { useUserStore } from '@/store/modules/user';
75
+ import { storeToRefs } from 'pinia';
76
+ import { useI18n } from 'vue-i18n';
77
+ import { User, Setting, Key, Top, Bottom, Plus, Refresh } from '@element-plus/icons-vue';
78
+
79
+ const { t, locale } = useI18n();
80
+ const router = useRouter();
81
+ const userStore = useUserStore();
82
+ const { userInfo } = storeToRefs(userStore);
83
+
84
+ const currentTime = ref('');
85
+ const currentDate = ref('');
86
+ let timer: ReturnType<typeof setInterval>;
87
+
88
+ function updateTime() {
89
+ const lang = locale.value === 'en' ? 'en' : 'zh-CN';
90
+ currentTime.value = new Date().toLocaleTimeString(lang, { hour12: false });
91
+ currentDate.value = new Date().toLocaleDateString(lang, {
92
+ year: 'numeric', month: 'long', day: 'numeric', weekday: 'long',
93
+ });
94
+ }
95
+
96
+ onMounted(() => {
97
+ updateTime();
98
+ timer = setInterval(updateTime, 1000);
99
+ });
100
+
101
+ onUnmounted(() => {
102
+ if (timer) clearInterval(timer);
103
+ });
104
+
105
+ const stats = computed(() => [
106
+ { key: 'users', label: t('admin.userTotal'), value: 128, trend: 12, icon: User, color: 'linear-gradient(135deg, #667eea, #764ba2)' },
107
+ { key: 'roles', label: t('admin.roleTotal'), value: 5, trend: 0, icon: Key, color: 'linear-gradient(135deg, #f093fb, #f5576c)' },
108
+ { key: 'perms', label: t('admin.permTotal'), value: 8, trend: 0, icon: Setting, color: 'linear-gradient(135deg, #4facfe, #00f2fe)' },
109
+ { key: 'active', label: t('home.activeToday'), value: 36, trend: -5, icon: Refresh, color: 'linear-gradient(135deg, #43e97b, #38f9d7)' },
110
+ ]);
111
+
112
+ const trendData = [20, 45, 30, 60, 50, 75, 65, 90, 80, 95, 85, 100];
113
+ const trendW = 400;
114
+ const trendH = 150;
115
+ const trendStep = trendW / (trendData.length - 1);
116
+ const trendPoints = computed(() =>
117
+ trendData.map((v, i) => `${i * trendStep},${trendH - (v / 100) * trendH}`).join(' ')
118
+ );
119
+ const trendArea = computed(() => {
120
+ const pts = trendData.map((v, i) => `${i * trendStep},${trendH - (v / 100) * trendH}`);
121
+ return `M${pts.join(' L')} L${trendW},${trendH} L0,${trendH} Z`;
122
+ });
123
+
124
+ const quickActions = computed(() => [
125
+ { key: 'addUser', label: t('user.addUser'), icon: Plus, type: 'primary' as const, handler: () => router.push('/user') },
126
+ { key: 'manageRole', label: t('role.addRole'), icon: Key, type: 'success' as const, handler: () => router.push('/role') },
127
+ { key: 'sysAdmin', label: t('menu.admin'), icon: Setting, type: 'warning' as const, handler: () => router.push('/admin') },
128
+ ]);
129
+ </script>
130
+
131
+ <style scoped>
132
+ .dashboard {
133
+ flex: 1;
134
+ display: flex;
135
+ flex-direction: column;
136
+ gap: 16px;
137
+ }
138
+
139
+ .welcome-banner {
140
+ display: flex;
141
+ justify-content: space-between;
142
+ align-items: center;
143
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
144
+ padding: 24px 32px;
145
+ color: #fff;
146
+ border-radius: 12px;
147
+ position: relative;
148
+ overflow: hidden;
149
+ }
150
+
151
+ .welcome-banner::before {
152
+ content: '';
153
+ position: absolute;
154
+ top: -60%;
155
+ right: -5%;
156
+ width: 400px;
157
+ height: 400px;
158
+ border-radius: 50%;
159
+ background: rgba(255, 255, 255, 0.06);
160
+ }
161
+
162
+ .welcome-banner::after {
163
+ content: '';
164
+ position: absolute;
165
+ bottom: -30%;
166
+ right: 15%;
167
+ width: 200px;
168
+ height: 200px;
169
+ border-radius: 50%;
170
+ background: rgba(255, 255, 255, 0.04);
171
+ }
172
+
173
+ .welcome-text {
174
+ position: relative;
175
+ z-index: 1;
176
+ }
177
+
178
+ .welcome-text h1 {
179
+ margin: 0 0 4px;
180
+ font-size: 22px;
181
+ font-weight: 600;
182
+ }
183
+
184
+ .welcome-text p {
185
+ margin: 0;
186
+ font-size: 14px;
187
+ opacity: 0.8;
188
+ }
189
+
190
+ .welcome-time {
191
+ text-align: right;
192
+ position: relative;
193
+ z-index: 1;
194
+ }
195
+
196
+ .welcome-time .time {
197
+ display: block;
198
+ font-size: 26px;
199
+ font-weight: 300;
200
+ letter-spacing: 1px;
201
+ }
202
+
203
+ .welcome-time .date {
204
+ display: block;
205
+ font-size: 13px;
206
+ opacity: 0.75;
207
+ margin-top: 4px;
208
+ }
209
+
210
+ .stats-grid {
211
+ display: grid;
212
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
213
+ gap: 16px;
214
+ }
215
+
216
+ .stat-card {
217
+ border-radius: 12px;
218
+ border: 1px solid #ebeef5;
219
+ transition: transform 0.25s ease, box-shadow 0.3s ease;
220
+ cursor: default;
221
+ }
222
+
223
+ .stat-card:hover {
224
+ transform: translateY(-4px);
225
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.08) !important;
226
+ }
227
+
228
+ .stat-content {
229
+ display: flex;
230
+ justify-content: space-between;
231
+ align-items: center;
232
+ }
233
+
234
+ .stat-label {
235
+ font-size: 14px;
236
+ color: #909399;
237
+ margin-bottom: 4px;
238
+ font-weight: 500;
239
+ }
240
+
241
+ .stat-value {
242
+ font-size: 32px;
243
+ font-weight: 700;
244
+ color: #1a1a2e;
245
+ line-height: 1.2;
246
+ margin-bottom: 6px;
247
+ }
248
+
249
+ .stat-trend {
250
+ font-size: 13px;
251
+ display: flex;
252
+ align-items: center;
253
+ gap: 4px;
254
+ font-weight: 500;
255
+ }
256
+
257
+ .stat-trend.up {
258
+ color: #52c41a;
259
+ }
260
+
261
+ .stat-trend.down {
262
+ color: #ff4d4f;
263
+ }
264
+
265
+ .stat-icon {
266
+ width: 52px;
267
+ height: 52px;
268
+ border-radius: 14px;
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: center;
272
+ color: #fff;
273
+ flex-shrink: 0;
274
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
275
+ }
276
+
277
+ .dashboard-grid {
278
+ display: grid;
279
+ grid-template-columns: 2fr 1fr;
280
+ gap: 16px;
281
+ flex: 1;
282
+ }
283
+
284
+ .chart-card {
285
+ border-radius: 12px;
286
+ border: 1px solid #ebeef5;
287
+ }
288
+
289
+ .quick-actions-card {
290
+ border-radius: 12px;
291
+ border: 1px solid #ebeef5;
292
+ }
293
+
294
+ .chart-placeholder {
295
+ height: 200px;
296
+ display: flex;
297
+ align-items: flex-end;
298
+ }
299
+
300
+ .trend-svg {
301
+ width: 100%;
302
+ height: 180px;
303
+ }
304
+
305
+ .quick-actions {
306
+ display: flex;
307
+ flex-direction: column;
308
+ gap: 10px;
309
+ }
310
+
311
+ .quick-actions :deep(.el-button + .el-button) {
312
+ margin-left: 0;
313
+ }
314
+
315
+ .action-btn {
316
+ justify-content: flex-start;
317
+ padding: 14px 16px;
318
+ height: auto;
319
+ gap: 10px;
320
+ font-size: 14px;
321
+ border-radius: 10px;
322
+ border: 1px solid #ebeef5;
323
+ transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s;
324
+ }
325
+
326
+ .action-btn:hover {
327
+ transform: translateX(4px);
328
+ border-color: #409eff;
329
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.1);
330
+ }
331
+
332
+ @media (max-width: 900px) {
333
+ .dashboard-grid {
334
+ grid-template-columns: 1fr;
335
+ }
336
+
337
+ .welcome-banner {
338
+ flex-direction: column;
339
+ gap: 12px;
340
+ align-items: flex-start;
341
+ }
342
+
343
+ .welcome-time {
344
+ text-align: left;
345
+ }
346
+
347
+ .stats-grid {
348
+ grid-template-columns: repeat(2, 1fr);
349
+ }
350
+ }
351
+
352
+ @media (max-width: 600px) {
353
+ .stats-grid {
354
+ grid-template-columns: 1fr;
355
+ }
356
+
357
+ .welcome-text h1 {
358
+ font-size: 18px;
359
+ }
360
+
361
+ .welcome-time .time {
362
+ font-size: 20px;
363
+ }
364
+ }
365
+
366
+ html.dark .stat-value {
367
+ color: #c9d1d9;
368
+ }
369
+
370
+ html.dark .stat-label {
371
+ color: #8b949e;
372
+ }
373
+
374
+ html.dark .action-btn {
375
+ border-color: #30363d;
376
+ }
377
+
378
+ html.dark .action-btn:hover {
379
+ border-color: #58a6ff;
380
+ box-shadow: 0 4px 12px rgba(88, 166, 255, 0.15);
381
+ }
382
+ </style>