library-picturebook-activity/lesingle-creation-frontend/src/views/contests/judges/Index.vue
zhonghua eff55b6f7b fix: 乐读派作品元数据本地同步与创作流程路由修复
- 后端:PUT 代理成功码兼容 0/200;syncWork 在状态未变时同步元数据;请求体覆盖本地 t_ugc_work
- 前端:EditInfo 强制拉详情、workId 字符串化、副标题/简介完整提交
- 评委管理:联系方式增加手机号校验
- Dubbing:仅未编目完成时按状态跳转,避免已配音作品从编辑页进配音被立即打回

Made-with: Cursor
2026-04-15 10:51:25 +08:00

561 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="judges-page">
<a-card class="mb-4">
<template #title>评委管理</template>
<template #extra>
<a-space>
<a-button v-permission="'judge:create'" type="primary" @click="handleAdd">
<template #icon>
<PlusOutlined />
</template>
新增
</a-button>
<a-tooltip title="功能开发中,敬请期待">
<a-button v-permission="'judge:create'" disabled>
<template #icon>
<UploadOutlined />
</template>
导入
</a-button>
</a-tooltip>
<a-tooltip title="功能开发中,敬请期待">
<a-button v-permission="'judge:read'" disabled>
<template #icon>
<DownloadOutlined />
</template>
导出
</a-button>
</a-tooltip>
<a-popconfirm v-permission="'judge:delete'" title="确定要删除选中的评委吗?"
:disabled="selectedRowKeys.length === 0 || selectedRows.every(r => r.isPlatform)"
@confirm="handleBatchDelete">
<a-button danger :disabled="selectedRowKeys.length === 0 || selectedRows.every(r => r.isPlatform)">
<template #icon>
<DeleteOutlined />
</template>
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</a-card>
<!-- 搜索表单 -->
<a-form :model="searchParams" layout="inline" class="search-form" @finish="handleSearch">
<a-form-item label="所属单位">
<a-input v-model:value="searchParams.organization" placeholder="请输入所属单位" allow-clear style="width: 200px" />
</a-form-item>
<a-form-item label="姓名">
<a-input v-model:value="searchParams.nickname" placeholder="请输入姓名" allow-clear style="width: 150px" />
</a-form-item>
<a-form-item label="账号">
<a-input v-model:value="searchParams.username" placeholder="请输入账号" allow-clear style="width: 150px" />
</a-form-item>
<a-form-item label="状态">
<a-select v-model:value="searchParams.status" placeholder="请选择状态" allow-clear style="width: 120px"
@change="handleSearch">
<a-select-option value="disabled">停用</a-select-option>
<a-select-option value="enabled">启用</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit">
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<a-button style="margin-left: 8px" @click="handleReset">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-form-item>
</a-form>
<!-- 数据表格 -->
<a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="pagination"
:row-selection="rowSelection" row-key="id" @change="handleTableChange">
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'index'">
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
</template>
<template v-else-if="column.key === 'organization'">
{{ record.organization || "-" }}
</template>
<template v-else-if="column.key === 'gender'">
<span v-if="record.gender === 'male'">男</span>
<span v-else-if="record.gender === 'female'">女</span>
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'source'">
<a-tag v-if="record.isPlatform" color="blue">平台</a-tag>
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="record.status === 'enabled' ? 'success' : 'error'">
{{ record.status === "enabled" ? "启用" : "停用" }}
</a-tag>
</template>
<template v-else-if="column.key === 'ongoingContests'">
<template v-if="record.contestJudges && record.contestJudges.length > 0">
<a-tooltip>
<template #title>
<div v-for="cj in record.contestJudges" :key="cj.contest.id">
{{ cj.contest.contestName }}
</div>
</template>
<a-tag color="processing">
{{ record.contestJudges.length }}个活动
</a-tag>
</a-tooltip>
</template>
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<template v-if="!record.isPlatform">
<a-button v-permission="'judge:update'" type="link" size="small" @click="handleToggleStatus(record)">
{{ record.status === "enabled" ? "冻结" : "解冻" }}
</a-button>
</template>
<template v-if="!record.isPlatform">
<a-button v-permission="'judge:update'" type="link" size="small" @click="handleEdit(record)">
编辑
</a-button>
</template>
<a-popconfirm v-if="!record.isPlatform" v-permission="'judge:delete'" title="确定要删除这个评委吗?"
@confirm="handleDelete(record.id)">
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<!-- 新增/编辑评委抽屉 -->
<a-drawer v-model:open="drawerVisible" :title="isEditing ? '编辑评委' : '新增评委'" placement="right" width="500px"
:footer-style="{ textAlign: 'right' }">
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="账号" name="username">
<a-input v-model:value="form.username" placeholder="请输入账号" :maxlength="50" :disabled="isEditing" />
</a-form-item>
<a-form-item label="姓名" name="nickname">
<a-input v-model:value="form.nickname" placeholder="请输入姓名" :maxlength="50" />
</a-form-item>
<a-form-item label="性别" name="gender">
<a-radio-group v-model:value="form.gender">
<a-radio value="male">男</a-radio>
<a-radio value="female">女</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="所属单位" name="organization">
<a-input v-model:value="form.organization" placeholder="请输入所属单位" :maxlength="100" />
</a-form-item>
<a-form-item label="联系方式" name="phone">
<a-input v-model:value="form.phone" placeholder="请输入手机号" :maxlength="11" />
</a-form-item>
<a-form-item label="初始密码" name="password">
<a-input-password v-model:value="form.password" :placeholder="isEditing ? '请输入新密码' : '请输入初始密码'"
:maxlength="50" />
</a-form-item>
</a-form>
<template #footer>
<a-button style="margin-right: 8px" @click="handleCancel">取消</a-button>
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">
确定
</a-button>
</template>
</a-drawer>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from "vue"
import { useRoute } from "vue-router"
import { message, Modal } from "ant-design-vue"
import type { FormInstance, TableProps } from "ant-design-vue"
import {
PlusOutlined,
UploadOutlined,
DownloadOutlined,
DeleteOutlined,
SearchOutlined,
ReloadOutlined,
} from "@ant-design/icons-vue"
import { useListRequest } from "@/composables/useListRequest"
import { contestsApi } from "@/api/contests"
import {
judgesManagementApi,
type Judge,
type QueryJudgeParams,
} from "@/api/judges-management"
const route = useRoute()
const contestId = route.params.id ? Number(route.params.id) : null
const tenantCode = route.params.tenantCode as string
// 检查 contestId 是否有效
const isValidContestId =
contestId !== null && !isNaN(contestId) && contestId > 0
// 获取评委列表
const fetchJudgesList = async (params: QueryJudgeParams) => {
const response = await judgesManagementApi.getList(params)
return response
}
// 使用列表请求组合函数
const {
loading,
dataSource,
pagination,
searchParams,
fetchList,
resetSearch,
search,
handleTableChange,
} = useListRequest<Judge, QueryJudgeParams>({
requestFn: fetchJudgesList,
defaultSearchParams: {} as QueryJudgeParams,
defaultPageSize: 10,
errorMessage: "获取评委列表失败",
})
// 活动信息
const contestName = ref("")
// 表格选择
const selectedRowKeys = ref<number[]>([])
const rowSelection = computed<TableProps["rowSelection"]>(() => ({
selectedRowKeys: selectedRowKeys.value,
onChange: (keys: any) => {
selectedRowKeys.value = keys
},
}))
// 抽屉相关
const drawerVisible = ref(false)
const isEditing = ref(false)
const editingId = ref<number | null>(null)
const submitLoading = ref(false)
const formRef = ref<FormInstance>()
const form = reactive<{
username: string
nickname: string
gender: "male" | "female" | undefined
organization: string
phone: string
password: string
}>({
username: "",
nickname: "",
gender: undefined,
organization: "",
phone: "",
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(() => ({
username: isEditing.value
? []
: [{ required: true, message: "请输入账号", trigger: "blur" }],
nickname: [{ required: true, message: "请输入姓名", trigger: "blur" }],
gender: [{ required: true, message: "请选择性别", trigger: "change" }],
organization: [{ required: true, message: "请输入所属单位", trigger: "blur" }],
phone: [
{ required: true, message: "请输入手机号", trigger: "blur" },
{
pattern: /^1[3-9]\d{9}$/,
message: "手机号格式不正确",
trigger: "blur",
},
],
password: isEditing.value
? []
: [{ required: true, message: "请输入初始密码", trigger: "blur" }],
}))
// 表格列定义
const columns = [
{
title: "序号",
key: "index",
width: 70,
},
{
title: "来源",
key: "source",
width: 80,
},
{
title: "所属单位",
key: "organization",
dataIndex: "organization",
width: 150,
},
{
title: "姓名",
dataIndex: "nickname",
key: "nickname",
width: 120,
},
{
title: "性别",
key: "gender",
width: 80,
},
{
title: "账号",
dataIndex: "username",
key: "username",
width: 150,
},
{
title: "联系方式",
dataIndex: "phone",
key: "phone",
width: 130,
},
{
title: "关联进行中的活动",
key: "ongoingContests",
width: 150,
},
{
title: "状态",
key: "status",
width: 80,
},
{
title: "操作",
key: "action",
width: 180,
fixed: "right" as const,
},
]
// 加载活动信息
const loadContestInfo = async () => {
// 如果 contestId 无效,跳过加载
if (!isValidContestId) {
return
}
try {
const contest = await contestsApi.getDetail(contestId!)
contestName.value = contest.contestName
} catch (error) {
console.error("获取活动信息失败", error)
}
}
// 搜索
const handleSearch = () => {
search()
}
// 重置搜索
const handleReset = () => {
resetSearch()
}
// 新增
const handleAdd = () => {
isEditing.value = false
editingId.value = null
drawerVisible.value = true
form.username = ""
form.nickname = ""
form.gender = undefined
form.organization = ""
form.phone = ""
form.password = ""
}
// 编辑
const handleEdit = (record: Judge) => {
isEditing.value = true
editingId.value = record.id
drawerVisible.value = true
form.username = record.username || ""
form.nickname = record.nickname || ""
form.gender = record.gender as "male" | "female" | undefined
form.organization = record.organization || ""
form.phone = record.phone || ""
form.password = ""
}
// 切换状态(冻结/解冻)+ 二次确认
const handleToggleStatus = (record: Judge) => {
const isFreeze = record.status === "enabled"
Modal.confirm({
title: isFreeze ? '确定冻结该评委?' : '确定解冻该评委?',
content: isFreeze
? `冻结后「${record.nickname}」将无法登录评委端,进行中的评审任务将暂停`
: `解冻后「${record.nickname}」将恢复登录和评审功能`,
okText: isFreeze ? '确定冻结' : '确定解冻',
okType: isFreeze ? 'danger' : 'primary',
onOk: async () => {
try {
if (isFreeze) {
await judgesManagementApi.freeze(record.id)
message.success("冻结成功")
} else {
await judgesManagementApi.unfreeze(record.id)
message.success("解冻成功")
}
fetchList()
} catch (error: any) {
message.error(error?.response?.data?.message || "操作失败")
}
},
})
}
// 删除
const handleDelete = async (id: number) => {
try {
await judgesManagementApi.delete(id)
message.success("删除成功")
fetchList()
} catch (error: any) {
message.error(error?.response?.data?.message || "删除失败")
}
}
// 批量删除
const handleBatchDelete = async () => {
if (selectedRowKeys.value.length === 0) return
try {
await judgesManagementApi.batchDelete(selectedRowKeys.value)
message.success("批量删除成功")
selectedRowKeys.value = []
fetchList()
} catch (error: any) {
message.error(error?.response?.data?.message || "批量删除失败")
}
}
// 提交表单
const handleSubmit = async () => {
try {
trimJudgeFormStrings()
await formRef.value?.validate()
submitLoading.value = true
if (isEditing.value && editingId.value) {
// 编辑评委
await judgesManagementApi.update(editingId.value, {
nickname: form.nickname,
gender: form.gender,
organization: form.organization,
phone: form.phone,
...(form.password && { password: form.password }),
})
message.success("编辑成功")
} else {
// 新增评委
await judgesManagementApi.create({
username: form.username,
nickname: form.nickname,
gender: form.gender!,
organization: form.organization,
phone: form.phone,
password: form.password,
status: "enabled",
})
message.success("创建成功")
}
drawerVisible.value = false
fetchList()
} catch (error: any) {
if (error?.errorFields) {
return
}
message.error(
error?.response?.data?.message ||
(isEditing.value ? "编辑失败" : "创建失败")
)
} finally {
submitLoading.value = false
}
}
// 取消
const handleCancel = () => {
drawerVisible.value = false
formRef.value?.resetFields()
}
onMounted(() => {
loadContestInfo()
})
</script>
<style scoped lang="scss">
$primary: #6366f1;
.judges-page {
:deep(.ant-card) {
border: none;
border-radius: 12px;
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) {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
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 {
margin-bottom: 16px;
padding: 20px 24px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
</style>