- 侧栏按 概览 / 教学内容 / 学情分析 / 站点管理 分组,去掉超管菜单里的重复项; 菜单项的角色与路由守卫一一对应,没有题目权限的教师不再看到点了就被踢回首页的「题目」 - 顶栏:面包屑、回到前台、明暗切换、用户菜单(退出登录);窄屏侧栏自动收起 - 新增 PageHeader:标题 + 右侧主操作 + 下方筛选栏,编辑页带返回按钮 - 卡点分析 / 年度趋势 / 标签管理从题目列表的按钮挪进侧栏 - 首页改成统计卡片 + 班级排名卡片;设置页按钮移到卡片右上角 - 题目、比赛、题单、用户列表的「选项」列固定在右侧,不再被挤出屏幕 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d25b5dbe6d
commit
d3823430f9
25 files changed
+764
-587
No files matched your search
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { NButton, NFlex } from "naive-ui"
|
||||
import { deleteAchievement, getAdminAchievements, type AdminAchievement } from "admin/api"
|
||||
import AchievementIcon from "shared/components/AchievementIcon.vue"
|
||||
@@ -106,22 +107,21 @@ onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card :title="title">
|
||||
<template #header-extra>
|
||||
<PageHeader
|
||||
:title="title"
|
||||
description="「已解锁人数」是唯一的仪表盘:配置一周后仍为 0,多半是阈值配错了而不是太难。"
|
||||
>
|
||||
<template #actions>
|
||||
<n-button type="primary" @click="create">新建成就</n-button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<n-alert type="info" style="margin-bottom: 12px">
|
||||
「已解锁人数」是唯一的仪表盘:配置一周后仍为 0,多半是阈值配错了而不是太难。
|
||||
</n-alert>
|
||||
<n-data-table
|
||||
:loading="loading"
|
||||
:data="list"
|
||||
:columns="columns"
|
||||
:row-key="(row: AdminAchievement) => row.id"
|
||||
/>
|
||||
|
||||
<n-data-table
|
||||
:loading="loading"
|
||||
:data="list"
|
||||
:columns="columns"
|
||||
:row-key="(row: AdminAchievement) => row.id"
|
||||
/>
|
||||
|
||||
<AchievementModal v-model:show="showModal" :editing="editing" @saved="load" />
|
||||
</n-card>
|
||||
<AchievementModal v-model:show="showModal" :editing="editing" @saved="load" />
|
||||
</template>
|
||||
@@ -1,13 +1,14 @@
|
||||
<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>
|
||||
<PageHeader title="AI 学习分析报告">
|
||||
<template #filters>
|
||||
<n-input
|
||||
v-model:value="query.username"
|
||||
clearable
|
||||
placeholder="输入用户名筛选"
|
||||
style="width: 220px"
|
||||
/>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<n-alert
|
||||
v-if="pinnedReports.length > 0"
|
||||
type="warning"
|
||||
@@ -60,6 +61,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { MdPreview } from "md-editor-v3"
|
||||
import "md-editor-v3/lib/preview.css"
|
||||
import Pagination from "shared/components/Pagination.vue"
|
||||
@@ -168,13 +170,6 @@ watchDebounced(() => query.username, listReports, {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.titleWrapper {
|
||||
margin-bottom: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
.detail .meta {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { errorMessage } from "utils/api"
|
||||
import TextEditor from "shared/components/TextEditor.vue"
|
||||
import type { AnnouncementEdit } from "utils/types"
|
||||
@@ -72,9 +73,10 @@ async function submit() {
|
||||
onMounted(init)
|
||||
</script>
|
||||
<template>
|
||||
<h2 class="title">
|
||||
{{ route.name === "admin announcement create" ? "新建公告" : "编辑公告" }}
|
||||
</h2>
|
||||
<PageHeader
|
||||
:title="route.name === 'admin announcement create' ? '新建公告' : '编辑公告'"
|
||||
:back="{ name: 'admin announcement list' }"
|
||||
/>
|
||||
<n-form inline>
|
||||
<n-form-item label="标题">
|
||||
<n-input class="contestTitle" v-model:value="announcement.title" />
|
||||
@@ -95,10 +97,6 @@ onMounted(init)
|
||||
</n-flex>
|
||||
</template>
|
||||
<style scoped>
|
||||
.title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.select {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { NSwitch } from "naive-ui"
|
||||
import Pagination from "shared/components/Pagination.vue"
|
||||
import { parseTime } from "utils/functions"
|
||||
@@ -93,22 +94,13 @@ watch(query, listAnnouncements, { deep: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex align="center" class="titleWrapper">
|
||||
<h2 class="title">网站公告</h2>
|
||||
<n-button type="primary" @click="$router.push({ name: 'admin announcement create' })">
|
||||
新建
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<PageHeader title="网站公告">
|
||||
<template #actions>
|
||||
<n-button type="primary" @click="$router.push({ name: 'admin announcement create' })">
|
||||
新建公告
|
||||
</n-button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<n-data-table striped :columns="columns" :data="announcements" />
|
||||
<Pagination :total="total" v-model:limit="query.limit" v-model:page="query.page" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.titleWrapper {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from "@iconify/vue"
|
||||
import type { RouteLocationRaw } from "vue-router"
|
||||
|
||||
/**
|
||||
* 后台每一页顶上的那一块:标题行(返回 · 标题 · 标签 ········ 操作按钮),
|
||||
* 下面可选一行筛选栏。以前每页各写一份 `.titleWrapper`,筛选控件和「新建」
|
||||
* 挤在同一行、左右位置每页不一样,这里统一成一种排法。
|
||||
*
|
||||
* - `back`:给了就在标题前出一个返回按钮,跳到这个位置(不用 router.back,
|
||||
* 从外链直接打开编辑页时没有「上一页」可回)
|
||||
* - `#extra`:紧跟标题的小东西(计数 tag 之类)
|
||||
* - `#actions`:标题行最右边的按钮,主操作放最后
|
||||
* - `#filters`:筛选栏,靠左排
|
||||
*/
|
||||
defineProps<{
|
||||
title: string
|
||||
description?: string
|
||||
back?: RouteLocationRaw
|
||||
}>()
|
||||
|
||||
const slots = defineSlots<{
|
||||
extra?: () => any
|
||||
actions?: () => any
|
||||
filters?: () => any
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pageHeader">
|
||||
<div class="row">
|
||||
<div class="heading">
|
||||
<n-button v-if="back" quaternary circle size="small" @click="$router.push(back)">
|
||||
<template #icon><Icon icon="ph:arrow-left" /></template>
|
||||
</n-button>
|
||||
<h2 class="title">{{ title }}</h2>
|
||||
<slot name="extra" />
|
||||
</div>
|
||||
<div v-if="slots.actions" class="actions">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
<n-text v-if="description" depth="3" class="description">{{ description }}</n-text>
|
||||
<div v-if="slots.filters" class="filters">
|
||||
<slot name="filters" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pageHeader {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { errorMessage } from "utils/api"
|
||||
import { formatISO } from "date-fns"
|
||||
import TextEditor from "shared/components/TextEditor.vue"
|
||||
@@ -112,23 +113,19 @@ onMounted(getContestDetail)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex class="titleWrapper" align="center">
|
||||
<h2 class="title">
|
||||
{{ route.name === "admin contest create" ? "新建比赛" : "编辑比赛" }}
|
||||
</h2>
|
||||
<template v-if="!props.contestID">
|
||||
<n-alert type="success">
|
||||
<template #header>
|
||||
开始时间 {{ parseTime(contest.startTime, "YYYY年M月D日 HH:mm:ss") }}
|
||||
</template>
|
||||
</n-alert>
|
||||
<n-alert type="warning">
|
||||
<template #header>
|
||||
结束时间 {{ parseTime(contest.endTime, "YYYY年M月D日 HH:mm:ss") }}
|
||||
</template>
|
||||
</n-alert>
|
||||
<PageHeader
|
||||
:title="route.name === 'admin contest create' ? '新建比赛' : '编辑比赛'"
|
||||
:back="{ name: 'admin contest list' }"
|
||||
>
|
||||
<template v-if="!props.contestID" #extra>
|
||||
<n-tag type="success" round>
|
||||
开始 {{ parseTime(contest.startTime, "YYYY年M月D日 HH:mm:ss") }}
|
||||
</n-tag>
|
||||
<n-tag type="warning" round>
|
||||
结束 {{ parseTime(contest.endTime, "YYYY年M月D日 HH:mm:ss") }}
|
||||
</n-tag>
|
||||
</template>
|
||||
</n-flex>
|
||||
</PageHeader>
|
||||
<n-form inline>
|
||||
<n-form-item label="标题">
|
||||
<n-input style="width: 300px" v-model:value="contest.title" />
|
||||
@@ -164,13 +161,3 @@ onMounted(getContestDetail)
|
||||
<n-button type="primary" @click="submit">保存</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.titleWrapper {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { errorMessage } from "utils/api"
|
||||
import type { AcmHelperItem, ContestSubmissionInfo } from "utils/types"
|
||||
import { NButton, NCheckbox, NSelect, NTag } from "naive-ui"
|
||||
@@ -232,38 +233,37 @@ onMounted(loadData)
|
||||
|
||||
<template>
|
||||
<n-flex vertical>
|
||||
<n-flex justify="space-between" align="center">
|
||||
<n-flex align="center">
|
||||
<h2 style="margin: 0">比赛辅助检查</h2>
|
||||
<n-tag type="info" size="large"> 总计: {{ stats.total }} </n-tag>
|
||||
<n-tag type="success" size="large"> 已检查: {{ stats.checked }} </n-tag>
|
||||
<n-tag type="warning" size="large"> 未检查: {{ stats.unchecked }} </n-tag>
|
||||
</n-flex>
|
||||
<n-button type="primary" :disabled="stats.unchecked === 0" @click="markAllAsChecked">
|
||||
标记全部为已检查
|
||||
</n-button>
|
||||
</n-flex>
|
||||
|
||||
<n-alert type="info" style="margin-bottom: 16px">
|
||||
<template #header>使用说明</template>
|
||||
此工具用于赛后人工审核代码,检查是否存在抄袭、作弊等行为。请逐个查看通过(AC)的提交代码,检查完成后勾选"已检查"。
|
||||
</n-alert>
|
||||
|
||||
<n-flex align="center" style="margin-bottom: 16px">
|
||||
<n-input
|
||||
v-model:value="query.username"
|
||||
placeholder="筛选用户名"
|
||||
style="width: 150px"
|
||||
clearable
|
||||
/>
|
||||
<n-input
|
||||
v-model:value="query.problemId"
|
||||
placeholder="筛选题目"
|
||||
style="width: 150px"
|
||||
clearable
|
||||
/>
|
||||
<n-select v-model:value="query.checked" :options="checkedOptions" style="width: 120px" />
|
||||
</n-flex>
|
||||
<PageHeader
|
||||
title="比赛辅助检查"
|
||||
description="赛后人工审核代码,检查是否存在抄袭、作弊等行为。请逐个查看通过(AC)的提交代码,检查完成后勾选「已检查」。"
|
||||
:back="{ name: 'admin contest list' }"
|
||||
>
|
||||
<template #extra>
|
||||
<n-tag type="info" round>总计 {{ stats.total }}</n-tag>
|
||||
<n-tag type="success" round>已检查 {{ stats.checked }}</n-tag>
|
||||
<n-tag type="warning" round>未检查 {{ stats.unchecked }}</n-tag>
|
||||
</template>
|
||||
<template #actions>
|
||||
<n-button type="primary" :disabled="stats.unchecked === 0" @click="markAllAsChecked">
|
||||
标记全部为已检查
|
||||
</n-button>
|
||||
</template>
|
||||
<template #filters>
|
||||
<n-input
|
||||
v-model:value="query.username"
|
||||
placeholder="筛选用户名"
|
||||
style="width: 150px"
|
||||
clearable
|
||||
/>
|
||||
<n-input
|
||||
v-model:value="query.problemId"
|
||||
placeholder="筛选题目"
|
||||
style="width: 150px"
|
||||
clearable
|
||||
/>
|
||||
<n-select v-model:value="query.checked" :options="checkedOptions" style="width: 120px" />
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { NSwitch, NTag } from "naive-ui"
|
||||
import ContestTitle from "shared/components/ContestTitle.vue"
|
||||
import ContestAccessTag from "shared/components/ContestAccessTag.vue"
|
||||
@@ -79,6 +80,7 @@ const columns: DataTableColumn<AdminContest>[] = [
|
||||
{
|
||||
title: "选项",
|
||||
key: "actions",
|
||||
fixed: "right",
|
||||
width: 300,
|
||||
render: (row) => h(Actions, { contest: row }),
|
||||
},
|
||||
@@ -99,27 +101,21 @@ watchDebounced(() => query.keyword, listContests, {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex justify="space-between" class="titleWrapper">
|
||||
<n-flex align="center">
|
||||
<h2 class="title">比赛列表</h2>
|
||||
<PageHeader title="比赛列表">
|
||||
<template #actions>
|
||||
<n-button type="primary" @click="$router.push({ name: 'admin contest create' })">
|
||||
新建
|
||||
新建比赛
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<div>
|
||||
<n-input v-model:value="query.keyword" placeholder="输入标题关键字" />
|
||||
</div>
|
||||
</n-flex>
|
||||
</template>
|
||||
<template #filters>
|
||||
<n-input
|
||||
v-model:value="query.keyword"
|
||||
placeholder="输入标题关键字"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
/>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<n-data-table :columns="columns" :data="contests" />
|
||||
<Pagination :total="total" v-model:limit="query.limit" v-model:page="query.page" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.titleWrapper {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { TUTORIAL_READ_SECONDS } from "@oj2/contract"
|
||||
import { NProgress, NTag, NText } from "naive-ui"
|
||||
import { getLearnStudents, getLearnTutorials, getLearnExercises } from "admin/api"
|
||||
@@ -357,33 +358,31 @@ onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 style="margin-top: 0">自学情况</h2>
|
||||
|
||||
<n-flex align="center" style="margin-bottom: 16px">
|
||||
<n-radio-group v-model:value="type" size="small">
|
||||
<n-radio-button
|
||||
v-for="item in typeOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
<!-- 口径写在表上方,免得老师对着「已读 0 课 / 累计 25 分钟」猜是不是坏了 -->
|
||||
<PageHeader
|
||||
title="自学情况"
|
||||
:description="`「已读」按累计停留满 ${TUTORIAL_READ_SECONDS / 60} 分钟算,不足的只计时长`"
|
||||
>
|
||||
<template #filters>
|
||||
<n-radio-group v-model:value="type" size="small">
|
||||
<n-radio-button
|
||||
v-for="item in typeOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
/>
|
||||
</n-radio-group>
|
||||
<n-input
|
||||
v-model:value="className"
|
||||
placeholder="班级或年级,如 241 / 24"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="load"
|
||||
@clear="load"
|
||||
/>
|
||||
</n-radio-group>
|
||||
<n-input
|
||||
v-model:value="className"
|
||||
placeholder="班级或年级,如 241 / 24"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="load"
|
||||
@clear="load"
|
||||
/>
|
||||
<n-button type="primary" secondary @click="load">查询</n-button>
|
||||
<n-text depth="3"> {{ studentCount }} 名学生,{{ startedCount }} 人已经开始学 </n-text>
|
||||
<!-- 口径写在表上方,免得老师对着「已读 0 课 / 累计 25 分钟」猜是不是坏了 -->
|
||||
<n-text depth="3" style="font-size: 12px">
|
||||
「已读」按累计停留满
|
||||
{{ TUTORIAL_READ_SECONDS / 60 }} 分钟算,不足的只计时长
|
||||
</n-text>
|
||||
</n-flex>
|
||||
<n-button type="primary" secondary @click="load">查询</n-button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<n-grid cols="2 s:3 m:5" :x-gap="12" :y-gap="12" responsive="screen" style="margin-bottom: 16px">
|
||||
<n-gi>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { getStuckProblems } from "admin/api"
|
||||
import type { StuckProblem } from "utils/types"
|
||||
|
||||
@@ -37,7 +38,7 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 style="margin-top: 0">学生卡点分析(只分析前40道题目)</h2>
|
||||
<PageHeader title="卡点分析" description="只分析前 40 道题目" />
|
||||
<n-data-table
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import type { AcTrend } from "utils/types"
|
||||
import { Line } from "vue-chartjs"
|
||||
import {
|
||||
@@ -127,31 +128,34 @@ onMounted(fetchData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 style="margin-top: 0">年度趋势</h2>
|
||||
<n-space align="center" style="margin-bottom: 16px">
|
||||
<span>年份范围</span>
|
||||
<n-select
|
||||
v-model:value="sinceYear"
|
||||
:options="yearOptions"
|
||||
style="width: 100px"
|
||||
@update:value="fetchData"
|
||||
/>
|
||||
<span>—</span>
|
||||
<n-select
|
||||
v-model:value="untilYear"
|
||||
:options="yearOptions"
|
||||
style="width: 100px"
|
||||
@update:value="fetchData"
|
||||
/>
|
||||
<span>年提交下限</span>
|
||||
<n-select
|
||||
v-model:value="minPerYear"
|
||||
:options="minPerYearOptions"
|
||||
style="width: 90px"
|
||||
@update:value="fetchData"
|
||||
/>
|
||||
<n-tag type="info" size="small">共 {{ data.length }} 题</n-tag>
|
||||
</n-space>
|
||||
<PageHeader title="年度趋势">
|
||||
<template #extra>
|
||||
<n-tag type="info" size="small" round>共 {{ data.length }} 题</n-tag>
|
||||
</template>
|
||||
<template #filters>
|
||||
<span>年份范围</span>
|
||||
<n-select
|
||||
v-model:value="sinceYear"
|
||||
:options="yearOptions"
|
||||
style="width: 100px"
|
||||
@update:value="fetchData"
|
||||
/>
|
||||
<span>—</span>
|
||||
<n-select
|
||||
v-model:value="untilYear"
|
||||
:options="yearOptions"
|
||||
style="width: 100px"
|
||||
@update:value="fetchData"
|
||||
/>
|
||||
<span>年提交下限</span>
|
||||
<n-select
|
||||
v-model:value="minPerYear"
|
||||
:options="minPerYearOptions"
|
||||
style="width: 90px"
|
||||
@update:value="fetchData"
|
||||
/>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<n-spin :show="loading">
|
||||
<div v-if="!loading && data.length === 0" style="text-align: center; padding: 40px">
|
||||
暂无数据
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { errorCode, errorMessage } from "utils/api"
|
||||
import { PROBLEM_TAG_MAX_LENGTH } from "@oj2/contract"
|
||||
import { getProblemTagList } from "shared/api"
|
||||
@@ -529,10 +530,18 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex>
|
||||
<h2 class="title">{{ title }}</h2>
|
||||
<n-button v-if="showClear" @click="clear">清空缓存</n-button>
|
||||
</n-flex>
|
||||
<PageHeader
|
||||
:title="title ?? '题目'"
|
||||
:back="
|
||||
props.contestID
|
||||
? { name: 'admin contest problem list', params: { contestID: props.contestID } }
|
||||
: { name: 'admin problem list' }
|
||||
"
|
||||
>
|
||||
<template #actions>
|
||||
<n-button v-if="showClear" @click="clear">清空缓存</n-button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<n-form inline label-placement="left">
|
||||
<n-form-item label="显示编号">
|
||||
<n-input class="w-100" v-model:value="problem._id" />
|
||||
@@ -832,10 +841,6 @@ watch(
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.box {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useRouteQuery } from "@vueuse/router"
|
||||
import AuthorSelect from "shared/components/AuthorSelect.vue"
|
||||
import type { DataTableRowKey } from "naive-ui"
|
||||
import BatchTagModal from "./components/BatchTagModal.vue"
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
|
||||
interface Props {
|
||||
contestID?: string
|
||||
@@ -166,6 +167,7 @@ const baseColumns: DataTableColumn<AdminProblemRow>[] = [
|
||||
{
|
||||
title: "选项",
|
||||
key: "actions",
|
||||
fixed: "right",
|
||||
width: 320,
|
||||
render: (row) =>
|
||||
h(Actions, {
|
||||
@@ -232,54 +234,41 @@ watch(() => [query.page, query.limit, query.author], listProblems)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex class="titleWrapper" justify="space-between">
|
||||
<n-flex align="center">
|
||||
<h2 class="title">{{ title }}</h2>
|
||||
<n-button
|
||||
v-if="!isContestProblemList"
|
||||
type="primary"
|
||||
@click="$router.push({ name: 'admin problem create' })"
|
||||
>
|
||||
新建
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="!isContestProblemList"
|
||||
@click="$router.push({ name: 'admin stuck problems' })"
|
||||
>
|
||||
卡点分析
|
||||
</n-button>
|
||||
<n-button v-if="!isContestProblemList" @click="$router.push({ name: 'admin top ac trend' })">
|
||||
年度趋势
|
||||
</n-button>
|
||||
<n-button v-if="!isContestProblemList" @click="$router.push({ name: 'admin tag list' })">
|
||||
标签管理
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-flex>
|
||||
<template v-if="!isContestProblemList && selectedProblemIds.length">
|
||||
<n-button type="primary" @click="openBatchTag('add')">
|
||||
添加标签({{ selectedProblemIds.length }})
|
||||
<PageHeader
|
||||
:title="title ?? '题目列表'"
|
||||
:back="isContestProblemList ? { name: 'admin contest list' } : undefined"
|
||||
>
|
||||
<template #actions>
|
||||
<template v-if="!isContestProblemList">
|
||||
<template v-if="selectedProblemIds.length">
|
||||
<n-button @click="openBatchTag('remove')">移除标签</n-button>
|
||||
<n-button secondary type="primary" @click="openBatchTag('add')">
|
||||
添加标签({{ selectedProblemIds.length }})
|
||||
</n-button>
|
||||
</template>
|
||||
<n-button type="primary" @click="$router.push({ name: 'admin problem create' })">
|
||||
新建题目
|
||||
</n-button>
|
||||
<n-button @click="openBatchTag('remove')">移除标签</n-button>
|
||||
</template>
|
||||
<n-button v-if="isContestProblemList" @click="createContestProblem"> 新建比赛题目 </n-button>
|
||||
<n-button v-if="isContestProblemList" type="primary" @click="selectProblems">
|
||||
从题目中选择
|
||||
</n-button>
|
||||
<n-flex align="center" v-if="!props.contestID">
|
||||
<span>出题人</span>
|
||||
<template v-else>
|
||||
<n-button @click="createContestProblem">新建比赛题目</n-button>
|
||||
<n-button type="primary" @click="selectProblems">从题库中选择</n-button>
|
||||
</template>
|
||||
</template>
|
||||
<template #filters>
|
||||
<n-input
|
||||
v-model:value="query.keyword"
|
||||
placeholder="输入标题关键字"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
@clear="clearQuery"
|
||||
/>
|
||||
<n-flex v-if="!props.contestID" align="center" :size="8">
|
||||
<n-text depth="3">出题人</n-text>
|
||||
<AuthorSelect v-model:value="query.author" all />
|
||||
</n-flex>
|
||||
<div>
|
||||
<n-input
|
||||
v-model:value="query.keyword"
|
||||
placeholder="输入标题关键字"
|
||||
clearable
|
||||
@clear="clearQuery"
|
||||
/>
|
||||
</div>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<n-data-table
|
||||
striped
|
||||
:columns="columns"
|
||||
@@ -301,13 +290,3 @@ watch(() => [query.page, query.limit, query.author], listProblems)
|
||||
@done="onBatchTagDone"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.titleWrapper {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { NButton, NFlex, NInput } from "naive-ui"
|
||||
import type { AdminTag } from "utils/types"
|
||||
import { deleteTag, getTagAdminList, renameTag } from "../api"
|
||||
@@ -144,13 +145,11 @@ watchDebounced(keyword, listTags, { debounce: 500, maxWait: 1000 })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex class="titleWrapper" justify="space-between">
|
||||
<n-flex align="center">
|
||||
<h2 class="title">标签管理</h2>
|
||||
<n-button @click="$router.push({ name: 'admin problem list' })"> 返回题目列表 </n-button>
|
||||
</n-flex>
|
||||
<n-input v-model:value="keyword" style="width: 200px" placeholder="搜索标签" clearable />
|
||||
</n-flex>
|
||||
<PageHeader title="标签管理">
|
||||
<template #filters>
|
||||
<n-input v-model:value="keyword" style="width: 220px" placeholder="搜索标签" clearable />
|
||||
</template>
|
||||
</PageHeader>
|
||||
<n-data-table striped :columns="columns" :data="tags" />
|
||||
<TagProblemsModal
|
||||
v-model:show="showTagProblems"
|
||||
@@ -159,13 +158,3 @@ watchDebounced(keyword, listTags, { debounce: 500, maxWait: 1000 })
|
||||
@changed="listTags"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.titleWrapper {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { errorMessage } from "utils/api"
|
||||
import { NTabPane, NTabs, NButton, NFlex } from "naive-ui"
|
||||
import { NTabPane, NTabs, NButton } from "naive-ui"
|
||||
import type {
|
||||
AdminProblemSet,
|
||||
AdminProblemSetProblem,
|
||||
@@ -200,20 +201,21 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div v-if="problemSet">
|
||||
<n-flex class="titleWrapper" justify="space-between" align="center">
|
||||
<h2 class="title">{{ problemSet.title }}</h2>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="
|
||||
router.push({
|
||||
name: 'admin problemset edit',
|
||||
params: { problemSetId },
|
||||
})
|
||||
"
|
||||
>
|
||||
编辑题单
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<PageHeader :title="problemSet.title" :back="{ name: 'admin problemset list' }">
|
||||
<template #actions>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="
|
||||
router.push({
|
||||
name: 'admin problemset edit',
|
||||
params: { problemSetId },
|
||||
})
|
||||
"
|
||||
>
|
||||
编辑题单
|
||||
</n-button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<ProblemSetInfo :problem-set="problemSet" />
|
||||
|
||||
@@ -257,13 +259,3 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.titleWrapper {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { errorMessage } from "utils/api"
|
||||
import type { CreateProblemSetData, EditProblemSetData } from "utils/types"
|
||||
import { fromPickerValue, toPickerValue } from "utils/functions"
|
||||
@@ -101,7 +102,14 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="title">{{ isEdit ? "编辑题单" : "创建题单" }}</h2>
|
||||
<PageHeader
|
||||
:title="isEdit ? '编辑题单' : '新建题单'"
|
||||
:back="
|
||||
isEdit
|
||||
? { name: 'admin problemset detail', params: { problemSetId } }
|
||||
: { name: 'admin problemset list' }
|
||||
"
|
||||
/>
|
||||
|
||||
<n-form :model="formData" label-placement="top">
|
||||
<n-flex>
|
||||
@@ -158,9 +166,3 @@ onMounted(() => {
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import Pagination from "shared/components/Pagination.vue"
|
||||
import { usePagination } from "shared/composables/pagination"
|
||||
import { parseTime } from "utils/functions"
|
||||
@@ -96,6 +97,7 @@ const columns: DataTableColumn<AdminProblemSet>[] = [
|
||||
{
|
||||
title: "选项",
|
||||
key: "actions",
|
||||
fixed: "right",
|
||||
width: 300,
|
||||
render: (row) =>
|
||||
h(Actions, {
|
||||
@@ -142,53 +144,36 @@ watch(() => [query.page, query.limit, query.difficulty, query.status], listProbl
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex class="titleWrapper" justify="space-between">
|
||||
<n-flex align="center">
|
||||
<h2 class="title">题单管理</h2>
|
||||
<PageHeader title="题单列表">
|
||||
<template #actions>
|
||||
<n-button type="primary" @click="$router.push({ name: 'admin problemset create' })">
|
||||
新建题单
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-flex align="center">
|
||||
<n-flex align="center">
|
||||
<span>难度:</span>
|
||||
<n-select
|
||||
v-model:value="query.difficulty"
|
||||
:options="difficultyOptions"
|
||||
placeholder="选择难度"
|
||||
style="width: 120px"
|
||||
clearable
|
||||
/>
|
||||
</n-flex>
|
||||
<n-flex align="center">
|
||||
<span>状态:</span>
|
||||
<n-select
|
||||
v-model:value="query.status"
|
||||
:options="statusOptions"
|
||||
placeholder="选择状态"
|
||||
style="width: 120px"
|
||||
clearable
|
||||
/>
|
||||
</n-flex>
|
||||
</template>
|
||||
<template #filters>
|
||||
<n-input
|
||||
v-model:value="query.keyword"
|
||||
placeholder="输入标题关键字"
|
||||
clearable
|
||||
@clear="clearQuery"
|
||||
style="width: 200px"
|
||||
style="width: 220px"
|
||||
/>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
<n-select
|
||||
v-model:value="query.difficulty"
|
||||
:options="difficultyOptions"
|
||||
placeholder="全部难度"
|
||||
style="width: 120px"
|
||||
clearable
|
||||
/>
|
||||
<n-select
|
||||
v-model:value="query.status"
|
||||
:options="statusOptions"
|
||||
placeholder="全部状态"
|
||||
style="width: 120px"
|
||||
clearable
|
||||
/>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<n-data-table striped :columns="columns" :data="problemSets" />
|
||||
<Pagination :total="total" v-model:limit="query.limit" v-model:page="query.page" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.titleWrapper {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { errorMessage } from "utils/api"
|
||||
import { NButton, NTag } from "naive-ui"
|
||||
import { CLASS_NAME_MAX_DIGITS, CLASS_NAME_MIN_DIGITS, CLASS_NAME_RE } from "utils/constants"
|
||||
@@ -250,12 +251,10 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card class="box">
|
||||
<template #header>
|
||||
<n-flex align="center">
|
||||
网站设置
|
||||
<n-button type="primary" size="small" @click="saveWebsiteConfig"> 保存 </n-button>
|
||||
</n-flex>
|
||||
<PageHeader title="设置" />
|
||||
<n-card class="box" title="网站">
|
||||
<template #header-extra>
|
||||
<n-button type="primary" size="small" @click="saveWebsiteConfig">保存</n-button>
|
||||
</template>
|
||||
<n-form inline label-placement="left">
|
||||
<n-form-item label="网站 URL">
|
||||
@@ -295,31 +294,25 @@ onMounted(() => {
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
<n-card class="box">
|
||||
<template #header>
|
||||
<n-flex align="center">
|
||||
判题服务器
|
||||
<n-button
|
||||
v-if="abnormalServers.length"
|
||||
size="small"
|
||||
type="warning"
|
||||
@click="deleteAbnormalServers"
|
||||
>
|
||||
删除无效服务器
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-card class="box" title="判题服务器">
|
||||
<template #header-extra>
|
||||
<n-button
|
||||
v-if="abnormalServers.length"
|
||||
size="small"
|
||||
type="warning"
|
||||
@click="deleteAbnormalServers"
|
||||
>
|
||||
删除无效服务器
|
||||
</n-button>
|
||||
</template>
|
||||
<div class="box">
|
||||
接口凭证 <n-tag size="small">{{ token }}</n-tag>
|
||||
</div>
|
||||
<n-data-table :single-line="false" striped :columns="serverColumns" :data="servers" />
|
||||
</n-card>
|
||||
<n-card class="box" v-if="testcases.length">
|
||||
<template #header>
|
||||
<n-flex align="center">
|
||||
无效的测试用例
|
||||
<n-button size="small" type="warning" @click="() => deleteTestcase()"> 全部删除 </n-button>
|
||||
</n-flex>
|
||||
<n-card class="box" v-if="testcases.length" title="无效的测试用例">
|
||||
<template #header-extra>
|
||||
<n-button size="small" type="warning" @click="() => deleteTestcase()">全部删除</n-button>
|
||||
</template>
|
||||
<n-data-table striped class="table" :columns="testcaseColumns" :data="testcases" />
|
||||
</n-card>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { h, onMounted, reactive, ref, watch } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import { NButton } from "naive-ui"
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import Pagination from "shared/components/Pagination.vue"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import { getACRate } from "utils/functions"
|
||||
@@ -151,50 +152,56 @@ watch(showModal, (v) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex align="center">
|
||||
<n-avatar round :size="60" :src="userStore.profile?.avatar" />
|
||||
<h1 class="name">亲爱的管理员:{{ userStore.user?.username }}</h1>
|
||||
</n-flex>
|
||||
<n-flex>
|
||||
<h2>
|
||||
<n-gradient-text type="info"> 总用户数:{{ userCount }} </n-gradient-text>
|
||||
</h2>
|
||||
<h2>
|
||||
<n-gradient-text type="error"> 今日提交:{{ submissionCount }} </n-gradient-text>
|
||||
</h2>
|
||||
<h2>
|
||||
<n-gradient-text type="warning"> 近期比赛:{{ contestCount }} </n-gradient-text>
|
||||
</h2>
|
||||
<h2>
|
||||
<PageHeader :title="`你好,${userStore.user?.username ?? '管理员'}`">
|
||||
<template #actions>
|
||||
<n-button @click="router.push('/admin/contest/create')">新建比赛</n-button>
|
||||
<n-button type="primary" @click="router.push('/admin/problem/create')">新建题目</n-button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<n-grid cols="2 m:4" :x-gap="12" :y-gap="12" responsive="screen" class="stats">
|
||||
<n-gi>
|
||||
<n-card size="small"><n-statistic label="总用户数" :value="userCount" /></n-card>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card size="small"><n-statistic label="今日提交" :value="submissionCount" /></n-card>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card size="small"><n-statistic label="近期比赛" :value="contestCount" /></n-card>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<!-- 判题机数量后端一直在下发,这里以前没显示 —— 判题机全掉线的时候,
|
||||
这一栏是 0,比学生喊「交了没反应」早得多 -->
|
||||
<n-gradient-text :type="judgeServerCount > 0 ? 'success' : 'error'">
|
||||
在线判题机:{{ judgeServerCount }}
|
||||
</n-gradient-text>
|
||||
</h2>
|
||||
</n-flex>
|
||||
<n-flex align="center" class="actions">
|
||||
<span>我猜你要:</span>
|
||||
<n-button @click="router.push('/admin/problem/create')">新题目</n-button>
|
||||
<n-button @click="router.push('/admin/contest/create')">新比赛</n-button>
|
||||
<div>
|
||||
<n-input
|
||||
style="width: 200px"
|
||||
clearable
|
||||
v-model:value="query.classroom"
|
||||
placeholder="班级前缀"
|
||||
/>
|
||||
</div>
|
||||
<n-button @click="listRanks">用户排名</n-button>
|
||||
<n-button @click="getRandomModal" v-if="query.classroom">随机抽签</n-button>
|
||||
<Pagination
|
||||
class="pagination"
|
||||
:total="total"
|
||||
v-model:page="query.page"
|
||||
v-model:limit="query.limit"
|
||||
/>
|
||||
</n-flex>
|
||||
<n-data-table v-if="data.length" striped :data="data" :columns="columns" />
|
||||
<n-card size="small">
|
||||
<n-statistic label="在线判题机">
|
||||
<n-text :type="judgeServerCount > 0 ? 'success' : 'error'">
|
||||
{{ judgeServerCount }}
|
||||
</n-text>
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-card size="small" title="班级排名">
|
||||
<template #header-extra>
|
||||
<n-flex align="center" :size="8">
|
||||
<n-input
|
||||
style="width: 160px"
|
||||
clearable
|
||||
v-model:value="query.classroom"
|
||||
placeholder="班级前缀"
|
||||
@keyup.enter="listRanks"
|
||||
/>
|
||||
<n-button secondary type="primary" @click="listRanks">查询</n-button>
|
||||
<n-button v-if="query.classroom" @click="getRandomModal">随机抽签</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
<template v-if="data.length">
|
||||
<n-data-table striped :data="data" :columns="columns" />
|
||||
<Pagination :total="total" v-model:page="query.page" v-model:limit="query.limit" />
|
||||
</template>
|
||||
<n-empty v-else description="输入班级前缀后查询" style="padding: 24px 0" />
|
||||
</n-card>
|
||||
<n-modal preset="card" title="猜猜看幸运儿是谁?" v-model:show="showModal" style="width: 400px">
|
||||
<n-flex vertical justify="center" align="center">
|
||||
<n-h1 :key="pulseKey" class="lucky pulse">{{ luckyGuy }}</n-h1>
|
||||
@@ -206,17 +213,8 @@ watch(showModal, (v) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.name {
|
||||
font-size: 32px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin: 0;
|
||||
.stats {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.lucky {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { errorMessage } from "utils/api"
|
||||
import CodeEditor from "shared/components/CodeEditor.vue"
|
||||
import MarkdownEditor from "shared/components/MarkdownEditor.vue"
|
||||
@@ -72,9 +73,10 @@ async function submit() {
|
||||
onMounted(init)
|
||||
</script>
|
||||
<template>
|
||||
<h2 class="title">
|
||||
{{ route.name === "admin tutorial create" ? "新建教程" : "编辑教程" }}
|
||||
</h2>
|
||||
<PageHeader
|
||||
:title="route.name === 'admin tutorial create' ? '新建教程' : '编辑教程'"
|
||||
:back="{ name: 'admin tutorial list' }"
|
||||
/>
|
||||
<n-form inline>
|
||||
<n-form-item label="标题">
|
||||
<n-input class="contestTitle" v-model:value="tutorial.title" />
|
||||
@@ -111,10 +113,6 @@ onMounted(init)
|
||||
</n-tabs>
|
||||
</template>
|
||||
<style scoped>
|
||||
.title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.select {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { errorMessage } from "utils/api"
|
||||
import { NSwitch } from "naive-ui"
|
||||
import { parseTime } from "utils/functions"
|
||||
@@ -77,12 +78,13 @@ onMounted(listTutorials)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex align="center" class="titleWrapper">
|
||||
<h2 class="title">教程列表</h2>
|
||||
<n-button type="primary" @click="$router.push({ name: 'admin tutorial create' })">
|
||||
新建
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<PageHeader title="教程列表">
|
||||
<template #actions>
|
||||
<n-button type="primary" @click="$router.push({ name: 'admin tutorial create' })">
|
||||
新建教程
|
||||
</n-button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<n-tabs v-model:value="activeTab" type="line" animated>
|
||||
<n-tab-pane name="python" tab="Python">
|
||||
<n-data-table striped :columns="columns" :data="tutorials.python" />
|
||||
@@ -92,13 +94,3 @@ onMounted(listTutorials)
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.titleWrapper {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { errorMessage } from "utils/api"
|
||||
import {
|
||||
CLASS_NAME_MAX_DIGITS,
|
||||
@@ -81,6 +82,11 @@ async function submit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader
|
||||
title="批量导入用户"
|
||||
description="每行一个名字,用户名为 ks + 班级号 + 名字,密码随机生成;导入后会下载账号清单"
|
||||
:back="{ name: 'admin user list' }"
|
||||
/>
|
||||
<n-space>
|
||||
<n-flex vertical>
|
||||
<n-flex align="center">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { errorMessage } from "utils/api"
|
||||
import { DataTableRowKey, NFlex, NTag, SelectOption } from "naive-ui"
|
||||
import Pagination from "shared/components/Pagination.vue"
|
||||
@@ -105,6 +106,7 @@ const columns: DataTableColumn<AdminUserForm>[] = [
|
||||
key: "actions",
|
||||
title: "选项",
|
||||
width: 280,
|
||||
fixed: "right",
|
||||
render: (row) =>
|
||||
h(Actions, {
|
||||
user: row,
|
||||
@@ -252,43 +254,39 @@ watch(() => [query.page, query.limit, query.type, query.orderBy], listUsers)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex class="titleWrapper" justify="space-between">
|
||||
<n-flex>
|
||||
<h2 class="title">用户列表</h2>
|
||||
<n-button type="primary" @click="createNewUser">新建</n-button>
|
||||
<n-button @click="$router.push({ name: 'admin user generate' })"> 导入 </n-button>
|
||||
</n-flex>
|
||||
<n-flex>
|
||||
<PageHeader title="用户列表">
|
||||
<template #actions>
|
||||
<n-popconfirm v-if="userIDs.length" @positive-click="onDeleteUsers(userIDs)">
|
||||
<template #trigger>
|
||||
<n-button type="warning">删除</n-button>
|
||||
<n-button type="error" secondary>删除选中({{ userIDs.length }})</n-button>
|
||||
</template>
|
||||
确定删除选中的用户吗?删除后无法恢复!
|
||||
</n-popconfirm>
|
||||
<n-flex align="center">
|
||||
<n-select
|
||||
v-model:value="query.orderBy"
|
||||
:options="sortOptions"
|
||||
placeholder="排序方式"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<n-select
|
||||
v-model:value="query.type"
|
||||
:options="adminOptions"
|
||||
placeholder="选择用户类型"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<div>
|
||||
<n-input
|
||||
style="width: 200px"
|
||||
v-model:value="query.keyword"
|
||||
clearable
|
||||
@clear="clearQuery"
|
||||
/>
|
||||
</div>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
<n-button @click="$router.push({ name: 'admin user generate' })">批量导入</n-button>
|
||||
<n-button type="primary" @click="createNewUser">新建用户</n-button>
|
||||
</template>
|
||||
<template #filters>
|
||||
<n-input
|
||||
style="width: 220px"
|
||||
v-model:value="query.keyword"
|
||||
placeholder="用户名 / 真名 / 邮箱"
|
||||
clearable
|
||||
@clear="clearQuery"
|
||||
/>
|
||||
<n-select
|
||||
v-model:value="query.type"
|
||||
:options="adminOptions"
|
||||
placeholder="选择用户类型"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<n-select
|
||||
v-model:value="query.orderBy"
|
||||
:options="sortOptions"
|
||||
placeholder="排序方式"
|
||||
style="width: 120px"
|
||||
/>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<n-data-table
|
||||
:data="users"
|
||||
:columns="columns"
|
||||
@@ -353,13 +351,3 @@ watch(() => [query.page, query.limit, query.type, query.orderBy], listUsers)
|
||||
</n-form>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.titleWrapper {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user