学生端:目录 + 居中限宽正文 + 可收起的示例代码栏;目录改成三态圆点, 顶部加总进度;上一课/下一课栏桌面端固定在底部。 教师端:汇总卡片、学生状态标签与筛选(未开始/7 天没学/只读不练等)、 最后学习补「N 天前」、按练习表加「没人一次对/多数人卡住」提示。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
55
apps/web/src/oj/learn/components/LearnSummary.vue
Normal file
55
apps/web/src/oj/learn/components/LearnSummary.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { TUTORIAL_READ_SECONDS } from "@oj2/contract"
|
||||
import type { TutorialProgress } from "utils/types"
|
||||
|
||||
const props = defineProps<{
|
||||
titles: { id: number; title: string }[]
|
||||
progress: Record<number, TutorialProgress>
|
||||
traced: boolean
|
||||
}>()
|
||||
|
||||
const stats = computed(() => {
|
||||
const rows = props.titles.map((t) => props.progress[t.id])
|
||||
const read = rows.filter(
|
||||
(p) => p && p.totalSeconds >= TUTORIAL_READ_SECONDS,
|
||||
).length
|
||||
const solved = rows.reduce((n, p) => n + (p?.exerciseSolved ?? 0), 0)
|
||||
const total = rows.reduce((n, p) => n + (p?.exerciseTotal ?? 0), 0)
|
||||
return { read, solved, total }
|
||||
})
|
||||
|
||||
const percent = computed(() =>
|
||||
props.titles.length
|
||||
? Math.round((stats.value.read / props.titles.length) * 100)
|
||||
: 0,
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="traced && titles.length" class="summary">
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="percent"
|
||||
:height="8"
|
||||
:show-indicator="false"
|
||||
status="success"
|
||||
/>
|
||||
<n-text depth="3" class="numbers">
|
||||
已读 {{ stats.read }}/{{ titles.length }} 课
|
||||
<template v-if="stats.total">
|
||||
· 练一练 {{ stats.solved }}/{{ stats.total }}
|
||||
</template>
|
||||
</n-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.summary {
|
||||
padding: 4px 10px 12px;
|
||||
}
|
||||
.numbers {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
24
apps/web/src/oj/learn/components/LessonBody.vue
Normal file
24
apps/web/src/oj/learn/components/LessonBody.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { MdPreview } from "md-editor-v3"
|
||||
import "md-editor-v3/lib/preview.css"
|
||||
import type { Segment } from "../composables/useExerciseParse"
|
||||
|
||||
defineProps<{ segments: Segment[]; lang?: string }>()
|
||||
|
||||
const isDark = useDark()
|
||||
const ExerciseWidget = defineAsyncComponent(
|
||||
() => import("./ExerciseWidget.vue"),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-for="(seg, i) in segments" :key="i">
|
||||
<MdPreview
|
||||
v-if="seg.type === 'md'"
|
||||
preview-theme="vuepress"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
:model-value="seg.content"
|
||||
/>
|
||||
<ExerciseWidget v-else :exercise="seg.exercise" :lang="lang" />
|
||||
</template>
|
||||
</template>
|
||||
@@ -1,9 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { TUTORIAL_READ_SECONDS } from "@oj2/contract"
|
||||
import type { TutorialProgress } from "utils/types"
|
||||
import { readableDuration } from "utils/functions"
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
titles: { id: number; title: string }[]
|
||||
step: number
|
||||
/** 按教程 id 索引的自学留痕,未登录时是空的 */
|
||||
@@ -14,70 +13,114 @@ defineProps<{
|
||||
|
||||
const emit = defineEmits<{ select: [lesson: number] }>()
|
||||
|
||||
// 打开过但一秒都没攒够时 readableDuration 给的是 "-",「读了 -」不像人话。
|
||||
// 心跳 15 秒一跳,点开就走确实会落在 0 上
|
||||
function readSoFar(seconds: number) {
|
||||
return seconds > 0 ? readableDuration(seconds) : "不到 1 分钟"
|
||||
type Status = "todo" | "reading" | "done"
|
||||
|
||||
/**
|
||||
* 三态:没打开过 / 读过但没读满或练习没做完 / 读满且练习全对。
|
||||
* 没有练习的课只看阅读;「已读」的门槛沿用契约的 TUTORIAL_READ_SECONDS。
|
||||
*/
|
||||
function statusOf(id: number): Status {
|
||||
const p = props.progress[id]
|
||||
if (!p?.viewCount) return "todo"
|
||||
const read = p.totalSeconds >= TUTORIAL_READ_SECONDS
|
||||
const practiced = !p.exerciseTotal || p.exerciseSolved >= p.exerciseTotal
|
||||
return read && practiced ? "done" : "reading"
|
||||
}
|
||||
|
||||
function hint(id: number) {
|
||||
const p = props.progress[id]
|
||||
if (!p?.exerciseTotal) return ""
|
||||
return `练一练 ${p.exerciseSolved}/${p.exerciseTotal}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item
|
||||
<ol class="lessons">
|
||||
<li
|
||||
v-for="(item, index) in titles"
|
||||
:key="item.id"
|
||||
class="lesson"
|
||||
:class="{ active: step === index + 1 }"
|
||||
@click="emit('select', index + 1)"
|
||||
>
|
||||
<!-- 标题独占一行:目录栏只有屏幕的五分之一宽,把「已读」摆在同一行会把
|
||||
中文标题挤成两截 -->
|
||||
<n-flex vertical :size="2">
|
||||
<n-text
|
||||
:type="step === index + 1 ? 'primary' : undefined"
|
||||
:strong="step === index + 1"
|
||||
>
|
||||
{{ index + 1 }}. {{ item.title }}
|
||||
</n-text>
|
||||
<!-- 每篇教程都有一条进度(没读过的是一行零),所以这里判的是读没读过,
|
||||
不是有没有这条记录。
|
||||
满 TUTORIAL_READ_SECONDS 才打 ✓:打开过但没读满的仍然显示时长,
|
||||
只是不带勾、也不是成功色 —— 记是记下了,还没到「已读」 -->
|
||||
<n-text
|
||||
v-if="progress[item.id]?.totalSeconds >= TUTORIAL_READ_SECONDS"
|
||||
type="success"
|
||||
style="font-size: 12px"
|
||||
>
|
||||
✓ 已读 · {{ readableDuration(progress[item.id].totalSeconds) }}
|
||||
</n-text>
|
||||
<n-text
|
||||
v-else-if="progress[item.id]?.viewCount"
|
||||
depth="3"
|
||||
style="font-size: 12px"
|
||||
>
|
||||
读了 {{ readSoFar(progress[item.id].totalSeconds) }}
|
||||
</n-text>
|
||||
<n-text
|
||||
v-if="progress[item.id]?.exerciseTotal"
|
||||
:type="
|
||||
progress[item.id].exerciseSolved === progress[item.id].exerciseTotal
|
||||
? 'success'
|
||||
: undefined
|
||||
"
|
||||
:depth="
|
||||
progress[item.id].exerciseSolved === progress[item.id].exerciseTotal
|
||||
? undefined
|
||||
: 3
|
||||
"
|
||||
style="font-size: 12px"
|
||||
>
|
||||
练一练 {{ progress[item.id].exerciseSolved }} /
|
||||
{{ progress[item.id].exerciseTotal }}
|
||||
</n-text>
|
||||
</n-flex>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
|
||||
<!-- 只在没登录时提一句。登录了却还没读的人不需要被提醒「你还没读」 -->
|
||||
<n-text v-if="!traced" depth="3" style="display: block; padding: 8px 4px">
|
||||
<span class="dot" :class="traced ? statusOf(item.id) : 'todo'">
|
||||
<template v-if="traced && statusOf(item.id) === 'done'">✓</template>
|
||||
<template v-else>{{ index + 1 }}</template>
|
||||
</span>
|
||||
<span class="text">
|
||||
<span class="title">{{ item.title }}</span>
|
||||
<span v-if="traced && hint(item.id)" class="hint">
|
||||
{{ hint(item.id) }}
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
<n-text v-if="!traced" depth="3" class="login-tip">
|
||||
登录后可以记录学习进度
|
||||
</n-text>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.lessons {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.lesson {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
.lesson:hover {
|
||||
background: rgba(128, 128, 128, 0.12);
|
||||
}
|
||||
.lesson.active {
|
||||
background: rgba(24, 160, 88, 0.14);
|
||||
}
|
||||
.dot {
|
||||
flex: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 12px;
|
||||
border: 1.5px solid rgba(128, 128, 128, 0.5);
|
||||
}
|
||||
.dot.reading {
|
||||
border-color: #f0a020;
|
||||
color: #f0a020;
|
||||
}
|
||||
.dot.done {
|
||||
border-color: #18a058;
|
||||
background: #18a058;
|
||||
color: #fff;
|
||||
}
|
||||
.active .dot.todo {
|
||||
border-color: #18a058;
|
||||
color: #18a058;
|
||||
}
|
||||
.text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
.title {
|
||||
line-height: 1.4;
|
||||
}
|
||||
.active .title {
|
||||
font-weight: 600;
|
||||
}
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.login-tip {
|
||||
display: block;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
38
apps/web/src/oj/learn/components/PagerBar.vue
Normal file
38
apps/web/src/oj/learn/components/PagerBar.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from "naive-ui"
|
||||
|
||||
defineProps<{ step: number; total: number }>()
|
||||
const theme = useThemeVars()
|
||||
const emit = defineEmits<{ go: [lesson: number] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="pager" :style="{ background: theme.bodyColor }">
|
||||
<n-button secondary :disabled="step <= 1" @click="emit('go', step - 1)">
|
||||
← 上一课
|
||||
</n-button>
|
||||
<n-text depth="3">{{ step }} / {{ total }}</n-text>
|
||||
<n-button
|
||||
type="primary"
|
||||
:secondary="step >= total"
|
||||
:disabled="step >= total"
|
||||
@click="emit('go', step + 1)"
|
||||
>
|
||||
下一课 →
|
||||
</n-button>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pager {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
margin-top: 16px;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
border-top: 1px solid rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Exercise } from "utils/types"
|
||||
|
||||
type Segment =
|
||||
export type Segment =
|
||||
{ type: "md"; content: string } | { type: "exercise"; exercise: Exercise }
|
||||
|
||||
export function parseExercises(
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
<template>
|
||||
<div class="learn-container">
|
||||
<!-- 桌面端布局 -->
|
||||
<n-grid
|
||||
:cols="5"
|
||||
:x-gap="16"
|
||||
v-if="tutorial.id && isDesktop"
|
||||
class="learn-grid"
|
||||
>
|
||||
<n-gi :span="1" class="learn-col">
|
||||
<n-card title="教程目录" :bordered="false" size="small">
|
||||
<template v-if="tutorial.id">
|
||||
<!-- 桌面端:目录 | 正文(居中限宽) | 可收起的示例代码 -->
|
||||
<div
|
||||
v-if="isDesktop"
|
||||
class="learn-layout"
|
||||
:class="{ 'with-code': codeOpen }"
|
||||
>
|
||||
<aside class="rail">
|
||||
<LearnSummary
|
||||
:titles="titles"
|
||||
:progress="progress"
|
||||
:traced="traced"
|
||||
/>
|
||||
<LessonList
|
||||
:titles="titles"
|
||||
:step="step"
|
||||
@@ -16,103 +20,60 @@
|
||||
:traced="traced"
|
||||
@select="goToLesson"
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</aside>
|
||||
|
||||
<n-gi :span="tutorial.code ? 2 : 4" class="learn-col">
|
||||
<n-card
|
||||
:title="`第 ${step} 课:${titles[step - 1]?.title}`"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
>
|
||||
<template v-for="(seg, i) in segments" :key="i">
|
||||
<MdPreview
|
||||
v-if="seg.type === 'md'"
|
||||
preview-theme="vuepress"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
:model-value="seg.content"
|
||||
/>
|
||||
<ExerciseWidget
|
||||
v-else
|
||||
:exercise="seg.exercise"
|
||||
:lang="tutorial.type"
|
||||
/>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<main class="reader">
|
||||
<article class="reader-body">
|
||||
<header class="lesson-head">
|
||||
<n-text depth="3">第 {{ step }} / {{ titles.length }} 课</n-text>
|
||||
<n-flex align="center" justify="space-between" :wrap="false">
|
||||
<span />
|
||||
<n-button
|
||||
v-if="tutorial.code"
|
||||
size="small"
|
||||
secondary
|
||||
@click="codeOpen = !codeOpen"
|
||||
>
|
||||
{{ codeOpen ? "收起示例代码" : "展开示例代码" }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</header>
|
||||
<LessonBody :segments="segments" :lang="tutorial.type" />
|
||||
</article>
|
||||
<PagerBar :step="step" :total="titles.length" @go="goToLesson" />
|
||||
</main>
|
||||
|
||||
<n-gi :span="2" v-if="tutorial.code" class="learn-col learn-col--code">
|
||||
<n-card
|
||||
title="示例代码"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
class="code-card"
|
||||
content-style="height: calc(100% - 44px); padding: 0;"
|
||||
>
|
||||
<aside v-if="tutorial.code && codeOpen" class="code-panel">
|
||||
<CodeEditor
|
||||
:language="editorLanguage"
|
||||
v-model="tutorial.code"
|
||||
height="100%"
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- 手机端布局 -->
|
||||
<template v-if="tutorial.id && !isDesktop">
|
||||
<n-tabs type="line" animated v-model:value="activeTab">
|
||||
<n-tab-pane name="catalog" tab="目录">
|
||||
<LessonList
|
||||
:titles="titles"
|
||||
:step="step"
|
||||
:progress="progress"
|
||||
:traced="traced"
|
||||
@select="goToLesson"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
|
||||
<n-tab-pane name="content" :tab="`第 ${step} 课`">
|
||||
<template v-for="(seg, i) in segments" :key="i">
|
||||
<MdPreview
|
||||
v-if="seg.type === 'md'"
|
||||
preview-theme="vuepress"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
:model-value="seg.content"
|
||||
<!-- 手机端 -->
|
||||
<template v-else>
|
||||
<LearnSummary :titles="titles" :progress="progress" :traced="traced" />
|
||||
<n-tabs type="line" animated v-model:value="activeTab">
|
||||
<n-tab-pane name="catalog" tab="目录">
|
||||
<LessonList
|
||||
:titles="titles"
|
||||
:step="step"
|
||||
:progress="progress"
|
||||
:traced="traced"
|
||||
@select="goToLesson"
|
||||
/>
|
||||
<ExerciseWidget
|
||||
v-else
|
||||
:exercise="seg.exercise"
|
||||
:lang="tutorial.type"
|
||||
/>
|
||||
</template>
|
||||
</n-tab-pane>
|
||||
|
||||
<n-tab-pane name="code" tab="示例代码" v-if="tutorial.code">
|
||||
<CodeEditor :language="editorLanguage" v-model="tutorial.code" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
|
||||
<n-divider style="margin: 12px 0" />
|
||||
|
||||
<n-flex align="center" justify="space-between">
|
||||
<n-button
|
||||
secondary
|
||||
type="primary"
|
||||
:disabled="isFirstLesson"
|
||||
@click="goToPrevLesson"
|
||||
>
|
||||
← 上一课
|
||||
</n-button>
|
||||
<n-text>{{ step }} / {{ titles.length }}</n-text>
|
||||
<n-button
|
||||
secondary
|
||||
type="primary"
|
||||
:disabled="isLastLesson"
|
||||
@click="goToNextLesson"
|
||||
>
|
||||
下一课 →
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="content" :tab="`第 ${step} 课`">
|
||||
<LessonBody :segments="segments" :lang="tutorial.type" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="code" tab="示例代码" v-if="tutorial.code">
|
||||
<CodeEditor :language="editorLanguage" v-model="tutorial.code" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
<PagerBar :step="step" :total="titles.length" @go="goToLesson" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<n-empty
|
||||
@@ -124,8 +85,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MdPreview } from "md-editor-v3"
|
||||
import "md-editor-v3/lib/preview.css"
|
||||
import type {
|
||||
Tutorial,
|
||||
Exercise,
|
||||
@@ -144,15 +103,13 @@ import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import { useLearnProgress } from "shared/composables/learnProgress"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import LessonList from "./components/LessonList.vue"
|
||||
|
||||
const ExerciseWidget = defineAsyncComponent(
|
||||
() => import("./components/ExerciseWidget.vue"),
|
||||
)
|
||||
import LearnSummary from "./components/LearnSummary.vue"
|
||||
import LessonBody from "./components/LessonBody.vue"
|
||||
import PagerBar from "./components/PagerBar.vue"
|
||||
const CodeEditor = defineAsyncComponent(
|
||||
() => import("shared/components/CodeEditor.vue"),
|
||||
)
|
||||
|
||||
const isDark = useDark()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { isDesktop } = useBreakpoints()
|
||||
@@ -186,6 +143,8 @@ const titles = ref<{ id: number; title: string }[]>([])
|
||||
const progress = ref<Record<number, TutorialProgress>>({})
|
||||
const exercises = ref<Exercise[]>([])
|
||||
const activeTab = ref("content")
|
||||
// 示例代码栏默认展开,收起后正文独占版面;偏好记在本机
|
||||
const codeOpen = useStorage("oj2:learn-code-open", true)
|
||||
const isEmpty = ref(false)
|
||||
|
||||
const segments = computed(() =>
|
||||
@@ -198,22 +157,12 @@ useLearnTrace(
|
||||
traced,
|
||||
)
|
||||
|
||||
const isFirstLesson = computed(() => step.value === 1)
|
||||
const isLastLesson = computed(() => step.value === titles.value.length)
|
||||
|
||||
function goToLesson(lessonNumber: number) {
|
||||
activeTab.value = "content"
|
||||
router.push(
|
||||
`/learn/${type.value}/${lessonNumber.toString().padStart(2, "0")}`,
|
||||
)
|
||||
}
|
||||
function goToPrevLesson() {
|
||||
if (step.value > 1) goToLesson(step.value - 1)
|
||||
}
|
||||
function goToNextLesson() {
|
||||
if (step.value < titles.value.length) goToLesson(step.value + 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉自己的自学留痕,给目录打勾。失败就当没有 —— 目录少几个勾不影响上课,
|
||||
* 但弹个错会把「我是不是没学」的焦虑塞给学生。
|
||||
@@ -263,27 +212,58 @@ watch(traced, loadProgress)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 桌面端固定高度,让目录/内容/代码三栏各自内部滚动;移动端不限高,交给页面整体滚动 */
|
||||
/* 桌面端固定高度,目录/正文/代码各自内部滚动;移动端交给页面整体滚动 */
|
||||
@media (min-width: 769px) {
|
||||
.learn-container {
|
||||
height: calc(100vh - 138px);
|
||||
}
|
||||
}
|
||||
|
||||
.learn-grid {
|
||||
.learn-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 260px minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
height: 100%;
|
||||
}
|
||||
.learn-layout.with-code {
|
||||
grid-template-columns: 240px minmax(0, 1fr) minmax(360px, 40%);
|
||||
}
|
||||
|
||||
.learn-col {
|
||||
.rail,
|
||||
.reader {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.learn-col--code {
|
||||
overflow-y: hidden;
|
||||
.reader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.reader-body {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.reader :deep(.pager) {
|
||||
max-width: 820px;
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.code-card {
|
||||
.lesson-head h1,
|
||||
.mobile-title {
|
||||
margin: 4px 0 12px;
|
||||
font-size: 26px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.mobile-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.code-panel {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user