feat(后台题目): 题目列表重排——1366 屏上不用横滚就看得到「学生看得到」开关;一行只剩「编辑」和 ⋯,有人交过的题删除直接灰掉;勾几道一次放出、藏起、加减知识点
去掉内部 ID、难度、看不懂的「功能」图标和反馈表情几列;题目名点了就是编辑。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
952edbf86f
commit
97049d562b
5 files changed
+280
-120
No files matched your search
@@ -72,17 +72,36 @@ export async function getProblemList(
|
|||||||
tagId?: number,
|
tagId?: number,
|
||||||
/** 题型:sql = SQL 题,code = 编程题;不传是全部(比赛题列表不认) */
|
/** 题型:sql = SQL 题,code = 编程题;不传是全部(比赛题列表不认) */
|
||||||
kind?: "sql" | "code" | "",
|
kind?: "sql" | "code" | "",
|
||||||
|
/** 1 = 学生看得到的,0 = 藏着的;不传是全部(比赛题列表不认) */
|
||||||
|
visible?: "1" | "0" | "",
|
||||||
) {
|
) {
|
||||||
const endpoint = contestID ? `admin/contests/${contestID}/problems` : "admin/problems"
|
const endpoint = contestID ? `admin/contests/${contestID}/problems` : "admin/problems"
|
||||||
const res = await api.get<AdminProblemList>(endpoint, {
|
const res = await api.get<AdminProblemList>(endpoint, {
|
||||||
params: { offset, limit, keyword, author, tagId, kind: kind || undefined },
|
params: {
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
keyword,
|
||||||
|
author,
|
||||||
|
tagId,
|
||||||
|
kind: kind || undefined,
|
||||||
|
visible: visible || undefined,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
results: res.results.map(toAdminProblemRow),
|
results: res.results.map(toAdminProblemRow),
|
||||||
total: res.total,
|
total: res.total,
|
||||||
|
counts: res.counts ?? null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 勾了几道,一次让学生看得到 / 藏起来 */
|
||||||
|
export function batchSetProblemVisible(problemIds: number[], visible: boolean) {
|
||||||
|
return api.post<{ problemCount: number }>("admin/problems/batch-visible", {
|
||||||
|
problemIds,
|
||||||
|
visible,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 比赛「从题库中选择」用:列所有可见的公开题,不只是自己创建的
|
// 比赛「从题库中选择」用:列所有可见的公开题,不只是自己创建的
|
||||||
export async function getLibraryProblemList(offset = 0, limit = 10, keyword: string) {
|
export async function getLibraryProblemList(offset = 0, limit = 10, keyword: string) {
|
||||||
const res = await api.get<AdminProblemList>("admin/problems", {
|
const res = await api.get<AdminProblemList>("admin/problems", {
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { Icon } from "@iconify/vue"
|
||||||
import { errorCode, errorMessage } from "utils/api"
|
import { errorCode, errorMessage } from "utils/api"
|
||||||
import { deleteContestProblem, deleteProblem, makeProblemPublic } from "admin/api"
|
import { deleteContestProblem, deleteProblem, makeProblemPublic } from "admin/api"
|
||||||
import download from "utils/download"
|
import download from "utils/download"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目列表一行的操作(设计稿「后台题目列表」PlA):常用的「编辑」常驻,其余收进 ⋯ ——
|
||||||
|
* 原来是四颗彩色按钮(编辑 / 查看 / 删除 / 下载)并排,1366 宽的屏上把「可见」开关挤出了屏幕。
|
||||||
|
* 有人交过的题删不掉(服务端也拦),「删除」直接灰掉并写明为什么,不用点了才知道
|
||||||
|
*/
|
||||||
interface Props {
|
interface Props {
|
||||||
problemID: number
|
problemID: number
|
||||||
problemDisplayID: string
|
problemDisplayID: string
|
||||||
|
/** 做过这道题的人数,> 0 就删不掉 */
|
||||||
|
triedUsers?: number
|
||||||
}
|
}
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
const dialog = useDialog()
|
||||||
const emit = defineEmits(["updated"])
|
const emit = defineEmits(["updated"])
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -60,6 +69,31 @@ function goCheck() {
|
|||||||
window.open(data.href, "_blank")
|
window.open(data.href, "_blank")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const menuOptions = computed<DropdownOption[]>(() => [
|
||||||
|
{ label: "在前台看", key: "view" },
|
||||||
|
{ label: "下载测试数据", key: "download" },
|
||||||
|
...(isContestProblem.value ? [{ label: "转成公开题", key: "public" }] : []),
|
||||||
|
{ type: "divider", key: "divider" },
|
||||||
|
props.triedUsers
|
||||||
|
? { label: `删除(有 ${props.triedUsers} 人交过)`, key: "delete", disabled: true }
|
||||||
|
: { label: "删除", key: "delete" },
|
||||||
|
])
|
||||||
|
|
||||||
|
function onMenu(key: string) {
|
||||||
|
if (key === "view") goCheck()
|
||||||
|
else if (key === "download") downloads()
|
||||||
|
else if (key === "public") openMakePublicModal()
|
||||||
|
else if (key === "delete") {
|
||||||
|
dialog.warning({
|
||||||
|
title: `删除 ${props.problemDisplayID}?`,
|
||||||
|
content: "删掉就找不回来了。",
|
||||||
|
positiveText: "删除",
|
||||||
|
negativeText: "不删",
|
||||||
|
onPositiveClick: handleDeleteProblem,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openMakePublicModal() {
|
function openMakePublicModal() {
|
||||||
newDisplayID.value = ""
|
newDisplayID.value = ""
|
||||||
showMakePublicModal.value = true
|
showMakePublicModal.value = true
|
||||||
@@ -88,29 +122,13 @@ async function handleMakePublic() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<n-flex>
|
<n-flex :size="4" justify="end" :wrap="false">
|
||||||
<n-button size="small" secondary type="primary" @click="goEdit"> 编辑 </n-button>
|
<n-button size="small" @click="goEdit">编辑</n-button>
|
||||||
<n-button size="small" secondary type="info" @click="goCheck"> 查看 </n-button>
|
<n-dropdown trigger="click" placement="bottom-end" :options="menuOptions" @select="onMenu">
|
||||||
<n-tooltip v-if="isContestProblem">
|
<n-button size="small" quaternary aria-label="更多">
|
||||||
<template #trigger>
|
<template #icon><Icon icon="ph:dots-three-bold" /></template>
|
||||||
<n-button size="small" secondary type="warning" @click="openMakePublicModal">
|
</n-button>
|
||||||
公开
|
</n-dropdown>
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
将此竞赛题目转为公开题目
|
|
||||||
</n-tooltip>
|
|
||||||
<n-popconfirm @positive-click="handleDeleteProblem">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button secondary size="small" type="error">删除</n-button>
|
|
||||||
</template>
|
|
||||||
确定删除这道题目吗?相关的提交也会被相应删除哦 😯
|
|
||||||
</n-popconfirm>
|
|
||||||
<n-tooltip>
|
|
||||||
<template #trigger>
|
|
||||||
<n-button size="small" secondary @click="downloads">下载</n-button>
|
|
||||||
</template>
|
|
||||||
下载测试用例
|
|
||||||
</n-tooltip>
|
|
||||||
</n-flex>
|
</n-flex>
|
||||||
|
|
||||||
<n-modal
|
<n-modal
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NFlex, NSwitch, NTag, NTooltip } from "naive-ui"
|
import { NSwitch, useThemeVars } from "naive-ui"
|
||||||
import { Icon } from "@iconify/vue"
|
|
||||||
import Pagination from "shared/components/Pagination.vue"
|
import Pagination from "shared/components/Pagination.vue"
|
||||||
import { usePagination } from "shared/composables/pagination"
|
import { usePagination } from "shared/composables/pagination"
|
||||||
import { getTagColor, parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
import type { AdminProblemRow } from "utils/types"
|
import type { AdminProblemRow } from "utils/types"
|
||||||
import { DIFFICULTY, REACTIONS } from "utils/constants"
|
import { errorMessage } from "utils/api"
|
||||||
import { getProblemList, toggleProblemVisible } from "../api"
|
import { batchSetProblemVisible, getProblemList, toggleProblemVisible } from "../api"
|
||||||
import Actions from "./components/Actions.vue"
|
import Actions from "./components/Actions.vue"
|
||||||
import AddFromLibraryModal from "./components/AddFromLibraryModal.vue"
|
import AddFromLibraryModal from "./components/AddFromLibraryModal.vue"
|
||||||
import { useRouteQuery } from "@vueuse/router"
|
import { useRouteQuery } from "@vueuse/router"
|
||||||
@@ -17,6 +16,14 @@ import CreateProblemMenu from "./components/CreateProblemMenu.vue"
|
|||||||
import PageHeader from "admin/components/PageHeader.vue"
|
import PageHeader from "admin/components/PageHeader.vue"
|
||||||
import ProblemTypeTag from "oj/problem/components/ProblemTypeTag.vue"
|
import ProblemTypeTag from "oj/problem/components/ProblemTypeTag.vue"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台题目列表(设计稿「后台题目列表」PlA / PlB)。一行一道题:题号、题目(点了就是编辑)、
|
||||||
|
* 知识点、做对 / 做过的人数、出题人、创建日期、「学生看得到」开关,最右「编辑」+ ⋯。
|
||||||
|
*
|
||||||
|
* 原来 1366 宽的屏上「可见」开关和创建时间要横滚才看得到,出题人被截成三个字;
|
||||||
|
* 内部 ID 和显示编号两列、难度、看不懂的「功能」图标列、反馈表情列都拿掉了。
|
||||||
|
* 比赛题目列表(contestID)也用这一页,没有勾选、人数和「全部 / 看得到 / 藏着的」
|
||||||
|
*/
|
||||||
interface Props {
|
interface Props {
|
||||||
contestID?: string
|
contestID?: string
|
||||||
}
|
}
|
||||||
@@ -24,6 +31,7 @@ interface Props {
|
|||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const theme = useThemeVars()
|
||||||
|
|
||||||
const title = computed(
|
const title = computed(
|
||||||
() =>
|
() =>
|
||||||
@@ -75,6 +83,8 @@ interface ProblemQuery {
|
|||||||
keyword: string
|
keyword: string
|
||||||
author: string
|
author: string
|
||||||
kind: "" | "code" | "sql"
|
kind: "" | "code" | "sql"
|
||||||
|
/** 1 = 学生看得到的,0 = 藏着的 */
|
||||||
|
visible: "" | "1" | "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用分页 composable
|
// 使用分页 composable
|
||||||
@@ -82,95 +92,88 @@ const { query, clearQuery } = usePagination<ProblemQuery>({
|
|||||||
keyword: useRouteQuery("keyword", "").value,
|
keyword: useRouteQuery("keyword", "").value,
|
||||||
author: useRouteQuery("author", "").value,
|
author: useRouteQuery("author", "").value,
|
||||||
kind: useRouteQuery<ProblemQuery["kind"]>("kind", "").value,
|
kind: useRouteQuery<ProblemQuery["kind"]>("kind", "").value,
|
||||||
|
visible: useRouteQuery<ProblemQuery["visible"]>("visible", "").value,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const counts = ref<{ all: number; visible: number; hidden: number } | null>(null)
|
||||||
|
const visibleTabs = computed(() => [
|
||||||
|
{ value: "" as const, label: "全部", count: counts.value?.all },
|
||||||
|
{ value: "1" as const, label: "学生看得到", count: counts.value?.visible },
|
||||||
|
{ value: "0" as const, label: "藏着的", count: counts.value?.hidden },
|
||||||
|
])
|
||||||
|
|
||||||
|
/** 知识点只摆前两个,多的写 +N,鼠标放上去看全部(P073 这类题挂了六个) */
|
||||||
|
function renderTags(tags: string[]) {
|
||||||
|
if (!tags.length) return h("span", { class: "muted" }, "—")
|
||||||
|
return h("span", { class: "tags", title: tags.join("、") }, [
|
||||||
|
...tags.slice(0, 2).map((tag) => h("span", { class: "tag", key: tag }, tag)),
|
||||||
|
tags.length > 2 ? h("span", { class: "muted more" }, `+${tags.length - 2}`) : null,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTitle(row: AdminProblemRow) {
|
||||||
|
const kinds = [
|
||||||
|
row.isSql ? h(ProblemTypeTag, { kind: "sql" }) : null,
|
||||||
|
row.allowFlowchart ? h(ProblemTypeTag, { kind: "flowchart" }) : null,
|
||||||
|
row.hasAstRules ? h(ProblemTypeTag, { kind: "ast" }) : null,
|
||||||
|
]
|
||||||
|
return h("span", { class: "title-cell" }, [
|
||||||
|
h(
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
class: "title-link",
|
||||||
|
href: router.resolve(editRoute(row.id)).href,
|
||||||
|
onClick: (event: MouseEvent) => {
|
||||||
|
if (event.ctrlKey || event.metaKey || event.shiftKey) return
|
||||||
|
event.preventDefault()
|
||||||
|
router.push(editRoute(row.id))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
row.title,
|
||||||
|
),
|
||||||
|
...kinds,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
function editRoute(problemID: number) {
|
||||||
|
return {
|
||||||
|
name: isContestProblemList.value ? "admin contest problem edit" : "admin problem edit",
|
||||||
|
params: isContestProblemList.value ? { contestID: props.contestID, problemID } : { problemID },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const baseColumns: DataTableColumn<AdminProblemRow>[] = [
|
const baseColumns: DataTableColumn<AdminProblemRow>[] = [
|
||||||
{ title: "ID", key: "id", width: 100 },
|
|
||||||
{ title: "显示编号", key: "_id", width: 100 },
|
|
||||||
{
|
{
|
||||||
title: "标题",
|
title: "题号",
|
||||||
key: "title",
|
key: "_id",
|
||||||
minWidth: 200,
|
width: 84,
|
||||||
|
render: (row) => h("span", { class: "did", title: `内部编号 ${row.id}` }, row._id),
|
||||||
|
},
|
||||||
|
{ title: "题目", key: "title", minWidth: 220, ellipsis: false, render: renderTitle },
|
||||||
|
{ title: "知识点", key: "tags", width: 190, render: (row) => renderTags(row.tags) },
|
||||||
|
{
|
||||||
|
title: "做对 / 做过",
|
||||||
|
key: "users",
|
||||||
|
width: 100,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
row.isSql
|
row.triedUsers
|
||||||
? h(NFlex, { size: 6, align: "center", wrap: false }, () => [
|
? h("span", { class: "num" }, [
|
||||||
h("span", row.title),
|
String(row.solvedUsers),
|
||||||
h(ProblemTypeTag, { kind: "sql" }),
|
h("span", { class: "muted" }, ` / ${row.triedUsers}`),
|
||||||
])
|
])
|
||||||
: row.title,
|
: h("span", { class: "muted" }, "没人做过"),
|
||||||
},
|
},
|
||||||
|
{ title: "出题人", key: "username", width: 100, ellipsis: { tooltip: true } },
|
||||||
{
|
{
|
||||||
title: "难度",
|
title: "创建",
|
||||||
key: "difficulty",
|
|
||||||
width: 80,
|
|
||||||
render: (row) =>
|
|
||||||
h(
|
|
||||||
NTag,
|
|
||||||
{ type: getTagColor(row.difficulty), size: "small" },
|
|
||||||
() => DIFFICULTY[row.difficulty],
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "标签",
|
|
||||||
key: "tags",
|
|
||||||
minWidth: 120,
|
|
||||||
render: (row) =>
|
|
||||||
h(NFlex, { size: 4 }, () => row.tags.map((t) => h(NTag, { key: t, size: "small" }, () => t))),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "功能",
|
|
||||||
key: "features",
|
|
||||||
width: 80,
|
|
||||||
render: (row) =>
|
|
||||||
h(NFlex, { size: 4, align: "center" }, () => [
|
|
||||||
row.allowFlowchart
|
|
||||||
? h(Icon, {
|
|
||||||
width: 18,
|
|
||||||
icon: "vscode-icons:file-type-drawio",
|
|
||||||
title: "绘图",
|
|
||||||
})
|
|
||||||
: row.showFlowchart
|
|
||||||
? h(Icon, {
|
|
||||||
width: 18,
|
|
||||||
icon: "vscode-icons:file-type-graphql",
|
|
||||||
title: "流程图",
|
|
||||||
})
|
|
||||||
: null,
|
|
||||||
row.hasAstRules
|
|
||||||
? h(Icon, {
|
|
||||||
width: 18,
|
|
||||||
icon: "vscode-icons:file-type-light-todo",
|
|
||||||
title: "AST",
|
|
||||||
})
|
|
||||||
: null,
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "反馈",
|
|
||||||
key: "topReaction",
|
|
||||||
width: 60,
|
|
||||||
render: (row) => {
|
|
||||||
const top = row.topReaction
|
|
||||||
if (!top) return null
|
|
||||||
const reaction = REACTIONS.find((it) => it.key === top.type)
|
|
||||||
if (!reaction) return null
|
|
||||||
return h(NTooltip, null, {
|
|
||||||
trigger: () => h(Icon, { width: 18, icon: reaction.icon }),
|
|
||||||
default: () => `${reaction.label} ${top.count} 人`,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ title: "出题人", key: "username", width: 120 },
|
|
||||||
{
|
|
||||||
title: "创建时间",
|
|
||||||
key: "create_time",
|
key: "create_time",
|
||||||
width: 200,
|
width: 96,
|
||||||
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) => h("span", { class: "num muted" }, parseTime(row.createTime, "YYYY-MM-DD")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "可见",
|
title: "学生看得到",
|
||||||
key: "visible",
|
key: "visible",
|
||||||
minWidth: 100,
|
width: 96,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
h(NSwitch, {
|
h(NSwitch, {
|
||||||
value: row.visible,
|
value: row.visible,
|
||||||
@@ -180,23 +183,22 @@ const baseColumns: DataTableColumn<AdminProblemRow>[] = [
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "选项",
|
title: "",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
fixed: "right",
|
width: 104,
|
||||||
width: 300,
|
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
h(Actions, {
|
h(Actions, {
|
||||||
problemID: row.id,
|
problemID: row.id,
|
||||||
problemDisplayID: row._id,
|
problemDisplayID: row._id,
|
||||||
|
triedUsers: row.triedUsers,
|
||||||
onUpdated: listProblems,
|
onUpdated: listProblems,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// 比赛题目接口不返回 top_reaction,这一列只在普通题目列表里显示
|
|
||||||
const columns = computed<DataTableColumn<AdminProblemRow>[]>(() =>
|
const columns = computed<DataTableColumn<AdminProblemRow>[]>(() =>
|
||||||
isContestProblemList.value
|
isContestProblemList.value
|
||||||
? baseColumns.filter((it) => !("key" in it) || it.key !== "topReaction")
|
? baseColumns.filter((it) => !("key" in it) || it.key !== "users")
|
||||||
: [{ type: "selection" }, ...baseColumns],
|
: [{ type: "selection" }, ...baseColumns],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -211,9 +213,24 @@ async function listProblems() {
|
|||||||
props.contestID,
|
props.contestID,
|
||||||
undefined,
|
undefined,
|
||||||
query.kind,
|
query.kind,
|
||||||
|
props.contestID ? "" : query.visible,
|
||||||
)
|
)
|
||||||
total.value = res.total
|
total.value = res.total
|
||||||
problems.value = res.results
|
problems.value = res.results
|
||||||
|
counts.value = res.counts
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 勾了几道,一次让学生看得到 / 藏起来(lwp0201 一批 P0xx 题出完一起放出来就是这么用) */
|
||||||
|
const message = useMessage()
|
||||||
|
async function batchVisible(visible: boolean) {
|
||||||
|
try {
|
||||||
|
const { problemCount } = await batchSetProblemVisible(selectedProblemIds.value, visible)
|
||||||
|
message.success(`${problemCount} 道题${visible ? "学生看得到了" : "藏起来了"}`)
|
||||||
|
selectedRowKeys.value = []
|
||||||
|
listProblems()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(errorMessage(error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleVisible(problemID: number) {
|
async function toggleVisible(problemID: number) {
|
||||||
@@ -224,6 +241,15 @@ async function toggleVisible(problemID: number) {
|
|||||||
}
|
}
|
||||||
return it
|
return it
|
||||||
})
|
})
|
||||||
|
// 三个按钮上的数跟着变;停在「藏着的」时这道题也该从列表里走掉,但当场消失会让人以为没点上,下次刷新再说
|
||||||
|
if (counts.value) {
|
||||||
|
const now = problems.value.find((it) => it.id === problemID)?.visible
|
||||||
|
counts.value = {
|
||||||
|
...counts.value,
|
||||||
|
visible: counts.value.visible + (now ? 1 : -1),
|
||||||
|
hidden: counts.value.hidden + (now ? -1 : 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const kindOptions = [
|
const kindOptions = [
|
||||||
@@ -254,7 +280,7 @@ watchDebounced(() => query.keyword, listProblems, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 监听其他查询条件变化
|
// 监听其他查询条件变化
|
||||||
watch(() => [query.page, query.limit, query.author, query.kind], listProblems)
|
watch(() => [query.page, query.limit, query.author, query.kind, query.visible], listProblems)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -264,12 +290,6 @@ watch(() => [query.page, query.limit, query.author, query.kind], listProblems)
|
|||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<template v-if="!isContestProblemList">
|
<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>
|
|
||||||
<CreateProblemMenu label="新建题目" primary @select="createProblem" />
|
<CreateProblemMenu label="新建题目" primary @select="createProblem" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -280,7 +300,7 @@ watch(() => [query.page, query.limit, query.author, query.kind], listProblems)
|
|||||||
<template #filters>
|
<template #filters>
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="query.keyword"
|
v-model:value="query.keyword"
|
||||||
placeholder="输入标题关键字"
|
placeholder="题号或题目名"
|
||||||
clearable
|
clearable
|
||||||
style="width: 220px"
|
style="width: 220px"
|
||||||
@clear="clearQuery"
|
@clear="clearQuery"
|
||||||
@@ -296,13 +316,35 @@ watch(() => [query.page, query.limit, query.author, query.kind], listProblems)
|
|||||||
<n-text depth="3">出题人</n-text>
|
<n-text depth="3">出题人</n-text>
|
||||||
<AuthorSelect v-model:value="query.author" all />
|
<AuthorSelect v-model:value="query.author" all />
|
||||||
</n-flex>
|
</n-flex>
|
||||||
|
<n-button-group v-if="!props.contestID">
|
||||||
|
<n-button
|
||||||
|
v-for="tab in visibleTabs"
|
||||||
|
:key="tab.value"
|
||||||
|
:type="query.visible === tab.value ? 'primary' : 'default'"
|
||||||
|
:secondary="query.visible === tab.value"
|
||||||
|
@click="query.visible = tab.value"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
<span v-if="tab.count !== undefined" class="tab-count">{{ tab.count }}</span>
|
||||||
|
</n-button>
|
||||||
|
</n-button-group>
|
||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
<div v-if="selectedProblemIds.length" class="batch-bar">
|
||||||
|
<b>已选 {{ selectedProblemIds.length }} 道</b>
|
||||||
|
<n-button size="small" @click="batchVisible(true)">让学生看得到</n-button>
|
||||||
|
<n-button size="small" @click="batchVisible(false)">藏起来</n-button>
|
||||||
|
<n-button size="small" @click="openBatchTag('add')">加知识点</n-button>
|
||||||
|
<n-button size="small" @click="openBatchTag('remove')">去掉知识点</n-button>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<n-button size="small" quaternary @click="selectedRowKeys = []">取消选择</n-button>
|
||||||
|
</div>
|
||||||
<n-data-table
|
<n-data-table
|
||||||
striped
|
class="problem-table"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="problems"
|
:data="problems"
|
||||||
:row-key="rowKey"
|
:row-key="rowKey"
|
||||||
|
:checked-row-keys="selectedRowKeys"
|
||||||
@update:checked-row-keys="chooseProblems"
|
@update:checked-row-keys="chooseProblems"
|
||||||
/>
|
/>
|
||||||
<Pagination :total="total" v-model:limit="query.limit" v-model:page="query.page" />
|
<Pagination :total="total" v-model:limit="query.limit" v-model:page="query.page" />
|
||||||
@@ -319,3 +361,79 @@ watch(() => [query.page, query.limit, query.author, query.kind], listProblems)
|
|||||||
@done="onBatchTagDone"
|
@done="onBatchTagDone"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.batch-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid v-bind("theme.successColorSuppl");
|
||||||
|
background: v-bind("theme.hoverColor");
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-count {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-table :deep(.did) {
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-table :deep(.title-cell) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-table :deep(.title-link) {
|
||||||
|
color: inherit;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-table :deep(.title-link:hover) {
|
||||||
|
color: v-bind("theme.primaryColor");
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-table :deep(.tags) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-table :deep(.tag) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0 7px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: v-bind("theme.actionColor");
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-table :deep(.muted) {
|
||||||
|
color: v-bind("theme.textColor3");
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-table :deep(.more) {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-table :deep(.num) {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,5 +16,7 @@ export function toAdminProblemRow(result: AdminProblemListItem): AdminProblemRow
|
|||||||
allowFlowchart: result.allowFlowchart,
|
allowFlowchart: result.allowFlowchart,
|
||||||
showFlowchart: result.showFlowchart,
|
showFlowchart: result.showFlowchart,
|
||||||
topReaction: result.topReaction,
|
topReaction: result.topReaction,
|
||||||
|
solvedUsers: result.solvedUsers,
|
||||||
|
triedUsers: result.triedUsers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,6 +189,9 @@ export interface AdminProblemRow {
|
|||||||
showFlowchart: boolean
|
showFlowchart: boolean
|
||||||
// 比赛题目列表恒为 null —— 只有公开题列表下发最高票评价
|
// 比赛题目列表恒为 null —— 只有公开题列表下发最高票评价
|
||||||
topReaction: { type: ReactionKey; count: number } | null
|
topReaction: { type: ReactionKey; count: number } | null
|
||||||
|
/** 做对 / 做过的人数(题库里的代码提交);比赛题列表恒为 0 */
|
||||||
|
solvedUsers: number
|
||||||
|
triedUsers: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// 题单相关类型
|
// 题单相关类型
|
||||||
|
|||||||
Reference in new issue
Block a user