Deploy / deploy (push) Successful in 58s
- 课堂、比赛在电脑上挪到左栏最左一条竖栏,只写题号(题名鼠标停上去看),顶上从 148px 降到 104px, 页签行和编辑器工具栏对齐成一根线;专题条去掉蓝底、改成 40px 细横条 - 工具栏按钮只剩三种分量:实心提交、浅绿运行例子、其余不带框;统计/点评/我的提交改成图标 + 字 - 题面:题号题名难度状态一行;小标题改成线条图标 + 绿字 + 细线(语法要求、提示用黄色); 去掉空段落、老师写的大标题降成粗体;输入输出上下排;来源缩成底部一行灰字;SQL 去掉图例 - 例子改成「输入 → 输出」:「无」写成「不用输入」、行首空格画成点、长句折行缩进两格写全, 运行例子后序号上标 ✓ / ✗ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
532 lines
19 KiB
Vue
532 lines
19 KiB
Vue
<script setup lang="ts">
|
||
import { Icon } from '@iconify/vue'
|
||
import { useThemeVars } from 'naive-ui'
|
||
import { errorCode } from 'utils/api'
|
||
import { getProblem } from 'oj/api'
|
||
import { useBreakpoints } from 'shared/composables/breakpoints'
|
||
import { storeToRefs } from 'pinia'
|
||
import { useProblemStore } from 'oj/store/problem'
|
||
import { useMyFlowchartStore } from 'shared/store/myFlowchart'
|
||
import { useUserStore } from 'shared/store/user'
|
||
import { useSubmissionStore } from 'oj/store/submission'
|
||
import { useFlowchartStore } from 'oj/store/flowchart'
|
||
import { useCollabStore } from 'shared/store/collab'
|
||
import type { RouteLocationNormalized } from 'vue-router'
|
||
import NotFound from 'shared/components/NotFound.vue'
|
||
// 判完之后该发生的事(烟花、点评、回专题)。静态引入:它得在第一次判完之前就挂好
|
||
import SubmissionEffects from './components/SubmissionEffects.vue'
|
||
import ResultTabLabel from './components/ResultTabLabel.vue'
|
||
import ContextBar from './components/ContextBar.vue'
|
||
import { useEditorMenu } from './composables/editorMenu'
|
||
import { DRAWER_TITLE, useProblemPageContext, type ProblemDrawer as DrawerKey } from './composables/problemPageContext'
|
||
import { useCollabHere, useTeacherCollab } from './composables/teacherCollab'
|
||
import { useProblemMascotGuide } from './composables/mascotGuide'
|
||
|
||
// 抽成具名 loader,便于进页面时与接口并行预取编辑器 chunk。
|
||
// 题库、比赛、专题三种入口用的是同一个编辑器:原来比赛和专题用的是另一个精简版,
|
||
// 没有协作、也没有错误标记,于是专题里「求助」老师接不上、「把中文标点都换成英文」
|
||
// 点了没反应(见 docs/specs/2026-09-28-problem-page-redesign-design.md 第 10 节)
|
||
const loadProblemEditor = () => import('./components/ProblemEditor.vue')
|
||
const ProblemEditor = defineAsyncComponent(loadProblemEditor)
|
||
const ProblemContent = defineAsyncComponent(() => import('./components/ProblemContent.vue'))
|
||
const ProblemDrawer = defineAsyncComponent(() => import('./components/ProblemDrawer.vue'))
|
||
const ResultPane = defineAsyncComponent(() => import('./components/ResultPane.vue'))
|
||
const MobileActionBar = defineAsyncComponent(() => import('./components/MobileActionBar.vue'))
|
||
const SymbolKeys = defineAsyncComponent(() => import('./components/SymbolKeys.vue'))
|
||
|
||
interface Props {
|
||
problemID: string
|
||
contestID?: string
|
||
problemSetId?: string
|
||
}
|
||
|
||
const { problemID, contestID = '', problemSetId = '' } = defineProps<Props>()
|
||
|
||
/** 拉题面失败的原因:null 是还在拉(或拉到了),别的就整页换成 NotFound */
|
||
const failure = ref<null | 'missing' | 'not-started' | 'failed'>(null)
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
|
||
const problemStore = useProblemStore()
|
||
const myFlowchartStore = useMyFlowchartStore()
|
||
const { problem } = storeToRefs(problemStore)
|
||
const ctx = useProblemPageContext()
|
||
|
||
const { isMobile, isDesktop } = useBreakpoints()
|
||
|
||
/**
|
||
* 左栏只有「题目 / 结果」两个页签(手机上编辑器也是一个页签,排在中间)。
|
||
* 原来的六个页签里,流程图并进了「题目」,统计 / 点评 / 我的提交收进了抽屉 ——
|
||
* 见 docs/specs/2026-09-28-problem-page-redesign-design.md 第 4 节。
|
||
*/
|
||
const tabOptions = computed(() => (isMobile.value ? ['content', 'editor', 'result'] : ['content', 'result']))
|
||
|
||
const currentTab = ref('content')
|
||
|
||
watch(
|
||
[() => route.query.tab, () => tabOptions.value],
|
||
([rawTab]) => {
|
||
const tabs = tabOptions.value
|
||
const fallback = tabs[0] ?? 'content'
|
||
currentTab.value = tabs.includes(rawTab as string) ? (rawTab as string) : fallback
|
||
},
|
||
{ immediate: true },
|
||
)
|
||
|
||
watch(currentTab, (tab) => {
|
||
if (!tabOptions.value.includes(tab) || route.query.tab === tab) return
|
||
router.replace({
|
||
query: { ...route.query, tab },
|
||
})
|
||
})
|
||
|
||
// 「结果」第一次切过去才挂(它带着 DataTable 和 Markdown 渲染),挂上之后切走也不卸载:
|
||
// 挂着的错误说明在编辑器里标着红,AI 提示也在渲染
|
||
const resultMounted = ref(false)
|
||
watch(
|
||
currentTab,
|
||
(tab) => {
|
||
if (tab === 'result') resultMounted.value = true
|
||
},
|
||
{ immediate: true },
|
||
)
|
||
|
||
const drawer = ref<DrawerKey | null>(null)
|
||
// 抽屉挂进左栏:要等左栏这个元素挂上之后才能当 Teleport 的目标
|
||
const leftPane = useTemplateRef<HTMLElement>('leftPane')
|
||
const theme = useThemeVars()
|
||
|
||
/**
|
||
* 分隔条两头按像素卡,不按比例:左栏窄于 360 时专题条的「下一题」箭头被裁掉、三个抽屉按钮
|
||
* 折成两行;右栏窄于 460 时工具栏收进「⋯」之后也放不下、语言下拉被挤成两行(原来 0.8 在 1280 屏上只给右栏 256)
|
||
*/
|
||
const { width: windowWidth } = useWindowSize()
|
||
const splitMax = computed(() => `${Math.max(360, windowWidth.value - 460)}px`)
|
||
|
||
// 协作中的老师看的是学生的提交
|
||
const teacherCollab = useTeacherCollab()
|
||
const collabHere = useCollabHere()
|
||
const collabStore = useCollabStore()
|
||
|
||
// 老师刚接上(或下一个人):左栏先切到「结果」,那里是他最后一次交的为什么错。
|
||
// 要演示时老师自己点回「题目」,题目和代码就并排了
|
||
watch(
|
||
() => teacherCollab.value && collabStore.room,
|
||
(room) => {
|
||
if (room) {
|
||
drawer.value = null
|
||
currentTab.value = 'result'
|
||
submissionStore.resultSegment = 'submit'
|
||
}
|
||
},
|
||
)
|
||
const drawerLabel = (key: DrawerKey) => (key === 'submission' && teacherCollab.value ? '学生的提交' : DRAWER_TITLE[key])
|
||
|
||
/** 抽屉按钮前的小图标:页签行右边三个不带框的按钮,和左边的页签分得开(原来是三个圆角胶囊,看着像另一组页签) */
|
||
const DRAWER_ICON: Record<DrawerKey, string> = {
|
||
info: 'ph:chart-bar',
|
||
reaction: 'ph:chat-circle',
|
||
submission: 'ph:clock-counter-clockwise',
|
||
}
|
||
|
||
function toggleDrawer(key: DrawerKey) {
|
||
drawer.value = drawer.value === key ? null : key
|
||
}
|
||
|
||
/**
|
||
* 手机页签行的「⋯」:统计 / 点评 / 我的提交,分隔线,再是编辑器的「更多」那几项
|
||
* (去自测猫、复制、重置……)。原来后面这几项在「代码」页签的工具栏里,得先切过去才点得到
|
||
*/
|
||
const editorMenu = useEditorMenu(computed(() => false))
|
||
const drawerMenu = computed<DropdownOption[]>(() => {
|
||
const drawers: DropdownOption[] = ctx.value.drawers.map((key) => ({
|
||
label: drawerLabel(key),
|
||
key,
|
||
}))
|
||
const editor = editorMenu.options.value
|
||
if (drawers.length && editor.length) drawers.push({ type: 'divider', key: 'divider' })
|
||
return [...drawers, ...editor]
|
||
})
|
||
|
||
function onMobileMenu(key: string) {
|
||
if (editorMenu.select(key)) return
|
||
toggleDrawer(key as DrawerKey)
|
||
}
|
||
|
||
// 交上去、或者语法检查没过:切到「结果」,抽屉开着就先关掉。提交按钮在编辑器那边,页签归这一页管
|
||
const submissionStore = useSubmissionStore()
|
||
watch(
|
||
() => submissionStore.resultSeq,
|
||
() => {
|
||
drawer.value = null
|
||
currentTab.value = 'result'
|
||
},
|
||
)
|
||
|
||
/**
|
||
* 学生正在排队求助、或者老师正在帮他时换题:协作是开在这道题上的,换题就断了
|
||
* (SyncCodeEditor 的 detach),排队的求助也跟着撤掉。先问一句,别让他一点「下一题」
|
||
* 就把正在帮他的老师踢出去。只拦「离开这道题」,切页签改 `?tab=` 不算
|
||
*/
|
||
const dialog = useDialog()
|
||
/**
|
||
* 已经同意过要去的地方。通过之后 1.5 秒内点「下一题」:这里先问一次,接着被强制点评拦下
|
||
* (SubmissionEffects 的 holdForReview),评完再 push 同一个地址 —— 不记着的话会再问一遍,
|
||
* 第二次点「留在这道题」就成了同意换题之后又被留下
|
||
*/
|
||
let approvedTarget: string | null = null
|
||
function confirmLeavingHelp(to: RouteLocationNormalized, from: RouteLocationNormalized) {
|
||
if (to.name === from.name && to.params.problemID === from.params.problemID) return true
|
||
// 协作中的老师:「页面即协作现场」,跳走这一页协作就断了、求助退回排队。所以上下文条、
|
||
// 相似题、下一题这些站内链接一律开新标签,这一页留着(设计文档第 9 节)
|
||
if (teacherCollab.value) {
|
||
window.open(router.resolve(to).href, '_blank')
|
||
return false
|
||
}
|
||
// 老师在任务页上主动打开了他的代码(没举手):helpStatus 是 idle,但老师在
|
||
const peeked = collabHere.value && collabStore.room?.mode === 'peek'
|
||
if (collabStore.isTeacher || (collabStore.helpStatus === 'idle' && !peeked)) return true
|
||
if (approvedTarget === to.fullPath) {
|
||
approvedTarget = null
|
||
return true
|
||
}
|
||
const active = collabStore.helpStatus === 'active' || peeked
|
||
return new Promise<boolean>((resolve) => {
|
||
dialog.warning({
|
||
title: peeked ? '老师正在看你的代码' : active ? '老师正在帮你看这道题' : '你正在排队等老师',
|
||
content: active ? '换题之后,老师那边就断了。确定要换吗?' : '换题之后这次举手就撤掉了,要在新的题目里重新举手。确定要换吗?',
|
||
positiveText: '换题',
|
||
negativeText: '留在这道题',
|
||
onPositiveClick: () => {
|
||
approvedTarget = to.fullPath
|
||
resolve(true)
|
||
},
|
||
onNegativeClick: () => resolve(false),
|
||
onClose: () => resolve(false),
|
||
onMaskClick: () => resolve(false),
|
||
})
|
||
})
|
||
}
|
||
onBeforeRouteUpdate(confirmLeavingHelp)
|
||
onBeforeRouteLeave(confirmLeavingHelp)
|
||
// 到了就作废:没被点评拦下、一次就换过去的,不能留着让下次去同一道题时不再问
|
||
watch(
|
||
() => route.fullPath,
|
||
(path) => {
|
||
if (path === approvedTarget) approvedTarget = null
|
||
},
|
||
)
|
||
|
||
// 看板娘气泡里的「我错在哪」:切到「结果」页签里出错的那一段
|
||
useProblemMascotGuide((segment) => {
|
||
drawer.value = null
|
||
currentTab.value = 'result'
|
||
submissionStore.resultSegment = segment
|
||
})
|
||
|
||
// 结果页签的标题带状态图标(ResultTabLabel),n-tab-pane 的 tab 接受渲染函数
|
||
const resultTab = () => h(ResultTabLabel, { active: currentTab.value === 'result' })
|
||
|
||
// 连着换了几次题,先发的请求后回来的话,地址栏是 C、页面却是 B:只认最后一次
|
||
let initSeq = 0
|
||
async function init() {
|
||
const mine = ++initSeq
|
||
// 「我的流程图」是上一道题的。这道题也画到了 A/S 的话,SubmitFlowchart 查完会再亮出来
|
||
myFlowchartStore.hide()
|
||
// 并行预取右侧编辑器 chunk(CodeMirror ~370K+),
|
||
// 避免等 getProblem 返回后才串行下载,编辑器才迟迟出现
|
||
loadProblemEditor()
|
||
failure.value = null
|
||
try {
|
||
const res = await getProblem(problemID, contestID)
|
||
if (mine !== initSeq) return
|
||
problem.value = res
|
||
} catch (err) {
|
||
if (mine !== initSeq) return
|
||
problem.value = null
|
||
const code = errorCode(err)
|
||
failure.value = code === 'contest-not-started' ? 'not-started' : code === 'problem-not-found' ? 'missing' : 'failed'
|
||
}
|
||
}
|
||
onMounted(init)
|
||
|
||
/** 题不存在时回哪:比赛里回比赛,专题里回专题,否则回题目列表 */
|
||
const backTo = computed(() =>
|
||
contestID
|
||
? { to: `/contest/${contestID}`, toLabel: '回到比赛' }
|
||
: problemSetId
|
||
? { to: `/problemset/${problemSetId}`, toLabel: '回到专题' }
|
||
: { to: '/problem', toLabel: '去题目列表' },
|
||
)
|
||
watch(() => problemID, init)
|
||
|
||
// 题目详情里的 myStatus / myFailedCount 是按当前用户算的,而登录不重新挂载这个页面 ——
|
||
// 会话过期后直接在题目页登录的(机房里最常见的那条路)不补拉一次,AI 提示的解锁进度
|
||
// 就还是匿名时的 0,等于白改。只换 problem,不走 init:那里会把整页重来一遍。
|
||
watch(
|
||
() => useUserStore().isAuthed,
|
||
async (authed) => {
|
||
if (!authed || !problem.value) return
|
||
try {
|
||
problem.value = await getProblem(problemID, contestID)
|
||
} catch {
|
||
// 拉不到就留着现在这份题面,不要把页面清空
|
||
}
|
||
},
|
||
)
|
||
onBeforeUnmount(() => {
|
||
// 提交状态在 store 里、离开页面也不会自己没:不清的话回到题目页(哪怕换了一道题),
|
||
// 结果面板里还是上一次的那条
|
||
useSubmissionStore().reset()
|
||
// 流程图那边同理,还在评的那次不再跟(照常评完落库)
|
||
useFlowchartStore().reset()
|
||
problem.value = null
|
||
failure.value = null
|
||
myFlowchartStore.hide()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<template v-if="problem">
|
||
<SubmissionEffects />
|
||
<!--
|
||
桌面:两栏顶满整个内容区(设计稿「机房(方案 B)」),中间一条 1px 分隔线、可拖动。
|
||
布局给内容区留了 16px 内边距,这里用负边距抵掉
|
||
-->
|
||
<n-split v-if="isDesktop" class="problem-split" direction="horizontal" :default-size="0.43" min="360px" :max="splitMax" :resize-trigger-size="1">
|
||
<template #1>
|
||
<div id="problem-left-pane" ref="leftPane" class="left-pane">
|
||
<ContextBar />
|
||
<div class="tab-row">
|
||
<div class="page-tabs" role="tablist" aria-label="题目 / 结果">
|
||
<button
|
||
type="button"
|
||
role="tab"
|
||
class="page-tab"
|
||
:class="{ active: currentTab === 'content' }"
|
||
:aria-selected="currentTab === 'content'"
|
||
@click="currentTab = 'content'"
|
||
>
|
||
题目
|
||
</button>
|
||
<button
|
||
type="button"
|
||
role="tab"
|
||
class="page-tab"
|
||
:class="{ active: currentTab === 'result' }"
|
||
:aria-selected="currentTab === 'result'"
|
||
@click="currentTab = 'result'"
|
||
>
|
||
<ResultTabLabel :active="currentTab === 'result'" />
|
||
</button>
|
||
</div>
|
||
<div v-if="ctx.drawers.length" class="drawer-buttons">
|
||
<button v-for="key in ctx.drawers" :key="key" type="button" class="drawer-button" @click="toggleDrawer(key)">
|
||
<Icon :icon="DRAWER_ICON[key]" :width="16" aria-hidden="true" />{{ drawerLabel(key) }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<!-- 两个页签各滚各的:读题读到底下切去看结果,结果不该也停在底下 -->
|
||
<div v-show="currentTab === 'content'" class="pane-body">
|
||
<n-scrollbar content-style="padding: 16px 20px">
|
||
<ProblemContent />
|
||
</n-scrollbar>
|
||
</div>
|
||
<div v-if="resultMounted" v-show="currentTab === 'result'" class="pane-body">
|
||
<n-scrollbar content-style="padding: 14px 20px">
|
||
<ResultPane />
|
||
</n-scrollbar>
|
||
</div>
|
||
<!-- 抽屉盖住整个左栏(连上下文条和页签行),抽屉顶上有自己的页签可以互相切换;编辑器不动 -->
|
||
<ProblemDrawer v-if="leftPane" v-model="drawer" :to="leftPane" />
|
||
</div>
|
||
</template>
|
||
<template #2>
|
||
<div class="right-pane">
|
||
<ProblemEditor />
|
||
</div>
|
||
</template>
|
||
</n-split>
|
||
|
||
<!-- Mobile:底部固定「运行例子 / 提交」,内容底下留出它的高度 -->
|
||
<div v-else class="mobile">
|
||
<ContextBar />
|
||
<n-tabs v-model:value="currentTab" type="segment">
|
||
<n-tab-pane name="content" tab="题目">
|
||
<ProblemContent />
|
||
</n-tab-pane>
|
||
<!--
|
||
编辑器一直挂着(show,不是默认的 if):底部的「提交」在三个页签下都能点,
|
||
没切到过「代码」页签的话草稿就没读进来、按钮是灰的;切走就卸载还会丢掉撤销历史
|
||
-->
|
||
<n-tab-pane name="editor" tab="代码" display-directive="show">
|
||
<ProblemEditor />
|
||
</n-tab-pane>
|
||
<n-tab-pane name="result" :tab="resultTab" display-directive="show:lazy">
|
||
<ResultPane />
|
||
</n-tab-pane>
|
||
<template v-if="drawerMenu.length" #suffix>
|
||
<n-dropdown trigger="click" :options="drawerMenu" @select="onMobileMenu">
|
||
<n-button size="small" quaternary aria-label="更多:统计、点评、我的提交、复制代码……"> ⋯ </n-button>
|
||
</n-dropdown>
|
||
</template>
|
||
</n-tabs>
|
||
<ProblemDrawer v-model="drawer" />
|
||
<MobileActionBar />
|
||
<SymbolKeys />
|
||
</div>
|
||
</template>
|
||
<NotFound
|
||
v-else-if="failure === 'not-started'"
|
||
icon="ph:clock"
|
||
title="比赛还没有开始"
|
||
hint="开始以后才能看题"
|
||
:to="`/contest/${contestID}`"
|
||
to-label="回到比赛"
|
||
/>
|
||
<NotFound v-else-if="failure === 'failed'" icon="ph:wifi-slash" title="题目没拉下来">
|
||
<n-button type="primary" @click="init()">再试一次</n-button>
|
||
</NotFound>
|
||
<NotFound v-else-if="failure === 'missing'" title="这道题不存在" hint="可能是题号打错了,或者这道题还没公开" v-bind="backTo" />
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* 手机:课堂条贴满屏宽(设计稿「手机:题目 / 代码 / 结果」),下面的内容照旧留 16px */
|
||
.mobile {
|
||
margin: -16px -16px 0;
|
||
padding-bottom: calc(72px + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.mobile > :deep(.n-tabs) {
|
||
padding: 10px 16px 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.problem-split {
|
||
margin: -16px;
|
||
width: calc(100% + 32px);
|
||
height: calc(100vh - 56px);
|
||
}
|
||
|
||
/* 分隔线:看上去 1px,拖的时候热区宽一点 */
|
||
.problem-split :deep(.n-split__resize-trigger) {
|
||
background-color: v-bind('theme.borderColor');
|
||
}
|
||
|
||
/* 热区挂在分隔线本身上:wrapper 不是定位元素,挂在它上面的 ::before 会盖住整个分栏 */
|
||
.problem-split :deep(.n-split__resize-trigger-wrapper) {
|
||
position: relative;
|
||
}
|
||
|
||
.problem-split :deep(.n-split__resize-trigger-wrapper)::before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0 -4px;
|
||
cursor: col-resize;
|
||
}
|
||
|
||
/*
|
||
* 左栏:课堂的时候最左边是一条竖栏(ContextBar 的 .lesson-rail),竖栏右边从上到下是
|
||
* 专题 / 比赛条、页签行、正文。两个页签的正文叠在同一格里,v-show 只显示一个
|
||
*/
|
||
.left-pane {
|
||
position: relative;
|
||
height: 100%;
|
||
display: grid;
|
||
grid-template-columns: auto minmax(0, 1fr);
|
||
grid-template-rows: auto auto minmax(0, 1fr);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.left-pane > :deep(.lesson-rail) {
|
||
grid-column: 1;
|
||
grid-row: 1 / -1;
|
||
}
|
||
|
||
.left-pane > :deep(.context-bar),
|
||
.left-pane > .tab-row {
|
||
grid-column: 2;
|
||
}
|
||
|
||
.left-pane > .pane-body {
|
||
grid-column: 2;
|
||
grid-row: 3;
|
||
}
|
||
|
||
.right-pane {
|
||
height: 100%;
|
||
}
|
||
|
||
/* 「题目 / 结果」:下划线式页签,右边是抽屉的三个按钮(图标 + 字,不带框)。
|
||
高度和右栏工具栏、抽屉头部一样是 48(设计稿画的 42):没有上下文条时两边并排,
|
||
差 6px 的话两条下边线在分隔线处错成一个台阶 */
|
||
.tab-row {
|
||
flex: none;
|
||
height: 48px;
|
||
box-sizing: border-box;
|
||
padding: 0 10px 0 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
border-bottom: 1px solid v-bind('theme.dividerColor');
|
||
}
|
||
|
||
.page-tabs {
|
||
display: flex;
|
||
height: 100%;
|
||
}
|
||
|
||
.page-tab {
|
||
height: 48px;
|
||
padding: 0 12px;
|
||
border: 0;
|
||
border-bottom: 2px solid transparent;
|
||
background: transparent;
|
||
font: inherit;
|
||
color: v-bind('theme.textColor3');
|
||
cursor: pointer;
|
||
}
|
||
|
||
.page-tab.active {
|
||
border-bottom-color: v-bind('theme.primaryColor');
|
||
color: v-bind('theme.textColor1');
|
||
font-weight: 600;
|
||
}
|
||
|
||
.drawer-buttons {
|
||
margin-left: auto;
|
||
display: flex;
|
||
gap: 2px;
|
||
}
|
||
|
||
.drawer-button {
|
||
height: 32px;
|
||
padding: 0 9px;
|
||
border: 0;
|
||
border-radius: 6px;
|
||
background: transparent;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
font: inherit;
|
||
font-size: 14px;
|
||
color: v-bind('theme.textColor2');
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.drawer-button svg {
|
||
color: v-bind('theme.textColor3');
|
||
}
|
||
|
||
.drawer-button:hover {
|
||
background-color: rgba(128, 128, 128, 0.1);
|
||
color: v-bind('theme.textColor1');
|
||
}
|
||
|
||
.pane-body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
</style>
|