Shrimp.getMenuTree() 之前默认输出 { t: '<moduleKey>.menuManage' },
强加一个用户未必写过的 i18n key,不写翻译就显示 raw key。退化为直接
用 module.label —— 模块注册时已声明,不另起 key。
UserModule / RoleModule 同步补 label: 之前 super(parent, "user", ...)
没传 label,会落到 ShrimpTableModule 默认的 { t: moduleKey };现在显
式传 { t: "user.menuManage" } / { t: "role.menuManage" },沿用 hand-
admin 已有的翻译键,迁移零成本。
demo 里临时塞的 DemoShrimp 子类清掉,直接 new Shrimp()。
175 lines
6.6 KiB
TypeScript
175 lines
6.6 KiB
TypeScript
/**
|
|
* Shrimp basic-admin demo
|
|
*
|
|
* 用 MemoryShrimpCrud 跑一个零依赖的 admin server,展示:
|
|
* - Shrimp 核心(模块注册 / RPC dispatch / 权限树)
|
|
* - ShrimpTableModule + MemoryShrimpCrud 的 CRUD 自动事件
|
|
* - 通过 Express 暴露成完整的 hand-admin 协议:
|
|
* GET /admin-api/config
|
|
* POST /admin-api/login
|
|
* GET /admin-api/me
|
|
* POST /admin-api/<module>[/<sub>...]/<event>
|
|
*
|
|
* 不使用 ShrimpServer/Mongoose/JWT,刻意暴露 ShrimpServer 把太多东西耦合在
|
|
* Express+Mongo+Auth 一起的事实,作为后续重构的对照样本。
|
|
*
|
|
* 单一 mock super-admin operator,前端登录直接放行,token 是固定字符串。
|
|
*/
|
|
import express, { type Request, type Response } from "express"
|
|
import {
|
|
Shrimp,
|
|
ShrimpTableModule,
|
|
MemoryShrimpCrud,
|
|
ShrimpFieldTypes,
|
|
ShrimpError,
|
|
ShrimpCode,
|
|
} from "shrimp-server"
|
|
import type {
|
|
IFieldConfig,
|
|
WithId,
|
|
IShrimpOperator,
|
|
IModuleInfo,
|
|
IMenuItem,
|
|
IMeResult,
|
|
} from "shrimp-server"
|
|
|
|
interface ITag { _id: string; name: string }
|
|
interface IProduct { _id: string; code: string; name: string; price: number; tagIds: string[] }
|
|
|
|
const tagFields: IFieldConfig[] = [
|
|
{ key: "name", label: "标签名", type: ShrimpFieldTypes.String, required: true, searchable: true },
|
|
]
|
|
|
|
const productFields: IFieldConfig[] = [
|
|
{ key: "code", label: "编码", type: ShrimpFieldTypes.String, required: true, editable: "addOnly" },
|
|
{ key: "name", label: "名称", type: ShrimpFieldTypes.String, required: true, searchable: true },
|
|
{ key: "price", label: "价格", type: ShrimpFieldTypes.Number, required: true },
|
|
{
|
|
key: "tagIds", label: "标签", type: ShrimpFieldTypes.Relation,
|
|
relation: { moduleKey: "tag", labelKey: "name" }, multiple: true, defaultValue: [],
|
|
},
|
|
]
|
|
|
|
const FAKE_TOKEN = "demo-super-admin-token"
|
|
|
|
const superAdmin: IShrimpOperator = {
|
|
userId: "demo-admin",
|
|
roleId: ["demo-super"],
|
|
isSuperAdmin: true,
|
|
}
|
|
|
|
async function bootstrap() {
|
|
const shrimp = new Shrimp()
|
|
|
|
// TODO(70→90): ShrimpTableModule 当前只在 dataSource.model 存在时自动登记 crudRegistry,
|
|
// 这里需要手动 wire 让 MemoryShrimpCrud 的 populate 也能查到关联表
|
|
const tagCrud = new MemoryShrimpCrud<WithId<ITag>>()
|
|
tagCrud.getRelatedCrud = (k) => shrimp.crudRegistry.get(k)
|
|
shrimp.crudRegistry.set("tag", tagCrud)
|
|
new ShrimpTableModule(shrimp, "tag", tagCrud, tagFields, "标签管理").initCrud()
|
|
|
|
const productCrud = new MemoryShrimpCrud<WithId<IProduct>>()
|
|
productCrud.getRelatedCrud = (k) => shrimp.crudRegistry.get(k)
|
|
shrimp.crudRegistry.set("product", productCrud)
|
|
new ShrimpTableModule(shrimp, "product", productCrud, productFields, "商品管理").initCrud()
|
|
|
|
// 种子数据
|
|
const t1 = await tagCrud.add({ name: "热门" } as any)
|
|
const t2 = await tagCrud.add({ name: "促销" } as any)
|
|
await productCrud.add({ code: "P001", name: "苹果", price: 5, tagIds: [t1._id] } as any)
|
|
await productCrud.add({ code: "P002", name: "香蕉", price: 3, tagIds: [t1._id, t2._id] } as any)
|
|
|
|
// super admin 看得到所有模块
|
|
const allKeys = (await shrimp.allPower()).map(p => p.key)
|
|
const superPowers = new Set(allKeys)
|
|
|
|
const app = express()
|
|
app.use(express.json())
|
|
|
|
// 简单认证: 仅检查 Authorization,通过即注入 super-admin operator
|
|
function authedRunner(req: Request, res: Response, run: () => Promise<any>) {
|
|
const auth = req.headers.authorization || ""
|
|
const token = auth.replace(/^Bearer\s+/i, "")
|
|
if (token !== FAKE_TOKEN) {
|
|
res.json({ code: ShrimpCode.UNAUTHORIZED, tip: { t: "shrimp.relogin" } })
|
|
return
|
|
}
|
|
Shrimp.asyncLocalStorage.run(superAdmin, async () => {
|
|
try {
|
|
const data = await run()
|
|
res.json({ code: ShrimpCode.OK, data })
|
|
} catch (e: any) {
|
|
if (e instanceof ShrimpError) {
|
|
res.json({ code: e.code, tip: e.tip })
|
|
} else {
|
|
console.error(e)
|
|
res.json({ code: ShrimpCode.SERVER_ERROR, log: String(e?.message || e) })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- 公开接口 ---
|
|
app.get("/admin-api/config", (_req, res) => {
|
|
res.json({ code: ShrimpCode.OK, data: { title: "Shrimp Basic Admin Demo" } })
|
|
})
|
|
|
|
app.post("/admin-api/login", (req, res) => {
|
|
const { username, password } = req.body || {}
|
|
// demo 模式不校验密码,任意账号都给同一个 token
|
|
if (!username || !password) {
|
|
res.json({ code: ShrimpCode.UNAUTHORIZED, tip: { t: "shrimp.loginFailed" } })
|
|
return
|
|
}
|
|
res.json({ code: ShrimpCode.OK, data: { token: FAKE_TOKEN } })
|
|
})
|
|
|
|
// --- 需要登录的接口 ---
|
|
app.get("/admin-api/me", (req, res) => {
|
|
authedRunner(req, res, async () => {
|
|
const moduleInfo: IModuleInfo[] = shrimp.getModuleInfo(superPowers)
|
|
const fullMenu = shrimp.getMenuTree()
|
|
const menuTree: IMenuItem[] = shrimp.filterMenuTree(fullMenu, allKeys)
|
|
const result: IMeResult = {
|
|
nickname: "Demo Admin",
|
|
moduleInfo,
|
|
menuTree,
|
|
options: {},
|
|
}
|
|
return result
|
|
})
|
|
})
|
|
|
|
// RPC dispatch: POST /admin-api/<...path> body 是参数数组
|
|
app.post(/^\/admin-api\/.+/, (req, res) => {
|
|
const rawPath = req.path.replace(/^\/admin-api\//, "")
|
|
// 不要把 login/config 也接进来
|
|
if (rawPath === "login" || rawPath === "config" || rawPath === "me") {
|
|
res.status(404).end()
|
|
return
|
|
}
|
|
authedRunner(req, res, async () => {
|
|
const path = rawPath.split("/").filter(Boolean)
|
|
const args = Array.isArray(req.body) ? req.body : []
|
|
return shrimp.call(path, ...args)
|
|
})
|
|
})
|
|
|
|
const port = 3100
|
|
app.listen(port, () => {
|
|
console.log(`Shrimp basic-admin demo: http://localhost:${port}`)
|
|
console.log("")
|
|
console.log("# 配合前端: cd web && npm install && npm run dev,然后访问 http://localhost:5173")
|
|
console.log("# 登录: 任意账号密码")
|
|
console.log("")
|
|
console.log("# 也可以直接 curl:")
|
|
console.log(` TOKEN=${FAKE_TOKEN}`)
|
|
console.log(` curl -s -X POST http://localhost:${port}/admin-api/product/list -H 'content-type: application/json' -H "Authorization: Bearer $TOKEN" -d '[{}]' | jq`)
|
|
})
|
|
}
|
|
|
|
bootstrap().catch(err => {
|
|
console.error("bootstrap failed:", err)
|
|
process.exit(1)
|
|
})
|