add cache for admin problem detail

This commit is contained in:
2024-06-11 23:46:29 +08:00
parent 89114a9296
commit 73db76e605
5 changed files with 126 additions and 98 deletions

View File

@@ -3,7 +3,11 @@ import TextEditor from "~/shared/components/TextEditor.vue"
import { unique } from "~/utils/functions" import { unique } from "~/utils/functions"
import { BlankProblem, LANGUAGE, Tag } from "~/utils/types" import { BlankProblem, LANGUAGE, Tag } from "~/utils/types"
import { getProblemTagList } from "~/shared/api" import { getProblemTagList } from "~/shared/api"
import { LANGUAGE_SHOW_VALUE, CODE_TEMPLATES } from "~/utils/constants" import {
LANGUAGE_SHOW_VALUE,
CODE_TEMPLATES,
STORAGE_KEY,
} from "~/utils/constants"
import download from "~/utils/download" import download from "~/utils/download"
import { import {
createContestProblem, createContestProblem,
@@ -37,7 +41,7 @@ const title = computed(
"admin contest problem edit": "编辑比赛题目", "admin contest problem edit": "编辑比赛题目",
})[<string>route.name], })[<string>route.name],
) )
const problem = reactive<BlankProblem>({ const problem = useLocalStorage<BlankProblem>(STORAGE_KEY.ADMIN_PROBLEM, {
_id: "", _id: "",
title: "", title: "",
description: "", description: "",
@@ -75,9 +79,19 @@ const problem = reactive<BlankProblem>({
const template = reactive(JSON.parse(JSON.stringify(CODE_TEMPLATES))) const template = reactive(JSON.parse(JSON.stringify(CODE_TEMPLATES)))
const currentActiveTemplate = ref<LANGUAGE>("C") const currentActiveTemplate = ref<LANGUAGE>("C")
const existingTags = shallowRef<Tag[]>([]) // 从服务器来的tag列表
const fromExistingTags = shallowRef<string[]>([]) const tagList = shallowRef<Tag[]>([])
const newTags = shallowRef<string[]>([])
interface Tags {
select: string[]
upload: string[]
}
// 选择的 和 新的
const tags = useLocalStorage<Tags>(STORAGE_KEY.ADMIN_PROBLEM_TAGS, {
select: [],
upload: [],
})
const [needTemplate, toggleNeedTemplate] = useToggle(false) const [needTemplate, toggleNeedTemplate] = useToggle(false)
const [ready, toggleReady] = useToggle(false) const [ready, toggleReady] = useToggle(false)
@@ -93,7 +107,7 @@ const languageOptions = [
] ]
const tagOptions = computed(() => const tagOptions = computed(() =>
existingTags.value.map((tag) => ({ label: tag.name, value: tag.name })), tagList.value.map((tag) => ({ label: tag.name, value: tag.name })),
) )
async function getProblemDetail() { async function getProblemDetail() {
@@ -103,76 +117,76 @@ async function getProblemDetail() {
} }
const { data } = await getProblem(props.problemID) const { data } = await getProblem(props.problemID)
toggleReady(true) toggleReady(true)
problem.id = data.id problem.value.id = data.id
problem._id = data._id problem.value._id = data._id
problem.title = data.title problem.value.title = data.title
problem.description = data.description problem.value.description = data.description
problem.input_description = data.input_description problem.value.input_description = data.input_description
problem.output_description = data.output_description problem.value.output_description = data.output_description
problem.time_limit = data.time_limit problem.value.time_limit = data.time_limit
problem.memory_limit = data.memory_limit problem.value.memory_limit = data.memory_limit
problem.memory_limit = data.memory_limit problem.value.memory_limit = data.memory_limit
problem.difficulty = data.difficulty problem.value.difficulty = data.difficulty
problem.visible = data.visible problem.value.visible = data.visible
problem.share_submission = data.share_submission problem.value.share_submission = data.share_submission
problem.tags = data.tags problem.value.tags = data.tags
problem.languages = data.languages problem.value.languages = data.languages
problem.template = data.template problem.value.template = data.template
problem.samples = data.samples problem.value.samples = data.samples
problem.samples = data.samples problem.value.samples = data.samples
problem.spj = data.spj problem.value.spj = data.spj
problem.spj_language = data.spj_language problem.value.spj_language = data.spj_language
problem.spj_code = data.spj_code problem.value.spj_code = data.spj_code
problem.spj_compile_ok = data.spj_compile_ok problem.value.spj_compile_ok = data.spj_compile_ok
problem.test_case_id = data.test_case_id problem.value.test_case_id = data.test_case_id
problem.test_case_score = data.test_case_score problem.value.test_case_score = data.test_case_score
problem.rule_type = data.rule_type problem.value.rule_type = data.rule_type
problem.hint = data.hint problem.value.hint = data.hint
problem.source = data.source problem.value.source = data.source
problem.io_mode = data.io_mode problem.value.io_mode = data.io_mode
if (problem.contest_id) { if (problem.value.contest_id) {
problem.contest_id = problem.contest_id problem.value.contest_id = problem.value.contest_id
} }
// 下面是用来显示的: // 下面是用来显示的:
// 代码模板 和 模板开关 // 代码模板 和 模板开关
problem.languages.forEach((lang) => { problem.value.languages.forEach((lang) => {
if (data.template[lang]) { if (data.template[lang]) {
template[lang] = data.template[lang] template[lang] = data.template[lang]
toggleNeedTemplate(true) toggleNeedTemplate(true)
} }
}) })
// 标签 // 标签
fromExistingTags.value = data.tags tags.value.select = data.tags
} }
async function listTags() { async function getTagList() {
const res = await getProblemTagList() const res = await getProblemTagList()
existingTags.value = res.data tagList.value = res.data
} }
function updateNewTags(v: string[]) { function updateNewTags(v: string[]) {
const blanks = [] const blanks = []
const uniqueTags = unique(v) const uniqueTags = unique(v)
const tags = existingTags.value.map((t) => t.name) const items = tagList.value.map((t) => t.name)
for (let i = 0; i < uniqueTags.length; i++) { for (let i = 0; i < uniqueTags.length; i++) {
const tag = uniqueTags[i] const tag = uniqueTags[i]
if (tags.indexOf(tag) < 0) { if (items.indexOf(tag) < 0) {
blanks.push(tag) blanks.push(tag)
} else { } else {
message.error("已经存在标签:" + tag) message.error("已经存在标签:" + tag)
break break
} }
} }
newTags.value = blanks tags.value.upload = blanks
} }
function addSample() { function addSample() {
problem.samples.push({ input: "", output: "" }) problem.value.samples.push({ input: "", output: "" })
} }
function removeSample(index: number) { function removeSample(index: number) {
problem.samples.splice(index, 1) problem.value.samples.splice(index, 1)
} }
function resetTemplate(language: LANGUAGE) { function resetTemplate(language: LANGUAGE) {
@@ -182,7 +196,7 @@ function resetTemplate(language: LANGUAGE) {
async function handleUploadTestcases({ file }: UploadCustomRequestOptions) { async function handleUploadTestcases({ file }: UploadCustomRequestOptions) {
try { try {
const res = await uploadTestcases(file.file!) const res = await uploadTestcases(file.file!)
// @ts-ignore: // @ts-ignore
if (res.error) { if (res.error) {
message.error("上传测试用例失败") message.error("上传测试用例失败")
return return
@@ -190,51 +204,51 @@ async function handleUploadTestcases({ file }: UploadCustomRequestOptions) {
const testcases = res.data.info const testcases = res.data.info
for (let file of testcases) { for (let file of testcases) {
file.score = (100 / testcases.length).toFixed(0) file.score = (100 / testcases.length).toFixed(0)
if (!file.output_name && problem.spj) { if (!file.output_name && problem.value.spj) {
file.output_name = "-" file.output_name = "-"
} }
} }
problem.test_case_score = testcases problem.value.test_case_score = testcases
problem.test_case_id = res.data.id problem.value.test_case_id = res.data.id
} catch (err) { } catch (err) {
message.error("上传测试用例失败") message.error("上传测试用例失败")
} }
} }
function downloadTestcases() { function downloadTestcases() {
download("test_case?problem_id=" + problem.id) download("test_case?problem_id=" + problem.value.id)
} }
// 题目是否有漏写的 // 题目是否有漏写的
function detectProblemCompletion() { function detectProblemCompletion() {
let flag = false let flag = false
// 标题 // 标题
if (!problem._id || !problem.title) { if (!problem.value._id || !problem.value.title) {
message.error("编号或标题没有填写") message.error("编号或标题没有填写")
flag = true flag = true
} }
// 标签 // 标签
else if (newTags.value.length === 0 && fromExistingTags.value.length === 0) { else if (tags.value.upload.length === 0 && tags.value.select.length === 0) {
message.error("标签没有填写") message.error("标签没有填写")
flag = true flag = true
} }
// 题目 // 题目
else if ( else if (
!problem.description || !problem.value.description ||
!problem.input_description || !problem.value.input_description ||
!problem.output_description !problem.value.output_description
) { ) {
message.error("题目或输入或输出没有填写") message.error("题目或输入或输出没有填写")
flag = true flag = true
} }
// 样例 // 样例
else if (problem.samples.length == 0) { else if (problem.value.samples.length == 0) {
message.error("样例没有填写") message.error("样例没有填写")
flag = true flag = true
} }
// 样例是空的 // 样例是空的
else if ( else if (
problem.samples.some( problem.value.samples.some(
(sample) => sample.output === "" || sample.input === "", (sample) => sample.output === "" || sample.input === "",
) )
) { ) {
@@ -242,10 +256,10 @@ function detectProblemCompletion() {
flag = true flag = true
} }
// 测试用例 // 测试用例
else if (problem.test_case_score.length === 0) { else if (problem.value.test_case_score.length === 0) {
message.error("测试用例没有上传") message.error("测试用例没有上传")
flag = true flag = true
} else if (problem.languages.length === 0) { } else if (problem.value.languages.length === 0) {
message.error("编程语言没有选择") message.error("编程语言没有选择")
flag = true flag = true
} }
@@ -258,13 +272,13 @@ function detectProblemCompletion() {
function getTemplate() { function getTemplate() {
if (!needTemplate.value) { if (!needTemplate.value) {
problem.template = {} problem.value.template = {}
} else { } else {
problem.languages.forEach((lang) => { problem.value.languages.forEach((lang) => {
if (CODE_TEMPLATES[lang] !== template[lang]) { if (CODE_TEMPLATES[lang] !== template[lang]) {
problem.template[lang] = template[lang] problem.value.template[lang] = template[lang]
} else { } else {
delete problem.template[lang] delete problem.value.template[lang]
} }
}) })
} }
@@ -272,8 +286,8 @@ function getTemplate() {
function filterHint() { function filterHint() {
// 编辑器会自动添加一段 HTML // 编辑器会自动添加一段 HTML
if (problem.hint === "<p><br></p>") { if (problem.value.hint === "<p><br></p>") {
problem.hint = "" problem.value.hint = ""
} }
} }
@@ -282,7 +296,6 @@ async function submit() {
if (notCompleted) return if (notCompleted) return
filterHint() filterHint()
getTemplate() getTemplate()
problem.tags = [...newTags.value, ...fromExistingTags.value]
const api = { const api = {
"admin problem create": createProblem, "admin problem create": createProblem,
"admin problem edit": editProblem, "admin problem edit": editProblem,
@@ -293,10 +306,12 @@ async function submit() {
route.name === "admin contest problem create" || route.name === "admin contest problem create" ||
route.name === "admin contest problem edit" route.name === "admin contest problem edit"
) { ) {
problem.contest_id = props.contestID problem.value.contest_id = props.contestID
} }
try { try {
await api!(problem) await api!(problem.value)
problem.value = null
tags.value = null
if ( if (
route.name === "admin problem create" || route.name === "admin problem create" ||
route.name === "admin contest problem create" route.name === "admin contest problem create"
@@ -323,19 +338,38 @@ async function submit() {
} }
} }
const showClear = computed(
() =>
route.name === "admin problem create" ||
route.name === "admin contest problem create",
)
function clear() {
problem.value = null
tags.value = null
// TODO: 这里是 TextEditor 不更新,所以刷一下页面
location.reload()
}
onMounted(() => { onMounted(() => {
listTags() getTagList()
getProblemDetail() getProblemDetail()
}) })
watch([fromExistingTags, newTags], (tags) => { watch(
() => [tags.value.select, tags.value.upload],
(tags) => {
const uniqueTags = unique<string>(tags[0].concat(tags[1])) const uniqueTags = unique<string>(tags[0].concat(tags[1]))
problem.tags = uniqueTags problem.value.tags = uniqueTags
}) },
)
</script> </script>
<template> <template>
<n-flex>
<h2 class="title">{{ title }}</h2> <h2 class="title">{{ title }}</h2>
<n-button v-if="showClear" @click="clear">清空缓存</n-button>
</n-flex>
<n-form inline label-placement="left"> <n-form inline label-placement="left">
<n-form-item label="显示编号"> <n-form-item label="显示编号">
<n-input class="w-100" v-model:value="problem._id" /> <n-input class="w-100" v-model:value="problem._id" />
@@ -359,12 +393,15 @@ watch([fromExistingTags, newTags], (tags) => {
<n-select <n-select
class="tag" class="tag"
multiple multiple
v-model:value="fromExistingTags" v-model:value="tags.select"
:options="tagOptions" :options="tagOptions"
/> />
</n-form-item> </n-form-item>
<n-form-item label="新增的标签"> <n-form-item label="新增的标签">
<n-dynamic-tags v-model:value="newTags" @update:value="updateNewTags" /> <n-dynamic-tags
v-model:value="tags.upload"
@update:value="updateNewTags"
/>
</n-form-item> </n-form-item>
</n-form> </n-form>
<TextEditor <TextEditor
@@ -436,7 +473,7 @@ watch([fromExistingTags, newTags], (tags) => {
:name="lang" :name="lang"
> >
<CodeEditor <CodeEditor
v-model="template[lang]" v-model:value="template[lang]"
:language="lang" :language="lang"
:font-size="16" :font-size="16"
height="200px" height="200px"
@@ -503,7 +540,13 @@ watch([fromExistingTags, newTags], (tags) => {
</n-button> </n-button>
</n-form-item> </n-form-item>
</n-form> </n-form>
<n-space> <n-space align="center">
<n-tooltip placement="left">
<template #trigger>
<n-button text>温馨提醒</n-button>
</template>
测试用例最好要有10个要考虑边界情况不要跟测试样例一模一样
</n-tooltip>
<n-upload <n-upload
:show-file-list="false" :show-file-list="false"
accept=".zip" accept=".zip"
@@ -511,7 +554,7 @@ watch([fromExistingTags, newTags], (tags) => {
> >
<n-button type="info">上传测试用例</n-button> <n-button type="info">上传测试用例</n-button>
</n-upload> </n-upload>
<n-button type="primary" @click="submit">保存</n-button> <n-button type="primary" @click="submit">提交</n-button>
</n-space> </n-space>
</n-space> </n-space>
</template> </template>

View File

@@ -20,7 +20,6 @@ const styleTheme = EditorView.baseTheme({
}) })
interface Props { interface Props {
modelValue: string
language?: LANGUAGE language?: LANGUAGE
fontSize?: number fontSize?: number
height?: string height?: string
@@ -36,28 +35,16 @@ const props = withDefaults(defineProps<Props>(), {
placeholder: "", placeholder: "",
}) })
const code = ref(props.modelValue) const code = defineModel<string>("value")
const isDark = useDark() const isDark = useDark()
watch(
() => props.modelValue,
(v) => {
code.value = v
},
)
const emit = defineEmits(["update:modelValue"])
const lang = computed(() => { const lang = computed(() => {
if (props.language === "Python3" || props.language === "Python2") { if (props.language === "Python3" || props.language === "Python2") {
return python() return python()
} }
return cpp() return cpp()
}) })
function onChange(v: string) {
emit("update:modelValue", v)
}
</script> </script>
<template> <template>
<Codemirror <Codemirror
@@ -68,6 +55,5 @@ function onChange(v: string) {
:tabSize="4" :tabSize="4"
:placeholder="props.placeholder" :placeholder="props.placeholder"
:style="{ height: props.height, fontSize: props.fontSize + 'px' }" :style="{ height: props.height, fontSize: props.fontSize + 'px' }"
@change="onChange"
/> />
</template> </template>

View File

@@ -5,21 +5,18 @@ import { Editor, Toolbar } from "@wangeditor/editor-for-vue"
import { uploadImage } from "../../admin/api" import { uploadImage } from "../../admin/api"
interface Props { interface Props {
value: string
title: string title: string
minHeight?: number minHeight?: number
} }
const rawHtml = defineModel<string>("value")
type InsertFnType = (url: string, alt: string, href: string) => void type InsertFnType = (url: string, alt: string, href: string) => void
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
minHeight: 0, minHeight: 0,
}) })
const emit = defineEmits(["update:value"])
const message = useMessage() const message = useMessage()
const rawHtml = ref(props.value)
watch(rawHtml, () => emit("update:value", rawHtml.value))
const editorRef = shallowRef<IDomEditor>() const editorRef = shallowRef<IDomEditor>()

View File

@@ -121,6 +121,8 @@ export const PROBLEM_PERMISSION = {
export const STORAGE_KEY = { export const STORAGE_KEY = {
AUTHED: "authed", AUTHED: "authed",
LANGUAGE: "problemLanguage", LANGUAGE: "problemLanguage",
ADMIN_PROBLEM: "adminProblem",
ADMIN_PROBLEM_TAGS: "adminProblemTags",
} }
export const DIFFICULTY = { export const DIFFICULTY = {

View File

@@ -7,7 +7,7 @@ import Components from "unplugin-vue-components/vite"
import { NaiveUiResolver } from "unplugin-vue-components/resolvers" import { NaiveUiResolver } from "unplugin-vue-components/resolvers"
const dev = process.env.NODE_ENV === "development" const dev = process.env.NODE_ENV === "development"
const url = dev ? "http://localhost:8080" : "https://oj.xuyue.cc" const url = dev ? "https://ojtest.xuyue.cc" : "https://oj.xuyue.cc"
const proxyConfig = { const proxyConfig = {
target: url, target: url,
changeOrigin: true, changeOrigin: true,