添加后端评论的管理页面

This commit is contained in:
2024-07-02 22:18:16 +08:00
parent 60c1a495f2
commit 73eb288d3c
18 changed files with 198 additions and 22 deletions

View File

@@ -81,7 +81,7 @@ watch(query, listAnnouncements, { deep: true })
<template>
<h2 class="title">网站公告</h2>
<n-data-table striped size="small" :columns="columns" :data="announcements" />
<n-data-table striped :columns="columns" :data="announcements" />
<Pagination
:total="total"
v-model:limit="query.limit"

View File

@@ -59,6 +59,10 @@ export function editProblem(problem: AdminProblem | BlankProblem) {
return http.put("admin/problem", problem)
}
export function toggleProblemVisible(problemID: number) {
return http.put("admin/problem/visible", { id: problemID })
}
export function editContestProblem(problem: AdminProblem | BlankProblem) {
return http.put("admin/contest/problem", problem)
}
@@ -199,3 +203,13 @@ export function editAnnouncement(announcement: AnnouncementEdit) {
export function createAnnouncement(announcement: AnnouncementEdit) {
return http.post("admin/announcement", announcement)
}
export function getCommentList(offset = 0, limit = 10, problem: string) {
return http.get("admin/comment", {
params: { offset, limit, problem },
})
}
export function deleteComment(id: number) {
return http.delete("admin/comment", { params: { id } })
}

View File

@@ -0,0 +1,89 @@
<template>
<n-space justify="space-between" class="titleWrapper">
<h2 class="title">评论列表</h2>
<n-input
v-model:value="query.problem"
clearable
placeholder="输入题目序号"
/>
</n-space>
<n-data-table striped :columns="columns" :data="comments" />
<Pagination
:total="total"
v-model:limit="query.limit"
v-model:page="query.page"
/>
</template>
<script lang="ts" setup>
import { NButton } from "naive-ui"
import { getCommentList } from "../api"
import Pagination from "~/shared/components/Pagination.vue"
import { Comment } from "~/utils/types"
import { parseTime } from "~/utils/functions"
import CommentActions from "./components/CommentActions.vue"
const comments = ref<Comment[]>([])
const total = ref(0)
const query = reactive({
limit: 10,
page: 1,
problem: "",
})
const columns: DataTableColumn<Comment>[] = [
{
title: "题目",
key: "problem",
width: 100,
render: (row) =>
h(NButton, { text: true, type: "info" }, () => row.problem),
},
{
title: "提交",
key: "submission",
width: 200,
render: (row) =>
h(NButton, { text: true, type: "info" }, () =>
row.submission.slice(0, 12),
),
},
{ title: "描述评分", key: "description_rating", width: 100 },
{ title: "难度评分", key: "difficulty_rating", width: 100 },
{ title: "综合评分", key: "comprehensive_rating", width: 100 },
{
title: "时间",
key: "create_time",
render: (row) => parseTime(row.create_time, "YYYY-MM-DD HH:mm:ss"),
width: 200,
},
{ title: "内容", key: "content", maxWidth: 300, ellipsis: true },
{
title: "选项",
key: "action",
width: 100,
render: (row) => h(CommentActions, { commentID: row.id, onDeleted: listComments }),
},
]
async function listComments() {
const offset = (query.page - 1) * query.limit
const res = await getCommentList(offset, query.limit, query.problem)
comments.value = res.data.results
total.value = res.data.total
}
onMounted(listComments)
watchDebounced(() => [query.problem], listComments, {
debounce: 500,
maxWait: 1000,
})
</script>
<style scoped>
.titleWrapper {
margin-bottom: 16px;
}
.title {
margin: 0;
}
</style>

View File

@@ -0,0 +1,23 @@
<script lang="ts" setup>
import { deleteComment } from '~/admin/api';
const props = defineProps<{commentID: number}>()
const emit = defineEmits(["deleted"])
const message = useMessage()
async function submit() {
await deleteComment(props.commentID)
message.success("成功删除")
emit("deleted")
}
</script>
<template>
<n-popconfirm @positive-click="submit">
<template #trigger>
<n-button secondary size="small" type="error">删除</n-button>
</template>
确定删除这条评论吗
</n-popconfirm>
</template>

View File

@@ -0,0 +1,2 @@
<template>未完待续</template>
<script lang="ts" setup></script>

View File

@@ -78,7 +78,8 @@ async function listContests() {
total.value = res.data.total
}
onMounted(listContests)
watch(query, listContests, { deep: true })
watch(() => [query.page, query.limit], listContests)
watchDebounced(() => query.keyword, listContests, { debounce: 500, maxWait: 1000 })
</script>
<template>
@@ -86,7 +87,7 @@ watch(query, listContests, { deep: true })
<h2 class="title">比赛列表</h2>
<n-input v-model:value="query.keyword" placeholder="输入标题关键字" />
</n-space>
<n-data-table :columns="columns" :data="contests" size="small" />
<n-data-table :columns="columns" :data="contests" />
<Pagination
:total="total"
v-model:limit="query.limit"

View File

@@ -74,7 +74,7 @@ watch(query, getList, { deep: true })
v-model:value="query.keyword"
placeholder="搜索标题或编号"
/>
<n-data-table size="small" :columns="columns" :data="problems" />
<n-data-table striped :columns="columns" :data="problems" />
<Pagination
:total="total"
v-model:limit="query.limit"

View File

@@ -1,5 +1,10 @@
<script setup lang="ts">
import { getProblemList, getProblem, editProblem } from "../api"
import {
getProblemList,
getProblem,
editProblem,
toggleProblemVisible,
} from "../api"
import Pagination from "~/shared/components/Pagination.vue"
import { NSwitch } from "naive-ui"
import { AdminProblemFiltered } from "~/utils/types"
@@ -61,7 +66,7 @@ const columns: DataTableColumn<AdminProblemFiltered>[] = [
{
title: "选项",
key: "actions",
width: 250,
width: 260,
render: (row) =>
h(Actions, {
problemID: row.id,
@@ -83,10 +88,8 @@ async function listProblems() {
problems.value = res.results
}
// 这里比较傻逼,因为我传进来的时候 filter 了而且只有 edit problem 一个接口,所以只能先 get 再 edit
async function toggleVisible(problemID: number) {
const res = await getProblem(problemID)
await editProblem({ ...res.data, visible: !res.data.visible })
await toggleProblemVisible(problemID)
problems.value = problems.value.map((it) => {
if (it.id === problemID) {
it.visible = !it.visible
@@ -108,7 +111,11 @@ async function selectProblems() {
}
onMounted(listProblems)
watch(query, listProblems, { deep: true })
watch(() => [query.limit, query.page], listProblems)
watchDebounced(() => query.keyword, listProblems, {
debounce: 500,
maxWait: 1000,
})
</script>
<template>
@@ -128,7 +135,7 @@ watch(query, listProblems, { deep: true })
<n-input v-model:value="query.keyword" placeholder="输入标题关键字" />
</n-space>
</n-space>
<n-data-table striped size="small" :columns="columns" :data="problems" />
<n-data-table striped :columns="columns" :data="problems" />
<Pagination
:total="total"
v-model:limit="query.limit"

View File

@@ -214,7 +214,6 @@ onMounted(() => {
<n-data-table
:single-line="false"
striped
size="small"
:columns="serverColumns"
:data="servers"
/>
@@ -230,7 +229,6 @@ onMounted(() => {
</template>
<n-data-table
striped
size="small"
class="table"
:columns="testcaseColumns"
:data="testcases"

View File

@@ -176,7 +176,6 @@ watch(query, listUsers, { deep: true })
<n-data-table
:data="users"
:columns="columns"
size="small"
striped
:row-key="rowKey"
@update:checked-row-keys="chooseUsers"

View File

@@ -197,7 +197,6 @@ export function createComment(data: {
difficulty_rating: number
comprehensive_rating: number
content: string
submission_id?: string
}) {
return http.post("comment", data)
}

View File

@@ -20,7 +20,8 @@
v-if="hasCommented"
icon="noto:star"
:width="24"
v-for="i in description_rating"
v-for="(_, i) in description_rating"
:key="i"
/>
<n-rate v-else size="large" v-model:value="description_rating" />
</n-form-item>
@@ -36,7 +37,8 @@
v-if="hasCommented"
icon="noto:star"
:width="24"
v-for="i in difficulty_rating"
v-for="(_, i) in difficulty_rating"
:key="i"
/>
<n-rate v-else size="large" v-model:value="difficulty_rating" />
</n-form-item>
@@ -50,7 +52,8 @@
v-if="hasCommented"
icon="noto:star"
:width="24"
v-for="i in difficulty_rating"
v-for="(_, i) in difficulty_rating"
:key="i"
/>
<n-rate v-else size="large" v-model:value="comprehensive_rating" />
</n-form-item>

View File

@@ -212,7 +212,9 @@ watch(
origin: { x: 0.5, y: 0.4 },
})
// 题目在第一次完成之后,弹出点评框
showCommentPanel()
if (!contestID) {
showCommentPanel()
}
}
},
)

View File

@@ -72,7 +72,7 @@ onBeforeUnmount(() => {
<n-tab-pane name="content" tab="题目描述">
<ProblemContent />
</n-tab-pane>
<n-tab-pane name="comment" tab="题目点评">
<n-tab-pane v-if="!props.contestID" name="comment" tab="题目点评">
<ProblemComment />
</n-tab-pane>
<n-tab-pane name="info" tab="题目统计">
@@ -90,7 +90,7 @@ onBeforeUnmount(() => {
<n-tab-pane name="editor" tab="代码编辑">
<Editor />
</n-tab-pane>
<n-tab-pane name="comment" tab="题目点评">
<n-tab-pane v-if="!props.contestID" name="comment" tab="题目点评">
<ProblemComment />
</n-tab-pane>
<n-tab-pane name="info" tab="题目统计">

View File

@@ -68,7 +68,6 @@
<n-data-table
v-if="list.length"
striped
size="small"
:columns="columns"
:data="list"
/>

View File

@@ -182,5 +182,15 @@ export const admins: RouteRecordRaw = {
component: () => import("admin/announcement/detail.vue"),
props: true,
},
{
path: "comment/list",
name: "admin comment list",
component: () => import("admin/communication/comments.vue"),
},
{
path: "message/list",
name: "admin message list",
component: () => import("admin/communication/messages.vue"),
},
],
}

View File

@@ -35,6 +35,25 @@ const options: MenuOption[] = [
),
key: "admin problem create",
},
{label: "交流", key: "communication", disabled: true},
{
label: () =>
h(
RouterLink,
{ to: "/admin/comment/list" },
{ default: () => "评论列表" },
),
key: "admin comment list",
},
{
label: () =>
h(
RouterLink,
{ to: "/admin/message/list" },
{ default: () => "消息列表" },
),
key: "admin message list",
},
{ label: "用户", key: "user", disabled: true },
{
label: () =>

View File

@@ -360,3 +360,14 @@ export interface CreateMessage {
submission: string
message: string
}
export interface Comment {
id: number
problem: string
submission: string
content: string
description_rating: 1 | 2 | 3 | 4 | 5
difficulty_rating: 1 | 2 | 3 | 4 | 5
comprehensive_rating: 1 | 2 | 3 | 4 | 5
create_time: Date
}