家长端回执弹窗的专属区动态规则此前只写 required + trigger、没给 type。
async-validator 的 getValidationMethod 仅在规则「除掉 message 后只剩 required」
时才走 required 校验器;带上 trigger 后会回退到默认 string 校验器,而 DatePicker
的 v-model 给出的是 Date 对象,被判类型不符。真正的报错又在 cb() 里被自定义
message 顶掉,界面上只剩「请填写XX」——表现为字段明明填了却不让提交。
- ParentNotices.vue: 新增 ruleTypeOf() 统一映射规则类型
(number -> type:"number",date/datetime -> type:"date",其余保持默认 string)。
注意 async-validator 没有 datetime 类型,写上去会导致 validate 抛异常。
- ParentNotices.vue: 日期字段回填统一转 Date(本地时间语义,避免 new Date("yyyy-MM-dd")
的 UTC 偏移),提交时序列化成 yyyy-MM-dd / yyyy-MM-dd HH:mm。
这同时修掉一处数据污染:此前 String(Date) 会把
"Tue Sep 22 2026 10:33:00 GMT+0800 (中国标准时间)" 这类 locale 脏串写进 fillData。
- NewNotice.vue: 定时发布时间弹窗的规则存在同类问题,补 type:"date"。
- 覆盖四类回执单:stay / visit / adjust 的日期字段曾坏,general 及公共区字段本就正常。
.gitignore 追加 .workbuddy/(本地 AI 工作目录,不入库)。
This commit is contained in:
@@ -31,3 +31,6 @@ yarn.lock
|
||||
# 本地备份文件(不入库,如 icons.css.bak 为旧版图标集)
|
||||
*.bak
|
||||
*.orig
|
||||
|
||||
# AI agent workspace (local only)
|
||||
.workbuddy/
|
||||
|
||||
@@ -254,7 +254,12 @@
|
||||
这些是模板项目已经踩过并写进注释的坑,本模块原样继承:
|
||||
|
||||
1. **`<FormItem required>` 必须配 `prop`** —— 否则字段名被解析成 `undefined`,model 取到 `undefined`,家长**填了值也照样报错**、表单永远提交不了。
|
||||
2. **数字字段规则必须显式写 `type: "number"`** —— async-validator 只在规则「除掉 message 后只剩 required」时走 required 校验器;一旦带 `trigger`,会回退到 `type: "string"`,`InputNumber` 给出的数字被判类型不符,错误文案还被自定义 message 掩盖,极具误导性。
|
||||
2. **非 string 取值的控件,规则里必须显式写 `type`** —— async-validator 只在规则「除掉 message 后只剩 required」时走 required 校验器;一旦带 `trigger`,会回退到 `default validators[type || 'string']`,再拿 `typeof value !== 'string'` 判类型不符,而**真实报错会被自定义 `message` 覆盖**,于是「明明填了值,红字仍显示『请填写XX』、表单交不上去」,极具误导性。
|
||||
- `InputNumber` → `type: "number"`;
|
||||
- `DatePicker` → **`type: "date"`**(v-model 给的是 `Date` 对象;async-validator **没有 `datetime` 这个类型**,写上去 `getType()` 直接抛 `Unknown rule type datetime`,整个 validate 崩掉,被 `.catch(() => false)` 吞成「提交失败」);
|
||||
- `Select`/`Input`/`Textarea` → string,默认即可。
|
||||
- 配套:日期字段在模型里统一存 `Date`、提交时再格式化成 `yyyy-MM-dd` / `yyyy-MM-dd HH:mm`(本地时间手工拼,别 `new Date("2026-09-23")` —— 按 UTC 解析会跑日期),否则 `String(Date)` 会提交成 `"Tue Sep 22 2026 10:33:00 GMT+0800 (中国标准时间)"`,回执名单与导出 CSV 全废。
|
||||
- 30 秒定位法:在 node 里用项目自己的 `node_modules/async-validator` 复刻 `FormItem.validate()`(`descriptor[prop] = rules`、`model[prop] = fieldValue`、`{ firstFields: true }`),逐个类型跑一遍,比读代码猜快得多。
|
||||
3. **表单模型是稳定的 `reactive` 对象,不能写成 `computed(() => ({...}))`** —— 每次依赖变化生成新对象,而 `FormItem` 校验读的是 `Form.model`,props 要等父组件重渲染才更新,同步 change 校验拿到的还是旧对象(「选了却红字不消失」)。
|
||||
4. **`rules` 写成普通常量/`computed` 要分清场景** —— Form 内有 `watch: { rules () { this.validate() } }`,引用一变就全量重校验。
|
||||
5. **存草稿只校验标题**,并用 `clearFieldState()` 清掉其它字段的残留红字。
|
||||
|
||||
@@ -341,7 +341,10 @@ const timeModal = reactive({ open: false, date: "" });
|
||||
/** 定时发布弹窗表单(同样需要 :model + prop 才能正常校验) */
|
||||
const timeFormRef = ref();
|
||||
const timeRules = {
|
||||
date: [{ required: true, message: "请选择定时发布时间", trigger: "change" }],
|
||||
// 必须显式写 type:"date":DatePicker 给的是 Date 对象,规则带 trigger 时 async-validator
|
||||
// 会退化成默认的 string 校验器,把 Date 判成「不是字符串」;又因带自定义 message,
|
||||
// 报错文案被覆盖成「请选择定时发布时间」,看起来像「选了时间还提示必选、发不出去」
|
||||
date: [{ required: true, type: "date", message: "请选择定时发布时间", trigger: "change" }],
|
||||
};
|
||||
const dateOpt = { disabledDate: (d: Date) => d && d.valueOf() < Date.now() - 86400000 };
|
||||
function fmt(d: any) {
|
||||
|
||||
@@ -220,6 +220,36 @@ function resetFillKeys() {
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 日期字段的类型归一 ---------- */
|
||||
/**
|
||||
* 日期字段两端类型不一致,必须归一,否则校验必然打架:
|
||||
* - 控件侧:`DatePicker` 的 v-model 拿到的是 **Date 对象**(picker.vue 的 publicVModelValue 原样 emit);
|
||||
* - 后端侧:`fillData` 是 `Map<String,String>`,回填给的是 `"2026-09-22"` 字符串。
|
||||
* 口径:**模型里统一存 Date**(与控件输出一致,校验规则才好写),提交时再格式化成字符串。
|
||||
*/
|
||||
function toDate(v: any): Date | undefined {
|
||||
if (v === undefined || v === null || v === "") return undefined;
|
||||
if (v instanceof Date) return isNaN(v.getTime()) ? undefined : v;
|
||||
const m = String(v).match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?/);
|
||||
if (!m) return undefined;
|
||||
// 手工拼本地时间:`new Date("2026-09-23")` 按 UTC 解析,时区一偏日期就会跑掉
|
||||
return new Date(+m[1], +m[2] - 1, +m[3], +(m[4] || 0), +(m[5] || 0), +(m[6] || 0));
|
||||
}
|
||||
const pad2 = (n: number) => String(n).padStart(2, "0");
|
||||
/**
|
||||
* 提交口径:date → `yyyy-MM-dd`,datetime → `yyyy-MM-dd HH:mm`。
|
||||
* 不能直接 `String(value)` —— Date 会被序列化成
|
||||
* `"Tue Sep 22 2026 10:33:00 GMT+0800 (中国标准时间)"`,回执名单与导出 CSV 全废。
|
||||
*/
|
||||
function fmtDateValue(v: any, type: FillField["type"]): string {
|
||||
const d = toDate(v);
|
||||
if (!d) return "";
|
||||
const day = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||
return type === "datetime" ? `${day} ${pad2(d.getHours())}:${pad2(d.getMinutes())}` : day;
|
||||
}
|
||||
/** 是否日期类字段(date / datetime 在模型里都存 Date) */
|
||||
const isDateType = (t: FillField["type"]) => t === "date" || t === "datetime";
|
||||
|
||||
/** 把已提交的 fillData 回填到表单(未提交过则留空) */
|
||||
function prefillFillForm() {
|
||||
const my = detail.value?.my;
|
||||
@@ -227,7 +257,8 @@ function prefillFillForm() {
|
||||
syncFillRules();
|
||||
curFields.value.forEach(f => {
|
||||
const v = my?.fillData?.[f.key];
|
||||
fillModal[f.key] = f.type === "number" && v !== undefined && v !== "" ? Number(v) : v ?? "";
|
||||
if (isDateType(f.type)) fillModal[f.key] = toDate(v);
|
||||
else fillModal[f.key] = f.type === "number" && v !== undefined && v !== "" ? Number(v) : v ?? "";
|
||||
});
|
||||
fillModal.guardianName = my?.guardianName || "";
|
||||
fillModal.guardianPhone = my?.guardianPhone || "";
|
||||
@@ -244,7 +275,8 @@ function prefillFillForm() {
|
||||
* 原地改键不会改引用,Form 不会触发全量重校验。
|
||||
*
|
||||
* 另两条硬约束:Form 必须绑 :model 且 FormItem 必须有 prop(否则字段名解析成 undefined,
|
||||
* 填了值也照样报错);数字字段要显式 type:"number"(async-validator 默认按 string 校验)。
|
||||
* 填了值也照样报错);非 string 取值的控件(number / DatePicker)规则里要显式写 type,
|
||||
* 见下方 `ruleTypeOf()`。
|
||||
*/
|
||||
const fillRules: Record<string, any[]> = {
|
||||
guardianName: [{ required: true, message: "请输入家长姓名", trigger: "blur" }],
|
||||
@@ -255,6 +287,24 @@ const fillRules: Record<string, any[]> = {
|
||||
};
|
||||
/** 选择类控件(Select / DatePicker / InputNumber)派发的是 change,其余按 blur */
|
||||
const CHANGE_TRIGGER_TYPES = ["select", "date", "datetime", "number"];
|
||||
/**
|
||||
* 规则里**必须显式声明 `type`**,否则 async-validator 会退化成默认的 `string` 校验器。
|
||||
*
|
||||
* 原理:`getValidationMethod()` 只在规则「除掉 message 后只剩 required」时才走 required 校验器;
|
||||
* 一旦带上 `trigger`,就回退到 `validators[rule.type || 'string']`,然后 `rules.type` 拿
|
||||
* `typeof value !== 'string'` 判定类型不符 —— 而规则一旦带自定义 `message`,真实报错会被
|
||||
* `rule.message` 覆盖,于是**家长明明填了值,红字仍显示「请填写XX」、表单死活交不上去**。
|
||||
*
|
||||
* 各控件的取值类型:`Input`/`Select`/`Textarea` → string(默认即可);
|
||||
* `InputNumber` → number(`type: "number"`);`DatePicker` → **Date 对象**(`type: "date"`)。
|
||||
* 注意 async-validator 只有 `date` 没有 `datetime`,写 `datetime` 会直接抛 `Unknown rule type`,
|
||||
* 所以 datetime 也用 `date` 承接(该校验器对 Date 对象与 `yyyy-MM-dd[ HH:mm]` 字符串都放行)。
|
||||
*/
|
||||
function ruleTypeOf(type: FillField["type"]) {
|
||||
if (type === "number") return { type: "number" as const };
|
||||
if (isDateType(type)) return { type: "date" as const };
|
||||
return {};
|
||||
}
|
||||
/** 按当前单据的字段清单原地同步规则(保持对象引用不变) */
|
||||
function syncFillRules() {
|
||||
Object.keys(fillRules).forEach(k => {
|
||||
@@ -264,11 +314,7 @@ function syncFillRules() {
|
||||
const arr: any[] = [];
|
||||
if (f.required) {
|
||||
const trigger = CHANGE_TRIGGER_TYPES.includes(f.type) ? "change" : "blur";
|
||||
arr.push(
|
||||
f.type === "number"
|
||||
? { required: true, type: "number", message: `请填写${f.label}`, trigger }
|
||||
: { required: true, message: `请填写${f.label}`, trigger }
|
||||
);
|
||||
arr.push({ required: true, ...ruleTypeOf(f.type), message: `请填写${f.label}`, trigger });
|
||||
}
|
||||
fillRules[f.key] = arr;
|
||||
});
|
||||
@@ -298,11 +344,14 @@ async function doFill() {
|
||||
}
|
||||
fillModal.submitting = true;
|
||||
try {
|
||||
// 数值字段统一转字符串提交(后端 fillData 为 Map<String,String>)
|
||||
// 后端 fillData 为 Map<String,String>,逐字段按控件类型序列化:
|
||||
// 日期字段转 yyyy-MM-dd / yyyy-MM-dd HH:mm(不能直接 String(Date)),其余原样转串
|
||||
const fillData: Record<string, string> = {};
|
||||
curFields.value.forEach(f => {
|
||||
const v = fillModal[f.key];
|
||||
if (v !== undefined && v !== null && String(v) !== "") fillData[f.key] = String(v);
|
||||
if (v === undefined || v === null || v === "") return;
|
||||
const s = isDateType(f.type) ? fmtDateValue(v, f.type) : String(v);
|
||||
if (s !== "") fillData[f.key] = s;
|
||||
});
|
||||
await noticeApi
|
||||
.submitReceipt({
|
||||
|
||||
Reference in New Issue
Block a user