@neatui/vue 5.1.4 → 5.1.6
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 +1 -1
- package/package.json +2 -2
- package/src/components/form/Input.vue +1 -1
- package/src/components/form/InputNumber.vue +2 -2
- package/src/components/form/InputRange.vue +1 -1
- package/src/components/form/Textarea.vue +1 -1
- package/src/components/loader/FormLoader/FormLoader.vue +10 -9
- package/src/components/loader/FormLoader/FormRender.vue +20 -9
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
|
|
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
|
+
"version": "5.1.6",
|
|
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.
|
|
39
|
+
"@neatui/css": "^5.1.6",
|
|
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",
|
|
@@ -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 };
|
|
@@ -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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
490
|
-
|
|
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
|
});
|