0.8.0: 契约副本刷齐并改为自动同步;未注册字段类型不再静默回落

契约副本(src/IShrimpType.ts)此前漂了一整代:停在旧的 json 类型,
缺 object / array / IFieldConfig.fields / IFieldConfig.item / parse / IPageResult.refs。
而本包自己的组件早就在读这些字段——FieldArray 读 field.item、FieldObject 读 fields、
FieldRelation 与 ShrimpTable 读 refs。等于组件在用、类型说没有。
接入方照这份副本理解 shrimp 能渲染什么,只会得出"结构化数据只有 json"的结论。

现在由 shrimp-server 的 postbuild 单向覆盖过来(它是契约真身),
本仓库另提供 npm run check:contract / sync:contract 手动比对与同步。
注释头不参与同步,它声明的是"我是副本",与契约正文是两回事。

VueAdmin.getFieldComponent:未注册类型仍回落成 string(类型词汇表是开放的,
消费侧放个 Field<X>.vue 就能扩,改成严格校验会当场打死 theseus 的 gesture/goodsList
和 hand 的 dropList/matterPointDropList),但按类型去重告警一次,
打印出现位置、已注册类型和两条修法。上面那个 json 就是这么静默活下来的。

resolveVue:内置组件表为空时报"本包没被 vite 正常转换、消费侧缺 optimizeDeps.exclude",
不再报"找不到 layout.vue"把人引去查文件路径。

README 重写成可照抄的接入模板 + 出错对照表 + "那几行为什么不能删"。
版本与 shrimp-server 归一到 0.8.0,删掉 @syncedFrom 这条手维护的版本标记。
This commit is contained in:
2026-08-10 19:48:57 +08:00
parent b7042c8387
commit 603e33d17f
6 changed files with 239 additions and 20 deletions
+18 -7
View File
@@ -1,15 +1,18 @@
/**
* Shrimp 类型契约 — 前后端 wire protocol 定义(前端副本)
*
* @syncedFrom shrimp-server contract 1.0.0
* 这是 shrimp-server/src/IShrimpType.ts 的逐字副本(除本注释块外)。
* 不要在这里改 wire protocol,先改 shrimp-server 那份升版本,再整份 cp 过来。
*
* 这是 shrimp-server/src/IShrimpType.ts 的逐字副本。
* 不要在这里改 wire protocol,先改 shrimp-server 那份升版本,再 copy 过来更新 @syncedFrom。
* 本文件由 shrimp-server 的 postbuild 自动同步过来,手改会被下次 build 覆盖。
* 手动同步: npm run sync:contract 只想看漂没漂: npm run check:contract
* 自动同步这件事存在的原因是:曾经靠人工复制,漂了一整代都没人发现——
* 副本停在旧的 json 类型、缺 object/array/fields/item/parse/refs,
* 而前端对未注册类型静默回落到 string,所以漂移不会报错、只会悄悄渲染错。
*
* 故意保留副本而不 import 'shrimp-server' 的原因:
* - shrimp-server 入口会把 mongoose / express 牵进前端 bundle
* - 拆 './types' 子入口会绕开 vite optimize、可读性也差
* - 类型契约低频改动,版本号对照即可
* - shrimp-server 默认入口会把 mongoose / express 牵进前端 bundle
* - 前端项目不该为一份纯类型去装后端包
*/
/** 业务错误码 */
@@ -238,8 +241,9 @@ export const ShrimpFieldTypes = {
Textarea: "textarea" as ShrimpFieldType,
Password: "password" as ShrimpFieldType,
Relation: "relation" as ShrimpFieldType,
Json: "json" as ShrimpFieldType,
Tab: "tab" as ShrimpFieldType,
Object: "object" as ShrimpFieldType,
Array: "array" as ShrimpFieldType,
}
/** 字段可编辑状态 */
@@ -279,6 +283,12 @@ export interface IFieldConfig {
rules?: IFieldRule[] // 通用验证规则(前后端共用)
/** tab 类型专用:每个 type 对应的子字段配置 */
tabs?: { key: any; label: ILabel; fields: IFieldConfig[] }[]
/** object 类型专用:嵌套字段组 */
fields?: IFieldConfig[]
/** array 类型专用:每个元素的字段配置(递归 IFieldConfig;元素无需 key,label 用作每项标题) */
item?: IFieldConfig
/** tab 类型专用:import 反序列化(把字符串还原成对象,如 xlsx 导入),默认 JSON.parse */
parse?: (text: string) => any
}
/** 通用验证规则(前后端共用) */
@@ -315,6 +325,7 @@ export interface IPageResult<T> {
page: number // 当前页码
pageSize: number // 每页数量
totalPages: number // 总页数
refs?: Record<string, Record<string, any>> // relation 显示名字典: { moduleKey: { id: 显示名 } },前端显示时查名,替代逐行 _label
}
// ============== 数据源查询协议 ==============
+28 -1
View File
@@ -80,6 +80,15 @@ export function resolveVue(suffix: string): VueLoader {
for (const key in internalSources) {
if (key.endsWith(tail)) return internalSources[key] as any
}
// 内置表整个是空的,说明本包被 esbuild 预打包了——import.meta.glob 是 vite 编译期宏,
// 预打包不认它,内置组件一个都收不到。报"找不到 layout.vue"会把人引向错误的方向。
if (Object.keys(internalSources).length === 0) {
throw new Error(
`[shrimp-admin] 内置组件表为空,本包没有被 vite 正常转换。\n` +
` 消费侧 vite.config 需要: optimizeDeps: { exclude: ["shrimp-admin"] }\n` +
` (或直接用 shrimp-admin/vite 提供的插件,它会自动加上)`
)
}
throw new Error(`[shrimp-admin] resolveVue: ${suffix} not found in sources`)
}
@@ -90,6 +99,19 @@ export function resolveVue(suffix: string): VueLoader {
const fieldRegistry = new Map<string, Component>()
/** 未注册类型每种只喊一次——表格逐格调 getFieldComponent,不去重会刷屏 */
const warnedTypes = new Set<string>()
function warnOnce(type: string, moduleKey?: string, fieldKey?: string) {
if (warnedTypes.has(type)) return
warnedTypes.add(type)
console.warn(
`[shrimp-admin] 字段类型 "${type}" 没有对应组件,已按 string 渲染` +
(moduleKey || fieldKey ? `(出现在 ${moduleKey ?? '?'}.${fieldKey ?? '?'})` : '') +
`\n 已注册: ${[...fieldRegistry.keys()].sort().join(', ')}` +
`\n 要么后端把 type 改成上面之一,要么消费侧补一个 shrimp/components/fields/Field${type.charAt(0).toUpperCase() + type.slice(1)}.vue`
)
}
function deriveFieldType(path: string): string | null {
const m = path.match(/\/components\/fields\/Field([A-Z][A-Za-z0-9]*)\.vue$/)
if (!m) return null
@@ -129,7 +151,12 @@ export class VueAdmin extends ShrimpClient {
moduleRoutes = new Map<string, IRouteInfo>()
getFieldComponent(_moduleKey: string, type: string, _fieldKey?: string): Component {
return fieldRegistry.get(type) || fieldRegistry.get('string')!
const hit = fieldRegistry.get(type)
if (hit) return hit
// 类型词汇表是开放的(消费侧放个 Field<X>.vue 就能扩),所以这里不能报错、只能回落。
// 但静默回落等于打错字永远发现不了——曾经有个 json 类型这么活了一整代。
warnOnce(type, _moduleKey, _fieldKey)
return fieldRegistry.get('string')!
}
/** 目标模块能否作为「单项查看」跳转目标:已按权限注册路由 且 非子模块。返回其路由信息或 undefined(不可跳) */
+1 -1
View File
@@ -2,7 +2,7 @@
//
// 消费侧用法见 README.md。本文件只做 re-export。
// ===== 类型契约(IShrimpType 本地副本,@syncedFrom shrimp-server contract 1.0.0)=====
// ===== 类型契约(IShrimpType 是 shrimp-server 那份的副本,由其 postbuild 自动同步)=====
export {
ShrimpCode,
ShrimpError,