添加后端评论的管理页面

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> <template>
<h2 class="title">网站公告</h2> <h2 class="title">网站公告</h2>
<n-data-table striped size="small" :columns="columns" :data="announcements" /> <n-data-table striped :columns="columns" :data="announcements" />
<Pagination <Pagination
:total="total" :total="total"
v-model:limit="query.limit" v-model:limit="query.limit"

View File

@@ -59,6 +59,10 @@ export function editProblem(problem: AdminProblem | BlankProblem) {
return http.put("admin/problem", problem) 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) { export function editContestProblem(problem: AdminProblem | BlankProblem) {
return http.put("admin/contest/problem", problem) return http.put("admin/contest/problem", problem)
} }
@@ -199,3 +203,13 @@ export function editAnnouncement(announcement: AnnouncementEdit) {
export function createAnnouncement(announcement: AnnouncementEdit) { export function createAnnouncement(announcement: AnnouncementEdit) {
return http.post("admin/announcement", announcement) 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 total.value = res.data.total
} }
onMounted(listContests) onMounted(listContests)
watch(query, listContests, { deep: true }) watch(() => [query.page, query.limit], listContests)
watchDebounced(() => query.keyword, listContests, { debounce: 500, maxWait: 1000 })
</script> </script>
<template> <template>
@@ -86,7 +87,7 @@ watch(query, listContests, { deep: true })
<h2 class="title">比赛列表</h2> <h2 class="title">比赛列表</h2>
<n-input v-model:value="query.keyword" placeholder="输入标题关键字" /> <n-input v-model:value="query.keyword" placeholder="输入标题关键字" />
</n-space> </n-space>
<n-data-table :columns="columns" :data="contests" size="small" /> <n-data-table :columns="columns" :data="contests" />
<Pagination <Pagination
:total="total" :total="total"
v-model:limit="query.limit" v-model:limit="query.limit"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,7 +20,8 @@
v-if="hasCommented" v-if="hasCommented"
icon="noto:star" icon="noto:star"
:width="24" :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-rate v-else size="large" v-model:value="description_rating" />
</n-form-item> </n-form-item>
@@ -36,7 +37,8 @@
v-if="hasCommented" v-if="hasCommented"
icon="noto:star" icon="noto:star"
:width="24" :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-rate v-else size="large" v-model:value="difficulty_rating" />
</n-form-item> </n-form-item>
@@ -50,7 +52,8 @@
v-if="hasCommented" v-if="hasCommented"
icon="noto:star" icon="noto:star"
:width="24" :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-rate v-else size="large" v-model:value="comprehensive_rating" />
</n-form-item> </n-form-item>

View File

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

View File

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

View File

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

View File

@@ -182,5 +182,15 @@ export const admins: RouteRecordRaw = {
component: () => import("admin/announcement/detail.vue"), component: () => import("admin/announcement/detail.vue"),
props: true, 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", 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: "用户", key: "user", disabled: true },
{ {
label: () => label: () =>

View File

@@ -360,3 +360,14 @@ export interface CreateMessage {
submission: string submission: string
message: 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
}