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,114 @@
|
|
|
1
|
+
import { ref, reactive } from "vue"
|
|
2
|
+
import { ElMessage } from "element-plus"
|
|
3
|
+
import type { UseFormOptions, UseFormReturn, AnyRecord, ApiFn } from '../types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 表单状态与提交封装 Hook
|
|
7
|
+
*/
|
|
8
|
+
export function useForm<T extends AnyRecord = AnyRecord>(options: UseFormOptions<T> = {}): UseFormReturn<T> {
|
|
9
|
+
const {
|
|
10
|
+
initFormData = {} as T,
|
|
11
|
+
rules = {},
|
|
12
|
+
onSuccess,
|
|
13
|
+
onError,
|
|
14
|
+
transform,
|
|
15
|
+
} = options
|
|
16
|
+
|
|
17
|
+
const formRef = ref<any>(null)
|
|
18
|
+
const getInitialData = (): T => (typeof initFormData === "function" ? initFormData() : initFormData)
|
|
19
|
+
const formData = reactive<T>({ ...getInitialData() }) as T
|
|
20
|
+
const loading = ref(false)
|
|
21
|
+
|
|
22
|
+
// 提交表单 (skipValidate 可控制是否跳过校验)
|
|
23
|
+
const submit = async (
|
|
24
|
+
apiCall: ApiFn,
|
|
25
|
+
successMessage: string = "操作成功",
|
|
26
|
+
customTransform?: (formData: T) => any,
|
|
27
|
+
skipValidate: boolean = false,
|
|
28
|
+
): Promise<any> => {
|
|
29
|
+
if (!formRef.value && !skipValidate) return
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
if (!skipValidate) {
|
|
33
|
+
const valid = await validate()
|
|
34
|
+
if (!valid) {
|
|
35
|
+
ElMessage.warning("请检查表单填写是否正确")
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
loading.value = true
|
|
40
|
+
|
|
41
|
+
const transformFn = customTransform || transform
|
|
42
|
+
const submitData = transformFn ? transformFn(formData) : formData
|
|
43
|
+
|
|
44
|
+
const result = await apiCall(submitData)
|
|
45
|
+
|
|
46
|
+
if (successMessage) {
|
|
47
|
+
ElMessage.success(successMessage)
|
|
48
|
+
}
|
|
49
|
+
onSuccess?.(result)
|
|
50
|
+
|
|
51
|
+
return result
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error("useForm submit error:", error)
|
|
54
|
+
onError?.(error)
|
|
55
|
+
throw error
|
|
56
|
+
} finally {
|
|
57
|
+
loading.value = false
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 手动验证表单
|
|
62
|
+
const validate = async (): Promise<boolean> => {
|
|
63
|
+
if (!formRef.value) return false
|
|
64
|
+
try {
|
|
65
|
+
await formRef.value.validate()
|
|
66
|
+
return true
|
|
67
|
+
} catch {
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 验证指定字段
|
|
73
|
+
const validateField = (field: string, callback?: (...args: any[]) => void): void => {
|
|
74
|
+
if (!formRef.value) return
|
|
75
|
+
formRef.value.validateField(field, callback)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 重置表单
|
|
79
|
+
const reset = (): void => {
|
|
80
|
+
formRef.value?.resetFields()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 清空验证
|
|
84
|
+
const clearValidate = (props?: string | string[]): void => {
|
|
85
|
+
formRef.value?.clearValidate(props)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 设置表单数据
|
|
89
|
+
const setFormData = (data: Partial<T>): void => {
|
|
90
|
+
Object.assign(formData, data)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 重置为初始数据
|
|
94
|
+
const resetFormData = (): void => {
|
|
95
|
+
Object.keys(formData).forEach((key) => {
|
|
96
|
+
delete (formData as any)[key]
|
|
97
|
+
})
|
|
98
|
+
Object.assign(formData, getInitialData())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
formRef,
|
|
103
|
+
formData,
|
|
104
|
+
loading,
|
|
105
|
+
rules,
|
|
106
|
+
submit,
|
|
107
|
+
validate,
|
|
108
|
+
validateField,
|
|
109
|
+
reset,
|
|
110
|
+
clearValidate,
|
|
111
|
+
setFormData,
|
|
112
|
+
resetFormData,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { ref } from "vue"
|
|
2
|
+
import { ElMessage, ElMessageBox } from "element-plus"
|
|
3
|
+
import type { UseListPageOptions, UseListPageReturn, AnyRecord, NavigateLocation } from '../types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 列表页标准 CRUD 流程组合式函数
|
|
7
|
+
*/
|
|
8
|
+
export function useListPage(options: UseListPageOptions = {}): UseListPageReturn {
|
|
9
|
+
const {
|
|
10
|
+
apiList,
|
|
11
|
+
apiChangeState,
|
|
12
|
+
apiDelete,
|
|
13
|
+
apiBatchDelete,
|
|
14
|
+
apiExport,
|
|
15
|
+
addPath,
|
|
16
|
+
editPath,
|
|
17
|
+
navigate,
|
|
18
|
+
exportFileName = "数据导出",
|
|
19
|
+
stateKey = "state",
|
|
20
|
+
idKey = "id",
|
|
21
|
+
activeValue = 1,
|
|
22
|
+
inactiveValue = 0,
|
|
23
|
+
statusMap = { 0: '启用', 1: '停用' },
|
|
24
|
+
deleteConfirmText = "确定要删除该数据吗?删除后将无法恢复!",
|
|
25
|
+
deleteSuccessText = "删除成功",
|
|
26
|
+
deleteErrorText = "删除失败",
|
|
27
|
+
openDialog,
|
|
28
|
+
} = options
|
|
29
|
+
|
|
30
|
+
const tableRef = ref<any>(null)
|
|
31
|
+
const searchParams = ref<AnyRecord>({})
|
|
32
|
+
|
|
33
|
+
// 路由跳转适配
|
|
34
|
+
const doNavigate = (location: NavigateLocation): void => {
|
|
35
|
+
if (typeof navigate === 'function') {
|
|
36
|
+
navigate(location)
|
|
37
|
+
} else {
|
|
38
|
+
console.warn("useListPage: 未提供 navigate 函数,如需路由跳转请在 options 中传入 navigate: (loc) => router.push(loc)")
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 搜索处理
|
|
43
|
+
const handleSearch = (params?: AnyRecord): void => {
|
|
44
|
+
searchParams.value = params || {}
|
|
45
|
+
tableRef.value?.search?.(searchParams.value)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 重置搜索
|
|
49
|
+
const handleReset = (): void => {
|
|
50
|
+
searchParams.value = {}
|
|
51
|
+
tableRef.value?.reset?.()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 状态切换
|
|
55
|
+
const changeState = async (row: AnyRecord, callback?: (row: AnyRecord) => void): Promise<void> => {
|
|
56
|
+
if (!apiChangeState) {
|
|
57
|
+
console.warn("useListPage: 未提供 apiChangeState 接口")
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const currentState = row[stateKey]
|
|
63
|
+
const nextState = currentState === activeValue ? inactiveValue : activeValue
|
|
64
|
+
const action = statusMap[nextState] || '更新'
|
|
65
|
+
|
|
66
|
+
await ElMessageBox.confirm(`确定要${action}该数据吗?`, "提示", {
|
|
67
|
+
type: "warning",
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
await apiChangeState({
|
|
71
|
+
[idKey]: row[idKey],
|
|
72
|
+
[stateKey]: nextState,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
ElMessage.success(`${action}成功`)
|
|
76
|
+
tableRef.value?.refresh?.()
|
|
77
|
+
callback?.(row)
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error !== "cancel" && error !== "close") {
|
|
80
|
+
ElMessage.error("操作失败")
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 删除 - 单条
|
|
86
|
+
const handleDelete = async (row: AnyRecord, callback?: (row: AnyRecord) => void): Promise<void> => {
|
|
87
|
+
if (!apiDelete) {
|
|
88
|
+
console.warn("useListPage: 未提供 apiDelete 接口")
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await ElMessageBox.confirm(deleteConfirmText, "警告", {
|
|
94
|
+
confirmButtonText: "确定",
|
|
95
|
+
cancelButtonText: "取消",
|
|
96
|
+
type: "error",
|
|
97
|
+
distinguishCancelAndClose: true,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
await apiDelete({ [idKey]: row[idKey] })
|
|
101
|
+
|
|
102
|
+
ElMessage.success(deleteSuccessText)
|
|
103
|
+
tableRef.value?.refresh?.()
|
|
104
|
+
callback?.(row)
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error !== "cancel" && error !== "close") {
|
|
107
|
+
ElMessage.error(deleteErrorText)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 批量删除
|
|
113
|
+
const handleBatchDelete = async (rows: AnyRecord[], callback?: (rows: AnyRecord[]) => void): Promise<void> => {
|
|
114
|
+
if (!apiDelete) {
|
|
115
|
+
console.warn("useListPage: 未提供 apiDelete 接口")
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!rows || rows.length === 0) {
|
|
120
|
+
ElMessage.warning("请选择要删除的数据")
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await ElMessageBox.confirm(
|
|
126
|
+
`确定要删除选中的 ${rows.length} 条数据吗?删除后将无法恢复!`,
|
|
127
|
+
"警告",
|
|
128
|
+
{
|
|
129
|
+
confirmButtonText: "确定",
|
|
130
|
+
cancelButtonText: "取消",
|
|
131
|
+
type: "error",
|
|
132
|
+
distinguishCancelAndClose: true,
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
const ids = rows.map((row) => row[idKey])
|
|
137
|
+
|
|
138
|
+
if (apiBatchDelete) {
|
|
139
|
+
await apiBatchDelete({ ids })
|
|
140
|
+
} else if (apiDelete.batch) {
|
|
141
|
+
await apiDelete.batch({ ids })
|
|
142
|
+
} else {
|
|
143
|
+
await Promise.all(ids.map((id) => apiDelete({ [idKey]: id })))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
ElMessage.success(`成功删除 ${rows.length} 条数据`)
|
|
147
|
+
tableRef.value?.refresh?.()
|
|
148
|
+
callback?.(rows)
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error !== "cancel" && error !== "close") {
|
|
151
|
+
ElMessage.error("删除失败")
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 导出
|
|
157
|
+
const exportExcel = async (): Promise<void> => {
|
|
158
|
+
if (!apiExport) {
|
|
159
|
+
console.warn("useListPage: 未提供 apiExport 接口")
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const res = await apiExport(searchParams.value)
|
|
165
|
+
const data = res?.data || res
|
|
166
|
+
const blob = data instanceof Blob ? data : new Blob([data], {
|
|
167
|
+
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
168
|
+
})
|
|
169
|
+
const url = window.URL.createObjectURL(blob)
|
|
170
|
+
const link = document.createElement("a")
|
|
171
|
+
link.href = url
|
|
172
|
+
link.download = `${exportFileName}_${Date.now()}.xlsx`
|
|
173
|
+
document.body.appendChild(link)
|
|
174
|
+
link.click()
|
|
175
|
+
document.body.removeChild(link)
|
|
176
|
+
window.URL.revokeObjectURL(url)
|
|
177
|
+
ElMessage.success("导出成功")
|
|
178
|
+
} catch (error) {
|
|
179
|
+
ElMessage.error("导出失败")
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 新增
|
|
184
|
+
const handleAdd = (query: AnyRecord = {}): void => {
|
|
185
|
+
if (!addPath) {
|
|
186
|
+
console.warn("useListPage: 未提供 addPath")
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
doNavigate({ path: addPath, query })
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const handleDialog = (data?: any): void => {
|
|
193
|
+
if (typeof openDialog === "function") {
|
|
194
|
+
openDialog(data)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 编辑
|
|
199
|
+
const handleEdit = (row: AnyRecord, query: AnyRecord = {}): void => {
|
|
200
|
+
if (!editPath) {
|
|
201
|
+
console.warn("useListPage: 未提供 editPath")
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
doNavigate({
|
|
205
|
+
path: editPath,
|
|
206
|
+
query: { id: row[idKey], ...query },
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 查看详情
|
|
211
|
+
const handleView = (row: AnyRecord, detailPath: string, query: AnyRecord = {}): void => {
|
|
212
|
+
if (!detailPath) {
|
|
213
|
+
console.warn("useListPage: 未提供 detailPath")
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
doNavigate({
|
|
217
|
+
path: detailPath,
|
|
218
|
+
query: { id: row[idKey], ...query },
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 刷新表格
|
|
223
|
+
const refresh = (): void => {
|
|
224
|
+
tableRef.value?.refresh?.()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
tableRef,
|
|
229
|
+
searchParams,
|
|
230
|
+
handleSearch,
|
|
231
|
+
handleReset,
|
|
232
|
+
changeState,
|
|
233
|
+
handleDelete,
|
|
234
|
+
handleBatchDelete,
|
|
235
|
+
handleDialog,
|
|
236
|
+
exportExcel,
|
|
237
|
+
handleAdd,
|
|
238
|
+
handleEdit,
|
|
239
|
+
handleView,
|
|
240
|
+
refresh,
|
|
241
|
+
apiList,
|
|
242
|
+
}
|
|
243
|
+
}
|
package/src/env.d.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { App, Component } from 'vue'
|
|
2
|
+
import { components } from './components/index'
|
|
3
|
+
|
|
4
|
+
// 导出所有组件
|
|
5
|
+
export * from './components/index'
|
|
6
|
+
|
|
7
|
+
// 导出所有组合式函数 (Hooks)
|
|
8
|
+
export * from './composables/index'
|
|
9
|
+
|
|
10
|
+
// 导出所有通用工具类
|
|
11
|
+
export * from './utils/index'
|
|
12
|
+
|
|
13
|
+
// 导出类型定义
|
|
14
|
+
export type * from './types'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 将 PascalCase 转换为 kebab-case
|
|
18
|
+
* 例如: CSimpleTable -> c-simple-table
|
|
19
|
+
*/
|
|
20
|
+
function toKebabCase(str: string): string {
|
|
21
|
+
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 全局安装插件
|
|
26
|
+
* 规范:固定强制使用 C / c- 前缀,不支持自定义前缀,确保各项目命名一致性
|
|
27
|
+
*/
|
|
28
|
+
const install = (app: App): void => {
|
|
29
|
+
components.forEach((component: Component) => {
|
|
30
|
+
if (component.name) {
|
|
31
|
+
// 注册 PascalCase 形式,例如 CSimpleTable
|
|
32
|
+
app.component(component.name, component)
|
|
33
|
+
// 注册 kebab-case 形式,例如 c-simple-table
|
|
34
|
+
const kebabName = toKebabCase(component.name)
|
|
35
|
+
if (kebabName !== component.name) {
|
|
36
|
+
app.component(kebabName, component)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default {
|
|
43
|
+
install,
|
|
44
|
+
}
|