fix: 优化表单检测逻辑

This commit is contained in:
ZhuRui
2026-08-06 09:23:18 +08:00
parent e3bd35e154
commit 8c7e8f807c
10 changed files with 326 additions and 135 deletions
@@ -13,10 +13,25 @@ import {
Spin,
} from 'ant-design-vue';
import { JUMP_TYPE_OPTIONS } from '../model/useBannerModel';
import {
TITLE_MAX,
PATH_MAX,
URL_MAX,
titleRules,
imageUrlRules,
typeRules,
linkUrlRules,
miniProgramPathRules,
areaRules,
startTimeRules,
endTimeRules,
sortRules,
} from '../model/form';
import { getProvinceCityCascaderOptions, cityNameToCascaderPath } from '@/utils/areaData';
import { ImageCropper } from '@yp-component/root';
import { FileImageOutlined } from '@ant-design/icons-vue';
import { uploadFile, OssUploadType } from '@/utils/oss';
import { isImageFile } from '@/utils/form';
import { getEventList, getEventDetail, type TournamentAdminVO } from '../../list/model/services';
import dayjs from 'dayjs';
import styles from './BannerFormModal.module.less';
@@ -40,11 +55,6 @@ const getDefaultForm = () => ({
sort: 1,
});
/** 表单字段最大长度限制 */
const TITLE_MAX = 50;
const PATH_MAX = 200;
const URL_MAX = 500;
/** 图片上传限制:最大 10MB */
const IMAGE_MAX_SIZE = 10 * 1024 * 1024;
@@ -351,7 +361,7 @@ export default defineComponent({
target.value = '';
if (!file) return;
if (!/^image\//.test(file.type)) {
if (!isImageFile(file)) {
message.error('请选择图片文件');
return;
}
@@ -468,14 +478,7 @@ export default defineComponent({
requiredMark
>
{/* ===== Banner 标题 ===== */}
<Form.Item
label="Banner标题"
name="title"
rules={[
{ required: true, message: '请输入' },
{ max: TITLE_MAX, message: `标题最多 ${TITLE_MAX} 个字符` },
]}
>
<Form.Item label="Banner标题" name="title" rules={titleRules}>
<Input
placeholder="请输入标题"
maxlength={TITLE_MAX}
@@ -488,23 +491,10 @@ export default defineComponent({
<Form.Item
label="上传图片"
name="imageUrl"
rules={[
{ required: true, message: '请上传 Banner 图片' },
{
validator: async () => {
if (formData.imageStatus === 'uploading') {
return Promise.reject(new Error('图片仍在上传中,请稍候'));
}
if (
formData.imageUrl?.startsWith('blob:') ||
formData.imageUrl?.startsWith('data:')
) {
return Promise.reject(new Error('请等待图片上传完成'));
}
return Promise.resolve();
},
},
]}
rules={imageUrlRules(
() => formData.imageStatus,
() => formData.imageUrl,
)}
>
<div class={styles.uploader}>
<input
@@ -549,11 +539,7 @@ export default defineComponent({
</Form.Item>
{/* ===== 跳转类型 ===== */}
<Form.Item
label="跳转类型"
name="type"
rules={[{ required: true, message: '请选择跳转类型' }]}
>
<Form.Item label="跳转类型" name="type" rules={typeRules}>
<Select
options={JUMP_TYPE_OPTIONS as any}
placeholder="请选择"
@@ -584,14 +570,7 @@ export default defineComponent({
{/* ===== 联动字段:自定义链接 ===== */}
{formData.type === '2' && (
<Form.Item
label="跳转地址"
name="linkUrl"
rules={[
{ required: true, message: '请输入跳转地址' },
{ max: URL_MAX, message: `链接最多 ${URL_MAX} 个字符` },
]}
>
<Form.Item label="跳转地址" name="linkUrl" rules={linkUrlRules}>
<Input
placeholder="https://xxx"
maxlength={URL_MAX}
@@ -603,14 +582,7 @@ export default defineComponent({
{/* ===== 联动字段:小程序页面 ===== */}
{formData.type === '3' && (
<Form.Item
label="小程序路径"
name="linkUrl"
rules={[
{ required: true, message: '请输入小程序页面路径' },
{ max: PATH_MAX, message: `路径最多 ${PATH_MAX} 个字符` },
]}
>
<Form.Item label="小程序路径" name="linkUrl" rules={miniProgramPathRules}>
<Input
placeholder="/pages/index/index"
maxlength={PATH_MAX}
@@ -621,11 +593,7 @@ export default defineComponent({
)}
{/* ===== 所属区域(省市树种选择 + 全国) ===== */}
<Form.Item
label="所属区域"
name="area"
rules={[{ required: true, message: '请选择所属区域' }]}
>
<Form.Item label="所属区域" name="area" rules={areaRules}>
<Cascader
v-model:value={areaPath.value}
options={areaCascaderOptions}
@@ -641,16 +609,7 @@ export default defineComponent({
label="开始时间"
name="displayStartTime"
class={styles.timeItem}
rules={[
{
validator: async () => {
if (!formData.displayTimeRange?.[0]) {
return Promise.reject(new Error('请选择开始时间'));
}
return Promise.resolve();
},
},
]}
rules={startTimeRules(() => formData.displayTimeRange)}
>
<DatePicker
showTime
@@ -679,16 +638,7 @@ export default defineComponent({
label="结束时间"
name="displayEndTime"
class={styles.timeItem}
rules={[
{
validator: async () => {
if (!formData.displayTimeRange?.[1]) {
return Promise.reject(new Error('请选择结束时间'));
}
return Promise.resolve();
},
},
]}
rules={endTimeRules(() => formData.displayTimeRange)}
>
<DatePicker
showTime
@@ -715,14 +665,7 @@ export default defineComponent({
</div>
{/* ===== 排序 ===== */}
<Form.Item
label="排序"
name="sort"
rules={[
{ required: true, message: '请输入排序' },
{ type: 'number', min: 1, max: 999, message: '排序范围 1-999' },
]}
>
<Form.Item label="排序" name="sort" rules={sortRules}>
<InputNumber
placeholder="数字越小越靠前"
min={1}
+114
View File
@@ -0,0 +1,114 @@
/**
* Banner 表单域控制器
*
* 集中管理 Banner 新增/编辑弹窗的所有表单规则校验与常量。
* 各规则数组 / 工厂函数可直接传给 antd Form.Item 的 rules prop。
*/
import type { Rule } from 'ant-design-vue/es/form';
// ============================================================
// 常量
// ============================================================
/** 标题最大长度 */
export const TITLE_MAX = 50;
/** 小程序路径最大长度 */
export const PATH_MAX = 200;
/** 跳转链接最大长度 */
export const URL_MAX = 500;
// ============================================================
// 表单规则
// ============================================================
/** Banner 标题:必填 + 50 字符上限 */
export const titleRules: Rule[] = [
{ required: true, message: '请输入' },
{ max: TITLE_MAX, message: `标题最多 ${TITLE_MAX} 个字符` },
];
/**
* Banner 图片:必填 + 上传状态校验
*
* @param getImageStatus 获取当前上传状态的 getter
* @param getImageUrl 获取当前图片 URL 的 getter
*/
export function imageUrlRules(getImageStatus: () => string, getImageUrl: () => string): Rule[] {
return [
{ required: true, message: '请上传 Banner 图片' },
{
validator: async () => {
if (getImageStatus() === 'uploading') {
return Promise.reject(new Error('图片仍在上传中,请稍候'));
}
const url = getImageUrl();
if (url.startsWith('blob:') || url.startsWith('data:')) {
return Promise.reject(new Error('请等待图片上传完成'));
}
return Promise.resolve();
},
},
];
}
/** 跳转类型:必选 */
export const typeRules: Rule[] = [{ required: true, message: '请选择跳转类型' }];
/** 跳转地址(自定义链接 scene=2):必填 + 最大长度 */
export const linkUrlRules: Rule[] = [
{ required: true, message: '请输入跳转地址' },
{ max: URL_MAX, message: `链接最多 ${URL_MAX} 个字符` },
];
/** 小程序页面路径(scene=3):必填 + 最大长度 */
export const miniProgramPathRules: Rule[] = [
{ required: true, message: '请输入小程序页面路径' },
{ max: PATH_MAX, message: `路径最多 ${PATH_MAX} 个字符` },
];
/** 所属区域:必选 */
export const areaRules: Rule[] = [{ required: true, message: '请选择所属区域' }];
/** 排序:必填 + 1-999 整数 */
export const sortRules: Rule[] = [
{ required: true, message: '请输入排序' },
{ type: 'number', min: 1, max: 999, message: '排序范围 1-999' },
];
/**
* 展示开始时间规则:校验 displayTimeRange[0] 非空
*
* @param getDisplayTimeRange 获取展示时间范围的 getter
*/
export function startTimeRules(getDisplayTimeRange: () => [string, string] | null): Rule[] {
return [
{
validator: async () => {
const range = getDisplayTimeRange();
if (!range?.[0]) {
return Promise.reject(new Error('请选择开始时间'));
}
return Promise.resolve();
},
},
];
}
/**
* 展示结束时间规则:校验 displayTimeRange[1] 非空
*
* @param getDisplayTimeRange 获取展示时间范围的 getter
*/
export function endTimeRules(getDisplayTimeRange: () => [string, string] | null): Rule[] {
return [
{
validator: async () => {
const range = getDisplayTimeRange();
if (!range?.[1]) {
return Promise.reject(new Error('请选择结束时间'));
}
return Promise.resolve();
},
},
];
}
@@ -1,5 +1,6 @@
import { defineComponent, reactive } from 'vue';
import { Modal, Descriptions, Image, Radio, Input, Form } from 'ant-design-vue';
import { Modal, Descriptions, Image, Radio, Input, Form, message } from 'ant-design-vue';
import { validateEventAuditSubmit } from '../model/form';
import styles from './EventAuditModal.module.less';
const { TextArea } = Input;
@@ -58,11 +59,13 @@ export default defineComponent({
});
const handleOk = () => {
if (!auditForm.comment.trim()) {
// 不通过时强制要求填写审核内容
if (!auditForm.approved) {
// 这里交给校验,简化版本不做强制校验
}
const check = validateEventAuditSubmit({
approved: auditForm.approved,
comment: auditForm.comment,
});
if (!check.valid) {
message.warning(check.error!);
return;
}
if (props.onSubmit) {
props.onSubmit({ approved: auditForm.approved, comment: auditForm.comment });
+32
View File
@@ -0,0 +1,32 @@
/**
* 赛事审核 - 表单域控制器
*
* 集中管理赛事审核弹窗的提交校验逻辑。
*/
// ============================================================
// 提交校验
// ============================================================
export interface EventAuditForm {
approved: boolean;
comment: string;
}
export interface EventAuditSubmitCheckResult {
valid: boolean;
error?: string;
}
/**
* 校验赛事审核提交数据
*
* @param form 审核表单 { approved, comment }
*/
export function validateEventAuditSubmit(form: EventAuditForm): EventAuditSubmitCheckResult {
// 不通过时强制要求填写审核内容
if (!form.approved && !form.comment.trim()) {
return { valid: false, error: '审核不通过时请填写审核内容' };
}
return { valid: true };
}
+11 -11
View File
@@ -50,24 +50,24 @@ export function useLogColumns() {
width: 200,
align: 'center' as const,
},
{
title: '操作对象',
dataIndex: 'obj',
key: 'obj',
width: 240,
align: 'center' as const,
},
// {
// title: '操作对象',
// dataIndex: 'obj',
// key: 'obj',
// width: 240,
// align: 'center' as const,
// },
{
title: '操作内容',
dataIndex: 'content',
key: 'content',
dataIndex: 'obj',
key: 'obj',
width: 360,
align: 'center' as const,
},
{
title: '操作时间',
dataIndex: 'createDate',
key: 'createDate',
dataIndex: 'createTime',
key: 'createTime',
width: 170,
align: 'center' as const,
},