feat(表单): 表单支持条件显示与「隐藏时提交固定值」
build / build (push) Successful in 2m17s

- types/agent.ts:FormField 新增 visibleWhen / requiredWhenVisible / hiddenValue。
- FormCard.vue:
  · 新增 isFieldVisible() —— 字段随上级字段取值增删(多个条件之间是「且」);
    只判断显示、**不清值**(与模块页 form.formType 保留的语义一致)。
  · renderFields 过滤不可见字段;missingCount 只算可见的必填项;必填星号含条件必填。
  · onSubmit:必填校验只看可见字段;隐藏字段不进提交体,但后端标了 hiddenValue 时按固定值提交
    (对齐 NewActivity.vue 的 `quota: receiptMode === "none" ? 0 : …`)。
This commit is contained in:
lijing
2026-10-15 15:47:36 +08:00
parent b0f31608d4
commit 41aa77531a
2 changed files with 68 additions and 6 deletions
+43 -6
View File
@@ -113,6 +113,27 @@ function extraFieldId(name: string) {
return Number(name.slice(EXTRA_PREFIX.length));
}
/**
* 条件显示:本字段当前该不该出现在表单里。
*
* <p>后端可以在字段上标注 {@code visibleWhen}(如宿舍通知的「回执单据」只在
* 「回执方式 = 需填报」时才有、「宿管到场配合」只在「回执方式 ≠ 仅通知」时才有
* —— 与模块页 {@code NewNotice.vue} 的 {@code isFill / showReceiptOpts} 两个 v-if 一一对应)。
* 没标注的字段恒显示;多个条件之间是「且」。
*
* <p>★ 这里**只判断显示,不清值**:模块页切回「仅通知」时 {@code form.formType} 仍留在模型里,
* 只是不随包提交 —— 清掉反而会让用户切回来时白选一次。
*/
function isFieldVisible(f: FormField): boolean {
const spec = f.visibleWhen;
if (!spec) return true;
return Object.entries(spec).every(([dep, vals]) => {
const cur = values[dep];
const s = cur === undefined || cur === null ? "" : String(cur);
return (vals || []).map(String).includes(s);
});
}
/**
* 渲染哪几个字段。
*
@@ -121,8 +142,8 @@ function extraFieldId(name: string) {
* 锚点不存在时退化为追加到末尾。
*/
const renderFields = computed(() => {
const base = (props.form?.fields || []).filter(f => !f.mirrorOf && !hiddenSingle[f.name]);
const ext = extraFields.value.filter(f => !hiddenSingle[f.name]);
const base = (props.form?.fields || []).filter(f => !f.mirrorOf && !hiddenSingle[f.name] && isFieldVisible(f));
const ext = extraFields.value.filter(f => !hiddenSingle[f.name] && isFieldVisible(f));
if (!ext.length) return base;
const anchor = props.form?.extraFields?.before;
const idx = anchor ? base.findIndex(f => f.name === anchor) : -1;
@@ -912,8 +933,10 @@ const presetList = computed(() => {
}));
});
/** 还差几项必填没填(页脚给出明确预期) */
const missingCount = computed(() => (props.form?.fields || []).filter(f => f.required).length);
/** 还差几项必填没填(页脚给出明确预期);条件字段不显示时不计入 */
const missingCount = computed(
() => (props.form?.fields || []).filter(f => isFieldVisible(f) && (f.required || f.requiredWhenVisible)).length
);
function labelOf(name: string): string {
const f = (props.form?.fields || []).find(x => x.name === name);
@@ -1001,7 +1024,10 @@ function onSubmit() {
for (const f of [...(props.form?.fields || []), ...extraFields.value]) {
const v = values[f.name];
const empty = v === undefined || v === null || String(v).trim() === "";
if (f.required && empty) {
// 条件必填:只有字段真的显示在界面上(visibleWhen 成立)才要求填 ——
// 隐藏的字段连值都不提交,报红等于让用户去填一个看不见的输入框。
const must = isFieldVisible(f) && (f.required || f.requiredWhenVisible);
if (must && empty) {
errors[f.name] = `请填写「${f.label}」`;
ok = false;
} else {
@@ -1021,6 +1047,17 @@ function onSubmit() {
// 2) 用户填写的字段
const rows: { label: string; text: string }[] = [];
for (const f of props.form?.fields || []) {
// 条件字段当前不显示 ⇒ 它的值不进提交体(模块页「仅通知」时就不发 formType,
// 见 NewNotice.vue 的 `form.receiptMode === "fill" ? form.formType : undefined`)
// ★ 例外:模块页有「控件隐藏但请求体照发」的写法(NewActivity.vue 的
// `quota: form.receiptMode === "none" ? 0 : ...`)—— 那种字段由后端标了
// hiddenValue,隐藏时仍按固定值提交,不能整键不发。
if (!isFieldVisible(f)) {
if (f.hiddenValue !== undefined && f.hiddenValue !== null) {
args[f.name] = f.hiddenValue;
}
continue;
}
// 镜像字段(deptName 等):不带输入控件,取源字段「选中项的显示名」一起提交,与模块页一致
if (f.mirrorOf) {
const src = values[f.mirrorOf];
@@ -1212,7 +1249,7 @@ function reset() {
</div>
<div class="field" :class="{ 'field--wide': isWide(f) }">
<label v-if="widgetOf(f) !== 'switch'" class="field__label">
<span v-if="f.required" class="field__star">*</span>{{ f.label }}
<span v-if="f.required || f.requiredWhenVisible" class="field__star">*</span>{{ f.label }}
</label>
<div class="field__control">
+25
View File
@@ -137,6 +137,31 @@ export interface FormField {
autoPickFirst?: boolean;
/** 选项只有一个时自动填入并隐藏本字段(模块页「班级」:单班时不显示) */
hideWhenSingleOption?: boolean;
/**
* 条件显示:本字段**仅当**所列字段的当前取值命中给定值时渲染、校验、提交。
*
* <p>形如 `{receiptMode: ["confirm","fill"]}` —— 宿舍通知模块页「新建通知」里,
* 「回执方式」这个单选决定了下面还显示哪些参数(`NewNotice.vue` 的
* `isFill = receiptMode === "fill"` 控制「回执单据」、
* `showReceiptOpts = receiptMode !== "none"` 控制「宿管到场配合」)。
* 多个键之间是「且」;不标注 = 恒显示。
*/
visibleWhen?: Record<string, string[]>;
/**
* 条件必填:仅当字段因 `visibleWhen` 真的显示时才要求填写。
* 区别于 `required` —— 后者会进给模型的 schema,而条件必填只约束界面上这一次填写。
*/
requiredWhenVisible?: boolean;
/**
* 「隐藏时提交固定值」:本字段因 `visibleWhen` 不成立而**不显示**时,提交体里照样带上这个值。
*
* <p>模块页的隐藏有**两种口径**,别混:
* - 不提交(宿舍通知「回执单据」,`receiptMode === "fill" ? form.formType : undefined`)
* → 后端不给 `hiddenValue`,隐藏时整键不发;
* - 提交固定值(家校活动「名额上限 / 家长随行」,
* `quota: form.receiptMode === "none" ? 0 : ...`)→ 后端给 `hiddenValue: 0`。
*/
hiddenValue?: any;
/**
* 快捷时段按钮组:`{ start, end, items: [{label,start,end}] }`。
* `start/end` 是**时刻**(HH:mm:ss),点按钮时套用到当前已选日期上。