feat: 表单提交前去除首尾空格;评委性别接口返回与持久化

Made-with: Cursor
This commit is contained in:
zhonghua 2026-04-09 11:34:40 +08:00
parent c4f4613c49
commit d5657d8d23
5 changed files with 153 additions and 112 deletions

View File

@ -103,6 +103,21 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
} }
} }
/** 与前端约定male / female */
private static String normalizeJudgeGender(Object raw) {
if (raw == null) {
return null;
}
String g = raw.toString().trim();
if (g.isEmpty()) {
return null;
}
if ("male".equals(g) || "female".equals(g)) {
return g;
}
return null;
}
/** /**
* SysUser 转为前端需要的 Map * SysUser 转为前端需要的 Map
*/ */
@ -115,6 +130,7 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
map.put("email", user.getEmail()); map.put("email", user.getEmail());
map.put("phone", user.getPhone()); map.put("phone", user.getPhone());
map.put("organization", user.getOrganization()); map.put("organization", user.getOrganization());
map.put("gender", user.getGender());
map.put("avatar", user.getAvatar()); map.put("avatar", user.getAvatar());
map.put("status", user.getStatus()); map.put("status", user.getStatus());
map.put("userSource", user.getUserSource()); map.put("userSource", user.getUserSource());
@ -134,6 +150,7 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
String email = (String) params.get("email"); String email = (String) params.get("email");
String phone = (String) params.get("phone"); String phone = (String) params.get("phone");
String organization = (String) params.get("organization"); String organization = (String) params.get("organization");
String gender = normalizeJudgeGender(params.get("gender"));
if (username == null || username.isBlank()) { if (username == null || username.isBlank()) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "用户名不能为空"); throw BusinessException.of(ErrorCode.BAD_REQUEST, "用户名不能为空");
@ -141,6 +158,9 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
if (password == null || password.isBlank()) { if (password == null || password.isBlank()) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "密码不能为空"); throw BusinessException.of(ErrorCode.BAD_REQUEST, "密码不能为空");
} }
if (gender == null) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "请选择性别");
}
Long currentTenantId = SecurityUtil.getCurrentTenantId(); Long currentTenantId = SecurityUtil.getCurrentTenantId();
@ -161,6 +181,7 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
user.setEmail(email); user.setEmail(email);
user.setPhone(phone); user.setPhone(phone);
user.setOrganization(organization); user.setOrganization(organization);
user.setGender(gender);
user.setUserSource(UserSource.ADMIN_CREATED.getValue()); user.setUserSource(UserSource.ADMIN_CREATED.getValue());
user.setUserType(UserType.ADULT.getValue()); user.setUserType(UserType.ADULT.getValue());
user.setStatus(CommonStatus.ENABLED.getValue()); user.setStatus(CommonStatus.ENABLED.getValue());
@ -269,6 +290,9 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
if (params.containsKey("organization")) { if (params.containsKey("organization")) {
user.setOrganization((String) params.get("organization")); user.setOrganization((String) params.get("organization"));
} }
if (params.containsKey("gender")) {
user.setGender(normalizeJudgeGender(params.get("gender")));
}
if (params.containsKey("password")) { if (params.containsKey("password")) {
String newPassword = (String) params.get("password"); String newPassword = (String) params.get("password");
if (newPassword != null && !newPassword.isBlank()) { if (newPassword != null && !newPassword.isBlank()) {

View File

@ -244,6 +244,20 @@ const fetchReviewRules = async () => {
} catch { /* */ } } catch { /* */ }
} }
/** 提交前对字符串字段 trim再参与校验与保存避免首尾空格导致误判或脏数据 */
function trimContestFormStrings() {
form.organizers = String(form.organizers ?? "").trim()
form.coOrganizers = String(form.coOrganizers ?? "").trim()
form.sponsors = String(form.sponsors ?? "").trim()
form.contestName = String(form.contestName ?? "").trim()
if (typeof form.content === "string") form.content = form.content.trim()
if (Array.isArray(form.targetCities)) {
form.targetCities = form.targetCities
.map((c) => String(c ?? "").trim())
.filter((c) => c.length > 0)
}
}
const rules = { const rules = {
contestName: [{ required: true, message: "请输入活动名称", trigger: "blur" }], contestName: [{ required: true, message: "请输入活动名称", trigger: "blur" }],
contestType: [{ required: true, message: "请选择活动类型", trigger: "change" }], contestType: [{ required: true, message: "请选择活动类型", trigger: "change" }],
@ -413,6 +427,7 @@ const loadContestData = async () => {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
trimContestFormStrings()
await formRef.value?.validate() await formRef.value?.validate()
submitLoading.value = true submitLoading.value = true

View File

@ -334,6 +334,14 @@ const form = reactive<{
password: "", password: "",
}) })
/** 提交前对文本字段 trim再参与校验与保存 */
function trimJudgeFormStrings() {
form.username = String(form.username ?? "").trim()
form.nickname = String(form.nickname ?? "").trim()
form.organization = String(form.organization ?? "").trim()
form.phone = String(form.phone ?? "").trim()
}
// //
const rules = computed(() => ({ const rules = computed(() => ({
username: isEditing.value username: isEditing.value
@ -511,6 +519,7 @@ const handleBatchDelete = async () => {
// //
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
trimJudgeFormStrings()
await formRef.value?.validate() await formRef.value?.validate()
submitLoading.value = true submitLoading.value = true

View File

@ -4,12 +4,10 @@
<a-card class="mb-4"> <a-card class="mb-4">
<template #title>公告管理</template> <template #title>公告管理</template>
<template #extra> <template #extra>
<a-button <a-button v-permission="'notice:create'" type="primary" @click="handleAdd">
v-permission="'notice:create'" <template #icon>
type="primary" <PlusOutlined />
@click="handleAdd" </template>
>
<template #icon><PlusOutlined /></template>
新建公告 新建公告
</a-button> </a-button>
</template> </template>
@ -18,48 +16,38 @@
<!-- 搜索表单 --> <!-- 搜索表单 -->
<a-form layout="inline" :model="searchForm" class="search-form" @finish="handleSearch"> <a-form layout="inline" :model="searchForm" class="search-form" @finish="handleSearch">
<a-form-item label="标题名称"> <a-form-item label="标题名称">
<a-input <a-input v-model:value="searchForm.title" placeholder="请输入标题名称" style="width: 200px" allow-clear />
v-model:value="searchForm.title"
placeholder="请输入标题名称"
style="width: 200px"
allow-clear
/>
</a-form-item> </a-form-item>
<a-form-item label="发布状态"> <a-form-item label="发布状态">
<a-select v-model:value="searchForm.status" placeholder="全部" allow-clear style="width: 110px" @change="handleSearch"> <a-select v-model:value="searchForm.status" placeholder="全部" allow-clear style="width: 110px"
@change="handleSearch">
<a-select-option value="published">已发布</a-select-option> <a-select-option value="published">已发布</a-select-option>
<a-select-option value="unpublished">未发布</a-select-option> <a-select-option value="unpublished">未发布</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="发布时间"> <a-form-item label="发布时间">
<a-range-picker <a-range-picker v-model:value="searchForm.publishDateRange" style="width: 240px" allow-clear
v-model:value="searchForm.publishDateRange" @change="handleSearch" />
style="width: 240px"
allow-clear
@change="handleSearch"
/>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-button type="primary" html-type="submit"> <a-button type="primary" html-type="submit">
<template #icon><SearchOutlined /></template> <template #icon>
<SearchOutlined />
</template>
搜索 搜索
</a-button> </a-button>
<a-button style="margin-left: 8px" @click="handleReset"> <a-button style="margin-left: 8px" @click="handleReset">
<template #icon><ReloadOutlined /></template> <template #icon>
<ReloadOutlined />
</template>
重置 重置
</a-button> </a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>
<!-- 数据表格 --> <!-- 数据表格 -->
<a-table <a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="pagination" row-key="id"
:columns="columns" @change="handleTableChange">
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record, index }"> <template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'index'"> <template v-if="column.key === 'index'">
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }} {{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
@ -85,8 +73,10 @@
<a-space> <a-space>
<!-- 未发布发布编辑删除 --> <!-- 未发布发布编辑删除 -->
<template v-if="!record.publishTime"> <template v-if="!record.publishTime">
<a-button v-permission="'notice:update'" type="link" size="small" style="color: #10b981" @click="handleTogglePublish(record)">发布</a-button> <a-button v-permission="'notice:update'" type="link" size="small" style="color: #10b981"
<a-button v-permission="'notice:update'" type="link" size="small" @click="handleEdit(record)">编辑</a-button> @click="handleTogglePublish(record)">发布</a-button>
<a-button v-permission="'notice:update'" type="link" size="small"
@click="handleEdit(record)">编辑</a-button>
<a-popconfirm v-permission="'notice:delete'" title="确定要删除这条公告吗?" @confirm="handleDelete(record.id)"> <a-popconfirm v-permission="'notice:delete'" title="确定要删除这条公告吗?" @confirm="handleDelete(record.id)">
<a-button type="link" danger size="small">删除</a-button> <a-button type="link" danger size="small">删除</a-button>
</a-popconfirm> </a-popconfirm>
@ -94,7 +84,8 @@
<!-- 已发布查看取消发布 --> <!-- 已发布查看取消发布 -->
<template v-else> <template v-else>
<a-button type="link" size="small" @click="handleView(record)">查看</a-button> <a-button type="link" size="small" @click="handleView(record)">查看</a-button>
<a-button v-permission="'notice:update'" type="link" size="small" style="color: #f59e0b" @click="handleTogglePublish(record)">取消发布</a-button> <a-button v-permission="'notice:update'" type="link" size="small" style="color: #f59e0b"
@click="handleTogglePublish(record)">取消发布</a-button>
</template> </template>
</a-space> </a-space>
</template> </template>
@ -102,12 +93,7 @@
</a-table> </a-table>
<!-- 公告详情弹窗 --> <!-- 公告详情弹窗 -->
<a-modal <a-modal v-model:open="viewModalVisible" :title="currentNotice?.title" :footer="null" width="600px">
v-model:open="viewModalVisible"
:title="currentNotice?.title"
:footer="null"
width="600px"
>
<a-descriptions :column="1" bordered v-if="currentNotice"> <a-descriptions :column="1" bordered v-if="currentNotice">
<a-descriptions-item label="关联活动"> <a-descriptions-item label="关联活动">
{{ currentNotice.contest?.contestName || "-" }} {{ currentNotice.contest?.contestName || "-" }}
@ -122,65 +108,34 @@
</a-modal> </a-modal>
<!-- 创建/编辑公告抽屉 --> <!-- 创建/编辑公告抽屉 -->
<a-drawer <a-drawer v-model:open="drawerVisible" :title="isEditing ? '编辑公告' : '新建公告'" width="600px" placement="right">
v-model:open="drawerVisible" <a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
:title="isEditing ? '编辑公告' : '新建公告'"
width="600px"
placement="right"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
layout="vertical"
>
<a-form-item label="标题名称" name="title" required> <a-form-item label="标题名称" name="title" required>
<a-input <a-input v-model:value="formData.title" placeholder="请输入标题名称" />
v-model:value="formData.title"
placeholder="请输入标题名称"
/>
</a-form-item> </a-form-item>
<a-form-item label="关联活动" name="contestId" required> <a-form-item label="关联活动" name="contestId" required>
<a-select <a-select v-model:value="formData.contestId" placeholder="请选择关联活动" show-search
v-model:value="formData.contestId" :filter-option="filterContestOption" :loading="contestsLoading">
placeholder="请选择关联活动" <a-select-option v-for="contest in contestsList" :key="contest.id" :value="contest.id">
show-search
:filter-option="filterContestOption"
:loading="contestsLoading"
>
<a-select-option
v-for="contest in contestsList"
:key="contest.id"
:value="contest.id"
>
{{ contest.contestName }} {{ contest.contestName }}
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="正文" name="content" required> <a-form-item label="正文" name="content" required>
<a-textarea <a-textarea v-model:value="formData.content" placeholder="请输入正文内容" :rows="8" />
v-model:value="formData.content"
placeholder="请输入正文内容"
:rows="8"
/>
</a-form-item> </a-form-item>
<a-form-item label="活动附件"> <a-form-item label="活动附件">
<a-upload <a-upload v-model:file-list="attachmentFileList" :before-upload="beforeUpload" :custom-request="handleUpload"
v-model:file-list="attachmentFileList" :on-remove="handleRemoveAttachment">
:before-upload="beforeUpload"
:custom-request="handleUpload"
:on-remove="handleRemoveAttachment"
>
<a-button> <a-button>
<template #icon><UploadOutlined /></template> <template #icon>
<UploadOutlined />
</template>
上传附件 上传附件
</a-button> </a-button>
</a-upload> </a-upload>
<div v-if="attachmentList.length > 0" style="margin-top: 16px"> <div v-if="attachmentList.length > 0" style="margin-top: 16px">
<div <div v-for="attachment in attachmentList" :key="attachment.id" style="
v-for="attachment in attachmentList"
:key="attachment.id"
style="
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@ -188,23 +143,13 @@
border: 1px solid #d9d9d9; border: 1px solid #d9d9d9;
border-radius: 4px; border-radius: 4px;
margin-bottom: 8px; margin-bottom: 8px;
" ">
>
<span>{{ attachment.fileName }}</span> <span>{{ attachment.fileName }}</span>
<a-space> <a-space>
<a-button <a-button type="link" size="small" @click="handleDownload(attachment)">
type="link"
size="small"
@click="handleDownload(attachment)"
>
下载 下载
</a-button> </a-button>
<a-button <a-button type="link" danger size="small" @click="handleDeleteAttachment(attachment.id)">
type="link"
danger
size="small"
@click="handleDeleteAttachment(attachment.id)"
>
删除 删除
</a-button> </a-button>
</a-space> </a-space>
@ -216,11 +161,7 @@
<template #footer> <template #footer>
<a-space> <a-space>
<a-button @click="handleCancel">取消</a-button> <a-button @click="handleCancel">取消</a-button>
<a-button <a-button type="primary" :loading="submitLoading" @click="handleSubmit">
type="primary"
:loading="submitLoading"
@click="handleSubmit"
>
保存 保存
</a-button> </a-button>
</a-space> </a-space>
@ -275,7 +216,7 @@ const isEditing = ref(false)
const editingId = ref<number | null>(null) const editingId = ref<number | null>(null)
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const formData = reactive<CreateNoticeForm>({ const formData = reactive<CreateNoticeForm>({
contestId: 0, contestId: undefined,
title: "", title: "",
content: "", content: "",
}) })
@ -294,6 +235,12 @@ const formRules = {
content: [{ required: true, message: "请输入正文内容", trigger: "blur" }], content: [{ required: true, message: "请输入正文内容", trigger: "blur" }],
} }
/** 提交前对文本字段 trim再参与校验与保存 */
function trimNoticeFormStrings() {
formData.title = String(formData.title ?? "").trim()
formData.content = String(formData.content ?? "").trim()
}
// //
const columns = [ const columns = [
{ {
@ -453,7 +400,7 @@ const handleView = (record: ContestNotice) => {
const handleAdd = async () => { const handleAdd = async () => {
isEditing.value = false isEditing.value = false
editingId.value = null editingId.value = null
formData.contestId = 0 formData.contestId = undefined;
formData.title = "" formData.title = ""
formData.content = "" formData.content = ""
attachmentList.value = [] attachmentList.value = []
@ -570,6 +517,7 @@ const handleDeleteAttachment = async (id: number) => {
// //
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
trimNoticeFormStrings()
await formRef.value?.validate() await formRef.value?.validate()
submitLoading.value = true submitLoading.value = true
@ -597,7 +545,7 @@ const handleSubmit = async () => {
} }
message.error( message.error(
error?.response?.data?.message || error?.response?.data?.message ||
(isEditing.value ? "更新失败" : "创建失败") (isEditing.value ? "更新失败" : "创建失败")
) )
} finally { } finally {
submitLoading.value = false submitLoading.value = false
@ -620,22 +568,56 @@ $primary: #6366f1;
.notices-page { .notices-page {
:deep(.ant-card) { :deep(.ant-card) {
border: none; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); margin-bottom: 16px; border: none;
.ant-card-head { border-bottom: none; .ant-card-head-title { font-size: 18px; font-weight: 600; } } border-radius: 12px;
.ant-card-body { padding: 0; } box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
margin-bottom: 16px;
.ant-card-head {
border-bottom: none;
.ant-card-head-title {
font-size: 18px;
font-weight: 600;
}
}
.ant-card-body {
padding: 0;
}
} }
:deep(.ant-table-wrapper) { :deep(.ant-table-wrapper) {
background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; background: #fff;
.ant-table-thead > tr > th { background: #fafafa; font-weight: 600; } border-radius: 12px;
.ant-table-tbody > tr:hover > td { background: rgba($primary, 0.03); } box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
.ant-table-pagination { padding: 16px; margin: 0; } overflow: hidden;
.ant-table-thead>tr>th {
background: #fafafa;
font-weight: 600;
}
.ant-table-tbody>tr:hover>td {
background: rgba($primary, 0.03);
}
.ant-table-pagination {
padding: 16px;
margin: 0;
}
} }
} }
.search-form { .search-form {
margin-bottom: 16px; padding: 20px 24px; background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); margin-bottom: 16px;
padding: 20px 24px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
} }
.mb-4 { margin-bottom: 16px; } .mb-4 {
margin-bottom: 16px;
}
</style> </style>

View File

@ -410,6 +410,16 @@ const formatDimensions = (dimensions: any) => {
} }
} }
/** 提交前对文本字段 trim再参与校验与保存 */
function trimReviewRuleFormStrings() {
form.ruleName = String(form.ruleName ?? "").trim()
form.ruleDescription = String(form.ruleDescription ?? "").trim()
for (const dim of form.dimensions) {
dim.name = String(dim.name ?? "").trim()
dim.description = String(dim.description ?? "").trim()
}
}
// //
const handleSearch = () => { const handleSearch = () => {
search() search()
@ -518,6 +528,7 @@ const handleDelete = async (id: number) => {
// //
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
trimReviewRuleFormStrings()
await formRef.value?.validate() await formRef.value?.validate()
submitLoading.value = true submitLoading.value = true