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 +8 -9
- package/src/App.vue +2 -4
- package/src/components/{DetailPanel.vue → detailPanel.vue} +71 -36
- package/src/components/pageDialog.vue +223 -0
- package/src/components/{PagedTableDetailPage.vue → pagedTableDetail.vue} +157 -56
- package/src/layouts/default.vue +151 -166
- package/src/plugins/config.ts +6 -6
- package/src/plugins/http.ts +3 -4
- package/src/plugins/storage.ts +1 -1
- package/src/styles/global.scss +2 -12
- package/src/types/auto-imports.d.ts +14 -2
- package/src/types/components.d.ts +7 -3
- package/src/types/typed-router.d.ts +18 -13
- package/src/utils/tdesignPlugins.ts +57 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly-admin",
|
|
3
|
-
"version": "3.12.
|
|
4
|
-
"gitHead": "
|
|
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.
|
|
32
|
-
"@iconify-json/lucide": "^1.2.
|
|
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.
|
|
35
|
-
"befly-vite": "^1.4.
|
|
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.
|
|
41
|
-
"vue-router": "^
|
|
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
|
-
<
|
|
2
|
+
<t-config-provider :global-config="globalConfig">
|
|
3
3
|
<div id="app">
|
|
4
4
|
<RouterView />
|
|
5
5
|
</div>
|
|
6
|
-
</
|
|
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.
|
|
6
|
-
<div class="detail-label">{{ field.
|
|
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.
|
|
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.
|
|
16
|
-
<slot :name="field.
|
|
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.
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
*
|
|
81
|
+
* 标准化字段配置(仅 columns:colKey/title)
|
|
82
|
+
* 注意:模板对“嵌套 ref(对象属性里的 computed/ref)”不会自动解包,
|
|
83
|
+
* 所以这里必须暴露为顶层 computed,避免 v-for 误迭代 computed 对象本身。
|
|
73
84
|
*/
|
|
74
85
|
const normalizedFields = computed(() => {
|
|
75
|
-
const
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
.
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
121
|
+
if (typeof dataId !== "undefined" && !fields.some((f) => f.colKey === "id")) {
|
|
91
122
|
fields.unshift({
|
|
92
|
-
|
|
93
|
-
|
|
123
|
+
colKey: "id",
|
|
124
|
+
title: "ID",
|
|
94
125
|
default: "-",
|
|
95
126
|
formatter: undefined
|
|
96
127
|
});
|
|
97
128
|
}
|
|
98
129
|
|
|
99
|
-
|
|
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
|
-
|
|
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>
|