From 8c7e8f807cff79b21dce9309fe1f85ba7a9e8be6 Mon Sep 17 00:00:00 2001 From: ZhuRui Date: Thu, 6 Aug 2026 09:23:18 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E8=A1=A8=E5=8D=95?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../banner/components/BannerFormModal.tsx | 113 +++++------------ src/pages/events/banner/model/form.ts | 114 ++++++++++++++++++ .../list/components/EventAuditModal.tsx | 15 ++- src/pages/events/list/model/form.ts | 32 +++++ src/pages/events/logs/model/useLogColumns.ts | 22 ++-- .../components/WithdrawDetailModal.tsx | 46 +++---- src/pages/finance/withdraw/model/form.ts | 84 +++++++++++++ src/pages/login/index.tsx | 8 +- src/pages/login/model/form.ts | 16 +++ src/utils/form.ts | 11 ++ 10 files changed, 326 insertions(+), 135 deletions(-) create mode 100644 src/pages/events/banner/model/form.ts create mode 100644 src/pages/events/list/model/form.ts create mode 100644 src/pages/finance/withdraw/model/form.ts create mode 100644 src/pages/login/model/form.ts diff --git a/src/pages/events/banner/components/BannerFormModal.tsx b/src/pages/events/banner/components/BannerFormModal.tsx index c4806c8..a52ca84 100644 --- a/src/pages/events/banner/components/BannerFormModal.tsx +++ b/src/pages/events/banner/components/BannerFormModal.tsx @@ -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 标题 ===== */} - + { - 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, + )} >
{/* ===== 跳转类型 ===== */} - + + + { - if (!formData.displayTimeRange?.[0]) { - return Promise.reject(new Error('请选择开始时间')); - } - return Promise.resolve(); - }, - }, - ]} + rules={startTimeRules(() => formData.displayTimeRange)} > { - if (!formData.displayTimeRange?.[1]) { - return Promise.reject(new Error('请选择结束时间')); - } - return Promise.resolve(); - }, - }, - ]} + rules={endTimeRules(() => formData.displayTimeRange)} > {/* ===== 排序 ===== */} - + 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(); + }, + }, + ]; +} diff --git a/src/pages/events/list/components/EventAuditModal.tsx b/src/pages/events/list/components/EventAuditModal.tsx index 0803e0e..4670538 100644 --- a/src/pages/events/list/components/EventAuditModal.tsx +++ b/src/pages/events/list/components/EventAuditModal.tsx @@ -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 }); diff --git a/src/pages/events/list/model/form.ts b/src/pages/events/list/model/form.ts new file mode 100644 index 0000000..a6933d6 --- /dev/null +++ b/src/pages/events/list/model/form.ts @@ -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 }; +} diff --git a/src/pages/events/logs/model/useLogColumns.ts b/src/pages/events/logs/model/useLogColumns.ts index 2c033e3..c0a982d 100644 --- a/src/pages/events/logs/model/useLogColumns.ts +++ b/src/pages/events/logs/model/useLogColumns.ts @@ -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, }, diff --git a/src/pages/finance/withdraw/components/WithdrawDetailModal.tsx b/src/pages/finance/withdraw/components/WithdrawDetailModal.tsx index 221dbc9..7565a18 100644 --- a/src/pages/finance/withdraw/components/WithdrawDetailModal.tsx +++ b/src/pages/finance/withdraw/components/WithdrawDetailModal.tsx @@ -6,11 +6,15 @@ import { PlusOutlined } from '@ant-design/icons-vue'; import { uploadFile, OssUploadType } from '@/utils/oss'; import { getWithdrawInfo, postWithdrawAudit, type WithdrawInfoVO } from '../model/services'; import { getAuditStatusText, getPayStatusText } from '../model/useWithdrawModel'; +import { + MAX_IMAGE_COUNT, + MAX_IMAGE_SIZE, + validateImageFile, + validateWithdrawAuditSubmit, +} from '../model/form'; import styles from './WithdrawDetailModal.module.less'; const { TextArea } = Input; -const MAX_IMAGE_COUNT = 3; -const MAX_IMAGE_SIZE = 5 * 1024 * 1024; interface WithdrawDetailModalProps { visible: boolean; @@ -67,16 +71,9 @@ export default defineComponent({ const beforeUpload = async (file: UploadFile) => { const rawFile = file as unknown as File; - if (imageList.value.length >= MAX_IMAGE_COUNT) { - message.warning(`最多上传 ${MAX_IMAGE_COUNT} 张图片`); - return false; - } - if (!/^image\//.test(rawFile.type)) { - message.error('请选择图片文件'); - return false; - } - if (rawFile.size >= MAX_IMAGE_SIZE) { - message.error('图片大小不能超过 5MB'); + const check = validateImageFile(rawFile, imageList.value.length); + if (!check.valid) { + message.error(check.error!); return false; } @@ -114,21 +111,16 @@ export default defineComponent({ // ===== 提交审核 ===== const handleSubmit = async () => { - // 驳回:审核内容必填 - if (!auditForm.pass && !auditForm.remark.trim()) { - message.warning('驳回时请填写审核内容'); - return; - } - // 通过:必须上传图片 - if (auditForm.pass) { - const hasDoneImage = imageList.value.some((item) => item.status === 'done' && item.url); - if (!hasDoneImage) { - message.warning('审核通过时必须上传审核图片'); - return; - } - } - if (imageList.value.some((item) => item.status === 'uploading')) { - message.warning('图片仍在上传中,请稍候'); + // 提交前校验 + const hasDoneImage = imageList.value.some((item) => item.status === 'done' && item.url); + const isUploading = imageList.value.some((item) => item.status === 'uploading'); + const check = validateWithdrawAuditSubmit( + { pass: auditForm.pass, remark: auditForm.remark }, + hasDoneImage, + isUploading, + ); + if (!check.valid) { + message.warning(check.error!); return; } diff --git a/src/pages/finance/withdraw/model/form.ts b/src/pages/finance/withdraw/model/form.ts new file mode 100644 index 0000000..c4217cc --- /dev/null +++ b/src/pages/finance/withdraw/model/form.ts @@ -0,0 +1,84 @@ +/** + * 提现审核 - 表单域控制器 + * + * 集中管理提现审核弹窗的图片上传校验、提交校验逻辑与常量。 + */ +import { isImageFile } from '@/utils/form'; + +// ============================================================ +// 常量 +// ============================================================ + +/** 审核图片最大上传张数 */ +export const MAX_IMAGE_COUNT = 3; +/** 审核图片单张最大体积:5MB */ +export const MAX_IMAGE_SIZE = 5 * 1024 * 1024; + +// ============================================================ +// 图片上传校验 +// ============================================================ + +export interface ImageUploadCheckResult { + valid: boolean; + error?: string; +} + +/** + * 校验待上传的审核图片 + * @param file 待上传的文件对象 + * @param currentCount 当前已上传的图片数量 + * @returns 校验结果 + */ +export function validateImageFile(file: File, currentCount: number): ImageUploadCheckResult { + if (currentCount >= MAX_IMAGE_COUNT) { + return { valid: false, error: `最多上传 ${MAX_IMAGE_COUNT} 张图片` }; + } + if (!isImageFile(file)) { + return { valid: false, error: '请选择图片文件' }; + } + if (file.size >= MAX_IMAGE_SIZE) { + return { valid: false, error: '图片大小不能超过 5MB' }; + } + return { valid: true }; +} + +// ============================================================ +// 提交审核校验 +// ============================================================ + +export interface WithdrawAuditForm { + pass: boolean; + remark: string; +} + +export interface WithdrawAuditSubmitCheckResult { + valid: boolean; + error?: string; +} + +/** + * 校验提现审核提交数据 + * + * @param form 审核表单 { pass, remark } + * @param hasDoneImage 是否有上传成功的图片 + * @param isUploading 是否有图片仍在上传中 + */ +export function validateWithdrawAuditSubmit( + form: WithdrawAuditForm, + hasDoneImage: boolean, + isUploading: boolean, +): WithdrawAuditSubmitCheckResult { + // 驳回:审核内容必填 + if (!form.pass && !form.remark.trim()) { + return { valid: false, error: '驳回时请填写审核内容' }; + } + // 通过:必须上传图片 + if (form.pass && !hasDoneImage) { + return { valid: false, error: '审核通过时必须上传审核图片' }; + } + // 图片上传未完成 + if (isUploading) { + return { valid: false, error: '图片仍在上传中,请稍候' }; + } + return { valid: true }; +} diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 8f1059c..dce1405 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -6,6 +6,7 @@ import { auth } from '@/hooks/useAuth'; import { useEffect, useState } from '@/hooks'; import { useUserStore } from '@/stores/userStore'; import { login as loginApi } from '@/api/login'; +import { loginFormRules } from './model/form'; import styles from './index.module.less'; interface LoginForm { @@ -40,11 +41,6 @@ export default defineComponent({ } }, []); - const rules = { - username: [{ required: true, message: '请输入用户名' }], - password: [{ required: true, message: '请输入密码' }], - }; - const handleSubmit = async () => { if (!form.phone || !form.password) { message.warning('请输入用户名和密码'); @@ -214,7 +210,7 @@ export default defineComponent({
diff --git a/src/pages/login/model/form.ts b/src/pages/login/model/form.ts new file mode 100644 index 0000000..56350c3 --- /dev/null +++ b/src/pages/login/model/form.ts @@ -0,0 +1,16 @@ +/** + * 登录页 - 表单域控制器 + * + * 集中管理登录表单的校验规则。 + */ +import type { Rule } from 'ant-design-vue/es/form'; + +// ============================================================ +// 表单规则 +// ============================================================ + +/** 登录表单的整体规则(form-level rules,key 对应 FormItem 的 name) */ +export const loginFormRules: Record = { + phone: [{ required: true, message: '请输入用户名' }], + password: [{ required: true, message: '请输入密码' }], +}; diff --git a/src/utils/form.ts b/src/utils/form.ts index 27b11ea..e2c0996 100644 --- a/src/utils/form.ts +++ b/src/utils/form.ts @@ -27,6 +27,12 @@ export const LOGIN_PASSWORD_REGEX = /^(?=.*[A-Za-z])(?=.*\d)[\w!@#$%^&*()_+\-=]{ */ export const REAL_NAME_REGEX = /^[\u4e00-\u9fa5A-Za-z0-9·]{1,8}$/; +/** + * 图片 MIME 类型前缀 + * - 用于上传前校验文件是否为图片 + */ +export const IMAGE_TYPE_REGEX = /^image\//; + // ============================================================ // 校验函数 // ============================================================ @@ -45,3 +51,8 @@ export function isValidPassword(value: unknown): boolean { export function isValidRealName(value: unknown): boolean { return typeof value === 'string' && value.length > 0 && REAL_NAME_REGEX.test(value); } + +/** 校验文件是否为图片类型 */ +export function isImageFile(file: { type: string }): boolean { + return IMAGE_TYPE_REGEX.test(file.type); +}