@neatui/vue 5.1.3 → 5.1.5

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
@@ -62,7 +62,7 @@ Vue 最低版本声明统一为 3.5.11,与已有 Pinia 3 的要求一致;同
62
62
 
63
63
  - 渲染结构精简:节点即元素,attrs 原样落在节点元素上。无标题的 Text / Html / Button 节点只输出内容元素本身;分组、数组内部的层级不再包根元素,子节点直接落在节点元素(无标题分组)或内容容器(有标题分组)里;纯文本不再包 span,不输出空的 style / ui-col。模板不输出说明注释,空内容、没有提示、没有 submit 时不留占位注释(客户端渲染)。依赖旧包裹层(`.flex-block > [ui-row]`、内容节点外层 div、`span`)的样式或测试需要调整。
64
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 时同样不加默认间距(与根元素规则一致)。
65
+ - `child` 可写成 `{ attrs, lists }`:attrs(可为函数)描述承载子节点的容器;容器用 ui-flex 布局时不加栅格和默认间距 `n-sm-sub`(5.1.5 前为 `n-sl-sub`),写了 class 时替换默认间距;无标题分组的节点元素就是容器,节点写了 class 时同样不加默认间距(与根元素规则一致)。
66
66
  - `media: false`:这一层不用栅格,自身不带 ui-col,分组 / 数组的下一层也不带 ui-row;ui-col 只在所在层有栅格时输出;`rows=""` 时根元素不再输出空的 ui-row。
67
67
  - 标题与内容均可递归组合;数组支持默认值、数量限制、增删排序和行上下文。
68
68
  - 新增 `setErrors`(回填服务端错误)、`isDirty / getChanges`(未保存改动)、`setBaseline`(设置重置基线)、`getData({ visibleOnly })`(去掉隐藏字段),`validate({ paths })` 支持局部校验。在同一对象上用 `Object.assign` 加载记录后调用 `setBaseline()`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neatui/vue",
3
- "version": "5.1.3",
3
+ "version": "5.1.5",
4
4
  "description": "basic component ui",
5
5
  "main": "./src/index.ts",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@
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.1.3",
39
+ "@neatui/css": "^5.1.5",
40
40
  "@rollup/plugin-commonjs": "^26.0.1",
41
41
  "@rollup/plugin-node-resolve": "^15.2.3",
42
42
  "@vue/compiler-sfc": "^3.4.30",
@@ -163,7 +163,7 @@
163
163
 
164
164
  // 校验
165
165
  watch(
166
- [() => state.interacted, () => props.modelValue],
166
+ [() => state.interacted, () => props.modelValue, () => props.tips, () => props.co],
167
167
  async () => {
168
168
  if (state.interacted) {
169
169
  const hasValue =
@@ -138,7 +138,7 @@
138
138
 
139
139
  // 校验
140
140
  watch(
141
- [() => state.interacted, () => state.value],
141
+ [() => state.interacted, () => state.value, () => props.tips, () => props.co],
142
142
  async () => {
143
143
  if (state.interacted) {
144
144
  // 提示文案
@@ -184,7 +184,7 @@
184
184
  tips = props.tips;
185
185
  }
186
186
  // 是否显示提示
187
- const ontips = verify === 'no' && tips ? 'show' : '';
187
+ const ontips = (verify === 'no' && tips) || props.tips ? 'show' : '';
188
188
  // 状态颜色
189
189
  const co = verify === 'no' ? 'co:' + type : props.co ? 'co:' + props.co : '';
190
190
  state.error = { tips, ontips, verify, co };
@@ -200,7 +200,7 @@
200
200
  tips = props.tips;
201
201
  }
202
202
  // 是否显示提示
203
- const ontips = verify === 'no' && tips ? 'show' : '';
203
+ const ontips = (verify === 'no' && tips) || props.tips ? 'show' : '';
204
204
  // 状态颜色
205
205
  const co = verify === 'no' ? 'co:' + type : props.co ? 'co:' + props.co : '';
206
206
  return { tips, ontips, verify, co };
@@ -344,7 +344,7 @@
344
344
 
345
345
  // 校验
346
346
  watch(
347
- [() => state.interacted, () => props.modelValue],
347
+ [() => state.interacted, () => props.modelValue, () => props.tips, () => props.co],
348
348
  async () => {
349
349
  if (state.interacted) {
350
350
  // 提示文案
@@ -5,7 +5,7 @@
5
5
  <FormRender v-if="isBareContent(col)" bare :col="col" :mode="mode" :interacted="touched" :ctx="context()" v-bind="nodeAttrs(col)" />
6
6
 
7
7
  <div v-else v-bind="nodeAttrs(col)" v-on="isGroup(col) || col._array ? eventsOf(col, context()) : {}">
8
- <div v-if="rowSlot(col)" class="flex-block" @focusout="col._touched = true">
8
+ <div v-if="rowSlot(col)" class="flex-fill" @focusout="col._touched = true">
9
9
  <slot :name="rowSlot(col)" v-bind="fieldScope(col)"></slot>
10
10
  <FieldMessage :col="col" :mode="mode" />
11
11
  </div>
@@ -29,8 +29,9 @@
29
29
  :interacted="touched"
30
30
  :debug="debug"
31
31
  rows=""
32
- class="w-full"
33
- ui-flex="row lm"
32
+ :class="isVertical(col) ? 'w-full' : 'flex-fixed nr-sm'"
33
+ :style="isVertical(col) ? undefined : labelStyle(col)"
34
+ :ui-flex="isVertical(col) ? 'row lm' : 'row rm'"
34
35
  />
35
36
  <h6 v-else-if="labelText(col.label) && isVertical(col)" :style="showStar(col) ? 'text-indent: -1em' : undefined" :class="labelClass || undefined" v-bind="labelAttrs(col.label)">
36
37
  <slot v-if="labelSlot(col)" :name="labelSlot(col)" :col="col" :node="col" :data="col._data" :text="formatLabel(col)"></slot>
@@ -49,7 +50,7 @@
49
50
  </FormLoader>
50
51
  </div>
51
52
 
52
- <div v-else-if="col._array" class="flex-block">
53
+ <div v-else-if="col._array" class="flex-fill">
53
54
  <div v-if="listOf(col).length" ui-row>
54
55
  <div v-for="(row, i) in listOf(col)" :key="rowKey(row)" v-bind="rowAttrs(col)">
55
56
  <div v-if="hasActions(col)" v-bind="rowBox(col)">
@@ -541,7 +542,7 @@
541
542
  const attrs = useAttrs();
542
543
  const slots = useSlots();
543
544
  // 外部写了 class 时去掉默认根 class,由外部接管
544
- const rootClass = computed(() => (attrs.class ? undefined : 'n-sl-sub'));
545
+ const rootClass = computed(() => (attrs.class ? undefined : 'n-sm-sub'));
545
546
 
546
547
  // 入参
547
548
  const props: any = defineProps({
@@ -769,7 +770,7 @@
769
770
  const { 'ui-row': row, ...own } = childAttrsOf(col);
770
771
  const layout = own['ui-flex'] ?? flex;
771
772
  const rows = col.media === false || layout ? '' : String(row ?? col.attrs?.['ui-row'] ?? props.rows ?? '');
772
- return { rows, attrs: { ...own, 'ui-row': rows || undefined, class: 'class' in own ? own.class : layout || styled ? undefined : 'n-sl-sub' } };
773
+ return { rows, attrs: { ...own, 'ui-row': rows || undefined, class: 'class' in own ? own.class : layout || styled ? undefined : 'n-sm-sub' } };
773
774
  };
774
775
  // 节点元素;无标题分组的节点元素同时是下一层容器
775
776
  const nodeAttrs = (col: any) => {
@@ -797,7 +798,7 @@
797
798
  // 有标题分组的子节点容器
798
799
  const boxAttrs = (col: any) => {
799
800
  const { attrs } = levelOf(col);
800
- return { ...attrs, class: joinClass('flex-block', attrs.class) };
801
+ return { ...attrs, class: joinClass('flex-fill', attrs.class) };
801
802
  };
802
803
  const levelProps = () => ({
803
804
  bare: true,
@@ -832,11 +833,11 @@
832
833
  } else if (own['ui-flex'] === undefined) {
833
834
  // 没有操作按钮时行元素本身承载这一行的字段
834
835
  out['ui-row'] = rowRows(col) || undefined;
835
- out.class = joinClass('mb-xs', own.class ?? 'n-sl-sub');
836
+ out.class = joinClass('mb-xs', own.class ?? 'n-sm-sub');
836
837
  }
837
838
  return out;
838
839
  };
839
- const rowBox = (col: any) => ({ class: 'flex-block n-sl-sub', 'ui-row': rowRows(col) || undefined });
840
+ const rowBox = (col: any) => ({ class: 'flex-fill n-sm-sub', 'ui-row': rowRows(col) || undefined });
840
841
  const rowProps = (col: any, row: any, i: number) => ({
841
842
  ...levelProps(),
842
843
  form: col._array.child,
@@ -2,7 +2,7 @@
2
2
  <button v-if="bare && col.model[0] === 'Button'" type="button" ui-btn="@a s" v-bind="contentAttrs" v-on="events"><template v-if="plain !== null">{{ plain }}</template><FieldExtra v-else :value="displayOf(col, ctx)" :trusted="trusted" /></button>
3
3
  <div v-else-if="bare && col.model[0] === 'Html' && trusted" v-bind="contentAttrs" v-on="events" v-html="displayOf(col, ctx)"></div>
4
4
  <div v-else-if="bare" v-bind="contentAttrs" v-on="events"><template v-if="plain !== null">{{ plain }}</template><FieldExtra v-else :value="displayOf(col, ctx)" :trusted="trusted" /></div>
5
- <div v-else ref="root" class="flex-block pr" @focusout="col._touched = true">
5
+ <div v-else ref="root" class="flex-fill pr" @focusout="col._touched = true">
6
6
  <slot v-if="custom" />
7
7
  <button v-else-if="col.model[0] === 'Button'" type="button" ui-btn="@a s" v-bind="contentAttrs" v-on="events"><template v-if="plain !== null">{{ plain }}</template><FieldExtra v-else :value="displayOf(col, ctx)" :trusted="trusted" /></button>
8
8
  <div v-else-if="col.model[0] === 'Html' && trusted" v-bind="contentAttrs" v-on="events" v-html="displayOf(col, ctx)"></div>
@@ -17,7 +17,7 @@
17
17
  v-on="controlEvents"
18
18
  @update:modelValue="update"
19
19
  />
20
- <FieldMessage :col="col" :mode="mode" />
20
+ <FieldMessage :col="col" :mode="mode" :native="nativeTips" />
21
21
  </div>
22
22
  </template>
23
23
  <script lang="ts">
@@ -278,14 +278,16 @@
278
278
  };
279
279
 
280
280
  export const FieldMessage = defineComponent({
281
- props: ['col', 'mode'],
281
+ // native:控件自己用 ui-form-tips 显示错误,这里只留隐藏文本给读屏关联
282
+ props: ['col', 'mode', 'native'],
282
283
  // 没有内容时返回空数组:客户端渲染不留占位注释
283
284
  setup: (props) => () => props.mode !== 'view' ? [
284
285
  props.col.model?.[1]?.verify && props.col._checked
285
286
  ? h('i', { 'ui-form-verify': props.col._error ? 'no' : isEmpty(props.col._data?.[props.col.field]) ? '' : 'ok', 'aria-hidden': 'true' }) : null,
286
287
  props.col._error ? h('p', { id: `${props.col._fid}-error`, 'data-field-error': props.col.field,
287
- role: props.col._error.type === 'warn' ? 'status' : 'alert',
288
- class: `mt-xs ${props.col._error.type === 'warn' ? 'co-warn' : 'co-risk'}` }, props.col._error.msg) : null,
288
+ role: props.native ? undefined : props.col._error.type === 'warn' ? 'status' : 'alert',
289
+ hidden: props.native || undefined,
290
+ class: props.native ? undefined : `mt-xs ${props.col._error.type === 'warn' ? 'co-warn' : 'co-risk'}` }, props.col._error.msg) : null,
289
291
  ].filter(Boolean) : [],
290
292
  });
291
293
 
@@ -470,10 +472,18 @@
470
472
  };
471
473
 
472
474
  // FormLoader 自己的参数只传给声明了它们的控件,避免落到 Radio、Gallery 等控件的 DOM 上。
473
- const formOnly = ['interacted', 'rules', 'verify', 'co', 'format', 'empty'];
475
+ const formOnly = ['interacted', 'rules', 'verify', 'co', 'tips', 'format', 'empty'];
476
+ const declaresOf = (entry: any, key: string) => {
477
+ const declared: any = entry?.component?.props;
478
+ return !declared || (isArray(declared) ? declared.includes(key) : Object.prototype.hasOwnProperty.call(declared, key));
479
+ };
480
+ // 控件声明了 tips + interacted(Input 及包着 Input 的选择、日期控件)就用它原生的 ui-form-tips 气泡显示错误,不另起一行;
481
+ // Upload 等的 tips 是悬停说明,不算
482
+ const nativeTips = computed(() => !props.custom && !!(props.entry?.component as any)?.props && declaresOf(props.entry, 'tips') && declaresOf(props.entry, 'interacted'));
474
483
  const controlProps = computed(() => {
475
484
  const { col, entry, mode } = props;
476
485
  const options = col.model?.[1] || {};
486
+ const error = mode !== 'view' && nativeTips.value ? col._error : null;
477
487
  const merged: Record<string, any> = {
478
488
  interacted: props.interacted,
479
489
  options: entry?.options ? col.enums : undefined,
@@ -481,13 +491,14 @@
481
491
  rules: [],
482
492
  verify: false,
483
493
  co: col._error?.type || options.co,
494
+ tips: error?.msg || options.tips,
495
+ ...(error ? { interacted: true } : {}),
484
496
  readonly: mode === 'view' ? true : options.readonly,
485
497
  disabled: mode === 'view' ? true : options.disabled,
486
498
  };
487
- const declared: any = (entry?.component as any)?.props;
488
499
  // 没有声明 props 的自定义注册控件可能通过 $attrs 读取这些参数,保持透传。
489
- const declares = (key: string) => !declared || (isArray(declared) ? declared.includes(key) : Object.prototype.hasOwnProperty.call(declared, key));
490
- formOnly.forEach((key) => { if (!declares(key)) delete merged[key]; });
500
+ formOnly.forEach((key) => { if (!declaresOf(entry, key)) delete merged[key]; });
501
+ if (merged.tips === undefined) delete merged.tips;
491
502
  if (merged.options === undefined) delete merged.options;
492
503
  return merged;
493
504
  });