12
This commit is contained in:
File diff suppressed because one or more lines are too long
203
admin/src/mixins/tplTableMixin.js
Normal file
203
admin/src/mixins/tplTableMixin.js
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
* 演示页 / 轻量业务列表用的表格与编辑辅助方法(原 tableMixin 能力的最小子集)。
|
||||||
|
*/
|
||||||
|
import { mapFormFieldsToEditColumns } from "@/utils/formEditColumns.js";
|
||||||
|
import { getForeignRowLabel } from "@/utils/foreignRowLabel.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* 把表单字段配置转成「编辑弹窗 / 表单」用的列定义(含下拉选项渲染等)。
|
||||||
|
* @param {Array} formFields 表单字段元数据
|
||||||
|
* @param {Object} selectSources 各字段对应的 { key: [{ key, value }] } 下拉数据
|
||||||
|
*/
|
||||||
|
generateEditColumns(formFields, selectSources) {
|
||||||
|
return mapFormFieldsToEditColumns(formFields, selectSources);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据列表展示字段生成 iView Table 的 columns;select 或 *_id 数字列用下拉文案渲染。
|
||||||
|
* @param {Array} listFields 列表要展示的字段配置
|
||||||
|
* @param {Array} _allFields 预留,与全量字段对齐时可扩展
|
||||||
|
* @param {Object} selectSources 与 generateEditColumns 同源的下拉映射
|
||||||
|
*/
|
||||||
|
generateListColumns(listFields, _allFields, selectSources) {
|
||||||
|
if (!Array.isArray(listFields)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return listFields.map((f) => {
|
||||||
|
const col = {
|
||||||
|
title: f.title || f.key,
|
||||||
|
key: f.key,
|
||||||
|
minWidth: f.minWidth || 120,
|
||||||
|
ellipsis: true,
|
||||||
|
};
|
||||||
|
const useLookup =
|
||||||
|
f.type === "select" ||
|
||||||
|
(f.type === "number" && /_id$/.test(String(f.key)));
|
||||||
|
if (useLookup) {
|
||||||
|
const srcKey = f.source_key || f.key;
|
||||||
|
col.render = (h, params) => {
|
||||||
|
const v = params.row[f.key];
|
||||||
|
const opts = (selectSources && selectSources[srcKey]) || [];
|
||||||
|
const o = opts.find(
|
||||||
|
(x) => x.key === v || String(x.key) === String(v)
|
||||||
|
);
|
||||||
|
return h("span", o ? o.value : v != null ? String(v) : "");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return col;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把一行记录转成人类可读的主展示文案(用于外键下拉、关联展示等)。
|
||||||
|
* @param {Object} row 后端返回的单条记录
|
||||||
|
*/
|
||||||
|
getRowLabel(row) {
|
||||||
|
return getForeignRowLabel(row);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按字段配置批量请求 `/{model}/all`,组装成 selectSources(下拉 key/value)。
|
||||||
|
* idFieldToModel(fieldKey) 返回的模型名为空则跳过该字段;依赖 this.$http。
|
||||||
|
* @param {Array} fields 表单或列表字段元数据
|
||||||
|
* @param {Function} idFieldToModel 字段 key -> 后端资源名(如 tenant_id -> sys_tenant)
|
||||||
|
* @param {Function} _getRowLabel 预留,可传入自定义取文案函数
|
||||||
|
*/
|
||||||
|
async loadSelectSources(fields, idFieldToModel, _getRowLabel) {
|
||||||
|
const result = {};
|
||||||
|
if (!Array.isArray(fields) || !this.$http) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
for (const f of fields) {
|
||||||
|
const model =
|
||||||
|
typeof idFieldToModel === "function" ? idFieldToModel(f.key) : "";
|
||||||
|
if (!model) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await this.$http.get(`/${model}/all`, {});
|
||||||
|
if (res && res.code === 0 && Array.isArray(res.data)) {
|
||||||
|
const key = f.source_key || f.key;
|
||||||
|
result[key] = res.data.map((r) => ({
|
||||||
|
key: r.id,
|
||||||
|
value: getForeignRowLabel(r),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("loadSelectSources skip", f.key, e && e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据字段上的 required 生成 iView Form 校验 rules(必填 + 失焦提示)。
|
||||||
|
* @param {Array} fields 表单字段元数据
|
||||||
|
*/
|
||||||
|
generateEditRules(fields) {
|
||||||
|
const rules = {};
|
||||||
|
if (!Array.isArray(fields)) {
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
for (const f of fields) {
|
||||||
|
if (f.required) {
|
||||||
|
const label = f.title || f.label || f.key;
|
||||||
|
rules[f.key] = [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请填写" + label,
|
||||||
|
trigger: "blur",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rules;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按字段类型生成一份「空表单」初始值(number 为 null,bool 为 false,其余为 "")。
|
||||||
|
* @param {Array} fields 表单字段元数据
|
||||||
|
*/
|
||||||
|
generateEmptyForm(fields) {
|
||||||
|
const form = {};
|
||||||
|
if (!Array.isArray(fields)) {
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
for (const f of fields) {
|
||||||
|
if (f.type === "number") {
|
||||||
|
form[f.key] = null;
|
||||||
|
} else if (f.type === "bool") {
|
||||||
|
form[f.key] = false;
|
||||||
|
} else {
|
||||||
|
form[f.key] = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return form;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从原始对象中只挑出 fields 里声明过的 key,用于提交前裁剪多余字段。
|
||||||
|
* @param {Object} raw 表单或行数据
|
||||||
|
* @param {Array} fields 允许的字段列表
|
||||||
|
*/
|
||||||
|
generatePayload(raw, fields) {
|
||||||
|
const out = {};
|
||||||
|
if (!raw || !Array.isArray(fields)) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
for (const f of fields) {
|
||||||
|
const k = f.key;
|
||||||
|
if (Object.prototype.hasOwnProperty.call(raw, k)) {
|
||||||
|
out[k] = raw[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑弹窗打开时浅拷贝一行,避免表格行对象与表单双向绑定互相污染。
|
||||||
|
* @param {Object} row 当前行
|
||||||
|
*/
|
||||||
|
normalizeRowForEditModal(row) {
|
||||||
|
return row && typeof row === "object" ? { ...row } : {};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表格「操作」列里渲染一组小按钮;点击会 stopPropagation,避免触发行选择。
|
||||||
|
* @param {Function} h Vue 的 createElement
|
||||||
|
* @param {Array<{ title, type?, click }>} items 按钮文案、样式类型、点击回调
|
||||||
|
*/
|
||||||
|
renderRowActionBtns(h, items) {
|
||||||
|
if (!Array.isArray(items) || !items.length) {
|
||||||
|
return h("span", "");
|
||||||
|
}
|
||||||
|
return h(
|
||||||
|
"div",
|
||||||
|
{ class: "table-row-actions" },
|
||||||
|
items.map((item, i) =>
|
||||||
|
h(
|
||||||
|
"Button",
|
||||||
|
{
|
||||||
|
key: i,
|
||||||
|
props: { type: item.type || "default", size: "small" },
|
||||||
|
style: { marginRight: i < items.length - 1 ? "8px" : "0" },
|
||||||
|
on: {
|
||||||
|
click: (e) => {
|
||||||
|
if (e && e.stopPropagation) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
if (typeof item.click === "function") {
|
||||||
|
item.click();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
item.title || ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -32,9 +32,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import tplDemoServer from "@/api/demo/tplDemoServer.js";
|
import tplDemoServer from "@/api/demo/tplDemoServer.js";
|
||||||
|
import tplTableMixin from "@/mixins/tplTableMixin.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "TplDemoPage",
|
name: "TplDemoPage",
|
||||||
|
mixins: [tplTableMixin],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -131,8 +133,8 @@ export default {
|
|||||||
buildEmptyForm() {
|
buildEmptyForm() {
|
||||||
return this.generateEmptyForm(this.meta.fields.filter((f) => f.form !== false));
|
return this.generateEmptyForm(this.meta.fields.filter((f) => f.form !== false));
|
||||||
},
|
},
|
||||||
normalizePayload(raw) {
|
formFields() {
|
||||||
return this.generatePayload(raw, this.meta.fields.filter((f) => f.form !== false));
|
return this.meta.fields.filter((f) => f.form !== false);
|
||||||
},
|
},
|
||||||
async query(page) {
|
async query(page) {
|
||||||
if (!this.modelName || !this.meta) {
|
if (!this.modelName || !this.meta) {
|
||||||
@@ -157,27 +159,47 @@ export default {
|
|||||||
this.resetSearchFromMeta();
|
this.resetSearchFromMeta();
|
||||||
this.query(1);
|
this.query(1);
|
||||||
},
|
},
|
||||||
|
/** 仅兜住网络/HTTP 异常;业务 code 由调用方判断 */
|
||||||
|
async safeApi(promise) {
|
||||||
|
try {
|
||||||
|
return await promise;
|
||||||
|
} catch (e) {
|
||||||
|
const d = e.response && e.response.data;
|
||||||
|
this.$Message.error((d && (d.message || d.msg)) || e.message || "请求失败");
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
showAdd() {
|
showAdd() {
|
||||||
|
const fields = this.formFields();
|
||||||
this.$refs.editModal.addShow(this.buildEmptyForm(), async (data) => {
|
this.$refs.editModal.addShow(this.buildEmptyForm(), async (data) => {
|
||||||
await this.persist(data, false);
|
const res = await this.safeApi(tplDemoServer.add(this.generatePayload(data, fields)));
|
||||||
|
if (res && res.code === 0) {
|
||||||
|
this.$Message.success("新增成功");
|
||||||
|
this.query(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$Message.error((res && (res.message || res.Msg)) || "新增失败");
|
||||||
|
throw new Error("save");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
showEdit(row) {
|
showEdit(row) {
|
||||||
|
const fields = this.formFields();
|
||||||
|
const rowId = row && row.id;
|
||||||
this.$refs.editModal.editShow(this.normalizeRowForEditModal({ ...row }), async (data) => {
|
this.$refs.editModal.editShow(this.normalizeRowForEditModal({ ...row }), async (data) => {
|
||||||
await this.persist(data, true);
|
const merged = { ...data, id: data.id != null && data.id !== "" ? data.id : rowId };
|
||||||
});
|
const payload = this.generatePayload(merged, fields);
|
||||||
},
|
if (merged.id != null && merged.id !== "") {
|
||||||
async persist(data, isEdit) {
|
payload.id = Number.isFinite(Number(merged.id)) ? Number(merged.id) : merged.id;
|
||||||
const payload = this.normalizePayload(data);
|
|
||||||
const req = isEdit ? tplDemoServer.edit(payload) : tplDemoServer.add(payload);
|
|
||||||
const res = await req;
|
|
||||||
if (res && res.code === 0) {
|
|
||||||
this.$Message.success(isEdit ? "保存成功" : "新增成功");
|
|
||||||
this.query(1);
|
|
||||||
} else {
|
|
||||||
this.$Message.error((res && (res.message || res.Msg)) || "保存失败");
|
|
||||||
throw new Error("save failed");
|
|
||||||
}
|
}
|
||||||
|
const res = await this.safeApi(tplDemoServer.edit(payload));
|
||||||
|
if (res && res.code === 0) {
|
||||||
|
this.$Message.success("保存成功");
|
||||||
|
this.query(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$Message.error((res && (res.message || res.Msg)) || "保存失败");
|
||||||
|
throw new Error("save");
|
||||||
|
});
|
||||||
},
|
},
|
||||||
delConfirm(row) {
|
delConfirm(row) {
|
||||||
this.$Modal.confirm({
|
this.$Modal.confirm({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const Sequelize = require("sequelize");
|
|||||||
|
|
||||||
/** 租户表:用户、角色按 tenant_id 隔离;is_platform=1 的租户可管理本表 */
|
/** 租户表:用户、角色按 tenant_id 隔离;is_platform=1 的租户可管理本表 */
|
||||||
module.exports = (db) => {
|
module.exports = (db) => {
|
||||||
return db.define("sys_tenant", {
|
const sys_tenant= db.define("sys_tenant", {
|
||||||
name: {
|
name: {
|
||||||
type: Sequelize.STRING(100),
|
type: Sequelize.STRING(100),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
@@ -37,4 +37,6 @@ module.exports = (db) => {
|
|||||||
comment: "是否平台租户",
|
comment: "是否平台租户",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return sys_tenant
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const Sequelize = require("sequelize");
|
|||||||
|
|
||||||
/** 模板示例:与库表字段 create_time / last_modify_time / is_delete 一致(无 createdAt/updatedAt) */
|
/** 模板示例:与库表字段 create_time / last_modify_time / is_delete 一致(无 createdAt/updatedAt) */
|
||||||
module.exports = (db) => {
|
module.exports = (db) => {
|
||||||
return db.define(
|
const tpl_demo = db.define(
|
||||||
"tpl_demo",
|
"tpl_demo",
|
||||||
{
|
{
|
||||||
title: {
|
title: {
|
||||||
@@ -57,4 +57,8 @@ module.exports = (db) => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// tpl_demo.sync({})
|
||||||
|
return tpl_demo
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user