z-crud-table
Version:
A powerful and flexible CRUD table component for Vue 3 and Element Plus.
488 lines (487 loc) • 19.1 kB
JavaScript
import { defineComponent as W, ref as A, resolveComponent as f, createBlock as p, openBlock as n, mergeProps as v, withCtx as s, createElementBlock as h, Fragment as x, renderList as q, createCommentVNode as g, createTextVNode as _, toDisplayString as ae, computed as P, reactive as L, onMounted as re, resolveDirective as ne, normalizeClass as ie, renderSlot as y, createElementVNode as O, withDirectives as T, createVNode as b, createSlots as se } from "vue";
import { ElMessage as F } from "element-plus";
import ue from "axios";
const de = /* @__PURE__ */ W({
__name: "DynamicForm",
props: {
modelValue: {},
formConfig: {},
rules: {}
},
setup(m, { expose: C }) {
const z = A(null);
return C({
validate: () => {
var i;
return (i = z.value) == null ? void 0 : i.validate();
}
}), (i, t) => {
const B = f("el-input"), j = f("el-option"), w = f("el-select"), d = f("el-radio"), D = f("el-radio-group"), k = f("el-form-item"), U = f("el-form");
return n(), p(U, v({
model: i.modelValue,
rules: i.rules,
ref_key: "formRef",
ref: z
}, i.$attrs), {
default: s(() => [
(n(!0), h(x, null, q(i.formConfig, (a) => (n(), p(k, {
key: a.prop,
label: a.label,
prop: a.prop
}, {
default: s(() => [
a.type === "input" ? (n(), p(B, v({
key: 0,
modelValue: i.modelValue[a.prop],
"onUpdate:modelValue": (o) => i.modelValue[a.prop] = o
}, { ref_for: !0 }, a.componentProps), null, 16, ["modelValue", "onUpdate:modelValue"])) : g("", !0),
a.type === "textarea" ? (n(), p(B, v({
key: 1,
type: "textarea",
modelValue: i.modelValue[a.prop],
"onUpdate:modelValue": (o) => i.modelValue[a.prop] = o
}, { ref_for: !0 }, a.componentProps), null, 16, ["modelValue", "onUpdate:modelValue"])) : g("", !0),
a.type === "select" ? (n(), p(w, v({
key: 2,
modelValue: i.modelValue[a.prop],
"onUpdate:modelValue": (o) => i.modelValue[a.prop] = o
}, { ref_for: !0 }, a.componentProps), {
default: s(() => [
(n(!0), h(x, null, q(a.options, (o) => (n(), p(j, {
key: o.value,
label: o.label,
value: o.value
}, null, 8, ["label", "value"]))), 128))
]),
_: 2
}, 1040, ["modelValue", "onUpdate:modelValue"])) : g("", !0),
a.type === "radio-group" ? (n(), p(D, v({
key: 3,
modelValue: i.modelValue[a.prop],
"onUpdate:modelValue": (o) => i.modelValue[a.prop] = o
}, { ref_for: !0 }, a.componentProps), {
default: s(() => [
(n(!0), h(x, null, q(a.options, (o) => (n(), p(d, {
key: o.value,
label: o.value
}, {
default: s(() => [
_(ae(o.label), 1)
]),
_: 2
}, 1032, ["label"]))), 128))
]),
_: 2
}, 1040, ["modelValue", "onUpdate:modelValue"])) : g("", !0),
a.type === "input-disabled" ? (n(), p(B, v({
key: 4,
"model-value": i.modelValue[a.prop],
disabled: ""
}, { ref_for: !0 }, a.componentProps), null, 16, ["model-value"])) : g("", !0)
]),
_: 2
}, 1032, ["label", "prop"]))), 128))
]),
_: 1
}, 16, ["model", "rules"]);
};
}
}), S = ue.create({
// VITE_APP_BASE_API 是在 .env 文件中定义的基础 URL
// 您可以根据您的项目需求进行修改
baseURL: "",
timeout: 1e4
// 请求超时时间
});
S.interceptors.request.use(
(m) => {
const C = localStorage.getItem("token");
return C && (m.headers.Authorization = "Bearer " + C), m;
},
(m) => (console.log(m), Promise.reject(m))
);
S.interceptors.response.use(
(m) => m.data,
(m) => (console.log("err" + m), F({
message: "接口错误,请刷新接口",
// 这里是您要求的统一错误提示
type: "error",
duration: 5 * 1e3
}), Promise.reject(m))
);
const pe = { class: "flex flex-wrap items-center justify-between gap-4 mb-6" }, fe = { class: "flex items-center gap-x-2" }, me = { class: "flex items-center gap-x-3 action-buttons flex-shrink-0" }, ge = {
key: 0,
class: "flex items-center gap-x-2"
}, ce = {
key: 0,
class: "flex justify-end"
}, ye = { class: "dialog-footer" }, he = /* @__PURE__ */ W({
__name: "CrudTable",
props: {
/**
* @description 组件的主题风格
* @type {'default' | 'large-screen'}
*/
theme: { type: String, default: "default" },
// API 相关配置
apiUrlQuery: { type: String, required: !0 },
apiUrlDetail: { type: String, required: !0 },
apiUrlCreate: { type: String, required: !0 },
apiUrlUpdate: { type: String, required: !0 },
apiUrlDelete: { type: String, required: !0 },
// 表格列定义,通过配置数组动态渲染,比 slot 更灵活。
columns: {
type: Array,
default: () => []
},
// 生命周期钩子函数,允许在特定操作前后注入自定义逻辑。
onBeforeQuery: { type: Function },
onAfterQuery: { type: Function },
onBeforeOpenDialog: { type: Function },
onAfterOpenDialog: { type: Function },
onBeforeSubmit: { type: Function },
onAfterSubmit: { type: Function },
onBeforeDelete: { type: Function },
onAfterDelete: { type: Function },
// 功能开关,控制UI元素的显示/隐藏。
showSelectionColumn: { type: Boolean, default: !0 },
showIndexColumn: { type: Boolean, default: !0 },
showActionsColumn: { type: Boolean, default: !0 },
showEditButton: { type: Boolean, default: !0 },
showDeleteButton: { type: Boolean, default: !0 },
// UI 定制化配置
actionsColumnWidth: { type: [String, Number], default: 120 },
dialogWidth: { type: String, default: "50%" },
initialSearchForm: { type: Object, default: () => ({ pageNum: 1, pageSize: 10 }) },
// 分页相关配置
showPagination: { type: Boolean, default: !0 },
pageSizes: { type: Array, default: () => [10, 20, 50, 100] },
paginationLayout: { type: String, default: "total, sizes, prev, pager, next, jumper" },
paginationBackground: { type: Boolean, default: !0 },
paginationSmall: { type: Boolean, default: !1 },
paginationHideOnSinglePage: { type: Boolean, default: !1 },
// 弹窗表单配置
dialogFormConfig: { type: Array, default: () => [] },
dialogFormRules: { type: Object, default: () => ({}) }
},
emits: ["open-dialog", "submit", "delete"],
setup(m, { expose: C, emit: z }) {
const i = z, t = m, B = P(() => ["crud-table-wrapper", `theme-${t.theme}`]), j = P(() => t.theme === "large-screen" ? "large-screen-dialog" : ""), w = (e, l) => e ? !0 : (F.error(`${l} prop is required.`), !1), d = L({ pageNum: 1, pageSize: 10, ...t.initialSearchForm }), D = A([]), k = A(0), U = A(!1), a = A([]), o = L({ visible: !1, loading: !1, submitting: !1, mode: "add", data: {}, formRef: null }), H = P(() => o.mode === "add" ? "新增" : "编辑"), M = P(() => {
if (o.mode === "add") return t.dialogFormConfig.filter((l) => l.prop !== "id");
const e = [...t.dialogFormConfig.filter((l) => l.prop !== "id")];
return e.some((l) => l.prop === "id") || e.unshift({ type: "input-disabled", prop: "id", label: "用户ID" }), e;
}), V = async () => {
if (w(t.apiUrlQuery, "apiUrlQuery")) {
U.value = !0;
try {
let e = { ...d };
t.onBeforeQuery && (e = await t.onBeforeQuery(e));
const l = await S.get(t.apiUrlQuery, { params: e });
if (l && Array.isArray(l.data) && typeof l.total == "number") {
let u = l.data;
t.onAfterQuery && (u = await t.onAfterQuery(u)), D.value = u, k.value = l.total;
} else
console.warn("API response is not in the expected { data: [], total: 0 } format."), D.value = [], k.value = 0;
} catch (e) {
console.error("Fetch data failed:", e);
} finally {
U.value = !1;
}
}
}, $ = () => {
d.pageNum = 1, V();
}, G = () => {
const { pageNum: e, pageSize: l, ...u } = t.initialSearchForm;
Object.keys(d).forEach((c) => {
c !== "pageNum" && c !== "pageSize" && delete d[c];
}), Object.assign(d, u), $();
}, J = (e) => {
a.value = e;
}, Q = async (e, l) => {
let u = e === "add" ? { role: "user" } : { ...l };
if (t.onBeforeOpenDialog) {
const c = await t.onBeforeOpenDialog(e, u);
c && (u = c);
}
if (o.mode = e, o.visible = !0, e === "edit") {
if (!w(t.apiUrlDetail, "apiUrlDetail")) return;
o.loading = !0;
try {
const c = await S.get(t.apiUrlDetail + "/" + u.id);
o.data = c.data;
} finally {
o.loading = !1, t.onAfterOpenDialog && t.onAfterOpenDialog(e, o.data), i("open-dialog", { mode: e, data: o.data });
}
} else
o.data = u, t.onAfterOpenDialog && t.onAfterOpenDialog(e, o.data), i("open-dialog", { mode: e, data: o.data });
}, K = async () => {
try {
o.formRef && await o.formRef.validate();
let e = { ...o.data };
if (t.onBeforeSubmit && (e = await t.onBeforeSubmit(e)), o.submitting = !0, o.mode === "add") {
if (!w(t.apiUrlCreate, "apiUrlCreate")) return;
await S.post(t.apiUrlCreate, e), F.success("新增成功");
} else {
if (!w(t.apiUrlUpdate, "apiUrlUpdate")) return;
await S.put(t.apiUrlUpdate, e), F.success("更新成功");
}
t.onAfterSubmit && t.onAfterSubmit(o.mode, e), i("submit", { mode: o.mode, data: e }), o.visible = !1, V();
} catch (e) {
console.log("Submit error or validation failed:", e);
} finally {
o.submitting = !1;
}
}, E = async (e) => {
if (w(t.apiUrlDelete, "apiUrlDelete"))
try {
if (t.onBeforeDelete && await t.onBeforeDelete(e) === !1)
return;
const l = e.join(",");
await S.delete(t.apiUrlDelete + "/" + l.toString()), F.success("删除成功"), t.onAfterDelete && t.onAfterDelete(e), i("delete", e), D.value.length === e.length && d.pageNum > 1 && d.pageNum--, V();
} catch (l) {
console.error("Delete failed", l);
}
}, X = (e) => {
d.pageSize = e, $();
}, Y = (e) => {
d.pageNum = e, V();
};
return re(V), C({
refresh: V,
// 刷新表格
search: $,
// 按当前条件搜索
handleDelete: E,
// 手动触发删除
openDialog: Q
// 手动打开弹窗
}), (e, l) => {
const u = f("el-button"), c = f("el-form-item"), Z = f("el-form"), N = f("el-table-column"), ee = f("el-popconfirm"), te = f("el-table"), oe = f("el-pagination"), le = f("el-dialog"), I = ne("loading");
return n(), h("div", {
class: ie(B.value)
}, [
y(e.$slots, "header"),
O("div", pe, [
b(Z, {
model: d,
class: "query-form flex flex-nowrap items-center gap-x-4",
style: { "overflow-x": "auto", "padding-bottom": "8px" }
}, {
default: s(() => [
y(e.$slots, "query-conditions", { searchForm: d }),
b(c, { class: "!mr-0 flex-shrink-0" }, {
default: s(() => [
O("div", fe, [
y(e.$slots, "query-left"),
b(u, {
style: { "margin-left": "16px" },
type: "primary",
onClick: $,
loading: U.value
}, {
default: s(() => l[6] || (l[6] = [
_("搜索 ")
])),
_: 1,
__: [6]
}, 8, ["loading"]),
b(u, { onClick: G }, {
default: s(() => l[7] || (l[7] = [
_("清空")
])),
_: 1,
__: [7]
}),
y(e.$slots, "query-right")
])
]),
_: 3
})
]),
_: 3
}, 8, ["model"]),
O("div", me, [
y(e.$slots, "action-left", { selections: a.value }),
y(e.$slots, "add-button-content", {}, () => [
b(u, {
type: "success",
onClick: l[0] || (l[0] = (r) => Q("add"))
}, {
default: s(() => l[8] || (l[8] = [
_("新增")
])),
_: 1,
__: [8]
})
]),
y(e.$slots, "action-right")
])
]),
T((n(), p(te, v({
data: D.value,
onSelectionChange: J
}, e.$attrs, { style: { width: "100%", "margin-bottom": "1.5rem" } }), {
default: s(() => [
t.showSelectionColumn ? (n(), p(N, {
key: 0,
type: "selection",
width: "55",
fixed: ""
})) : g("", !0),
t.showIndexColumn ? (n(), p(N, {
key: 1,
type: "index",
label: "序号",
width: "70",
fixed: ""
})) : g("", !0),
(n(!0), h(x, null, q(t.columns, (r) => (n(), p(N, v({
key: r.prop,
prop: r.prop,
label: r.label,
width: r.width,
sortable: r.sortable || !1
}, { ref_for: !0 }, r.attrs), se({ _: 2 }, [
r.slot ? {
name: "default",
fn: s((R) => [
y(e.$slots, r.slot, {
row: R.row
})
]),
key: "0"
} : void 0
]), 1040, ["prop", "label", "width", "sortable"]))), 128)),
t.showActionsColumn ? (n(), p(N, {
key: 2,
label: "操作",
width: m.actionsColumnWidth
}, {
default: s((r) => [
r.row ? (n(), h("div", ge, [
y(e.$slots, "actions", {
row: r.row
}, () => [
t.showEditButton ? (n(), p(u, {
key: 0,
size: "small",
type: "primary",
link: "",
onClick: (R) => Q("edit", r.row)
}, {
default: s(() => l[9] || (l[9] = [
_("编辑 ")
])),
_: 2,
__: [9]
}, 1032, ["onClick"])) : g("", !0),
t.showDeleteButton ? (n(), p(ee, {
key: 1,
title: "确定要删除这条数据吗?",
onConfirm: (R) => E([r.row.id]),
"confirm-button-text": "确定",
"cancel-button-text": "取消",
width: "200"
}, {
reference: s(() => [
b(u, {
size: "small",
type: "danger",
link: ""
}, {
default: s(() => l[10] || (l[10] = [
_("删除")
])),
_: 1,
__: [10]
})
]),
_: 2
}, 1032, ["onConfirm"])) : g("", !0)
])
])) : g("", !0)
]),
_: 3
}, 8, ["width"])) : g("", !0)
]),
_: 3
}, 16, ["data"])), [
[I, U.value]
]),
t.showPagination && k.value > 0 ? (n(), h("div", ce, [
b(oe, {
"current-page": d.pageNum,
"onUpdate:currentPage": l[1] || (l[1] = (r) => d.pageNum = r),
"page-size": d.pageSize,
"onUpdate:pageSize": l[2] || (l[2] = (r) => d.pageSize = r),
"page-sizes": t.pageSizes,
layout: t.paginationLayout,
total: k.value,
background: t.paginationBackground,
small: t.paginationSmall,
"hide-on-single-page": t.paginationHideOnSinglePage,
onSizeChange: X,
onCurrentChange: Y
}, null, 8, ["current-page", "page-size", "page-sizes", "layout", "total", "background", "small", "hide-on-single-page"])
])) : g("", !0),
b(le, {
modelValue: o.visible,
"onUpdate:modelValue": l[5] || (l[5] = (r) => o.visible = r),
title: H.value,
width: t.dialogWidth,
"destroy-on-close": !0,
"custom-class": j.value
}, {
footer: s(() => [
O("div", ye, [
b(u, {
onClick: l[4] || (l[4] = (r) => o.visible = !1)
}, {
default: s(() => l[11] || (l[11] = [
_("取消")
])),
_: 1,
__: [11]
}),
b(u, {
type: "primary",
onClick: K,
loading: o.submitting
}, {
default: s(() => l[12] || (l[12] = [
_("确定")
])),
_: 1,
__: [12]
}, 8, ["loading"])
])
]),
default: s(() => [
T((n(), h("div", null, [
y(e.$slots, "dialog-form-content", {
formData: o.data,
mode: o.mode
}, () => [
t.dialogFormConfig.length > 0 ? (n(), p(de, {
key: 0,
"form-config": M.value,
modelValue: o.data,
"onUpdate:modelValue": l[3] || (l[3] = (r) => o.data = r),
ref: (r) => o.formRef = r,
rules: t.dialogFormRules,
"label-width": "80px"
}, null, 8, ["form-config", "modelValue", "rules"])) : g("", !0)
])
])), [
[I, o.loading]
])
]),
_: 3
}, 8, ["modelValue", "title", "width", "custom-class"])
], 2);
};
}
});
export {
he as CrudTable
};