c-admin-kit 1.0.0
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/README.md +232 -0
- package/dist/c-admin-kit.css +1 -0
- package/dist/c-admin-kit.js +2482 -0
- package/dist/c-admin-kit.umd.cjs +10 -0
- package/dist/components/AsyncButton/index.vue.d.ts +48 -0
- package/dist/components/CollapsibleContainer/index.vue.d.ts +73 -0
- package/dist/components/CustomDrawer/index.vue.d.ts +149 -0
- package/dist/components/ImagePreview/index.vue.d.ts +34 -0
- package/dist/components/SearchBox/index.vue.d.ts +122 -0
- package/dist/components/SelectWithAll/index.vue.d.ts +169 -0
- package/dist/components/SelectWithPage/index.vue.d.ts +78 -0
- package/dist/components/SimpleTable/index.vue.d.ts +337 -0
- package/dist/components/SimpleTable/useTableColumnConfig.d.ts +5 -0
- package/dist/components/diff/index.vue.d.ts +33 -0
- package/dist/components/index.d.ts +12 -0
- package/dist/composables/index.d.ts +6 -0
- package/dist/composables/useConfirmAction.d.ts +5 -0
- package/dist/composables/useConfirmSubmit.d.ts +5 -0
- package/dist/composables/useDialog.d.ts +5 -0
- package/dist/composables/useDownload.d.ts +5 -0
- package/dist/composables/useForm.d.ts +5 -0
- package/dist/composables/useListPage.d.ts +5 -0
- package/dist/index.d.ts +9 -0
- package/dist/types.d.ts +282 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/scroll-to.d.ts +7 -0
- package/dist/utils/searchFieldFactory.d.ts +34 -0
- package/dist/utils/treeManager.d.ts +64 -0
- package/dist/utils/validate.d.ts +30 -0
- package/package.json +86 -0
- package/src/components/AsyncButton/index.vue +58 -0
- package/src/components/CollapsibleContainer/index.vue +240 -0
- package/src/components/CustomDrawer/index.vue +167 -0
- package/src/components/ImagePreview/index.vue +88 -0
- package/src/components/SearchBox/index.vue +582 -0
- package/src/components/SelectWithAll/index.vue +281 -0
- package/src/components/SelectWithPage/index.vue +204 -0
- package/src/components/SimpleTable/index.vue +781 -0
- package/src/components/SimpleTable/useTableColumnConfig.ts +139 -0
- package/src/components/diff/index.vue +265 -0
- package/src/components/index.ts +35 -0
- package/src/composables/index.ts +6 -0
- package/src/composables/useConfirmAction.ts +62 -0
- package/src/composables/useConfirmSubmit.ts +68 -0
- package/src/composables/useDialog.ts +48 -0
- package/src/composables/useDownload.ts +83 -0
- package/src/composables/useForm.ts +114 -0
- package/src/composables/useListPage.ts +243 -0
- package/src/env.d.ts +7 -0
- package/src/index.ts +44 -0
- package/src/types.ts +344 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/scroll-to.ts +60 -0
- package/src/utils/searchFieldFactory.ts +302 -0
- package/src/utils/treeManager.ts +218 -0
- package/src/utils/validate.ts +64 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { ref, computed, nextTick } from "vue"
|
|
2
|
+
import type { TableColumn, TableColumnConfigProps, UseTableColumnConfigReturn } from '../../types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 表格自定义列配置 Hook (解耦版,优先使用外部传入 API,未传则默认降级使用 LocalStorage)
|
|
6
|
+
*/
|
|
7
|
+
export function useTableColumnConfig(
|
|
8
|
+
props: TableColumnConfigProps,
|
|
9
|
+
calculateTableHeight?: () => void
|
|
10
|
+
): UseTableColumnConfigReturn {
|
|
11
|
+
// 所有带有标识的列,附加一个 _key
|
|
12
|
+
const enhancedColumns = computed<TableColumn[]>(() => {
|
|
13
|
+
return (props.columns || []).map((col, index) => {
|
|
14
|
+
return {
|
|
15
|
+
...col,
|
|
16
|
+
_key: col.prop || col.slot || col.type || col.label || `col_${index}`,
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// 允许在面板中配置的列 (过滤掉 selection、drag 和没有 label 的列)
|
|
22
|
+
const settingColumns = computed<TableColumn[]>(() => {
|
|
23
|
+
return enhancedColumns.value.filter(
|
|
24
|
+
(col) => col.type !== "selection" && col.type !== "drag" && col.label
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const visibleColKeys = ref<string[]>([])
|
|
29
|
+
const tempVisibleColKeys = ref<string[]>([])
|
|
30
|
+
const popoverRef = ref<any>(null)
|
|
31
|
+
const savingColumns = ref(false)
|
|
32
|
+
|
|
33
|
+
// 真正传递给 el-table 的列
|
|
34
|
+
const computedColumns = computed<TableColumn[]>(() => {
|
|
35
|
+
if (!props.tableKey) return enhancedColumns.value
|
|
36
|
+
return enhancedColumns.value.filter((col) => {
|
|
37
|
+
const isConfigurable = settingColumns.value.some((c) => c._key === col._key)
|
|
38
|
+
if (!isConfigurable) return true
|
|
39
|
+
return visibleColKeys.value.includes(col._key!)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const getStorageKey = (): string => {
|
|
44
|
+
return `c_table_hide_cols_${props.tableKey}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 初始化列配置
|
|
48
|
+
const initTableConfig = async (): Promise<void> => {
|
|
49
|
+
visibleColKeys.value = settingColumns.value.map((c) => c._key!)
|
|
50
|
+
if (!props.tableKey) return
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
let hideList: string[] = []
|
|
54
|
+
// 1. 如果外部提供了获取配置的 API
|
|
55
|
+
if (typeof props.getColumnConfigApi === 'function') {
|
|
56
|
+
const res = await props.getColumnConfigApi({ tableKey: props.tableKey })
|
|
57
|
+
hideList = res?.data || res || []
|
|
58
|
+
} else {
|
|
59
|
+
// 2. 默认降级使用 localStorage
|
|
60
|
+
const cached = localStorage.getItem(getStorageKey())
|
|
61
|
+
if (cached) {
|
|
62
|
+
hideList = JSON.parse(cached)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(hideList) && hideList.length > 0) {
|
|
67
|
+
visibleColKeys.value = settingColumns.value
|
|
68
|
+
.filter((c) => !hideList.includes(c._key!))
|
|
69
|
+
.map((c) => c._key!)
|
|
70
|
+
}
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.warn("CSimpleTable: 获取表格隐藏列配置失败", e)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const handlePopoverShow = (): void => {
|
|
77
|
+
tempVisibleColKeys.value = [...visibleColKeys.value]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const handleCancelSettings = (): void => {
|
|
81
|
+
if (popoverRef.value) {
|
|
82
|
+
popoverRef.value.hide()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const handleConfirmSettings = async (): Promise<void> => {
|
|
87
|
+
savingColumns.value = true
|
|
88
|
+
|
|
89
|
+
if (props.tableKey) {
|
|
90
|
+
const hideFields = settingColumns.value
|
|
91
|
+
.filter((c) => !tempVisibleColKeys.value.includes(c._key!))
|
|
92
|
+
.map((c) => c._key!)
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
if (typeof props.saveColumnConfigApi === 'function') {
|
|
96
|
+
await props.saveColumnConfigApi({
|
|
97
|
+
tableKey: props.tableKey,
|
|
98
|
+
hideFields,
|
|
99
|
+
})
|
|
100
|
+
} else {
|
|
101
|
+
localStorage.setItem(getStorageKey(), JSON.stringify(hideFields))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
visibleColKeys.value = [...tempVisibleColKeys.value]
|
|
105
|
+
|
|
106
|
+
if (calculateTableHeight) {
|
|
107
|
+
nextTick(() => {
|
|
108
|
+
calculateTableHeight()
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (popoverRef.value) {
|
|
113
|
+
popoverRef.value.hide()
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error("CSimpleTable: 保存表格隐藏列配置失败", error)
|
|
117
|
+
} finally {
|
|
118
|
+
savingColumns.value = false
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
visibleColKeys.value = [...tempVisibleColKeys.value]
|
|
122
|
+
savingColumns.value = false
|
|
123
|
+
if (popoverRef.value) popoverRef.value.hide()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
computedColumns,
|
|
129
|
+
settingColumns,
|
|
130
|
+
visibleColKeys,
|
|
131
|
+
tempVisibleColKeys,
|
|
132
|
+
popoverRef,
|
|
133
|
+
savingColumns,
|
|
134
|
+
initTableConfig,
|
|
135
|
+
handlePopoverShow,
|
|
136
|
+
handleCancelSettings,
|
|
137
|
+
handleConfirmSettings,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="c-diff-container" v-loading="loading">
|
|
3
|
+
<span
|
|
4
|
+
v-for="(part, index) in diffResult"
|
|
5
|
+
:key="index"
|
|
6
|
+
:class="getPartClass(part)"
|
|
7
|
+
:title="getPartTitle(part)"
|
|
8
|
+
v-html="escapeHtml(part.value)"
|
|
9
|
+
></span>
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import { ref, watch, nextTick } from "vue";
|
|
15
|
+
|
|
16
|
+
interface DiffPart {
|
|
17
|
+
type: "unchanged" | "removed" | "added";
|
|
18
|
+
value: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
defineOptions({ name: "CDiff" });
|
|
22
|
+
|
|
23
|
+
const props = defineProps({
|
|
24
|
+
oldText: { type: String, default: "" },
|
|
25
|
+
newText: { type: String, default: "" },
|
|
26
|
+
similarityThreshold: { type: Number, default: 0.6 },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const loading = ref(false);
|
|
30
|
+
const diffResult = ref<DiffPart[]>([]);
|
|
31
|
+
|
|
32
|
+
const escapeHtml = (text: string | number) => {
|
|
33
|
+
return String(text)
|
|
34
|
+
.replace(/&/g, "&")
|
|
35
|
+
.replace(/</g, "<")
|
|
36
|
+
.replace(/>/g, ">")
|
|
37
|
+
.replace(/"/g, """)
|
|
38
|
+
.replace(/'/g, "'");
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Levenshtein 编辑距离算法
|
|
42
|
+
const levenshteinDistance = (a: string, b: string): number => {
|
|
43
|
+
const dp: number[][] = Array(a.length + 1)
|
|
44
|
+
.fill(null)
|
|
45
|
+
.map(() => Array(b.length + 1).fill(0));
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i <= a.length; i++) dp[i][0] = i;
|
|
48
|
+
for (let j = 0; j <= b.length; j++) dp[0][j] = j;
|
|
49
|
+
|
|
50
|
+
for (let i = 1; i <= a.length; i++) {
|
|
51
|
+
for (let j = 1; j <= b.length; j++) {
|
|
52
|
+
dp[i][j] =
|
|
53
|
+
a[i - 1] === b[j - 1]
|
|
54
|
+
? dp[i - 1][j - 1]
|
|
55
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return dp[a.length][b.length];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const calculateSimilarity = (a: string, b: string): number => {
|
|
62
|
+
if (!a && !b) return 1;
|
|
63
|
+
if (!a || !b) return 0;
|
|
64
|
+
const longer = a.length > b.length ? a : b;
|
|
65
|
+
return (longer.length - levenshteinDistance(a, b)) / longer.length;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const diffChars = (oldStr: string, newStr: string): DiffPart[] => {
|
|
69
|
+
const oldArr = Array.from(oldStr);
|
|
70
|
+
const newArr = Array.from(newStr);
|
|
71
|
+
|
|
72
|
+
if (oldArr.length * newArr.length > 1_000_000) {
|
|
73
|
+
return [
|
|
74
|
+
...(oldStr ? [{ type: "removed" as const, value: oldStr }] : []),
|
|
75
|
+
...(newStr ? [{ type: "added" as const, value: newStr }] : []),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const dp = Array(oldArr.length + 1)
|
|
80
|
+
.fill(null)
|
|
81
|
+
.map(() => new Int32Array(newArr.length + 1));
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i <= oldArr.length; i++) {
|
|
84
|
+
for (let j = 0; j <= newArr.length; j++) {
|
|
85
|
+
if (i === 0) dp[i][j] = j;
|
|
86
|
+
else if (j === 0) dp[i][j] = i;
|
|
87
|
+
else if (oldArr[i - 1] === newArr[j - 1]) dp[i][j] = dp[i - 1][j - 1];
|
|
88
|
+
else
|
|
89
|
+
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result: DiffPart[] = [];
|
|
94
|
+
let i = oldArr.length,
|
|
95
|
+
j = newArr.length;
|
|
96
|
+
while (i > 0 || j > 0) {
|
|
97
|
+
if (i > 0 && j > 0 && oldArr[i - 1] === newArr[j - 1]) {
|
|
98
|
+
result.push({ type: "unchanged", value: oldArr[i - 1] });
|
|
99
|
+
i--;
|
|
100
|
+
j--;
|
|
101
|
+
} else if (i > 0 && (j === 0 || dp[i][j] === dp[i - 1][j] + 1)) {
|
|
102
|
+
result.push({ type: "removed", value: oldArr[i - 1] });
|
|
103
|
+
i--;
|
|
104
|
+
} else {
|
|
105
|
+
result.push({ type: "added", value: newArr[j - 1] });
|
|
106
|
+
j--;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const merged: DiffPart[] = [];
|
|
111
|
+
for (const item of result.reverse()) {
|
|
112
|
+
if (merged.length && merged[merged.length - 1].type === item.type) {
|
|
113
|
+
merged[merged.length - 1].value += item.value;
|
|
114
|
+
} else {
|
|
115
|
+
merged.push({ ...item });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return merged;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const splitIntoLines = (text: string): string[] => {
|
|
122
|
+
return text ? text.split(/\r?\n/) : [];
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const diffLines = (oldText: string, newText: string): DiffPart[] => {
|
|
126
|
+
const oldLines = splitIntoLines(oldText);
|
|
127
|
+
const newLines = splitIntoLines(newText);
|
|
128
|
+
|
|
129
|
+
const result: DiffPart[] = [];
|
|
130
|
+
let i = 0,
|
|
131
|
+
j = 0;
|
|
132
|
+
|
|
133
|
+
while (i < oldLines.length || j < newLines.length) {
|
|
134
|
+
if (i >= oldLines.length) {
|
|
135
|
+
result.push({ type: "added", value: newLines[j++] + "\n" });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (j >= newLines.length) {
|
|
139
|
+
result.push({ type: "removed", value: oldLines[i++] + "\n" });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const oldLine = oldLines[i];
|
|
144
|
+
const newLine = newLines[j];
|
|
145
|
+
const oldTrim = oldLine.trim();
|
|
146
|
+
const newTrim = newLine.trim();
|
|
147
|
+
const sim = calculateSimilarity(oldTrim, newTrim);
|
|
148
|
+
|
|
149
|
+
const lenRatio =
|
|
150
|
+
Math.min(oldLine.length, newLine.length) /
|
|
151
|
+
Math.max(oldLine.length, newLine.length || 1);
|
|
152
|
+
|
|
153
|
+
const simNextOld =
|
|
154
|
+
i + 1 < oldLines.length
|
|
155
|
+
? calculateSimilarity(oldLines[i + 1].trim(), newTrim)
|
|
156
|
+
: 0;
|
|
157
|
+
const simNextNew =
|
|
158
|
+
j + 1 < newLines.length
|
|
159
|
+
? calculateSimilarity(oldTrim, newLines[j + 1].trim())
|
|
160
|
+
: 0;
|
|
161
|
+
|
|
162
|
+
const maxSim = Math.max(sim, simNextOld, simNextNew);
|
|
163
|
+
|
|
164
|
+
if (sim >= props.similarityThreshold && sim === maxSim && lenRatio > 0.5) {
|
|
165
|
+
if (oldLine === newLine) {
|
|
166
|
+
result.push({ type: "unchanged", value: oldLine + "\n" });
|
|
167
|
+
} else {
|
|
168
|
+
result.push(...diffChars(oldLine, newLine));
|
|
169
|
+
result.push({ type: "unchanged", value: "\n" });
|
|
170
|
+
}
|
|
171
|
+
i++;
|
|
172
|
+
j++;
|
|
173
|
+
} else if (simNextOld > sim && simNextOld >= props.similarityThreshold) {
|
|
174
|
+
result.push({ type: "removed", value: oldLine + "\n" });
|
|
175
|
+
i++;
|
|
176
|
+
} else if (simNextNew > sim && simNextNew >= props.similarityThreshold) {
|
|
177
|
+
result.push({ type: "added", value: newLine + "\n" });
|
|
178
|
+
j++;
|
|
179
|
+
} else {
|
|
180
|
+
if (sim < 0.3) {
|
|
181
|
+
result.push({ type: "removed", value: oldLine + "\n" });
|
|
182
|
+
result.push({ type: "added", value: newLine + "\n" });
|
|
183
|
+
i++;
|
|
184
|
+
j++;
|
|
185
|
+
} else {
|
|
186
|
+
result.push(...diffChars(oldLine, newLine));
|
|
187
|
+
result.push({ type: "unchanged", value: "\n" });
|
|
188
|
+
i++;
|
|
189
|
+
j++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return result;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
let cancelled = false;
|
|
198
|
+
|
|
199
|
+
const computeDiff = async () => {
|
|
200
|
+
loading.value = true;
|
|
201
|
+
cancelled = false;
|
|
202
|
+
await nextTick();
|
|
203
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
204
|
+
|
|
205
|
+
if (cancelled) return;
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
diffResult.value = diffLines(props.oldText, props.newText);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.error("CDiff compute error:", err);
|
|
211
|
+
diffResult.value = [{ type: "unchanged", value: "Diff 计算出错" }];
|
|
212
|
+
} finally {
|
|
213
|
+
if (!cancelled) loading.value = false;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
watch(
|
|
218
|
+
() => [props.oldText, props.newText],
|
|
219
|
+
() => {
|
|
220
|
+
cancelled = true;
|
|
221
|
+
computeDiff();
|
|
222
|
+
},
|
|
223
|
+
{ immediate: true }
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const getPartClass = (part: DiffPart) => {
|
|
227
|
+
return {
|
|
228
|
+
"diff-removed": part.type === "removed",
|
|
229
|
+
"diff-added": part.type === "added",
|
|
230
|
+
};
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const getPartTitle = (part: DiffPart) => {
|
|
234
|
+
return part.type === "removed"
|
|
235
|
+
? "已删除"
|
|
236
|
+
: part.type === "added"
|
|
237
|
+
? "已添加"
|
|
238
|
+
: "";
|
|
239
|
+
};
|
|
240
|
+
</script>
|
|
241
|
+
|
|
242
|
+
<style scoped>
|
|
243
|
+
.c-diff-container {
|
|
244
|
+
font-family: monospace;
|
|
245
|
+
font-size: 14px;
|
|
246
|
+
line-height: 1.6;
|
|
247
|
+
white-space: pre-wrap;
|
|
248
|
+
word-break: break-word;
|
|
249
|
+
color: #333;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.diff-removed {
|
|
253
|
+
background-color: #fee2e2;
|
|
254
|
+
color: #991b1b;
|
|
255
|
+
text-decoration: line-through;
|
|
256
|
+
text-decoration-color: #dc2626;
|
|
257
|
+
text-decoration-thickness: 2px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.diff-added {
|
|
261
|
+
background-color: #dcfce7;
|
|
262
|
+
color: #166534;
|
|
263
|
+
font-weight: 600;
|
|
264
|
+
}
|
|
265
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Component } from 'vue'
|
|
2
|
+
|
|
3
|
+
import CAsyncButton from './AsyncButton/index.vue'
|
|
4
|
+
import CSelectWithAll from './SelectWithAll/index.vue'
|
|
5
|
+
import CSelectWithPage from './SelectWithPage/index.vue'
|
|
6
|
+
import CSearchBox from './SearchBox/index.vue'
|
|
7
|
+
import CCollapsibleContainer from './CollapsibleContainer/index.vue'
|
|
8
|
+
import CCustomDrawer from './CustomDrawer/index.vue'
|
|
9
|
+
import CSimpleTable from './SimpleTable/index.vue'
|
|
10
|
+
import CDiff from './diff/index.vue'
|
|
11
|
+
import CImagePreview from './ImagePreview/index.vue'
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
CAsyncButton,
|
|
15
|
+
CSelectWithAll,
|
|
16
|
+
CSelectWithPage,
|
|
17
|
+
CSearchBox,
|
|
18
|
+
CCollapsibleContainer,
|
|
19
|
+
CCustomDrawer,
|
|
20
|
+
CSimpleTable,
|
|
21
|
+
CDiff,
|
|
22
|
+
CImagePreview,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const components: Component[] = [
|
|
26
|
+
CAsyncButton,
|
|
27
|
+
CSelectWithAll,
|
|
28
|
+
CSelectWithPage,
|
|
29
|
+
CSearchBox,
|
|
30
|
+
CCollapsibleContainer,
|
|
31
|
+
CCustomDrawer,
|
|
32
|
+
CSimpleTable,
|
|
33
|
+
CDiff,
|
|
34
|
+
CImagePreview,
|
|
35
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ElMessageBox } from 'element-plus'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { UseConfirmActionOptions, UseConfirmActionReturn, AnyRecord, ApiFn } from '../types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 二次确认操作的通用 Hook (支持后端根据业务返回 confirm: 0/1 的二次确认机制)
|
|
7
|
+
*/
|
|
8
|
+
export function useConfirmAction(apiFn: ApiFn, options: UseConfirmActionOptions = {}): UseConfirmActionReturn {
|
|
9
|
+
const {
|
|
10
|
+
onSuccess,
|
|
11
|
+
onError,
|
|
12
|
+
confirmTitle = '提示',
|
|
13
|
+
confirmButtonText = '确认',
|
|
14
|
+
cancelButtonText = '取消',
|
|
15
|
+
confirmType = 'warning'
|
|
16
|
+
} = options
|
|
17
|
+
|
|
18
|
+
const loading = ref(false)
|
|
19
|
+
|
|
20
|
+
const execute = async (params: AnyRecord): Promise<any> => {
|
|
21
|
+
try {
|
|
22
|
+
loading.value = true
|
|
23
|
+
|
|
24
|
+
// 第一次调用, 传入 confirm: 0
|
|
25
|
+
const firstParams = { ...params, confirm: 0 }
|
|
26
|
+
const res = await apiFn(firstParams)
|
|
27
|
+
|
|
28
|
+
// 判断是否需要二次确认 (后端接口约定 msg 提示)
|
|
29
|
+
if (res && res.code === 200 && res.msg) {
|
|
30
|
+
await ElMessageBox.confirm(
|
|
31
|
+
res.msg,
|
|
32
|
+
confirmTitle,
|
|
33
|
+
{
|
|
34
|
+
confirmButtonText,
|
|
35
|
+
cancelButtonText,
|
|
36
|
+
type: confirmType,
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// 用户确认后再次调用,传入 confirm: 1
|
|
41
|
+
const secondParams = { ...params, confirm: 1 }
|
|
42
|
+
const finalRes = await apiFn(secondParams)
|
|
43
|
+
|
|
44
|
+
onSuccess?.(finalRes)
|
|
45
|
+
return finalRes
|
|
46
|
+
} else {
|
|
47
|
+
onSuccess?.(res)
|
|
48
|
+
return res
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
onError?.(error)
|
|
52
|
+
throw error
|
|
53
|
+
} finally {
|
|
54
|
+
loading.value = false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
execute,
|
|
60
|
+
loading
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
2
|
+
import type { UseConfirmSubmitOptions } from '../types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 确认提交 Hook
|
|
6
|
+
*/
|
|
7
|
+
export function useConfirmSubmit(
|
|
8
|
+
asyncFn: (...args: any[]) => Promise<any>,
|
|
9
|
+
onSuccess?: (result: any) => void,
|
|
10
|
+
options: UseConfirmSubmitOptions = {}
|
|
11
|
+
): (...args: any[]) => Promise<any> {
|
|
12
|
+
const {
|
|
13
|
+
title = '提示',
|
|
14
|
+
message = '确定要执行此操作吗?',
|
|
15
|
+
confirmButtonText = '确定',
|
|
16
|
+
cancelButtonText = '取消',
|
|
17
|
+
type = 'warning',
|
|
18
|
+
successMessage = '操作成功',
|
|
19
|
+
cancelMessage = '已取消操作',
|
|
20
|
+
errorMessage = '操作失败',
|
|
21
|
+
showCancelMessage = true,
|
|
22
|
+
messageBoxOptions = {}
|
|
23
|
+
} = options
|
|
24
|
+
|
|
25
|
+
const execute = async (...args: any[]): Promise<any> => {
|
|
26
|
+
try {
|
|
27
|
+
await ElMessageBox.confirm(message, title, {
|
|
28
|
+
confirmButtonText,
|
|
29
|
+
cancelButtonText,
|
|
30
|
+
type,
|
|
31
|
+
...messageBoxOptions
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const result = await asyncFn(...args)
|
|
35
|
+
|
|
36
|
+
if (successMessage) {
|
|
37
|
+
ElMessage({
|
|
38
|
+
type: 'success',
|
|
39
|
+
message: successMessage
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (onSuccess) {
|
|
44
|
+
onSuccess(result)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error === 'cancel' || error === 'close') {
|
|
50
|
+
if (showCancelMessage) {
|
|
51
|
+
ElMessage({
|
|
52
|
+
type: 'info',
|
|
53
|
+
message: cancelMessage
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
console.error('Confirm submit error:', error)
|
|
58
|
+
ElMessage({
|
|
59
|
+
type: 'error',
|
|
60
|
+
message: errorMessage
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
throw error
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return execute
|
|
68
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ref, reactive } from "vue"
|
|
2
|
+
import type { UseDialogOptions, UseDialogReturn, AnyRecord } from '../types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 弹窗状态与数据管理 Hook
|
|
6
|
+
*/
|
|
7
|
+
export function useDialog(options: UseDialogOptions = {}): UseDialogReturn {
|
|
8
|
+
const { width = "50%", title = "Dialog", beforeClose } = options
|
|
9
|
+
|
|
10
|
+
const visible = ref(false)
|
|
11
|
+
const dialogTitle = ref(title)
|
|
12
|
+
const dialogData: AnyRecord = reactive({})
|
|
13
|
+
|
|
14
|
+
const open = (data: AnyRecord = {}, customTitle?: string): void => {
|
|
15
|
+
if (customTitle) {
|
|
16
|
+
dialogTitle.value = customTitle
|
|
17
|
+
}
|
|
18
|
+
Object.assign(dialogData, data)
|
|
19
|
+
visible.value = true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const close = (): void => {
|
|
23
|
+
visible.value = false
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
Object.keys(dialogData).forEach((key) => {
|
|
26
|
+
delete dialogData[key]
|
|
27
|
+
})
|
|
28
|
+
}, 300)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const handleClose = (done: () => void): void => {
|
|
32
|
+
if (beforeClose) {
|
|
33
|
+
beforeClose(done)
|
|
34
|
+
} else {
|
|
35
|
+
done()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
visible,
|
|
41
|
+
dialogTitle,
|
|
42
|
+
dialogData,
|
|
43
|
+
width,
|
|
44
|
+
open,
|
|
45
|
+
close,
|
|
46
|
+
handleClose,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ref } from "vue"
|
|
2
|
+
import { ElMessage } from "element-plus"
|
|
3
|
+
import dayjs from "dayjs"
|
|
4
|
+
import type { UseDownloadOptions, UseDownloadReturn, AnyRecord } from '../types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 文件导出与下载 Hook
|
|
8
|
+
*/
|
|
9
|
+
export const useDownload = (
|
|
10
|
+
apiFunction: (params: AnyRecord, config?: AnyRecord) => Promise<any>,
|
|
11
|
+
options: UseDownloadOptions = {}
|
|
12
|
+
): UseDownloadReturn => {
|
|
13
|
+
const {
|
|
14
|
+
filename = "导出数据",
|
|
15
|
+
fileExtension = "xlsx",
|
|
16
|
+
mimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8",
|
|
17
|
+
timeout = 60000,
|
|
18
|
+
loadingMessage = "正在导出,请稍候...",
|
|
19
|
+
successMessage = "导出成功!",
|
|
20
|
+
} = options
|
|
21
|
+
|
|
22
|
+
const downloading = ref(false)
|
|
23
|
+
|
|
24
|
+
const download = async (params: AnyRecord = {}): Promise<void> => {
|
|
25
|
+
downloading.value = true
|
|
26
|
+
try {
|
|
27
|
+
if (loadingMessage) {
|
|
28
|
+
ElMessage.info(loadingMessage)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const res = await apiFunction(params, {
|
|
32
|
+
responseType: "blob",
|
|
33
|
+
timeout,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// 尝试从 content-disposition 头提取真实文件名
|
|
37
|
+
let actualFilename = `${filename}_${dayjs().format("YYYYMMDDHHmmss")}.${fileExtension}`
|
|
38
|
+
const contentDisposition = res?.headers?.['content-disposition'] || res?.headers?.['Content-Disposition']
|
|
39
|
+
if (contentDisposition) {
|
|
40
|
+
const match = contentDisposition.match(/filename\*?=(?:UTF-8'')?([^;]+)/i)
|
|
41
|
+
if (match && match[1]) {
|
|
42
|
+
actualFilename = decodeURIComponent(match[1].replace(/['"]/g, '').trim())
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const data = res?.data || res
|
|
47
|
+
const blob = data instanceof Blob ? data : new Blob([data], { type: mimeType })
|
|
48
|
+
const url = URL.createObjectURL(blob)
|
|
49
|
+
const a = document.createElement("a")
|
|
50
|
+
|
|
51
|
+
a.href = url
|
|
52
|
+
a.download = actualFilename
|
|
53
|
+
document.body.appendChild(a)
|
|
54
|
+
a.click()
|
|
55
|
+
document.body.removeChild(a)
|
|
56
|
+
URL.revokeObjectURL(url)
|
|
57
|
+
|
|
58
|
+
if (successMessage) {
|
|
59
|
+
ElMessage.success(successMessage)
|
|
60
|
+
}
|
|
61
|
+
} catch (error: any) {
|
|
62
|
+
if (error?.response?.data instanceof Blob) {
|
|
63
|
+
const reader = new FileReader()
|
|
64
|
+
reader.onload = (e) => {
|
|
65
|
+
try {
|
|
66
|
+
const errorMsg = JSON.parse((e.target as FileReader).result as string).message
|
|
67
|
+
ElMessage.error(`导出失败:${errorMsg}`)
|
|
68
|
+
} catch {
|
|
69
|
+
ElMessage.error("导出失败:服务器返回异常")
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
reader.readAsText(error.response.data)
|
|
73
|
+
} else {
|
|
74
|
+
ElMessage.error(error?.message || "导出失败:网络异常或接口错误")
|
|
75
|
+
}
|
|
76
|
+
console.error("useDownload error:", error)
|
|
77
|
+
} finally {
|
|
78
|
+
downloading.value = false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { download, downloading }
|
|
83
|
+
}
|