update
This commit is contained in:
@@ -8,8 +8,14 @@
|
||||
style="width: 200px"
|
||||
/>
|
||||
</n-flex>
|
||||
<n-alert v-if="pinnedReports.length > 0" type="warning" :show-icon="true" style="margin-bottom: 12px">
|
||||
以下 <strong>{{ pinnedReports.length }}</strong> 位用户的 AI 分析报告已被锁定,前台将固定显示该报告:
|
||||
<n-alert
|
||||
v-if="pinnedReports.length > 0"
|
||||
type="warning"
|
||||
:show-icon="true"
|
||||
style="margin-bottom: 12px"
|
||||
>
|
||||
以下 <strong>{{ pinnedReports.length }}</strong> 位用户的 AI
|
||||
分析报告已被锁定,前台将固定显示该报告:
|
||||
<n-flex style="margin-top: 8px" :wrap="true" :size="[8, 6]">
|
||||
<n-tag
|
||||
v-for="r in pinnedReports"
|
||||
@@ -30,13 +36,24 @@
|
||||
v-model:page="query.page"
|
||||
/>
|
||||
|
||||
<n-modal v-model:show="showModal" preset="card" title="分析报告详情" style="width: 800px; max-width: 95vw">
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
preset="card"
|
||||
title="分析报告详情"
|
||||
style="width: 800px; max-width: 95vw"
|
||||
>
|
||||
<n-spin :show="loadingDetail">
|
||||
<div v-if="detail" class="detail">
|
||||
<n-descriptions :column="2" bordered size="small" class="meta">
|
||||
<n-descriptions-item label="用户">{{ detail.username }}</n-descriptions-item>
|
||||
<n-descriptions-item label="班级">{{ detail.class_name || "-" }}</n-descriptions-item>
|
||||
<n-descriptions-item label="时间" :span="2">{{ parseTime(detail.create_time, "YYYY-MM-DD HH:mm:ss") }}</n-descriptions-item>
|
||||
<n-descriptions-item label="用户">{{
|
||||
detail.username
|
||||
}}</n-descriptions-item>
|
||||
<n-descriptions-item label="班级">{{
|
||||
detail.class_name || "-"
|
||||
}}</n-descriptions-item>
|
||||
<n-descriptions-item label="时间" :span="2">{{
|
||||
parseTime(detail.create_time, "YYYY-MM-DD HH:mm:ss")
|
||||
}}</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
<n-scrollbar style="max-height: 60vh; margin-top: 12px">
|
||||
<MdPreview :model-value="detail.analysis" />
|
||||
@@ -51,7 +68,12 @@ import { MdPreview } from "md-editor-v3"
|
||||
import "md-editor-v3/lib/preview.css"
|
||||
import Pagination from "shared/components/Pagination.vue"
|
||||
import { parseTime } from "utils/functions"
|
||||
import { getAIReportList, getAIReportDetail, pinAIReport, getPinnedAIReports } from "../api"
|
||||
import {
|
||||
getAIReportList,
|
||||
getAIReportDetail,
|
||||
pinAIReport,
|
||||
getPinnedAIReports,
|
||||
} from "../api"
|
||||
import { NButton, NTag } from "naive-ui"
|
||||
|
||||
interface ReportItem {
|
||||
@@ -83,7 +105,11 @@ const columns: DataTableColumn<ReportItem>[] = [
|
||||
key: "username",
|
||||
width: 150,
|
||||
render: (row) =>
|
||||
h("span", { style: row.is_pinned ? "font-weight:600" : "" }, row.username),
|
||||
h(
|
||||
"span",
|
||||
{ style: row.is_pinned ? "font-weight:600" : "" },
|
||||
row.username,
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "AI 分析内容",
|
||||
@@ -111,7 +137,11 @@ const columns: DataTableColumn<ReportItem>[] = [
|
||||
width: 160,
|
||||
render: (row) =>
|
||||
h("span", { style: "display:flex;gap:8px" }, [
|
||||
h(NButton, { size: "small", type: "primary", onClick: () => openDetail(row.id) }, () => "查看"),
|
||||
h(
|
||||
NButton,
|
||||
{ size: "small", type: "primary", onClick: () => openDetail(row.id) },
|
||||
() => "查看",
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
@@ -156,7 +186,10 @@ async function openDetail(id: number) {
|
||||
|
||||
onMounted(() => Promise.all([listReports(), loadPinnedReports()]))
|
||||
watch(() => [query.page, query.limit], listReports)
|
||||
watchDebounced(() => query.username, listReports, { debounce: 500, maxWait: 1000 })
|
||||
watchDebounced(() => query.username, listReports, {
|
||||
debounce: 500,
|
||||
maxWait: 1000,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import http from "utils/http"
|
||||
import { toProblemListItem } from "admin/transforms"
|
||||
import type {
|
||||
AdminProblem,
|
||||
Announcement,
|
||||
@@ -30,30 +31,21 @@ export async function getProblemList(
|
||||
contestID?: string,
|
||||
) {
|
||||
const endpoint = !!contestID ? "admin/contest/problem" : "admin/problem"
|
||||
const res = await http.get(endpoint, {
|
||||
params: {
|
||||
paging: true,
|
||||
offset,
|
||||
limit,
|
||||
keyword,
|
||||
author,
|
||||
contest_id: contestID,
|
||||
const res = await http.get<{ results: AdminProblem[]; total: number }>(
|
||||
endpoint,
|
||||
{
|
||||
params: {
|
||||
paging: true,
|
||||
offset,
|
||||
limit,
|
||||
keyword,
|
||||
author,
|
||||
contest_id: contestID,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
return {
|
||||
results: res.data.results.map((result: AdminProblem) => ({
|
||||
id: result.id,
|
||||
_id: result._id,
|
||||
title: result.title,
|
||||
username: result.created_by.username,
|
||||
create_time: result.create_time,
|
||||
visible: result.visible,
|
||||
difficulty: result.difficulty,
|
||||
tags: result.tags,
|
||||
has_ast_rules: result.has_ast_rules,
|
||||
allow_flowchart: result.allow_flowchart,
|
||||
show_flowchart: result.show_flowchart,
|
||||
})),
|
||||
results: res.data.results.map(toProblemListItem),
|
||||
total: res.data.total,
|
||||
}
|
||||
}
|
||||
@@ -133,10 +125,10 @@ export function getContestList(offset = 0, limit = 10, keyword: string) {
|
||||
export async function uploadImage(file: File): Promise<string> {
|
||||
const form = new window.FormData()
|
||||
form.append("image", file)
|
||||
const res: { success: boolean; file_path: string; msg: "Success" } =
|
||||
await http.post("admin/upload_image", form, {
|
||||
headers: { "content-type": "multipart/form-data" },
|
||||
})
|
||||
// 该端点不走 { error, data } 信封,直接返回上传结果
|
||||
const res = (await http.post("admin/upload_image", form, {
|
||||
headers: { "content-type": "multipart/form-data" },
|
||||
})) as unknown as { success: boolean; file_path: string; msg: "Success" }
|
||||
return res.success ? res.file_path : ""
|
||||
}
|
||||
|
||||
@@ -244,17 +236,17 @@ export function deleteComment(id: number) {
|
||||
}
|
||||
|
||||
export async function getTutorialList() {
|
||||
const res = await http.get("admin/tutorial")
|
||||
const res = await http.get<Tutorial[]>("admin/tutorial")
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function getTutorial(id: number) {
|
||||
const res = await http.get("admin/tutorial", { params: { id } })
|
||||
const res = await http.get<Tutorial>("admin/tutorial", { params: { id } })
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function createTutorial(data: Partial<Tutorial>) {
|
||||
const res = await http.post("admin/tutorial", data)
|
||||
const res = await http.post<Tutorial>("admin/tutorial", data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
@@ -272,10 +264,10 @@ export function setTutorialVisibility(id: number, is_public: boolean) {
|
||||
}
|
||||
|
||||
export async function getAdminExercises(tutorialId: number) {
|
||||
const res = await http.get("admin/exercise", {
|
||||
const res = await http.get<Exercise[]>("admin/exercise", {
|
||||
params: { tutorial_id: tutorialId },
|
||||
})
|
||||
return res.data as Exercise[]
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function createExercise(data: {
|
||||
@@ -284,8 +276,8 @@ export async function createExercise(data: {
|
||||
data: object
|
||||
order: number
|
||||
}) {
|
||||
const res = await http.post("admin/exercise", data)
|
||||
return res.data as Exercise
|
||||
const res = await http.post<Exercise>("admin/exercise", data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function updateExercise(data: {
|
||||
|
||||
18
src/admin/transforms.ts
Normal file
18
src/admin/transforms.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { AdminProblem } from "utils/types"
|
||||
|
||||
// 把后端的 AdminProblem 塑形成管理端列表项,与请求逻辑解耦。
|
||||
export function toProblemListItem(result: AdminProblem) {
|
||||
return {
|
||||
id: result.id,
|
||||
_id: result._id,
|
||||
title: result.title,
|
||||
username: result.created_by.username,
|
||||
create_time: result.create_time,
|
||||
visible: result.visible,
|
||||
difficulty: result.difficulty,
|
||||
tags: result.tags,
|
||||
has_ast_rules: result.has_ast_rules,
|
||||
allow_flowchart: result.allow_flowchart,
|
||||
show_flowchart: result.show_flowchart,
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,13 @@ const isNotRegularUser = computed(
|
||||
>
|
||||
{{ getUserRole(props.user.admin_type).label }}
|
||||
</n-tag>
|
||||
<n-tag size="small" v-if="props.user.admin_type === USER_TYPE.STUDENT_ADMIN || props.user.admin_type === USER_TYPE.TEACHER_ADMIN">
|
||||
<n-tag
|
||||
size="small"
|
||||
v-if="
|
||||
props.user.admin_type === USER_TYPE.STUDENT_ADMIN ||
|
||||
props.user.admin_type === USER_TYPE.TEACHER_ADMIN
|
||||
"
|
||||
>
|
||||
{{
|
||||
props.user.problem_permission === PROBLEM_PERMISSION.ALL
|
||||
? "全部"
|
||||
|
||||
@@ -314,7 +314,11 @@ watch(() => [query.page, query.limit, query.type, query.orderBy], listUsers)
|
||||
<n-input v-model:value="password" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi
|
||||
v-if="!create && (userEditing.admin_type === USER_TYPE.STUDENT_ADMIN || userEditing.admin_type === USER_TYPE.TEACHER_ADMIN)"
|
||||
v-if="
|
||||
!create &&
|
||||
(userEditing.admin_type === USER_TYPE.STUDENT_ADMIN ||
|
||||
userEditing.admin_type === USER_TYPE.TEACHER_ADMIN)
|
||||
"
|
||||
:span="1"
|
||||
label="出题权限"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user