添加后端评论的管理页面
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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 } })
|
||||||
|
}
|
||||||
|
|||||||
89
src/admin/communication/comments.vue
Normal file
89
src/admin/communication/comments.vue
Normal 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>
|
||||||
23
src/admin/communication/components/CommentActions.vue
Normal file
23
src/admin/communication/components/CommentActions.vue
Normal 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>
|
||||||
2
src/admin/communication/messages.vue
Normal file
2
src/admin/communication/messages.vue
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<template>未完待续</template>
|
||||||
|
<script lang="ts" setup></script>
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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="题目统计">
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: () =>
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user