feat(后台): 手机上也能用了——菜单收进左边抽屉,列表一行一张卡,不用再横着滑
- 侧栏手机上不再占一列图标,顶栏左边 ☰ 拉出整张菜单,按返回键先关菜单 - 题目、比赛、用户、公告、题单列表手机上一项一张卡:名字一行、小字一行、按钮一行, 「学生看得到」开关单独一列靠右 - 卡点分析、AI 报告手机上点开一项整屏看,返回键关掉;年度趋势、自学情况、首页改成上下排 - 出题页、教程编辑、题单编辑、标签、设置手机上排成一列;测试数据「删掉」手机上常驻 - 弹框一律收进屏幕宽度 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b757e2ac38
commit
02e2fa7d9b
30 files changed
+1328
-183
No files matched your search
@@ -1029,4 +1029,21 @@ onMounted(async () => {
|
||||
.stale {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 手机:编号 + 题目一行,难度 + 可见折到第二行;判题方式那行也折 */
|
||||
@media (max-width: 768px) {
|
||||
.card {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.headRow,
|
||||
.cardHead,
|
||||
.moreRow {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.headRow .grow {
|
||||
flex: 1 1 180px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouteQuery } from "@vueuse/router"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { zonedParts } from "utils/functions"
|
||||
import type { StuckProblem, StuckProblemDetail, StuckStudent } from "utils/types"
|
||||
import { getStuckProblemDetail, getStuckProblems } from "admin/api"
|
||||
@@ -8,6 +10,8 @@ import UserName from "shared/components/UserName.vue"
|
||||
import { classLabel, groupClassOptions } from "oj/submission/utils"
|
||||
import { periodRange, type Period } from "oj/statistics/period"
|
||||
import { resultName, useResultColor } from "oj/statistics/results"
|
||||
import { useBackClose } from "shared/composables/backClose"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import "oj/learn/learn.css"
|
||||
|
||||
/**
|
||||
@@ -20,6 +24,8 @@ import "oj/learn/learn.css"
|
||||
*/
|
||||
const router = useRouter()
|
||||
const configStore = useConfigStore()
|
||||
const { isMobile } = useBreakpoints()
|
||||
const theme = useThemeVars()
|
||||
const resultColor = useResultColor()
|
||||
|
||||
const PERIODS: { label: string; value: Period }[] = [
|
||||
@@ -145,6 +151,27 @@ async function loadDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机上三栏放不下:知识点横着滑一排、题目列表占满一屏,点一道题,这道题的详情盖满整屏,
|
||||
* 按返回键回到列表
|
||||
*/
|
||||
const detailOpen = ref(false)
|
||||
useBackClose(detailOpen, isMobile)
|
||||
watch(isMobile, (mobile) => {
|
||||
if (!mobile) detailOpen.value = false
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
async function pick(row: StuckProblem) {
|
||||
if (!isMobile.value) {
|
||||
picked.value = String(row.id)
|
||||
return
|
||||
}
|
||||
// 先把地址换好再打开:开着的时候 useBackClose 会拦下一切路由跳转(连 replace 也拦)
|
||||
await router.replace({ query: { ...route.query, problem: String(row.id) } })
|
||||
detailOpen.value = true
|
||||
}
|
||||
|
||||
watch([className, period], load)
|
||||
watch(() => [selected.value?.id, problems.value] as const, loadDetail)
|
||||
onMounted(load)
|
||||
@@ -296,24 +323,26 @@ const links = computed(() => {
|
||||
<span class="grow" />
|
||||
<span class="muted small">一次就对</span>
|
||||
</div>
|
||||
<button
|
||||
v-for="row in knowledge"
|
||||
:key="row.name"
|
||||
type="button"
|
||||
class="item"
|
||||
:class="{ on: row.name === activeTag }"
|
||||
@click="tag = row.name"
|
||||
>
|
||||
<span class="line">
|
||||
<span class="name">{{ row.label }}</span>
|
||||
<span class="grow" />
|
||||
<span class="muted small num">{{ row.problems }} 道</span>
|
||||
</span>
|
||||
<span class="line">
|
||||
<span class="track"><span :style="{ width: `${row.rate}%` }" /></span>
|
||||
<span class="pct num">{{ row.rate }}%</span>
|
||||
</span>
|
||||
</button>
|
||||
<div class="know-list">
|
||||
<button
|
||||
v-for="row in knowledge"
|
||||
:key="row.name"
|
||||
type="button"
|
||||
class="item"
|
||||
:class="{ on: row.name === activeTag }"
|
||||
@click="tag = row.name"
|
||||
>
|
||||
<span class="line">
|
||||
<span class="name">{{ row.label }}</span>
|
||||
<span class="grow" />
|
||||
<span class="muted small num">{{ row.problems }} 道</span>
|
||||
</span>
|
||||
<span class="line">
|
||||
<span class="track"><span :style="{ width: `${row.rate}%` }" /></span>
|
||||
<span class="pct num">{{ row.rate }}%</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card problems">
|
||||
@@ -339,7 +368,7 @@ const links = computed(() => {
|
||||
type="button"
|
||||
class="item"
|
||||
:class="{ on: row.id === selected?.id }"
|
||||
@click="picked = String(row.id)"
|
||||
@click="pick(row)"
|
||||
>
|
||||
<span class="line">
|
||||
<span class="pid muted num">{{ row.displayId }}</span>
|
||||
@@ -359,7 +388,13 @@ const links = computed(() => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="list.length" class="card detail">
|
||||
<section v-if="list.length" class="card detail" :class="{ open: detailOpen }">
|
||||
<div v-if="isMobile" class="detail-bar">
|
||||
<n-button quaternary circle @click="detailOpen = false">
|
||||
<template #icon><Icon icon="ph:arrow-left" /></template>
|
||||
</n-button>
|
||||
<span class="strong">{{ activeTag || "全部" }}</span>
|
||||
</div>
|
||||
<n-spin :show="detailLoading">
|
||||
<div v-if="detail && links" class="detail-body">
|
||||
<div class="line dhead">
|
||||
@@ -860,4 +895,112 @@ const links = computed(() => {
|
||||
width: 96px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.top {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.top .grow {
|
||||
display: none;
|
||||
}
|
||||
.title {
|
||||
width: 100%;
|
||||
}
|
||||
.top .n-select {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.seg {
|
||||
width: 100%;
|
||||
}
|
||||
.seg button {
|
||||
flex: 1;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.main {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.know,
|
||||
.problems {
|
||||
width: auto;
|
||||
}
|
||||
/* 知识点横着一排滑 */
|
||||
.know-list {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
border-top: 1px solid var(--learn-line);
|
||||
}
|
||||
.know .item {
|
||||
flex: 0 0 150px;
|
||||
border-top: 0;
|
||||
border-left: 1px solid var(--learn-line);
|
||||
}
|
||||
.know .item:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
.know .item.on {
|
||||
box-shadow: inset 0 -3px 0 var(--learn-green);
|
||||
}
|
||||
.know .track {
|
||||
width: auto;
|
||||
flex: 1;
|
||||
}
|
||||
.scroll {
|
||||
max-height: none;
|
||||
}
|
||||
/* 一道题的详情盖满整屏 */
|
||||
.detail {
|
||||
display: none;
|
||||
}
|
||||
.detail.open {
|
||||
display: block;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
overflow-y: auto;
|
||||
background: v-bind("theme.bodyColor");
|
||||
}
|
||||
.detail-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 48px;
|
||||
padding: 0 8px;
|
||||
border-bottom: 1px solid var(--learn-line);
|
||||
background: v-bind("theme.bodyColor");
|
||||
}
|
||||
.detail-body {
|
||||
padding: 14px 16px 32px;
|
||||
}
|
||||
.links {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.stats {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
row-gap: 12px;
|
||||
}
|
||||
.cavg {
|
||||
display: none;
|
||||
}
|
||||
.cname {
|
||||
width: 84px;
|
||||
}
|
||||
.track.mid {
|
||||
width: 64px;
|
||||
}
|
||||
.dcol {
|
||||
flex: 1;
|
||||
width: auto;
|
||||
}
|
||||
.names {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -150,14 +150,14 @@ onMounted(async () => {
|
||||
<n-select
|
||||
:value="selected || null"
|
||||
:options="gradeOptions"
|
||||
style="width: 100px"
|
||||
class="grade-select"
|
||||
@update:value="pickGrade"
|
||||
/>
|
||||
<span class="muted">和</span>
|
||||
<n-select
|
||||
:value="compare"
|
||||
:options="compareOptions"
|
||||
style="width: 170px"
|
||||
class="compare-select"
|
||||
@update:value="(value: string) => (compare = value)"
|
||||
/>
|
||||
</div>
|
||||
@@ -455,6 +455,12 @@ a.c-title:hover .ptitle {
|
||||
border-radius: 5px;
|
||||
background: var(--worse);
|
||||
}
|
||||
.grade-select {
|
||||
width: 100px;
|
||||
}
|
||||
.compare-select {
|
||||
width: 170px;
|
||||
}
|
||||
.dot-before {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
@@ -462,4 +468,78 @@ a.c-title:hover .ptitle {
|
||||
box-sizing: border-box;
|
||||
border: 2px solid rgba(128, 128, 128, 0.8);
|
||||
}
|
||||
|
||||
/* 手机上一道题两行:题名和两个百分比一行,下面一整行是轨道;人数藏掉 */
|
||||
@media (max-width: 768px) {
|
||||
.top {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.top .title {
|
||||
width: 100%;
|
||||
}
|
||||
.top .grow {
|
||||
display: none;
|
||||
}
|
||||
.grade-select {
|
||||
width: 90px;
|
||||
}
|
||||
.compare-select {
|
||||
flex: 1;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.stats {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.stat {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.value {
|
||||
font-size: 22px;
|
||||
}
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
column-gap: 10px;
|
||||
row-gap: 4px;
|
||||
}
|
||||
.axis {
|
||||
height: auto;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.axis .c-title {
|
||||
display: none;
|
||||
}
|
||||
.legend {
|
||||
grid-row: 1;
|
||||
margin-left: 0;
|
||||
}
|
||||
.item {
|
||||
height: auto;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.c-title {
|
||||
width: auto;
|
||||
}
|
||||
.c-track {
|
||||
grid-column: 1 / -1;
|
||||
min-width: 0;
|
||||
}
|
||||
.item .c-track {
|
||||
grid-row: 2;
|
||||
}
|
||||
.c-values {
|
||||
grid-row: 1;
|
||||
grid-column: 2;
|
||||
width: auto;
|
||||
}
|
||||
.c-delta {
|
||||
grid-row: 1;
|
||||
grid-column: 3;
|
||||
}
|
||||
.c-users {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { errorCode, errorMessage } from "utils/api"
|
||||
import { deleteContestProblem, deleteProblem, makeProblemPublic } from "admin/api"
|
||||
import download from "utils/download"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
|
||||
/**
|
||||
* 题目列表一行的操作:编辑、在前台看、下载测试数据、删除直接摆出来(用户说的:不要收进 ⋯)。
|
||||
@@ -22,6 +23,10 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
// 手机上题目列表一道题一张卡,按钮在卡片里放不下一行就折下去;测试数据是 zip,手机上下了也没处用,
|
||||
// 转成公开题要想好新题号,也留给电脑,都不摆
|
||||
const { isMobile } = useBreakpoints()
|
||||
|
||||
const isContestProblem = computed(() => route.name === "admin contest problem list")
|
||||
|
||||
const showMakePublicModal = ref(false)
|
||||
@@ -106,11 +111,13 @@ async function handleMakePublic() {
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<n-flex :size="6" :wrap="false">
|
||||
<n-flex :size="6" :wrap="isMobile">
|
||||
<n-button size="small" @click="goEdit">编辑</n-button>
|
||||
<n-button size="small" title="在前台打开这道题" @click="goCheck">前台看</n-button>
|
||||
<n-button size="small" title="下载测试数据" @click="downloads">测试数据</n-button>
|
||||
<n-button v-if="isContestProblem" size="small" @click="openMakePublicModal">
|
||||
<n-button v-if="!isMobile" size="small" title="下载测试数据" @click="downloads">
|
||||
测试数据
|
||||
</n-button>
|
||||
<n-button v-if="isContestProblem && !isMobile" size="small" @click="openMakePublicModal">
|
||||
转成公开题
|
||||
</n-button>
|
||||
<span :title="props.triedUsers ? `有 ${props.triedUsers} 人交过,删不掉` : undefined">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import { caseStatus, type AnswerSource, type CaseRow } from "./caseRows"
|
||||
|
||||
/**
|
||||
@@ -14,6 +15,8 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const theme = useThemeVars()
|
||||
// 手机上「当例子」那一列的表头藏了,勾选框自己带上字
|
||||
const { isMobile } = useBreakpoints()
|
||||
|
||||
/** 输出最多先露这么多行,长了点开看全 */
|
||||
const CLIP_LINES = 6
|
||||
@@ -106,26 +109,30 @@ const status = (row: CaseRow) => caseStatus(row, props.source)
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<n-checkbox
|
||||
v-if="!examplesOnly"
|
||||
v-model:checked="row.example"
|
||||
class="example"
|
||||
:aria-label="`第 ${index + 1} 组当例子`"
|
||||
/>
|
||||
<span class="mark" :class="status(row)">
|
||||
<template v-if="status(row) === 'ok'">✓</template>
|
||||
<template v-else-if="status(row) === 'error'">✗</template>
|
||||
<template v-else-if="status(row) === 'changed'">●</template>
|
||||
</span>
|
||||
<n-button
|
||||
quaternary
|
||||
size="tiny"
|
||||
class="remove"
|
||||
:aria-label="`删掉第 ${index + 1} 组`"
|
||||
@click="remove(index)"
|
||||
>
|
||||
删掉
|
||||
</n-button>
|
||||
<!-- 桌面上 display: contents,三样各占一格;手机上折到输入输出下面排成一行 -->
|
||||
<div class="tools">
|
||||
<n-checkbox
|
||||
v-if="!examplesOnly"
|
||||
v-model:checked="row.example"
|
||||
class="example"
|
||||
:aria-label="`第 ${index + 1} 组当例子`"
|
||||
:label="isMobile ? '当例子' : undefined"
|
||||
/>
|
||||
<span class="mark" :class="status(row)">
|
||||
<template v-if="status(row) === 'ok'">✓</template>
|
||||
<template v-else-if="status(row) === 'error'">✗</template>
|
||||
<template v-else-if="status(row) === 'changed'">●</template>
|
||||
</span>
|
||||
<n-button
|
||||
quaternary
|
||||
size="tiny"
|
||||
class="remove"
|
||||
:aria-label="`删掉第 ${index + 1} 组`"
|
||||
@click="remove(index)"
|
||||
>
|
||||
删掉
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -254,4 +261,37 @@ const status = (row: CaseRow) => caseStatus(row, props.source)
|
||||
.remove:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tools {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
/* 手机:一组占两行 —— 编号 / 输入 / 输出,下面一行 当例子 ✓ 删掉(没有悬停,删掉常显) */
|
||||
@media (max-width: 768px) {
|
||||
.row,
|
||||
.examplesOnly .row {
|
||||
grid-template-columns: 18px minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.row.head > :nth-child(n + 4) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tools {
|
||||
grid-column: 2 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.example {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.remove {
|
||||
margin-left: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@ import { NButton, NTag } from "naive-ui"
|
||||
import Pagination from "shared/components/Pagination.vue"
|
||||
import type { AdminProblemRow } from "utils/types"
|
||||
import { batchTagProblems, getProblemList } from "admin/api"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
@@ -25,7 +26,9 @@ const page = ref(1)
|
||||
const limit = ref(10)
|
||||
const keyword = ref("")
|
||||
|
||||
const columns: DataTableColumn<AdminProblemRow>[] = [
|
||||
const { isMobile } = useBreakpoints()
|
||||
|
||||
const allColumns: DataTableColumn<AdminProblemRow>[] = [
|
||||
{ title: "题号", key: "_id", width: 90 },
|
||||
{
|
||||
title: "题目",
|
||||
@@ -51,6 +54,15 @@ const columns: DataTableColumn<AdminProblemRow>[] = [
|
||||
},
|
||||
]
|
||||
|
||||
// 手机上放不下四列:不摆「学生看得到」,题号收窄,题目吃剩下的宽度
|
||||
const mobileColumns: DataTableColumn<AdminProblemRow>[] = [
|
||||
{ ...allColumns[0], width: 64 } as DataTableColumn<AdminProblemRow>,
|
||||
{ ...allColumns[1], minWidth: undefined } as DataTableColumn<AdminProblemRow>,
|
||||
{ ...allColumns[3], width: 64 } as DataTableColumn<AdminProblemRow>,
|
||||
]
|
||||
|
||||
const columns = computed(() => (isMobile.value ? mobileColumns : allColumns))
|
||||
|
||||
async function listProblems() {
|
||||
if (page.value < 1) page.value = 1
|
||||
const offset = (page.value - 1) * limit.value
|
||||
@@ -117,7 +129,7 @@ watchDebounced(
|
||||
<span>共 {{ total }} 道题</span>
|
||||
<n-input
|
||||
v-model:value="keyword"
|
||||
style="width: 220px"
|
||||
:style="{ width: isMobile ? '160px' : '220px' }"
|
||||
placeholder="题号或题目名"
|
||||
clearable
|
||||
/>
|
||||
|
||||
@@ -1707,4 +1707,42 @@ onMounted(async () => {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 手机:卡片收窄,挤在一行的东西折下去 */
|
||||
@media (max-width: 768px) {
|
||||
.card {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* 编号 + 题目一行,难度 + 可见折到第二行 */
|
||||
.headRow {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.headRow .grow {
|
||||
flex: 1 1 180px;
|
||||
}
|
||||
|
||||
.io {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
/* 老题面里的图片带着写死的像素宽(有 1118px 的),把整页撑出横向滚动 */
|
||||
.card :deep(.w-e-image-container) {
|
||||
max-width: 100%;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.card :deep(.w-e-image-container img) {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.inlineRow {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.blockers {
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -15,6 +15,7 @@ import BatchTagModal from "./components/BatchTagModal.vue"
|
||||
import CreateProblemMenu from "./components/CreateProblemMenu.vue"
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import ProblemTypeTag from "oj/problem/components/ProblemTypeTag.vue"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
|
||||
/**
|
||||
* 后台题目列表(设计稿「后台题目列表」PlA / PlB)。一行一道题:题号、题目(点了就是编辑)、
|
||||
@@ -32,6 +33,7 @@ const props = defineProps<Props>()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const theme = useThemeVars()
|
||||
const { isMobile } = useBreakpoints()
|
||||
|
||||
const title = computed(
|
||||
() =>
|
||||
@@ -96,7 +98,7 @@ const { query, clearQuery } = usePagination<ProblemQuery>({
|
||||
})
|
||||
|
||||
const counts = ref<{ all: number; visible: number; hidden: number } | null>(null)
|
||||
// 手机上后台左边还有一列图标,三个按钮放不下、「藏着的」被切掉:换小号,再不够就横着滑
|
||||
// 手机上三个按钮放不下、「藏着的」被切掉:换小号,再不够就横着滑
|
||||
const narrow = useMediaQuery("(max-width: 640px)")
|
||||
const visibleTabs = computed(() => [
|
||||
{ value: "" as const, label: "全部", count: counts.value?.all },
|
||||
@@ -206,11 +208,62 @@ const baseColumns: DataTableColumn<AdminProblemRow>[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const columns = computed<DataTableColumn<AdminProblemRow>[]>(() =>
|
||||
isContestProblemList.value
|
||||
? baseColumns.filter((it) => !("key" in it) || it.key !== "users")
|
||||
: [{ type: "selection" }, ...baseColumns],
|
||||
)
|
||||
/**
|
||||
* 手机上一道题一张卡:题号 + 题目一行,知识点 / 人数 / 出题人 / 日期一行小字,操作按钮一行;
|
||||
* 「学生看得到」开关单独一列靠右,不用横着滑就能点到
|
||||
*/
|
||||
function renderMobileCard(row: AdminProblemRow) {
|
||||
const meta = [
|
||||
row.tags.length
|
||||
? row.tags.slice(0, 2).join("、") + (row.tags.length > 2 ? ` +${row.tags.length - 2}` : "")
|
||||
: null,
|
||||
isContestProblemList.value
|
||||
? null
|
||||
: row.triedUsers
|
||||
? `做对 ${row.solvedUsers} / ${row.triedUsers}`
|
||||
: "没人做过",
|
||||
row.username,
|
||||
parseTime(row.createTime, "YYYY-MM-DD"),
|
||||
].filter((it): it is string => !!it)
|
||||
return h("div", { class: "m-card" }, [
|
||||
h("div", { class: "m-head" }, [h("span", { class: "did" }, row._id), renderTitle(row)]),
|
||||
// 每一项自己不断开(「做对 8 / 14」折成两半看不懂),放不下整项换行
|
||||
h(
|
||||
"div",
|
||||
{ class: "m-meta muted" },
|
||||
meta.map((text, i) => h("span", { class: "m-item", key: i }, i ? ` · ${text}` : text)),
|
||||
),
|
||||
h(Actions, {
|
||||
problemID: row.id,
|
||||
problemDisplayID: row._id,
|
||||
triedUsers: row.triedUsers,
|
||||
onUpdated: listProblems,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
const mobileColumns: DataTableColumn<AdminProblemRow>[] = [
|
||||
{ title: "题目", key: "title", render: renderMobileCard },
|
||||
{
|
||||
title: "看得到",
|
||||
key: "visible",
|
||||
width: 72,
|
||||
align: "center",
|
||||
render: (row) =>
|
||||
h(NSwitch, {
|
||||
value: row.visible,
|
||||
rubberBand: false,
|
||||
onUpdateValue: () => toggleVisible(row.id),
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
const columns = computed<DataTableColumn<AdminProblemRow>[]>(() => {
|
||||
const cols = isMobile.value ? mobileColumns : baseColumns
|
||||
return isContestProblemList.value
|
||||
? cols.filter((it) => !("key" in it) || it.key !== "users")
|
||||
: [{ type: "selection" }, ...cols]
|
||||
})
|
||||
|
||||
async function listProblems() {
|
||||
if (query.page < 1) query.page = 1
|
||||
@@ -312,7 +365,7 @@ watch(() => [query.page, query.limit, query.author, query.kind, query.visible],
|
||||
v-model:value="query.keyword"
|
||||
placeholder="题号或题目名"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
:style="{ width: isMobile ? '100%' : '220px' }"
|
||||
@clear="clearQuery"
|
||||
/>
|
||||
<n-select
|
||||
@@ -385,6 +438,7 @@ watch(() => [query.page, query.limit, query.author, query.kind, query.visible],
|
||||
border: 1px solid v-bind("theme.successColorSuppl");
|
||||
background: v-bind("theme.hoverColor");
|
||||
white-space: nowrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
@@ -409,6 +463,7 @@ watch(() => [query.page, query.limit, query.author, query.kind, query.visible],
|
||||
|
||||
.problem-table :deep(.did) {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -456,6 +511,28 @@ watch(() => [query.page, query.limit, query.author, query.kind, query.visible],
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.problem-table :deep(.m-card) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.problem-table :deep(.m-head) {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.problem-table :deep(.m-meta) {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.problem-table :deep(.m-item) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.problem-table :deep(.num) {
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -301,4 +301,34 @@ watchDebounced(keyword, listTags, { debounce: 500, maxWait: 1000 })
|
||||
.empty {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
/* 手机:两栏上下排;题数条不画,名字吃剩下的宽度,三个按钮放得下 */
|
||||
@media (max-width: 768px) {
|
||||
.columns {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.row {
|
||||
gap: 6px;
|
||||
padding: 0 0 0 6px;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.count {
|
||||
width: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user