@neatui/vue 5.0.0 → 5.1.1

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.
package/MIGRATION-5.0.md CHANGED
@@ -16,11 +16,9 @@ Vue 最低版本声明统一为 3.5.11,与已有 Pinia 3 的要求一致;同
16
16
 
17
17
  | 旧配置 | 5.0 写法 |
18
18
  | --- | --- |
19
- | 字段的 `slot: 'custom'` | `model: 'Custom'` 配合 `#Custom`,或使用同名字段插槽 |
20
- | 字段级 `colon` | 用 `label` 节点树精确输出文字;表单级 `colon` 继续支持 |
21
19
  | 只读取 `col` 的自定义插槽 | `col` 继续提供;通过 `bind / update` 接入双向绑定、转换和只读状态 |
22
20
 
23
- 例如将 `{ field: 'name', slot: 'custom' }` 改为 `{ field: 'name', model: 'Custom' }`:
21
+ 字段级 `slot` 和 `colon` 继续支持。新写法可以用 `model` 配合同名插槽,并通过 `bind` 接入双向绑定:
24
22
 
25
23
  ```vue
26
24
  <FormLoader :form="form" :data="data">
@@ -32,6 +30,12 @@ Vue 最低版本声明统一为 3.5.11,与已有 Pinia 3 的要求一致;同
32
30
 
33
31
  旧联动签名 `(data, item, route, router)` 继续可用。第三参数的 `path` 始终是网址路径,`formPath` 是表单数据路径;它还提供 `root / data / index / array / route / router`。事件、值转换和插槽里的 `ctx.path` 仍是表单数据路径。
34
32
 
33
+ `clear` 行为有变化:4.8.2 隐藏即丢弃原值,现在默认暂存、重新显示时恢复,避免控制字段与被控字段同时赋值(如加载记录)时联动尚未完成就清掉数据。需要旧行为时返回 `{ value, restore: false }`。
34
+
35
+ 字段 id:不传 `idx` 时,4.8.2 生成 `form-field-<字段>`,同页多个表单会冲突;现在自动生成唯一前缀。依赖固定 id 的样式或测试请显式传 `idx`。
36
+
37
+ 联动函数按实际读取的字段追踪,只有读到的字段变化才重新执行(4.8.2 为同层任意字段变化都执行);函数拿到的 data 可以改写,不影响表单数据。
38
+
35
39
  `model[1].rules` 继续覆盖节点 `rules`,空数组表示关闭该节点校验。`verify: true` 显示统一校验图标;不会再同时触发控件和 FormLoader 两套校验。
36
40
 
37
41
  ## 从桌面旧组件或中间扩展配置迁移
@@ -48,7 +52,7 @@ Vue 最低版本声明统一为 3.5.11,与已有 Pinia 3 的要求一致;同
48
52
  | `transverse` | `label` 属性中的 `style.width: 'auto'` |
49
53
  | `before / prefix / suffix / extra / childAttrs` | 使用 `child` 内容节点与 `attrs` 容器 |
50
54
  | 节点 `rows` | `attrs: { 'ui-row': '…' }` |
51
- | `restore` | `clear: () => ({ value: 是否清空, restore: true })` |
55
+ | `restore` | 默认即暂存并在重新显示时恢复;需要丢弃时写 `clear: () => ({ value: 是否清空, restore: false })` |
52
56
  | `array.media` | `array.attrs: { 'ui-col': '…' }` |
53
57
  | `every-label` 插槽 | `label` 插槽,接收 `col / node / data / text` |
54
58
 
@@ -56,11 +60,18 @@ Vue 最低版本声明统一为 3.5.11,与已有 Pinia 3 的要求一致;同
56
60
 
57
61
  ## 新增与行为修复
58
62
 
63
+ - 渲染结构精简:节点即元素,attrs 原样落在节点元素上。无标题的 Text / Html / Button 节点只输出内容元素本身;分组、数组内部的层级不再包根元素,子节点直接落在节点元素(无标题分组)或内容容器(有标题分组)里;纯文本不再包 span,不输出空的 style / ui-col。模板不输出说明注释,空内容、没有提示、没有 submit 时不留占位注释(客户端渲染)。依赖旧包裹层(`.flex-block > [ui-row]`、内容节点外层 div、`span`)的样式或测试需要调整。
64
+ - id 只给字段:`表单前缀-数据路径`(`.` 换成 `-`),如 `apply-contact-phone`、`apply-members-0-name`;前缀为 `idx`,未传时由 useId 生成 6 位短串。分组、容器、内容节点不再输出 id,需要时在 attrs 里写。旧的 `form-<idx>-g0-field-x` / `group-0` 形式不再生成,错误提示 id 为 `<字段 id>-error`。
65
+ - `child` 可写成 `{ attrs, lists }`:attrs(可为函数)描述承载子节点的容器;容器用 ui-flex 布局时不加栅格和默认间距 `n-sl-sub`,写了 class 时替换默认间距;无标题分组的节点元素就是容器,节点写了 class 时同样不加默认间距(与根元素规则一致)。
66
+ - `media: false`:这一层不用栅格,自身不带 ui-col,分组 / 数组的下一层也不带 ui-row;ui-col 只在所在层有栅格时输出;`rows=""` 时根元素不再输出空的 ui-row。
59
67
  - 标题与内容均可递归组合;数组支持默认值、数量限制、增删排序和行上下文。
68
+ - 新增 `setErrors`(回填服务端错误)、`isDirty / getChanges`(未保存改动)、`setBaseline`(设置重置基线)、`getData({ visibleOnly })`(去掉隐藏字段),`validate({ paths })` 支持局部校验。在同一对象上用 `Object.assign` 加载记录后调用 `setBaseline()`。
60
69
  - `validate()` 等待异步联动并检查最新数据;`reset()` 恢复当前数据对象的初始状态;`clearValidate()` 取消未完成校验。
61
70
  - 换一条记录时替换 `data` 对象,建立新的重置基线;对同一对象 `Object.assign` 视为编辑。
62
71
  - `paths` 缺失时补全对象,已有非对象路径明确报错。
63
- - 增加 Radio、Gallery(别名 SelectImg)、RichTextInput;TimePicker 使用浏览器原生时间输入并校验范围与步长。
64
- - Gallery 提供通用图片选择与搜索事件,不包含旧业务素材库的接口、分页和后台管理。RichTextInput 默认支持文本格式、列表、链接和撤销重做,未配置图片上传或表格扩展。
72
+ - 增加 Radio、Gallery(别名 SelectImg),FormLoader 已内置。NeatUI 不提供富文本控件,业务编辑器用同名字段插槽接入 `<MyEditor v-bind="bind" />`。TimePicker 使用浏览器原生时间输入并校验范围与步长。
73
+ - Select 不再清空不在当前选项中的值:只有上级切换使原本有效的值失效(如省市级联)时才清空;加载记录、异步或分页选项尚未包含当前值时保留,并显示原始值。4.x 会直接清空为 null。
74
+ - 类型:`form` 标注为 `FormItem[]`;`FormItem.value` 的 `get / set` 参数可正确推断;新增 `FormSlotScope` 描述字段与控件插槽参数。
75
+ - Gallery 提供通用图片选择与搜索事件,不包含旧业务素材库的接口、分页和后台管理。
65
76
 
66
77
  示例与完整 API 见仓库的 `web/app/pages/components/formloader.vue`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neatui/vue",
3
- "version": "5.0.0",
3
+ "version": "5.1.1",
4
4
  "description": "basic component ui",
5
5
  "main": "./src/index.ts",
6
6
  "license": "MIT",
@@ -36,13 +36,9 @@
36
36
  "@fekit/scrollto": "^3.0.3",
37
37
  "@fekit/toast": "^2.7.2",
38
38
  "@fekit/utils": "^6.0.0",
39
- "@neatui/css": "^5.0.0",
39
+ "@neatui/css": "^5.1.1",
40
40
  "@rollup/plugin-commonjs": "^26.0.1",
41
41
  "@rollup/plugin-node-resolve": "^15.2.3",
42
- "@tiptap/core": "^3.31.3",
43
- "@tiptap/pm": "^3.31.3",
44
- "@tiptap/starter-kit": "^3.31.3",
45
- "@tiptap/vue-3": "^3.31.3",
46
42
  "@vue/compiler-sfc": "^3.4.30",
47
43
  "compressorjs": "^1.2.1",
48
44
  "echarts": "^5.5.0",
@@ -316,7 +316,7 @@
316
316
  .side-preview {
317
317
  min-width: 10rem;
318
318
  height: 100%;
319
- box-shadow: -0.25em 0 0.5em rgba(0, 0, 0, 0.05);
319
+ box-shadow: -0.25em 0 0.5em var(--co-deep-o-mm);
320
320
  pointer-events: auto;
321
321
 
322
322
  &[ui-card~='@a'][ui-card~=':max'] {
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- :ui-form="`${theme} sz:${sz} ${state.error.co} type:input ${block ? ' :block' : ''} ${disabled ? 'disabled' : ''} ${readonly ? 'readonly' : ''} ${preview ? 'preview' : ''}`"
3
+ :ui-form="[theme, sz && `sz:${sz}`, state.error.co, 'type:input', block && ':block', disabled && 'disabled', readonly && 'readonly', preview && 'preview'].filter(Boolean).join(' ')"
4
4
  @click="emits('click', $event)"
5
5
  >
6
6
  <div v-if="state.interacted" :ui-form-tips="state.error.ontips">{{ state.error.tips }}</div>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- :ui-form="`${theme} sz:${sz} ${state.error.co} type:input ${block ? ' :block' : ''} ${disabled ? 'disabled' : ''} ${readonly ? 'readonly' : ''}`"
3
+ :ui-form="[theme, sz && `sz:${sz}`, state.error.co, 'type:input', block && ':block', disabled && 'disabled', readonly && 'readonly'].filter(Boolean).join(' ')"
4
4
  @click="emits('click', $event)"
5
5
  >
6
6
  <div :ui-form-tips="state.error.ontips">{{ state.error.tips }}</div>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :ui-form="`${theme} sz:${sz} ${state.co} type:input ${block ? ' :block' : ''} ${disabled ? 'disabled' : ''} ${readonly ? 'readonly' : ''}`" @click="emits('click', $event)">
2
+ <div :ui-form="[theme, sz && `sz:${sz}`, state.co, 'type:input', block && ':block', disabled && 'disabled', readonly && 'readonly'].filter(Boolean).join(' ')" @click="emits('click', $event)">
3
3
  <div v-if="ready" :ui-form-tips="state.ontips">{{ state.tips }}</div>
4
4
  <div ui-form-prefix="">
5
5
  <template v-for="(item, idx) in prefix" :key="idx">
@@ -235,6 +235,11 @@
235
235
  };
236
236
  }
237
237
 
238
+ // 值不在当前选项中(选项未加载、分页或远程搜索)时显示原始值,避免看起来像未选择
239
+ if (props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== '') {
240
+ return { label: String(props.modelValue), value: props.modelValue };
241
+ }
242
+
238
243
  return {};
239
244
  }),
240
245
  displayLabel: computed((): string => {
@@ -285,35 +290,24 @@
285
290
  { immediate: true },
286
291
  );
287
292
 
288
- // 监听并验证modelValue的有效性
293
+ // 值是否在选项中(含 all 选项)
294
+ const inOptions = (value: any, options: any[] | undefined, allOption: any) =>
295
+ (allOption && value === allOption.value) || !!options?.some((option: any) => `${option.value}` === `${value}`);
296
+
297
+ // 只在选项变化使原本有效的值失效时清空(如级联切换上级后)。
298
+ // 值由外部写入(加载记录)、选项异步未到、分页或远程搜索尚未包含当前值时保留,避免丢失数据。
289
299
  watch(
290
300
  [() => props.modelValue, () => props.options, () => props.all, () => props.other],
291
- ([value, options, allOption, otherConfig]) => {
301
+ ([value, options, allOption, otherConfig], [previousValue, previousOptions]) => {
292
302
  // 如果配置了 other,则允许任意值(可能是用户自定义输入)
293
- if (otherConfig) {
294
- return;
295
- }
296
-
297
- // 如果有值但不在有效选项中,清空该值
298
- if (value != null && value !== '' && options?.length > 0) {
299
- let isValid = false;
300
-
301
- // 检查是否是 all 选项
302
- if (allOption && value === allOption.value) {
303
- isValid = true;
304
- } else {
305
- // 检查是否在普通选项中
306
- isValid = options.some((option: any) => `${option.value}` === `${value}`);
307
- }
308
-
309
- if (!isValid) {
310
- nextTick(() => {
311
- change(null);
312
- });
313
- }
303
+ if (otherConfig || value == null || value === '' || !options?.length) return;
304
+ const invalidated = value === previousValue && options !== previousOptions && inOptions(value, previousOptions, allOption) && !inOptions(value, options, allOption);
305
+ if (invalidated) {
306
+ nextTick(() => {
307
+ change(null);
308
+ });
314
309
  }
315
310
  },
316
- { immediate: true },
317
311
  );
318
312
 
319
313
  // 选择
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- :ui-form="`${theme} sz:${sz} ${state.error.co} type:textarea ${isAutoHeight ? 'auto-height' : ''} ${block ? ' :block' : ''} ${disabled ? 'disabled' : ''} ${readonly ? 'readonly' : ''}`"
3
+ :ui-form="[theme, sz && `sz:${sz}`, state.error.co, 'type:textarea', isAutoHeight && 'auto-height', block && ':block', disabled && 'disabled', readonly && 'readonly'].filter(Boolean).join(' ')"
4
4
  @click="emits('click', $event)"
5
5
  >
6
6
  <div :ui-form-tips="state.error.ontips">{{ state.error.tips }}</div>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :ui-form="`${theme} type:input ${disabled ? 'disabled' : ''}`">
2
+ <div :ui-form="[theme, 'type:input', disabled && 'disabled'].filter(Boolean).join(' ')">
3
3
  <input type="time" :value="modelValue || ''" :min="min" :max="max" :step="step" :disabled="disabled" :readonly="readonly"
4
4
  :aria-invalid="error ? true : undefined" :aria-label="label" :required="required" @input="update" @change="change" @blur="emit('blur', $event)" @focus="emit('focus', $event)" />
5
5
  <span v-if="verify && error" role="alert" class="co-risk">{{ error }}</span>
@@ -1,6 +1,5 @@
1
1
  import Radio from './Radio.vue';
2
2
  import Gallery from './Gallery.vue';
3
- import RichTextInput from './RichTextInput.vue';
4
3
  import Input from './Input.vue';
5
4
  import InputRange from './InputRange.vue';
6
5
  import Switch from './Switch.vue';
@@ -36,7 +35,6 @@ export {
36
35
  Radio,
37
36
  Gallery,
38
37
  Gallery as SelectImg,
39
- RichTextInput,
40
38
  Input,
41
39
  InputRange,
42
40
  Switch,