Compare commits

...

6 Commits

Author SHA1 Message Date
张成
6186255378 1 2026-04-29 14:45:05 +08:00
张成
e487b3d8b3 1 2026-04-29 14:39:06 +08:00
张成
b6237011ef 1 2026-04-29 14:37:27 +08:00
张成
cfb92ce33d 12 2026-04-29 14:31:53 +08:00
张成
1850ec93a2 1 2026-04-29 14:14:19 +08:00
张成
418dfe1583 1 2026-04-29 13:57:10 +08:00
12 changed files with 329 additions and 636 deletions

View File

@@ -1,3 +1,44 @@
# 快速开始
**环境**Node.js建议 LTS、MySQL 5.7+ / 8.x`utf8mb4`)。
### 1. 安装依赖
- 项目根目录:`npm install`
- 管理端:`cd admin && npm install`(根目录 `npm run dev` 会进入 `admin`,但首次建议两处各装一次依赖)
### 2. 数据库与 `sql/init.sql`
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`**、**`zc` / `zc123`**MD5 与框架一致)、`sys_title` / `sys_logo`、菜单等。**库名须与下一步 `config.development.js` 里的 `db.database` 一致。**
4. 需要「演示数据」列表时,再执行 **`sql/tpl_demo.sql`**。
### 3. 配置 `config/config.development.js`
- 填写 **`db`**`username``password``database``host``port``dialect: "mysql"`。**框架要求 `password` 非空**(不能留空字符串)。
- 可选:用环境变量覆盖数据库——`DB_HOST``DB_PORT``DB_USER``DB_PASSWORD``DB_NAME`(与文件内默认值二选一,以你本机为准)。
- **阿里云 OSS**:本地可保留非空占位 `accessKeyId` / `accessKeySecret` / `bucket`,避免 `sys_file` 依赖加载失败;真实 OSS 再改为控制台密钥,或通过 `ALIYUN_ACCESS_KEY_ID``ALIYUN_ACCESS_KEY_SECRET``ALIYUN_OSS_URL``ALIYUN_OSS_BUCKET` 注入。
### 4. 启动后端
- 根目录:`npm run api`nodemon`npm start``node app.js`)。
- 默认 **API 端口**见 `config/config.js` 的 `port.node`(模板为 **9098**)。
- 文档:**`http://localhost:<port>/api/docs`**
### 5. 启动管理端
- 根目录:`npm run dev`(默认 **9000** 端口,见根目录 `package.json` 脚本)。
- 接口根地址在 **`admin/config/index.js`** 的 `apiUrl`,须与后端端口一致(模板为 `http://localhost:9098/admin_api/`)。
### 6. 登录与排错
- 默认:**`admin` / `123456`** ;租户可不传或按 `sys_tenant.code`(如 `default`)。
- **`sys_file` 加载失败**:检查 `aliyun` 字段是否非空或已配置环境变量。
- **连库失败**:核对 `db`、防火墙、是否已执行 `init.sql`
---
# 前后端项目模板 # 前后端项目模板
基于 **admin-framework**(管理端 Vue2 + ViewUI**node-core-framework**Koa2 + Sequelize的最小可运行骨架已去掉原 WMS 业务,仅保留一条演示业务链与系统接口。 基于 **admin-framework**(管理端 Vue2 + ViewUI**node-core-framework**Koa2 + Sequelize的最小可运行骨架已去掉原 WMS 业务,仅保留一条演示业务链与系统接口。
@@ -10,14 +51,6 @@
- `api/model/`:业务模型(模板保留 `tpl_demo``sys_tenant` - `api/model/`:业务模型(模板保留 `tpl_demo``sys_tenant`
- `config/``config.js` 白名单、`framework.config.js` 框架项、环境库表连接 - `config/``config.js` 白名单、`framework.config.js` 框架项、环境库表连接
## 本地运行
1. 安装依赖:项目根目录 `npm install``admin` 目录 `npm install`
2. 在 MySQL 中创建库并 `USE` 后,执行 **`sql/init.sql`**(或 `migrations/init.sql`,内容一致):创建系统表并写入默认租户、**超级管理员角色**、用户 **`admin` / `123456`**、**站点标题 `sys_title` 与 Logo `sys_logo`**、菜单树、控件字典等;再按需执行 **`sql/tpl_demo.sql`** 创建演示表
3. 修改 `config/config.development.js` 中的数据库账号等
4. 根目录 `npm run api` 启动后端(默认端口见 `config/config.js`
5. `npm run dev` 启动管理端,浏览器访问 webpack 提示的端口
## 菜单与演示页 ## 菜单与演示页
`sys_menu` 中增加一条菜单:`component`**`demo/tpl_demo`**(与 `admin/src/router/component-map.js` 一致),分配权限后即可打开「演示数据」列表页。 `sys_menu` 中增加一条菜单:`component`**`demo/tpl_demo`**(与 `admin/src/router/component-map.js` 一致),分配权限后即可打开「演示数据」列表页。

View File

@@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>沁羿物流 · 仓库管理系统</title> <title>admin管理系统</title>
</head> </head>
<body> <body>

File diff suppressed because one or more lines are too long

View File

@@ -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_idtype=select + data_type=numberOption 常为字符串;
// 仅用 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;
}
}
};

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,11 +32,12 @@
<script> <script>
import tplDemoServer from "@/api/demo/tplDemoServer.js"; import tplDemoServer from "@/api/demo/tplDemoServer.js";
import tableMixin from "@/mixins/tableMixin.js"; import tplTableMixin from "@/mixins/tplTableMixin.js";
export default { export default {
name: "TplDemoPage", name: "TplDemoPage",
mixins: [tableMixin], mixins: [tplTableMixin],
data() { data() {
return { return {
select_sources: {}, select_sources: {},
@@ -132,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) {
@@ -158,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
}; };

View File

@@ -1,21 +1,25 @@
/** /**
* 本地开发配置(请按实际环境修改数据库等) * 本地开发配置
*
* 第一步:按本机 MySQL 修改下方 db尤其 password库名需与 sql/init.sql 执行前 USE 的库一致。
* 可用环境变量覆盖DB_HOST、DB_PORT、DB_USER、DB_PASSWORD、DB_NAME未设置则用下方默认值
*/ */
module.exports = { module.exports = {
db: { db: {
username: "root", username: process.env.DB_USER || "framework_project_web",
password: "your_password", password: process.env.DB_PASSWORD || "6EiMpcJW63db5DTY",
database: "fullstack_template", database: process.env.DB_NAME || "framework_project_web",
host: "127.0.0.1", host: process.env.DB_HOST || "101.132.75.138",
port: 3306, port: Number(process.env.DB_PORT) || 3306,
dialect: "mysql", dialect: "mysql",
}, },
// 非空占位即可通过 oss 模块加载;真用 OSS 上传请改为控制台密钥与 bucket
aliyun: { aliyun: {
accessKeyId: "", accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || "local-dev-placeholder",
accessKeySecret: "", accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || "local-dev-placeholder",
ossUrl: "", ossUrl: process.env.ALIYUN_OSS_URL || "",
bucket: "", bucket: process.env.ALIYUN_OSS_BUCKET || "local-dev-placeholder",
}, },
redis: null, redis: null,
}; };

File diff suppressed because one or more lines are too long

View File

@@ -1,127 +0,0 @@
-- 与 sql/init.sql 同步(内容一致)
-- =============================================================================
-- 前后端项目模板 · 初始化库表与基础数据MySQL 5.7+ / 8.xutf8mb4
-- 执行前请创建空库并 USE 到目标 database会 DROP 后重建系统表,请勿在生产库直接全量执行。
-- 默认管理员admin / 123456密码为 MD5 小写十六进制,与 node_core tokenService.getMd5 一致)
-- =============================================================================
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `sys_log`;
DROP TABLE IF EXISTS `sys_user`;
DROP TABLE IF EXISTS `sys_menu`;
DROP TABLE IF EXISTS `sys_role`;
DROP TABLE IF EXISTS `sys_parameter`;
DROP TABLE IF EXISTS `sys_tenant`;
CREATE TABLE `sys_tenant` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '租户名称',
`code` varchar(64) NOT NULL DEFAULT '' COMMENT '租户编码(登录 tenant_code',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`status` int NOT NULL DEFAULT 1 COMMENT '1启用 0停用',
`is_platform` int NOT NULL DEFAULT 0 COMMENT '1平台租户',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_delete` int NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_sys_tenant_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户';
CREATE TABLE `sys_role` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '角色名称',
`type` int NOT NULL DEFAULT 0 COMMENT '0普通 1系统',
`menus` json DEFAULT NULL COMMENT '权限菜单id数组等',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_delete` int NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色';
CREATE TABLE `sys_user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '登录名',
`password` varchar(100) NOT NULL DEFAULT '' COMMENT 'MD5密码',
`roleId` int NOT NULL COMMENT '角色id',
`tenant_id` int NOT NULL DEFAULT 1 COMMENT '租户id',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_delete` int NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_sys_user_tenant` (`tenant_id`),
KEY `idx_sys_user_role` (`roleId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户';
CREATE TABLE `sys_menu` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '菜单名称',
`parent_id` int unsigned DEFAULT 0 COMMENT '父id',
`icon` varchar(100) NOT NULL DEFAULT '' COMMENT '图标',
`path` varchar(255) NOT NULL DEFAULT '' COMMENT '路径',
`type` varchar(255) NOT NULL DEFAULT '页面' COMMENT '菜单/页面/外链/功能',
`model_id` int unsigned DEFAULT 0 COMMENT '模型id',
`form_id` int unsigned DEFAULT 0 COMMENT '表单id',
`component` varchar(100) NOT NULL DEFAULT '' COMMENT '组件地址',
`api_path` varchar(100) NOT NULL DEFAULT '' COMMENT 'api地址',
`is_show_menu` int NOT NULL DEFAULT 1 COMMENT '是否显示在菜单',
`is_show` int NOT NULL DEFAULT 1 COMMENT '是否展示',
`sort` int NOT NULL DEFAULT 0 COMMENT '排序',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_delete` int NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_sys_menu_parent` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜单';
CREATE TABLE `sys_parameter` (
`id` int NOT NULL AUTO_INCREMENT,
`key` varchar(100) NOT NULL DEFAULT '' COMMENT '参数key',
`value` varchar(500) NOT NULL DEFAULT '' COMMENT '',
`remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注',
`is_modified` int NOT NULL DEFAULT 0 COMMENT '0允许修改 1不允许',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_delete` int NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_sys_parameter_key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统参数';
CREATE TABLE `sys_log` (
`id` int NOT NULL AUTO_INCREMENT,
`table_name` varchar(100) NOT NULL DEFAULT '' COMMENT '表名',
`operate` varchar(100) NOT NULL DEFAULT '' COMMENT '操作',
`content` json DEFAULT NULL COMMENT '内容',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_delete` int NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志';
SET FOREIGN_KEY_CHECKS = 1;
INSERT INTO `sys_tenant` (`id`, `name`, `code`, `remark`, `status`, `is_platform`, `is_delete`)
VALUES (1, '默认平台租户', 'default', '初始化数据', 1, 1, 0);
INSERT INTO `sys_role` (`id`, `name`, `type`, `menus`, `is_delete`)
VALUES (1, '超级管理员', 1, CAST('[]' AS JSON), 0);
INSERT INTO `sys_user` (`id`, `name`, `password`, `roleId`, `tenant_id`, `is_delete`)
VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, 1, 0);
INSERT INTO `sys_parameter` (`key`, `value`, `remark`, `is_modified`, `is_delete`) VALUES
('sys_title', '管理系统', '登录页与顶栏标题', 0, 0),
('sys_logo', '', '顶栏 Logo 图片地址,可填上传后的相对路径', 0, 0);
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `icon`, `path`, `type`, `model_id`, `form_id`, `component`, `api_path`, `is_show_menu`, `is_show`, `sort`, `is_delete`) VALUES
(1, '首页', 0, 'md-home', '/home', '页面', 0, 0, 'home/index', '', 1, 1, 1, 0),
(5, '系统管理', 0, 'md-settings', '/system', '菜单', 0, 0, '', '', 1, 1, 2, 0),
(11, '用户管理', 5, 'md-person', '/system/user', '页面', 0, 0, 'system/sys_user', '', 1, 1, 1, 0),
(12, '角色管理', 5, 'md-people', '/system/role', '页面', 0, 0, 'system/sys_role', '', 1, 1, 2, 0),
(13, '系统日志', 5, 'md-list', '/system/log', '页面', 0, 0, 'system/sys_log', '', 1, 1, 3, 0),
(15, '参数设置', 5, 'md-options', '/system/param', '页面', 0, 0, 'system/sys_param_setup', '', 1, 1, 4, 0),
(120, '高级管理', 0, 'md-construct', '/system', '菜单', 0, 0, '', '', 1, 1, 3, 0),
(122, '菜单管理', 120, 'md-menu', '/system/menu', '页面', 0, 0, 'system/sys_menu', '', 1, 1, 1, 0),
(124, '系统标题', 120, 'md-text', '/system/title', '页面', 0, 0, 'system/sys_title', '', 1, 1, 2, 0),
(125, '租户管理', 120, 'md-git-branch', '/system/tenant', '页面', 0, 0, 'system/sys_tenant', '', 1, 1, 3, 0);

View File

@@ -1,7 +1,7 @@
-- ============================================================================= -- =============================================================================
-- 前后端项目模板 · 初始化库表与基础数据MySQL 5.7+ / 8.xutf8mb4 -- 前后端项目模板 · 初始化库表与基础数据MySQL 5.7+ / 8.xutf8mb4
-- 执行前请创建空库并 USE 到目标 database会 DROP 后重建系统表,请勿在生产库直接全量执行。 -- 执行前请创建空库并 USE 到目标 database(库名须与 config/config.development.js 里 db.database 一致);会 DROP 后重建系统表,请勿在生产库直接全量执行。
-- 默认管理员admin / 123456密码为 MD5 小写十六进制,与 node_core tokenService.getMd5 一致) -- 默认账号admin / 123456zc / zc123(密码为 MD5 小写十六进制,与 node_core tokenService.getMd5 一致)
-- ============================================================================= -- =============================================================================
SET NAMES utf8mb4; SET NAMES utf8mb4;
@@ -122,6 +122,10 @@ VALUES (1, '超级管理员', 1, CAST('[]' AS JSON), 0);
INSERT INTO `sys_user` (`id`, `name`, `password`, `roleId`, `tenant_id`, `is_delete`) INSERT INTO `sys_user` (`id`, `name`, `password`, `roleId`, `tenant_id`, `is_delete`)
VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, 1, 0); VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, 1, 0);
-- 用户 zc / zc123 → MD5同为超级管理员角色
INSERT INTO `sys_user` (`id`, `name`, `password`, `roleId`, `tenant_id`, `is_delete`)
VALUES (2, 'zc', 'd3df61764ee9a26091f714b88958caef', 1, 1, 0);
-- 站点标题与 Logo与 admin_core paramSetupServer / sys_title 页一致) -- 站点标题与 Logo与 admin_core paramSetupServer / sys_title 页一致)
INSERT INTO `sys_parameter` (`key`, `value`, `remark`, `is_modified`, `is_delete`) VALUES INSERT INTO `sys_parameter` (`key`, `value`, `remark`, `is_modified`, `is_delete`) VALUES
('sys_title', '管理系统', '登录页与顶栏标题', 0, 0), ('sys_title', '管理系统', '登录页与顶栏标题', 0, 0),
@@ -130,15 +134,25 @@ INSERT INTO `sys_parameter` (`key`, `value`, `remark`, `is_modified`, `is_delete
-- ----------------------------------------------------------------------------- 菜单(与 admin_core defaultMenus 对齐,便于权限与动态路由) -- ----------------------------------------------------------------------------- 菜单(与 admin_core defaultMenus 对齐,便于权限与动态路由)
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `icon`, `path`, `type`, `model_id`, `form_id`, `component`, `api_path`, `is_show_menu`, `is_show`, `sort`, `is_delete`) VALUES INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `icon`, `path`, `type`, `model_id`, `form_id`, `component`, `api_path`, `is_show_menu`, `is_show`, `sort`, `is_delete`) VALUES
(1, '首页', 0, 'md-home', '/home', '页面', 0, 0, 'home/index', '', 1, 1, 1, 0), (1, '首页', 0, 'md-home', '/home', '页面', 0, 0, 'home/index', '', 1, 1, 1, 0),
(200, '业务演示', 0, 'md-bulb', '/demo', '菜单', 0, 0, '', '', 1, 1, 4, 0),
(201, '演示数据', 200, 'md-grid', '/demo/tpl-demo', '页面', 0, 0, 'demo/tpl_demo', '', 1, 1, 1, 0),
(5, '系统管理', 0, 'md-settings', '/system', '菜单', 0, 0, '', '', 1, 1, 2, 0), (5, '系统管理', 0, 'md-settings', '/system', '菜单', 0, 0, '', '', 1, 1, 2, 0),
(11, '用户管理', 5, 'md-person', '/system/user', '页面', 0, 0, 'system/sys_user', '', 1, 1, 1, 0), (11, '用户管理', 5, 'md-person', '/system/user', '页面', 0, 0, 'system/sys_user', '', 1, 1, 1, 0),
(12, '角色管理', 5, 'md-people', '/system/role', '页面', 0, 0, 'system/sys_role', '', 1, 1, 2, 0), (12, '角色管理', 5, 'md-people', '/system/role', '页面', 0, 0, 'system/sys_role', '', 1, 1, 2, 0),
(13, '系统日志', 5, 'md-list', '/system/log', '页面', 0, 0, 'system/sys_log', '', 1, 1, 3, 0), (13, '系统日志', 5, 'md-list', '/system/log', '页面', 0, 0, 'system/sys_log', '', 1, 1, 3, 0),
(15, '参数设置', 5, 'md-options', '/system/param', '页面', 0, 0, 'system/sys_param_setup', '', 1, 1, 4, 0), (14, '操作日志', 5, 'md-pulse', '/system/log-operate', '页面', 0, 0, 'system/sys_log_operate', '', 1, 1, 4, 0),
(15, '参数设置', 5, 'md-options', '/system/param', '页面', 0, 0, 'system/sys_param_setup', '', 1, 1, 5, 0),
(120, '高级管理', 0, 'md-construct', '/system', '菜单', 0, 0, '', '', 1, 1, 3, 0), (120, '高级管理', 0, 'md-construct', '/system', '菜单', 0, 0, '', '', 1, 1, 3, 0),
(122, '菜单管理', 120, 'md-menu', '/system/menu', '页面', 0, 0, 'system/sys_menu', '', 1, 1, 1, 0), (122, '菜单管理', 120, 'md-menu', '/system/menu', '页面', 0, 0, 'system/sys_menu', '', 1, 1, 1, 0),
(124, '系统标题', 120, 'md-text', '/system/title', '页面', 0, 0, 'system/sys_title', '', 1, 1, 2, 0), (124, '系统标题', 120, 'md-text', '/system/title', '页面', 0, 0, 'system/sys_title', '', 1, 1, 2, 0),
(125, '租户管理', 120, 'md-git-branch', '/system/tenant', '页面', 0, 0, 'system/sys_tenant', '', 1, 1, 3, 0); (125, '租户管理', 120, 'md-git-branch', '/system/tenant', '页面', 0, 0, 'system/sys_tenant', '', 1, 1, 3, 0),
(126, '同步框架', 120, 'md-cloud-download', '/system/framework-sync', '页面', 0, 0, 'system/sys_framework_sync', '', 1, 1, 4, 0);
-- ----------------------------------------------------------------------------- sys_log 示例数据表结构见上content 与 Sequelize JSON 包装一致,含 value 键)
INSERT INTO `sys_log` (`table_name`, `operate`, `content`, `is_delete`) VALUES
('sys_user', '登录', JSON_OBJECT('value', '<p>初始化示例:管理员 <span class="bold">admin</span> 登录系统</p>'), 0),
('sys_menu', '新增', JSON_OBJECT('value', '<p>初始化菜单与权限数据已写入 <span class="bold">sys_menu</span></p>'), 0),
('sys_parameter', '修改', JSON_OBJECT('value', '<p><span class="bold">sys_title</span> 默认值为「管理系统」</p>'), 0);
-- ============================================================================= -- =============================================================================
-- 说明: -- 说明: