befly-admin-ui 1.8.16 → 1.8.18
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/components/detailPanel.vue +195 -0
- package/components/pageDialog.vue +199 -0
- package/components/pagedTableDetail.vue +514 -0
- package/layouts/1.vue +5 -0
- package/layouts/default.vue +489 -0
- package/package.json +6 -5
- package/views/config/dict/components/edit.vue +1 -1
- package/views/config/dict/index.vue +3 -3
- package/views/config/dictType/components/edit.vue +1 -1
- package/views/config/dictType/index.vue +3 -3
- package/views/config/system/components/edit.vue +1 -1
- package/views/config/system/index.vue +4 -4
- package/views/index/components/addonList.vue +1 -0
- package/views/index/components/operationLogs.vue +1 -0
- package/views/log/email/index.vue +5 -5
- package/views/log/login/index.vue +4 -4
- package/views/log/operate/index.vue +4 -4
- package/views/people/admin/components/edit.vue +1 -1
- package/views/people/admin/index.vue +3 -3
- package/views/permission/api/index.vue +1 -1
- package/views/permission/menu/index.vue +1 -1
- package/views/permission/role/components/api.vue +1 -1
- package/views/permission/role/components/edit.vue +1 -1
- package/views/permission/role/components/menu.vue +1 -1
- package/views/permission/role/index.vue +3 -3
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="detail-panel">
|
|
3
|
+
<div class="detail-content">
|
|
4
|
+
<div v-if="data">
|
|
5
|
+
<div v-for="field in normalizedFields" :key="field.colKey" class="detail-item">
|
|
6
|
+
<div class="detail-label">{{ field.title }}</div>
|
|
7
|
+
<div class="detail-value">
|
|
8
|
+
<!-- 状态字段特殊处理 -->
|
|
9
|
+
<template v-if="field.colKey === 'state'">
|
|
10
|
+
<TTag v-if="data.state === 1" shape="round" theme="success" variant="light-outline">正常</TTag>
|
|
11
|
+
<TTag v-else-if="data.state === 2" shape="round" theme="warning" variant="light-outline">禁用</TTag>
|
|
12
|
+
<TTag v-else-if="data.state === 0" shape="round" theme="danger" variant="light-outline">已删除</TTag>
|
|
13
|
+
</template>
|
|
14
|
+
<!-- 自定义插槽 -->
|
|
15
|
+
<template v-else-if="$slots[field.colKey]">
|
|
16
|
+
<slot :name="field.colKey" :value="data[field.colKey]" :row="data"></slot>
|
|
17
|
+
</template>
|
|
18
|
+
<!-- 默认显示 -->
|
|
19
|
+
<template v-else>
|
|
20
|
+
{{ formatValue(data[field.colKey], field) }}
|
|
21
|
+
</template>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<div v-else class="detail-empty">
|
|
26
|
+
<div class="empty-icon">📋</div>
|
|
27
|
+
<div class="empty-text">{{ emptyText }}</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup>
|
|
34
|
+
import { computed } from "vue";
|
|
35
|
+
import { Tag as TTag } from "tdesign-vue-next";
|
|
36
|
+
|
|
37
|
+
const props = defineProps({
|
|
38
|
+
/**
|
|
39
|
+
* 当前行数据
|
|
40
|
+
*/
|
|
41
|
+
data: {
|
|
42
|
+
type: Object,
|
|
43
|
+
default: null
|
|
44
|
+
},
|
|
45
|
+
/**
|
|
46
|
+
* 字段配置(columns 格式):[{ colKey: 'id', title: 'ID' }]
|
|
47
|
+
* 自动过滤 row-select、operation 等非数据列
|
|
48
|
+
*/
|
|
49
|
+
fields: {
|
|
50
|
+
type: Array,
|
|
51
|
+
required: true
|
|
52
|
+
},
|
|
53
|
+
/**
|
|
54
|
+
* 需要过滤的列 key
|
|
55
|
+
*/
|
|
56
|
+
excludeKeys: {
|
|
57
|
+
type: Array,
|
|
58
|
+
default: () => ["row-select", "operation", "index"]
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* 空数据时的提示文字
|
|
62
|
+
*/
|
|
63
|
+
emptyText: {
|
|
64
|
+
type: String,
|
|
65
|
+
default: "暂无数据"
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 标准化字段配置(仅 columns:colKey/title)
|
|
71
|
+
* 注意:模板对“嵌套 ref(对象属性里的 computed/ref)”不会自动解包,
|
|
72
|
+
* 所以这里必须暴露为顶层 computed,避免 v-for 误迭代 computed 对象本身。
|
|
73
|
+
*/
|
|
74
|
+
const normalizedFields = computed(() => {
|
|
75
|
+
const row = props.data && typeof props.data === "object" ? props.data : null;
|
|
76
|
+
const dataId = row && Object.hasOwn(row, "id") ? row.id : undefined;
|
|
77
|
+
|
|
78
|
+
const rawFields = props.fields;
|
|
79
|
+
const inputFields = Array.isArray(rawFields) ? rawFields : [];
|
|
80
|
+
const excludeKeys = Array.isArray(props.excludeKeys) ? props.excludeKeys : [];
|
|
81
|
+
|
|
82
|
+
const fields = inputFields
|
|
83
|
+
.filter((item) => {
|
|
84
|
+
return Boolean(item) && typeof item === "object";
|
|
85
|
+
})
|
|
86
|
+
.map((item) => {
|
|
87
|
+
const colKey = item.colKey;
|
|
88
|
+
if (item.colKey.length === 0) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (excludeKeys.includes(item.colKey)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
colKey: item.colKey,
|
|
97
|
+
title: item.title || item.colKey,
|
|
98
|
+
default: item.default,
|
|
99
|
+
formatter: item.formatter
|
|
100
|
+
};
|
|
101
|
+
})
|
|
102
|
+
.filter((item) => {
|
|
103
|
+
return Boolean(item);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// 约定:页面表格不展示 id,但右侧详情始终展示 id(如果 data.id 存在)
|
|
107
|
+
if (typeof dataId !== "undefined" && !fields.some((f) => f.colKey === "id")) {
|
|
108
|
+
fields.unshift({
|
|
109
|
+
colKey: "id",
|
|
110
|
+
title: "ID",
|
|
111
|
+
default: "-",
|
|
112
|
+
formatter: undefined
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const safeFields = fields.filter((item) => {
|
|
117
|
+
if (!item || typeof item !== "object") {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
return item.colKey?.length > 0;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return safeFields;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
function formatValue(value, field) {
|
|
127
|
+
if (value === null || value === undefined || value === "") {
|
|
128
|
+
return field.default || "-";
|
|
129
|
+
}
|
|
130
|
+
if (field.formatter) {
|
|
131
|
+
const result = field.formatter(value);
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<style scoped lang="scss">
|
|
139
|
+
.detail-panel {
|
|
140
|
+
height: 100%;
|
|
141
|
+
overflow: auto;
|
|
142
|
+
background: var(--bg-color-container);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.detail-content {
|
|
146
|
+
padding: var(--spacing-md);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.detail-item {
|
|
150
|
+
margin-bottom: var(--spacing-sm);
|
|
151
|
+
padding: var(--spacing-sm) 0;
|
|
152
|
+
border-bottom: 1px solid var(--border-color-light);
|
|
153
|
+
|
|
154
|
+
&:first-child {
|
|
155
|
+
padding-top: 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
&:last-child {
|
|
159
|
+
margin-bottom: 0;
|
|
160
|
+
padding-bottom: 0;
|
|
161
|
+
border-bottom: none;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.detail-label {
|
|
166
|
+
color: var(--text-secondary);
|
|
167
|
+
margin-bottom: 6px;
|
|
168
|
+
font-size: var(--font-size-xs);
|
|
169
|
+
font-weight: var(--font-weight-medium);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.detail-value {
|
|
173
|
+
color: var(--text-primary);
|
|
174
|
+
font-size: var(--font-size-sm);
|
|
175
|
+
font-weight: var(--font-weight-medium);
|
|
176
|
+
word-break: break-all;
|
|
177
|
+
line-height: 1.5;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.detail-empty {
|
|
181
|
+
text-align: center;
|
|
182
|
+
padding: var(--spacing-xl) 0;
|
|
183
|
+
color: var(--text-placeholder);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.empty-icon {
|
|
187
|
+
font-size: 40px;
|
|
188
|
+
margin-bottom: var(--spacing-sm);
|
|
189
|
+
opacity: 0.5;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.empty-text {
|
|
193
|
+
font-size: var(--font-size-sm);
|
|
194
|
+
}
|
|
195
|
+
</style>
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<TDialog v-model:visible="innerVisible" :header="props.title === '' ? true : props.title" :width="props.width" placement="center" attach="body" :close-btn="false" :footer="true" :confirm-loading="props.confirmLoading" :close-on-overlay-click="false" :close-on-esc-keydown="false" @close="onDialogClose" @confirm="onDialogConfirm" @cancel="onDialogCancel">
|
|
3
|
+
<template #footer>
|
|
4
|
+
<div class="dialog-footer">
|
|
5
|
+
<TButton variant="outline" @click="onFooterClose">关闭</TButton>
|
|
6
|
+
<TPopconfirm v-if="props.isConfirm" content="确定要提交吗?" :disabled="props.confirmLoading" @confirm="onFooterConfirm">
|
|
7
|
+
<TButton theme="primary" :loading="props.confirmLoading">确定</TButton>
|
|
8
|
+
</TPopconfirm>
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
<div class="dialog-wrapper">
|
|
12
|
+
<slot :visible="innerVisible" :open="open" :close="close" :toggle="toggle" />
|
|
13
|
+
</div>
|
|
14
|
+
</TDialog>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup>
|
|
18
|
+
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
19
|
+
|
|
20
|
+
import { Button as TButton, Dialog as TDialog, Popconfirm as TPopconfirm } from "tdesign-vue-next";
|
|
21
|
+
|
|
22
|
+
defineOptions({
|
|
23
|
+
inheritAttrs: false
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const openDelay = 100;
|
|
27
|
+
const closeDelay = 300;
|
|
28
|
+
|
|
29
|
+
const props = defineProps({
|
|
30
|
+
modelValue: {
|
|
31
|
+
type: Boolean,
|
|
32
|
+
default: undefined
|
|
33
|
+
},
|
|
34
|
+
title: {
|
|
35
|
+
type: String,
|
|
36
|
+
default: ""
|
|
37
|
+
},
|
|
38
|
+
width: {
|
|
39
|
+
type: String,
|
|
40
|
+
default: "600px"
|
|
41
|
+
},
|
|
42
|
+
confirmLoading: {
|
|
43
|
+
type: Boolean,
|
|
44
|
+
default: false
|
|
45
|
+
},
|
|
46
|
+
isConfirm: {
|
|
47
|
+
type: Boolean,
|
|
48
|
+
default: true
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const emit = defineEmits(["update:modelValue", "close", "confirm", "cancel"]);
|
|
53
|
+
|
|
54
|
+
let openDelayTimer = null;
|
|
55
|
+
let closeDelayTimer = null;
|
|
56
|
+
|
|
57
|
+
function clearTimer(timer) {
|
|
58
|
+
if (timer) {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 所有弹框约定:始终使用 v-if + v-model(modelValue)
|
|
65
|
+
const innerVisible = ref(false);
|
|
66
|
+
|
|
67
|
+
function open() {
|
|
68
|
+
emit("update:modelValue", true);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function close() {
|
|
72
|
+
openDelayTimer = clearTimer(openDelayTimer);
|
|
73
|
+
closeDelayTimer = clearTimer(closeDelayTimer);
|
|
74
|
+
|
|
75
|
+
innerVisible.value = false;
|
|
76
|
+
// 延迟通知上层关闭:配合 v-if,保证离场动画播完再卸载
|
|
77
|
+
closeDelayTimer = setTimeout(() => {
|
|
78
|
+
emit("update:modelValue", false);
|
|
79
|
+
closeDelayTimer = null;
|
|
80
|
+
}, closeDelay);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function toggle() {
|
|
84
|
+
if (innerVisible.value) {
|
|
85
|
+
close();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
open();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function createEventContext(trigger) {
|
|
92
|
+
return {
|
|
93
|
+
trigger: trigger,
|
|
94
|
+
visible: innerVisible.value,
|
|
95
|
+
open: open,
|
|
96
|
+
close: close,
|
|
97
|
+
toggle: toggle,
|
|
98
|
+
getVisible: () => innerVisible.value
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function applyModelValue(value) {
|
|
103
|
+
if (value) {
|
|
104
|
+
// 关键点:先渲染为 false,再延迟 true,保证 v-if + 初次 visible=true 时也能触发进入动画
|
|
105
|
+
openDelayTimer = clearTimer(openDelayTimer);
|
|
106
|
+
innerVisible.value = false;
|
|
107
|
+
openDelayTimer = setTimeout(() => {
|
|
108
|
+
innerVisible.value = true;
|
|
109
|
+
openDelayTimer = null;
|
|
110
|
+
}, openDelay);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
openDelayTimer = clearTimer(openDelayTimer);
|
|
115
|
+
innerVisible.value = false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
onMounted(() => {
|
|
119
|
+
if (props.modelValue === true) {
|
|
120
|
+
applyModelValue(true);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
onBeforeUnmount(() => {
|
|
125
|
+
openDelayTimer = clearTimer(openDelayTimer);
|
|
126
|
+
closeDelayTimer = clearTimer(closeDelayTimer);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
watch(
|
|
130
|
+
() => props.modelValue,
|
|
131
|
+
(value) => {
|
|
132
|
+
if (value !== true && value !== false) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
applyModelValue(value);
|
|
137
|
+
},
|
|
138
|
+
{ immediate: false }
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
function closeByTrigger(trigger) {
|
|
142
|
+
// 固定交互:confirm 不自动关闭(等待异步提交成功后由调用侧 context.close() 决定关闭)
|
|
143
|
+
if (trigger === "confirm") {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// cancel/close 自动关闭
|
|
147
|
+
close();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function onDialogClose() {
|
|
151
|
+
const context = createEventContext("close");
|
|
152
|
+
closeByTrigger("close");
|
|
153
|
+
emit("close", context);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function onDialogConfirm() {
|
|
157
|
+
const context = createEventContext("confirm");
|
|
158
|
+
emit("confirm", context);
|
|
159
|
+
closeByTrigger("confirm");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function onDialogCancel() {
|
|
163
|
+
const context = createEventContext("cancel");
|
|
164
|
+
emit("cancel", context);
|
|
165
|
+
closeByTrigger("cancel");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function onFooterConfirm() {
|
|
169
|
+
const context = createEventContext("confirm");
|
|
170
|
+
emit("confirm", context);
|
|
171
|
+
closeByTrigger("confirm");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function onFooterClose() {
|
|
175
|
+
const context = createEventContext("cancel");
|
|
176
|
+
emit("cancel", context);
|
|
177
|
+
closeByTrigger("cancel");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
defineExpose({
|
|
181
|
+
open: open,
|
|
182
|
+
close: close,
|
|
183
|
+
toggle: toggle,
|
|
184
|
+
getVisible: () => innerVisible.value
|
|
185
|
+
});
|
|
186
|
+
</script>
|
|
187
|
+
|
|
188
|
+
<style lang="scss" scoped>
|
|
189
|
+
.dialog-wrapper {
|
|
190
|
+
background-color: var(--bg-color-page);
|
|
191
|
+
padding: var(--spacing-md);
|
|
192
|
+
border-radius: var(--border-radius);
|
|
193
|
+
}
|
|
194
|
+
.dialog-footer {
|
|
195
|
+
width: 100%;
|
|
196
|
+
display: flex;
|
|
197
|
+
justify-content: center;
|
|
198
|
+
}
|
|
199
|
+
</style>
|