feat(题单): 题单页重做——布置中的放最上面,详情顶上一条进度和奖章阶梯,没加入看得见题点不进;老师看全班、后台一屏编辑
Deploy / deploy (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
- 列表:布置中的大卡片(做对几道、布置到哪天、再做对几道拿哪个奖章),其余小卡片,加入过的在前 - 详情:进度 + 奖章阶梯(下一个金圈「再做对 n 道」),题目两列写几点做对 / 交过几次; 以前做对过的标「旧代码先藏着」;做完整条变绿,烟花只在做完那一刻放 - 老师多一个「全班情况」页签;题目页从链接直接进来没加入时,条上给「加入题单」 - 后台一屏编辑:布置一周 / 两周 / 四周 / 选日期,公开开关,粘贴多个题号一次加,拖动排序,奖章只剩两种条件 - 旧代码藏着的地方都说清楚是哪个题单、哪天之后能看 - 修掉列表卡片把选做题数说成「新加了 N 题」 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8c53e28dcb
commit
c17f88cdbb
32 files changed
+2630
-2181
No files matched your search
@@ -1,130 +1,637 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouteQuery } from "@vueuse/router"
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { getProblemSetDetail, getProblemSetProblems, joinProblemSet } from "oj/api"
|
||||
import { useTone } from "oj/submission/composables/tone"
|
||||
import { errorMessage } from "utils/api"
|
||||
import { getProblemSetDetail, getProblemSetProblems, joinProblemSet, getUserBadges } from "../api"
|
||||
import type { ProblemSet, ProblemSetProblem, UserBadge as UserBadgeType } from "utils/types"
|
||||
import { useFireworks } from "../problem/composables/useFireworks"
|
||||
import ProblemSetHeader from "./components/ProblemSetHeader.vue"
|
||||
import ProblemSetProblemsList from "./components/ProblemSetProblemsList.vue"
|
||||
import UserProgressView from "./components/UserProgressView.vue"
|
||||
import { parseTime } from "utils/functions"
|
||||
import type { ProblemSet, ProblemSetProblem } from "utils/types"
|
||||
import { useAuthModalStore } from "shared/store/authModal"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import { useFireworks } from "../problem/composables/useFireworks"
|
||||
import BadgeLadder from "./components/BadgeLadder.vue"
|
||||
import ClassView from "./components/ClassView.vue"
|
||||
|
||||
/**
|
||||
* 题单详情(设计稿「题单重设计」详情 B):顶上一条是进度和奖章阶梯,下面题目分两列。
|
||||
*
|
||||
* - 还没加入:题目看得见、点不进去,右边一个大按钮「加入题单,开始做」。加入是用户要留的
|
||||
* 一步(「只是打开就算加入的话,会有疑惑的」),所以这里要让人一眼看懂为什么要点。
|
||||
* - 布置期内以前做对过的题标「以前做对过 · 旧代码先藏着」,规则见后端 problemSetLockCutoffs。
|
||||
* - 做完了:整条变绿,写几点做完、用了多久。烟花只在做完的那一刻放(做对最后一道、
|
||||
* 1.5 秒后跳回来的那一下),以后再打开不放。
|
||||
* - 老师多一个「全班情况」页签。
|
||||
*/
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
const { celebrate } = useFireworks()
|
||||
const theme = useThemeVars()
|
||||
const tone = useTone()
|
||||
const userStore = useUserStore()
|
||||
const authStore = useAuthModalStore()
|
||||
const { celebrate } = useFireworks()
|
||||
|
||||
const problemSetId = computed(() => Number(route.params.problemSetId))
|
||||
|
||||
const problemSet = ref<ProblemSet | null>(null)
|
||||
const id = computed(() => Number(route.params.problemSetId))
|
||||
const set = ref<ProblemSet | null>(null)
|
||||
const problems = ref<ProblemSetProblem[]>([])
|
||||
const isJoined = ref(false)
|
||||
const isJoining = ref(false)
|
||||
const userBadges = ref<UserBadgeType[]>([])
|
||||
const activeTab = ref("problems")
|
||||
const missing = ref(false)
|
||||
const joining = ref(false)
|
||||
const tab = useRouteQuery<string>("tab", "problems")
|
||||
|
||||
async function loadProblemSetDetail() {
|
||||
const res = await getProblemSetDetail(problemSetId.value)
|
||||
problemSet.value = res
|
||||
isJoined.value = res.userProgress?.isJoined || false
|
||||
}
|
||||
|
||||
async function loadProblems() {
|
||||
const res = await getProblemSetProblems(problemSetId.value)
|
||||
problems.value = res
|
||||
}
|
||||
|
||||
async function loadUserBadges() {
|
||||
if (!isJoined.value) return
|
||||
|
||||
const res = await getUserBadges()
|
||||
userBadges.value = res.filter(
|
||||
(badge: UserBadgeType) => badge.badge.problemSetId === problemSetId.value,
|
||||
)
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await Promise.all([loadProblemSetDetail(), loadProblems()])
|
||||
if (isJoined.value) {
|
||||
if (problemSet.value?.userProgress?.isCompleted) {
|
||||
celebrate()
|
||||
}
|
||||
loadUserBadges()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleProblemClick(problemId: string) {
|
||||
if (!userStore.isAuthed) {
|
||||
message.warning("请先登录!")
|
||||
return
|
||||
}
|
||||
if (!isJoined.value) {
|
||||
message.warning("请先点击【加入题单】按钮!")
|
||||
return
|
||||
}
|
||||
router.push({
|
||||
name: "problemset problem",
|
||||
params: {
|
||||
problemSetId: problemSetId.value,
|
||||
problemID: problemId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleJoinProblemSet() {
|
||||
if (isJoining.value) return
|
||||
|
||||
isJoining.value = true
|
||||
async function load() {
|
||||
missing.value = false
|
||||
try {
|
||||
await joinProblemSet(problemSetId.value)
|
||||
isJoined.value = true
|
||||
message.success("成功加入题单!")
|
||||
// 加入题单后加载用户徽章
|
||||
await loadUserBadges()
|
||||
} catch (err) {
|
||||
message.error("加入题单失败:" + errorMessage(err, "未知错误"))
|
||||
} finally {
|
||||
isJoining.value = false
|
||||
const [detail, list] = await Promise.all([
|
||||
getProblemSetDetail(id.value),
|
||||
getProblemSetProblems(id.value),
|
||||
])
|
||||
set.value = detail
|
||||
problems.value = list
|
||||
} catch {
|
||||
missing.value = true
|
||||
return
|
||||
}
|
||||
const done = set.value.userProgress.completeTime
|
||||
if (set.value.userProgress.isCompleted && done && Date.now() - Date.parse(done) < 60_000) {
|
||||
celebrate()
|
||||
}
|
||||
}
|
||||
|
||||
// 「用户进度」那一栏调的是 requireTeacher 的接口(Teacher Admin | Super Admin),
|
||||
// 所以这里的条件必须跟它一致。以前写的是「超管 或 自己完成了题单」,两边正好错开:
|
||||
// 学生做完题单会看到这一栏、点进去 403;而真正该用它的 Teacher Admin 反倒看不到。
|
||||
const showTabs = computed(() => userStore.isTeacherOrAbove)
|
||||
watch(id, load)
|
||||
onMounted(load)
|
||||
|
||||
onMounted(init)
|
||||
const progress = computed(() => set.value?.userProgress)
|
||||
const joined = computed(() => !!progress.value?.isJoined)
|
||||
/** 老师不用加入也能点进题目去看(不记进度),也不给他摆「加入」按钮 */
|
||||
const canOpen = computed(() => joined.value || userStore.isTeacherOrAbove)
|
||||
const optional = computed(() => (set.value ? set.value.problemsCount - set.value.requiredCount : 0))
|
||||
const percent = computed(() =>
|
||||
set.value && progress.value?.isJoined && set.value.requiredCount
|
||||
? Math.min(100, (progress.value.completedCount / set.value.requiredCount) * 100)
|
||||
: 0,
|
||||
)
|
||||
const hiddenCount = computed(() => problems.value.filter((item) => item.oldCodeHidden).length)
|
||||
|
||||
/** 做完用了多久:从加入到做完 */
|
||||
const spent = computed(() => {
|
||||
const p = progress.value
|
||||
if (!p?.joinTime || !p.completeTime) return ""
|
||||
const minutes = Math.round((Date.parse(p.completeTime) - Date.parse(p.joinTime)) / 60_000)
|
||||
if (minutes < 60) return `${Math.max(1, minutes)} 分钟`
|
||||
if (minutes < 24 * 60) return `${Math.round(minutes / 60)} 小时`
|
||||
return `${Math.round(minutes / 1440)} 天`
|
||||
})
|
||||
|
||||
const columns = computed(() => {
|
||||
const half = Math.ceil(problems.value.length / 2)
|
||||
return [problems.value.slice(0, half), problems.value.slice(half)].filter((col) => col.length)
|
||||
})
|
||||
|
||||
function indexOf(item: ProblemSetProblem) {
|
||||
return problems.value.indexOf(item) + 1
|
||||
}
|
||||
|
||||
function solvedAt(time: string) {
|
||||
const today = parseTime(new Date(), "YYYY-MM-DD") === parseTime(time, "YYYY-MM-DD")
|
||||
return parseTime(time, today ? "HH:mm" : "M月D日")
|
||||
}
|
||||
|
||||
function problemLink(item: ProblemSetProblem) {
|
||||
return {
|
||||
name: "problemset problem",
|
||||
params: { problemSetId: id.value, problemID: item.problem._id },
|
||||
}
|
||||
}
|
||||
|
||||
async function join() {
|
||||
if (!userStore.isAuthed) {
|
||||
authStore.openLoginModal()
|
||||
return
|
||||
}
|
||||
if (joining.value) return
|
||||
joining.value = true
|
||||
try {
|
||||
await joinProblemSet(id.value)
|
||||
message.success("加入了,现在开始在这里做对的题都会记进度")
|
||||
await load()
|
||||
} catch (err) {
|
||||
message.error(errorMessage(err, "加入失败"))
|
||||
} finally {
|
||||
joining.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑只给出题人和超管(后台按归属校验,别人点进去是 404) */
|
||||
const canEdit = computed(
|
||||
() => !!set.value && (userStore.isSuperAdmin || set.value.createdBy.id === userStore.user?.id),
|
||||
)
|
||||
|
||||
const info = computed(() => tone("info"))
|
||||
const success = computed(() => tone("success"))
|
||||
const warning = computed(() => tone("warning"))
|
||||
const purple = { color: "#5b3fa8", background: "rgba(122, 95, 208, 0.12)" }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="problemSet">
|
||||
<ProblemSetHeader
|
||||
:problem-set="problemSet"
|
||||
:is-joined="isJoined"
|
||||
:is-joining="isJoining"
|
||||
:user-badges="userBadges"
|
||||
@join="handleJoinProblemSet"
|
||||
/>
|
||||
<n-tabs v-if="showTabs" v-model:value="activeTab" animated>
|
||||
<n-tab-pane name="problems" tab="题目列表">
|
||||
<ProblemSetProblemsList
|
||||
:problems="problems"
|
||||
:is-joined="isJoined"
|
||||
@problem-click="handleProblemClick"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="progress" tab="用户进度">
|
||||
<UserProgressView />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
<ProblemSetProblemsList
|
||||
v-else
|
||||
:problems="problems"
|
||||
:is-joined="isJoined"
|
||||
@problem-click="handleProblemClick"
|
||||
/>
|
||||
<n-result
|
||||
v-if="missing"
|
||||
status="404"
|
||||
title="题单不存在"
|
||||
description="可能还没公开,或者已经删掉了"
|
||||
>
|
||||
<template #footer>
|
||||
<n-button @click="router.push({ name: 'problemsets' })">回到题单列表</n-button>
|
||||
</template>
|
||||
</n-result>
|
||||
|
||||
<div v-else-if="set" class="page">
|
||||
<section class="band" :class="{ done: progress?.isCompleted }">
|
||||
<span v-if="progress?.isCompleted" class="done-mark" aria-hidden="true">✓</span>
|
||||
<div class="head">
|
||||
<div class="row">
|
||||
<router-link :to="{ name: 'problemsets' }" class="muted back">‹ 题单</router-link>
|
||||
<h2 class="ell">{{ set.title }}</h2>
|
||||
<span v-if="set.assigning" class="pill assign"
|
||||
>布置到 {{ parseTime(set.assignedUntil!, "M月D日") }}</span
|
||||
>
|
||||
</div>
|
||||
<template v-if="progress?.isCompleted">
|
||||
<div class="done-text">
|
||||
<b>全部做完了</b> · {{ parseTime(progress.completeTime!, "M月D日 HH:mm") }}
|
||||
<template v-if="spent">,加入后用了 {{ spent }}</template>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="joined">
|
||||
<div class="row baseline">
|
||||
<span class="muted small">做对</span>
|
||||
<span class="num count">{{ progress!.completedCount }}</span>
|
||||
<span class="muted num">/ {{ set.requiredCount }} 道</span>
|
||||
<span class="muted tiny gap">
|
||||
<template v-if="optional > 0">另有 {{ optional }} 道选做 · </template>
|
||||
{{ parseTime(progress!.joinTime!, "M月D日 HH:mm") }} 加入
|
||||
</span>
|
||||
</div>
|
||||
<div class="bar"><div class="fill" :style="{ width: `${percent}%` }"></div></div>
|
||||
</template>
|
||||
<div v-else class="muted small">
|
||||
{{ set.createdBy.username }} 出的 · {{ set.problemsCount }} 道<template
|
||||
v-if="optional > 0"
|
||||
>,{{ optional }} 道选做</template
|
||||
><template v-else>,全部必做</template>
|
||||
<template v-if="!set.badges.length"> · 这个题单没有奖章</template>
|
||||
</div>
|
||||
<div v-if="set.description && set.description !== set.title" class="sec small desc">
|
||||
{{ set.description }}
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="set.badges.length">
|
||||
<span class="divider"></span>
|
||||
<div class="ladder-box"><BadgeLadder :set="set" /></div>
|
||||
</template>
|
||||
<div class="spacer"></div>
|
||||
<div v-if="!joined && !userStore.isTeacherOrAbove" class="join">
|
||||
<n-button type="primary" size="large" :loading="joining" @click="join">
|
||||
加入题单,开始做
|
||||
</n-button>
|
||||
<span class="muted tiny">加入以后,在题单里做对的题才算进度</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="!joined && hiddenCount" class="notice" :style="purple">
|
||||
<svg
|
||||
class="lock"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="5" y="11" width="14" height="9" rx="2" />
|
||||
<path d="M8 11V8a4 4 0 0 1 8 0v3" />
|
||||
</svg>
|
||||
其中 <b>{{ hiddenCount }}</b> 道你以前做对过。布置期内(到
|
||||
{{
|
||||
parseTime(set.assignedUntil!, "M月D日")
|
||||
}})这几道的旧代码先藏起来,在题单里重新做对就能看。
|
||||
</div>
|
||||
|
||||
<div v-if="userStore.isTeacherOrAbove" class="tabs">
|
||||
<button class="seg" :class="{ on: tab !== 'class' }" @click="tab = 'problems'">题目</button>
|
||||
<button class="seg" :class="{ on: tab === 'class' }" @click="tab = 'class'">全班情况</button>
|
||||
<div class="spacer"></div>
|
||||
<n-button
|
||||
v-if="canEdit"
|
||||
size="small"
|
||||
@click="router.push({ name: 'admin problemset edit', params: { problemSetId: set.id } })"
|
||||
>
|
||||
编辑题单
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<ClassView v-if="userStore.isTeacherOrAbove && tab === 'class'" :problem-set-id="set.id" />
|
||||
|
||||
<template v-else>
|
||||
<div v-if="problems.length" class="cols">
|
||||
<section v-for="(col, ci) in columns" :key="ci" class="card list">
|
||||
<component
|
||||
:is="canOpen ? 'router-link' : 'div'"
|
||||
v-for="item in col"
|
||||
:key="item.id"
|
||||
:to="canOpen ? problemLink(item) : undefined"
|
||||
class="prow"
|
||||
:class="{ off: !canOpen }"
|
||||
>
|
||||
<span v-if="item.isCompleted" class="dot done-dot">✓</span>
|
||||
<span v-else-if="item.wrongCount" class="dot tried-dot">!</span>
|
||||
<span v-else class="dot empty-dot"></span>
|
||||
<span class="muted num idx">{{ indexOf(item) }}</span>
|
||||
<span class="ell ptitle">{{ item.problem.title }}</span>
|
||||
<span v-if="!item.isRequired" class="opt">选做</span>
|
||||
<span v-if="item.oldCodeHidden" class="old" :style="purple"
|
||||
><svg
|
||||
class="lock"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="5" y="11" width="14" height="9" rx="2" />
|
||||
<path d="M8 11V8a4 4 0 0 1 8 0v3" /></svg
|
||||
>以前做对过 · 旧代码先藏着</span
|
||||
>
|
||||
<div class="spacer"></div>
|
||||
<span v-if="item.isCompleted && item.solvedTime" class="muted num tiny"
|
||||
>{{ solvedAt(item.solvedTime) }} 做对</span
|
||||
>
|
||||
<span v-else-if="item.wrongCount && joined" class="tiny tried"
|
||||
>交过 {{ item.wrongCount }} 次</span
|
||||
>
|
||||
<span v-else-if="canOpen" class="go">去做 ›</span>
|
||||
</component>
|
||||
</section>
|
||||
</div>
|
||||
<n-empty v-else description="这个题单还没有题目" />
|
||||
<div v-if="joined && problems.length" class="muted tiny foot">做对一道会自动回到这里</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.band {
|
||||
box-sizing: border-box;
|
||||
padding: 14px 20px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px 24px;
|
||||
border: 1px solid v-bind("theme.borderColor");
|
||||
border-radius: 6px;
|
||||
background: v-bind("theme.cardColor");
|
||||
}
|
||||
|
||||
.band.done {
|
||||
background: v-bind("success.background");
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.done-mark {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
background: v-bind("theme.successColor");
|
||||
color: #ffffff;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.done-text {
|
||||
font-size: 14px;
|
||||
color: v-bind("success.color");
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 380px;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.head h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.row.baseline {
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.back {
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.gap {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: v-bind("theme.actionColor");
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fill {
|
||||
height: 100%;
|
||||
background: v-bind("theme.successColor");
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
align-self: stretch;
|
||||
background: v-bind("theme.dividerColor");
|
||||
}
|
||||
|
||||
.ladder-box {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.join {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.lock {
|
||||
flex-shrink: 0;
|
||||
vertical-align: -1px;
|
||||
}
|
||||
|
||||
.old .lock {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.notice {
|
||||
box-sizing: border-box;
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.seg {
|
||||
height: 30px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid v-bind("theme.borderColor");
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
color: v-bind("theme.textColor2");
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.seg:first-child {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
.seg + .seg {
|
||||
border-left: none;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.seg.on {
|
||||
color: v-bind("success.color");
|
||||
background: v-bind("success.background");
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cols {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid v-bind("theme.borderColor");
|
||||
border-radius: 6px;
|
||||
background: v-bind("theme.cardColor");
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.prow {
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid v-bind("theme.dividerColor");
|
||||
color: v-bind("theme.textColor1");
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.prow:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
a.prow:hover {
|
||||
background: v-bind("theme.hoverColor");
|
||||
}
|
||||
|
||||
.prow.off {
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.done-dot {
|
||||
background: v-bind("theme.successColor");
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.tried-dot {
|
||||
background: v-bind("warning.background");
|
||||
color: v-bind("warning.color");
|
||||
}
|
||||
|
||||
.empty-dot {
|
||||
border: 1.5px dashed v-bind("theme.borderColor");
|
||||
}
|
||||
|
||||
.idx {
|
||||
width: 20px;
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ptitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.opt {
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
border: 1px solid v-bind("theme.borderColor");
|
||||
color: v-bind("theme.textColor3");
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.old {
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tried {
|
||||
color: v-bind("warning.color");
|
||||
}
|
||||
|
||||
.go {
|
||||
font-size: 13px;
|
||||
color: v-bind("theme.primaryColor");
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.foot {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pill {
|
||||
height: 22px;
|
||||
padding: 0 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.assign {
|
||||
color: v-bind("info.color");
|
||||
background: v-bind("info.background");
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
|
||||
.sec {
|
||||
color: v-bind("theme.textColor2");
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tiny {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.num {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.ell {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.cols {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.head {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.join {
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user