fix(pc): 找回被 #feedback 吞掉的表单校验错误 + 收集任务文案精简

naive 的 NFormItem 一旦用了 #feedback 插槽,renderExplains 就永不渲染
(children || feedback ? feedback : …),校验错误被完全遮住 —— 用户看到的是
"红框 + 一句无关的正常提示",现象上等价于"点了保存没反应"。RoleFormModal 里
早就记录过这个机制,但只在自己那个文件绕开了,没固化成约定,于是扩散到 7 处。

- 7 处 validate() 的 catch 改成 message.error(firstError(errors) ?? 兜底)
  (login / org / role / user / task 建改任务 / 材料规则 / 表单设计)
- ⚠️ firstError 必须摊平后再取:naive reject 出来的是嵌套数组,
  errors[0].message 恒为 undefined,兜底文案会把每条错误都吞成"请检查表单填写"
- 批次号在"修改"时置灰(task_code = 批次号 + 4 位序号,且同时是文件目录名,
  而服务端 update 无条件写 batch_no、没有"编码已生成"的保护)
- 删掉「本期范围」区块(收集模式 / 认定类型恒定,请求体里仍写死 1)
- 清掉教材式描述:extras_json / extra_ 前缀 / 幂等 / 接收能力 / 识别结果只用于提示
- 规则沉淀进 frontend-pc/AGENTS.md 第 10 条与交接摘要 §一·四十
This commit is contained in:
2026-10-13 15:06:34 +08:00
parent 31e7483464
commit e94e76ba0f
10 changed files with 457 additions and 73 deletions
+29 -3
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { FormInst, FormRules } from 'naive-ui'
import { NAlert, NButton, NForm, NFormItem, NInput } from 'naive-ui'
import { NAlert, NButton, NForm, NFormItem, NInput, useMessage } from 'naive-ui'
import { computed, onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getCaptcha } from '@/apis/xt'
@@ -25,6 +25,7 @@ defineOptions({ name: 'Login' })
const route = useRoute()
const router = useRouter()
const user = useUserStore()
const message = useMessage()
const formRef = ref<FormInst | null>(null)
const loading = ref(false)
@@ -111,11 +112,36 @@ function handleFailure(e: unknown): void {
}
}
/**
* ⚠️ 本仓的 `NFormItem` 一旦用了 `#feedback` 插槽,**校验错误会被完全遮住**
* (naive 的渲染是 `children || feedback ? feedback : renderExplains`,
* 插槽存在时 `renderExplains` 永不渲染)。
* 本页的「租户码」有一句常驻提示,而它恰好是**必填**的 —— 漏填时
* 界面上那句提示原样留着、只多一个红框,所以必须自己把第一条错误弹出来。
*/
function firstError(errors: unknown): string | null {
if (!Array.isArray(errors)) {
return null
}
// ⚠️ naive 的 `Form.validate()` reject 出来的是**嵌套数组**(`[[{message}]…]`),
// 见 node_modules/naive-ui/es/form/src/Form.mjs:96-107 —— 每一层的 `result.errors` 被整个 push 进去。
// 所以不能直接读 `errors[0].message`,必须摊平后再找第一条。
for (const entry of errors.flat(2)) {
const message = (entry as { message?: unknown } | null)?.message
if (typeof message === 'string' && message) {
return message
}
}
return null
}
async function onSubmit(): Promise<void> {
try {
await formRef.value?.validate()
} catch {
// 校验失败时 Naive 会把每条错误显示在对应字段下,这里不需要再弹一次
} catch (errors) {
// 没有 `#feedback` 的字段 Naive 会自己把错误显示在字段下,这里多弹一次的收益是:
// 「租户码」那句常驻提示不会把必填错误吃掉(见 firstError 的注释)
message.error(firstError(errors) ?? '请检查表单填写')
return
}
+26 -1
View File
@@ -105,10 +105,35 @@ function edit(row: OrgTreeVO): void {
visible.value = true
}
/**
* ⚠️ 本仓的 `NFormItem` 一旦用了 `#feedback` 插槽,**校验错误会被完全遮住**
* (naive 的渲染是 `children || feedback ? feedback : renderExplains`,
* 插槽存在时 `renderExplains` 永不渲染)。本表单的「组织类型」既带校验又带
* `#feedback` 提示,所以校验失败必须自己把第一条错误弹出来 ——
* 否则界面上只剩一个红框加一句无关的提示,用户看到的是"点了确定没反应"。
*/
function firstError(errors: unknown): string | null {
if (!Array.isArray(errors)) {
return null
}
// ⚠️ naive 的 `Form.validate()` reject 出来的是**嵌套数组**(`[[{message}]…]`),
// 见 node_modules/naive-ui/es/form/src/Form.mjs:96-107 —— 每一层的 `result.errors` 被整个 push 进去。
// 所以不能直接读 `errors[0].message`,必须摊平后再找第一条。
for (const entry of errors.flat(2)) {
const message = (entry as { message?: unknown } | null)?.message
if (typeof message === 'string' && message) {
return message
}
}
return null
}
async function confirm(): Promise<void> {
try {
await formRef.value?.validate()
} catch {
} catch (errors) {
// Naive 的 `validate()` 在不过时是 reject(不是返回 false)
message.error(firstError(errors) ?? '请检查表单填写')
return
}
+26 -1
View File
@@ -131,10 +131,35 @@ function edit(row: RoleVO): void {
visible.value = true
}
/**
* ⚠️ 本仓的 `NFormItem` 一旦用了 `#feedback` 插槽,**校验错误会被完全遮住**
* (naive 的渲染是 `children || feedback ? feedback : renderExplains`,
* 插槽存在时 `renderExplains` 永不渲染)。本表单有 3 个字段带 `#feedback`,
* 所以校验失败必须自己把第一条错误弹出来 —— 否则界面上只剩一个红框,
* 用户看到的是"点了确定没反应"。
*/
function firstError(errors: unknown): string | null {
if (!Array.isArray(errors)) {
return null
}
// ⚠️ naive 的 `Form.validate()` reject 出来的是**嵌套数组**(`[[{message}]…]`),
// 见 node_modules/naive-ui/es/form/src/Form.mjs:96-107 —— 每一层的 `result.errors` 被整个 push 进去。
// 所以不能直接读 `errors[0].message`,必须摊平后再找第一条。
for (const entry of errors.flat(2)) {
const message = (entry as { message?: unknown } | null)?.message
if (typeof message === 'string' && message) {
return message
}
}
return null
}
async function confirm(): Promise<void> {
try {
await formRef.value?.validate()
} catch {
} catch (errors) {
// Naive 的 `validate()` 在不过时是 reject(不是返回 false)
message.error(firstError(errors) ?? '请检查表单填写')
return
}
+82 -39
View File
@@ -2,18 +2,20 @@
/**
* 建 / 改任务弹窗(《03-接口设计》§7.2)。
*
* 三处刻意的设计:
* 四处刻意的设计:
* 1. **命名模板带实时预览**:光看 `{姓名}-{证件号}-{材料名}` 很难想象最终长什么样,
* 而它直接决定导出包里每个文件的名字。用样例数据渲染出来一眼就能判断。
* 2. **收集模式与认定类型做成只读**:它们决定审核流与材料清单,本期也不开放其它取值。
* 可编辑只会让人以为能改。
* 3. **变量用按钮插入而不是让人手打**:手打 `{姓名}` 少一个大括号或写成 `{姓名 }`,
* 2. **不展示收集模式与认定类型**:本期它们恒为「学校收集 + 自主认定」,
* 界面上开放不了别的取值,写出来只是让人多读一段与自己无关的话。
* 取值在 `confirm()` 里写死进请求体(后端 `assertScopeSupported` 会校验)。
* 3. **变量用按钮追加而不是让人手打**:手打 `{姓名}` 少一个大括号或写成 `{姓名 }`,
* 服务端只会在发布时给一个 40020,而那时已经配了一堆别的东西。
* 4. **批次号在"修改"时置灰**:`task_code` = 批次号 + 4 位序号,且它就是文件目录名;
* 服务端 `update` 会无条件写入 `batch_no`,没有"编码已生成"的保护 —— 由前端守住。
*/
import type { FormInst, FormRules } from 'naive-ui'
import type { TaskUpsertReq, TaskVO } from '@/apis/xt/task'
import {
NAlert,
NButton,
NDatePicker,
NDivider,
@@ -41,18 +43,20 @@ const message = useMessage()
const visible = ref(false)
const submitting = ref(false)
const detailId = ref<number | null>(null)
/** 当前在编辑的任务编码,只在"修改"时有值 —— 用来告诉用户批次号为什么不能改 */
const detailTaskCode = ref('')
const formRef = useTemplateRef<FormInst>('formRef')
const isEdit = computed(() => !!detailId.value)
const title = computed(() => (isEdit.value ? '修改任务' : '新建收集任务'))
const title = computed(() => (isEdit.value ? '修改收集任务' : '新建收集任务'))
/**
* 表单自己的类型。
*
* 与 {@link TaskUpsertReq} 的差别是这里的 `jobName` / `jobLevel` 用空串表示"没填"
* (接口那边用 `null` 表示清空);另外收集模式与认定类型**界面上没有输入项**,
* 构造请求体时写死为 1(本期只有学校收集 + 自主认定)。
* 构造请求体时写死为 1(后端只接受这两个值,传别的返回 40044)。
*
* `startTime` / `endTime` 允许 `null`:Naive 的日期选择器清空时给的是 `null`。
*/
@@ -138,6 +142,19 @@ const namingPreview = computed(() => {
return out || '—'
})
/**
* 批次号提示分两态。
*
* 修改时只读:`task_code` = 批次号 + 4 位序号(后端 `TaskService.nextTaskCode`),
* 而它同时是文件目录名。服务端 `update` 会无条件写入 `batch_no`、没有"编码已生成"的保护,
* 所以由前端守住 —— 不改批次号就不会出现"列表里的批次号与任务编码前缀对不上"。
*/
const batchNoHint = computed(() =>
isEdit.value
? `任务编码 ${detailTaskCode.value || '—'} 已按它生成,不可再改`
: '4~8 位数字。任务编码 = 批次号 + 4 位序号(如 202609 → 2026090001)'
)
/**
* 变量按钮的文案。
*
@@ -154,12 +171,14 @@ function appendVariable(v: string) {
function add() {
detailId.value = null
detailTaskCode.value = ''
Object.assign(form, getInitForm())
visible.value = true
}
function edit(row: TaskVO) {
detailId.value = row.id
detailTaskCode.value = row.taskCode ?? ''
Object.assign(form, getInitForm())
form.taskName = row.taskName
form.batchNo = row.batchNo ?? ''
@@ -176,18 +195,42 @@ function close() {
visible.value = false
}
/**
* ⚠️ 本仓的 `NFormItem` 一旦用了 `#feedback` 插槽,**校验错误会被完全遮住**
* (naive 的渲染是 `children || feedback ? feedback : renderExplains`,
* 插槽存在时 `renderExplains` 永不渲染)。本表单有 3 个字段带 `#feedback`,
* 所以校验失败必须自己把第一条错误弹出来 —— 否则界面上只剩一个红框,
* 用户看到的是"点了保存没反应"。
*/
function firstError(errors: unknown): string | null {
if (!Array.isArray(errors)) {
return null
}
// ⚠️ naive 的 `Form.validate()` reject 出来的是**嵌套数组**(`[[{message}]…]`),
// 见 node_modules/naive-ui/es/form/src/Form.mjs:96-107 —— 每一层的 `result.errors` 被整个 push 进去。
// 所以不能直接读 `errors[0].message`(那样恒为 undefined,toast 只会显示兜底文案)。
for (const entry of errors.flat(2)) {
const message = (entry as { message?: unknown } | null)?.message
if (typeof message === 'string' && message) {
return message
}
}
return null
}
async function confirm() {
try {
await formRef.value?.validate()
} catch {
} catch (errors) {
// ⚠️ Naive 的 `validate()` 在不过时是 **reject**(不是返回 false),所以必须 catch
message.error(firstError(errors) ?? '请检查表单填写')
return
}
const end = form.endTime ?? ''
const payload: TaskUpsertReq = {
taskName: form.taskName.trim(),
// 本期只开放这两个取值,界面上也没有输入项 —— 写死而不是从表单里读
// 界面上没有输入项 —— 写死而不是从表单里读(后端 assertScopeSupported 只认这两个值)
collectMode: 1,
certType: 1,
batchNo: (form.batchNo ?? '').trim(),
@@ -238,9 +281,19 @@ defineExpose({ add, edit })
/>
</NFormItemGi>
<NFormItemGi :span="10" label="批次号" path="batchNo">
<NInput v-model:value="form.batchNo" placeholder="如 202609" :maxlength="8" clearable />
<!--
修改时置灰:`task_code` = 批次号 + 4 位序号,而它同时是文件目录名 ——
服务端 `update` 会无条件写入 `batch_no`,没有"编码已生成"的保护,所以由前端守住。
-->
<NInput
v-model:value="form.batchNo"
placeholder="如 202609"
:maxlength="8"
:disabled="isEdit"
clearable
/>
<template #feedback>
<span class="task-form__hint">4~8 位数字,同时作为任务编码前缀</span>
<span class="task-form__hint">{{ batchNoHint }}</span>
</template>
</NFormItemGi>
@@ -272,28 +325,16 @@ defineExpose({ add, edit })
class="task-form__date"
/>
<template #feedback>
<span class="task-form__hint">只选日期时按当天 23:59:59 处理</span>
<span class="task-form__hint">选到日即可,系统按当天 23:59 计算</span>
</template>
</NFormItemGi>
</NGrid>
<NDivider title-placement="left">
本期范围
</NDivider>
<NGrid :cols="2" :x-gap="16">
<NFormItem label="收集模式">
<NInput value="学校收集" disabled />
</NFormItem>
<NFormItem label="认定类型">
<NInput value="自主认定" disabled />
</NFormItem>
</NGrid>
<NAlert type="info" :show-icon="false" :bordered="false" class="task-form__scope">
本期只做 <strong>学校收集 + 自主认定</strong>:收集模式与认定类型由系统固定,
界面上不开放其它取值。
</NAlert>
<!--
⚠️ 这里**故意**不放「收集模式 / 认定类型」的只读展示,也不再写"本期只做学校收集 + 自主认定":
它们的取值在系统里是恒定的,写出来只是让建任务的人多读一段与自己无关的话。
请求体里 `collectMode: 1` / `certType: 1` 由 confirm() 写死 —— 那才是后端要的东西。
-->
<NDivider title-placement="left">
命名规则
@@ -303,14 +344,13 @@ defineExpose({ add, edit })
<NInput v-model:value="form.namingTemplate" :maxlength="256" clearable />
<template #feedback>
<span class="task-form__hint">
必须含 <code>{姓名}</code> <code>{证件号}</code> <code>{材料名}</code> ——
缺了任一项,文件名就无法唯一归属到人。
必须含 <code>{姓名}</code> <code>{证件号}</code> <code>{材料名}</code>。用下面的按钮追加变量。
</span>
</template>
</NFormItem>
<div class="task-form__vars">
<span class="task-form__vars-label">插入变量</span>
<span class="task-form__vars-label">追加变量</span>
<NSpace :size="6">
<NButton
v-for="opt in namingVariableOptions"
@@ -324,7 +364,8 @@ defineExpose({ add, edit })
</NSpace>
</div>
<NFormItem label="预览(以「张同学 / 361029200912100015 / 身份证」为例)">
<NFormItem label="文件名预览">
<span class="task-form__preview-note">以「张同学 / 361029200912100015 / 身份证」为例</span>
<code class="task-form__preview">{{ namingPreview }}</code>
</NFormItem>
</NForm>
@@ -335,7 +376,7 @@ defineExpose({ add, edit })
取消
</NButton>
<NButton type="primary" :loading="submitting" @click="confirm">
保存
{{ isEdit ? '保存修改' : '创建任务' }}
</NButton>
</NSpace>
</template>
@@ -357,11 +398,6 @@ defineExpose({ add, edit })
}
}
&__scope {
margin-top: -6px;
line-height: 1.8;
}
&__date {
width: 100%;
}
@@ -375,6 +411,13 @@ defineExpose({ add, edit })
&__vars-label {
flex: none;
font-size: 11px;
color: #8a8a8a;
}
&__preview-note {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: #8a8a8a;
}
@@ -346,7 +346,7 @@ const extraColumns = computed<DataTableColumns<EditField>>(() => [
onPositiveClick: () => onDeleteExtra(row)
}, {
trigger: () => h(NButton, { text: true, size: 'small', type: 'error' }, { default: () => '删除' }),
default: () => '删除后学生端不再显示这一项。确认删除?'
default: () => '删除后学生端不再显示这一项。注意:要等「保存为新版本」后才真正生效。确认删除?'
})
])
}
@@ -427,11 +427,35 @@ function onDeleteExtra(row: EditField) {
message.info('已从列表移除,点「保存为新版本」后生效')
}
/**
* ⚠️ 本仓的 `NFormItem` 一旦用了 `#feedback` 插槽,**校验错误会被完全遮住**
* (naive 的渲染是 `children || feedback ? feedback : renderExplains`,
* 插槽存在时 `renderExplains` 永不渲染)。扩展字段弹窗的「字段标识」就带 `#feedback`,
* 它的格式校验(只能小写字母/数字/下划线)**唯一**的提示路径就是这里 ——
* 不弹出来,用户看到的就是"点了确定没反应"。
*/
function firstError(errors: unknown): string | null {
if (!Array.isArray(errors)) {
return null
}
// ⚠️ naive 的 `Form.validate()` reject 出来的是**嵌套数组**(`[[{message}]…]`),
// 见 node_modules/naive-ui/es/form/src/Form.mjs:96-107 —— 每一层的 `result.errors` 被整个 push 进去。
// 所以不能直接读 `errors[0].message`,必须摊平后再找第一条。
for (const entry of errors.flat(2)) {
const message = (entry as { message?: unknown } | null)?.message
if (typeof message === 'string' && message) {
return message
}
}
return null
}
async function saveExtra() {
try {
await extraFormRef.value?.validate()
} catch {
} catch (errors) {
// Naive 的 `validate()` 在不过时是 reject(不是返回 false)
message.error(firstError(errors) ?? '请检查表单填写')
return
}
const opts = needOptions.value
@@ -520,16 +544,17 @@ defineExpose({ open })
<NSpin :show="loading">
<NDivider title-placement="left">
内置字段(官方 21 项,不可删除)
内置字段
</NDivider>
<!--
为什么内置字段不能删:删掉之后"这个值到底该不该有"就永远说不清了
(服务端 40026)。不想收的信息用「条件显示 → 不收集」隐藏,
这样它仍然在定义里,只是学生看不到、也不参与必填校验。
⚠️ 标题里**不要**再写"(官方 21 项,不可删除)":数量与"不可删"下面这句已经说了,重复就是噪音。
-->
<div class="fd__hint">
这些是认定机构要的官方字段。<strong>不想要某一项时不要删</strong>(也删不掉),
把它的「条件显示」设为「不收集」即可。带「只读」的字段由老师在名册里填,学生看不到输入框。
不想收的字段不用删(也删不掉):把它的「条件显示」设为「不收集」。
带「只读」的字段由老师在名册里填,学生看不到输入框。
</div>
<NDataTable
@@ -543,6 +568,12 @@ defineExpose({ open })
<NDivider title-placement="left">
扩展字段(自定义,仅展示与导出,不参与统计)
</NDivider>
<!--
为什么 key 必须带 `extra_` 前缀、值落在 `extras_json`……都是实现细节,
界面上不写:`suggestExtraKey()`(apis/xt/form-schema.ts:187)已经按字段名称自动生成
合法 key,人不需要知道前缀规则。前缀本身是防重名的护栏 ——
否则"这个值到底是内置的还是扩展的"永远说不清。
-->
<NSpace class="fd__tool" :size="10" align="center">
<NButton v-if="!readonly" type="primary" @click="onAddExtra">
<template #icon>
@@ -550,10 +581,6 @@ defineExpose({ open })
</template>
新增扩展字段
</NButton>
<span class="fd__hint fd__hint--inline">
扩展字段的值落在 <code>extras_json</code>,key 必须带 <code>extra_</code> 前缀 ——
前缀是防重名的护栏,否则"这个值到底是内置的还是扩展的"永远说不清
</span>
</NSpace>
<NEmpty v-if="!extras.length" description="没有扩展字段" />
@@ -581,11 +608,18 @@ defineExpose({ open })
<NFormItemGi :span="12" label="字段名称" path="label">
<NInput v-model:value="extraForm.label" placeholder="如 家长联系电话" :maxlength="64" clearable />
</NFormItemGi>
<NFormItemGi :span="12" label="字段标识(key)" path="key">
<NFormItemGi :span="12" label="字段标识" path="key">
<!--
⚠️ 这里**不能**写"按字段名称自动生成":中文标签没法可靠转拼音,
`suggestExtraKey()`(apis/xt/form-schema.ts:187-198)对中文一律落到 `extra_field`,
说"自动生成"会让人以为它是从"家长联系电话"推出来的。
写"能填什么 + 改了有什么后果"才是有用的;`extra_` 前缀的规则交给校验器(:379-395),
失败时由 saveExtra 弹 toast(这个 #feedback 会把错误文案吃掉)。
-->
<NInput v-model:value="extraForm.key" placeholder="extra_xxx" :maxlength="46" clearable />
<template #feedback>
<span class="fd__hint fd__hint--inline">
必须以 <code>extra_</code> 开头,只含小写字母、数字、下划线
只能用小写字母、数字、下划线;改动后导出列名会跟着变
</span>
</template>
</NFormItemGi>
@@ -94,7 +94,7 @@ const templates = computed(() => catalog.value.templates)
const visibleWhenOptions = matApi.VISIBLE_WHEN_OPTIONS
const maxSizeMin = computed(() => catalog.value.maxSizeRange?.[0] ?? 307)
const maxSizeMax = computed(() => catalog.value.maxSizeRange?.[1] ?? 5120)
const maxSizeHint = computed(() => `可配范围 ${maxSizeMin.value}~${maxSizeMax.value}KB(认定机构的接收能力)`)
const maxSizeHint = computed(() => `可配 ${maxSizeMin.value}~${maxSizeMax.value} KB(0.3M~5M)`)
const visibleWhenIndex = matApi.visibleWhenIndex
const OCR_TYPE_NAMES = matApi.OCR_TYPE_NAMES
@@ -359,11 +359,35 @@ function closeForm() {
editingId.value = null
}
/**
* ⚠️ 本仓的 `NFormItem` 一旦用了 `#feedback` 插槽,**校验错误会被完全遮住**
* (naive 的渲染是 `children || feedback ? feedback : renderExplains`,
* 插槽存在时 `renderExplains` 永不渲染)。本表单多数字段都带 `#feedback`,
* 所以校验失败必须自己把第一条错误弹出来 —— 否则界面上只剩一个红框,
* 用户看到的是"点了保存没反应"。
*/
function firstError(errors: unknown): string | null {
if (!Array.isArray(errors)) {
return null
}
// ⚠️ naive 的 `Form.validate()` reject 出来的是**嵌套数组**(`[[{message}]…]`),
// 见 node_modules/naive-ui/es/form/src/Form.mjs:96-107 —— 每一层的 `result.errors` 被整个 push 进去。
// 所以不能直接读 `errors[0].message`,必须摊平后再找第一条。
for (const entry of errors.flat(2)) {
const message = (entry as { message?: unknown } | null)?.message
if (typeof message === 'string' && message) {
return message
}
}
return null
}
async function save() {
try {
await formRef.value?.validate()
} catch {
} catch (errors) {
// Naive 的 `validate()` 在不过时是 reject(不是返回 false)
message.error(firstError(errors) ?? '请检查表单填写')
return
}
if (!taskId.value) {
@@ -489,16 +513,16 @@ defineExpose({ open })
刷新
</NButton>
<!--
模板是幂等的(已存在的材料码会跳过),所以先说清楚"会跳过已存在的",
否则管理员点两次会以为第一次没生效。
模板是幂等的(已存在的材料码会跳过),所以要说清楚"已有的不会重复添加",
否则管理员点两次会以为第一次没生效。界面上不写"幂等"这个词 —— 那是实现语言。
-->
<span class="mat-drawer__hint">按模板初始化是幂等的,已有的材料不会重复添加</span>
<span class="mat-drawer__hint">按模板初始化只补缺失的材料,已有的不会重复添加</span>
</NSpace>
<NSpin :show="loading">
<NEmpty v-if="!rows.length" description="还没有材料要求,请先「按模板初始化」或「新增材料」">
<div class="mat-drawer__empty-hint">
必传材料一项都没有时,任务<strong>发布不了</strong>(服务端会返回 40018)。
必传材料一项都没有时,任务<strong>发布不了</strong>。
</div>
</NEmpty>
@@ -618,6 +642,10 @@ defineExpose({ open })
</NFormItemGi>
<NFormItemGi :span="24" label="来源引导语" path="tipText">
<!--
这里**不要**再加 `#feedback`:它与 placeholder 说的是同一件事,写了就是同一句话出现两遍;
而多一个 feedback 只会让这一格更高。
-->
<NInput
v-model:value="form.tipText"
type="textarea"
@@ -626,18 +654,10 @@ defineExpose({ open })
show-count
placeholder="显示在学生端这一项下面,如:拍身份证人像面,四角要拍全"
/>
<template #feedback>
<span class="mat-form__hint">
写得越具体,学生传错的比例越低 —— 这句话省下的是班主任逐人纠错的时间
</span>
</template>
</NFormItemGi>
<NFormItemGi :span="12" label="自动识别" path="ocrType">
<NSelect v-model:value="form.ocrType" clearable placeholder="不做识别" :options="ocrTypeOptions" />
<template #feedback>
<span class="mat-form__hint">识别结果只用于提示,不会自动改动材料归属</span>
</template>
</NFormItemGi>
<NFormItemGi :span="12" label="排序号" path="sortNo">
<NInputNumber v-model:value="form.sortNo" :min="0" class="mat-form__num" />
+26 -1
View File
@@ -190,10 +190,35 @@ function closeCreated(): void {
emit('save-success')
}
/**
* ⚠️ 本仓的 `NFormItem` 一旦用了 `#feedback` 插槽,**校验错误会被完全遮住**
* (naive 的渲染是 `children || feedback ? feedback : renderExplains`,
* 插槽存在时 `renderExplains` 永不渲染)。本表单有 4 个字段带 `#feedback`,
* 所以校验失败必须自己把第一条错误弹出来 —— 否则界面上只剩一个红框,
* 用户看到的是"点了确定没反应"。
*/
function firstError(errors: unknown): string | null {
if (!Array.isArray(errors)) {
return null
}
// ⚠️ naive 的 `Form.validate()` reject 出来的是**嵌套数组**(`[[{message}]…]`),
// 见 node_modules/naive-ui/es/form/src/Form.mjs:96-107 —— 每一层的 `result.errors` 被整个 push 进去。
// 所以不能直接读 `errors[0].message`,必须摊平后再找第一条。
for (const entry of errors.flat(2)) {
const message = (entry as { message?: unknown } | null)?.message
if (typeof message === 'string' && message) {
return message
}
}
return null
}
async function confirm(): Promise<void> {
try {
await formRef.value?.validate()
} catch {
} catch (errors) {
// Naive 的 `validate()` 在不过时是 reject(不是返回 false)
message.error(firstError(errors) ?? '请检查表单填写')
return
}