befly-admin 3.12.4 → 3.12.6

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "befly-admin",
3
- "version": "3.12.4",
4
- "gitHead": "9823cc29996ba9950dd08bfd72856e3b65815039",
3
+ "version": "3.12.6",
4
+ "gitHead": "607fe5ea9c06653ec6da8a9e4175d1f9c7b98c13",
5
5
  "private": false,
6
6
  "description": "Befly Admin - 基于 Vue3 + TDesign Vue Next 的后台管理系统",
7
7
  "files": [
@@ -28,17 +28,16 @@
28
28
  "preview": "vite preview"
29
29
  },
30
30
  "dependencies": {
31
- "@befly-addon/admin": "^1.8.4",
32
- "@iconify-json/lucide": "^1.2.85",
31
+ "@befly-addon/admin": "^1.8.5",
32
+ "@iconify-json/lucide": "^1.2.86",
33
33
  "axios": "^1.13.2",
34
- "befly-shared": "1.4.3",
35
- "befly-vite": "^1.4.6",
34
+ "befly-shared": "^1.4.5",
35
+ "befly-vite": "^1.4.8",
36
36
  "pinia": "^3.0.4",
37
37
  "tdesign-vue-next": "^1.18.0",
38
- "unplugin-vue-router": "^0.19.2",
39
38
  "vite": "^8.0.0-beta.8",
40
- "vue": "^3.5.26",
41
- "vue-router": "^4.6.4"
39
+ "vue": "^3.5.27",
40
+ "vue-router": "^5.0.0-beta.0"
42
41
  },
43
42
  "engines": {
44
43
  "bun": ">=1.3.0",
package/src/App.vue CHANGED
@@ -1,14 +1,12 @@
1
1
  <template>
2
- <ConfigProvider :global-config="globalConfig">
2
+ <t-config-provider :global-config="globalConfig">
3
3
  <div id="app">
4
4
  <RouterView />
5
5
  </div>
6
- </ConfigProvider>
6
+ </t-config-provider>
7
7
  </template>
8
8
 
9
9
  <script setup lang="ts">
10
- import { ConfigProvider } from "tdesign-vue-next";
11
-
12
10
  const globalConfig = {
13
11
  dialog: {
14
12
  closeOnOverlayClick: false
@@ -2,22 +2,22 @@
2
2
  <div class="detail-panel">
3
3
  <div class="detail-content">
4
4
  <div v-if="data">
5
- <div v-for="field in normalizedFields" :key="field.key" class="detail-item">
6
- <div class="detail-label">{{ field.label }}</div>
5
+ <div v-for="field in normalizedFields" :key="field.colKey" class="detail-item">
6
+ <div class="detail-label">{{ field.title }}</div>
7
7
  <div class="detail-value">
8
8
  <!-- 状态字段特殊处理 -->
9
- <template v-if="field.key === 'state'">
9
+ <template v-if="field.colKey === 'state'">
10
10
  <TTag v-if="data.state === 1" shape="round" theme="success" variant="light-outline">正常</TTag>
11
11
  <TTag v-else-if="data.state === 2" shape="round" theme="warning" variant="light-outline">禁用</TTag>
12
12
  <TTag v-else-if="data.state === 0" shape="round" theme="danger" variant="light-outline">已删除</TTag>
13
13
  </template>
14
14
  <!-- 自定义插槽 -->
15
- <template v-else-if="$slots[field.key]">
16
- <slot :name="field.key" :value="data[field.key]" :row="data"></slot>
15
+ <template v-else-if="$slots[field.colKey]">
16
+ <slot :name="field.colKey" :value="data[field.colKey]" :row="data"></slot>
17
17
  </template>
18
18
  <!-- 默认显示 -->
19
19
  <template v-else>
20
- {{ formatValue(data[field.key], field) }}
20
+ {{ formatValue(data[field.colKey], field) }}
21
21
  </template>
22
22
  </div>
23
23
  </div>
@@ -31,8 +31,19 @@
31
31
  </template>
32
32
 
33
33
  <script setup lang="ts">
34
- import { computed } from "vue";
35
- import { Tag as TTag } from "tdesign-vue-next";
34
+ type DetailPanelFieldInput = {
35
+ colKey?: string;
36
+ title?: string;
37
+ default?: string;
38
+ formatter?: (value: unknown) => unknown;
39
+ };
40
+
41
+ type DetailPanelFieldNormalized = {
42
+ colKey: string;
43
+ title: string;
44
+ default?: string;
45
+ formatter?: (value: unknown) => unknown;
46
+ };
36
47
 
37
48
  const props = defineProps({
38
49
  /**
@@ -43,9 +54,7 @@ const props = defineProps({
43
54
  default: null
44
55
  },
45
56
  /**
46
- * 字段配置,支持两种格式:
47
- * 1. fields 格式: [{ key: 'id', label: 'ID' }]
48
- * 2. columns 格式: [{ colKey: 'id', title: 'ID' }]
57
+ * 字段配置(columns 格式):[{ colKey: 'id', title: 'ID' }]
49
58
  * 自动过滤 row-select、operation 等非数据列
50
59
  */
51
60
  fields: {
@@ -69,50 +78,76 @@ const props = defineProps({
69
78
  });
70
79
 
71
80
  /**
72
- * 标准化字段配置,支持 columns 和 fields 两种格式
81
+ * 标准化字段配置(仅 columns:colKey/title)
82
+ * 注意:模板对“嵌套 ref(对象属性里的 computed/ref)”不会自动解包,
83
+ * 所以这里必须暴露为顶层 computed,避免 v-for 误迭代 computed 对象本身。
73
84
  */
74
85
  const normalizedFields = computed(() => {
75
- const dataId = props.data && typeof props.data.id !== "undefined" ? props.data.id : undefined;
86
+ const row = props.data as Record<string, unknown> | null;
87
+ const dataId = row && Object.hasOwn(row, "id") ? row.id : undefined;
76
88
 
77
- const fields = props.fields
78
- .filter((item) => {
79
- const key = item.colKey || item.key;
80
- return key && !props.excludeKeys.includes(key);
89
+ const rawFields = props.fields as unknown;
90
+ const inputFields = Array.isArray(rawFields) ? rawFields : [];
91
+ const excludeKeys = props.excludeKeys as Array<string>;
92
+
93
+ const fields = inputFields
94
+ .filter((item): item is DetailPanelFieldInput => {
95
+ return Boolean(item) && typeof item === "object";
96
+ })
97
+ .map((item): DetailPanelFieldNormalized | null => {
98
+ const colKey = typeof item.colKey === "string" ? item.colKey : "";
99
+ if (colKey.length === 0) {
100
+ return null;
101
+ }
102
+ if (excludeKeys.includes(colKey)) {
103
+ return null;
104
+ }
105
+
106
+ const titleRaw = item.title;
107
+ const title = typeof titleRaw === "string" && titleRaw.length > 0 ? titleRaw : colKey;
108
+
109
+ return {
110
+ colKey: colKey,
111
+ title: title,
112
+ default: item.default,
113
+ formatter: item.formatter
114
+ };
81
115
  })
82
- .map((item) => ({
83
- key: item.colKey || item.key,
84
- label: item.title || item.label,
85
- default: item.default,
86
- formatter: item.formatter
87
- }));
116
+ .filter((item): item is DetailPanelFieldNormalized => {
117
+ return Boolean(item);
118
+ });
88
119
 
89
120
  // 约定:页面表格不展示 id,但右侧详情始终展示 id(如果 data.id 存在)
90
- if (typeof dataId !== "undefined" && !fields.some((f) => f.key === "id")) {
121
+ if (typeof dataId !== "undefined" && !fields.some((f) => f.colKey === "id")) {
91
122
  fields.unshift({
92
- key: "id",
93
- label: "ID",
123
+ colKey: "id",
124
+ title: "ID",
94
125
  default: "-",
95
126
  formatter: undefined
96
127
  });
97
128
  }
98
129
 
99
- return fields;
130
+ const safeFields = fields.filter((item) => {
131
+ if (!item || typeof item !== "object") {
132
+ return false;
133
+ }
134
+ const record = item as unknown as Record<string, unknown>;
135
+ const colKey = record["colKey"];
136
+ return typeof colKey === "string" && colKey.length > 0;
137
+ });
138
+
139
+ return safeFields;
100
140
  });
101
141
 
102
- /**
103
- * 格式化字段值
104
- * @param {any} value - 字段值
105
- * @param {Object} field - 字段配置
106
- * @returns {string} 格式化后的值
107
- */
108
- function formatValue(value, field) {
142
+ function formatValue(value: unknown, field: DetailPanelFieldNormalized) {
109
143
  if (value === null || value === undefined || value === "") {
110
144
  return field.default || "-";
111
145
  }
112
146
  if (field.formatter) {
113
- return field.formatter(value);
147
+ const result = field.formatter(value);
148
+ return typeof result === "string" ? result : String(result);
114
149
  }
115
- return value;
150
+ return typeof value === "string" ? value : String(value);
116
151
  }
117
152
  </script>
118
153
 
@@ -0,0 +1,223 @@
1
+ <template>
2
+ <TDialog
3
+ v-model:visible="innerVisible"
4
+ :header="props.title === '' ? true : props.title"
5
+ :width="props.width"
6
+ placement="center"
7
+ attach="body"
8
+ :close-btn="false"
9
+ :footer="true"
10
+ :confirm-loading="props.confirmLoading"
11
+ :close-on-overlay-click="false"
12
+ :close-on-esc-keydown="false"
13
+ @close="onDialogClose"
14
+ @confirm="onDialogConfirm"
15
+ @cancel="onDialogCancel"
16
+ >
17
+ <template #footer>
18
+ <div class="dialog-footer">
19
+ <TButton variant="outline" @click="onFooterClose">关闭</TButton>
20
+ <TPopconfirm v-if="props.isConfirm" content="确定要提交吗?" :disabled="props.confirmLoading" @confirm="onFooterConfirm">
21
+ <TButton theme="primary" :loading="props.confirmLoading">确定</TButton>
22
+ </TPopconfirm>
23
+ </div>
24
+ </template>
25
+ <div class="dialog-wrapper">
26
+ <slot :visible="innerVisible" :open="open" :close="close" :toggle="toggle" />
27
+ </div>
28
+ </TDialog>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ import { onBeforeUnmount, onMounted, ref, watch } from "vue";
33
+
34
+ import { Button as TButton, Dialog as TDialog, Popconfirm as TPopconfirm } from "tdesign-vue-next";
35
+
36
+ defineOptions({
37
+ inheritAttrs: false
38
+ });
39
+
40
+ type DialogTrigger = "close" | "confirm" | "cancel";
41
+
42
+ type PageDialogEventContext = {
43
+ trigger: DialogTrigger;
44
+ visible: boolean;
45
+ open: () => void;
46
+ close: () => void;
47
+ toggle: () => void;
48
+ getVisible: () => boolean;
49
+ };
50
+
51
+ const openDelay = 100;
52
+ const closeDelay = 300;
53
+
54
+ const props = withDefaults(
55
+ defineProps<{
56
+ // 受控模式:传 v-model / modelValue 时由上层控制;组件会帮你处理关闭时的 update
57
+ modelValue?: boolean;
58
+ title?: string;
59
+ width?: string;
60
+ confirmLoading?: boolean;
61
+ isConfirm?: boolean;
62
+ }>(),
63
+ {
64
+ title: "",
65
+ width: "600px",
66
+ confirmLoading: false,
67
+ isConfirm: true
68
+ }
69
+ );
70
+
71
+ const emit = defineEmits<{
72
+ (e: "update:modelValue", value: boolean): void;
73
+ (e: "close", context: PageDialogEventContext): void;
74
+ (e: "confirm", context: PageDialogEventContext): void;
75
+ (e: "cancel", context: PageDialogEventContext): void;
76
+ }>();
77
+
78
+ let openDelayTimer: ReturnType<typeof setTimeout> | null = null;
79
+ let closeDelayTimer: ReturnType<typeof setTimeout> | null = null;
80
+
81
+ function clearTimer(timer: ReturnType<typeof setTimeout> | null): null {
82
+ if (timer) {
83
+ clearTimeout(timer);
84
+ }
85
+ return null;
86
+ }
87
+
88
+ // 所有弹框约定:始终使用 v-if + v-model(modelValue)
89
+ const innerVisible = ref<boolean>(false);
90
+
91
+ function open(): void {
92
+ emit("update:modelValue", true);
93
+ }
94
+
95
+ function close(): void {
96
+ openDelayTimer = clearTimer(openDelayTimer);
97
+ closeDelayTimer = clearTimer(closeDelayTimer);
98
+
99
+ innerVisible.value = false;
100
+ // 延迟通知上层关闭:配合 v-if,保证离场动画播完再卸载
101
+ closeDelayTimer = setTimeout(() => {
102
+ emit("update:modelValue", false);
103
+ closeDelayTimer = null;
104
+ }, closeDelay);
105
+ }
106
+
107
+ function toggle(): void {
108
+ if (innerVisible.value) {
109
+ close();
110
+ return;
111
+ }
112
+ open();
113
+ }
114
+
115
+ function createEventContext(trigger: DialogTrigger): PageDialogEventContext {
116
+ return {
117
+ trigger: trigger,
118
+ visible: innerVisible.value,
119
+ open: open,
120
+ close: close,
121
+ toggle: toggle,
122
+ getVisible: () => innerVisible.value
123
+ };
124
+ }
125
+
126
+ function applyModelValue(value: boolean): void {
127
+ if (value) {
128
+ // 关键点:先渲染为 false,再延迟 true,保证 v-if + 初次 visible=true 时也能触发进入动画
129
+ openDelayTimer = clearTimer(openDelayTimer);
130
+ innerVisible.value = false;
131
+ openDelayTimer = setTimeout(() => {
132
+ innerVisible.value = true;
133
+ openDelayTimer = null;
134
+ }, openDelay);
135
+ return;
136
+ }
137
+
138
+ openDelayTimer = clearTimer(openDelayTimer);
139
+ innerVisible.value = false;
140
+ }
141
+
142
+ onMounted(() => {
143
+ if (props.modelValue === true) {
144
+ applyModelValue(true);
145
+ }
146
+ });
147
+
148
+ onBeforeUnmount(() => {
149
+ openDelayTimer = clearTimer(openDelayTimer);
150
+ closeDelayTimer = clearTimer(closeDelayTimer);
151
+ });
152
+
153
+ watch(
154
+ () => props.modelValue,
155
+ (value) => {
156
+ if (value !== true && value !== false) {
157
+ return;
158
+ }
159
+
160
+ applyModelValue(value);
161
+ },
162
+ { immediate: false }
163
+ );
164
+
165
+ function closeByTrigger(trigger: DialogTrigger): void {
166
+ // 固定交互:confirm 不自动关闭(等待异步提交成功后由调用侧 context.close() 决定关闭)
167
+ if (trigger === "confirm") {
168
+ return;
169
+ }
170
+ // cancel/close 自动关闭
171
+ close();
172
+ }
173
+
174
+ function onDialogClose(): void {
175
+ const context = createEventContext("close");
176
+ closeByTrigger("close");
177
+ emit("close", context);
178
+ }
179
+
180
+ function onDialogConfirm(): void {
181
+ const context = createEventContext("confirm");
182
+ emit("confirm", context);
183
+ closeByTrigger("confirm");
184
+ }
185
+
186
+ function onDialogCancel(): void {
187
+ const context = createEventContext("cancel");
188
+ emit("cancel", context);
189
+ closeByTrigger("cancel");
190
+ }
191
+
192
+ function onFooterConfirm(): void {
193
+ const context = createEventContext("confirm");
194
+ emit("confirm", context);
195
+ closeByTrigger("confirm");
196
+ }
197
+
198
+ function onFooterClose(): void {
199
+ const context = createEventContext("cancel");
200
+ emit("cancel", context);
201
+ closeByTrigger("cancel");
202
+ }
203
+
204
+ defineExpose({
205
+ open: open,
206
+ close: close,
207
+ toggle: toggle,
208
+ getVisible: () => innerVisible.value
209
+ });
210
+ </script>
211
+
212
+ <style lang="scss" scoped>
213
+ .dialog-wrapper {
214
+ background-color: var(--bg-color-page);
215
+ padding: var(--spacing-md);
216
+ border-radius: var(--border-radius);
217
+ }
218
+ .dialog-footer {
219
+ width: 100%;
220
+ display: flex;
221
+ justify-content: center;
222
+ }
223
+ </style>