library-picturebook-activity/lesingle-creation-frontend/src/views/activities/PresetComments.vue

480 lines
12 KiB
Vue
Raw Normal View History

<template>
<div class="preset-comments-page">
<a-card class="mb-4">
<template #title>预设评语管理</template>
<template #extra>
<a-space>
<a-button
type="primary"
:disabled="!currentContestId"
@click="handleAdd"
>
<template #icon><PlusOutlined /></template>
新增
</a-button>
<a-popconfirm
title="确定要删除选中的评语吗?"
:disabled="selectedRowKeys.length === 0"
@confirm="handleBatchDelete"
>
<a-button danger :disabled="selectedRowKeys.length === 0">
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-popconfirm>
<a-button
:disabled="!currentContestId || dataSource.length === 0"
@click="handleOpenSync"
>
<template #icon><SyncOutlined /></template>
同步到其他活动
</a-button>
</a-space>
</template>
</a-card>
<!-- 数据表格 -->
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:row-selection="rowSelection"
row-key="id"
:pagination="false"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'index'">
{{ index + 1 }}
</template>
<template v-else-if="column.key === 'content'">
<a-tooltip :title="record.content">
<span class="content-cell">{{ record.content }}</span>
</a-tooltip>
</template>
<template v-else-if="column.key === 'useCount'">
<a-tag v-if="record.useCount > 0" color="blue">
{{ record.useCount }}
</a-tag>
<span v-else>0次</span>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-popconfirm
title="确定要删除这条评语吗?"
@confirm="handleDelete(record.id)"
>
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<!-- 新增/编辑评语弹框 -->
<a-modal
v-model:open="modalVisible"
:title="isEditing ? '编辑评语' : '新增评语'"
:confirm-loading="submitLoading"
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<a-form-item label="评语内容" name="content">
<a-textarea
v-model:value="form.content"
placeholder="请输入评语内容"
:rows="4"
:maxlength="500"
show-count
/>
</a-form-item>
</a-form>
</a-modal>
<!-- 同步评语弹框 -->
<a-modal
v-model:open="syncModalVisible"
title="同步评语到其他活动"
:confirm-loading="syncLoading"
@ok="handleSync"
@cancel="syncModalVisible = false"
>
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="目标活动">
<a-select
v-model:value="syncTargetContestIds"
mode="multiple"
placeholder="请选择要同步到的活动"
style="width: 100%"
:options="syncContestOptions"
/>
</a-form-item>
</a-form>
<a-alert
type="info"
show-icon
message="提示"
description="同步将把当前活动的所有预设评语复制到选中的目标活动中"
style="margin-top: 16px"
/>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from "vue"
import { useRoute } from "vue-router"
import { message } from "ant-design-vue"
import type { FormInstance, TableProps } from "ant-design-vue"
import {
PlusOutlined,
DeleteOutlined,
SyncOutlined,
} from "@ant-design/icons-vue"
import {
presetCommentsApi,
type PresetComment,
type JudgeContest,
} from "@/api/preset-comments"
const route = useRoute()
// 活动相关
const contestsList = ref<JudgeContest[]>([])
const contestsLoading = ref(false)
const currentContestId = ref<number | undefined>(undefined)
// 表格数据
const loading = ref(false)
const dataSource = ref<PresetComment[]>([])
// 表格选择
const selectedRowKeys = ref<number[]>([])
const rowSelection = computed<TableProps["rowSelection"]>(() => ({
selectedRowKeys: selectedRowKeys.value,
onChange: (keys: any) => {
selectedRowKeys.value = keys
},
}))
// 弹框相关
const modalVisible = ref(false)
const isEditing = ref(false)
const editingId = ref<number | null>(null)
const submitLoading = ref(false)
const formRef = ref<FormInstance>()
const form = reactive<{
content: string
}>({
content: "",
})
// 表单验证规则
const rules = {
content: [{ required: true, message: "请输入评语内容", trigger: "blur" }],
}
// 同步弹框相关
const syncModalVisible = ref(false)
const syncLoading = ref(false)
const syncTargetContestIds = ref<number[]>([])
const syncContestOptions = computed(() => {
return contestsList.value
.filter((c) => c.id !== currentContestId.value)
.map((c) => ({
value: c.id,
label: c.contestName,
}))
})
// 表格列定义
const columns = [
{
title: "序号",
key: "index",
width: 70,
},
{
title: "评语内容",
key: "content",
dataIndex: "content",
ellipsis: true,
},
{
title: "使用次数",
key: "useCount",
dataIndex: "useCount",
width: 100,
},
{
title: "操作",
key: "action",
width: 150,
fixed: "right" as const,
},
]
// 加载评委的活动列表
const loadContests = async () => {
contestsLoading.value = true
try {
const data = await presetCommentsApi.getJudgeContests()
contestsList.value = data
// 从 URL 参数获取 contestId
const urlContestId = route.query.contestId
? Number(route.query.contestId)
: null
// 如果 URL 有 contestId 且在列表中存在,选中它;否则选第一个
if (urlContestId && data.some((c) => c.id === urlContestId)) {
currentContestId.value = urlContestId
} else if (data.length > 0) {
currentContestId.value = data[0].id
}
if (currentContestId.value) {
loadComments()
}
} catch (error: any) {
message.error(error?.response?.data?.message || "获取活动列表失败")
} finally {
contestsLoading.value = false
}
}
// 加载评语列表
const loadComments = async () => {
if (!currentContestId.value) return
loading.value = true
try {
const data = await presetCommentsApi.getList(currentContestId.value)
dataSource.value = data
} catch (error: any) {
message.error(error?.response?.data?.message || "获取评语列表失败")
} finally {
loading.value = false
}
}
// 新增
const handleAdd = () => {
isEditing.value = false
editingId.value = null
modalVisible.value = true
form.content = ""
}
// 编辑
const handleEdit = (record: PresetComment) => {
isEditing.value = true
editingId.value = record.id
modalVisible.value = true
form.content = record.content
}
// 删除
const handleDelete = async (id: number) => {
try {
await presetCommentsApi.delete(id)
message.success("删除成功")
loadComments()
} catch (error: any) {
message.error(error?.response?.data?.message || "删除失败")
}
}
// 批量删除
const handleBatchDelete = async () => {
if (selectedRowKeys.value.length === 0) return
try {
await presetCommentsApi.batchDelete(selectedRowKeys.value)
message.success("批量删除成功")
selectedRowKeys.value = []
loadComments()
} catch (error: any) {
message.error(error?.response?.data?.message || "批量删除失败")
}
}
// 提交表单
const handleSubmit = async () => {
try {
await formRef.value?.validate()
submitLoading.value = true
if (isEditing.value && editingId.value) {
await presetCommentsApi.update(editingId.value, {
content: form.content,
})
message.success("编辑成功")
} else {
await presetCommentsApi.create({
contestId: currentContestId.value!,
content: form.content,
})
message.success("创建成功")
}
modalVisible.value = false
loadComments()
} catch (error: any) {
if (error?.errorFields) {
return
}
message.error(
error?.response?.data?.message ||
(isEditing.value ? "编辑失败" : "创建失败"),
)
} finally {
submitLoading.value = false
}
}
// 取消
const handleCancel = () => {
modalVisible.value = false
formRef.value?.resetFields()
}
// 打开同步弹框
const handleOpenSync = () => {
syncTargetContestIds.value = []
syncModalVisible.value = true
}
// 同步评语
const handleSync = async () => {
if (syncTargetContestIds.value.length === 0) {
message.warning("请选择目标活动")
return
}
syncLoading.value = true
try {
const result = await presetCommentsApi.sync({
sourceContestId: currentContestId.value!,
targetContestIds: syncTargetContestIds.value,
})
message.success(`${result.message},共同步 ${result.count} 条评语`)
syncModalVisible.value = false
} catch (error: any) {
message.error(error?.response?.data?.message || "同步失败")
} finally {
syncLoading.value = false
}
}
onMounted(() => {
loadContests()
})
</script>
<style scoped lang="scss">
// 主色调
$primary: #1890ff;
$primary-dark: #0958d9;
$gradient-primary: linear-gradient(135deg, $primary 0%, $primary-dark 100%);
.preset-comments-page {
// 标题卡片样式
:deep(.ant-card) {
border: none;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
margin-bottom: 16px;
.ant-card-head {
border-bottom: none;
padding: 16px 24px;
.ant-card-head-title {
font-size: 18px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
}
.ant-card-body {
padding: 0;
}
}
// 渐变主按钮样式
:deep(.ant-btn-primary) {
background: $gradient-primary;
border: none;
box-shadow: 0 4px 12px rgba($primary, 0.35);
transition: all 0.3s ease;
&:hover {
background: linear-gradient(
135deg,
$primary-dark 0%,
darken($primary-dark, 8%) 100%
);
box-shadow: 0 6px 16px rgba($primary, 0.45);
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
}
// 表格样式
:deep(.ant-table-wrapper) {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
.ant-table {
.ant-table-thead > tr > th {
background: #fafafa;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
border-bottom: 1px solid #f0f0f0;
}
.ant-table-tbody > tr {
transition: all 0.2s ease;
&:hover > td {
background: rgba($primary, 0.04);
}
> td {
border-bottom: 1px solid #f5f5f5;
}
}
}
.ant-table-pagination {
padding: 16px;
margin: 0;
}
}
.content-cell {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
}
</style>