From 2b33bca68173c7e92da441463bc32b376eb00cf6 Mon Sep 17 00:00:00 2001 From: wc24 Date: Mon, 25 May 2026 15:05:16 +0800 Subject: [PATCH] =?UTF-8?q?v0.3.0:=20=E5=8D=95=20sources=20=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=20+=20=E8=B7=AF=E5=BE=84=E5=90=8E=E7=BC=80=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E7=BA=A6=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 破坏性重构。框架对外只暴露一个 `sources` glob 入口,消费侧任何 .vue 都按文件路径后缀匹配同后缀覆盖: - shrimp/components/.vue 同后缀覆盖框架内置(login/layout/topbar/ShrimpTable/...) - shrimp/components/fields/Field.vue 注册类型 (首字母小写) - shrimp/module/.vue 业务模块 变更: - 删除 customFields / moduleSources / overrideField / registerField 旧入口 - 删除 fieldOverrides Map,字段表纯靠 glob 扫描注册 - 框架内 .vue 对可覆盖 peer 改用 defineAsyncComponent(resolveVue(...)) (login → ShrimpLangSwitch / layout → sideM/navMenu/topbar/userbar/logo / ShrimpTable → ShrimpEditDrawer/ShrimpFilterDrawer/...) - views/ 全部并入 components/,App.vue 上提到 src/ 根 - LangSwitch → ShrimpLangSwitch,PowerTree → ShrimpPowerTree - module/FieldRelativeTime.vue → components/fields/FieldRelativeTime.vue - module/user.vue 改用 #column-updatedAt slot 替代旧的 overrideField 字段类型约定从 'field' + Name 改为 lowercase-first(FieldString → 'string'), 内置和消费侧统一。 --- README.md | 63 ++++++++-- package.json | 2 +- src/{views => }/App.vue | 4 +- src/VueAdmin.ts | 117 +++++++++--------- src/{views => components}/404.vue | 0 .../{LangSwitch.vue => ShrimpLangSwitch.vue} | 0 .../{PowerTree.vue => ShrimpPowerTree.vue} | 0 src/components/ShrimpTable.vue | 13 +- .../fields}/FieldRelativeTime.vue | 0 src/{views => components}/layout.vue | 15 +-- src/{views => components}/login.vue | 7 +- src/{views => components}/logo.vue | 0 src/{views => components}/navMenu.vue | 0 src/{views => components}/sideM.vue | 9 +- src/{views => components}/topbar.vue | 0 src/{views => components}/userbar.vue | 6 +- src/index.ts | 11 +- src/module/role.vue | 9 +- src/module/user.vue | 14 ++- src/module/user/operationLog.vue | 7 +- 20 files changed, 165 insertions(+), 112 deletions(-) rename src/{views => }/App.vue (87%) rename src/{views => components}/404.vue (100%) rename src/components/{LangSwitch.vue => ShrimpLangSwitch.vue} (100%) rename src/components/{PowerTree.vue => ShrimpPowerTree.vue} (100%) rename src/{module => components/fields}/FieldRelativeTime.vue (100%) rename src/{views => components}/layout.vue (85%) rename src/{views => components}/login.vue (92%) rename src/{views => components}/logo.vue (100%) rename src/{views => components}/navMenu.vue (100%) rename src/{views => components}/sideM.vue (84%) rename src/{views => components}/topbar.vue (100%) rename src/{views => components}/userbar.vue (90%) diff --git a/README.md b/README.md index b677c59..02b7be2 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,7 @@ Shrimp 框架前端 —— 一种**经典解决方案**(不是框架本身),消 ## 安装 ```bash -# 推荐 HTTP -npm i git+http://git.zeromvc.com/wc24/shrimp-admin.git#v0.1.0 +npm i git+http://git.zeromvc.com/wc24/shrimp-admin.git#v0.3.0 ``` shrimp-admin 是**源码 ship**(.ts + .vue 直接发),由消费侧 Vite 编译。 @@ -20,6 +19,8 @@ shrimp-admin 是**源码 ship**(.ts + .vue 直接发),由消费侧 Vite 编译 import { createApp } from 'vue' import { createShrimpAdmin, App } from 'shrimp-admin' import 'shrimp-admin/style' +import zhCn from './locales/zh-cn.json' +import en from './locales/en.json' const app = createApp(App) app.use(createShrimpAdmin({ @@ -27,22 +28,62 @@ app.use(createShrimpAdmin({ API_URL: '/admin-api', TIMEOUT: 10000, }, - moduleSources: import.meta.glob('./module/**/*.vue'), - customFields: import.meta.glob('./module/fields/Field*.vue'), + locales: { 'zh-cn': zhCn, en }, + sources: import.meta.glob('./shrimp/**/*.vue'), })) app.mount('#app') ``` +## 唯一约定: `sources` 路径后缀覆盖 + +消费侧把所有要交给框架的 .vue 通过单个 `sources` 入口注入(`import.meta.glob` 必须由消费侧自己调用,vite 静态扫描相对调用文件)。框架按**文件路径后缀**匹配,同后缀消费侧优先,内置兜底。 + +``` +项目/ +└── src/shrimp/ 消费侧目录,可任意命名 + ├── components/ + │ ├── login.vue 覆盖框架内置 login 页 + │ ├── topbar.vue 覆盖顶栏 + │ └── fields/ + │ └── FieldRelativeTime.vue 注册字段类型 relativeTime + └── module/ + ├── product.vue 业务模块,routed by moduleKey + └── user/ + └── orders.vue 子模块,parent=user +``` + +### 框架内置 slot(可覆盖,suffix 列表) + +``` +components/login.vue components/404.vue components/layout.vue +components/logo.vue components/navMenu.vue components/sideM.vue +components/topbar.vue components/userbar.vue +components/ShrimpLangSwitch.vue components/ShrimpPowerTree.vue +components/ShrimpTable.vue components/ShrimpEditDrawer.vue +components/ShrimpFilterDrawer.vue components/ShrimpFilterTrigger.vue +components/ShrimpSearchInput.vue +components/fields/Field{String,Boolean,Date,Json,Number,Password,Relation,Select,Tab,Textarea,RelativeTime}.vue +``` + +### 字段类型注册 + +`components/fields/Field.vue` 自动注册类型 ``(首字母小写): + +| 文件 | type 注册键 | +| ------------------------ | ------------------ | +| `FieldString.vue` | `string` | +| `FieldRelativeTime.vue` | `relativeTime` | +| 你自己的 `FieldMyType.vue` | `myType` | + +后端 moduleInfo 里 `field.type === 'myType'` 时就用你这个组件。 + +### 业务模块 + +`components/X.vue` 命中 `moduleKey === 'X'` 的路由,自动作为内容页。子表用嵌套目录(`module//.vue`)。 + ## 消费的契约 参见 shrimp-server README `## 约定` 章节。本包消费以下条件契约: - **ShrimpTable**: list 响应 `{list,total,page,pageSize,totalPages}` / 字段 type 枚举 / populate `${key}_label` - **ShrimpServer**: HTTP 路由 / JWT Bearer / IUserAccessor 返回的 powers -- **默认 Field*.vue**: 字段 type 必须是内置枚举之一,或通过 `customFields` 注册 - -## 扩展点 - -- `customFields` — 业务字段组件(`Field.vue`) -- `moduleSources` — 业务模块组件(`module/.vue` / `module//.vue`) -- `vueAdmin.registerField(type, comp)` / `overrideField(moduleKey, fieldKey, comp)` — 运行时也可 diff --git a/package.json b/package.json index 330f4ff..22ecac1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shrimp-admin", - "version": "0.2.4", + "version": "0.3.0", "private": true, "description": "Shrimp 框架前端 — 消费 ShrimpServer + ShrimpTable 契约的经典 Vue3 + Element Plus 实现", "main": "src/index.ts", diff --git a/src/views/App.vue b/src/App.vue similarity index 87% rename from src/views/App.vue rename to src/App.vue index 10e19ec..ff29368 100644 --- a/src/views/App.vue +++ b/src/App.vue @@ -6,7 +6,7 @@ diff --git a/src/VueAdmin.ts b/src/VueAdmin.ts index 16b516f..cc51f9f 100644 --- a/src/VueAdmin.ts +++ b/src/VueAdmin.ts @@ -10,7 +10,6 @@ import { createI18n, I18n } from 'vue-i18n' import { IShrimpResponse, ILabel } from './IShrimpType' import { ShrimpClient, handleShrimpResponse, type IShrimpTipper, type IRouteInfo } from './ShrimpClient' -import FieldString from './components/fields/FieldString.vue' // ============== 工具 ============== @@ -39,50 +38,62 @@ export interface ShrimpAdminConfig { LS_ENCRYPTION_key?: string } +type VueLoader = () => Promise + export interface ShrimpAdminOptions { config: ShrimpAdminConfig - /** 业务模块组件:`import.meta.glob('./module/**\/*.vue')` 的结果 */ - moduleSources?: Record Promise<{ default: Component } | Component>> - /** 业务字段组件:`import.meta.glob('./module/fields/Field*.vue')`,文件名 FieldXxx → type='fieldXxx' */ - customFields?: Record Promise<{ default: Component } | Component>> + /** + * 消费侧 .vue 源,推荐 `import.meta.glob('./shrimp/**\/*.vue')`。 + * - `shrimp/components/.vue` 同后缀覆盖框架内置(login/layout/topbar/ShrimpTable/...) + * - `shrimp/components/fields/Field.vue` 注册字段类型 ``(首字母小写),同名覆盖内置 + * - `shrimp/module/.vue` 业务模块组件(routed by moduleKey) + */ + sources?: Record /** 语言资源,与框架自带的合并(消费侧同 key 覆盖框架默认) */ locales?: Record /** 语言列表,默认 [{ lang: 'zh-cn', name: '简体中文' }] */ langList?: { lang: string; name: string }[] } +// ============== 源注册表 ============== +// +// 唯一约定:消费侧把 .vue 源放进 `sources`,框架按文件路径后缀解析。 +// 框架内部对可覆盖的 .vue 也用 resolveVue 解析(而不是静态 import),保证 consumer 覆盖在框架内部使用点也生效。 + +const internalSources = import.meta.glob('./{components,module}/**/*.vue') +const externalSources: Record = {} + +/** + * 路径后缀匹配解析: 消费侧优先,内置兜底。 + * suffix 形如 `components/login.vue` / `module/user.vue` / `module/user/operationLog.vue`。 + */ +export function resolveVue(suffix: string): VueLoader { + const tail = '/' + suffix + for (const key in externalSources) { + if (key.endsWith(tail)) return externalSources[key] + } + for (const key in internalSources) { + if (key.endsWith(tail)) return internalSources[key] as any + } + throw new Error(`[shrimp-admin] resolveVue: ${suffix} not found in sources`) +} + // ============== 字段组件注册表 ============== +// +// 约定:`components/fields/Field.vue` → 字段类型 ``(首字母小写)。 +// 后端 moduleInfo 的 field.type 命中这里的 key 就用对应组件,否则回退到 string。 const fieldRegistry = new Map() -const fieldOverrides = new Map() -fieldRegistry.set('string', FieldString) -fieldRegistry.set('boolean', defineAsyncComponent(() => import('./components/fields/FieldBoolean.vue'))) -fieldRegistry.set('date', defineAsyncComponent(() => import('./components/fields/FieldDate.vue'))) -fieldRegistry.set('number', defineAsyncComponent(() => import('./components/fields/FieldNumber.vue'))) -fieldRegistry.set('password', defineAsyncComponent(() => import('./components/fields/FieldPassword.vue'))) -fieldRegistry.set('relation', defineAsyncComponent(() => import('./components/fields/FieldRelation.vue'))) -fieldRegistry.set('select', defineAsyncComponent(() => import('./components/fields/FieldSelect.vue'))) -fieldRegistry.set('textarea', defineAsyncComponent(() => import('./components/fields/FieldTextarea.vue'))) -fieldRegistry.set('json', defineAsyncComponent(() => import('./components/fields/FieldJson.vue'))) -fieldRegistry.set('tab', defineAsyncComponent(() => import('./components/fields/FieldTab.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 + return m[1][0].toLowerCase() + m[1].slice(1) +} -// 框架自带模块组件 — vite glob 相对本文件解析,扫到的是 shrimp-admin 内部的 module/ -const internalModules = import.meta.glob('./module/**/*.vue') - -// 业务模块由消费侧通过 createShrimpAdmin({ moduleSources }) 注册 -const externalModules: Record Promise> = {} - -function resolveModuleComponent(componentPath: string): (() => Promise) | null { - const suffix = `/module/${componentPath}.vue` - // 业务优先(允许覆盖同名内置) - for (const key in externalModules) { - if (key.endsWith(suffix)) return externalModules[key] - } - for (const key in internalModules) { - if (key.endsWith(suffix)) return internalModules[key] as any - } - return null +for (const path in internalSources) { + const type = deriveFieldType(path) + if (type) fieldRegistry.set(type, defineAsyncComponent(internalSources[path] as any)) } // ============== LocalData ============== @@ -110,20 +121,8 @@ export class VueAdmin extends ShrimpClient { config: ShrimpAdminConfig langList: { lang: string; name: string }[] - registerField(type: string, comp: Component) { fieldRegistry.set(type, comp) } - - overrideField(moduleKey: string, fieldKey: string, comp: Component) { - const registryKey = `__override_${moduleKey}_${fieldKey}` - fieldRegistry.set(registryKey, comp) - fieldOverrides.set(`${moduleKey}.${fieldKey}`, registryKey) - } - - getFieldComponent(moduleKey: string, type: string, fieldKey?: string): Component { - if (fieldKey) { - const override = fieldOverrides.get(`${moduleKey}.${fieldKey}`) - if (override && fieldRegistry.has(override)) return fieldRegistry.get(override)! - } - return fieldRegistry.get(type) || FieldString + getFieldComponent(_moduleKey: string, type: string, _fieldKey?: string): Component { + return fieldRegistry.get(type) || fieldRegistry.get('string')! } state = reactive({ isMobile: false }) @@ -161,15 +160,11 @@ export class VueAdmin extends ShrimpClient { this.config = opts.config this.langList = opts.langList || [{ lang: 'zh-cn', name: '简体中文' }] - // 业务模块/字段注册 - if (opts.moduleSources) Object.assign(externalModules, opts.moduleSources) - if (opts.customFields) { - for (const path in opts.customFields) { - const match = path.match(/\/Field(.+)\.vue$/) - if (match) { - const name = 'field' + match[1] - fieldRegistry.set(name, defineAsyncComponent(opts.customFields[path] as any)) - } + if (opts.sources) { + Object.assign(externalSources, opts.sources) + for (const path in opts.sources) { + const type = deriveFieldType(path) + if (type) fieldRegistry.set(type, defineAsyncComponent(opts.sources[path] as any)) } } @@ -244,8 +239,10 @@ export class VueAdmin extends ShrimpClient { registerRoute(info: IRouteInfo) { const componentPath = info.componentPath || info.moduleKey - const importer = resolveModuleComponent(componentPath) - if (!importer) { + let importer: VueLoader + try { + importer = resolveVue(`module/${componentPath}.vue`) + } catch { console.warn(`[shrimp-admin] module component not found: module/${componentPath}.vue`) return } @@ -265,17 +262,17 @@ export class VueAdmin extends ShrimpClient { { name: "layout", path: "/", - component: () => import('./views/layout.vue'), + component: resolveVue('components/layout.vue'), children: [] }, { path: "/login", - component: () => import('./views/login.vue'), + component: resolveVue('components/login.vue'), meta: { title: { t: 'login.signIn' } } }, { path: "/:pathMatch(.*)", - component: () => import('./views/404.vue'), + component: resolveVue('components/404.vue'), }, ], }) diff --git a/src/views/404.vue b/src/components/404.vue similarity index 100% rename from src/views/404.vue rename to src/components/404.vue diff --git a/src/components/LangSwitch.vue b/src/components/ShrimpLangSwitch.vue similarity index 100% rename from src/components/LangSwitch.vue rename to src/components/ShrimpLangSwitch.vue diff --git a/src/components/PowerTree.vue b/src/components/ShrimpPowerTree.vue similarity index 100% rename from src/components/PowerTree.vue rename to src/components/ShrimpPowerTree.vue diff --git a/src/components/ShrimpTable.vue b/src/components/ShrimpTable.vue index 9c8fc1b..478ac94 100644 --- a/src/components/ShrimpTable.vue +++ b/src/components/ShrimpTable.vue @@ -78,21 +78,22 @@