befly-admin-ui 1.8.17 → 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.
@@ -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>