feat: 新增IM即时通讯(浮窗)、工作流、打印模块及工作台增强

- IM: 新增浮窗聊天(ImFloatWindow)、管理页(monitor/config/service/message)、SSE推送
- 工作流: 新增待办/我的流程页面及后端服务
- 打印: 新增打印模板、出库单打印(PrintPage)、模板种子脚本
- 工作台: 增强快捷入口与工作台数据
- 修复: TagsView页签关闭、CrudPage通用表格增强
- 移除导航菜单中的即时通讯入口,改为右下角浮窗
This commit is contained in:
2026-08-16 00:19:24 +08:00
parent 7235265749
commit 1bec470647
49 changed files with 5791 additions and 51 deletions
+168
View File
@@ -0,0 +1,168 @@
{
"name": "三等分原料出库单",
"content": {
"version": "1.0",
"document": {
"type": "report",
"page": {
"width": 210,
"height": 99,
"unit": "mm",
"orientation": "portrait",
"margin": { "top": 8, "bottom": 8, "left": 8, "right": 8 }
},
"sections": [
{
"type": "body",
"components": [
{
"id": "txt-title",
"type": "text",
"left": 0, "top": 0, "width": 194, "height": 10,
"value": "原料出库单",
"style": { "fontSize": 15, "fontWeight": "bold", "textAlign": "center" }
},
{
"id": "line-top",
"type": "line",
"left": 0, "top": 12, "width": 194, "height": 0.5,
"stroke": "#333333"
},
{
"id": "lbl-billno",
"type": "text",
"left": 4, "top": 17, "width": 26, "height": 6,
"value": "单号:",
"style": { "fontSize": 10 }
},
{
"id": "val-billno",
"type": "text",
"left": 34, "top": 17, "width": 60, "height": 6,
"binding": "RawMaterial_OutStock.BillNo",
"style": { "fontSize": 10 }
},
{
"id": "lbl-org",
"type": "text",
"left": 100, "top": 17, "width": 42, "height": 6,
"value": "领料车间:",
"style": { "fontSize": 10 }
},
{
"id": "val-org",
"type": "text",
"left": 144, "top": 17, "width": 50, "height": 6,
"binding": "RawMaterial_OutStock.OutOrgName",
"style": { "fontSize": 10 }
},
{
"id": "lbl-zk",
"type": "text",
"left": 4, "top": 26, "width": 26, "height": 6,
"value": "庄口:",
"style": { "fontSize": 10 }
},
{
"id": "val-zk",
"type": "text",
"left": 34, "top": 26, "width": 60, "height": 6,
"binding": "RawMaterial_OutStock.ZhuangkouCode",
"style": { "fontSize": 10 }
},
{
"id": "lbl-weight",
"type": "text",
"left": 100, "top": 26, "width": 42, "height": 6,
"value": "出库重量:",
"style": { "fontSize": 10 }
},
{
"id": "val-weight",
"type": "text",
"left": 144, "top": 26, "width": 34, "height": 6,
"binding": "RawMaterial_OutStock.OutWeight",
"format": { "kind": "decimal", "digits": 2 },
"style": { "fontSize": 10 }
},
{
"id": "unit-weight",
"type": "text",
"left": 180, "top": 26, "width": 14, "height": 6,
"value": "kg",
"style": { "fontSize": 10 }
},
{
"id": "lbl-date",
"type": "text",
"left": 4, "top": 35, "width": 26, "height": 6,
"value": "出库日期:",
"style": { "fontSize": 10 }
},
{
"id": "val-date",
"type": "text",
"left": 34, "top": 35, "width": 60, "height": 6,
"binding": "RawMaterial_OutStock.OutDate",
"format": { "kind": "date", "pattern": "YYYY-MM-DD" },
"style": { "fontSize": 10 }
},
{
"id": "lbl-type",
"type": "text",
"left": 100, "top": 35, "width": 42, "height": 6,
"value": "出库类型:",
"style": { "fontSize": 10 }
},
{
"id": "val-type",
"type": "text",
"left": 144, "top": 35, "width": 50, "height": 6,
"binding": "RawMaterial_OutStock.OutTypeText",
"style": { "fontSize": 10 }
},
{
"id": "line-mid",
"type": "line",
"left": 0, "top": 44, "width": 194, "height": 0.5,
"stroke": "#333333"
},
{
"id": "lbl-receiver",
"type": "text",
"left": 4, "top": 50, "width": 60, "height": 6,
"value": "领用人签字:",
"style": { "fontSize": 10 }
},
{
"id": "line-receiver",
"type": "line",
"left": 4, "top": 60, "width": 80, "height": 0.5,
"stroke": "#333333"
},
{
"id": "lbl-keeper",
"type": "text",
"left": 104, "top": 50, "width": 60, "height": 6,
"value": "仓管员签字:",
"style": { "fontSize": 10 }
},
{
"id": "line-keeper",
"type": "line",
"left": 104, "top": 60, "width": 80, "height": 0.5,
"stroke": "#333333"
},
{
"id": "lbl-note",
"type": "text",
"left": 4, "top": 70, "width": 186, "height": 6,
"value": "备注:",
"style": { "fontSize": 10 }
}
]
}
]
}
}
}
+96
View File
@@ -0,0 +1,96 @@
/**
* IM 管理菜单种子:协作(目录)下追加 4 个管理页面
* - 性能监测(/im/monitor
* - 服务监测(/im/service
* - 配置界面(/im/config
* - 消息与公告管理(/im/message
* ⚠️ 顶层必须用目录节点(MenuType=0),页面节点(MenuType=1)必须挂目录下。
* 用法:node server/scripts/seed-im-admin.mjs
*/
const BASE = process.env.F9MES_API || 'http://localhost:5136';
const PHONE = '13800000000';
const PASSWORD = '123456';
async function api(h, method, url, body) {
const res = await fetch(BASE + url, {
method,
headers: h,
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
let json = null;
try { json = JSON.parse(text); } catch { /* ignore */ }
if (!res.ok || (json && json.code !== 0)) {
throw new Error(`HTTP ${res.status} ${url}: ${text.slice(0, 300)}`);
}
return json;
}
const PAGES = [
{ name: '性能监测', routePath: '/im/monitor', component: 'Im/monitor', icon: 'Monitor', sort: 20 },
{ name: '服务监测', routePath: '/im/service', component: 'Im/service', icon: 'Connection', sort: 30 },
{ name: '配置界面', routePath: '/im/config', component: 'Im/config', icon: 'Setting', sort: 40 },
{ name: '消息与公告管理', routePath: '/im/message', component: 'Im/message', icon: 'Bell', sort: 50 },
];
async function main() {
const login = await api({ 'Content-Type': 'application/json' }, 'POST', '/api/auth/login', {
phone: PHONE,
password: PASSWORD,
});
const token = login.data.token;
const h = { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' };
const menus = await api(h, 'GET', '/api/data/BaseSys_Menu/all');
const items = menus.data?.data || [];
let dir = items.find((m) => m.name === '协作' && m.menuType === 0);
if (!dir) {
const r = await api(h, 'POST', '/api/data/BaseSys_Menu/add', {
name: '协作',
menuType: 0,
routePath: '',
component: '',
icon: 'ChatDotRound',
isVisible: 1,
isEnable: 1,
sort: 998,
parentId: 0,
level: 0,
path: '',
});
dir = { id: r.data && (r.data.id ?? r.data) };
console.log('菜单目录「协作」已创建 id=' + dir.id);
} else {
console.log('菜单目录「协作」已存在 id=' + dir.id);
}
const dirId = dir.id;
for (const p of PAGES) {
const exists = items.find((m) => m.parentId === dirId && m.routePath === p.routePath);
if (exists) {
console.log(`菜单「${p.name}」已存在 id=${exists.id}`);
continue;
}
const r = await api(h, 'POST', '/api/data/BaseSys_Menu/add', {
name: p.name,
menuType: 1,
routePath: p.routePath,
component: p.component,
icon: p.icon,
isVisible: 1,
isEnable: 1,
sort: p.sort,
parentId: dirId,
level: 1,
path: '/' + dirId,
});
console.log(`菜单「${p.name}」已创建 id=${r.data && (r.data.id ?? r.data)}`);
}
console.log('\n完成。协作菜单下已就绪 4 个 IM 管理页面。');
}
main().catch((e) => {
console.error('ERR: ' + e.message);
process.exit(1);
});
+64
View File
@@ -0,0 +1,64 @@
/**
* 轻量 IM 种子:
* 1) 发几条演示系统通知(工单变更/工艺单变更/审批通知)
* ⚠️ 说明:原「协作 → 即时通讯」菜单已移除(聊天入口改为右下角浮窗,见 layout 的 ImFloatWindow),
* 本脚本不再创建该菜单,仅保留演示通知功能。
* 用法:node server/scripts/seed-im.mjs
*/
const BASE = process.env.F9MES_API || 'http://localhost:5136';
const PHONE = '13800000000';
const PASSWORD = '123456';
const ME_ID = 1;
async function api(h, method, url, body) {
const res = await fetch(BASE + url, {
method,
headers: h,
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
let json = null;
try { json = JSON.parse(text); } catch { /* ignore */ }
if (!res.ok || (json && json.code !== 0)) {
throw new Error(`HTTP ${res.status} ${url}: ${text.slice(0, 300)}`);
}
return json;
}
async function main() {
const login = await api({ 'Content-Type': 'application/json' }, 'POST', '/api/auth/login', {
phone: PHONE,
password: PASSWORD,
});
const token = login.data.token;
const h = { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' };
// ============ 1. 演示系统通知 ============
const demos = [
{ title: '工单变更', content: '任务工单 GD-20260815-003 的排产时间由 08:30 调整为 09:30,请注意查看。', msgType: 1 },
{ title: '工艺单变更', content: '工艺庄口「四川凉山-2026春茧」工艺参数已更新:缫丝车速 95r/min → 100r/min。', msgType: 1 },
{ title: '审批通知', content: '您有 1 条原料出库审批待办(CK-20260815-002),请及时处理。', msgType: 2 },
];
for (const d of demos) {
const r = await api(h, 'POST', '/api/im/notify', {
userIds: [ME_ID],
msgType: d.msgType,
title: d.title,
content: d.content,
});
console.log('通知「' + d.title + '」已发送(' + r.data + ' 条)');
}
// ============ 2. 验证 ============
const unread = await api(h, 'GET', '/api/im/unread-count');
console.log('当前未读数:' + unread.data);
const sessions = await api(h, 'GET', '/api/im/sessions');
console.log('会话数:' + (sessions.data || []).length);
console.log('\n完成。演示通知已就绪。');
}
main().catch((e) => {
console.error('ERR: ' + e.message);
process.exit(1);
});
+37
View File
@@ -0,0 +1,37 @@
/**
* 一次性演示:群发一条「公告」类型系统通知(msgType=0)
* 用法:node server/scripts/seed-notice.mjs
*/
const BASE = process.env.F9MES_API || 'http://localhost:5136';
const PHONE = '13800000000';
const PASSWORD = '123456';
async function main() {
const login = await fetch(BASE + '/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: PHONE, password: PASSWORD }),
});
const lj = await login.json();
if (login.status !== 200 || lj.code !== 0) throw new Error('登录失败: ' + JSON.stringify(lj).slice(0, 300));
const token = lj.data.token;
const h = { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' };
const r = await fetch(BASE + '/api/im/notify', {
method: 'POST',
headers: h,
body: JSON.stringify({
msgType: 0,
title: '关于8月16日全厂停电检修的通知',
content: '各位同事:因供电局线路检修,8月16日(周日)08:00-18:00 全厂停电,请各车间提前做好设备断电与原料防护工作。综合管理部',
}),
});
const j = await r.json();
if (r.status !== 200 || j.code !== 0) throw new Error('发送失败: ' + JSON.stringify(j).slice(0, 300));
console.log('公告已群发:' + j.data + ' 条');
}
main().catch((e) => {
console.error('ERR: ' + e.message);
process.exit(1);
});
+104
View File
@@ -0,0 +1,104 @@
/**
* 创建缫丝厂组织树 + 为全部用户绑定所属组织
* 用法:node server/scripts/seed-org.mjs
* 依赖:管理员 13800000000/123456 可用
*/
const BASE = process.env.F9MES_API || 'http://localhost:5136';
// 组织树:name -> { type, children }
const ORGS = [
{ name: '缫丝总厂', type: 0, children: [
{ name: '选茧车间', type: 2 },
{ name: '煮茧车间', type: 2 },
{ name: '缫丝一车间', type: 2 },
{ name: '缫丝二车间', type: 2 },
{ name: '复摇车间', type: 2 },
{ name: '质检部', type: 1 },
{ name: '设备动力部', type: 1 },
{ name: '仓储部', type: 1 },
{ name: '综合管理部', type: 1 },
]},
];
const USER_ORG_PLAN = {
1: '综合管理部', // 管理员
2: '缫丝一车间', // 李工(车间主管)
// 3~22 随机用户:按 id 循环分配
};
async function api(h, method, url, body) {
const res = await fetch(BASE + url, {
method, headers: h, body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
let json = null;
try { json = JSON.parse(text); } catch { /* ignore */ }
return { ok: res.ok, json, text };
}
async function main() {
const login = await api({ 'Content-Type': 'application/json' }, 'POST', '/api/auth/login',
{ phone: '13800000000', password: '123456' });
if (!login.ok || login.json?.code !== 0) throw new Error('管理员登录失败: ' + login.text.slice(0, 200));
const token = login.json.data.token;
const h = { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' };
// ---- 1. 建组织树(若已存在同名则跳过)----
const orgs = await api(h, 'GET', '/api/data/BaseSys_Org/all');
const existing = (Array.isArray(orgs.json?.data) ? orgs.json.data : orgs.json?.data?.data ?? []);
const byName = new Map(existing.map((o) => [o.name, o]));
const nameToId = {}; // 组织名 -> id(含父子路径的 key 冗余,便于解析)
const addOrg = async (o, parentId, level, sort) => {
if (byName.has(o.name)) {
nameToId[o.name] = byName.get(o.name).id;
nameToId[`${parentId}/${o.name}`] = byName.get(o.name).id;
return;
}
const r = await api(h, 'POST', '/api/data/BaseSys_Org/add', {
parentId, name: o.name, orgType: o.type, sort, level,
path: parentId === 0 ? '' : `/${parentId}`,
});
if (!r.ok || r.json?.code !== 0) throw new Error(`建组织失败 ${o.name}: ${r.text.slice(0, 200)}`);
nameToId[o.name] = r.json.data.id;
nameToId[`${parentId}/${o.name}`] = r.json.data.id;
byName.set(o.name, r.json.data);
console.log(` 组织: ${' '.repeat(level)}${o.name} (id=${r.json.data.id})`);
};
console.log('=== 创建组织树 ===');
for (const root of ORGS) {
await addOrg(root, 0, 0, 1);
for (let i = 0; i < root.children.length; i++) {
await addOrg(root.children[i], nameToId[root.name], 1, i + 1);
}
}
// ---- 2. 绑定用户组织(先清空旧绑定,保证幂等)----
console.log('=== 绑定用户组织 ===');
const users = await api(h, 'GET', '/api/data/BaseSys_User/all');
const userList = (Array.isArray(users.json?.data) ? users.json.data : users.json?.data?.data ?? []);
const oldBinds = await api(h, 'GET', '/api/data/BaseSys_UserOrg/all');
const oldList = (Array.isArray(oldBinds.json?.data) ? oldBinds.json.data : oldBinds.json?.data?.data ?? []);
const delIds = oldList.map((b) => b.id);
if (delIds.length) await api(h, 'POST', '/api/data/BaseSys_UserOrg/deleteRange', delIds);
const orgIdOf = (name) => nameToId[name];
// 有效部门列表(车间+部门,不含总厂)
const leafNames = ORGS[0].children.map((c) => c.name);
let bindCount = 0;
for (const u of userList) {
let orgName = USER_ORG_PLAN[u.id];
if (!orgName) orgName = leafNames[(u.id - 3 + leafNames.length) % leafNames.length];
const orgId = orgIdOf(orgName);
if (!orgId) { console.log(` 跳过 ${u.name}(无组织 ${orgName}`); continue; }
const r = await api(h, 'POST', '/api/data/BaseSys_UserOrg/add', { userId: u.id, orgId });
if (r.ok && r.json?.code === 0) { bindCount++; console.log(` ${u.name} (id=${u.id}) -> ${orgName}`); }
else console.log(` 失败 ${u.name}: ${r.text.slice(0, 100)}`);
}
console.log(`\n完成:绑定 ${bindCount} 个用户`);
}
main().catch((e) => { console.error('ERR: ' + e.message); process.exit(1); });
+46
View File
@@ -0,0 +1,46 @@
// Seed script: create the "1/3 A4 out-stock form" print template.
// Run: node seed-print-template.mjs
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const BASE = process.env.F9_API_BASE || 'http://localhost:5136'
const PHONE = '13800000000'
const PASSWORD = '123456'
const tpl = JSON.parse(
fs.readFileSync(path.join(__dirname, 'print-template-outstock.json'), 'utf8')
)
async function main() {
const loginRes = await fetch(`${BASE}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: PHONE, password: PASSWORD })
})
const login = await loginRes.json()
if (login.code !== 0) throw new Error(`Login failed: ${login.message}`)
const token = login.data.token
const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
const list = await fetch(`${BASE}/api/print/templates`, { headers }).then((r) => r.json())
const dup = (list.items || []).find((i) => i.name === tpl.name)
if (dup) {
console.log(`Template already exists, skip: ${tpl.name} (id=${dup.id})`)
return
}
const res = await fetch(`${BASE}/api/print/templates`, {
method: 'POST',
headers,
body: JSON.stringify({ name: tpl.name, content: JSON.stringify(tpl.content) })
})
const row = await res.json()
console.log(`Template created: id=${row.id} name=${row.name}`)
}
main().catch((e) => {
console.error(e)
process.exit(1)
})
+78
View File
@@ -0,0 +1,78 @@
/**
* 任务1 联调收尾:造测试数据(庄口 + 原料出库单)并验证打印数据端点
* 用法:node server/scripts/seed-test-outstock.mjs
*/
const BASE = process.env.F9MES_API || 'http://localhost:5136';
const PHONE = '13800000000';
const PASSWORD = '123456';
async function main() {
const login = await fetch(BASE + '/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: PHONE, password: PASSWORD }),
}).then((r) => r.json());
if (login.code !== 0) throw new Error('登录失败: ' + JSON.stringify(login));
const h = { Authorization: 'Bearer ' + login.data.token, 'Content-Type': 'application/json' };
// 1. 庄口:有则复用,无则创建
let zkId = 0;
const zk = await fetch(BASE + '/api/data/RawMaterial_Zhuangkou/all', { headers: h }).then((r) => r.json());
const zkItems = (zk.data && zk.data.items) || (Array.isArray(zk.data) ? zk.data : []);
if (zkItems.length) {
zkId = zkItems[0].id;
console.log('复用庄口 id=' + zkId);
} else {
const code = 'YLZ' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + '-001';
const add = await fetch(BASE + '/api/data/RawMaterial_Zhuangkou/add', {
method: 'POST',
headers: h,
body: JSON.stringify({ code, name: '四川凉山-2026春茧', season: '春茧', cocoonType: 0, status: 1 }),
}).then((r) => r.json());
if (add.code !== 0) throw new Error('建庄口失败: ' + JSON.stringify(add));
zkId = add.data && (add.data.id ?? add.data);
if (typeof zkId !== 'number') {
const page = await fetch(BASE + '/api/data/RawMaterial_Zhuangkou/all', { headers: h }).then((r) => r.json());
const items = (page.data && page.data.items) || page.data || [];
zkId = items[items.length - 1].id;
}
console.log('新建庄口 id=' + zkId);
}
// 2. 原料出库单(若无则创建)
const all = await fetch(BASE + '/api/data/RawMaterial_OutStock/all', { headers: h }).then((r) => r.json());
const items = (all.data && all.data.items) || (Array.isArray(all.data) ? all.data : []);
let rowId;
if (items.length) {
rowId = items[0].id;
console.log('复用出库单 id=' + rowId);
} else {
const billNo = 'CK-' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + '-001';
const add = await fetch(BASE + '/api/data/RawMaterial_OutStock/add', {
method: 'POST',
headers: h,
body: JSON.stringify({
billNo,
zhuangkouId: zkId,
outOrgId: 0,
outWeight: 125.5,
receiver: '张三',
outDate: new Date().toISOString().slice(0, 10),
outType: 0,
status: 1,
}),
}).then((r) => r.json());
if (add.code !== 0) throw new Error('建出库单失败: ' + JSON.stringify(add));
rowId = add.data && (add.data.id ?? add.data);
console.log('新建出库单 id=' + rowId);
}
// 3. 验证打印数据端点
const pd = await fetch(BASE + '/api/print/data/RawMaterial_OutStock/' + rowId, { headers: h }).then((r) => r.json());
console.log('PRINTDATA=' + JSON.stringify(pd));
}
main().catch((e) => {
console.error('ERR: ' + e.message);
process.exit(1);
});
+78
View File
@@ -0,0 +1,78 @@
/**
* 随机新增 20 个测试用户(默认密码 123456)
* 用法:node server/scripts/seed-users.mjs
*/
const BASE = process.env.F9MES_API || 'http://localhost:5136';
const PHONE = '13800000000';
const PASSWORD = '123456';
const SURNAMES = '王李张刘陈杨黄赵周吴徐孙马朱胡郭何高林罗郑梁谢宋唐许韩冯邓曹彭曾肖田董袁潘于蒋蔡余杜叶程苏魏吕丁任沈姚卢姜崔钟谭陆汪范金石廖贾夏韦傅方白邹孟熊秦邱江尹薛闫段雷侯龙史陶黎贺顾毛郝龚邵万钱严覃武戴莫孔向汤'.split('');
const GIVEN = '伟芳娜敏静丽强磊军洋勇艳杰娟涛明超秀兰霞平刚桂英华建国志强建华建军建国云萍晓华小红美玲玉珍玉梅国强建平'.split('');
function rand(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
/** 随机 11 位手机号(138/139/150/151/152/158/159/186/188/189 开头) */
function randPhone() {
const head = rand(['138', '139', '150', '151', '152', '158', '159', '186', '188', '189']);
let tail = '';
for (let i = 0; i < 8; i++) tail += Math.floor(Math.random() * 10);
return head + tail;
}
async function api(h, method, url, body) {
const res = await fetch(BASE + url, {
method,
headers: h,
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
let json = null;
try { json = JSON.parse(text); } catch { /* ignore */ }
return { ok: res.ok, json, text };
}
async function main() {
const login = await api({ 'Content-Type': 'application/json' }, 'POST', '/api/auth/login', {
phone: PHONE,
password: PASSWORD,
});
if (!login.ok || login.json?.code !== 0) throw new Error('管理员登录失败: ' + login.text.slice(0, 200));
const token = login.json.data.token;
const h = { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' };
// 获取现有手机号,避免冲突
const exist = await api(h, 'GET', '/api/data/BaseSys_User/all');
const items = exist.json?.data?.items || (Array.isArray(exist.json?.data) ? exist.json.data : []);
const used = new Set(items.map((u) => u.phone || u.Phone).filter(Boolean));
let created = 0, skipped = 0;
const results = [];
while (created < 20) {
const phone = randPhone();
if (used.has(phone)) { skipped++; continue; }
used.add(phone);
const name = rand(SURNAMES) + rand(GIVEN);
const gender = Math.random() < 0.5 ? 1 : 2;
const r = await api(h, 'POST', '/api/basesys/user/create', {
phone, password: PASSWORD, name, gender, status: 1,
});
if (r.ok && r.json?.code === 0) {
const id = r.json.data?.id ?? r.json.data;
results.push({ id, name, phone });
created++;
} else {
// 该手机号可能已被占(并发/冲突),换号重试
skipped++;
}
}
console.log('新增用户 ' + created + ' 个(跳过/重试 ' + skipped + ' 次)');
console.log('默认密码:' + PASSWORD);
console.table(results.map((u) => ({ ID: u.id, 姓名: u.name, 手机号: u.phone })));
console.log('\n完成。');
}
main().catch((e) => {
console.error('ERR: ' + e.message);
process.exit(1);
});
+123
View File
@@ -0,0 +1,123 @@
/**
* 任务2 种子:简易工作流
* 1) 创建「原料出库审批」流程定义 + 节点(开始 → 车间主管审批 → 结束)
* 2) 创建侧边栏菜单:工作流(目录)→ 待办中心 / 我发起的
* 用法:node server/scripts/seed-workflow.mjs
*/
const BASE = process.env.F9MES_API || 'http://localhost:5136';
const PHONE = '13800000000';
const PASSWORD = '123456';
// 默认审批人:超级管理员(用户ID=1)
const APPROVER_ID = 1;
async function api(h, method, url, body) {
const res = await fetch(BASE + url, {
method,
headers: h,
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
let json = null;
try { json = JSON.parse(text); } catch { /* ignore */ }
if (!res.ok || (json && json.code !== 0)) {
throw new Error(`HTTP ${res.status} ${url}: ${text.slice(0, 300)}`);
}
return json;
}
async function main() {
const login = await api({ 'Content-Type': 'application/json' }, 'POST', '/api/auth/login', {
phone: PHONE,
password: PASSWORD,
});
const token = login.data.token;
const h = { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' };
// ============ 1. 流程定义 ============
const flows = (await api(h, 'GET', '/api/workflow/definitions?bizType=RawMaterial_OutStock')).data || [];
let flow = flows.find((f) => f.code === 'RM_OUT_APPROVE');
if (flow) {
console.log('流程已存在:id=' + flow.id + ' name=' + flow.name + '(跳过创建)');
} else {
// 直接写库:Common_Workflow + Common_WorkflowNode(走通用 data 接口)
const addFlow = await api(h, 'POST', '/api/data/Common_Workflow/add', {
name: '原料出库审批',
code: 'RM_OUT_APPROVE',
bizType: 'RawMaterial_OutStock',
status: 1,
});
const flowId = addFlow.data && (addFlow.data.id ?? addFlow.data);
console.log('流程已创建 id=' + flowId);
const nodes = [
{ workflowId: flowId, name: '发起', nodeType: 0, sort: 10 },
{ workflowId: flowId, name: '车间主管审批', nodeType: 1, approverJson: JSON.stringify([APPROVER_ID]), sort: 20 },
{ workflowId: flowId, name: '结束', nodeType: 4, sort: 99 },
];
for (const n of nodes) {
const r = await api(h, 'POST', '/api/data/Common_WorkflowNode/add', n);
console.log(' 节点「' + n.name + '」id=' + (r.data && (r.data.id ?? r.data)));
}
flow = { id: flowId };
}
// ============ 2. 菜单 ============
const menus = (await api(h, 'GET', '/api/data/BaseSys_Menu/all')).data || [];
const items = (menus.items || (Array.isArray(menus) ? menus : []));
let dir = items.find((m) => m.name === '工作流' && m.menuType === 0);
if (!dir) {
const r = await api(h, 'POST', '/api/data/BaseSys_Menu/add', {
name: '工作流',
menuType: 0,
routePath: '',
component: '',
icon: 'Checked',
isVisible: 1,
isEnable: 1,
sort: 999,
parentId: 0,
level: 0,
path: '',
});
dir = { id: r.data && (r.data.id ?? r.data) };
console.log('菜单目录「工作流」已创建 id=' + dir.id);
} else {
console.log('菜单目录「工作流」已存在 id=' + dir.id);
}
const dirId = dir.id;
const childDefs = [
{ name: '待办中心', routePath: '/workflow/todo', component: 'WorkFlow/todo', icon: 'Bell', sort: 10 },
{ name: '我发起的', routePath: '/workflow/my', component: 'WorkFlow/my', icon: 'Document', sort: 20 },
];
const refresh = (await api(h, 'GET', '/api/data/BaseSys_Menu/all')).data || {};
const allMenus = refresh.items || (Array.isArray(refresh) ? refresh : []);
for (const c of childDefs) {
const exists = allMenus.find((m) => m.parentId === dirId && m.routePath === c.routePath);
if (exists) {
console.log('菜单「' + c.name + '」已存在 id=' + exists.id);
continue;
}
const r = await api(h, 'POST', '/api/data/BaseSys_Menu/add', {
name: c.name,
menuType: 1,
routePath: c.routePath,
component: c.component,
icon: c.icon,
isVisible: 1,
isEnable: 1,
sort: c.sort,
parentId: dirId,
level: 1,
path: '/' + dirId,
});
console.log('菜单「' + c.name + '」已创建 id=' + (r.data && (r.data.id ?? r.data)));
}
console.log('\n完成。流程=' + (flow.id || '?') + '(原料出库审批 RM_OUT_APPROVE');
}
main().catch((e) => {
console.error('ERR: ' + e.message);
process.exit(1);
});
+128
View File
@@ -0,0 +1,128 @@
/**
* 一次性演示:为庄口 GYZ-2026-002 补充分工段生产数据(幂等,可重复执行)
* 选茧(领料5000→上车茧3200) → 煮茧(送茧2800) → 缫丝(下丝900) → 复摇(返丝830) → 秤大丝(820)
* 用法:node server/scripts/seed-zhuangkou-progress.mjs
*/
const BASE = process.env.F9MES_API || 'http://localhost:5136';
const PHONE = '13800000000';
const PASSWORD = '123456';
const PZ_ID = 1; // 工艺庄口 GYZ-2026-002
const RAW_ZK_ID = 2; // 原料庄口(选茧按原料庄口关联)
async function api(h, method, url, body) {
const res = await fetch(BASE + url, {
method,
headers: h,
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
let json = null;
try { json = JSON.parse(text); } catch { /* ignore */ }
if (!res.ok || (json && json.code !== 0)) {
throw new Error(`HTTP ${res.status} ${url}: ${text.slice(0, 300)}`);
}
return json;
}
async function clearRows(h, table, field, value) {
const r = await api(h, 'POST', `/api/data/${table}/page`, {
page: 1, size: 200, filters: [{ field, value }],
});
const items = (r.data && r.data.items) || [];
if (!items.length) return 0;
const ids = items.map((i) => i.id);
await api(h, 'POST', `/api/data/${table}/deleteRange`, ids);
return ids.length;
}
async function main() {
const login = await api({ 'Content-Type': 'application/json' }, 'POST', '/api/auth/login', {
phone: PHONE,
password: PASSWORD,
});
const token = login.data.token;
const h = { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' };
// 0) 清理该庄口旧演示数据(幂等)
const cleared = {
xuan: await clearRows(h, 'ProXuan_Daily', 'zhuangkouId', RAW_ZK_ID),
boil: await clearRows(h, 'ProQian_CocoonBoiling', 'processZhuangkouId', PZ_ID),
thread: await clearRows(h, 'ProQian_ThreadRecord', 'processZhuangkouId', PZ_ID),
hou: await clearRows(h, 'ProHou_Daily', 'processZhuangkouId', PZ_ID),
weigh: await clearRows(h, 'ProHou_WeighBig', 'processZhuangkouId', PZ_ID),
order: await clearRows(h, 'ProPlan_WorkOrder', 'processZhuangkouId', PZ_ID),
};
console.log('已清理旧数据:', JSON.stringify(cleared));
// 1) 生产计划工单(选茧 + 前缫),插入后自动联动重算庄口总体进度
await api(h, 'POST', '/api/data/ProPlan_WorkOrder/add', {
orderNo: 'WO-2026-08-001', processZhuangkouId: PZ_ID, targetProcess: 0,
planInput: 5000, planOutput: 4000, planStartDate: '2026-08-14T08:00:00', planEndDate: '2026-08-20T08:00:00', status: 1,
});
await api(h, 'POST', '/api/data/ProPlan_WorkOrder/add', {
orderNo: 'WO-2026-08-002', processZhuangkouId: PZ_ID, targetProcess: 1,
planInput: 3500, planOutput: 1500, planStartDate: '2026-08-14T08:00:00', planEndDate: '2026-08-22T08:00:00', status: 1,
});
console.log('工单已创建(选茧计划产出4000kg / 前缫计划产丝1500kg');
// 2) 选茧日报:领料 5000 → 上车茧 3200
const xuans = [
{ dailyDate: '2026-08-14T08:00:00', teamId: 1, zhuangkouId: RAW_ZK_ID, inputWeight: 3000, goodWeight: 1900, wasteWeight: 950, workerCount: 12, workHours: 8 },
{ dailyDate: '2026-08-15T08:00:00', teamId: 1, zhuangkouId: RAW_ZK_ID, inputWeight: 2000, goodWeight: 1300, wasteWeight: 550, workerCount: 12, workHours: 8 },
];
for (const row of xuans) {
const r = await api(h, 'POST', '/api/data/ProXuan_Daily/add', row);
console.log('选茧日报 id=' + (r.data && (r.data.id ?? JSON.stringify(r.data).slice(0, 60))));
}
// 3) 煮茧送茧 2800
const boils = [
{ boilDate: '2026-08-14T09:00:00', processZhuangkouId: PZ_ID, specId: 1, inputWeight: 1500, boilerNo: 'A01' },
{ boilDate: '2026-08-15T09:00:00', processZhuangkouId: PZ_ID, specId: 1, inputWeight: 1300, boilerNo: 'A02' },
];
for (const row of boils) {
const r = await api(h, 'POST', '/api/data/ProQian_CocoonBoiling/add', row);
console.log('煮茧 id=' + (r.data && (r.data.id ?? JSON.stringify(r.data).slice(0, 60))));
}
// 4) 缫丝落丝 900
const threads = [
{ recordDate: '2026-08-14T18:00:00', processZhuangkouId: PZ_ID, machineId: 1, shift: 1, threadCount: 48, weight: 480, specId: 1, employeeId: 1 },
{ recordDate: '2026-08-15T18:00:00', processZhuangkouId: PZ_ID, machineId: 2, shift: 2, threadCount: 42, weight: 420, specId: 1, employeeId: 1 },
];
for (const row of threads) {
const r = await api(h, 'POST', '/api/data/ProQian_ThreadRecord/add', row);
console.log('缫丝落丝 id=' + (r.data && (r.data.id ?? JSON.stringify(r.data).slice(0, 60))));
}
// 5) 复摇日报:上机 850 → 返丝 830
const hous = [
{ dailyDate: '2026-08-14T20:00:00', processZhuangkouId: PZ_ID, teamId: 2, inputWeight: 450, outputWeight: 440, outputCount: 44 },
{ dailyDate: '2026-08-15T20:00:00', processZhuangkouId: PZ_ID, teamId: 2, inputWeight: 400, outputWeight: 390, outputCount: 39 },
];
for (const row of hous) {
const r = await api(h, 'POST', '/api/data/ProHou_Daily/add', row);
console.log('复摇日报 id=' + (r.data && (r.data.id ?? JSON.stringify(r.data).slice(0, 60))));
}
// 6) 秤大丝 820
const weigh = await api(h, 'POST', '/api/data/ProHou_WeighBig/add', {
billNo: 'W-20260815-001', processZhuangkouId: PZ_ID, weighDate: '2026-08-15T21:00:00', totalWeight: 820, packageCount: 41,
});
console.log('秤大丝 id=' + (weigh.data && (weigh.data.id ?? JSON.stringify(weigh.data).slice(0, 60))));
// 7) 回读分工段进度
const r = await api(h, 'GET', '/api/workbench/zhuangkou-progress');
const d = r.data || {};
console.log('--- zhuangkou-progress total=' + d.total + ' producing=' + d.producing);
const it = (d.items || [])[0];
if (it) {
console.log('庄口=' + it.code + ' 状态=' + it.statusText + ' 总体进度=' + it.progress + ' 计划产丝=' + it.planOutput + 'kg');
for (const s of it.stages) console.log(' ' + s.name + ' ' + s.flow + ' 实际=' + s.actual + ' 基准=' + s.base + ' rate=' + s.rate + '%');
}
}
main().catch((e) => {
console.error('ERR: ' + e.message);
process.exit(1);
});