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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user