befly-admin 3.13.6 → 3.13.8

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.
@@ -1,199 +0,0 @@
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>
@@ -1,512 +0,0 @@
1
- <template>
2
- <div class="page-table-detail page-table">
3
- <div class="main-tool">
4
- <div class="left">
5
- <slot name="toolLeft" :reload="reload" :current-row="$Data.currentRow"></slot>
6
- </div>
7
- <div class="right">
8
- <slot name="toolRight" :reload="reload" :current-row="$Data.currentRow"></slot>
9
- </div>
10
- </div>
11
-
12
- <div class="main-content">
13
- <div class="main-table">
14
- <TTable :data="$Data.rows" :columns="tableColumns" :loading="$Data.loading" :active-row-keys="$Data.activeRowKeys" :row-key="rowKey" :height="tableHeight" active-row-type="single" @active-change="onActiveChange">
15
- <template #operation="scope">
16
- <slot name="operation" v-bind="buildOperationSlotProps(scope)"></slot>
17
- </template>
18
-
19
- <template v-for="name in forwardedTableSlotNames" :key="name" v-slot:[name]="scope">
20
- <slot :name="name" v-bind="scope"></slot>
21
- </template>
22
- </TTable>
23
- </div>
24
-
25
- <div class="main-detail">
26
- <slot name="detail" :row="$Data.currentRow" :reload="reload">
27
- <DetailPanel :data="$Data.currentRow" :fields="detailFields" />
28
- </slot>
29
- </div>
30
- </div>
31
-
32
- <div class="main-page">
33
- <TPagination :current-page="$Data.pager.currentPage" :page-size="$Data.pager.limit" :total="$Data.pager.total" align="right" :layout="paginationLayout" @current-change="onPageChange" @page-size-change="onPageSizeChange" />
34
- </div>
35
-
36
- <slot name="dialogs" :reload="reload" :current-row="$Data.currentRow"></slot>
37
- </div>
38
- </template>
39
-
40
- <script setup>
41
- defineOptions({ name: "PagedTableDetail" });
42
-
43
- import { reactive } from "vue";
44
- const props = defineProps({
45
- columns: {
46
- type: Array,
47
- required: true
48
- },
49
- rowKey: {
50
- type: String,
51
- default: "id"
52
- },
53
- tableHeight: {
54
- type: String,
55
- default: "calc(100vh - var(--search-height) - var(--pagination-height) - var(--layout-gap) * 4)"
56
- },
57
- pageSize: {
58
- type: Number,
59
- default: 30
60
- },
61
- endpoints: {
62
- type: Object,
63
- default: undefined
64
- },
65
- paginationLayout: {
66
- type: String,
67
- default: "total, prev, pager, next, jumper"
68
- },
69
- autoLoad: {
70
- type: Boolean,
71
- default: true
72
- },
73
- tableSlotNames: {
74
- type: Array,
75
- default: undefined
76
- }
77
- });
78
-
79
- const emit = defineEmits(["loaded", "row-change", "deleted"]);
80
-
81
- const slots = useSlots();
82
-
83
- const $Data = reactive({
84
- rows: [],
85
- loading: false,
86
- pager: {
87
- currentPage: 1,
88
- limit: props.pageSize,
89
- total: 0
90
- },
91
- currentRow: null,
92
- activeRowKeys: []
93
- });
94
-
95
- let requestSeq = 0;
96
-
97
- function getColKey(col) {
98
- const record = col;
99
- const rawColKey = record["colKey"];
100
- if (typeof rawColKey === "string") return rawColKey;
101
- if (typeof rawColKey === "number") return String(rawColKey);
102
- return "";
103
- }
104
-
105
- function filterValidColumns(input) {
106
- if (!Array.isArray(input)) {
107
- return [];
108
- }
109
-
110
- const out = [];
111
-
112
- for (const col of input) {
113
- if (!col || typeof col !== "object") {
114
- continue;
115
- }
116
- const key = getColKey(col);
117
- if (key.length === 0) {
118
- continue;
119
- }
120
- out.push(col);
121
- }
122
-
123
- return out;
124
- }
125
-
126
- function mergeDetailColumns(mainColumns, extraColumns) {
127
- const out = [];
128
- const set = new Set();
129
-
130
- for (const col of mainColumns) {
131
- out.push(col);
132
- const key = getColKey(col);
133
- if (key) {
134
- set.add(key);
135
- }
136
- }
137
-
138
- for (const col of extraColumns) {
139
- const key = getColKey(col);
140
- if (key && set.has(key)) {
141
- continue;
142
- }
143
- out.push(col);
144
- if (key) {
145
- set.add(key);
146
- }
147
- }
148
-
149
- return out;
150
- }
151
-
152
- function isDetail(col) {
153
- const record = col;
154
- if (record["detail"] === true) return true;
155
- return false;
156
- }
157
-
158
- const tableColumns = computed(() => {
159
- const out = [];
160
- const cols = filterValidColumns(props.columns);
161
- for (const col of cols) {
162
- if (isDetail(col)) continue;
163
- out.push(col);
164
- }
165
- return out;
166
- });
167
-
168
- const detailFields = computed(() => {
169
- // 只维护一个 columns:
170
- // - detail: false(默认)=> 表格展示(同时也会出现在详情里,且顺序靠前)
171
- // - detail: true => 仅在详情展示(顺序靠后)
172
- const extras = [];
173
- const cols = filterValidColumns(props.columns);
174
- for (const col of cols) {
175
- if (!isDetail(col)) continue;
176
- extras.push(col);
177
- }
178
-
179
- if (extras.length > 0) {
180
- return mergeDetailColumns(tableColumns.value, extras);
181
- }
182
-
183
- // 默认复用表格列
184
- return tableColumns.value;
185
- });
186
-
187
- const forwardedTableSlotNames = computed(() => {
188
- if (Array.isArray(props.tableSlotNames) && props.tableSlotNames.length > 0) {
189
- const out = [];
190
- const set = new Set();
191
- for (const name of props.tableSlotNames) {
192
- if (typeof name !== "string") continue;
193
- const trimmed = name.trim();
194
- if (trimmed.length === 0) continue;
195
- if (set.has(trimmed)) continue;
196
- set.add(trimmed);
197
- out.push(trimmed);
198
- }
199
- return out;
200
- }
201
-
202
- const names = [];
203
- const reserved = ["toolLeft", "toolRight", "detail", "dialogs", "operation", "default"]; // operation 单独处理
204
-
205
- for (const key of Object.keys(slots)) {
206
- if (reserved.includes(key)) continue;
207
- names.push(key);
208
- }
209
-
210
- return names;
211
- });
212
-
213
- function setCurrentRow(row) {
214
- $Data.currentRow = row;
215
- emit("row-change", { row: row });
216
- }
217
-
218
- function getRowKeyValue(row) {
219
- const key = props.rowKey;
220
- if (!key) return null;
221
-
222
- const record = row;
223
- const raw = record[key];
224
- if (typeof raw === "string") return raw;
225
- if (typeof raw === "number") return raw;
226
- return null;
227
- }
228
-
229
- function applyAutoSelection(list, preferKey) {
230
- if (!Array.isArray(list) || list.length === 0) {
231
- setCurrentRow(null);
232
- $Data.activeRowKeys = [];
233
- return;
234
- }
235
-
236
- if (preferKey !== null) {
237
- for (const row of list) {
238
- const k = getRowKeyValue(row);
239
- if (k === preferKey) {
240
- setCurrentRow(row);
241
- $Data.activeRowKeys = [k];
242
- return;
243
- }
244
- }
245
- }
246
-
247
- const first = list[0];
248
- const firstKey = getRowKeyValue(first);
249
- setCurrentRow(first);
250
-
251
- if (firstKey === null) {
252
- $Data.activeRowKeys = [];
253
- return;
254
- }
255
-
256
- $Data.activeRowKeys = [firstKey];
257
- }
258
-
259
- function getLastPage(total, limit) {
260
- if (total <= 0) return 1;
261
- if (limit <= 0) return 1;
262
- const pages = Math.ceil(total / limit);
263
- if (!Number.isFinite(pages) || pages <= 0) return 1;
264
- return pages;
265
- }
266
-
267
- async function loadList(options) {
268
- const listEndpoint = props.endpoints?.list;
269
- if (!listEndpoint) {
270
- return;
271
- }
272
-
273
- const preferKey = options?.keepSelection ? ($Data.currentRow ? getRowKeyValue($Data.currentRow) : null) : null;
274
-
275
- requestSeq += 1;
276
- const seq = requestSeq;
277
-
278
- $Data.loading = true;
279
- try {
280
- const pageKey = listEndpoint.pageKey || "page";
281
- const limitKey = listEndpoint.limitKey || "limit";
282
-
283
- const data = {};
284
- data[pageKey] = $Data.pager.currentPage;
285
- data[limitKey] = $Data.pager.limit;
286
-
287
- if (listEndpoint.buildData) {
288
- const extra = listEndpoint.buildData({
289
- currentPage: $Data.pager.currentPage,
290
- limit: $Data.pager.limit
291
- });
292
- if (extra && typeof extra === "object") {
293
- for (const k of Object.keys(extra)) {
294
- data[k] = extra[k];
295
- }
296
- }
297
- }
298
-
299
- const res = await $Http.json(listEndpoint.path, data, listEndpoint.dropValues, listEndpoint.dropKeyValue);
300
-
301
- // 并发保护:旧请求返回后不应覆盖新请求的状态
302
- if (seq !== requestSeq) {
303
- return;
304
- }
305
-
306
- const lists = Array.isArray(res?.data?.lists) ? res.data.lists : [];
307
- const total = typeof res?.data?.total === "number" ? res.data.total : 0;
308
-
309
- // B:页码越界修正(删除/过滤等导致 total 变小后,当前页可能已不存在)
310
- const allowPageFallback = options?.allowPageFallback !== false;
311
- if (allowPageFallback && lists.length === 0 && total > 0) {
312
- const lastPage = getLastPage(total, $Data.pager.limit);
313
- if ($Data.pager.currentPage > lastPage) {
314
- $Data.pager.currentPage = lastPage;
315
- await loadList({
316
- keepSelection: false,
317
- allowPageFallback: false
318
- });
319
- return;
320
- }
321
- }
322
-
323
- $Data.rows = lists;
324
- $Data.pager.total = total;
325
-
326
- applyAutoSelection(lists, preferKey);
327
- emit("loaded", { rows: lists, total: total });
328
- } catch (error) {
329
- if (seq !== requestSeq) {
330
- return;
331
- }
332
- MessagePlugin.error("加载数据失败");
333
- } finally {
334
- if (seq === requestSeq) {
335
- $Data.loading = false;
336
- }
337
- }
338
- }
339
-
340
- async function reload(options) {
341
- if (options?.resetPage) {
342
- $Data.pager.currentPage = 1;
343
- }
344
- await loadList({
345
- keepSelection: options?.keepSelection,
346
- allowPageFallback: true
347
- });
348
- }
349
-
350
- function onPageChange(info) {
351
- $Data.pager.currentPage = info.currentPage;
352
- reload({ keepSelection: true });
353
- }
354
-
355
- function onPageSizeChange(info) {
356
- $Data.pager.limit = info.pageSize;
357
- $Data.pager.currentPage = 1;
358
- reload({ keepSelection: false });
359
- }
360
-
361
- function onActiveChange(value, context) {
362
- // 禁止取消高亮:如果新值为空,保持当前选中
363
- if (value.length === 0) {
364
- if ($Data.activeRowKeys.length > 0) {
365
- return;
366
- }
367
-
368
- $Data.activeRowKeys = [];
369
- setCurrentRow(null);
370
- return;
371
- }
372
-
373
- $Data.activeRowKeys = value;
374
-
375
- const list = context.activeRowList;
376
- if (list && Array.isArray(list) && list.length > 0) {
377
- const row = list[0]?.row || null;
378
- setCurrentRow(row);
379
- return;
380
- }
381
-
382
- // C1:兜底回查(activeRowList 缺失时按 key 在 rows 中回查)
383
- const activeKey = value[0];
384
- for (const row of $Data.rows) {
385
- const key = getRowKeyValue(row);
386
- if (key === activeKey) {
387
- setCurrentRow(row);
388
- return;
389
- }
390
- }
391
- }
392
-
393
- function getDeleteConfirmContent(ep, row) {
394
- const confirm = ep.confirm;
395
- if (!confirm) {
396
- return {
397
- header: "确认删除",
398
- body: "确认删除该记录吗?",
399
- confirmBtn: "删除"
400
- };
401
- }
402
-
403
- if (typeof confirm === "function") {
404
- return confirm(row);
405
- }
406
-
407
- return confirm;
408
- }
409
-
410
- async function deleteRow(row) {
411
- const ep = props.endpoints?.delete;
412
- if (!ep) {
413
- MessagePlugin.error("未配置删除接口");
414
- return;
415
- }
416
-
417
- const idKey = ep.idKey || "id";
418
- const record = row;
419
- const rawId = record[idKey];
420
-
421
- if (rawId === null || rawId === undefined || rawId === "") {
422
- MessagePlugin.error("删除失败:缺少主键");
423
- return;
424
- }
425
-
426
- const confirmContent = getDeleteConfirmContent(ep, row);
427
-
428
- let dialog = null;
429
- let destroyed = false;
430
-
431
- const destroy = () => {
432
- if (destroyed) return;
433
- destroyed = true;
434
- if (dialog && typeof dialog.destroy === "function") {
435
- dialog.destroy();
436
- }
437
- };
438
-
439
- dialog = DialogPlugin.confirm({
440
- header: confirmContent.header,
441
- body: confirmContent.body,
442
- status: confirmContent.status || "warning",
443
- confirmBtn: confirmContent.confirmBtn || "删除",
444
- cancelBtn: confirmContent.cancelBtn || "取消",
445
- onConfirm: async () => {
446
- if (dialog && typeof dialog.setConfirmLoading === "function") {
447
- dialog.setConfirmLoading(true);
448
- }
449
-
450
- try {
451
- const data = {};
452
- data[idKey] = rawId;
453
-
454
- if (ep.buildData) {
455
- const extra = ep.buildData(row);
456
- if (extra && typeof extra === "object") {
457
- for (const k of Object.keys(extra)) {
458
- data[k] = extra[k];
459
- }
460
- }
461
- }
462
-
463
- await $Http.json(ep.path, data, ep.dropValues, ep.dropKeyValue);
464
-
465
- MessagePlugin.success("删除成功");
466
- destroy();
467
- emit("deleted", { row: row });
468
- await reload({ keepSelection: true });
469
- } catch (error) {
470
- MessagePlugin.error("删除失败");
471
- } finally {
472
- if (dialog && typeof dialog.setConfirmLoading === "function") {
473
- dialog.setConfirmLoading(false);
474
- }
475
- }
476
- },
477
- onClose: () => {
478
- destroy();
479
- }
480
- });
481
- }
482
-
483
- function buildOperationSlotProps(scope) {
484
- const out = {};
485
-
486
- for (const k of Object.keys(scope)) {
487
- out[k] = scope[k];
488
- }
489
-
490
- out["deleteRow"] = deleteRow;
491
- out["reload"] = reload;
492
-
493
- return out;
494
- }
495
-
496
- defineExpose({
497
- reload: reload,
498
- deleteRow: deleteRow,
499
- rows: $Data.rows,
500
- pager: $Data.pager,
501
- currentRow: $Data.currentRow
502
- });
503
-
504
- onMounted(() => {
505
- if (!props.autoLoad) return;
506
- reload({ keepSelection: false });
507
- });
508
- </script>
509
-
510
- <style scoped lang="scss">
511
- // 样式继承自全局 page-table
512
- </style>
package/src/layouts/1.vue DELETED
@@ -1,8 +0,0 @@
1
- <template>
2
- <router-view />
3
- </template>
4
-
5
- <script setup>
6
- // 空布局 - 不包含任何框架结构
7
- // 用于登录页等不需要导航栏和侧边栏的页面
8
- </script>