fix: 优化表单检测逻辑
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user