befly-admin 3.4.55 → 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.
package/.env CHANGED
@@ -1,4 +1,4 @@
1
1
  # 所有环境共享的基础配置
2
2
 
3
3
  # 应用标题
4
- VITE_APP_TITLE=野蜂飞舞后台管理
4
+ VITE_APP_TITLE=野蜂飞舞
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly-admin",
3
- "version": "3.4.55",
3
+ "version": "3.4.56",
4
4
  "description": "Befly Admin - 基于 Vue3 + OpenTiny Vue 的后台管理系统",
5
5
  "type": "module",
6
6
  "private": false,
@@ -26,11 +26,11 @@
26
26
  "preview": "vite preview"
27
27
  },
28
28
  "dependencies": {
29
- "@befly-addon/admin": "^1.0.55",
29
+ "@befly-addon/admin": "^1.0.56",
30
30
  "@iconify-json/lucide": "^1.2.76",
31
31
  "axios": "^1.13.2",
32
- "befly-shared": "^1.1.1",
33
- "befly-vite": "^1.0.13",
32
+ "befly-shared": "^1.1.2",
33
+ "befly-vite": "^1.0.14",
34
34
  "pinia": "^3.0.4",
35
35
  "tdesign-vue-next": "^1.17.5",
36
36
  "vite": "^7.2.4",
@@ -42,5 +42,5 @@
42
42
  "pnpm": ">=10.0.0",
43
43
  "bun": ">=1.3.0"
44
44
  },
45
- "gitHead": "2031550167896390ac58c50aeb52b8c23426844e"
45
+ "gitHead": "d9a4c57539f6eb692d7db6c7fd800e021891a07c"
46
46
  }
@@ -2,7 +2,7 @@
2
2
  <div class="detail-panel">
3
3
  <div class="detail-content">
4
4
  <div v-if="data">
5
- <div v-for="field in fields" :key="field.key" class="detail-item">
5
+ <div v-for="field in normalizedFields" :key="field.key" class="detail-item">
6
6
  <div class="detail-label">{{ field.label }}</div>
7
7
  <div class="detail-value">
8
8
  <!-- 状态字段特殊处理 -->
@@ -31,9 +31,10 @@
31
31
  </template>
32
32
 
33
33
  <script setup>
34
+ import { computed } from 'vue';
34
35
  import { Tag as TTag } from 'tdesign-vue-next';
35
36
 
36
- defineProps({
37
+ const props = defineProps({
37
38
  /**
38
39
  * 当前行数据
39
40
  */
@@ -42,13 +43,22 @@ defineProps({
42
43
  default: null
43
44
  },
44
45
  /**
45
- * 字段配置
46
- * @example [{ key: 'id', label: 'ID' }, { key: 'name', label: '名称', default: '-' }]
46
+ * 字段配置,支持两种格式:
47
+ * 1. fields 格式: [{ key: 'id', label: 'ID' }]
48
+ * 2. columns 格式: [{ colKey: 'id', title: 'ID' }]
49
+ * 自动过滤 row-select、operation 等非数据列
47
50
  */
48
51
  fields: {
49
52
  type: Array,
50
53
  required: true
51
54
  },
55
+ /**
56
+ * 需要过滤的列 key
57
+ */
58
+ excludeKeys: {
59
+ type: Array,
60
+ default: () => ['row-select', 'operation', 'index']
61
+ },
52
62
  /**
53
63
  * 空数据时的提示文字
54
64
  */
@@ -58,6 +68,23 @@ defineProps({
58
68
  }
59
69
  });
60
70
 
71
+ /**
72
+ * 标准化字段配置,支持 columns 和 fields 两种格式
73
+ */
74
+ const normalizedFields = computed(() => {
75
+ return props.fields
76
+ .filter((item) => {
77
+ const key = item.colKey || item.key;
78
+ return key && !props.excludeKeys.includes(key);
79
+ })
80
+ .map((item) => ({
81
+ key: item.colKey || item.key,
82
+ label: item.title || item.label,
83
+ default: item.default,
84
+ formatter: item.formatter
85
+ }));
86
+ });
87
+
61
88
  /**
62
89
  * 格式化字段值
63
90
  * @param {any} value - 字段值
@@ -79,44 +106,57 @@ function formatValue(value, field) {
79
106
  .detail-panel {
80
107
  height: 100%;
81
108
  overflow: auto;
109
+ background: var(--bg-color-container);
82
110
  }
83
111
 
84
112
  .detail-content {
85
- padding: 16px;
113
+ padding: var(--spacing-md);
86
114
  }
87
115
 
88
116
  .detail-item {
89
- margin-bottom: 16px;
117
+ margin-bottom: var(--spacing-sm);
118
+ padding: var(--spacing-sm) 0;
119
+ border-bottom: 1px solid var(--border-color-light);
120
+
121
+ &:first-child {
122
+ padding-top: 0;
123
+ }
90
124
 
91
125
  &:last-child {
92
126
  margin-bottom: 0;
127
+ padding-bottom: 0;
128
+ border-bottom: none;
93
129
  }
94
130
  }
95
131
 
96
132
  .detail-label {
97
133
  color: var(--text-secondary);
98
- margin-bottom: 4px;
99
- font-size: 12px;
134
+ margin-bottom: 6px;
135
+ font-size: var(--font-size-xs);
136
+ font-weight: var(--font-weight-medium);
100
137
  }
101
138
 
102
139
  .detail-value {
103
140
  color: var(--text-primary);
104
- font-size: 14px;
141
+ font-size: var(--font-size-sm);
142
+ font-weight: var(--font-weight-medium);
105
143
  word-break: break-all;
144
+ line-height: 1.5;
106
145
  }
107
146
 
108
147
  .detail-empty {
109
148
  text-align: center;
110
- padding: 48px 0;
149
+ padding: var(--spacing-xl) 0;
111
150
  color: var(--text-placeholder);
112
151
  }
113
152
 
114
153
  .empty-icon {
115
- font-size: 48px;
116
- margin-bottom: 8px;
154
+ font-size: 40px;
155
+ margin-bottom: var(--spacing-sm);
156
+ opacity: 0.5;
117
157
  }
118
158
 
119
159
  .empty-text {
120
- font-size: 14px;
160
+ font-size: var(--font-size-sm);
121
161
  }
122
162
  </style>
@@ -8,7 +8,7 @@
8
8
  */
9
9
  export const $Config = {
10
10
  /** 应用标题 */
11
- appTitle: import.meta.env.VITE_APP_TITLE || '管理后台',
11
+ appTitle: import.meta.env.VITE_APP_TITLE || '野蜂飞舞',
12
12
  /** API 基础地址 */
13
13
  apiBaseUrl: import.meta.env.VITE_API_BASE_URL || '',
14
14
  /** 存储命名空间 */
@@ -1,62 +1,76 @@
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
 
@@ -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