befly-admin 3.4.54 → 3.4.56

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.
@@ -1,67 +1,81 @@
1
1
  <template>
2
- <div class="layout-0-wrapper">
3
- <!-- 顶部导航栏 -->
4
- <div class="layout-header">
5
- <div class="logo">
2
+ <div class="layout-wrapper">
3
+ <!-- 左侧边栏:Logo + 菜单 + 底部操作 -->
4
+ <div class="layout-sidebar">
5
+ <!-- Logo 区域 -->
6
+ <div class="sidebar-logo">
7
+ <div class="logo-icon">
8
+ <i-lucide:box style="width: 24px; height: 24px; color: var(--primary-color)" />
9
+ </div>
6
10
  <h2>{{ $Config.appTitle }}</h2>
7
11
  </div>
8
- <div class="header-right">
9
- <div class="user-info-bar">
10
- <div class="user-text">
12
+
13
+ <!-- 菜单区域 -->
14
+ <div class="sidebar-menu">
15
+ <t-menu v-model:value="$Data.currentMenuKey" v-model:expanded="$Data.expandedKeys" width="220px" @change="$Method.onMenuClick">
16
+ <template v-for="menu in $Data.userMenus" :key="menu.id">
17
+ <!-- 无子菜单 -->
18
+ <t-menu-item v-if="!menu.children || menu.children.length === 0" :value="menu.path">
19
+ <template #icon>
20
+ <i-lucide:home v-if="menu.path === '/addon/admin/'" style="margin-right: 8px" />
21
+ <i-lucide:file-text v-else style="margin-right: 8px" />
22
+ </template>
23
+ {{ menu.name }}
24
+ </t-menu-item>
25
+ <!-- 有子菜单 -->
26
+ <t-submenu v-else :value="String(menu.id)" :title="menu.name">
27
+ <template #icon>
28
+ <i-lucide:folder style="margin-right: 8px" />
29
+ </template>
30
+ <t-menu-item v-for="child in menu.children" :key="child.id" :value="child.path">
31
+ <template #icon>
32
+ <i-lucide:file-text style="margin-right: 8px" />
33
+ </template>
34
+ {{ child.name }}
35
+ </t-menu-item>
36
+ </t-submenu>
37
+ </template>
38
+ </t-menu>
39
+ </div>
40
+
41
+ <!-- 底部操作区域 -->
42
+ <div class="sidebar-footer">
43
+ <div class="footer-item" @click="$Method.handleSettings">
44
+ <i-lucide:settings style="width: 18px; height: 18px" />
45
+ <span>系统设置</span>
46
+ </div>
47
+ <div class="footer-user">
48
+ <t-upload :action="$Config.uploadUrl" :headers="{ Authorization: $Storage.local.get('token') }" :show-upload-list="false" accept="image/*" @success="$Method.onAvatarUploadSuccess">
49
+ <div class="user-avatar" :class="{ 'has-avatar': $Data.userInfo.avatar }">
50
+ <img v-if="$Data.userInfo.avatar" :src="$Data.userInfo.avatar" alt="avatar" />
51
+ <i-lucide:user v-else style="width: 16px; height: 16px; color: #fff" />
52
+ <div class="avatar-overlay">
53
+ <i-lucide:camera style="width: 14px; height: 14px; color: #fff" />
54
+ </div>
55
+ </div>
56
+ </t-upload>
57
+ <div class="user-info">
11
58
  <span class="user-name">{{ $Data.userInfo.nickname || '管理员' }}</span>
12
- <t-tag theme="primary" size="small" variant="light">{{ $Data.userInfo.role || '超级管理员' }}</t-tag>
59
+ <span class="user-role">{{ $Data.userInfo.role || '超级管理员' }}</span>
13
60
  </div>
14
- <t-button class="logout-btn" theme="danger" shape="square" @click="$Method.handleLogout">
61
+ <t-button theme="default" variant="text" size="small" @click="$Method.handleLogout">
15
62
  <template #icon>
16
- <i-lucide:log-out style="color: #fff" />
63
+ <i-lucide:log-out style="width: 16px; height: 16px" />
17
64
  </template>
18
65
  </t-button>
19
66
  </div>
20
67
  </div>
21
68
  </div>
22
69
 
23
- <!-- 菜单栏 -->
24
- <div class="layout-menu">
25
- <t-menu v-model:value="$Data.currentMenuKey" v-model:expanded="$Data.expandedKeys" style="height: 100%" @change="$Method.onMenuClick">
26
- <template v-for="menu in $Data.userMenus" :key="menu.id">
27
- <!-- 无子菜单 -->
28
- <t-menu-item v-if="!menu.children || menu.children.length === 0" :value="menu.path">
29
- <template #icon>
30
- <i-lucide:home v-if="menu.path === '/addon/admin/'" style="margin-right: 8px" />
31
- <i-lucide:file-text v-else style="margin-right: 8px" />
32
- </template>
33
- {{ menu.name }}
34
- </t-menu-item>
35
- <!-- 有子菜单 -->
36
- <t-submenu v-else :value="String(menu.id)" :title="menu.name">
37
- <template #icon>
38
- <i-lucide:folder style="margin-right: 8px" />
39
- </template>
40
- <t-menu-item v-for="child in menu.children" :key="child.id" :value="child.path">
41
- <template #icon>
42
- <i-lucide:file-text style="margin-right: 8px" />
43
- </template>
44
- {{ child.name }}
45
- </t-menu-item>
46
- </t-submenu>
47
- </template>
48
- </t-menu>
49
- </div>
50
-
51
- <!-- 内容区域 -->
52
- <div class="layout-content">
70
+ <!-- 右侧内容区域 -->
71
+ <div class="layout-main">
53
72
  <RouterView />
54
73
  </div>
55
-
56
- <!-- 底部分页栏 -->
57
- <div class="layout-footer">
58
- <span>© 2024 Befly. All rights reserved.</span>
59
- </div>
60
74
  </div>
61
75
  </template>
62
76
 
63
77
  <script setup>
64
- import { arrayToTree } from 'befly-util/arrayToTree';
78
+ import { arrayToTree } from 'befly-shared/arrayToTree';
65
79
 
66
80
  const router = useRouter();
67
81
  const route = useRoute();
@@ -79,7 +93,8 @@ const $Data = $ref({
79
93
  currentMenuKey: '',
80
94
  userInfo: {
81
95
  nickname: '管理员',
82
- role: '超级管理员'
96
+ role: '超级管理员',
97
+ avatar: '' // 用户头像
83
98
  }
84
99
  });
85
100
 
@@ -138,15 +153,30 @@ const $Method = {
138
153
 
139
154
  // 处理退出登录
140
155
  handleLogout() {
141
- DialogPlugin.confirm({
156
+ const dialog = DialogPlugin.confirm({
142
157
  body: '确定要退出登录吗?',
143
158
  header: '确认',
144
159
  onConfirm: () => {
160
+ dialog.destroy();
145
161
  $Storage.local.remove('token');
146
162
  router.push('/internal/login');
147
163
  MessagePlugin.success('退出成功');
148
164
  }
149
165
  });
166
+ },
167
+
168
+ // 处理系统设置
169
+ handleSettings() {
170
+ router.push('/addon/admin/settings');
171
+ },
172
+
173
+ // 头像上传成功
174
+ onAvatarUploadSuccess(res) {
175
+ if (res.response?.code === 0 && res.response?.data?.url) {
176
+ $Data.userInfo.avatar = res.response.data.url;
177
+ MessagePlugin.success('头像上传成功');
178
+ // TODO: 可以调用接口保存用户头像
179
+ }
150
180
  }
151
181
  };
152
182
 
@@ -154,107 +184,227 @@ $Method.fetchUserMenus();
154
184
  </script>
155
185
 
156
186
  <style scoped lang="scss">
157
- .layout-0-wrapper {
158
- position: absolute;
159
- top: 0;
160
- left: 0;
187
+ .layout-wrapper {
188
+ display: flex;
161
189
  height: 100vh;
162
190
  width: 100vw;
163
191
  background: var(--bg-color-page);
192
+ padding: var(--layout-gap);
193
+ gap: var(--layout-gap);
164
194
  overflow: hidden;
165
195
 
166
- .layout-header {
167
- position: absolute;
168
- top: 0;
169
- left: 0;
170
- right: 0;
171
- height: var(--header-height);
196
+ // 左侧边栏
197
+ .layout-sidebar {
198
+ width: var(--sidebar-width);
199
+ flex-shrink: 0;
172
200
  display: flex;
173
- align-items: center;
174
- justify-content: space-between;
175
- padding: 0 var(--spacing-md) 0 var(--spacing-lg);
176
- background: var(--bg-color-page);
177
- border-bottom: 1px solid var(--border-color);
178
- z-index: 100;
179
-
180
- .logo {
201
+ flex-direction: column;
202
+ background: var(--bg-color-container);
203
+ border-radius: var(--border-radius-large);
204
+ box-shadow: var(--shadow-1);
205
+ overflow: hidden;
206
+
207
+ // Logo 区域
208
+ .sidebar-logo {
209
+ display: flex;
210
+ align-items: center;
211
+ gap: var(--spacing-sm);
212
+ padding: var(--spacing-md) var(--spacing-md);
213
+ border-bottom: 1px solid var(--border-color-light);
214
+
215
+ .logo-icon {
216
+ width: 36px;
217
+ height: 36px;
218
+ min-width: 36px;
219
+ display: flex;
220
+ align-items: center;
221
+ justify-content: center;
222
+ background: var(--primary-color-light);
223
+ border-radius: var(--border-radius);
224
+ }
225
+
181
226
  h2 {
182
227
  margin: 0;
183
- font-size: 22px;
184
- font-weight: 700;
228
+ font-size: var(--font-size-md);
229
+ font-weight: var(--font-weight-semibold);
185
230
  color: var(--text-primary);
186
- letter-spacing: 0.5px;
231
+ white-space: nowrap;
232
+ overflow: hidden;
187
233
  }
188
234
  }
189
235
 
190
- .header-right {
191
- display: flex;
192
- align-items: center;
193
- gap: var(--spacing-md);
236
+ // 菜单区域
237
+ .sidebar-menu {
238
+ flex: 1;
239
+ overflow-y: auto;
240
+ padding: var(--spacing-xs) 0;
241
+
242
+ :deep(.t-menu) {
243
+ border-right: none;
244
+ background: transparent;
245
+
246
+ // 子菜单项(非父级的菜单项)
247
+ .t-menu__item {
248
+ margin: 2px var(--spacing-sm);
249
+ border-radius: var(--border-radius);
250
+ transition: all var(--transition-fast);
251
+ position: relative;
252
+
253
+ &:hover {
254
+ background-color: var(--bg-color-hover);
255
+ }
194
256
 
195
- .user-info-bar {
257
+ &.t-is-active {
258
+ background-color: var(--primary-color-light);
259
+ color: var(--primary-color);
260
+ font-weight: var(--font-weight-medium);
261
+
262
+ &::before {
263
+ content: '';
264
+ position: absolute;
265
+ left: 0;
266
+ top: 50%;
267
+ transform: translateY(-50%);
268
+ width: var(--menu-active-indicator);
269
+ height: 60%;
270
+ background-color: var(--primary-color);
271
+ border-radius: 0 2px 2px 0;
272
+ }
273
+ }
274
+ }
275
+
276
+ // 父级菜单样式(有子菜单的)
277
+ .t-submenu {
278
+ // 父级菜单的 header(不显示指示条)
279
+ > .t-menu__item,
280
+ > .t-submenu__header {
281
+ margin: 2px var(--spacing-sm);
282
+ border-radius: var(--border-radius);
283
+ transition: all var(--transition-fast);
284
+ position: relative;
285
+
286
+ &:hover {
287
+ background-color: var(--bg-color-hover);
288
+ }
289
+
290
+ // 父级菜单不显示指示条和背景
291
+ &::before {
292
+ display: none !important;
293
+ }
294
+
295
+ &.t-is-active {
296
+ background-color: transparent !important;
297
+ }
298
+ }
299
+ }
300
+ }
301
+ }
302
+
303
+ // 底部操作区域
304
+ .sidebar-footer {
305
+ border-top: 1px solid var(--border-color-light);
306
+ padding: var(--spacing-sm);
307
+
308
+ .footer-item {
309
+ display: flex;
310
+ align-items: center;
311
+ gap: var(--spacing-sm);
312
+ padding: var(--spacing-sm) var(--spacing-md);
313
+ border-radius: var(--border-radius);
314
+ color: var(--text-secondary);
315
+ cursor: pointer;
316
+ transition: all var(--transition-fast);
317
+
318
+ &:hover {
319
+ background-color: var(--bg-color-hover);
320
+ color: var(--text-primary);
321
+ }
322
+
323
+ span {
324
+ font-size: var(--font-size-sm);
325
+ white-space: nowrap;
326
+ }
327
+ }
328
+
329
+ .footer-user {
196
330
  display: flex;
197
331
  align-items: center;
198
- padding: var(--spacing-xs) var(--spacing-sm);
199
- background: var(--bg-color-container);
200
- border: 1px solid var(--border-color);
201
- border-radius: var(--border-radius-small);
332
+ gap: var(--spacing-sm);
333
+ padding: var(--spacing-sm);
334
+ margin-top: var(--spacing-xs);
335
+ background: var(--bg-color-secondarycontainer);
336
+ border-radius: var(--border-radius);
202
337
 
203
- .user-text {
338
+ .user-avatar {
339
+ width: 32px;
340
+ height: 32px;
341
+ min-width: 32px;
342
+ display: flex;
343
+ align-items: center;
344
+ justify-content: center;
345
+ background: var(--primary-color);
346
+ border-radius: 50%;
347
+ flex-shrink: 0;
348
+ cursor: pointer;
349
+ position: relative;
350
+ overflow: hidden;
351
+
352
+ img {
353
+ width: 100%;
354
+ height: 100%;
355
+ object-fit: cover;
356
+ }
357
+
358
+ .avatar-overlay {
359
+ position: absolute;
360
+ top: 0;
361
+ left: 0;
362
+ right: 0;
363
+ bottom: 0;
364
+ background: rgba(0, 0, 0, 0.5);
365
+ display: flex;
366
+ align-items: center;
367
+ justify-content: center;
368
+ opacity: 0;
369
+ transition: opacity var(--transition-fast);
370
+ }
371
+
372
+ &:hover .avatar-overlay {
373
+ opacity: 1;
374
+ }
375
+ }
376
+
377
+ .user-info {
378
+ flex: 1;
379
+ min-width: 0;
204
380
  display: flex;
205
381
  flex-direction: column;
206
- align-items: flex-start;
207
382
 
208
383
  .user-name {
209
384
  font-size: var(--font-size-sm);
210
- font-weight: 500;
385
+ font-weight: var(--font-weight-medium);
211
386
  color: var(--text-primary);
387
+ line-height: 1.3;
388
+ overflow: hidden;
389
+ text-overflow: ellipsis;
390
+ white-space: nowrap;
391
+ }
392
+
393
+ .user-role {
394
+ font-size: var(--font-size-xs);
395
+ color: var(--text-placeholder);
396
+ line-height: 1.3;
212
397
  }
213
- }
214
- .logout-btn {
215
- color: var(--text-secondary);
216
- margin-left: var(--spacing-md);
217
398
  }
218
399
  }
219
400
  }
220
401
  }
221
402
 
222
- .layout-menu {
223
- position: absolute;
224
- top: var(--header-height);
225
- left: 0;
226
- bottom: var(--footer-height);
227
- width: var(--sidebar-width);
228
- background: var(--bg-color-container);
229
- border-right: 1px solid var(--border-color);
230
- z-index: 99;
231
- overflow-y: auto;
232
- }
233
-
234
- .layout-content {
235
- position: absolute;
236
- top: var(--header-height);
237
- left: var(--sidebar-width);
238
- right: 0;
239
- bottom: var(--footer-height);
240
- background: var(--bg-color-page);
241
- overflow-y: auto;
242
- }
243
-
244
- .layout-footer {
245
- position: absolute;
246
- bottom: 0;
247
- left: 0;
248
- right: 0;
249
- height: var(--footer-height);
250
- display: flex;
251
- align-items: center;
252
- justify-content: center;
253
- background: var(--bg-color-container);
254
- border-top: 1px solid var(--border-color);
255
- color: var(--text-secondary);
256
- font-size: var(--font-size-sm);
257
- z-index: 98;
403
+ // 右侧主内容区域
404
+ .layout-main {
405
+ flex: 1;
406
+ min-width: 0;
407
+ overflow: hidden;
258
408
  }
259
409
  }
260
410
  </style>
package/src/main.js CHANGED
@@ -43,10 +43,6 @@ app.component('TTable', {
43
43
  type: [String, Number],
44
44
  default: '100%'
45
45
  },
46
- headerCellClassName: {
47
- type: String,
48
- default: 'custom-table-cell-class'
49
- },
50
46
  selectOnRowClick: {
51
47
  type: Boolean,
52
48
  default: true
@@ -1,10 +1,10 @@
1
1
  import { createRouter, createWebHashHistory } from 'vue-router';
2
2
  import { routes } from 'vue-router/auto-routes';
3
3
  import { $Storage } from '@/plugins/storage';
4
- import { Layouts } from 'befly-util/layouts';
4
+ import { Layouts } from 'befly-shared/layouts';
5
5
 
6
6
  /**
7
- * @typedef {import('befly-util').LayoutConfig} LayoutConfig
7
+ * @typedef {import('befly-shared').LayoutConfig} LayoutConfig
8
8
  */
9
9
 
10
10
  /**