diff --git a/src/components/FormCard.vue b/src/components/FormCard.vue index f675e62..3f065e7 100644 --- a/src/components/FormCard.vue +++ b/src/components/FormCard.vue @@ -113,6 +113,27 @@ function extraFieldId(name: string) { return Number(name.slice(EXTRA_PREFIX.length)); } +/** + * 条件显示:本字段当前该不该出现在表单里。 + * + *

后端可以在字段上标注 {@code visibleWhen}(如宿舍通知的「回执单据」只在 + * 「回执方式 = 需填报」时才有、「宿管到场配合」只在「回执方式 ≠ 仅通知」时才有 + * —— 与模块页 {@code NewNotice.vue} 的 {@code isFill / showReceiptOpts} 两个 v-if 一一对应)。 + * 没标注的字段恒显示;多个条件之间是「且」。 + * + *

★ 这里**只判断显示,不清值**:模块页切回「仅通知」时 {@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() {

diff --git a/src/types/agent.ts b/src/types/agent.ts index f89997e..e4448f3 100644 --- a/src/types/agent.ts +++ b/src/types/agent.ts @@ -137,6 +137,31 @@ export interface FormField { autoPickFirst?: boolean; /** 选项只有一个时自动填入并隐藏本字段(模块页「班级」:单班时不显示) */ hideWhenSingleOption?: boolean; + /** + * 条件显示:本字段**仅当**所列字段的当前取值命中给定值时渲染、校验、提交。 + * + *

形如 `{receiptMode: ["confirm","fill"]}` —— 宿舍通知模块页「新建通知」里, + * 「回执方式」这个单选决定了下面还显示哪些参数(`NewNotice.vue` 的 + * `isFill = receiptMode === "fill"` 控制「回执单据」、 + * `showReceiptOpts = receiptMode !== "none"` 控制「宿管到场配合」)。 + * 多个键之间是「且」;不标注 = 恒显示。 + */ + visibleWhen?: Record; + /** + * 条件必填:仅当字段因 `visibleWhen` 真的显示时才要求填写。 + * 区别于 `required` —— 后者会进给模型的 schema,而条件必填只约束界面上这一次填写。 + */ + requiredWhenVisible?: boolean; + /** + * 「隐藏时提交固定值」:本字段因 `visibleWhen` 不成立而**不显示**时,提交体里照样带上这个值。 + * + *

模块页的隐藏有**两种口径**,别混: + * - 不提交(宿舍通知「回执单据」,`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),点按钮时套用到当前已选日期上。