diff --git a/README.md b/README.md index 9266152..12009ab 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ 1. 在 MySQL 中建空库,例如:`CREATE DATABASE your_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;` 2. `USE your_db;` -3. 执行 **`sql/init.sql`**(与 `migrations/init.sql` 内容一致)。会 `DROP`/`CREATE` 系统表并写入:默认租户、**超级管理员角色**、用户 **`admin` / `123456`**(MD5 与框架一致)、`sys_title` / `sys_logo`、菜单等。**库名须与下一步 `config.development.js` 里的 `db.database` 一致。** +3. 执行 **`sql/init.sql`**(与 `migrations/init.sql` 内容一致)。会 `DROP`/`CREATE` 系统表并写入:默认租户、**超级管理员角色**、用户 **`admin` / `123456`**、**`zc` / `zc123`**(MD5 与框架一致)、`sys_title` / `sys_logo`、菜单等。**库名须与下一步 `config.development.js` 里的 `db.database` 一致。** 4. 需要「演示数据」列表时,再执行 **`sql/tpl_demo.sql`**。 ### 3. 配置 `config/config.development.js` @@ -33,7 +33,7 @@ ### 6. 登录与排错 -- 默认:**`admin` / `123456`**;租户可不传或按 `sys_tenant.code`(如 `default`)。 +- 默认:**`admin` / `123456`** ;租户可不传或按 `sys_tenant.code`(如 `default`)。 - **`sys_file` 加载失败**:检查 `aliyun` 字段是否非空或已配置环境变量。 - **连库失败**:核对 `db`、防火墙、是否已执行 `init.sql`。 diff --git a/admin/src/mixins/tableMixin.js b/admin/src/mixins/tableMixin.js deleted file mode 100644 index eaa0944..0000000 --- a/admin/src/mixins/tableMixin.js +++ /dev/null @@ -1,461 +0,0 @@ -// 通用表格页面混入 -import dynamicModelApi from '@/api/entity/dynamicModelApi.js'; -import { mapFormFieldsToEditColumns } from '@/utils/formEditColumns.js'; -import { getForeignRowLabel, rowsToIdOptions } from '@/utils/foreignRowLabel.js'; -import { orderActionBtnsViewEditFirst } from '@/utils/actionBtnOrder.js'; - -export default { - created() { - this.ensure_meta_shape(); - }, - methods: { - /** 操作列:查看、编辑排在最左侧相邻,其余按钮顺序在后 */ - renderRowActionBtns(h, btns) { - return window.framework.uiTool.getBtn(h, orderActionBtnsViewEditFirst(btns)); - }, - - field_display_name(f) { - if (!f) return ""; - return f.label || f.title || f.key || ""; - }, - - /** - * 按 meta.formFields 整理行数据,供 editModal 绑定(InputNumber 不能接收 "")。 - * 外键数字字段空值用 undefined;其它 number 空值或非数字用 0。 - */ - normalizeRowForEditModal(row) { - if (!row || typeof row !== 'object') return row; - const fields = - this.meta && Array.isArray(this.meta.formFields) ? this.meta.formFields : []; - if (!fields.length) return { ...row }; - const o = { ...row }; - for (const f of fields) { - if (!f || f.form === false || f.type !== 'number') continue; - const k = f.key; - const v = o[k]; - if (/_id$/.test(k)) { - if (v === '' || v === null || v === undefined) { - o[k] = undefined; - } else { - const n = Number(v); - o[k] = Number.isNaN(n) ? undefined : n; - } - } else { - if (v === '' || v === null || v === undefined) { - o[k] = 0; - } else { - const n = Number(v); - o[k] = Number.isNaN(n) ? 0 : n; - } - } - } - return o; - }, - - // 兼容页面只定义 fields 的场景,统一补齐 formFields / columns - ensure_meta_shape() { - if (!this.meta || typeof this.meta !== 'object') { - return; - } - - const fields = Array.isArray(this.meta.formFields) - ? this.meta.formFields - : (Array.isArray(this.meta.fields) ? this.meta.fields : []); - - // code 字段:后端自动生成,表单中不显示 - fields.forEach((f) => { - if (f && f.key === 'code') { - f.form = false; - f.required = false; - } - }); - - if (!Array.isArray(this.meta.formFields)) { - const formFields = fields.filter((f) => f && f.form !== false); - this.$set(this.meta, 'formFields', formFields); - } - - if (!Array.isArray(this.meta.columns)) { - const columns = fields - .filter((f) => f && f.list) - .map((f) => ({ - key: f.key, - title: f.label || f.title, - minWidth: f.minWidth, - })); - - this.$set(this.meta, 'columns', columns); - } - }, - - // 通用的编辑列配置生成方法(实现见 utils/formEditColumns.js) - generateEditColumns(formFields, selectSources) { - return mapFormFieldsToEditColumns(formFields, selectSources); - }, - - /** - * 标准列表列:外键列显示关联名称(select_sources)、布尔、日期等 - * meta 需含 columns,以及 fields 或 formFields(带 type) - */ - buildListColumnsFromMeta(meta, selectSources) { - if (!meta || !Array.isArray(meta.columns)) return []; - const allFields = meta.fields || meta.formFields || []; - return this.generateListColumns(meta.columns, allFields, selectSources); - }, - - // 通用的列表列配置生成方法 - generateListColumns(columns, allFields, selectSources) { - if (!columns || !Array.isArray(columns)) return []; - - const fields = Array.isArray(allFields) ? allFields : []; - - /** 系统统一时间字段(与后端 create_time / last_modify_time 一致),排在业务列之后、操作列之前 */ - const colList = [...columns]; - const keySet = new Set(colList.map((c) => c && c.key).filter(Boolean)); - if (!keySet.has('create_time')) { - colList.push({ key: 'create_time', title: '创建时间', minWidth: 160 }); - keySet.add('create_time'); - } - if (!keySet.has('last_modify_time')) { - colList.push({ key: 'last_modify_time', title: '最后修改时间', minWidth: 160 }); - keySet.add('last_modify_time'); - } - - const boolKeys = new Set(fields.filter((f) => f.type === 'bool').map((f) => f.key)); - const selectKeys = new Set(fields.filter((f) => f.type === 'select').map((f) => f.key)); - - return colList.map((c) => { - // 外键:已加载下拉数据时显示名称(含仓库/库区/巷道及 mat_sku 等) - if ( - selectSources && - selectSources[c.key] && - Array.isArray(selectSources[c.key]) && - selectSources[c.key].length && - /_id$/.test(c.key) - ) { - return { - ...c, - render: (h, params) => { - const id = params.row[c.key]; - const source = selectSources[c.key]; - const item = source && source.find((w) => String(w.key) === String(id)); - return h('span', item ? item.value : id != null && id !== '' ? String(id) : '-'); - }, - }; - } - - // 处理选择字段显示(status 按字段 source_key) - if (selectKeys.has(c.key)) { - const fieldDef = fields.find((f) => f.key === c.key); - const source_key = fieldDef && fieldDef.source_key; - return { - ...c, - render: (h, params) => { - const value = params.row[c.key]; - let source; - if (source_key && selectSources && selectSources[source_key]) { - source = selectSources[source_key]; - } else if (c.key === 'type') { - source = selectSources && selectSources.warehouse_type_options; - } else if (c.key === 'tray_type_id') { - source = selectSources && selectSources.tray_type_id; - } else if (c.key === 'lock_type') { - source = selectSources && selectSources.lock_type_options; - } else if (c.key === 'status' && source_key) { - source = selectSources && selectSources[source_key]; - } else if (c.key === 'status') { - source = selectSources && selectSources.tray_status_options; - } else { - source = selectSources && selectSources.yes_no_options; - } - const option = source && source.find((opt) => String(opt.key) === String(value)); - return h('span', option ? option.value : value); - }, - }; - } - - // 处理布尔字段显示 - if (boolKeys.has(c.key)) { - return { - ...c, - render: (h, params) => { - const v = params.row[c.key]; - const on = v === true || v === 1 || v === '1'; - return h('Tag', { props: { color: on ? 'green' : 'default' } }, on ? '是' : '否'); - }, - }; - } - - // 处理日期字段显示 - if (this.isDateLikeColumnKey && this.isDateLikeColumnKey(c.key)) { - return { - ...c, - render: (h, params) => { - const raw = - c.key === 'create_time' || c.key === 'last_modify_time' - ? this.pickRowTimeCell(params.row, c.key) - : params.row[c.key]; - return h('span', this.formatCellDate(raw, c.key)); - }, - }; - } - - return { ...c }; - }); - }, - - // 从 Vuex store (wms_dict) 获取字典选项 - getDictOptions(key) { - if (!this.$store || !this.$store.state.wms_dict) return []; - return this.$store.state.wms_dict[key] || []; - }, - - // 通用的数据源加载方法(从 store 读取字典,不再请求 sys_parameter) - async loadSelectSources(formFields, idFieldToModelFn, getRowLabelFn) { - const sources = {}; - - sources.yes_no_options = this.getDictOptions('yes_no'); - sources.warehouse_type_options = this.getDictOptions('warehouse_type'); - sources.lock_type_options = this.getDictOptions('lock_type'); - sources.tray_status_options = this.getDictOptions('tray_status'); - sources.tray_type_status_options = this.getDictOptions('tray_type_status'); - sources.receiving_status_options = this.getDictOptions('receiving_status'); - sources.inbound_status_options = this.getDictOptions('inbound_status'); - sources.send_status_options = this.getDictOptions('send_status'); - sources.outbound_status_options = this.getDictOptions('outbound_status'); - sources.move_status_options = this.getDictOptions('move_status'); - sources.damage_status_options = this.getDictOptions('damage_status'); - sources.count_status_options = this.getDictOptions('count_status'); - sources.receiving_type_options = this.getDictOptions('receiving_type'); - sources.send_type_options = this.getDictOptions('send_type'); - sources.fee_charge_type_options = this.getDictOptions('fee_charge_type_options'); - sources.fee_generate_type_options = this.getDictOptions('fee_generate_type_options'); - sources.fee_collection_type_options = this.getDictOptions('fee_collection_type_options'); - sources.fee_item_inbound_options = this.getDictOptions('fee_item_inbound_options'); - sources.fee_item_outbound_options = this.getDictOptions('fee_item_outbound_options'); - sources.packaging_status_options = this.getDictOptions('packaging_status_options'); - - // 加载外键数据 - const idFields = formFields - .filter((f) => f.type === 'number' && /_id$/.test(f.key)) - .map((f) => f.key); - - for (const k of idFields) { - const model = idFieldToModelFn(k); - if (!model) { - sources[k] = []; - continue; - } - try { - const res = await dynamicModelApi.all(model); - const rows = res && res.code === 0 && res.data ? res.data : []; - sources[k] = rowsToIdOptions(rows, getRowLabelFn || getForeignRowLabel); - } catch (e) { - sources[k] = []; - } - } - - return sources; - }, - - // 通用的空表单构建方法 - generateEmptyForm(formFields) { - const o = {}; - if (!formFields) return o; - - for (const f of formFields) { - if (f.type === 'bool') o[f.key] = false; - else if (f.type === 'number') { - // 外键用 Select 时,0 易被当成「已选」且无法匹配 Option,校验与展示都不直观 - o[f.key] = /_id$/.test(f.key) ? undefined : 0; - } else if (f.type === 'select') { - if (f.key === 'lock_type') o[f.key] = 0; - else if (f.data_type === 'number' && /_id$/.test(f.key)) o[f.key] = undefined; - else o[f.key] = ''; - } else o[f.key] = ''; - } - return o; - }, - - // 通用的日期相关方法 - isDateLikeColumnKey(key) { - if (!key) return false; - return ( - key === 'stat_date' || - key === 'create_time' || - key === 'last_modify_time' || - /_time$/.test(key) || - /_date$/.test(key) - ); - }, - - /** - * 列表「创建时间 / 最后修改时间」与后端 Sequelize 字段对齐: - - */ - pickRowTimeCell(row, key) { - if (!row || !key) return undefined; - if (key === 'create_time') { - return row.create_time; - } - if (key === 'last_modify_time') { - return row.last_modify_time; - } - return row[key]; - }, - - formatCellDate(v, key) { - if (v === null || v === undefined || v === '') return '-'; - const d = v instanceof Date ? v : new Date(v); - if (Number.isNaN(d.getTime())) return String(v); - const pad = (n) => String(n).padStart(2, '0'); - const y = d.getFullYear(); - const m = pad(d.getMonth() + 1); - const day = pad(d.getDate()); - const hh = pad(d.getHours()); - const mm = pad(d.getMinutes()); - const dateOnly = key === 'stat_date' || (key && /_date$/.test(key) && !/_time$/.test(key)); - if (dateOnly) return y + '-' + m + '-' + day; - return y + '-' + m + '-' + day + ' ' + hh + ':' + mm; - }, - - /** 外键行展示文案;实现见 @/utils/foreignRowLabel.js */ - getRowLabel(row) { - return getForeignRowLabel(row); - }, - - // 通用的数据规范化方法 - generatePayload(raw, formFields) { - const o = { ...raw }; - if (!formFields) return o; - - const typeByKey = {}; - const selectDataTypeByKey = {}; - for (const f of formFields) { - if (!f || !f.key) continue; - typeByKey[f.key] = f.type; - if (f.data_type) selectDataTypeByKey[f.key] = f.data_type; - } - - for (const k of Object.keys(o)) { - const t = typeByKey[k]; - let v = o[k]; - - if (t === 'number') { - if (v === '' || v === null || v === undefined) { - o[k] = null; - } else { - const n = Number(v); - o[k] = Number.isNaN(n) ? v : n; - } - } else if (t === 'bool') { - o[k] = v === true || v === 1 || v === '1'; - } else if (t === 'select') { - const numSelect = k === 'lock_type' || selectDataTypeByKey[k] === 'number'; - if (numSelect) { - if (v === '' || v === null || v === undefined) { - o[k] = null; - } else { - const n = Number(v); - o[k] = Number.isNaN(n) ? v : n; - } - } - } else if (t === 'date' || t === 'datetime') { - if (v instanceof Date) { - const pad = (n) => String(n).padStart(2, '0'); - if (t === 'date') { - o[k] = v.getFullYear() + '-' + pad(v.getMonth() + 1) + '-' + pad(v.getDate()); - } else { - o[k] = v.getFullYear() + '-' + pad(v.getMonth() + 1) + '-' + pad(v.getDate()) + - ' ' + pad(v.getHours()) + ':' + pad(v.getMinutes()) + ':' + pad(v.getSeconds()); - } - } - } - } - return o; - }, - - // 通用的表单验证规则生成方法(与 iView Form / async-validator 约定一致:输入类 blur,选择类 change) - generateEditRules(formFields) { - if (!formFields) { - return {}; - } - - const rules = {}; - for (const f of formFields) { - if (f.form === false) continue; - if (!f.required) continue; - if (f.type === 'bool') continue; - const fieldTitle = f.label || f.title || f.key; - - if (f.type === 'date' || f.type === 'datetime') { - rules[f.key] = [ - { required: true, type: 'date', message: '请选择' + fieldTitle, trigger: 'change' } - ]; - continue; - } - - // 数字 id 用 Select 渲染(如 tenant_id:type=select + data_type=number),Option 常为字符串; - // 仅用 async-validator required 易与 iView 绑定不一致导致「已选仍报错」 - if (f.type === 'select' && f.data_type === 'number' && /_id$/.test(f.key)) { - rules[f.key] = [ - { - required: true, - trigger: 'change', - validator(rule, value, callback) { - const n = value === '' || value === null || value === undefined ? NaN : Number(value); - if (Number.isNaN(n) || n === 0) { - callback(new Error('请选择' + fieldTitle)); - } else { - callback(); - } - } - } - ]; - continue; - } - - if (f.type === 'select') { - rules[f.key] = [ - { required: true, message: '请选择' + fieldTitle, trigger: 'change' } - ]; - continue; - } - - // 外键:generateEditColumns 里用 Select,值为数字 id,但 Option 上可能是字符串;不能用 type:'number' 直接校验 - if (f.type === 'number' && /_id$/.test(f.key)) { - rules[f.key] = [ - { - required: true, - trigger: 'change', - validator(rule, value, callback) { - const n = value === '' || value === null || value === undefined ? NaN : Number(value); - if (Number.isNaN(n) || n === 0) { - callback(new Error('请选择' + fieldTitle)); - } else { - callback(); - } - } - } - ]; - continue; - } - - if (f.type === 'number') { - rules[f.key] = [ - { - required: true, - type: 'number', - message: '请填写' + fieldTitle, - trigger: 'blur' - } - ]; - continue; - } - - rules[f.key] = [{ required: true, message: '请填写' + fieldTitle, trigger: 'blur' }]; - } - return rules; - } - } -}; diff --git a/admin/src/views/demo/tpl_demo.vue b/admin/src/views/demo/tpl_demo.vue index 9ed814d..2824a9c 100644 --- a/admin/src/views/demo/tpl_demo.vue +++ b/admin/src/views/demo/tpl_demo.vue @@ -32,11 +32,10 @@