@deppon/create-deppon-app 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.
Files changed (57) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +63 -0
  3. package/deppon.js +640 -0
  4. package/package.json +51 -0
  5. package/template/.env +12 -0
  6. package/template/.env.dev-local.example +64 -0
  7. package/template/.env.development.example +64 -0
  8. package/template/.env.example +1 -0
  9. package/template/.env.production.example +64 -0
  10. package/template/.env.test.example +64 -0
  11. package/template/.eslintignore +2 -0
  12. package/template/.eslintrc.cjs +14 -0
  13. package/template/.prettierrc.js +3 -0
  14. package/template/.vscode/settings.json +8 -0
  15. package/template/Dockerfile +5 -0
  16. package/template/README.md +149 -0
  17. package/template/commitlint.config.js +11 -0
  18. package/template/gitignore +8 -0
  19. package/template/index.html +18 -0
  20. package/template/nginx.conf +70 -0
  21. package/template/npmrc +2 -0
  22. package/template/package.json +49 -0
  23. package/template/preview-server.js +117 -0
  24. package/template/public/favicon.ico +0 -0
  25. package/template/public/logo.png +0 -0
  26. package/template/src/App.vue +123 -0
  27. package/template/src/api/index.ts +13 -0
  28. package/template/src/api/prefercenter.ts +23 -0
  29. package/template/src/api/product.ts +16 -0
  30. package/template/src/api/user.ts +41 -0
  31. package/template/src/components/ExpandableMessage.vue +340 -0
  32. package/template/src/components/PageLayout.vue +43 -0
  33. package/template/src/config/dictionaryConfig.ts +24 -0
  34. package/template/src/directives/permission.ts +162 -0
  35. package/template/src/layouts/BaseLayout.vue +687 -0
  36. package/template/src/main.ts +27 -0
  37. package/template/src/router/index.ts +179 -0
  38. package/template/src/router/route.ts +61 -0
  39. package/template/src/stores/menu.ts +334 -0
  40. package/template/src/stores/product.ts +155 -0
  41. package/template/src/stores/route.ts +79 -0
  42. package/template/src/stores/user.ts +145 -0
  43. package/template/src/styles/index.ts +29 -0
  44. package/template/src/types/dictionary.d.ts +24 -0
  45. package/template/src/types/vite-env.d.ts +119 -0
  46. package/template/src/utils/dictionary.ts +188 -0
  47. package/template/src/utils/errorAnalyzer.ts +217 -0
  48. package/template/src/utils/messageVNode.ts +15 -0
  49. package/template/src/utils/request.ts +293 -0
  50. package/template/src/views/error/401.vue +30 -0
  51. package/template/src/views/error/403.vue +30 -0
  52. package/template/src/views/error/404.vue +30 -0
  53. package/template/src/views/home/index.vue +25 -0
  54. package/template/tsconfig.json +27 -0
  55. package/template/vite.config.ts +243 -0
  56. package/template/yarnrc +3 -0
  57. package/template/yarnrc.yml +7 -0
@@ -0,0 +1,687 @@
1
+ <template>
2
+ <pro-layout
3
+ :collapsed="collapsed"
4
+ :menu-items="menuItems"
5
+ :top-menu-items="topMenuItems"
6
+ :active-menu="activeMenu"
7
+ :commonMenusMax="commonMenusMax"
8
+ :showCommonMenu="showCommonMenu"
9
+ :menu-router="true"
10
+ :title="props.title"
11
+ :logo="props.logo"
12
+ :show-common-menus="false"
13
+ :fixed-header="props.fixedHeader"
14
+ @update:collapsed="collapsed = $event"
15
+ @common-menu-delete="handleCommonMenuDelete"
16
+ >
17
+ <template #header-right>
18
+ <el-dropdown trigger="click" placement="bottom-end" :hide-on-click="false" popper-class="header-user-dropdown-popper">
19
+ <div class="header-user-trigger">
20
+ <span>Hi,{{ userStore.profile?.userName ?? '未登录' }}</span>
21
+ <el-icon>
22
+ <component :is="ArrowDownIcon" />
23
+ </el-icon>
24
+ </div>
25
+ <template #dropdown>
26
+ <div class="header-user-dropdown">
27
+ <div class="user-dropdown-header">
28
+ <div class="user-avatar">{{ avatarText }}</div>
29
+ <div class="user-header-info">
30
+ <div class="user-name">{{ userStore.profile?.userName ?? '—' }}</div>
31
+ <div class="user-meta">{{ userStore.profile?.position ?? userStore.profile?.deptName ?? '—' }}</div>
32
+ </div>
33
+ </div>
34
+ <div class="user-dropdown-body">
35
+ <div class="detail-section">
36
+ <div class="detail-section-title">基本信息</div>
37
+ <div class="detail-grid">
38
+ <div class="detail-item">
39
+ <span class="detail-label">工号</span>
40
+ <span class="detail-value">{{ userStore.profile?.userCode ?? '—' }}</span>
41
+ </div>
42
+ <div class="detail-item">
43
+ <span class="detail-label">性别</span>
44
+ <span class="detail-value">{{ genderText }}</span>
45
+ </div>
46
+ <div class="detail-item">
47
+ <span class="detail-label">岗位</span>
48
+ <span class="detail-value">{{ userStore.profile?.position ?? '—' }}</span>
49
+ </div>
50
+ <div class="detail-item">
51
+ <span class="detail-label">岗位编码</span>
52
+ <span class="detail-value">{{ userStore.profile?.jobCode ?? '—' }}</span>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ <div class="detail-section">
57
+ <div class="detail-section-title">部门信息</div>
58
+ <div class="detail-grid">
59
+ <div class="detail-item full">
60
+ <span class="detail-label">部门</span>
61
+ <span class="detail-value">{{ userStore.profile?.deptName ?? '—' }}</span>
62
+ </div>
63
+ <div class="detail-item">
64
+ <span class="detail-label">部门编码</span>
65
+ <span class="detail-value">{{ userStore.profile?.deptCode ?? '—' }}</span>
66
+ </div>
67
+ <div class="detail-item">
68
+ <span class="detail-label">标准部门</span>
69
+ <span class="detail-value">{{ userStore.profile?.deptStandName ?? '—' }}</span>
70
+ </div>
71
+ <div class="detail-item">
72
+ <span class="detail-label">标准部门编码</span>
73
+ <span class="detail-value">{{ userStore.profile?.deptStandCode ?? '—' }}</span>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ <div class="detail-section">
78
+ <div class="detail-section-title">角色与时间</div>
79
+ <div class="detail-grid">
80
+ <div class="detail-item">
81
+ <span class="detail-label">人群/角色</span>
82
+ <span class="detail-value">{{ userStore.profile?.crowdName ?? '—' }}</span>
83
+ </div>
84
+ <div class="detail-item">
85
+ <span class="detail-label">角色编码</span>
86
+ <span class="detail-value">{{ userStore.profile?.crowdCode ?? '—' }}</span>
87
+ </div>
88
+ <div class="detail-item full">
89
+ <span class="detail-label">同步时间</span>
90
+ <span class="detail-value">{{ userStore.profile?.currentTime ?? '—' }}</span>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ <div class="user-dropdown-footer">
96
+ <el-button type="primary" link size="small" class="logout-btn" @click="handleLogout">
97
+ 退出登录
98
+ </el-button>
99
+ </div>
100
+ </div>
101
+ </template>
102
+ </el-dropdown>
103
+ </template>
104
+ <RouterView />
105
+ </pro-layout>
106
+ </template>
107
+
108
+ <script setup lang="ts">
109
+ import { ref, computed, markRaw, watch, onMounted } from 'vue';
110
+ import { useRoute, useRouter, RouterView } from '@deppon/deppon-router';
111
+ import { ProLayout } from '@deppon/deppon-template';
112
+ import { ElIcon, ElDropdown, ElButton, ElDivider } from '@deppon/deppon-ui';
113
+ import type { MenuNode } from '@deppon/deppon-auth';
114
+ import type { RouteRecordRaw } from '@deppon/deppon-router';
115
+ import { useRouteStore } from '../stores/route';
116
+ import { useMenuStore } from '../stores/menu';
117
+ import { useUserStore } from '../stores/user';
118
+ import { developmentRoutes } from '../router/route';
119
+ // 按需引入图标组件
120
+ import { HomeFilled, Document, ArrowDown } from '@deppon/deppon-ui/icons-vue';
121
+
122
+ const ArrowDownIcon = markRaw(ArrowDown);
123
+
124
+ const props = defineProps({
125
+ /** 标题 */
126
+ title: {
127
+ type: String,
128
+ default: 'DEPPON-CMC',
129
+ },
130
+ /** Logo 图片,可以是字符串(路径)或对象(包含 src 和 size) */
131
+ logo: {
132
+ type: [String, Object],
133
+ default: () => ({
134
+ src:'logo.png',
135
+ // src:'https://ca.deppon.com.cn/uap2/shortCutEntryImg/logo2.png',
136
+ // src: 'https://jdl-oss.s3-cache-accelerate.cn-north-1.jdcloud-oss.com/static/images/home/logo-s.png',
137
+ size: { width: 129, height: 30 }, // 可以是数字(宽高相同)、字符串(如 '24px')或对象({ width: 24, height: 24 })
138
+ }),
139
+ },
140
+ /** 是否固定头部 */
141
+ fixedHeader: {
142
+ type: Boolean,
143
+ default: true,
144
+ },
145
+ /** 菜单项(可选,如果不传则使用默认菜单) */
146
+ menuItems: {
147
+ type: Array,
148
+ default: null,
149
+ },
150
+ /** 是否显示常用菜单 */
151
+ showCommonMenu: {
152
+ type: Boolean,
153
+ default: false,
154
+ },
155
+ commonMenusMax: {
156
+ type: Number,
157
+ default: 5,
158
+ },
159
+ });
160
+ // 图标映射表:将字符串图标名映射到实际的图标组件
161
+ const iconMap: Record<string, any> = {
162
+ HomeFilled,
163
+ Document,
164
+ };
165
+
166
+ const routeStore = useRouteStore();
167
+ const menuStore = useMenuStore();
168
+ const userStore = useUserStore();
169
+ const route = useRoute();
170
+ const router = useRouter();
171
+ const collapsed = ref(false);
172
+
173
+ /** 头像占位文字:取姓名首字,最多 2 字 */
174
+ const avatarText = computed(() => {
175
+ const name = userStore.profile?.userName?.trim();
176
+ if (!name) return '—';
177
+ return name.length <= 2 ? name : name.slice(0, 2);
178
+ });
179
+
180
+ /** 性别展示:1 男 2 女 */
181
+ const genderText = computed(() => {
182
+ const g = userStore.profile?.gender;
183
+ if (g === 1) return '男';
184
+ if (g === 2) return '女';
185
+ return g != null ? String(g) : '—';
186
+ });
187
+
188
+ function handleLogout() {
189
+ userStore.clearAuth();
190
+ router.push('/');
191
+ }
192
+
193
+ // 初始化 menu store,传入 commonMenusMax;并请求 UAP 用户信息同步到 profile
194
+ onMounted(() => {
195
+ menuStore.init(props.commonMenusMax);
196
+
197
+ });
198
+
199
+ // 监听路由变化,记录访问历史
200
+ watch(
201
+ () => route.path,
202
+ newPath => {
203
+ const routeMeta = route.meta as { title?: string } | undefined;
204
+ const title = (routeMeta?.title as string) || route.name?.toString() || newPath;
205
+ menuStore.addRouteToHistory(newPath, title);
206
+ userStore.fetchUserInfoByCode();
207
+ },
208
+ { immediate: true },
209
+ );
210
+
211
+ // 根据当前路由自动设置激活菜单
212
+ const activeMenu = computed(() => route.path);
213
+
214
+ /**
215
+ * 将 MenuNode 转换为 ProLayout 需要的菜单格式
216
+ */
217
+ const convertMenuNodeToMenuItem = (node: MenuNode): any => {
218
+ const path = node.uri || node.path || `/${node.id}`;
219
+ const menuItem: any = {
220
+ key: String(node.id),
221
+ path: path,
222
+ title: node.text || node.name || '',
223
+ };
224
+
225
+ // 如果有图标,添加图标(需要根据实际图标格式处理)
226
+ if (node.iconCls || node.icon) {
227
+ // 这里可以根据实际需求处理图标
228
+ menuItem.icon = node.iconCls || node.icon;
229
+ }
230
+
231
+ // 如果有子节点,递归转换
232
+ if (node.children && node.children.length > 0) {
233
+ menuItem.children = node.children.map(child => convertMenuNodeToMenuItem(child));
234
+ }
235
+
236
+ return menuItem;
237
+ };
238
+
239
+ /**
240
+ * 将 RouteRecordRaw 转换为 ProLayout 需要的菜单格式
241
+ */
242
+ const convertRouteToMenuItem = (route: RouteRecordRaw, parentPath = ''): any | null => {
243
+ // 跳过隐藏的路由
244
+ if (route.meta?.hidden) {
245
+ return null;
246
+ }
247
+
248
+ // 跳过没有组件的路由(如 redirect)
249
+ if (!route.component && !route.children) {
250
+ return null;
251
+ }
252
+
253
+ // 构建完整路径
254
+ let fullPath: string;
255
+ if (parentPath) {
256
+ // 确保 parentPath 以 / 结尾,route.path 不以 / 开头
257
+ const normalizedParent = parentPath.endsWith('/') ? parentPath.slice(0, -1) : parentPath;
258
+ const normalizedChild = route.path.startsWith('/') ? route.path : `/${route.path}`;
259
+ fullPath = `${normalizedParent}${normalizedChild}`;
260
+ } else {
261
+ fullPath = route.path;
262
+ }
263
+
264
+ const menuItem: any = {
265
+ key: route.name ? String(route.name) : fullPath,
266
+ path: fullPath,
267
+ title: (route.meta?.title as string) || route.name || route.path,
268
+ };
269
+
270
+ // 处理图标:将字符串图标名转换为实际的图标组件
271
+ if (route.meta?.icon) {
272
+ const iconName = route.meta.icon as string;
273
+ // 从图标映射表中获取对应的图标组件
274
+ const IconComponent = iconMap[iconName];
275
+ if (IconComponent) {
276
+ menuItem.icon = IconComponent;
277
+ }
278
+ }
279
+
280
+ // 处理子路由
281
+ if (route.children && route.children.length > 0) {
282
+ const children = route.children.map(child => convertRouteToMenuItem(child, fullPath)).filter(Boolean); // 过滤掉 null 值
283
+
284
+ if (children.length > 0) {
285
+ menuItem.children = children;
286
+ }
287
+ }
288
+
289
+ return menuItem;
290
+ };
291
+
292
+ /**
293
+ * 从 developmentRoutes 中提取菜单项(分组结构)
294
+ */
295
+ const getMenuItemsFromDevelopmentRoutes = (): any[] => {
296
+ const navMenuItems: any[] = [];
297
+
298
+ developmentRoutes.forEach(route => {
299
+ // 只处理有 children 的路由(通常是布局路由)
300
+ if (route.children && route.children.length > 0) {
301
+ route.children.forEach(child => {
302
+ // 跳过 BlankLayout,直接处理它的 children
303
+ if (child.name === 'BlankLayout' && child.children && child.children.length > 0) {
304
+ child.children.forEach(grandChild => {
305
+ const menuItem = convertRouteToMenuItem(grandChild, route.path);
306
+ if (menuItem) {
307
+ navMenuItems.push(menuItem);
308
+ }
309
+ });
310
+ } else {
311
+ const menuItem = convertRouteToMenuItem(child, route.path);
312
+ if (menuItem) {
313
+ navMenuItems.push(menuItem);
314
+ }
315
+ }
316
+ });
317
+ } else {
318
+ // 处理没有 children 的路由
319
+ const menuItem = convertRouteToMenuItem(route);
320
+ if (menuItem) {
321
+ navMenuItems.push(menuItem);
322
+ }
323
+ }
324
+ });
325
+
326
+ // 返回分组结构
327
+ const result: any[] = [];
328
+
329
+ // 从 store 获取常用菜单并添加
330
+ const commonMenu = menuStore.formattedCommonMenu;
331
+ if (commonMenu.length > 0) {
332
+ result.push(...commonMenu);
333
+ }
334
+
335
+ // 如果有导航菜单,添加导航菜单分组
336
+ if (navMenuItems.length > 0) {
337
+ result.push({
338
+ type: 'menu',
339
+ title: '导航菜单',
340
+ children: navMenuItems,
341
+ });
342
+ }
343
+ return result;
344
+ };
345
+
346
+ // 从 store 中获取菜单并转换为 ProLayout 格式(分组结构)
347
+ // 注意:导航菜单保持原样,不进行任何修改
348
+ const menuItemsFromStore = computed(() => {
349
+ const result: any[] = [];
350
+
351
+ // 从 store 获取常用菜单并添加(如果有)
352
+ const commonMenu = menuStore.formattedCommonMenu;
353
+ if (commonMenu.length > 0) {
354
+ result.push(...commonMenu);
355
+ }
356
+
357
+ // 添加导航菜单(保持原样,不修改)
358
+ if (routeStore.menuTree && routeStore.menuTree.length > 0) {
359
+ const menuItems = routeStore.menuTree.map(node => convertMenuNodeToMenuItem(node));
360
+ result.push({
361
+ type: 'menu',
362
+ title: '导航菜单',
363
+ children: menuItems,
364
+ });
365
+ }
366
+
367
+ return result;
368
+ });
369
+
370
+ // 根据环境选择菜单源:开发环境使用 developmentRoutes,其他环境使用 store
371
+ const computedMenuItems = computed(() => {
372
+ if (import.meta.env.DP_USE_LOCAL_ROUTES === 'true') {
373
+ return getMenuItemsFromDevelopmentRoutes();
374
+ }
375
+ return menuItemsFromStore.value;
376
+ });
377
+
378
+ // 使用传入的菜单项或计算出的菜单(响应式更新常用菜单)
379
+ const menuItems = computed(() => {
380
+ if (props.menuItems) {
381
+ return props.menuItems;
382
+ }
383
+ // 直接使用计算出的菜单(getMenuItemsFromDevelopmentRoutes 已经处理了常用菜单和导航菜单的合并)
384
+ return computedMenuItems.value;
385
+ });
386
+
387
+ // 删除常用菜单项
388
+ const handleCommonMenuDelete = (menuItem: any) => {
389
+ if (menuItem && menuItem.key) {
390
+ menuStore.handleCommonMenuDelete(menuItem.key);
391
+ }
392
+ };
393
+
394
+ // 顶部菜单项配置
395
+ const topMenuItems = [
396
+ {
397
+ key: 'home-1',
398
+ path: '/homex',
399
+ title: '签客户',
400
+ label: '签客户',
401
+ badge: 'NEW',
402
+ },
403
+
404
+ {
405
+ key: 'quote-management',
406
+ path: '/quote-management',
407
+ title: '销售中心',
408
+ label: '报价管理',
409
+ children: [
410
+ {
411
+ key: 'multi-product-quote',
412
+ path: '/quote-management/multi-product',
413
+ title: '多产品报价管理',
414
+ label: '多产品报价管理',
415
+ },
416
+ {
417
+ key: 'service-quote',
418
+ path: '/quote-management/service',
419
+ title: '服务+报价管理',
420
+ label: '服务+报价管理',
421
+ },
422
+ {
423
+ key: 'market-scattered-quote',
424
+ path: '/quote-management/market-scattered',
425
+ title: '市场散单报价管理',
426
+ label: '市场散单报价管理',
427
+ },
428
+ ],
429
+ },
430
+ {
431
+ key: 'quote-management1',
432
+ path: '/quote-management1',
433
+ title: '客户中心',
434
+ label: '客户中心',
435
+ children: [
436
+ {
437
+ key: 'multi-product-quote',
438
+ path: '/quote-management/multi-product',
439
+ title: '多产品报价管理',
440
+ label: '多产品报价管理',
441
+ },
442
+ {
443
+ key: 'service-quote',
444
+ path: '/quote-management/service',
445
+ title: '服务+报价管理',
446
+ label: '服务+报价管理',
447
+ },
448
+ {
449
+ key: 'market-scattered-quote',
450
+ path: '/quote-management/market-scattered',
451
+ title: '市场散单报价管理',
452
+ label: '市场散单报价管理',
453
+ },
454
+ ],
455
+ },
456
+ {
457
+ key: 'price-management',
458
+ path: '/price-management',
459
+ title: '价格中心',
460
+ label: '价格中心',
461
+ children: [
462
+ {
463
+ key: 'multi-product-quote',
464
+ path: '/quote-management/multi-product',
465
+ title: '多产品报价管理',
466
+ label: '多产品报价管理',
467
+ },
468
+ {
469
+ key: 'service-quote',
470
+ path: '/quote-management/service',
471
+ title: '服务+报价管理',
472
+ label: '服务+报价管理',
473
+ },
474
+ {
475
+ key: 'market-scattered-quote',
476
+ path: '/quote-management/market-scattered',
477
+ title: '市场散单报价管理',
478
+ label: '市场散单报价管理',
479
+ },
480
+ ],
481
+ },
482
+ {
483
+ key: 'sign-management',
484
+ path: '/sign-management',
485
+ title: '签约中心',
486
+ label: '签约中心',
487
+ children: [
488
+ {
489
+ key: 'multi-product-quote',
490
+ path: '/quote-management/multi-product',
491
+ title: '多产品报价管理',
492
+ label: '多产品报价管理',
493
+ },
494
+ {
495
+ key: 'service-quote',
496
+ path: '/quote-management/service',
497
+ title: '服务+报价管理',
498
+ label: '服务+报价管理',
499
+ },
500
+ {
501
+ key: 'market-scattered-quote',
502
+ path: '/quote-management/market-scattered',
503
+ title: '市场散单报价管理',
504
+ label: '市场散单报价管理',
505
+ },
506
+ ],
507
+ },
508
+
509
+ {
510
+ key: 'data-management',
511
+ path: '/data-management',
512
+ title: '数据中心',
513
+ label: '数据中心',
514
+ children: [
515
+ {
516
+ key: 'order-list',
517
+ path: '/order-management/list',
518
+ title: '订单列表',
519
+ label: '订单列表',
520
+ },
521
+ {
522
+ key: 'order-detail',
523
+ path: '/order-management/detail',
524
+ title: '订单详情',
525
+ label: '订单详情',
526
+ },
527
+ ],
528
+ },
529
+ {
530
+ key: 'more',
531
+ path: '/more',
532
+ title: '更多',
533
+ label: '更多',
534
+ children: [
535
+ {
536
+ key: 'multi-product-quote',
537
+ path: '/quote-management/multi-product',
538
+ title: '多产品报价管理',
539
+ label: '多产品报价管理',
540
+ },
541
+ {
542
+ key: 'service-quote',
543
+ path: '/quote-management/service',
544
+ title: '服务+报价管理',
545
+ label: '服务+报价管理',
546
+ },
547
+ {
548
+ key: 'market-scattered-quote',
549
+ path: '/quote-management/market-scattered',
550
+ title: '市场散单报价管理',
551
+ label: '市场散单报价管理',
552
+ },
553
+ ],
554
+ },
555
+ ];
556
+ </script>
557
+
558
+ <style scoped>
559
+ .header-user-trigger {
560
+ display: flex;
561
+ align-items: center;
562
+ gap: 6px;
563
+ cursor: pointer;
564
+ padding: 4px 8px;
565
+ border-radius: 6px;
566
+ transition: background-color 0.2s;
567
+ }
568
+ .header-user-trigger:hover {
569
+ background: var(--el-fill-color-light);
570
+ }
571
+
572
+ .header-user-dropdown {
573
+ min-width: 320px;
574
+ max-width: 400px;
575
+ max-height: 80vh;
576
+ overflow: hidden;
577
+ display: flex;
578
+ flex-direction: column;
579
+ background: var(--el-bg-color-overlay);
580
+ border-radius: 12px;
581
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.06);
582
+ border: 1px solid var(--el-border-color-lighter);
583
+ }
584
+
585
+ .user-dropdown-header {
586
+ display: flex;
587
+ align-items: center;
588
+ gap: 12px;
589
+ padding: 16px;
590
+ background: linear-gradient(135deg, var(--el-color-primary-light-9) 0%, var(--el-fill-color-light) 100%);
591
+ border-bottom: 1px solid var(--el-border-color-lighter);
592
+ }
593
+ .user-avatar {
594
+ width: 44px;
595
+ height: 44px;
596
+ border-radius: 10px;
597
+ background: var(--el-color-primary);
598
+ color: #fff;
599
+ display: flex;
600
+ align-items: center;
601
+ justify-content: center;
602
+ font-size: 16px;
603
+ font-weight: 600;
604
+ flex-shrink: 0;
605
+ }
606
+ .user-header-info {
607
+ min-width: 0;
608
+ }
609
+ .user-name {
610
+ font-size: 16px;
611
+ font-weight: 600;
612
+ color: var(--el-text-color-primary);
613
+ line-height: 1.3;
614
+ }
615
+ .user-meta {
616
+ font-size: 12px;
617
+ color: var(--el-text-color-secondary);
618
+ margin-top: 2px;
619
+ }
620
+
621
+ .user-dropdown-body {
622
+ padding: 12px 16px;
623
+ overflow-y: auto;
624
+ max-height: 50vh;
625
+ }
626
+
627
+ .detail-section {
628
+ margin-bottom: 14px;
629
+ }
630
+ .detail-section:last-child {
631
+ margin-bottom: 0;
632
+ }
633
+ .detail-section-title {
634
+ font-size: 11px;
635
+ font-weight: 600;
636
+ color: var(--el-text-color-secondary);
637
+ text-transform: uppercase;
638
+ letter-spacing: 0.5px;
639
+ margin-bottom: 8px;
640
+ padding-bottom: 4px;
641
+ border-bottom: 1px dashed var(--el-border-color-lighter);
642
+ }
643
+ .detail-grid {
644
+ display: grid;
645
+ grid-template-columns: 1fr 1fr;
646
+ gap: 10px 16px;
647
+ }
648
+ .detail-item {
649
+ display: flex;
650
+ flex-direction: column;
651
+ gap: 2px;
652
+ min-width: 0;
653
+ }
654
+ .detail-item.full {
655
+ grid-column: 1 / -1;
656
+ }
657
+ .detail-label {
658
+ font-size: 11px;
659
+ color: var(--el-text-color-secondary);
660
+ }
661
+ .detail-value {
662
+ font-size: 13px;
663
+ color: var(--el-text-color-primary);
664
+ word-break: break-all;
665
+ }
666
+
667
+ .user-dropdown-footer {
668
+ padding: 10px 16px;
669
+ border-top: 1px solid var(--el-border-color-lighter);
670
+ background: var(--el-fill-color-blank);
671
+ border-radius: 0 0 12px 12px;
672
+ }
673
+ .logout-btn {
674
+ padding: 4px 0;
675
+ }
676
+ </style>
677
+
678
+ <!-- 下拉浮层通过 teleport 挂到 body,用非 scoped 样式为 el-popper 加圆角 -->
679
+ <style>
680
+ .header-user-dropdown-popper.el-dropdown__popper {
681
+ border-radius: 12px;
682
+ }
683
+ .header-user-dropdown-popper .el-scrollbar__wrap.el-scrollbar__wrap--hidden-default {
684
+ border-radius: 12px;
685
+ background: var(--el-bg-color-overlay) !important;
686
+ }
687
+ </style>