add ai report
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled

This commit is contained in:
2026-06-04 08:06:36 -06:00
parent b3edf5383a
commit 33b6e35d6b
4 changed files with 163 additions and 1 deletions

126
src/admin/ai/list.vue Normal file
View File

@@ -0,0 +1,126 @@
<template>
<n-flex justify="space-between" class="titleWrapper">
<h2 class="title">AI 学习分析报告</h2>
<n-input
v-model:value="query.username"
clearable
placeholder="输入用户名筛选"
style="width: 200px"
/>
</n-flex>
<n-data-table striped :columns="columns" :data="reports" />
<Pagination
:total="total"
v-model:limit="query.limit"
v-model:page="query.page"
/>
<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>
<n-scrollbar style="max-height: 60vh; margin-top: 12px">
<pre class="analysis">{{ detail.analysis }}</pre>
</n-scrollbar>
</div>
</n-spin>
</n-modal>
</template>
<script lang="ts" setup>
import Pagination from "shared/components/Pagination.vue"
import { parseTime } from "utils/functions"
import { getAIReportList, getAIReportDetail } from "../api"
import { NButton } from "naive-ui"
interface ReportItem {
id: number
create_time: string
username: string
class_name: string | null
}
interface ReportDetail extends ReportItem {
analysis: string
}
const reports = ref<ReportItem[]>([])
const total = ref(0)
const query = reactive({ limit: 10, page: 1, username: "" })
const showModal = ref(false)
const loadingDetail = ref(false)
const detail = ref<ReportDetail | null>(null)
const columns: DataTableColumn<ReportItem>[] = [
{ title: "ID", key: "id", width: 80 },
{ title: "用户名", key: "username", width: 150 },
{ title: "班级", key: "class_name", width: 150, render: (row) => row.class_name || "-" },
{
title: "生成时间",
key: "create_time",
width: 200,
render: (row) => parseTime(row.create_time, "YYYY-MM-DD HH:mm:ss"),
},
{
title: "操作",
key: "action",
width: 80,
render: (row) =>
h(
NButton,
{ size: "small", type: "primary", onClick: () => openDetail(row.id) },
() => "查看",
),
},
]
async function listReports() {
const offset = (query.page - 1) * query.limit
const res = await getAIReportList(offset, query.limit, query.username)
reports.value = res.data.results
total.value = res.data.total
}
async function openDetail(id: number) {
showModal.value = true
loadingDetail.value = true
detail.value = null
try {
const res = await getAIReportDetail(id)
detail.value = res.data
} finally {
loadingDetail.value = false
}
}
onMounted(listReports)
watch(() => [query.page, query.limit], listReports)
watchDebounced(() => query.username, listReports, { debounce: 500, maxWait: 1000 })
</script>
<style scoped>
.titleWrapper {
margin-bottom: 16px;
align-items: center;
}
.title {
margin: 0;
}
.detail .meta {
margin-bottom: 0;
}
.analysis {
white-space: pre-wrap;
word-break: break-word;
font-family: inherit;
font-size: 14px;
line-height: 1.6;
margin: 0;
padding: 8px;
}
</style>

View File

@@ -490,3 +490,14 @@ export function getTopACTrend(params: {
}) {
return http.get("admin/problem/top_ac_trend", { params })
}
// AI 学习分析报告
export function getAIReportList(offset = 0, limit = 10, username = "") {
return http.get("admin/ai/reports", {
params: { paging: true, offset, limit, username: username || undefined },
})
}
export function getAIReportDetail(id: number) {
return http.get("admin/ai/reports", { params: { id } })
}

View File

@@ -1,4 +1,4 @@
import { RouteRecordRaw } from "vue-router"
import type { RouteRecordRaw } from "vue-router"
export const ojs: RouteRecordRaw = {
path: "/",
@@ -315,5 +315,11 @@ export const admins: RouteRecordRaw = {
props: true,
meta: { requiresTeacherAdmin: true },
},
{
path: "ai/reports",
name: "admin ai reports",
component: () => import("admin/ai/list.vue"),
meta: { requiresTeacherAdmin: true },
},
],
}

View File

@@ -58,6 +58,15 @@ const options = computed<MenuOption[]>(() => {
),
key: "admin problemset list",
},
{
label: () =>
h(
RouterLink,
{ to: "/admin/ai/reports" },
{ default: () => "AI报告" },
),
key: "admin ai reports",
},
)
}
@@ -132,6 +141,15 @@ const options = computed<MenuOption[]>(() => {
),
key: "admin tutorial list",
},
{
label: () =>
h(
RouterLink,
{ to: "/admin/ai/reports" },
{ default: () => "AI报告" },
),
key: "admin ai reports",
},
)
}
@@ -152,6 +170,7 @@ const active = computed(() => {
if (path.startsWith("/admin/comment")) return "admin comment list"
if (path.startsWith("/admin/announcement")) return "admin announcement list"
if (path.startsWith("/admin/tutorial")) return "admin tutorial list"
if (path.startsWith("/admin/ai")) return "admin ai reports"
return route.name as string
})