vue3-django 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +92 -0
  2. package/dist/vue3-django.css +1 -0
  3. package/dist/vue3-django.js +4439 -0
  4. package/docs/iteration-01-preview.jpg +0 -0
  5. package/docs/iterations.md +37 -0
  6. package/docs/migration.md +133 -0
  7. package/docs/verification.md +25 -0
  8. package/package.json +53 -0
  9. package/src/components/form/Field.vue +113 -0
  10. package/src/components/form/Form.js +49 -0
  11. package/src/components/form/Form.vue +285 -0
  12. package/src/components/layout/Actions.vue +132 -0
  13. package/src/components/layout/Drawer.vue +87 -0
  14. package/src/components/layout/Layout.vue +193 -0
  15. package/src/components/layout/SideBar.vue +57 -0
  16. package/src/components/layout/ViewTabs.vue +136 -0
  17. package/src/components/mobile/Form.vue +16 -0
  18. package/src/components/model/Create.vue +20 -0
  19. package/src/components/model/Form.vue +136 -0
  20. package/src/components/model/Relations.vue +236 -0
  21. package/src/components/model/Search.vue +103 -0
  22. package/src/components/model/Select.vue +91 -0
  23. package/src/components/model/Table.vue +335 -0
  24. package/src/components/table/Widget.vue +195 -0
  25. package/src/components/widgets/ForeignKey.vue +58 -0
  26. package/src/components/widgets/Icon.vue +37 -0
  27. package/src/composables/context.js +16 -0
  28. package/src/composables/drawer.js +19 -0
  29. package/src/composables/tab.js +7 -0
  30. package/src/configs/axios.js +2 -0
  31. package/src/core/auth.js +68 -0
  32. package/src/core/http.js +52 -0
  33. package/src/core/menus.js +39 -0
  34. package/src/core/metadata.js +50 -0
  35. package/src/core/registry.js +127 -0
  36. package/src/core/relations.js +74 -0
  37. package/src/core/table.js +48 -0
  38. package/src/core/views.js +23 -0
  39. package/src/index.js +31 -0
  40. package/src/router/index.js +78 -0
  41. package/src/style.css +23 -0
  42. package/src/utils/app_model.js +2 -0
  43. package/src/utils/array_normalize.js +1 -0
Binary file
@@ -0,0 +1,37 @@
1
+ # 敏捷迭代计划
2
+
3
+ ## 原则
4
+
5
+ 每轮完成一个能演示、能验收的业务闭环。复用旧版模型标识、元数据、配置和事件语义;Vue 2 专属生命周期和组件写法在迁移页面时改为 Vue 3。保留 JavaScript 与 Options API 的选择,不强制业务全面改写为 TypeScript 或 Composition API。
6
+
7
+ ## 第 1 轮:模型 CRUD 基础(本次实现)
8
+
9
+ - 从真实旧源码确认 apps.models 注册、app.model、OPTIONS actions、config.js、尾斜杠、Token/CSRF、code/msg 错误和 form-posted 约定。
10
+ - 提供独立 HTTP/Registry/AppModel;注册表按实例隔离,元数据并发去重并缓存,失败可重试。
11
+ - 元数据字段 + 配置覆盖的列表、搜索、页码分页、新增、编辑、函数/API 行动作。
12
+ - 同一模型层驱动 Element Plus 与 Vant,支持字段插槽和自定义组件。
13
+ - 本地演示、模型协议和两端组件测试、库构建及演示构建。
14
+
15
+ 验收:演示两端读取同一批项目;搜索与翻页;新增、修改及暂停;重复名称显示字段错误;false/0 不丢失;部署前缀保留;保存不包含只读字段。
16
+
17
+ ## 第 2 轮:真实业务试迁移
18
+
19
+ 选一个低风险、包含外键与权限的业务模型,记录真实 OPTIONS、查询、400 和权限响应(脱敏)。实现登录态、401/403 处理与刷新后的身份恢复;读取原 user.model_permissions 并控制可用动作;后端权限作为实际校验。对齐菜单/路由并保留旧页面入口以便回退。明确主键、复杂字段、过滤器和权限的真实契约,再扩展基础层。
20
+
21
+ 完成条件:同一后端下新旧页面的查询和保存结果一致;完整走通登录、无权访问、错误修复与回退。
22
+
23
+ ## 第 3 轮:关系与动作
24
+
25
+ 外键搜索/显示、多对多、父子查询、generic_foreign_key、SEARCH filter_fields、排序、批量选择及 batchActions(ids + context)。建立 scoped 事件机制替代 Vue 2 bus,用 posted/deleted 刷新依赖模型,明确失败、取消和确认行为。
26
+
27
+ ## 第 4 轮:高频扩展
28
+
29
+ 分组表单、嵌套字段、JSON、文件上传、导入导出、异步任务进度。按首个真实业务的使用频率排序,每轮只实现被验证需要的能力。
30
+
31
+ ## 第 5 轮:逐步推广
32
+
33
+ 第二个独立业务接入,验证配置与插件扩展边界、按端入口及依赖拆分、包声明和发布流程。建立变更日志和版本兼容策略,再扩大替换范围。
34
+
35
+ ## 首轮明确缺口
36
+
37
+ 未接实际 DRF 服务;没有身份/权限界面、Vue Router/Vuex 替代、自动菜单、复杂搜索、外键选择、批量动作、删除界面、上传、导入导出和旧事件总线。当前不会解析字符串路由动作;需要宿主提供 do 函数。移动 choices 使用原生 select,日期输入尚未接 Vant Picker。嵌套验证错误暂以 JSON 显示。首轮未发布 npm、未改动旧仓库。
@@ -0,0 +1,133 @@
1
+ # 业务平稳迁移
2
+
3
+ 旧框架来源:`/Users/denishuang/Documents/workspace/szuprefix/front/exam/dashboard/vue-django`,本次只读分析。
4
+
5
+ ## 保留的理念和约定
6
+
7
+ 模型层 `AppModel`/`Register`、`apps.models`、`app.model`、OPTIONS 元数据、view config、items 字段覆盖、defaults、尾斜杠以及 DRF 错误契约延续。新项目可以继续用 Options API;组件按页面迁移,无需一次重写整个业务。
8
+
9
+ 推荐宿主通过 `createRegistry` 创建实例并用插件注入;旧式单例 `Register` 和 `src/utils/app_model.js` 保留入口。单例用于单应用过渡,多应用/SSR 使用独立 registry。没有注入插件时组件回退单例。
10
+
11
+ ## 第一个页面的操作顺序
12
+
13
+ 1. 复制一个模型的注册配置及 view config 到新业务;不要直接覆盖旧页面。
14
+ 2. 从实际后端核对 OPTIONS 与 DRF pagination,配置 `baseURL` 和认证。
15
+ 3. 使用 ModelTable、ModelForm,宿主接管 create/edit 的弹窗或路由;验证查询结果、新增和编辑载荷、字段错误。
16
+ 4. 用插槽迁移该页面的特殊字段,遇到通用能力再加入框架。
17
+ 5. 保留旧页面 URL,通过菜单或业务开关切换。上线前用同一个后端做验收,回退时重新打开旧入口,避免自动重发写请求。
18
+
19
+ ## Vue 3 必改项
20
+
21
+ | 旧版 | 新版 |
22
+ | ----------------------- | ---------------------------------------------- |
23
+ | Vue.use / Vue.prototype | app.use / app.config.globalProperties |
24
+ | value + input | modelValue + update:modelValue |
25
+ | .sync | v-model:参数 |
26
+ | slot / slot-scope | #slot="scope" |
27
+ | $listeners | $attrs 中合并事件 |
28
+ | beforeDestroy | beforeUnmount |
29
+ | $on/$off 事件总线 | 宿主暂接管保存事件,下一轮引入 scoped 事件机制 |
30
+ | 动态 import 模板路径 | 宿主 import.meta.glob 注入 loadViewsConfig |
31
+ | element-ui / vux | element-plus / vant |
32
+
33
+ 不是所有原有 config 配置都已实现:batchActions、topActions、string do 路由及复杂字段见迭代计划。不要将旧目录的 src 全量复制后假设已经兼容。
34
+
35
+ ## 路由与登录迁移
36
+
37
+ `ViewTabs` 接替旧版同名布局组件:按 route.path 去重、使用 meta.title、保留切换前的页面实例和 query/hash,关闭当前页优先选择右侧再选择左侧。
38
+ `meta.layout: 'main'` 或 `meta.tabs: false` 的页面独立展示;退出登录调用组件 ref 的 `resetTabs()` 清理页面。ref 还提供 `tabRemove(path)` 与 `clearTabs()`(保留当前页);旧 bus 的 tab-destroy 可转为调用 tabRemove。
39
+ 最后一个标签不显示关闭按钮;关闭标签销毁页面实例,刷新不恢复历史标签。组件通过 RouterView 渲染以支持路由 props,并为每个标签提供自己的 useRoute 上下文。
40
+
41
+ 新增 `createAuth({ http, endpoints })` 和 `createDjangoRouter({ routes, auth, history })`,无需引入 Vuex。
42
+ 认证沿用旧项目的 `auth/user/login/`(POST)、`auth/user/current/`(GET)、`auth/user/logout/`(GET),相对于 HTTP baseURL。
43
+ JWT 响应使用 `token.access`,Authorization 为 Bearer,cookie 名保留 `access_token`;无 token 时仍支持 Django session。
44
+ 登录页路径保留 `/auth/login/`,帐号记忆保留 `auth.username`,不保存密码。
45
+ 默认 hash 路由可避免现有 Django URLconf 增加 SPA fallback;需要 history 模式时传入 Vue Router history 实例。
46
+ 路由默认需要登录,公开页面设置 `meta.loginRequired: false`。认证失败(401/403)回跳登录,网络与服务器错误保留原错误。
47
+ `genModelRouters(apps, { list, create, edit })` 生成 `/<app>/<model>/`、`/<app>/<model>/add/` 和旧版编辑路径 `/<app>/<model>/:id/`;传单个组件仍可用,该组件通过 mode 区分页面。路由 props 提供 appModel/mode/id,编辑记录按路径各占一个 tab。模型动作、布局和权限菜单迁移尚未覆盖。
48
+ 真实演示启动:`VITE_REAL_API=true npm run dev -- --port 5173`,列表地址为 `/#/course/category/`,登录地址为 `/#/auth/login/`。
49
+
50
+ ## ModelTable 字段控件
51
+
52
+ 桌面和移动列表共用 `TableWidget`,`column-字段名` 插槽仍优先于默认渲染。
53
+ 自动支持 choices、布尔、数值千分位、percent、日期时间、外键、child.children 数组以及嵌套路径。
54
+ 日期时间当前显示本地完整时间,不复刻旧版 Date2Now 的相对时间文案。
55
+
56
+ `list.items` 可以配置字符串名称或字段对象:
57
+
58
+ ```js
59
+ list: {
60
+ items: [
61
+ 'name',
62
+ { name: 'budget', type: 'decimal', align: 'right' },
63
+ { name: 'cover', widget: 'Picture', imageRoot: '/media/' },
64
+ { name: 'detail', widget: 'JsonDisplay', items: ['name', 'status'] },
65
+ { name: 'name', formatter: (row, name, value) => value.toUpperCase() },
66
+ ],
67
+ }
68
+ ```
69
+
70
+ 字符串 widget 支持 ForeignKey、TrueFlag、ChoicesDisplay、Date2Now、Timestamp、
71
+ Picture/Image、Avatar、PictureGallery、Video、Html、JsonDisplay、ColorText、TooltipCell、Link/URL、Mobile、Email。
72
+ 对象 widget 使用 Vue 3 组件,接收整行 `value/modelValue`、`field`、`context: { row, $index }`。
73
+ 旧函数 widget 沿用 `(row, field)` 返回 HTML,Html 和函数输出经过 DOMPurify 清理;链接限制为安全协议。
74
+ formatter 沿用 `(row, fieldName, fieldValue)`,保留 0、false 和空字符串返回值。
75
+
76
+ `useFormWidget: true` 复用 Form 的字段控件,编辑时发出
77
+ `field-change: { row, field, value }`;不会自动修改记录或写入 API,宿主负责保存。
78
+ 支持 width、minWidth、align、fixed 和 showOverflowTooltip 列配置。
79
+ 旧版 subColumns、rows 和 headerWidget 尚未迁移,请继续使用自定义列插槽处理复杂单元格。
80
+
81
+ ## Layout Drawer 与 Actions
82
+
83
+ ModelTable 的“新增”默认在本地 Drawer 展示 ModelForm,不再跳转新建 tab。
84
+ 桌面默认 `createDrawerSize="66%"`,移动端 100%;`createDefaults` 覆盖 baseQueries 的默认字段值。
85
+ 成功保存后关闭抽屉、emit `created`(与 ModelForm 的 form-posted payload 一致)并刷新当前列表。
86
+ 失败时保留表单和错误。`create` 事件仍表示点击新增,不表示保存成功。
87
+ 已有宿主通过 `@create` 打开自定义页面时,请设置 `createMode="event"`,避免同时打开内置抽屉。
88
+ demo 编辑继续使用独立路由和 tab,直接访问已有 `/add/` 路由仍兼容。
89
+ 关联模型的新建抽屉保留关联默认值和多对多保存流程。
90
+
91
+ `Layout` 内置抽屉,页面组件使用 `useDrawer().open(options)` 或通过布局 ref 的
92
+ `openDrawer(options)` 打开,`closeDrawer()` 关闭。独立使用 `Drawer` 时,其 ref 提供
93
+ `open/onOpen/close`。不再依赖 Vue 2 的全局 bus `opendrawer`。
94
+
95
+ ```js
96
+ const drawer = useDrawer()
97
+ await drawer.open({
98
+ component: Editor,
99
+ context: { id: 1, title: '编辑记录', size: '50%' },
100
+ onDone: (result) => refresh(result),
101
+ })
102
+ ```
103
+
104
+ 抽屉内容组件 emit `done(result)` 后关闭并调用 onDone;取消关闭不调用 onDone。
105
+ 支持 title、size、direction、beforeClose;context 中 `$` 开头的键不传给内容组件。
106
+ 字符串组件由宿主提供 `loadDrawerView`,可用
107
+ `createDrawerViewLoader(import.meta.glob('./views/**/*.vue'))` 创建加载器。
108
+ 异步加载失败向调用方抛出并 emit error,旧请求不会覆盖后打开的抽屉。
109
+
110
+ `Actions` 保留原版 items/map/context/permissionFunction/size/trigger:
111
+ 字符串项从 map 取配置,嵌套数组项进入“更多”菜单,支持 label/title、icon、show(context)、
112
+ permission、disabled、confirm/notice 和异步 loading。函数 `do(context)` 执行业务操作;
113
+ 字符串或组件 `do` 打开抽屉,`drawer` 作为抽屉 context 默认值。
114
+ 确认函数签名为 `(action, context)`,返回 false 取消;执行失败 emit error,完成 emit done(result, action)。
115
+ 防止同名操作重复执行。权限函数只控制前端显示,后端仍须检查权限。
116
+
117
+ ```js
118
+ const actions = [
119
+ { name: 'refresh', label: '刷新', icon: 'refresh', do: () => refresh() },
120
+ [
121
+ {
122
+ name: 'edit',
123
+ label: '编辑',
124
+ do: 'course/course/Edit',
125
+ drawer: { title: '编辑', size: '50%' },
126
+ },
127
+ ],
128
+ ]
129
+ ```
130
+
131
+ 布局 props `actions/actionContext/actionMap/permissionFunction` 配置标题栏操作;
132
+ `#actions` 插槽可替换操作区域。布局转发 `action-done(result, action)` 和 `error`。
133
+ 其他位置也可直接使用 `<Actions>`;抽屉操作需要位于 Layout 子树中。
@@ -0,0 +1,25 @@
1
+ # 第 1 轮验收记录
2
+
3
+ 验收日期:2026-10-05。旧项目未修改;新目录提供独立框架库与本地演示。
4
+
5
+ ## 自动验证
6
+
7
+ - `npm test`:2 个测试文件,12 项全部通过。
8
+ - `npm run build`:ES 模块库与样式生成成功,Vue、Axios、qs、Element Plus、Vant 为外部依赖。
9
+ - `npm run build:demo`:演示生产构建成功。两套 UI 同时引用导致 JS chunk 超过 500 kB 的 Vite 提示;后续按端拆入口和样式。
10
+ - `npm pack --dry-run`:包入口和样式包含在发布文件中;未执行发布。
11
+ - 依赖通过 package-lock.json 固定;开发测试工具 @vue/test-utils 固定 2.4.6,以兼容当前 Node 24.12.0。
12
+
13
+ 覆盖:falsy 默认值、只读字段、注册表隔离、元数据并发缓存与重试、POST/PATCH、id=0、集合/详情动作、部署前缀、数组查询、Token 清除、字段/非字段/网络错误、两端表单、必填校验、列表编辑事件、请求返回顺序。
14
+
15
+ ## 浏览器验证
16
+
17
+ 本机没有 agent-browser CLI,按浏览器验证技能的验收目标使用 Codex 内置浏览器检查 `http://127.0.0.1:5173/`:
18
+
19
+ 1. 页面渲染项目列表、23 条数据与页码,未出现 Vite 错误覆盖层。
20
+ 2. 桌面新增输入已有名称,显示“项目名称已存在,请修改”。
21
+ 3. 新增“首轮验收项目”,搜索可读到 id=24、预算=0、启用=否。
22
+ 4. 切换 Vant 移动端,加载同一记录;编辑保存为“首轮验收项目 · 移动编辑”。
23
+ 5. 切回桌面,显示移动端修改的记录;浏览器捕获的 error/warn 日志为空。
24
+
25
+ 截图:[验收结果](iteration-01-preview.jpg)。演示仅内存 adapter 模拟 DRF,未验证真实服务器、认证与权限。后续必须选真实业务做第 2 轮契约验收。
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "vue3-django",
3
+ "version": "0.1.0",
4
+ "description": "DRF metadata-driven Vue 3 framework",
5
+ "type": "module",
6
+ "main": "./dist/vue3-django.js",
7
+ "exports": {
8
+ ".": "./dist/vue3-django.js",
9
+ "./style.css": "./dist/vue3-django.css",
10
+ "./src/*": "./src/*"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "src",
15
+ "docs"
16
+ ],
17
+ "scripts": {
18
+ "dev": "vite --host 127.0.0.1",
19
+ "build": "vite build",
20
+ "build:demo": "vite build --mode demo",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "format": "prettier --write .",
24
+ "format:check": "prettier --check ."
25
+ },
26
+ "peerDependencies": {
27
+ "element-plus": "^2.11.0",
28
+ "vant": "^4.9.0",
29
+ "vue": "^3.5.0"
30
+ },
31
+ "dependencies": {
32
+ "async-validator": "^4.2.5",
33
+ "axios": "^1.12.0",
34
+ "dompurify": "^3.4.16",
35
+ "qs": "^6.14.0",
36
+ "vue-router": "^4.6.4"
37
+ },
38
+ "devDependencies": {
39
+ "@vitejs/plugin-vue": "^6.0.0",
40
+ "@vue/test-utils": "2.4.6",
41
+ "element-plus": "^2.11.0",
42
+ "jsdom": "^26.1.0",
43
+ "prettier": "^3.9.9",
44
+ "vant": "^4.9.0",
45
+ "vite": "^7.1.0",
46
+ "vitest": "^3.2.0",
47
+ "vue": "^3.5.0"
48
+ },
49
+ "engines": {
50
+ "node": ">=22.12.0"
51
+ },
52
+ "license": "ISC"
53
+ }
@@ -0,0 +1,113 @@
1
+ <script setup>
2
+ import { computed } from 'vue'
3
+ import { ElInput, ElInputNumber, ElSwitch, ElSelect, ElOption, ElDatePicker } from 'element-plus'
4
+ import { Field as VanField, Switch as VanSwitch } from 'vant'
5
+ import { displayValue } from '../../core/metadata.js'
6
+ import ModelSelect from '../model/Select.vue'
7
+ const props = defineProps({
8
+ field: { type: Object, required: true },
9
+ modelValue: null,
10
+ mobile: Boolean,
11
+ })
12
+ const emit = defineEmits(['update:modelValue'])
13
+ const value = computed({ get: () => props.modelValue, set: (v) => emit('update:modelValue', v) })
14
+ const numberType = computed(() => ['integer', 'decimal', 'float'].includes(props.field.type))
15
+ function mobileInput(v) {
16
+ value.value = numberType.value ? (v === '' ? null : Number(v)) : v
17
+ }
18
+ </script>
19
+ <template>
20
+ <span v-if="field.read_only">{{ displayValue(field, value) }}</span>
21
+ <component
22
+ v-else-if="field.widget && typeof field.widget !== 'string'"
23
+ :is="field.widget"
24
+ v-model="value"
25
+ :field="field"
26
+ />
27
+ <ModelSelect
28
+ v-else-if="field.model || field.relateModel"
29
+ v-model="value"
30
+ :app-model="field.relateModel || field.model"
31
+ :field="field"
32
+ />
33
+ <template v-else-if="mobile">
34
+ <VanSwitch
35
+ v-if="field.type === 'boolean'"
36
+ v-model="value"
37
+ size="22px"
38
+ />
39
+ <select
40
+ v-else-if="field.choices"
41
+ v-model="value"
42
+ :multiple="field.multiple"
43
+ :aria-label="field.label || field.name"
44
+ >
45
+ <option
46
+ v-if="field.allow_null && !field.multiple"
47
+ :value="null"
48
+ >
49
+ 请选择
50
+ </option>
51
+ <option
52
+ v-for="choice in field.choices"
53
+ :key="String(choice.value)"
54
+ :value="choice.value"
55
+ >
56
+ {{ choice.display_name }}
57
+ </option>
58
+ </select>
59
+ <VanField
60
+ v-else
61
+ :model-value="value == null ? '' : String(value)"
62
+ :type="numberType ? 'number' : field.widget === 'textarea' ? 'textarea' : 'text'"
63
+ :placeholder="field.placeholder ?? field.help_text ?? '请输入'"
64
+ :aria-label="field.label || field.name"
65
+ @update:model-value="mobileInput"
66
+ />
67
+ </template>
68
+ <ElSelect
69
+ v-else-if="field.choices"
70
+ v-model="value"
71
+ :multiple="field.multiple"
72
+ :clearable="field.allow_null"
73
+ :placeholder="field.placeholder"
74
+ :aria-label="field.label || field.name"
75
+ style="width: 100%"
76
+ >
77
+ <ElOption
78
+ v-for="choice in field.choices"
79
+ :key="String(choice.value)"
80
+ :label="choice.display_name"
81
+ :value="choice.value"
82
+ />
83
+ </ElSelect>
84
+ <ElSwitch
85
+ v-else-if="field.type === 'boolean'"
86
+ v-model="value"
87
+ />
88
+ <ElInputNumber
89
+ v-else-if="numberType"
90
+ v-model="value"
91
+ :precision="field.type === 'integer' ? 0 : undefined"
92
+ :min="field.min_value"
93
+ :max="field.max_value"
94
+ :placeholder="field.placeholder"
95
+ :aria-label="field.label || field.name"
96
+ />
97
+ <ElDatePicker
98
+ v-else-if="['date', 'datetime'].includes(field.type)"
99
+ v-model="value"
100
+ :type="field.type"
101
+ :value-format="field.type === 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DDTHH:mm:ssZ'"
102
+ :placeholder="field.placeholder"
103
+ />
104
+ <ElInput
105
+ v-else
106
+ v-model="value"
107
+ :type="['textarea', 'password'].includes(field.widget) ? field.widget : 'text'"
108
+ :maxlength="field.max_length"
109
+ :autocomplete="field.autocomplete"
110
+ :placeholder="field.placeholder ?? field.help_text"
111
+ :aria-label="field.label || field.name"
112
+ />
113
+ </template>
@@ -0,0 +1,49 @@
1
+ export function defaultRuleType(field) {
2
+ if (field.multiple) return 'array'
3
+ if (field.choices?.length) return typeof field.choices[0].value
4
+ if (field.model || ['integer', 'decimal', 'float', 'number'].includes(field.type)) return 'number'
5
+ return field.type === 'boolean' ? 'boolean' : 'string'
6
+ }
7
+
8
+ export function defaultRules(field) {
9
+ const rules = []
10
+ if (field.required && !field.allow_null)
11
+ rules.push({ required: true, type: defaultRuleType(field), message: '不能为空' })
12
+ if (field.min_length != null)
13
+ rules.push({ min: field.min_length, message: `长度最小为${field.min_length}` })
14
+ if (field.max_length != null)
15
+ rules.push({ max: field.max_length, message: `长度最大为${field.max_length}` })
16
+ return rules
17
+ }
18
+
19
+ export function normalizeItems(items = []) {
20
+ return items.map((item) => {
21
+ const field = typeof item === 'string' ? { name: item } : { ...item }
22
+ const span =
23
+ typeof field.span === 'number'
24
+ ? { xs: field.span, sm: field.span, md: field.span, lg: field.span, xl: field.span }
25
+ : field.span || {}
26
+ return {
27
+ type: 'string',
28
+ ...field,
29
+ label: field.label ?? field.name,
30
+ rules: field.rules ?? defaultRules(field),
31
+ span: {
32
+ xs: 24,
33
+ sm: 24,
34
+ md: field.widget === 'textarea' ? 24 : 12,
35
+ lg: field.widget === 'textarea' ? 24 : 12,
36
+ xl: field.widget === 'textarea' ? 24 : 8,
37
+ ...span,
38
+ },
39
+ }
40
+ })
41
+ }
42
+
43
+ export function getItemRules(items) {
44
+ return Object.fromEntries(
45
+ items
46
+ .filter((field) => !field.hidden && !field.read_only && field.widget !== 'hidden')
47
+ .map((field) => [field.name, field.rules]),
48
+ )
49
+ }