This commit is contained in:
张成
2026-04-29 14:31:53 +08:00
parent 1850ec93a2
commit cfb92ce33d
5 changed files with 272 additions and 41 deletions

File diff suppressed because one or more lines are too long

View 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 的 columnsselect 或 *_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 为 nullbool 为 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 || ""
)
)
);
},
},
};

View File

@@ -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 this.safeApi(tplDemoServer.edit(payload));
const res = await req; if (res && res.code === 0) {
if (res && res.code === 0) { this.$Message.success("保存成功");
this.$Message.success(isEdit ? "保存成功" : "新增成功"); this.query(1);
this.query(1); return;
} else { }
this.$Message.error((res && (res.message || res.Msg)) || "保存失败"); this.$Message.error((res && (res.message || res.Msg)) || "保存失败");
throw new Error("save failed"); throw new Error("save");
} });
}, },
delConfirm(row) { delConfirm(row) {
this.$Modal.confirm({ this.$Modal.confirm({

View File

@@ -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
}; };

View File

@@ -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
}; };