落地(依据《事项库与办事大厅详细设计 v1》§7)
- 路由:办事事项 modId=21 / 服务字典 modId=22 + 子站镜像(/sch/{code}/site/{siteCode}/svc/*)
+ 3 条 legacyRedirect 旧链兼容;SchoolLayout 菜单图标兜底
- ServiceItemList:三维筛选/关键字/分页/发布下架删除/跳转编辑
- ServiceItemEdit:三段富文本 + 封面 + 指南附件上传回显(attList)+「我要办理外链」提示
+ 部门选择器不可达时提示"可留空保存"
- ServiceDictManage:object/category 双 Tab,被引用键编辑锁定 + 删除引用守护提示
- api/类型:SvcItemApi/SvcDictApi 与 types/cms.ts 契约补齐;站点设置补 svc_hall_base 键
审计修复(docs/18)
- D3 接线:列表页新增 ServiceItemSortModal 拖拽排序(顺序源 selList,保存按下标重排 sort_no)
与「同步部门名」按钮,此前后端 sort/syncDeptName 已备但前端无入口
- 附件场景拆分:封面与三个富文本编辑器改传 scene=svc_item_asset,指南附件保持 svc_item
(内容资产由后端按内容派生归属,不再依赖前端收集上传 id——删除只写不读的 uploadedIds 记账)
- 部门组树:SvcDeptVo 增 children,前端 flattenDepts 缩进扁平化(后端改嵌套下发,交互不变)
- RichTextEditor 补 articleId/slateMinHeight prop 并去掉死状态 editorLoading;
ArticleEdit 改为复用该组件(正文区最小高度按原口径传入,观感不变),消除双份 wangEditor 实现
测试与 mock
- mock 补 /api/svc/** 全量桩(事项分页/字典/部门树/排序/同步部门名/附件回显 + 站点 1、2 示例数据),
dev:mock 下三页可用(实测 curl 各项响应正确)
- cms.spec 补 save/syncDeptName/selList/dict save+delete 契约;siteRouter.spec 补事项三条路由
modId 与子站镜像 resolve 断言;新增 RichTextEditor.spec 5 例(上传参数三键/成功 emit/失败不 emit/工具栏排除项)
- vitest 127 用例全绿 + vite build 通过
This commit is contained in:
+181
-1
@@ -12,7 +12,7 @@
|
||||
import type { Plugin } from "vite";
|
||||
import type { IncomingMessage, ServerResponse } from "http";
|
||||
import { MockDb, categoryTree, categoryName, nextId, seedDb } from "./data";
|
||||
import { MockAttachment, MockBanner, MockLink, MockSite } from "./data";
|
||||
import { MockAttachment, MockBanner, MockLink, MockSite, MockSvcDict, MockSvcItem } from "./data";
|
||||
|
||||
const db: MockDb = seedDb();
|
||||
|
||||
@@ -855,6 +855,186 @@ async function handleApi(req: IncomingMessage, res: ServerResponse, path: string
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ---- 事项库:服务字典 + 服务部门 ---- */
|
||||
if (method === "GET" && path === "/api/svc/dict/list") {
|
||||
const siteId = num(q(req, "siteId"), 0)!;
|
||||
const dictType = q(req, "dictType");
|
||||
const rows = db.svcDicts
|
||||
.filter(d => d.siteId === siteId && (!dictType || d.dictType === dictType))
|
||||
.sort((x, y) => x.sortNo - y.sortNo || x.id - y.id)
|
||||
.map(d => ({
|
||||
...d,
|
||||
referenced: db.svcItems.filter(i =>
|
||||
d.dictType === "category" ? i.categoryKey === d.dictKey : i.objectKey === d.dictKey
|
||||
).length,
|
||||
}));
|
||||
sendJson(res, ok(rows));
|
||||
return true;
|
||||
}
|
||||
if (method === "POST" && path === "/api/svc/dict/save") {
|
||||
const body = await readJson<Partial<MockSvcDict>>(req);
|
||||
if (body.id) {
|
||||
const i = db.svcDicts.findIndex(d => d.id === body.id);
|
||||
if (i >= 0) db.svcDicts[i] = { ...db.svcDicts[i], ...body } as MockSvcDict;
|
||||
sendJson(res, ok(db.svcDicts[i]?.id ?? null));
|
||||
} else {
|
||||
const row: MockSvcDict = {
|
||||
id: nextId(db.svcDicts),
|
||||
siteId: body.siteId!,
|
||||
dictType: body.dictType || "object",
|
||||
dictKey: body.dictKey || "",
|
||||
dictName: body.dictName || "",
|
||||
sortNo: body.sortNo ?? 0,
|
||||
status: body.status ?? 1,
|
||||
};
|
||||
db.svcDicts.push(row);
|
||||
sendJson(res, ok(row.id));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (method === "POST" && path === "/api/svc/dict/delete") {
|
||||
const { id } = await readJson<{ id: number }>(req);
|
||||
db.svcDicts = db.svcDicts.filter(d => d.id !== id);
|
||||
sendJson(res, ok(null));
|
||||
return true;
|
||||
}
|
||||
if (method === "GET" && path === "/api/svc/dept/tree") {
|
||||
// 模拟 UserBase 组树经 type 过滤后嵌套下发:学校 → 校内处室
|
||||
sendJson(res, ok([
|
||||
{
|
||||
id: 100,
|
||||
name: "云海大学",
|
||||
children: [
|
||||
{ id: 10, name: "教务处", children: [] },
|
||||
{ id: 20, name: "后勤处", children: [] },
|
||||
{ id: 30, name: "学生工作处", children: [] },
|
||||
],
|
||||
},
|
||||
]));
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ---- 事项库:办事事项管理 ---- */
|
||||
if (method === "GET" && path === "/api/svc/item/page") {
|
||||
const siteId = num(q(req, "siteId"), 0)!;
|
||||
const status = q(req, "status");
|
||||
const objectKey = q(req, "objectKey");
|
||||
const categoryKey = q(req, "categoryKey");
|
||||
const keyword = q(req, "keyword");
|
||||
const page = num(q(req, "page"), 1)!;
|
||||
const pageSize = num(q(req, "pageSize"), 10)!;
|
||||
const dictName = (type: "object" | "category", key?: string) =>
|
||||
key ? db.svcDicts.find(d => d.siteId === siteId && d.dictType === type && d.dictKey === key)?.dictName : undefined;
|
||||
const filtered = db.svcItems
|
||||
.filter(i => i.siteId === siteId)
|
||||
.filter(i => (status === undefined || status === null || status === "" ? true : i.status === Number(status)))
|
||||
.filter(i => (!objectKey ? true : i.objectKey === objectKey))
|
||||
.filter(i => (!categoryKey ? true : i.categoryKey === categoryKey))
|
||||
.filter(i => (!keyword ? true : i.title.includes(keyword) || i.itemCode.includes(keyword)))
|
||||
.sort((x, y) => x.sortNo - y.sortNo || y.id - x.id);
|
||||
const start = (page - 1) * pageSize;
|
||||
sendJson(res, ok({
|
||||
total: filtered.length,
|
||||
list: filtered.slice(start, start + pageSize).map(i => ({
|
||||
...i,
|
||||
objectName: dictName("object", i.objectKey),
|
||||
categoryName: dictName("category", i.categoryKey),
|
||||
applyUrl: i.extJson ? (JSON.parse(i.extJson).apply_url as string) : undefined,
|
||||
})),
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
if (method === "GET" && path === "/api/svc/item/get") {
|
||||
const id = num(q(req, "id"), 0)!;
|
||||
const item = db.svcItems.find(i => i.id === id);
|
||||
if (!item) {
|
||||
sendJson(res, { code: 2, msg: "事项不存在", data: null });
|
||||
return true;
|
||||
}
|
||||
sendJson(res, ok({
|
||||
...item,
|
||||
applyUrl: item.extJson ? (JSON.parse(item.extJson).apply_url as string) : undefined,
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
if (method === "POST" && path === "/api/svc/item/save") {
|
||||
const body = await readJson<Partial<MockSvcItem> & { applyUrl?: string }>(req);
|
||||
const extJson = body.applyUrl ? JSON.stringify({ apply_url: body.applyUrl }) : undefined;
|
||||
if (body.id) {
|
||||
const i = db.svcItems.findIndex(x => x.id === body.id);
|
||||
if (i >= 0) db.svcItems[i] = { ...db.svcItems[i], ...body, extJson } as MockSvcItem;
|
||||
sendJson(res, ok(db.svcItems[i]?.id ?? null));
|
||||
} else {
|
||||
const row: MockSvcItem = {
|
||||
id: nextId(db.svcItems),
|
||||
siteId: body.siteId!,
|
||||
itemCode: body.itemCode || "",
|
||||
title: body.title || "",
|
||||
handleMode: body.handleMode ?? 1,
|
||||
onlineFlag: body.onlineFlag ?? 1,
|
||||
sortNo: body.sortNo ?? 0,
|
||||
status: 0,
|
||||
viewCount: 0,
|
||||
createTime: daysAgo(0),
|
||||
...body,
|
||||
extJson,
|
||||
} as MockSvcItem;
|
||||
db.svcItems.push(row);
|
||||
sendJson(res, ok(row.id));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const svcStateMatch = path.match(/^\/api\/svc\/item\/(publish|offline)\/(\d+)$/);
|
||||
if (method === "POST" && svcStateMatch) {
|
||||
const item = db.svcItems.find(i => i.id === Number(svcStateMatch[2]));
|
||||
if (item) {
|
||||
item.status = svcStateMatch[1] === "publish" ? 1 : 2;
|
||||
if (item.status === 1) item.publishTime = daysAgo(0);
|
||||
else item.offlineTime = daysAgo(0);
|
||||
}
|
||||
sendJson(res, ok(null));
|
||||
return true;
|
||||
}
|
||||
if (method === "POST" && path === "/api/svc/item/delete") {
|
||||
const { id } = await readJson<{ id: number }>(req);
|
||||
db.svcItems = db.svcItems.filter(i => i.id !== id);
|
||||
sendJson(res, ok(null));
|
||||
return true;
|
||||
}
|
||||
if (method === "POST" && path === "/api/svc/item/sort") {
|
||||
const pairs = await readJson<{ id: number; sortNo: number }[]>(req);
|
||||
(pairs || []).forEach(p => {
|
||||
const item = db.svcItems.find(i => i.id === p.id);
|
||||
if (item) item.sortNo = p.sortNo;
|
||||
});
|
||||
sendJson(res, ok(null));
|
||||
return true;
|
||||
}
|
||||
if (method === "POST" && path === "/api/svc/item/syncDeptName") {
|
||||
sendJson(res, ok({ updated: 0 }));
|
||||
return true;
|
||||
}
|
||||
if (method === "GET" && path === "/api/svc/item/selList") {
|
||||
const siteId = num(q(req, "siteId"), 0)!;
|
||||
const rows = db.svcItems
|
||||
.filter(i => i.siteId === siteId)
|
||||
.sort((x, y) => x.sortNo - y.sortNo || x.id - y.id)
|
||||
.map(i => ({ id: i.id, itemCode: i.itemCode, title: i.title, status: i.status }));
|
||||
sendJson(res, ok(rows));
|
||||
return true;
|
||||
}
|
||||
if (method === "GET" && path === "/api/svc/item/attList") {
|
||||
const siteId = num(q(req, "siteId"), 0)!;
|
||||
const id = num(q(req, "id"), 0)!;
|
||||
const ids = db.svcItems.find(i => i.id === id)?.attachmentIds || [];
|
||||
sendJson(res, ok(
|
||||
db.attachments
|
||||
.filter(a => a.siteId === siteId && ids.includes(a.id))
|
||||
.map(a => ({ id: a.id, fileName: a.fileName, webUrl: a.url, fileSize: a.fileSize }))
|
||||
));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user