befly-admin-ui 1.9.7 → 1.9.9

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.
@@ -37,49 +37,62 @@
37
37
  </template>
38
38
  </t-menu>
39
39
  </div>
40
+ </div>
40
41
 
41
- <!-- 底部操作区域 -->
42
- <div class="sidebar-footer">
43
- <div class="footer-item" @click="handleSettings">
44
- <SettingIcon style="width: 18px; height: 18px" />
45
- <span>系统设置</span>
46
- </div>
47
- <div class="footer-user">
48
- <t-upload :action="$Config.uploadPath" :headers="uploadHeaders" :show-upload-list="false" accept="image/*" @success="onAvatarUploadSuccess">
49
- <div class="user-avatar" :class="{ 'has-avatar': $Data.userInfo.avatar }">
42
+ <!-- 右侧内容区域 -->
43
+ <div class="layout-main">
44
+ <div class="main-toolbar">
45
+ <TDropdown trigger="click" placement="bottom-right" @click="onUserDropdownAction">
46
+ <div class="toolbar-user">
47
+ <div class="toolbar-user-avatar">
50
48
  <img v-if="$Data.userInfo.avatar" :src="$Data.userInfo.avatar" alt="avatar" />
51
49
  <UserIcon v-else style="width: 16px; height: 16px; color: #fff" />
52
- <div class="avatar-overlay">
53
- <CloudIcon style="width: 14px; height: 14px; color: #fff" />
54
- </div>
55
50
  </div>
56
- </t-upload>
57
- <div class="user-info">
58
- <span class="user-name">{{ $Data.userInfo.nickname || "管理员" }}</span>
59
- <span class="user-role">{{ $Data.userInfo.role || "超级管理员" }}</span>
51
+ <div class="toolbar-user-info">
52
+ <span class="user-name">{{ $Data.userInfo.nickname || "管理员" }}</span>
53
+ <span class="user-role">{{ $Data.userInfo.role || "超级管理员" }}</span>
54
+ </div>
55
+ <ChevronDownIcon class="toolbar-user-arrow" style="width: 16px; height: 16px" />
60
56
  </div>
61
- <t-button theme="default" variant="text" size="small" @click="handleLogout">
62
- <template #icon>
63
- <CloseCircleIcon style="width: 16px; height: 16px" />
64
- </template>
65
- </t-button>
66
- </div>
57
+ <TDropdownMenu slot="dropdown">
58
+ <TDropdownItem value="password">
59
+ <LockOnIcon style="width: 14px; height: 14px; margin-right: 6px" />
60
+ 修改密码
61
+ </TDropdownItem>
62
+ <TDropdownItem value="logout" :divider="true">
63
+ <CloseCircleIcon style="width: 14px; height: 14px; margin-right: 6px" />
64
+ 退出登录
65
+ </TDropdownItem>
66
+ </TDropdownMenu>
67
+ </TDropdown>
67
68
  </div>
68
- </div>
69
69
 
70
- <!-- 右侧内容区域 -->
71
- <div class="layout-main">
72
- <RouterView />
70
+ <div class="main-content">
71
+ <RouterView />
72
+ </div>
73
+
74
+ <PageDialog v-if="$Data.passwordDialogVisible" v-model="$Data.passwordDialogVisible" title="修改密码" :confirm-loading="$Data.passwordSubmitting" @confirm="onPasswordSubmit">
75
+ <TForm ref="passwordFormRef" :model="$Data.passwordForm" label-width="100px" label-position="left" :rules="passwordFormRules">
76
+ <TFormItem label="新密码" prop="password">
77
+ <TInput v-model="$Data.passwordForm.password" type="password" placeholder="请输入新密码,至少6位" autocomplete="new-password" />
78
+ </TFormItem>
79
+ <TFormItem label="确认密码" prop="confirmPassword">
80
+ <TInput v-model="$Data.passwordForm.confirmPassword" type="password" placeholder="请再次输入新密码" autocomplete="new-password" />
81
+ </TFormItem>
82
+ </TForm>
83
+ </PageDialog>
73
84
  </div>
74
85
  </div>
75
86
  </template>
76
87
 
77
88
  <script setup>
78
89
  import { arrayToTree } from "befly-admin-ui/utils/arrayToTree";
79
- import { Button as TButton, DialogPlugin, Menu as TMenu, MenuItem as TMenuItem, MessagePlugin, Submenu as TSubmenu, Upload as TUpload } from "tdesign-vue-next";
80
- import { CloudIcon, CloseCircleIcon, CodeIcon, LinkIcon, MenuIcon, SettingIcon, UserIcon, ControlPlatformIcon, AppIcon, HomeIcon } from "tdesign-icons-vue-next";
90
+ import PageDialog from "befly-admin-ui/components/pageDialog.vue";
91
+ import { hashPassword } from "befly-admin-ui/utils/hashPassword";
92
+ import { DialogPlugin, Dropdown as TDropdown, DropdownItem as TDropdownItem, DropdownMenu as TDropdownMenu, Form as TForm, FormItem as TFormItem, Input as TInput, Menu as TMenu, MenuItem as TMenuItem, MessagePlugin, Submenu as TSubmenu } from "tdesign-vue-next";
93
+ import { AppIcon, ChevronDownIcon, CloseCircleIcon, ControlPlatformIcon, HomeIcon, LockOnIcon, UserIcon } from "tdesign-icons-vue-next";
81
94
 
82
- import { reactive, watch } from "vue";
95
+ import { reactive, ref, watch } from "vue";
83
96
  import { useRoute, useRouter } from "vue-router";
84
97
  import { $Http } from "@/plugins/http.js";
85
98
  import { $Config } from "@/plugins/config.js";
@@ -87,7 +100,7 @@ import { $Store } from "@/plugins/store.js";
87
100
 
88
101
  const router = useRouter();
89
102
  const route = useRoute();
90
- const uploadHeaders = { Authorization: `Bearer ${$Store.local.get($Config.tokenName, "")}` };
103
+ const passwordFormRef = ref(null);
91
104
 
92
105
  function isString(value) {
93
106
  return typeof value === "string";
@@ -122,19 +135,39 @@ const normalizeParentPath = (parentPath) => {
122
135
  return normalized;
123
136
  };
124
137
 
138
+ function createPasswordForm() {
139
+ return {
140
+ password: "",
141
+ confirmPassword: ""
142
+ };
143
+ }
144
+
125
145
  // 响应式数据
126
146
  const $Data = reactive({
127
147
  userMenus: [],
128
148
  userMenusFlat: [], // 一维菜单数据
129
149
  expandedKeys: [],
130
150
  currentMenuKey: "",
151
+ passwordDialogVisible: false,
152
+ passwordSubmitting: false,
153
+ passwordForm: createPasswordForm(),
131
154
  userInfo: {
155
+ id: 0,
132
156
  nickname: "管理员",
133
157
  role: "超级管理员",
134
- avatar: "" // 用户头像
158
+ avatar: "",
159
+ roleCode: ""
135
160
  }
136
161
  });
137
162
 
163
+ const passwordFormRules = {
164
+ password: [
165
+ { required: true, message: "请输入新密码", trigger: "blur" },
166
+ { min: 6, message: "新密码至少6位", trigger: "blur" }
167
+ ],
168
+ confirmPassword: [{ required: true, message: "请再次输入新密码", trigger: "blur" }]
169
+ };
170
+
138
171
  function normalizeAvatarUrl(value) {
139
172
  if (!isString(value) || value.length === 0) {
140
173
  return "";
@@ -263,27 +296,67 @@ async function handleLogout() {
263
296
  });
264
297
  }
265
298
 
266
- function handleSettings() {
267
- router.push("/core/settings");
299
+ function openPasswordDialog() {
300
+ $Data.passwordForm = createPasswordForm();
301
+ $Data.passwordDialogVisible = true;
268
302
  }
269
303
 
270
- async function onAvatarUploadSuccess(res) {
271
- if (res.response?.code === 0 && res.response?.data?.url) {
272
- const avatarUrl = res.response.data.url;
304
+ function onUserDropdownAction(data) {
305
+ const record = data;
306
+ const rawValue = record && record["value"] ? record["value"] : "";
307
+ const cmd = rawValue ? String(rawValue) : "";
273
308
 
274
- try {
275
- if ($Data.userInfo.id) {
276
- await $Http("/core/admin/upd", {
277
- id: $Data.userInfo.id,
278
- avatar: avatarUrl
279
- });
280
- }
309
+ if (cmd === "password") {
310
+ openPasswordDialog();
311
+ return;
312
+ }
281
313
 
282
- $Data.userInfo.avatar = avatarUrl;
283
- MessagePlugin.success("头像上传成功");
284
- } catch (error) {
285
- MessagePlugin.error(error.msg || error.message || "头像保存失败");
314
+ if (cmd === "logout") {
315
+ handleLogout();
316
+ }
317
+ }
318
+
319
+ async function onPasswordSubmit(context) {
320
+ const form = passwordFormRef.value;
321
+ if (form === null) {
322
+ MessagePlugin.warning("表单未就绪");
323
+ return;
324
+ }
325
+
326
+ const valid = await form.validate();
327
+ if (valid !== true) {
328
+ return;
329
+ }
330
+
331
+ if ($Data.passwordForm.password !== $Data.passwordForm.confirmPassword) {
332
+ MessagePlugin.error("两次输入的密码不一致");
333
+ return;
334
+ }
335
+
336
+ if (!$Data.userInfo.id) {
337
+ MessagePlugin.error("用户信息未就绪");
338
+ return;
339
+ }
340
+
341
+ $Data.passwordSubmitting = true;
342
+
343
+ try {
344
+ const hashedPassword = await hashPassword($Data.passwordForm.password);
345
+ const storedPassword = await hashPassword(hashedPassword);
346
+
347
+ await $Http("/core/admin/upd", {
348
+ id: $Data.userInfo.id,
349
+ password: storedPassword
350
+ });
351
+
352
+ MessagePlugin.success("密码修改成功");
353
+ if (context && typeof context.close === "function") {
354
+ context.close();
286
355
  }
356
+ } catch (error) {
357
+ MessagePlugin.error(error.msg || error.message || "密码修改失败");
358
+ } finally {
359
+ $Data.passwordSubmitting = false;
287
360
  }
288
361
  }
289
362
 
@@ -413,112 +486,92 @@ watch(
413
486
  }
414
487
  }
415
488
  }
489
+ }
490
+
491
+ // 右侧主内容区域
492
+ .layout-main {
493
+ flex: 1;
494
+ min-width: 0;
495
+ display: flex;
496
+ flex-direction: column;
497
+ gap: var(--layout-gap);
498
+ overflow: hidden;
416
499
 
417
- // 底部操作区域
418
- .sidebar-footer {
419
- border-top: 1px solid var(--border-color-light);
420
- padding: var(--spacing-sm);
500
+ .main-toolbar {
501
+ flex-shrink: 0;
502
+ display: flex;
503
+ justify-content: flex-end;
504
+ align-items: center;
505
+ padding: var(--spacing-sm) var(--spacing-md);
506
+ background: var(--bg-color-container);
507
+ border-radius: var(--border-radius-large);
508
+ box-shadow: var(--shadow-1);
421
509
 
422
- .footer-item {
510
+ .toolbar-user {
423
511
  display: flex;
424
512
  align-items: center;
425
513
  gap: var(--spacing-sm);
426
- padding: var(--spacing-sm) var(--spacing-md);
514
+ padding: 4px 8px;
427
515
  border-radius: var(--border-radius);
428
- color: var(--text-secondary);
429
516
  cursor: pointer;
430
- transition: all var(--transition-fast);
517
+ transition: background-color var(--transition-fast);
431
518
 
432
519
  &:hover {
433
520
  background-color: var(--bg-color-hover);
434
- color: var(--text-primary);
435
- }
436
-
437
- span {
438
- font-size: var(--font-size-sm);
439
- white-space: nowrap;
440
521
  }
441
522
  }
442
523
 
443
- .footer-user {
524
+ .toolbar-user-avatar {
525
+ width: 36px;
526
+ height: 36px;
527
+ min-width: 36px;
444
528
  display: flex;
445
529
  align-items: center;
446
- gap: var(--spacing-sm);
447
- padding: var(--spacing-sm);
448
- margin-top: var(--spacing-xs);
449
- background: var(--bg-color-secondarycontainer);
450
- border-radius: var(--border-radius);
451
-
452
- .user-avatar {
453
- width: 32px;
454
- height: 32px;
455
- min-width: 32px;
456
- display: flex;
457
- align-items: center;
458
- justify-content: center;
459
- background: var(--primary-color);
460
- border-radius: 50%;
461
- flex-shrink: 0;
462
- cursor: pointer;
463
- position: relative;
464
- overflow: hidden;
465
-
466
- img {
467
- width: 100%;
468
- height: 100%;
469
- object-fit: cover;
470
- }
471
-
472
- .avatar-overlay {
473
- position: absolute;
474
- top: 0;
475
- left: 0;
476
- right: 0;
477
- bottom: 0;
478
- background: rgba(0, 0, 0, 0.5);
479
- display: flex;
480
- align-items: center;
481
- justify-content: center;
482
- opacity: 0;
483
- transition: opacity var(--transition-fast);
484
- }
530
+ justify-content: center;
531
+ background: var(--primary-color);
532
+ border-radius: 50%;
533
+ overflow: hidden;
534
+ flex-shrink: 0;
485
535
 
486
- &:hover .avatar-overlay {
487
- opacity: 1;
488
- }
536
+ img {
537
+ width: 100%;
538
+ height: 100%;
539
+ object-fit: cover;
489
540
  }
541
+ }
490
542
 
491
- .user-info {
492
- flex: 1;
493
- min-width: 0;
494
- display: flex;
495
- flex-direction: column;
543
+ .toolbar-user-info {
544
+ min-width: 0;
545
+ display: flex;
546
+ flex-direction: column;
496
547
 
497
- .user-name {
498
- font-size: var(--font-size-sm);
499
- font-weight: var(--font-weight-medium);
500
- color: var(--text-primary);
501
- line-height: 1.3;
502
- overflow: hidden;
503
- text-overflow: ellipsis;
504
- white-space: nowrap;
505
- }
548
+ .user-name {
549
+ font-size: var(--font-size-sm);
550
+ font-weight: var(--font-weight-medium);
551
+ color: var(--text-primary);
552
+ line-height: 1.3;
553
+ white-space: nowrap;
554
+ }
506
555
 
507
- .user-role {
508
- font-size: var(--font-size-xs);
509
- color: var(--text-placeholder);
510
- line-height: 1.3;
511
- }
556
+ .user-role {
557
+ font-size: var(--font-size-xs);
558
+ color: var(--text-placeholder);
559
+ line-height: 1.3;
560
+ white-space: nowrap;
512
561
  }
513
562
  }
563
+
564
+ .toolbar-user-arrow {
565
+ color: var(--text-placeholder);
566
+ flex-shrink: 0;
567
+ }
514
568
  }
515
- }
516
569
 
517
- // 右侧主内容区域
518
- .layout-main {
519
- flex: 1;
520
- min-width: 0;
521
- overflow: hidden;
570
+ .main-content {
571
+ flex: 1;
572
+ min-height: 0;
573
+ overflow: hidden;
574
+ }
522
575
  }
523
576
  }
524
577
  </style>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "befly-admin-ui",
3
- "version": "1.9.7",
4
- "gitHead": "913ad3b34f63ddf0936fb338fadc78ade7edf00d",
3
+ "version": "1.9.9",
4
+ "gitHead": "990aee4744b2c52e24c1a8c7a6cbee06b7d31833",
5
5
  "private": false,
6
6
  "description": "Befly - 管理后台功能组件",
7
7
  "keywords": [
@@ -8,14 +8,6 @@
8
8
  </TButton>
9
9
  </template>
10
10
 
11
- <template #toolRight="scope">
12
- <TButton shape="circle" @click="onReload(scope.reload)">
13
- <template #icon>
14
- <RefreshIcon />
15
- </template>
16
- </TButton>
17
- </template>
18
-
19
11
  <template #state="{ row }">
20
12
  <TTag v-if="row.state === 1" shape="round" theme="success" variant="light-outline">正常</TTag>
21
13
  <TTag v-else-if="row.state === 2" shape="round" theme="warning" variant="light-outline">禁用</TTag>
@@ -50,7 +42,7 @@
50
42
  <script setup>
51
43
  import { reactive } from "vue";
52
44
  import { Button as TButton, Dropdown as TDropdown, DropdownItem as TDropdownItem, DropdownMenu as TDropdownMenu, Tag as TTag } from "tdesign-vue-next";
53
- import { AddIcon, ChevronDownIcon, DeleteIcon, EditIcon, RefreshIcon } from "tdesign-icons-vue-next";
45
+ import { AddIcon, ChevronDownIcon, DeleteIcon, EditIcon } from "tdesign-icons-vue-next";
54
46
  import EditDialog from "./components/edit.vue";
55
47
  import PageTableDetail from "befly-admin-ui/components/pageTableDetail.vue";
56
48
  import { withDefaultColumns } from "befly-admin-ui/utils/withDefaultColumns";
@@ -88,10 +80,6 @@ function onAdd() {
88
80
  onAction("add", {});
89
81
  }
90
82
 
91
- function onReload(reload) {
92
- reload({ keepSelection: true });
93
- }
94
-
95
83
  function onDialogSuccess(reload) {
96
84
  reload({ keepSelection: true });
97
85
  }