教程的标题
This commit is contained in:
@@ -1,331 +1,323 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NButton, NCheckbox, NSelect, NTag } from "naive-ui"
|
import { NButton, NCheckbox, NSelect, NTag } from "naive-ui"
|
||||||
import { parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
import { getACMHelperList, getContest, updateACMHelperChecked } from "../api"
|
import { getACMHelperList, getContest, updateACMHelperChecked } from "../api"
|
||||||
import { getSubmission, getSubmissions } from "oj/api"
|
import { getSubmission, getSubmissions } from "oj/api"
|
||||||
import SubmissionDetail from "oj/submission/detail.vue"
|
import SubmissionDetail from "oj/submission/detail.vue"
|
||||||
import { isDesktop } from "shared/composables/breakpoints"
|
import { isDesktop } from "shared/composables/breakpoints"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
contestID: string
|
contestID: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HelperItem {
|
interface HelperItem {
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
real_name: string
|
real_name: string
|
||||||
problem_id: string
|
problem_id: string
|
||||||
problem_display_id: string
|
problem_display_id: string
|
||||||
ac_info: {
|
ac_info: {
|
||||||
is_ac: boolean
|
is_ac: boolean
|
||||||
ac_time: number
|
ac_time: number
|
||||||
error_number: number
|
error_number: number
|
||||||
checked?: boolean
|
checked?: boolean
|
||||||
}
|
}
|
||||||
checked: boolean
|
checked: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const submissions = ref<HelperItem[]>([])
|
const submissions = ref<HelperItem[]>([])
|
||||||
const contestStartTime = ref<Date | null>(null)
|
const contestStartTime = ref<Date | null>(null)
|
||||||
const query = reactive({
|
const query = reactive({
|
||||||
username: "",
|
username: "",
|
||||||
problemId: "",
|
problemId: "",
|
||||||
checked: "all",
|
checked: "all",
|
||||||
})
|
})
|
||||||
|
|
||||||
// 检查状态选项
|
// 检查状态选项
|
||||||
const checkedOptions = [
|
const checkedOptions = [
|
||||||
{ label: "全部", value: "all" },
|
{ label: "全部", value: "all" },
|
||||||
{ label: "已检查", value: "checked" },
|
{ label: "已检查", value: "checked" },
|
||||||
{ label: "未检查", value: "unchecked" },
|
{ label: "未检查", value: "unchecked" },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 代码查看模态框
|
// 代码查看模态框
|
||||||
const [codePanel, toggleCodePanel] = useToggle(false)
|
const [codePanel, toggleCodePanel] = useToggle(false)
|
||||||
const currentSubmission = ref<any>(null)
|
const currentSubmission = ref<any>(null)
|
||||||
|
|
||||||
// 格式化 AC 时间(ac_time 是相对于比赛开始的秒数)
|
// 格式化 AC 时间(ac_time 是相对于比赛开始的秒数)
|
||||||
function formatACTime(relativeSeconds: number) {
|
function formatACTime(relativeSeconds: number) {
|
||||||
if (!contestStartTime.value) return "-"
|
if (!contestStartTime.value) return "-"
|
||||||
const acTime = new Date(
|
const acTime = new Date(
|
||||||
contestStartTime.value.getTime() + relativeSeconds * 1000,
|
contestStartTime.value.getTime() + relativeSeconds * 1000,
|
||||||
)
|
)
|
||||||
return parseTime(acTime, "YYYY-MM-DD HH:mm:ss")
|
return parseTime(acTime, "YYYY-MM-DD HH:mm:ss")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换检查状态
|
// 切换检查状态
|
||||||
async function toggleChecked(item: HelperItem) {
|
async function toggleChecked(item: HelperItem) {
|
||||||
const newChecked = !item.checked
|
const newChecked = !item.checked
|
||||||
try {
|
try {
|
||||||
await updateACMHelperChecked(
|
await updateACMHelperChecked(
|
||||||
Number(props.contestID),
|
Number(props.contestID),
|
||||||
item.id,
|
item.id,
|
||||||
item.problem_id,
|
item.problem_id,
|
||||||
newChecked,
|
newChecked,
|
||||||
)
|
)
|
||||||
// 更新本地状态
|
// 更新本地状态
|
||||||
item.checked = newChecked
|
item.checked = newChecked
|
||||||
item.ac_info.checked = newChecked
|
item.ac_info.checked = newChecked
|
||||||
|
|
||||||
// 强制触发响应式更新
|
// 强制触发响应式更新
|
||||||
submissions.value = [...submissions.value]
|
submissions.value = [...submissions.value]
|
||||||
|
|
||||||
message.success(newChecked ? "已标记为已检查" : "已取消标记")
|
message.success(newChecked ? "已标记为已检查" : "已取消标记")
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
message.error(err.data || "操作失败")
|
message.error(err.data || "操作失败")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量标记为已检查
|
// 批量标记为已检查
|
||||||
async function markAllAsChecked() {
|
async function markAllAsChecked() {
|
||||||
const unchecked = filteredSubmissions.value.filter((item) => !item.checked)
|
const unchecked = filteredSubmissions.value.filter((item) => !item.checked)
|
||||||
if (unchecked.length === 0) {
|
if (unchecked.length === 0) {
|
||||||
message.info("没有需要标记的提交")
|
message.info("没有需要标记的提交")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadingMsg = message.loading("正在标记...", { duration: 0 })
|
const loadingMsg = message.loading("正在标记...", { duration: 0 })
|
||||||
try {
|
try {
|
||||||
for (const item of unchecked) {
|
for (const item of unchecked) {
|
||||||
await updateACMHelperChecked(
|
await updateACMHelperChecked(
|
||||||
Number(props.contestID),
|
Number(props.contestID),
|
||||||
item.id,
|
item.id,
|
||||||
item.problem_id,
|
item.problem_id,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
item.checked = true
|
item.checked = true
|
||||||
item.ac_info.checked = true
|
item.ac_info.checked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 强制触发响应式更新
|
// 强制触发响应式更新
|
||||||
submissions.value = [...submissions.value]
|
submissions.value = [...submissions.value]
|
||||||
|
|
||||||
loadingMsg.destroy()
|
loadingMsg.destroy()
|
||||||
message.success(`已标记 ${unchecked.length} 个提交为已检查`)
|
message.success(`已标记 ${unchecked.length} 个提交为已检查`)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
loadingMsg.destroy()
|
loadingMsg.destroy()
|
||||||
message.error(err.data || "批量操作失败")
|
message.error(err.data || "批量操作失败")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤后的提交列表
|
// 过滤后的提交列表
|
||||||
const filteredSubmissions = computed(() => {
|
const filteredSubmissions = computed(() => {
|
||||||
return submissions.value.filter((item) => {
|
return submissions.value.filter((item) => {
|
||||||
if (query.username && !item.username.includes(query.username))
|
if (query.username && !item.username.includes(query.username)) return false
|
||||||
return false
|
if (query.problemId && !item.problem_display_id.includes(query.problemId))
|
||||||
if (query.problemId && !item.problem_display_id.includes(query.problemId))
|
return false
|
||||||
return false
|
if (query.checked === "checked" && !item.checked) return false
|
||||||
if (query.checked === "checked" && !item.checked)
|
if (query.checked === "unchecked" && item.checked) return false
|
||||||
return false
|
return true
|
||||||
if (query.checked === "unchecked" && item.checked)
|
})
|
||||||
return false
|
})
|
||||||
return true
|
|
||||||
})
|
// 统计信息
|
||||||
})
|
const stats = computed(() => {
|
||||||
|
const total = submissions.value.length
|
||||||
// 统计信息
|
const checked = submissions.value.filter((item) => item.checked).length
|
||||||
const stats = computed(() => {
|
const unchecked = total - checked
|
||||||
const total = submissions.value.length
|
return { total, checked, unchecked }
|
||||||
const checked = submissions.value.filter((item) => item.checked).length
|
})
|
||||||
const unchecked = total - checked
|
|
||||||
return { total, checked, unchecked }
|
// 查看代码 - 获取该用户在该题目的 AC 提交
|
||||||
})
|
async function viewSubmission(item: HelperItem) {
|
||||||
|
try {
|
||||||
// 查看代码 - 获取该用户在该题目的 AC 提交
|
// 查询该用户在该竞赛该题目的 AC 提交
|
||||||
async function viewSubmission(item: HelperItem) {
|
const res = await getSubmissions({
|
||||||
try {
|
username: item.username,
|
||||||
// 查询该用户在该竞赛该题目的 AC 提交
|
problem_id: item.problem_display_id,
|
||||||
const res = await getSubmissions({
|
contest_id: props.contestID,
|
||||||
username: item.username,
|
result: "0", // ACCEPTED
|
||||||
problem_id: item.problem_display_id,
|
language: "",
|
||||||
contest_id: props.contestID,
|
page: 1,
|
||||||
result: "0", // ACCEPTED
|
offset: 0,
|
||||||
language: "",
|
limit: 1,
|
||||||
page: 1,
|
})
|
||||||
offset: 0,
|
|
||||||
limit: 1,
|
if (res.data.results.length === 0) {
|
||||||
})
|
message.warning("未找到该用户的 AC 提交")
|
||||||
|
return
|
||||||
if (res.data.results.length === 0) {
|
}
|
||||||
message.warning("未找到该用户的 AC 提交")
|
|
||||||
return
|
// 获取提交详情
|
||||||
}
|
const submissionListItem = res.data.results[0]
|
||||||
|
const detailRes = await getSubmission(submissionListItem.id)
|
||||||
// 获取提交详情
|
|
||||||
const submissionListItem = res.data.results[0]
|
// 手动添加 contest 字段(ACM模式下后端不返回此字段)
|
||||||
const detailRes = await getSubmission(submissionListItem.id)
|
currentSubmission.value = {
|
||||||
|
...detailRes.data,
|
||||||
// 手动添加 contest 字段(ACM模式下后端不返回此字段)
|
contest: Number(props.contestID),
|
||||||
currentSubmission.value = {
|
problem_display_id: item.problem_display_id,
|
||||||
...detailRes.data,
|
}
|
||||||
contest: Number(props.contestID),
|
|
||||||
problem_display_id: item.problem_display_id,
|
toggleCodePanel(true)
|
||||||
}
|
} catch (err: any) {
|
||||||
|
message.error(err.data || "加载提交失败")
|
||||||
toggleCodePanel(true)
|
}
|
||||||
} catch (err: any) {
|
}
|
||||||
message.error(err.data || "加载提交失败")
|
|
||||||
}
|
// 加载数据
|
||||||
}
|
async function loadData() {
|
||||||
|
try {
|
||||||
// 加载数据
|
// 先获取比赛信息,获取开始时间
|
||||||
async function loadData() {
|
const contestRes = await getContest(props.contestID)
|
||||||
try {
|
contestStartTime.value = new Date(contestRes.data.start_time)
|
||||||
// 先获取比赛信息,获取开始时间
|
|
||||||
const contestRes = await getContest(props.contestID)
|
// 再获取 AC 提交列表
|
||||||
contestStartTime.value = new Date(contestRes.data.start_time)
|
const { data } = await getACMHelperList(Number(props.contestID))
|
||||||
|
submissions.value = data
|
||||||
// 再获取 AC 提交列表
|
} catch (err: any) {
|
||||||
const { data } = await getACMHelperList(Number(props.contestID))
|
message.error(err.data || "加载失败")
|
||||||
submissions.value = data
|
}
|
||||||
} catch (err: any) {
|
}
|
||||||
message.error(err.data || "加载失败")
|
|
||||||
}
|
const columns: DataTableColumn<HelperItem>[] = [
|
||||||
}
|
{
|
||||||
|
title: "用户名",
|
||||||
const columns: DataTableColumn<HelperItem>[] = [
|
key: "username",
|
||||||
{
|
width: 150,
|
||||||
title: "用户名",
|
},
|
||||||
key: "username",
|
{
|
||||||
width: 150,
|
title: "题目",
|
||||||
},
|
key: "problem_display_id",
|
||||||
{
|
width: 100,
|
||||||
title: "题目",
|
render: (row) => h(NTag, { type: "info" }, () => row.problem_display_id),
|
||||||
key: "problem_display_id",
|
},
|
||||||
width: 100,
|
{
|
||||||
render: (row) => h(NTag, { type: "info" }, () => row.problem_display_id),
|
title: "AC时间",
|
||||||
},
|
key: "ac_time",
|
||||||
{
|
width: 180,
|
||||||
title: "AC时间",
|
render: (row) => formatACTime(row.ac_info.ac_time),
|
||||||
key: "ac_time",
|
},
|
||||||
width: 180,
|
{
|
||||||
render: (row) => formatACTime(row.ac_info.ac_time),
|
title: "错误次数",
|
||||||
},
|
key: "error_number",
|
||||||
{
|
width: 100,
|
||||||
title: "错误次数",
|
render: (row) =>
|
||||||
key: "error_number",
|
h(
|
||||||
width: 100,
|
NTag,
|
||||||
render: (row) =>
|
{
|
||||||
h(
|
type: row.ac_info.error_number > 0 ? "warning" : "success",
|
||||||
NTag,
|
size: "small",
|
||||||
{
|
},
|
||||||
type: row.ac_info.error_number > 0 ? "warning" : "success",
|
() => row.ac_info.error_number,
|
||||||
size: "small",
|
),
|
||||||
},
|
},
|
||||||
() => row.ac_info.error_number,
|
{
|
||||||
),
|
title: "已检查",
|
||||||
},
|
key: "checked",
|
||||||
{
|
width: 100,
|
||||||
title: "已检查",
|
render: (row) =>
|
||||||
key: "checked",
|
h(NCheckbox, {
|
||||||
width: 100,
|
checked: row.checked,
|
||||||
render: (row) =>
|
onUpdateChecked: () => toggleChecked(row),
|
||||||
h(NCheckbox, {
|
}),
|
||||||
checked: row.checked,
|
},
|
||||||
onUpdateChecked: () => toggleChecked(row),
|
{
|
||||||
}),
|
title: "操作",
|
||||||
},
|
key: "actions",
|
||||||
{
|
width: 100,
|
||||||
title: "操作",
|
render: (row) =>
|
||||||
key: "actions",
|
h(
|
||||||
width: 100,
|
NButton,
|
||||||
render: (row) =>
|
{
|
||||||
h(
|
size: "small",
|
||||||
NButton,
|
type: "primary",
|
||||||
{
|
secondary: true,
|
||||||
size: "small",
|
onClick: () => viewSubmission(row),
|
||||||
type: "primary",
|
},
|
||||||
secondary: true,
|
() => "查看代码",
|
||||||
onClick: () => viewSubmission(row),
|
),
|
||||||
},
|
},
|
||||||
() => "查看代码",
|
]
|
||||||
),
|
|
||||||
},
|
onMounted(loadData)
|
||||||
]
|
</script>
|
||||||
|
|
||||||
onMounted(loadData)
|
<template>
|
||||||
</script>
|
<n-flex vertical>
|
||||||
|
<n-flex justify="space-between" align="center">
|
||||||
<template>
|
<n-flex align="center">
|
||||||
<n-flex vertical>
|
<h2 style="margin: 0">比赛辅助检查</h2>
|
||||||
<n-flex justify="space-between" align="center">
|
<n-tag type="info" size="large"> 总计: {{ stats.total }} </n-tag>
|
||||||
<n-flex align="center">
|
<n-tag type="success" size="large"> 已检查: {{ stats.checked }} </n-tag>
|
||||||
<h2 style="margin: 0">比赛辅助检查</h2>
|
<n-tag type="warning" size="large">
|
||||||
<n-tag type="info" size="large">
|
未检查: {{ stats.unchecked }}
|
||||||
总计: {{ stats.total }}
|
</n-tag>
|
||||||
</n-tag>
|
</n-flex>
|
||||||
<n-tag type="success" size="large">
|
<n-button
|
||||||
已检查: {{ stats.checked }}
|
type="primary"
|
||||||
</n-tag>
|
:disabled="stats.unchecked === 0"
|
||||||
<n-tag type="warning" size="large">
|
@click="markAllAsChecked"
|
||||||
未检查: {{ stats.unchecked }}
|
>
|
||||||
</n-tag>
|
标记全部为已检查
|
||||||
</n-flex>
|
</n-button>
|
||||||
<n-button
|
</n-flex>
|
||||||
type="primary"
|
|
||||||
:disabled="stats.unchecked === 0"
|
<n-alert type="info" style="margin-bottom: 16px">
|
||||||
@click="markAllAsChecked"
|
<template #header>使用说明</template>
|
||||||
>
|
此工具用于赛后人工审核代码,检查是否存在抄袭、作弊等行为。请逐个查看通过(AC)的提交代码,检查完成后勾选"已检查"。
|
||||||
标记全部为已检查
|
</n-alert>
|
||||||
</n-button>
|
|
||||||
</n-flex>
|
<n-flex align="center" style="margin-bottom: 16px">
|
||||||
|
<n-input
|
||||||
<n-alert type="info" style="margin-bottom: 16px">
|
v-model:value="query.username"
|
||||||
<template #header>使用说明</template>
|
placeholder="筛选用户名"
|
||||||
此工具用于赛后人工审核代码,检查是否存在抄袭、作弊等行为。请逐个查看通过(AC)的提交代码,检查完成后勾选"已检查"。
|
style="width: 150px"
|
||||||
</n-alert>
|
clearable
|
||||||
|
/>
|
||||||
<n-flex align="center" style="margin-bottom: 16px">
|
<n-input
|
||||||
<n-input
|
v-model:value="query.problemId"
|
||||||
v-model:value="query.username"
|
placeholder="筛选题目"
|
||||||
placeholder="筛选用户名"
|
style="width: 150px"
|
||||||
style="width: 150px"
|
clearable
|
||||||
clearable
|
/>
|
||||||
/>
|
<n-select
|
||||||
<n-input
|
v-model:value="query.checked"
|
||||||
v-model:value="query.problemId"
|
:options="checkedOptions"
|
||||||
placeholder="筛选题目"
|
style="width: 120px"
|
||||||
style="width: 150px"
|
/>
|
||||||
clearable
|
</n-flex>
|
||||||
/>
|
|
||||||
<n-select
|
<n-data-table
|
||||||
v-model:value="query.checked"
|
:columns="columns"
|
||||||
:options="checkedOptions"
|
:data="filteredSubmissions"
|
||||||
style="width: 120px"
|
:pagination="{ pageSize: 20 }"
|
||||||
/>
|
:bordered="false"
|
||||||
</n-flex>
|
/>
|
||||||
|
|
||||||
<n-data-table
|
<n-modal
|
||||||
:columns="columns"
|
v-model:show="codePanel"
|
||||||
:data="filteredSubmissions"
|
preset="card"
|
||||||
:pagination="{ pageSize: 20 }"
|
:style="{ maxWidth: isDesktop && '70vw', maxHeight: '80vh' }"
|
||||||
:bordered="false"
|
:content-style="{ overflow: 'auto' }"
|
||||||
/>
|
title="代码详情"
|
||||||
|
>
|
||||||
<n-modal
|
<SubmissionDetail
|
||||||
v-model:show="codePanel"
|
v-if="currentSubmission"
|
||||||
preset="card"
|
:submission="currentSubmission"
|
||||||
:style="{ maxWidth: isDesktop && '70vw', maxHeight: '80vh' }"
|
:problemID="currentSubmission.problem_display_id"
|
||||||
:content-style="{ overflow: 'auto' }"
|
:submissionID="currentSubmission.id"
|
||||||
title="代码详情"
|
hideList
|
||||||
>
|
/>
|
||||||
<SubmissionDetail
|
</n-modal>
|
||||||
v-if="currentSubmission"
|
</n-flex>
|
||||||
:submission="currentSubmission"
|
</template>
|
||||||
:problemID="currentSubmission.problem_display_id"
|
|
||||||
:submissionID="currentSubmission.id"
|
<style scoped>
|
||||||
hideList
|
:deep(.n-data-table) {
|
||||||
/>
|
margin-top: 16px;
|
||||||
</n-modal>
|
}
|
||||||
</n-flex>
|
</style>
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
:deep(.n-data-table) {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-grid :cols="2" :x-gap="24" v-if="!!tutorial.id">
|
<n-grid :cols="5" :x-gap="16" v-if="tutorial.id">
|
||||||
<n-gi :span="1">
|
<n-gi :span="1">
|
||||||
<n-flex vertical>
|
<n-card title="目录" :bordered="false" size="small">
|
||||||
<n-flex align="center">
|
<n-scrollbar :style="{ maxHeight: 'calc(100vh - 180px)' }">
|
||||||
<n-button text :disabled="step == 1" @click="prev">
|
<n-list hoverable clickable>
|
||||||
<Icon :width="30" icon="pepicons-pencil:arrow-left"></Icon>
|
<n-list-item
|
||||||
</n-button>
|
v-for="(item, index) in titles"
|
||||||
<n-dropdown size="large" :options="menu" trigger="click">
|
:key="item.id"
|
||||||
<n-button tertiary style="flex: 1" size="large">
|
@click="goToLesson(index + 1)"
|
||||||
<n-flex align="center">
|
>
|
||||||
<span style="font-weight: bold">
|
<n-text
|
||||||
{{ title }}
|
:type="step === index + 1 ? 'primary' : undefined"
|
||||||
</span>
|
:strong="step === index + 1"
|
||||||
<Icon :width="24" icon="proicons:chevron-down"></Icon>
|
|
||||||
</n-flex>
|
|
||||||
</n-button>
|
|
||||||
</n-dropdown>
|
|
||||||
<n-button text :disabled="step == titles.length" @click="next">
|
|
||||||
<Icon :width="30" icon="pepicons-pencil:arrow-right"></Icon>
|
|
||||||
</n-button>
|
|
||||||
</n-flex>
|
|
||||||
<n-flex vertical size="large">
|
|
||||||
<MdPreview
|
|
||||||
preview-theme="vuepress"
|
|
||||||
:theme="isDark ? 'dark' : 'light'"
|
|
||||||
:model-value="tutorial.content"
|
|
||||||
/>
|
|
||||||
<n-flex justify="space-between">
|
|
||||||
<div style="flex: 1">
|
|
||||||
<n-button
|
|
||||||
block
|
|
||||||
style="height: 40px"
|
|
||||||
v-if="step !== 1"
|
|
||||||
@click="prev"
|
|
||||||
>
|
>
|
||||||
上一课时
|
{{ index + 1 }}. {{ item.title }}
|
||||||
</n-button>
|
</n-text>
|
||||||
</div>
|
</n-list-item>
|
||||||
<div style="flex: 1">
|
</n-list>
|
||||||
<n-button
|
</n-scrollbar>
|
||||||
block
|
</n-card>
|
||||||
style="height: 40px"
|
|
||||||
v-if="step !== titles.length"
|
|
||||||
@click="next"
|
|
||||||
>
|
|
||||||
下一课时
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</n-flex>
|
|
||||||
</n-flex>
|
|
||||||
</n-flex>
|
|
||||||
</n-gi>
|
</n-gi>
|
||||||
<n-gi :span="1">
|
|
||||||
<n-flex vertical>
|
<n-gi :span="tutorial.code ? 2 : 4">
|
||||||
<CodeEditor
|
<n-card
|
||||||
v-show="!!tutorial.code"
|
:title="`第 ${step} 课:${titles[step - 1]?.title}`"
|
||||||
language="Python3"
|
:bordered="false"
|
||||||
v-model="tutorial.code"
|
size="small"
|
||||||
|
>
|
||||||
|
<MdPreview
|
||||||
|
preview-theme="vuepress"
|
||||||
|
:theme="isDark ? 'dark' : 'light'"
|
||||||
|
:model-value="tutorial.content"
|
||||||
/>
|
/>
|
||||||
</n-flex>
|
</n-card>
|
||||||
|
</n-gi>
|
||||||
|
|
||||||
|
<n-gi :span="2" v-if="tutorial.code">
|
||||||
|
<n-card title="示例代码" :bordered="false" size="small">
|
||||||
|
<CodeEditor language="Python3" v-model="tutorial.code" />
|
||||||
|
</n-card>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from "@iconify/vue"
|
|
||||||
|
|
||||||
import { MdPreview } from "md-editor-v3"
|
import { MdPreview } from "md-editor-v3"
|
||||||
import "md-editor-v3/lib/preview.css"
|
import "md-editor-v3/lib/preview.css"
|
||||||
import { Tutorial } from "utils/types"
|
import { Tutorial } from "utils/types"
|
||||||
@@ -94,23 +72,11 @@ const tutorial = ref<Partial<Tutorial>>({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const titles = ref<{ id: number; title: string }[]>([])
|
const titles = ref<{ id: number; title: string }[]>([])
|
||||||
const title = computed(
|
|
||||||
() => `第 ${step.value} 课:${titles.value[step.value - 1].title}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
const menu = computed<DropdownOption[]>(() => {
|
function goToLesson(lessonNumber: number) {
|
||||||
return titles.value.map((item, index) => {
|
const dest = lessonNumber.toString().padStart(2, "0")
|
||||||
const id = (index + 1).toString().padStart(2, "0")
|
router.push("/learn/" + dest)
|
||||||
const prefix = `第 ${index + 1} 课:`
|
}
|
||||||
return {
|
|
||||||
key: id,
|
|
||||||
label: prefix + item.title,
|
|
||||||
props: {
|
|
||||||
onClick: () => router.push(`/learn/${id}`),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const res1 = await getTutorials()
|
const res1 = await getTutorials()
|
||||||
@@ -121,18 +87,6 @@ async function init() {
|
|||||||
tutorial.value = res2.data
|
tutorial.value = res2.data
|
||||||
}
|
}
|
||||||
|
|
||||||
function next() {
|
|
||||||
if (step.value === titles.value.length) return
|
|
||||||
const dest = (step.value + 1).toString().padStart(2, "0")
|
|
||||||
router.push("/learn/" + dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
function prev() {
|
|
||||||
if (step.value === 1) return
|
|
||||||
const dest = (step.value - 1).toString().padStart(2, "0")
|
|
||||||
router.push("/learn/" + dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.params.step,
|
() => route.params.step,
|
||||||
async () => {
|
async () => {
|
||||||
@@ -143,8 +97,8 @@ watch(
|
|||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
.md-editor-preview .md-editor-code .md-editor-code-head {
|
:deep(.md-editor-preview .md-editor-code .md-editor-code-head) {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -86,9 +86,7 @@ const languageOptions: DropdownOption[] = problem.value!.languages.map(
|
|||||||
:options="languageOptions"
|
:options="languageOptions"
|
||||||
@update:value="changeLanguage"
|
@update:value="changeLanguage"
|
||||||
/>
|
/>
|
||||||
<n-button @click="copy">
|
<n-button @click="copy">复制代码</n-button>
|
||||||
复制代码
|
|
||||||
</n-button>
|
|
||||||
<n-button @click="reset">重置代码</n-button>
|
<n-button @click="reset">重置代码</n-button>
|
||||||
<n-button type="primary" secondary @click="runCode">
|
<n-button type="primary" secondary @click="runCode">
|
||||||
运行代码
|
运行代码
|
||||||
|
|||||||
@@ -204,116 +204,138 @@ function type(status: ProblemStatus) {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
.problemContent .problemTitle {
|
.problemContent {
|
||||||
|
--border-color-light: rgb(239, 239, 245);
|
||||||
|
--bg-code-light: rgb(250, 250, 252);
|
||||||
|
--bg-code-dark: rgb(24, 24, 28);
|
||||||
|
--bg-table-light: rgba(250, 250, 252, 1);
|
||||||
|
--bg-table-dark: rgba(38, 38, 42, 1);
|
||||||
|
--border-color-dark: rgba(255, 255, 255, 0.09);
|
||||||
|
--blockquote-color: #7b7b7b;
|
||||||
|
--blockquote-border: #bbbec4;
|
||||||
|
--link-color: #18a058;
|
||||||
|
--transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.problemTitle {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemContent .title {
|
.title {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
margin: 12px 0;
|
margin: 12px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemContent .content {
|
.testcase {
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: pre;
|
||||||
|
font-family: "Monaco", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-alert {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 2;
|
line-height: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemContent .testcase {
|
/* 针对 v-html 渲染内容的深度样式 */
|
||||||
font-size: 14px;
|
.content :deep(p) {
|
||||||
white-space: pre;
|
|
||||||
font-family: "Monaco";
|
|
||||||
}
|
|
||||||
|
|
||||||
.problemContent .status-alert {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.problemContent .content > p {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemContent .content > blockquote {
|
.content :deep(blockquote) {
|
||||||
border-left: 3px solid #bbbec4;
|
border-left: 3px solid var(--blockquote-border);
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
color: #7b7b7b;
|
color: var(--blockquote-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemContent .content > pre {
|
.content :deep(pre) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: rgb(250, 250, 252);
|
background-color: var(--bg-code-light);
|
||||||
border: 1px solid rgb(239, 239, 245);
|
border: 1px solid var(--border-color-light);
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition:
|
transition:
|
||||||
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
background-color var(--transition),
|
||||||
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
border-color var(--transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemContent .content > pre > code {
|
.content :deep(pre code) {
|
||||||
font-family: "Monaco";
|
font-family: "Monaco", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .problemContent .content > pre {
|
.dark .content :deep(pre) {
|
||||||
background-color: rgb(24, 24, 28);
|
background-color: var(--bg-code-dark);
|
||||||
border-color: rgba(255, 255, 255, 0.09);
|
border-color: var(--border-color-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemContent .content > table {
|
.content :deep(table) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
border: 1px solid rgba(239, 239, 245, 1);
|
border: 1px solid var(--border-color-light);
|
||||||
transition:
|
transition:
|
||||||
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
background-color var(--transition),
|
||||||
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
border-color var(--transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemContent .content > table th {
|
.content :deep(table th) {
|
||||||
background-color: rgba(250, 250, 252, 1);
|
background-color: var(--bg-table-light);
|
||||||
|
padding: 8px;
|
||||||
transition:
|
transition:
|
||||||
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
background-color var(--transition),
|
||||||
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
border-color var(--transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .problemContent .content > table th {
|
.dark .content :deep(table th) {
|
||||||
background-color: rgba(38, 38, 42, 1);
|
background-color: var(--bg-table-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemContent .content > table td {
|
.content :deep(table td) {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemContent .content > table td,
|
.content :deep(table td),
|
||||||
.problemContent .content > table th {
|
.content :deep(table th) {
|
||||||
border-right: 1px solid rgba(239, 239, 245, 1);
|
border-right: 1px solid var(--border-color-light);
|
||||||
border-bottom: 1px solid rgba(239, 239, 245, 1);
|
border-bottom: 1px solid var(--border-color-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemContent .content > table th:last-child,
|
.content :deep(table th:last-child),
|
||||||
.problemContent .content > table td:last-child {
|
.content :deep(table td:last-child) {
|
||||||
border-right: 0px solid rgba(239, 239, 245, 1);
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemContent .content > table tr:last-child td {
|
.content :deep(table tr:last-child td) {
|
||||||
border-bottom: 0px solid rgba(239, 239, 245, 1);
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemContent .content > p > code {
|
.content :deep(p code) {
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
padding: 2px 5px;
|
padding: 2px 5px;
|
||||||
margin: 0px 4px;
|
margin: 0 4px;
|
||||||
background-color: rgba(27, 31, 35, 0.05);
|
background-color: rgba(27, 31, 35, 0.05);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
font-family: "Monaco", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemContent .content img {
|
.content :deep(img) {
|
||||||
max-width: 100% !important;
|
max-width: 100%;
|
||||||
height: 100% !important;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemContent .content a {
|
.content :deep(a) {
|
||||||
color: #18a058;
|
color: var(--link-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: opacity var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :deep(a:hover) {
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const useConfigStore = defineStore("config", () => {
|
|||||||
website_name_shortcut: "",
|
website_name_shortcut: "",
|
||||||
website_footer: "",
|
website_footer: "",
|
||||||
submission_list_show_all: true,
|
submission_list_show_all: true,
|
||||||
allow_register: true,
|
allow_register: false,
|
||||||
class_list: [],
|
class_list: [],
|
||||||
})
|
})
|
||||||
async function getConfig() {
|
async function getConfig() {
|
||||||
|
|||||||
Reference in New Issue
Block a user