@neatui/vue 5.5.1 → 5.5.2
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neatui/vue",
|
|
3
|
-
"version": "5.5.
|
|
3
|
+
"version": "5.5.2",
|
|
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.5.
|
|
39
|
+
"@neatui/css": "^5.5.2",
|
|
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",
|
|
@@ -29,16 +29,16 @@
|
|
|
29
29
|
:interacted="touched"
|
|
30
30
|
:debug="debug"
|
|
31
31
|
rows=""
|
|
32
|
-
:class="isVertical(col) ? 'w-full' : 'flex-fixed nr-sm'"
|
|
32
|
+
:class="[isVertical(col) ? 'w-full' : 'flex-fixed nr-sm', titleLayer(col)]"
|
|
33
33
|
:style="isVertical(col) ? undefined : labelStyle(col)"
|
|
34
34
|
:ui-flex="isVertical(col) ? 'row lm' : 'row rm'"
|
|
35
35
|
/>
|
|
36
|
-
<h6 v-else-if="labelText(col.label) && isVertical(col)" :style="showStar(col) ? 'text-indent: -1em' : undefined" :class="labelClass
|
|
36
|
+
<h6 v-else-if="labelText(col.label) && isVertical(col)" :style="showStar(col) ? 'text-indent: -1em' : undefined" :class="[labelClass, titleLayer(col)]" v-bind="labelAttrs(col.label)">
|
|
37
37
|
<slot v-if="labelSlot(col)" :name="labelSlot(col)" :col="col" :node="col" :data="col._data" :text="formatLabel(col)"></slot>
|
|
38
38
|
<template v-else-if="labelText(col.label) !== '*' && showStar(col)"><b class="co-risk pr" style="left: 0.1em">﹡</b>{{ formatLabel(col) }}</template>
|
|
39
39
|
<template v-else-if="labelText(col.label) !== '*'">{{ formatLabel(col) }}</template>
|
|
40
40
|
</h6>
|
|
41
|
-
<p v-else-if="labelText(col.label)" :class="[labelClass, clh({ ...(col.model[1] || {}) })]" :style="labelStyle(col)" class="ar nr-sm flex-fixed o-ls" v-bind="labelAttrs(col.label)">
|
|
41
|
+
<p v-else-if="labelText(col.label)" :class="[labelClass, clh({ ...(col.model[1] || {}) }), titleLayer(col)]" :style="labelStyle(col)" class="ar nr-sm flex-fixed o-ls" v-bind="labelAttrs(col.label)">
|
|
42
42
|
<slot v-if="labelSlot(col)" :name="labelSlot(col)" :col="col" :node="col" :data="col._data" :text="formatLabel(col)"></slot>
|
|
43
43
|
<template v-else-if="labelText(col.label) !== '*' && showStar(col)"><b class="co-risk">﹡</b>{{ formatLabel(col) }}</template>
|
|
44
44
|
<template v-else-if="labelText(col.label) !== '*'">{{ formatLabel(col) }}</template>
|
|
@@ -263,14 +263,19 @@
|
|
|
263
263
|
const checkRules = async (value: any, rules: FormRule[] = []) => {
|
|
264
264
|
if (!isArray(rules)) return null;
|
|
265
265
|
if (!isEmpty(value)) {
|
|
266
|
+
// warn 不阻断:记下第一条警告后继续检查,遇到阻断规则立即返回
|
|
267
|
+
let warning: { msg: string; type: string } | null = null;
|
|
266
268
|
for (const rule of rules) {
|
|
267
269
|
if (rule && isFunction(rule.validator) && !(await rule.validator!(value))) {
|
|
268
|
-
|
|
270
|
+
const type = rule.type || 'risk';
|
|
271
|
+
if (type !== 'warn') return { msg: rule.msg || '', type };
|
|
272
|
+
warning ||= { msg: rule.msg || '', type };
|
|
269
273
|
}
|
|
270
274
|
}
|
|
271
|
-
return
|
|
275
|
+
return warning;
|
|
272
276
|
}
|
|
273
|
-
const
|
|
277
|
+
const required = rules.filter((item) => item?.required);
|
|
278
|
+
const rule = required.find((item) => (item.type || 'risk') !== 'warn') || required[0];
|
|
274
279
|
return rule ? { msg: rule.msg || '', type: rule.type || 'risk' } : null;
|
|
275
280
|
};
|
|
276
281
|
|
|
@@ -627,6 +632,7 @@
|
|
|
627
632
|
});
|
|
628
633
|
|
|
629
634
|
// 各层级共享:Label 宽度、校验状态、隐藏字段暂存、已挂载的表单实例
|
|
635
|
+
type ServerError = { msg: string; type: string; input: any };
|
|
630
636
|
interface Shared {
|
|
631
637
|
state: { lw: number; validated: boolean };
|
|
632
638
|
cache: ClearCache;
|
|
@@ -640,8 +646,9 @@
|
|
|
640
646
|
validations: WeakMap<object, { input: any; epoch: number; pending: boolean; result: Promise<any> }>;
|
|
641
647
|
// 根数据任意变化时递增,供 watch: true 的联动共用
|
|
642
648
|
dataVersion: ShallowRef<number>;
|
|
643
|
-
//
|
|
644
|
-
|
|
649
|
+
// 外部回填的错误(如服务端校验),按「所在记录 → 行内路径」保存;字段值变化后自动失效。
|
|
650
|
+
// 外层 WeakMap 以记录对象为键,记录被删除并回收后错误随之释放;清空时整体替换,shallowRef 负责通知已渲染字段
|
|
651
|
+
serverErrors: ShallowRef<WeakMap<object, Map<string, ServerError>>>;
|
|
645
652
|
}
|
|
646
653
|
const FORM_SHARED = Symbol.for('neatui:form-loader');
|
|
647
654
|
const shared: Shared = (props.root && inject<Shared | null>(FORM_SHARED, null)) || {
|
|
@@ -656,7 +663,7 @@
|
|
|
656
663
|
cancelChecks: new Set(),
|
|
657
664
|
validations: new WeakMap(),
|
|
658
665
|
dataVersion: shallowRef(0),
|
|
659
|
-
serverErrors: reactive(new
|
|
666
|
+
serverErrors: shallowRef(reactive(new WeakMap())),
|
|
660
667
|
};
|
|
661
668
|
provide(FORM_SHARED, shared);
|
|
662
669
|
|
|
@@ -795,6 +802,8 @@
|
|
|
795
802
|
if (isCleared(col)) out.style = joinClass(own.style, { display: 'none' });
|
|
796
803
|
return out;
|
|
797
804
|
};
|
|
805
|
+
// 分组内容区的栅格负边距会伸进标题区域,标题提一层以保证按钮可点
|
|
806
|
+
const titleLayer = (col: any) => (isGroup(col) || col._array ? 'pr z-xs' : undefined);
|
|
798
807
|
// 有标题分组的子节点容器
|
|
799
808
|
const boxAttrs = (col: any) => {
|
|
800
809
|
const { attrs } = levelOf(col);
|
|
@@ -1024,7 +1033,7 @@
|
|
|
1024
1033
|
}
|
|
1025
1034
|
|
|
1026
1035
|
// 添加ID
|
|
1027
|
-
item._fid =
|
|
1036
|
+
item._fid = fieldIdOf(item);
|
|
1028
1037
|
|
|
1029
1038
|
// 数据路径处理
|
|
1030
1039
|
item._data = resolvePath(props.data, item.paths);
|
|
@@ -1042,6 +1051,9 @@
|
|
|
1042
1051
|
if (props.debug) console.debug(`[FormLoader] ${props.path || 'root'}`, deepcopy(toRaw(state.form)));
|
|
1043
1052
|
};
|
|
1044
1053
|
|
|
1054
|
+
// 字段 ID 随数据路径变化;行实例复用时由下方监听单独刷新,不触发重建
|
|
1055
|
+
const fieldIdOf = (item: any) => (item.field ? [baseIdx.value, fieldPath(item).replace(/\./g, '-')].filter(Boolean).join('-') : '');
|
|
1056
|
+
|
|
1045
1057
|
// 调试信息
|
|
1046
1058
|
const debugInfo = (col: any = {}) => [fieldPath(col) || '(group)', col.model?.[0] || (isGroup(col) ? 'child' : col._array ? 'array' : 'Input')].join(' · ');
|
|
1047
1059
|
watch(
|
|
@@ -1052,9 +1064,16 @@
|
|
|
1052
1064
|
{ deep: true, immediate: true },
|
|
1053
1065
|
);
|
|
1054
1066
|
watch([() => props.data, () => props.idx], ([data], [previous]) => {
|
|
1055
|
-
if (!props.root && data !== previous)
|
|
1067
|
+
if (!props.root && data !== previous) {
|
|
1068
|
+
captureInitial();
|
|
1069
|
+
clearValidate(); // 换了一条记录:上一条的服务端错误和校验状态不再适用
|
|
1070
|
+
}
|
|
1056
1071
|
rebuild(props.form);
|
|
1057
1072
|
});
|
|
1073
|
+
// 数组行增删、排序后行实例被复用,只刷新字段 ID,保留校验状态和联动
|
|
1074
|
+
watch(() => props.path, () => {
|
|
1075
|
+
state.form.forEach((item: any) => { item._fid = fieldIdOf(item); });
|
|
1076
|
+
});
|
|
1058
1077
|
|
|
1059
1078
|
// 隐藏的字段清空数据并暂存,重新显示时还原(restore: false 时只清空)
|
|
1060
1079
|
// 只在「隐藏 → 显示」的那一次和首次运行时还原,避免和分组内的子表单来回争抢同一个值
|
|
@@ -1112,8 +1131,19 @@
|
|
|
1112
1131
|
return keys.length === Object.keys(b).length && keys.every(key => Object.prototype.hasOwnProperty.call(b, key) && sameInput(a[key], b[key]));
|
|
1113
1132
|
};
|
|
1114
1133
|
// path 由收集字段的那一层计算(validate 在根表单调用时,嵌套字段的路径不能用根表单拼)
|
|
1134
|
+
// 回填错误以「所在记录(数组行)+ 行内路径」为键保存,不依赖完整路径:
|
|
1135
|
+
// 排序、删除前面的行后错误随原记录走;路径上换成别的记录时不显示;记录被删除后不再匹配
|
|
1136
|
+
const localPathOf = (col: any = {}) => [col.paths, col.field].filter(Boolean).join('.');
|
|
1137
|
+
const serverErrorOf = (col: any, data: any) => shared.serverErrors.value.get(toRaw(data))?.get(localPathOf(col));
|
|
1138
|
+
const deleteServerError = (col: any, data: any) => shared.serverErrors.value.get(toRaw(data))?.delete(localPathOf(col));
|
|
1139
|
+
const setServerError = (col: any, data: any, error: ServerError) => {
|
|
1140
|
+
const store = shared.serverErrors.value;
|
|
1141
|
+
const owner = toRaw(data);
|
|
1142
|
+
if (!store.has(owner)) store.set(owner, new Map());
|
|
1143
|
+
store.get(owner)!.set(localPathOf(col), error);
|
|
1144
|
+
};
|
|
1115
1145
|
const validateField = (col: any, ctx: FormContext, path: string, force = false): Promise<any> => {
|
|
1116
|
-
const server =
|
|
1146
|
+
const server = serverErrorOf(col, ctx.data);
|
|
1117
1147
|
if (server) return Promise.resolve({ msg: server.msg, type: server.type });
|
|
1118
1148
|
const input = deepcopy([displayOf(col, ctx), rulesOf(col), col.model]);
|
|
1119
1149
|
const previous = shared.validations.get(col);
|
|
@@ -1127,7 +1157,7 @@
|
|
|
1127
1157
|
error = message ? { msg: message, type: 'risk' } : await checkRules(input[0], input[1]);
|
|
1128
1158
|
} catch { error = { msg: '校验失败,请重试', type: 'risk' }; }
|
|
1129
1159
|
job.pending = false;
|
|
1130
|
-
if (shared.validations.get(col) === job && shared.validationEpoch === job.epoch && !
|
|
1160
|
+
if (shared.validations.get(col) === job && shared.validationEpoch === job.epoch && !serverErrorOf(col, ctx.data)) { col._error = error; col._checked = true; }
|
|
1131
1161
|
return error;
|
|
1132
1162
|
})();
|
|
1133
1163
|
return job.result;
|
|
@@ -1140,14 +1170,14 @@
|
|
|
1140
1170
|
deepcopy(col.model);
|
|
1141
1171
|
// 回填的错误在字段值变化前一直显示,值一变就交回正常校验
|
|
1142
1172
|
const path = col.field ? fieldPath(col) : '';
|
|
1143
|
-
const server = path ?
|
|
1173
|
+
const server = path ? serverErrorOf(col, props.data) : undefined;
|
|
1144
1174
|
if (server && !isCleared(col)) {
|
|
1145
1175
|
if (sameInput(server.input, value)) {
|
|
1146
1176
|
col._error = { msg: server.msg, type: server.type };
|
|
1147
1177
|
col._checked = true;
|
|
1148
1178
|
continue;
|
|
1149
1179
|
}
|
|
1150
|
-
|
|
1180
|
+
deleteServerError(col, props.data);
|
|
1151
1181
|
}
|
|
1152
1182
|
if (col.field && !isGroup(col) && !isCleared(col) && (touched.value || col._touched)) {
|
|
1153
1183
|
void validateField(col, context(), path);
|
|
@@ -1202,11 +1232,17 @@
|
|
|
1202
1232
|
if (shared.pending.size) await Promise.all([...shared.pending]);
|
|
1203
1233
|
} while (revision !== shared.revision || shared.pending.size);
|
|
1204
1234
|
revision = shared.revision;
|
|
1205
|
-
|
|
1235
|
+
const collected = fieldsOf().filter(({ path }) => withinPaths(path, paths));
|
|
1236
|
+
results = await Promise.all(collected.map(async ({ col, ctx, path }) => {
|
|
1206
1237
|
if (paths) col._touched = true;
|
|
1207
|
-
|
|
1208
|
-
return res ? { field: col.field, path, label: labelText(col.label), msg: res.msg, type: res.type, fid: col._fid } : null;
|
|
1238
|
+
return validateField(col, ctx, path, true);
|
|
1209
1239
|
}));
|
|
1240
|
+
// 校验期间 setErrors 回填的错误优先于本轮规则结果
|
|
1241
|
+
results = collected.map(({ col, path, ctx }, i) => {
|
|
1242
|
+
const server = serverErrorOf(col, ctx.data);
|
|
1243
|
+
const res = server ? { msg: server.msg, type: server.type } : results[i];
|
|
1244
|
+
return res ? { field: col.field, path, label: labelText(col.label), msg: res.msg, type: res.type, fid: col._fid } : null;
|
|
1245
|
+
});
|
|
1210
1246
|
if (epoch !== shared.validationEpoch) return cancelledResult();
|
|
1211
1247
|
} while (revision !== shared.revision);
|
|
1212
1248
|
const found = (results.filter(Boolean) as any[]).sort(byPosition);
|
|
@@ -1234,7 +1270,7 @@
|
|
|
1234
1270
|
|
|
1235
1271
|
const clearValidate = () => {
|
|
1236
1272
|
cancelValidation();
|
|
1237
|
-
shared.serverErrors.
|
|
1273
|
+
shared.serverErrors.value = reactive(new WeakMap());
|
|
1238
1274
|
shared.validations = new WeakMap();
|
|
1239
1275
|
shared.state.validated = false;
|
|
1240
1276
|
fieldsOf().forEach(({ col }) => { col._touched = false; col._error = null; col._checked = false; });
|
|
@@ -1251,7 +1287,7 @@
|
|
|
1251
1287
|
list.forEach(({ path, msg, type = 'risk' }) => {
|
|
1252
1288
|
const found = fields.find((item) => item.path === path);
|
|
1253
1289
|
if (!found) return missing.push(path);
|
|
1254
|
-
|
|
1290
|
+
setServerError(found.col, found.ctx.data, { msg, type, input: deepcopy(displayOf(found.col, found.ctx)) });
|
|
1255
1291
|
});
|
|
1256
1292
|
const first = list.find((item) => !missing.includes(item.path));
|
|
1257
1293
|
if (scroll && first) scrollToField(first.path);
|
|
@@ -210,7 +210,7 @@
|
|
|
210
210
|
</p>
|
|
211
211
|
</li>
|
|
212
212
|
<template v-if="treeEnabled && !item.__folding">
|
|
213
|
-
<template v-for="(child, cidx) in childrenOf(item)" :key="
|
|
213
|
+
<template v-for="(child, cidx) in childrenOf(item)" :key="childKeyOf(rowKeyOf(item, idx), child, cidx)">
|
|
214
214
|
<li v-if="childVisible(child)" class="table-tree-item bg-fore r-sm my-sl n-ms">
|
|
215
215
|
<table>
|
|
216
216
|
<tbody>
|
|
@@ -232,7 +232,7 @@
|
|
|
232
232
|
</p>
|
|
233
233
|
</li>
|
|
234
234
|
<template v-if="depth > 2 && !child.__folding">
|
|
235
|
-
<template v-for="(grand, gidx) in childrenOf(child)" :key="
|
|
235
|
+
<template v-for="(grand, gidx) in childrenOf(child)" :key="childKeyOf(childKeyOf(rowKeyOf(item, idx), child, cidx), grand, gidx)">
|
|
236
236
|
<li v-if="childVisible(grand)" class="table-tree-item table-tree-item-3 bg-fore r-sm my-sl n-ms">
|
|
237
237
|
<table>
|
|
238
238
|
<tbody>
|
|
@@ -652,6 +652,12 @@
|
|
|
652
652
|
if (key && item && typeof item === 'object') return item[key] ?? idx;
|
|
653
653
|
return idx;
|
|
654
654
|
};
|
|
655
|
+
// 子行标识:编码完整层级路径(设了 rowKey 时排序、增删后状态跟着记录走)。
|
|
656
|
+
// 用 JSON 数组而不是连字符拼接,避免「父 a + 子 b」与顶层 id「a-b」撞 key
|
|
657
|
+
const childKeyOf = (parentKey: any, item: any, idx: number) => {
|
|
658
|
+
const parent = typeof parentKey === 'string' && parentKey.startsWith('\u0000') ? JSON.parse(parentKey.slice(1)) : [parentKey];
|
|
659
|
+
return `\u0000${JSON.stringify([...parent, rowKeyOf(item, idx)])}`;
|
|
660
|
+
};
|
|
655
661
|
|
|
656
662
|
/**
|
|
657
663
|
* 虚拟滚动:上下用占位行撑出滚动高度,只渲染可视区附近的行。
|
|
@@ -705,11 +711,12 @@
|
|
|
705
711
|
const kids = childrenOf(item);
|
|
706
712
|
kids.forEach((child, cidx) => {
|
|
707
713
|
const last = cidx === kids.length - 1;
|
|
708
|
-
|
|
714
|
+
const childKey = childKeyOf(key, child, cidx);
|
|
715
|
+
out.push({ item: child, idx: cidx, level: 2, parent: item, key: childKey, last, parentLast: false });
|
|
709
716
|
if (props.depth < 3 || child?.__folding) return;
|
|
710
717
|
const grand = childrenOf(child);
|
|
711
718
|
grand.forEach((g, gidx) => {
|
|
712
|
-
out.push({ item: g, idx: gidx, level: 3, parent: child, key:
|
|
719
|
+
out.push({ item: g, idx: gidx, level: 3, parent: child, key: childKeyOf(childKey, g, gidx), last: gidx === grand.length - 1, parentLast: last });
|
|
713
720
|
});
|
|
714
721
|
});
|
|
715
722
|
});
|