feat(提交列表): 重做成左边列表、右边代码,方向键一条条翻,不再开弹框
- 右栏常驻选中那条的代码:编译失败 / 运行时错误用中文说清第几行错了并标红, 老师看得到逐个测试点,学生看别人的只显示结果 - 老师一键「这节课」:筛到正在上课的班、这节课的题、今天 - 「正在评分」的行自己变成结果;新交的不插队,顶上提示「有 N 条新提交」 - 按天分组、默认每页 30 条;状态筛选补全,语言补上 SQL;流程图的评分详情也放进右栏 - 课堂看板的格子和名字能直接点进这个学生今天的提交 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ea480d5e0c
commit
660f689a5c
14 files changed
+2845
-673
No files matched your search
@@ -0,0 +1,629 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { getSubmission } from "oj/api"
|
||||
import { explainPythonCompileError } from "oj/problem/utils/pythonError"
|
||||
import { explainRuntimeError } from "oj/problem/utils/runtimeError"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import {
|
||||
JUDGE_STATUS,
|
||||
LANGUAGE_FORMAT_VALUE,
|
||||
LANGUAGE_SHOW_VALUE,
|
||||
SubmissionStatus,
|
||||
} from "utils/constants"
|
||||
import {
|
||||
submissionCaseResults,
|
||||
submissionMemoryFormat,
|
||||
submissionTimeFormat,
|
||||
} from "utils/functions"
|
||||
import type { Submission, SubmissionListItem } from "utils/types"
|
||||
import { useCopySubmission } from "../composables/copySubmission"
|
||||
import { useTone } from "../composables/tone"
|
||||
import { submissionTimeText } from "../utils"
|
||||
import CodeView from "./CodeView.vue"
|
||||
import StatusPill from "./StatusPill.vue"
|
||||
import UserName from "./UserName.vue"
|
||||
|
||||
/**
|
||||
* 提交列表的右栏:选中那一条的代码和结果(设计稿「提交列表重设计 · 定稿」)。
|
||||
* 原来要点开一个弹框看,老师翻全班代码时是「点开 → 看 → 关掉 → 点下一行」。
|
||||
*/
|
||||
const props = defineProps<{
|
||||
row: SubmissionListItem
|
||||
/** 老师那套:「只看他」「只看这题」、重新判题、逐个测试点 */
|
||||
teacher: boolean
|
||||
/** 在比赛的提交列表里(不能重判,题目链接走比赛) */
|
||||
contest: boolean
|
||||
/** 右下角的「本页第 i / n 条」 */
|
||||
position: { index: number; count: number } | null
|
||||
/** 手机上的整屏详情:头部折行、不提方向键 */
|
||||
narrow?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
filterUser: [username: string]
|
||||
filterProblem: [displayId: string]
|
||||
openProblem: [row: SubmissionListItem]
|
||||
rejudge: [id: string]
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const theme = useThemeVars()
|
||||
// 暗色下测试点方块的底色是主题色本身(浅),白字就看不清了
|
||||
const isDark = useDark()
|
||||
const tone = useTone()
|
||||
const { copyToCat, copyToProblem } = useCopySubmission()
|
||||
|
||||
/**
|
||||
* 详情按「id + 结果」缓存:同一条判完之前结果会变,变了要重拉;判完的代码不会再变,
|
||||
* 老师在几行之间来回按方向键时不用每次都等网络。
|
||||
*/
|
||||
const cache = new Map<string, Submission>()
|
||||
const detail = ref<Submission | null>(null)
|
||||
const loading = ref(false)
|
||||
const failed = ref(false)
|
||||
|
||||
const judging = computed(
|
||||
() =>
|
||||
props.row.result === SubmissionStatus.pending || props.row.result === SubmissionStatus.judging,
|
||||
)
|
||||
|
||||
async function load() {
|
||||
const row = props.row
|
||||
failed.value = false
|
||||
if (!row.showLink) {
|
||||
detail.value = null
|
||||
return
|
||||
}
|
||||
const key = `${row.id}:${row.result}`
|
||||
const hit = cache.get(key)
|
||||
if (hit) {
|
||||
detail.value = hit
|
||||
return
|
||||
}
|
||||
// 换行时先别清空:接口一般几十毫秒就回来,清了会闪一下白
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getSubmission(row.id)
|
||||
if (props.row.id !== row.id) return
|
||||
if (!judging.value) cache.set(key, res)
|
||||
detail.value = res
|
||||
} catch {
|
||||
if (props.row.id === row.id) failed.value = true
|
||||
} finally {
|
||||
if (props.row.id === row.id) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => [props.row.id, props.row.result, props.row.showLink] as const, load, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
const shown = computed(() => (detail.value?.id === props.row.id ? detail.value : null))
|
||||
|
||||
const locked = computed(() => {
|
||||
if (props.row.showLink) return null
|
||||
if (!userStore.isAuthed) return "anon" as const
|
||||
if (props.row.userId === userStore.user?.id) return "problemset" as const
|
||||
return "others" as const
|
||||
})
|
||||
|
||||
/** 逐个测试点。只有管理员拿得到 info(每个点带 output_md5),学生只有 caseSummary 两个数 */
|
||||
const cases = computed(() => submissionCaseResults(shown.value?.info))
|
||||
|
||||
function stripJudgePath(text: string) {
|
||||
return text.replace(/\/judger\/run\/[^/"]+\//g, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* 编译失败 / 运行时错误说清楚「错在第几行、为什么」—— 翻译表和题目页结果区同一份
|
||||
* (pythonError.ts / runtimeError.ts)。原来的看代码弹框压根不显示报错。
|
||||
*/
|
||||
const explain = computed(() => {
|
||||
const s = shown.value
|
||||
if (!s) return null
|
||||
const info = s.statisticInfo
|
||||
const err = info?.err_info ? stripJudgePath(info.err_info) : ""
|
||||
switch (s.result) {
|
||||
case SubmissionStatus.compile_error: {
|
||||
const ex = s.language === "Python" && err ? explainPythonCompileError(err) : null
|
||||
if (ex) return { line: ex.line, text: ex.message, raw: err }
|
||||
return { line: null, text: "编译没通过,看看下面的原始报错。", raw: err }
|
||||
}
|
||||
case SubmissionStatus.runtime_error: {
|
||||
const rt = info?.runtime_error
|
||||
if (rt) return { line: rt.line ?? null, text: explainRuntimeError(rt), raw: err }
|
||||
return { line: null, text: "程序运行到一半出错,停下来了。", raw: err }
|
||||
}
|
||||
case SubmissionStatus.cpu_time_limit_exceeded:
|
||||
case SubmissionStatus.real_time_limit_exceeded:
|
||||
return {
|
||||
line: null,
|
||||
text: "程序跑了太久还没结束:可能有死循环,或者在等一个没有给的输入。",
|
||||
raw: "",
|
||||
}
|
||||
case SubmissionStatus.memory_limit_exceeded:
|
||||
return { line: null, text: "程序占的内存太多了。", raw: "" }
|
||||
case SubmissionStatus.system_error:
|
||||
return { line: null, text: "判题机出错了,不是代码的问题。", raw: err }
|
||||
case SubmissionStatus.wrong_answer: {
|
||||
const check = info?.sample_check
|
||||
if (!check) return null
|
||||
const text = check.passed
|
||||
? "题目里的例子都对,错在没公开的测试点上。"
|
||||
: `在例子 ${(check.index ?? 0) + 1} 上就错了。`
|
||||
return { line: null, text, raw: "" }
|
||||
}
|
||||
case SubmissionStatus.ast_check_failed: {
|
||||
const failedRules = (info?.ast_results ?? []).filter((rule) => !rule.passed)
|
||||
const text = failedRules.length
|
||||
? `答案对了,但没按要求写:${failedRules.map((rule) => rule.description).join(";")}`
|
||||
: "答案对了,但没按题目要求的写法写。"
|
||||
return { line: null, text, raw: "" }
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const explainTone = computed(() => {
|
||||
const r = props.row.result
|
||||
return tone(
|
||||
r === SubmissionStatus.compile_error ||
|
||||
r === SubmissionStatus.runtime_error ||
|
||||
r === SubmissionStatus.ast_check_failed
|
||||
? "warning"
|
||||
: r === SubmissionStatus.system_error
|
||||
? "default"
|
||||
: "error",
|
||||
)
|
||||
})
|
||||
|
||||
const rawOpen = ref(false)
|
||||
watch(
|
||||
() => props.row.id,
|
||||
() => (rawOpen.value = false),
|
||||
)
|
||||
|
||||
const failedCaseText = computed(() => {
|
||||
const bad = cases.value
|
||||
.map((item, index) => ({ item, index }))
|
||||
.filter(({ item }) => item.result !== SubmissionStatus.accepted)
|
||||
if (!bad.length) return ""
|
||||
const names = bad.map(({ index }) => index + 1).join("、")
|
||||
const kinds = new Set(bad.map(({ item }) => JUDGE_STATUS[item.result]?.name ?? ""))
|
||||
return kinds.size === 1 ? `测试点 ${names} ${[...kinds][0]}` : `测试点 ${names} 没通过`
|
||||
})
|
||||
|
||||
function caseColor(result: number) {
|
||||
if (result === SubmissionStatus.accepted) return tone("success").solid
|
||||
if (result === SubmissionStatus.runtime_error) return tone("warning").solid
|
||||
return tone("error").solid
|
||||
}
|
||||
|
||||
const canRejudge = computed(() => props.teacher && !props.contest)
|
||||
const moreOptions = computed(() => [
|
||||
{ label: "复制回到题目", key: "problem" },
|
||||
...(shown.value?.language !== "SQL" ? [{ label: "复制到自测猫", key: "cat" }] : []),
|
||||
])
|
||||
|
||||
function handleMore(key: string) {
|
||||
if (!shown.value) return
|
||||
if (key === "problem") copyToProblem(shown.value)
|
||||
else copyToCat(shown.value)
|
||||
}
|
||||
|
||||
function openStandalone() {
|
||||
window.open("/submission/" + props.row.id, "_blank")
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="pane" :class="{ narrow }">
|
||||
<div class="head">
|
||||
<div class="who">
|
||||
<UserName :username="row.username" :muted="!!locked && locked !== 'problemset'" />
|
||||
<button
|
||||
v-if="teacher"
|
||||
class="filter-pill"
|
||||
title="只看这个人的提交"
|
||||
@click="emit('filterUser', row.username)"
|
||||
>
|
||||
<Icon icon="ph:funnel-simple-bold" :width="11" />只看他
|
||||
</button>
|
||||
</div>
|
||||
<span class="dot">·</span>
|
||||
<a class="problem" href="#" @click.prevent="emit('openProblem', row)">
|
||||
<span class="problem-id">{{ row.problemDisplayId }}</span>
|
||||
{{ row.problemTitle }}
|
||||
</a>
|
||||
<button
|
||||
v-if="teacher"
|
||||
class="filter-pill"
|
||||
title="只看这道题的提交"
|
||||
@click="emit('filterProblem', row.problemDisplayId)"
|
||||
>
|
||||
<Icon icon="ph:funnel-simple-bold" :width="11" />只看这题
|
||||
</button>
|
||||
<a
|
||||
v-if="row.problemSet"
|
||||
class="problemset"
|
||||
:href="'/problemset/' + row.problemSet.id"
|
||||
target="_blank"
|
||||
:title="'从题单「' + row.problemSet.title + '」交的'"
|
||||
>
|
||||
题单 {{ row.problemSet.title }}
|
||||
</a>
|
||||
<div class="spacer"></div>
|
||||
<template v-if="shown">
|
||||
<template v-if="teacher">
|
||||
<n-button v-if="canRejudge" size="small" @click="emit('rejudge', row.id)">
|
||||
<template #icon><Icon icon="ph:arrow-clockwise" /></template>
|
||||
重新判题
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
quaternary
|
||||
title="新页面打开(有测试点明细)"
|
||||
aria-label="新页面打开"
|
||||
@click="openStandalone"
|
||||
>
|
||||
<template #icon><Icon icon="ph:arrow-square-out" /></template>
|
||||
</n-button>
|
||||
<n-dropdown :options="moreOptions" trigger="click" @select="handleMore">
|
||||
<n-button size="small" quaternary aria-label="更多" title="复制回到题目、复制到自测猫">
|
||||
<template #icon><Icon icon="ph:dots-three-bold" /></template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
<template v-else>
|
||||
<n-button size="small" type="primary" @click="copyToProblem(shown)">
|
||||
<template #icon><Icon icon="ph:arrow-u-up-left" /></template>
|
||||
复制回到题目
|
||||
</n-button>
|
||||
<n-button v-if="shown.language !== 'SQL'" size="small" @click="copyToCat(shown)">
|
||||
复制到自测猫
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
quaternary
|
||||
title="新页面打开"
|
||||
aria-label="新页面打开"
|
||||
@click="openStandalone"
|
||||
>
|
||||
<template #icon><Icon icon="ph:arrow-square-out" /></template>
|
||||
</n-button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="meta">
|
||||
<StatusPill :result="row.result" />
|
||||
<span v-if="teacher && cases.length" class="cases">
|
||||
<span
|
||||
v-for="(item, index) in cases"
|
||||
:key="index"
|
||||
class="case"
|
||||
:style="{ background: caseColor(item.result), color: isDark ? '#18181c' : '#ffffff' }"
|
||||
:title="`测试点 ${index + 1}:${JUDGE_STATUS[item.result]?.name ?? ''}`"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
<span v-if="failedCaseText" class="muted">{{ failedCaseText }}</span>
|
||||
</span>
|
||||
<span
|
||||
v-else-if="row.caseSummary && row.caseSummary.passed < row.caseSummary.total"
|
||||
class="partial"
|
||||
:style="{ color: tone('error').color }"
|
||||
>
|
||||
通过 {{ row.caseSummary.passed }}/{{ row.caseSummary.total }} 个测试点
|
||||
</span>
|
||||
<span>{{ LANGUAGE_SHOW_VALUE[row.language] }}</span>
|
||||
<span>{{ submissionTimeText(row.createTime, true) }}</span>
|
||||
<template v-if="row.statisticInfo?.time_cost !== undefined">
|
||||
<span>耗时 {{ submissionTimeFormat(row.statisticInfo.time_cost) }}</span>
|
||||
<span>内存 {{ submissionMemoryFormat(row.statisticInfo.memory_cost) }}</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 看不到代码的三种情况 -->
|
||||
<div v-if="locked" class="locked">
|
||||
<Icon icon="ph:lock-simple" :width="30" class="lock-icon" />
|
||||
<template v-if="locked === 'others'">
|
||||
<span class="locked-title">别人的代码看不到</span>
|
||||
<span class="muted">自己动手做出来,才是自己的。</span>
|
||||
<n-button type="primary" @click="emit('openProblem', row)">
|
||||
去做 {{ row.problemDisplayId }} {{ row.problemTitle }}
|
||||
</n-button>
|
||||
</template>
|
||||
<template v-else-if="locked === 'problemset'">
|
||||
<span class="locked-title">这道题在你加入的题单里</span>
|
||||
<span class="muted">加入题单之前交的代码先藏起来,在题单里做完这道题就能看到。</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="locked-title">登录之后才能看自己的代码</span>
|
||||
<span class="muted">谁交了什么题、对没对,不登录也能看。</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="explain"
|
||||
class="explain"
|
||||
:style="{ background: explainTone.background, borderColor: explainTone.background }"
|
||||
>
|
||||
<div class="explain-line">
|
||||
<b :style="{ color: explainTone.color }">
|
||||
<template v-if="explain.line">第 {{ explain.line }} 行:</template>{{ explain.text }}
|
||||
</b>
|
||||
<div class="spacer"></div>
|
||||
<a v-if="explain.raw" href="#" class="raw-toggle" @click.prevent="rawOpen = !rawOpen">
|
||||
原始报错
|
||||
<Icon :icon="rawOpen ? 'ph:caret-up-bold' : 'ph:caret-down-bold'" :width="11" />
|
||||
</a>
|
||||
</div>
|
||||
<pre v-if="rawOpen && explain.raw" class="raw">{{ explain.raw }}</pre>
|
||||
</div>
|
||||
<CodeView
|
||||
v-if="shown"
|
||||
:code="shown.code"
|
||||
:language="LANGUAGE_FORMAT_VALUE[shown.language]"
|
||||
:mark-line="explain?.line ?? null"
|
||||
/>
|
||||
<div v-else class="placeholder">
|
||||
<n-spin v-if="loading" size="small" />
|
||||
<span v-else-if="failed" class="muted">
|
||||
代码没拉下来,
|
||||
<a href="#" @click.prevent="load">再试一次</a>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="!narrow" class="foot">
|
||||
<span class="keys"><kbd>↑</kbd><kbd>↓</kbd></span>
|
||||
<span>
|
||||
{{
|
||||
teacher
|
||||
? "换上一条 / 下一条,到本页头尾自动翻页"
|
||||
: "换上一条 / 下一条(只在能看代码的提交之间走)"
|
||||
}}
|
||||
</span>
|
||||
<div class="spacer"></div>
|
||||
<span v-if="position">本页第 {{ position.index }} / {{ position.count }} 条</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pane {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 14px 20px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
background: v-bind("theme.bodyColor");
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
font-size: 15px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.narrow {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.narrow .head {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.narrow .head .spacer {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.who {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dot {
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
|
||||
.problem {
|
||||
color: v-bind("theme.textColor1");
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.problem:hover {
|
||||
color: v-bind("theme.primaryColor");
|
||||
}
|
||||
|
||||
.problem-id {
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
|
||||
.problemset {
|
||||
flex: none;
|
||||
height: 20px;
|
||||
padding: 0 7px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: v-bind("tone('info').color");
|
||||
background: v-bind("tone('info').background");
|
||||
}
|
||||
|
||||
.filter-pill {
|
||||
flex: none;
|
||||
height: 22px;
|
||||
padding: 0 7px;
|
||||
border: 1px solid v-bind("tone('success').background");
|
||||
border-radius: 11px;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
color: v-bind("tone('success').color");
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-pill:hover {
|
||||
background: v-bind("tone('success').background");
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
color: v-bind("theme.textColor3");
|
||||
white-space: nowrap;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 6px;
|
||||
}
|
||||
|
||||
.cases {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.case {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cases .muted {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.partial {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
|
||||
.explain {
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.explain-line {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.raw-toggle {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
color: v-bind("theme.textColor3");
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.raw {
|
||||
margin: 0;
|
||||
max-height: 140px;
|
||||
overflow: auto;
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.locked,
|
||||
.placeholder {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.locked {
|
||||
border: 1px dashed v-bind("theme.borderColor");
|
||||
background: v-bind("theme.cardColor");
|
||||
}
|
||||
|
||||
.lock-icon {
|
||||
color: v-bind("theme.textColor3");
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.locked-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
|
||||
.keys {
|
||||
display: inline-flex;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
kbd {
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 4px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid v-bind("theme.borderColor");
|
||||
border-radius: 3px;
|
||||
background: v-bind("theme.cardColor");
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user