diff --git a/src/admin/problem/detail.vue b/src/admin/problem/detail.vue index a8a8a7e..556a040 100644 --- a/src/admin/problem/detail.vue +++ b/src/admin/problem/detail.vue @@ -3,7 +3,11 @@ import TextEditor from "~/shared/components/TextEditor.vue" import { unique } from "~/utils/functions" import { BlankProblem, LANGUAGE, Tag } from "~/utils/types" 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 { createContestProblem, @@ -37,7 +41,7 @@ const title = computed( "admin contest problem edit": "编辑比赛题目", })[route.name], ) -const problem = reactive({ +const problem = useLocalStorage(STORAGE_KEY.ADMIN_PROBLEM, { _id: "", title: "", description: "", @@ -75,9 +79,19 @@ const problem = reactive({ const template = reactive(JSON.parse(JSON.stringify(CODE_TEMPLATES))) const currentActiveTemplate = ref("C") -const existingTags = shallowRef([]) -const fromExistingTags = shallowRef([]) -const newTags = shallowRef([]) +// 从服务器来的tag列表 +const tagList = shallowRef([]) + +interface Tags { + select: string[] + upload: string[] +} +// 选择的 和 新的 +const tags = useLocalStorage(STORAGE_KEY.ADMIN_PROBLEM_TAGS, { + select: [], + upload: [], +}) + const [needTemplate, toggleNeedTemplate] = useToggle(false) const [ready, toggleReady] = useToggle(false) @@ -93,7 +107,7 @@ const languageOptions = [ ] 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() { @@ -103,76 +117,76 @@ async function getProblemDetail() { } const { data } = await getProblem(props.problemID) toggleReady(true) - problem.id = data.id - problem._id = data._id - problem.title = data.title - problem.description = data.description - problem.input_description = data.input_description - problem.output_description = data.output_description - problem.time_limit = data.time_limit - problem.memory_limit = data.memory_limit - problem.memory_limit = data.memory_limit - problem.difficulty = data.difficulty - problem.visible = data.visible - problem.share_submission = data.share_submission - problem.tags = data.tags - problem.languages = data.languages - problem.template = data.template - problem.samples = data.samples - problem.samples = data.samples - problem.spj = data.spj - problem.spj_language = data.spj_language - problem.spj_code = data.spj_code - problem.spj_compile_ok = data.spj_compile_ok - problem.test_case_id = data.test_case_id - problem.test_case_score = data.test_case_score - problem.rule_type = data.rule_type - problem.hint = data.hint - problem.source = data.source - problem.io_mode = data.io_mode - if (problem.contest_id) { - problem.contest_id = problem.contest_id + problem.value.id = data.id + problem.value._id = data._id + problem.value.title = data.title + problem.value.description = data.description + problem.value.input_description = data.input_description + problem.value.output_description = data.output_description + problem.value.time_limit = data.time_limit + problem.value.memory_limit = data.memory_limit + problem.value.memory_limit = data.memory_limit + problem.value.difficulty = data.difficulty + problem.value.visible = data.visible + problem.value.share_submission = data.share_submission + problem.value.tags = data.tags + problem.value.languages = data.languages + problem.value.template = data.template + problem.value.samples = data.samples + problem.value.samples = data.samples + problem.value.spj = data.spj + problem.value.spj_language = data.spj_language + problem.value.spj_code = data.spj_code + problem.value.spj_compile_ok = data.spj_compile_ok + problem.value.test_case_id = data.test_case_id + problem.value.test_case_score = data.test_case_score + problem.value.rule_type = data.rule_type + problem.value.hint = data.hint + problem.value.source = data.source + problem.value.io_mode = data.io_mode + if (problem.value.contest_id) { + problem.value.contest_id = problem.value.contest_id } // 下面是用来显示的: // 代码模板 和 模板开关 - problem.languages.forEach((lang) => { + problem.value.languages.forEach((lang) => { if (data.template[lang]) { template[lang] = data.template[lang] toggleNeedTemplate(true) } }) // 标签 - fromExistingTags.value = data.tags + tags.value.select = data.tags } -async function listTags() { +async function getTagList() { const res = await getProblemTagList() - existingTags.value = res.data + tagList.value = res.data } function updateNewTags(v: string[]) { const blanks = [] 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++) { const tag = uniqueTags[i] - if (tags.indexOf(tag) < 0) { + if (items.indexOf(tag) < 0) { blanks.push(tag) } else { message.error("已经存在标签:" + tag) break } } - newTags.value = blanks + tags.value.upload = blanks } function addSample() { - problem.samples.push({ input: "", output: "" }) + problem.value.samples.push({ input: "", output: "" }) } function removeSample(index: number) { - problem.samples.splice(index, 1) + problem.value.samples.splice(index, 1) } function resetTemplate(language: LANGUAGE) { @@ -182,7 +196,7 @@ function resetTemplate(language: LANGUAGE) { async function handleUploadTestcases({ file }: UploadCustomRequestOptions) { try { const res = await uploadTestcases(file.file!) - // @ts-ignore: + // @ts-ignore if (res.error) { message.error("上传测试用例失败") return @@ -190,51 +204,51 @@ async function handleUploadTestcases({ file }: UploadCustomRequestOptions) { const testcases = res.data.info for (let file of testcases) { file.score = (100 / testcases.length).toFixed(0) - if (!file.output_name && problem.spj) { + if (!file.output_name && problem.value.spj) { file.output_name = "-" } } - problem.test_case_score = testcases - problem.test_case_id = res.data.id + problem.value.test_case_score = testcases + problem.value.test_case_id = res.data.id } catch (err) { message.error("上传测试用例失败") } } function downloadTestcases() { - download("test_case?problem_id=" + problem.id) + download("test_case?problem_id=" + problem.value.id) } // 题目是否有漏写的 function detectProblemCompletion() { let flag = false // 标题 - if (!problem._id || !problem.title) { + if (!problem.value._id || !problem.value.title) { message.error("编号或标题没有填写") 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("标签没有填写") flag = true } // 题目 else if ( - !problem.description || - !problem.input_description || - !problem.output_description + !problem.value.description || + !problem.value.input_description || + !problem.value.output_description ) { message.error("题目或输入或输出没有填写") flag = true } // 样例 - else if (problem.samples.length == 0) { + else if (problem.value.samples.length == 0) { message.error("样例没有填写") flag = true } // 样例是空的 else if ( - problem.samples.some( + problem.value.samples.some( (sample) => sample.output === "" || sample.input === "", ) ) { @@ -242,10 +256,10 @@ function detectProblemCompletion() { flag = true } // 测试用例 - else if (problem.test_case_score.length === 0) { + else if (problem.value.test_case_score.length === 0) { message.error("测试用例没有上传") flag = true - } else if (problem.languages.length === 0) { + } else if (problem.value.languages.length === 0) { message.error("编程语言没有选择") flag = true } @@ -258,13 +272,13 @@ function detectProblemCompletion() { function getTemplate() { if (!needTemplate.value) { - problem.template = {} + problem.value.template = {} } else { - problem.languages.forEach((lang) => { + problem.value.languages.forEach((lang) => { if (CODE_TEMPLATES[lang] !== template[lang]) { - problem.template[lang] = template[lang] + problem.value.template[lang] = template[lang] } else { - delete problem.template[lang] + delete problem.value.template[lang] } }) } @@ -272,8 +286,8 @@ function getTemplate() { function filterHint() { // 编辑器会自动添加一段 HTML - if (problem.hint === "


") { - problem.hint = "" + if (problem.value.hint === "


") { + problem.value.hint = "" } } @@ -282,7 +296,6 @@ async function submit() { if (notCompleted) return filterHint() getTemplate() - problem.tags = [...newTags.value, ...fromExistingTags.value] const api = { "admin problem create": createProblem, "admin problem edit": editProblem, @@ -293,10 +306,12 @@ async function submit() { route.name === "admin contest problem create" || route.name === "admin contest problem edit" ) { - problem.contest_id = props.contestID + problem.value.contest_id = props.contestID } try { - await api!(problem) + await api!(problem.value) + problem.value = null + tags.value = null if ( route.name === "admin 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(() => { - listTags() + getTagList() getProblemDetail() }) -watch([fromExistingTags, newTags], (tags) => { - const uniqueTags = unique(tags[0].concat(tags[1])) - problem.tags = uniqueTags -}) +watch( + () => [tags.value.select, tags.value.upload], + (tags) => { + const uniqueTags = unique(tags[0].concat(tags[1])) + problem.value.tags = uniqueTags + }, +) diff --git a/src/shared/components/CodeEditor.vue b/src/shared/components/CodeEditor.vue index 577c55e..eee2c18 100644 --- a/src/shared/components/CodeEditor.vue +++ b/src/shared/components/CodeEditor.vue @@ -20,7 +20,6 @@ const styleTheme = EditorView.baseTheme({ }) interface Props { - modelValue: string language?: LANGUAGE fontSize?: number height?: string @@ -36,28 +35,16 @@ const props = withDefaults(defineProps(), { placeholder: "", }) -const code = ref(props.modelValue) +const code = defineModel("value") const isDark = useDark() -watch( - () => props.modelValue, - (v) => { - code.value = v - }, -) -const emit = defineEmits(["update:modelValue"]) - const lang = computed(() => { if (props.language === "Python3" || props.language === "Python2") { return python() } return cpp() }) - -function onChange(v: string) { - emit("update:modelValue", v) -} diff --git a/src/shared/components/TextEditor.vue b/src/shared/components/TextEditor.vue index 5740889..ce6fc47 100644 --- a/src/shared/components/TextEditor.vue +++ b/src/shared/components/TextEditor.vue @@ -5,21 +5,18 @@ import { Editor, Toolbar } from "@wangeditor/editor-for-vue" import { uploadImage } from "../../admin/api" interface Props { - value: string title: string minHeight?: number } +const rawHtml = defineModel("value") type InsertFnType = (url: string, alt: string, href: string) => void const props = withDefaults(defineProps(), { minHeight: 0, }) -const emit = defineEmits(["update:value"]) const message = useMessage() -const rawHtml = ref(props.value) -watch(rawHtml, () => emit("update:value", rawHtml.value)) const editorRef = shallowRef() diff --git a/src/utils/constants.ts b/src/utils/constants.ts index fafbf4f..249b5e4 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -121,6 +121,8 @@ export const PROBLEM_PERMISSION = { export const STORAGE_KEY = { AUTHED: "authed", LANGUAGE: "problemLanguage", + ADMIN_PROBLEM: "adminProblem", + ADMIN_PROBLEM_TAGS: "adminProblemTags", } export const DIFFICULTY = { diff --git a/vite.config.ts b/vite.config.ts index fe72933..40c09fb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,7 +7,7 @@ import Components from "unplugin-vue-components/vite" import { NaiveUiResolver } from "unplugin-vue-components/resolvers" 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 = { target: url, changeOrigin: true,