add create a problem.

This commit is contained in:
2023-03-27 18:21:29 +08:00
parent 7f8260d34d
commit 9bc1a1b5b5
12 changed files with 831 additions and 11 deletions

View File

@@ -79,3 +79,14 @@ export function getContestList(offset = 0, limit = 10, keyword: string) {
params: { paging: true, offset, limit, keyword },
})
}
// 上传图片
export async function uploadImage(file: File): Promise<string> {
const form = new window.FormData()
form.append("image", file)
const res: { success: boolean; file_path: string; msg: "Success" } =
await http.post("admin/upload_image", form, {
headers: { "content-type": "multipart/form-data" },
})
return res.success ? res.file_path : ""
}

View File

@@ -1,7 +1,241 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { SelectOption } from "naive-ui"
import { getProblemTagList } from "~/shared/api"
import TextEditor from "~/shared/TextEditor.vue"
import { unique } from "~/utils/functions"
import { Problem, Tag } from "~/utils/types"
interface AlterProblem {
spj_language: string
spj_code: string
spj_compile_ok: boolean
test_case_id: string
test_case_score: any[]
}
type ExcludeKeys =
| "id"
| "created_by"
| "create_time"
| "last_update_time"
| "my_status"
| "contest"
| "statistic_info"
| "accepted_number"
| "submission_number"
| "total_score"
const message = useMessage()
const problem = reactive<Omit<Problem, ExcludeKeys> & AlterProblem>({
_id: "",
title: "",
description: "",
input_description: "",
output_description: "",
time_limit: 1000,
memory_limit: 64,
difficulty: "Low",
visible: true,
share_submission: false,
tags: [],
languages: ["C", "C++", "Python3"],
template: {},
samples: [{ input: "", output: "" }],
spj: false,
spj_language: "",
spj_code: "",
spj_compile_ok: false,
test_case_id: "",
test_case_score: [],
rule_type: "ACM",
hint: "",
source: "",
io_mode: {
io_mode: "Standard IO",
input: "input.txt",
output: "output.txt",
},
})
const existingTags = shallowRef<Tag[]>([])
const fromExistingTags = shallowRef<string[]>([])
const newTags = shallowRef<string[]>([])
const difficultyOptions: SelectOption[] = [
{ label: "简单", value: "Low" },
{ label: "中等", value: "Med" },
{ label: "困难", value: "High" },
]
const languageOptions = [
{ label: "C", value: "C" },
{ label: "C++", value: "C++" },
{ label: "Python", value: "Python3" },
{ label: "Java", value: "Java" },
{ label: "JS", value: "JavaScript" },
{ label: "Go", value: "Golang" },
]
const tagOptions = computed(() =>
existingTags.value.map((tag) => ({ label: tag.name, value: tag.name }))
)
async function listTags() {
const res = await getProblemTagList()
existingTags.value = res.data
}
function updateNewTags(v: string[]) {
const blanks = []
const uniqueTags = unique(v)
const tags = existingTags.value.map((t) => t.name)
for (let i = 0; i < uniqueTags.length; i++) {
const tag = uniqueTags[i]
if (tags.indexOf(tag) < 0) {
blanks.push(tag)
} else {
message.error("已经存在标签:" + tag)
break
}
}
newTags.value = blanks
}
function addSample() {
problem.samples.push({ input: "", output: "" })
}
function removeSample(index: number) {
problem.samples.splice(index, 1)
}
onMounted(() => {
listTags()
})
watch([fromExistingTags, newTags], (tags) => {
const uniqueTags = unique<string>(tags[0].concat(tags[1]))
problem.tags = uniqueTags
})
</script>
<template>
<div>problem detail</div>
<n-form inline label-placement="left">
<n-form-item label="显示编号">
<n-input class="id" v-model:value="problem._id" />
</n-form-item>
<n-form-item label="题目">
<n-input class="titleInput" v-model:value="problem.title" />
</n-form-item>
<n-form-item label="语言">
<n-checkbox-group v-model:value="problem.languages">
<n-space align="center">
<n-checkbox
v-for="(language, index) in languageOptions"
:key="index"
:value="language.value"
:label="language.label"
/>
</n-space>
</n-checkbox-group>
</n-form-item>
</n-form>
<n-form inline label-placement="left">
<n-form-item label="可见">
<n-switch v-model:value="problem.visible" />
</n-form-item>
<n-form-item label="难度">
<n-select
class="difficulty"
:options="difficultyOptions"
v-model:value="problem.difficulty"
/>
</n-form-item>
<n-form-item label="现成的标签">
<n-select
class="tag"
multiple
v-model:value="fromExistingTags"
:options="tagOptions"
/>
</n-form-item>
<n-form-item label="新增的标签">
<n-dynamic-tags v-model:value="newTags" @update:value="updateNewTags" />
</n-form-item>
</n-form>
<TextEditor
v-model:value="problem.description"
title="题目"
:min-height="300"
/>
<TextEditor v-model:value="problem.input_description" title="输入的描述" />
<TextEditor v-model:value="problem.output_description" title="输出的描述" />
<div class="samples" v-for="(sample, index) in problem.samples" :key="index">
<n-space justify="space-between" align="center">
<strong>测试样例 {{ index + 1 }}</strong>
<n-button
tertiary
type="warning"
size="small"
@click="removeSample(index)"
>
删除 {{ index + 1 }}
</n-button>
</n-space>
<n-grid x-gap="20" cols="2">
<n-gi span="1">
<n-space vertical>
<span>输入样例</span>
<n-input type="textarea" v-model:value="sample.input" />
</n-space>
</n-gi>
<n-gi span="1">
<n-space vertical>
<span>输出样例</span>
<n-input type="textarea" v-model:value="sample.output" />
</n-space>
</n-gi>
</n-grid>
</div>
<n-button class="addSamples" tertiary type="primary" @click="addSample">
添加用例
</n-button>
<TextEditor v-model:value="problem.hint" title="提示" />
<n-form>
<n-form-item label="题目的来源">
<n-input
v-model:value="problem.source"
placeholder="(比如来自某道题的改编等,或者网上的资料)"
/>
</n-form-item>
</n-form>
</template>
<style scoped></style>
<style scoped>
.id {
width: 100px;
}
.titleInput {
width: 300px;
}
.title {
margin-bottom: 12px;
}
.difficulty {
width: 100px;
}
.tag {
width: 300px;
}
.samples {
margin-bottom: 20px;
}
.addSamples {
width: 100%;
margin-bottom: 20px;
}
</style>

3
src/components.d.ts vendored
View File

@@ -21,12 +21,15 @@ declare module '@vue/runtime-core' {
NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
NCode: typeof import('naive-ui')['NCode']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDataTable: typeof import('naive-ui')['NDataTable']
NDescriptions: typeof import('naive-ui')['NDescriptions']
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
NDropdown: typeof import('naive-ui')['NDropdown']
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
NEmpty: typeof import('naive-ui')['NEmpty']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']

View File

@@ -51,10 +51,6 @@ export async function getProblemList(
}
}
export function getProblemTagList() {
return http.get("problem/tags")
}
export function getRandomProblemID() {
return http.get("pickone")
}

View File

@@ -3,10 +3,11 @@ import { useUserStore } from "~/shared/store/user"
import { filterEmptyValue, getTagColor } from "utils/functions"
import { ProblemFiltered } from "utils/types"
import { isDesktop } from "~/shared/composables/breakpoints"
import { getProblemList, getProblemTagList, getRandomProblemID } from "oj/api"
import { getProblemList, getRandomProblemID } from "oj/api"
import Pagination from "~/shared/Pagination.vue"
import { DataTableColumn, NSpace, NTag } from "naive-ui"
import ProblemStatus from "./components/ProblemStatus.vue"
import { getProblemTagList } from "~/shared/api"
interface Tag {
id: number

91
src/shared/TextEditor.vue Normal file
View File

@@ -0,0 +1,91 @@
<script setup lang="ts">
import "@wangeditor/editor/dist/css/style.css"
import { IDomEditor, IEditorConfig, IToolbarConfig } from "@wangeditor/editor"
import { Editor, Toolbar } from "@wangeditor/editor-for-vue"
import { uploadImage } from "../admin/api"
interface Props {
value: string
title: string
minHeight?: number
}
type InsertFnType = (url: string, alt: string, href: string) => void
const props = withDefaults(defineProps<Props>(), {
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<IDomEditor>()
const toolbarConfig: Partial<IToolbarConfig> = {
excludeKeys: ["todo", "insertVideo", "insertTable", "fullScreen"],
}
const editorConfig: Partial<IEditorConfig> = {
scroll: false,
MENU_CONF: {
uploadImage: { customUpload },
},
}
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor) editor.destroy()
})
function handleCreated(editor: IDomEditor) {
editorRef.value = editor
}
async function customUpload(file: File, insertFn: InsertFnType) {
const path = await uploadImage(file)
if (!path) {
message.error("图片上传失败")
return
}
const url = path
const alt = "图片"
const href = ""
insertFn(url, alt, href)
}
</script>
<template>
<div class="title" v-if="props.title">{{ props.title }}</div>
<div class="editorWrapper">
<Toolbar
class="toolbar"
:editor="editorRef"
:defaultConfig="toolbarConfig"
mode="simple"
/>
<Editor
:style="{ minHeight: props.minHeight + 'px' }"
v-model="rawHtml"
:defaultConfig="editorConfig"
mode="simple"
@onCreated="handleCreated"
/>
</div>
</template>
<style scoped>
.title {
margin-bottom: 12px;
}
.toolbar {
border-bottom: 1px solid #ddd;
}
.editorWrapper {
border: 1px solid #ddd;
margin-bottom: 20px;
}
</style>

View File

@@ -1,5 +1,5 @@
import http from "utils/http"
import { Profile } from "~/utils/types"
import { Profile, Tag } from "~/utils/types"
export function login(data: { username: string; password: string }) {
return http.post("login", data)
@@ -12,3 +12,7 @@ export function logout() {
export function getProfile(username: string = "") {
return http.get<Profile>("profile", { params: { username } })
}
export function getProblemTagList() {
return http.get<Tag[]>("problem/tags")
}

View File

@@ -110,7 +110,7 @@ onMounted(async () => {
<n-layout-sider width="160" bordered :native-scrollbar="false">
<n-menu :options="options" :value="active" />
</n-layout-sider>
<n-layout-content content-style="padding: 16px">
<n-layout-content content-style="padding: 16px; min-width: 600px">
<router-view></router-view>
</n-layout-content>
</n-layout>

View File

@@ -129,3 +129,12 @@ export function getUserRole(role: User["admin_type"]): {
}
return obj
}
export function unique<T>(arr: T[]) {
return arr.reduce((prev: T[], curr: T) => {
if (!prev.includes(curr)) {
prev.push(curr)
}
return prev
}, [])
}

View File

@@ -67,6 +67,11 @@ interface SampleUser {
real_name: string | null
}
export interface Tag {
id: number
name: string
}
export interface Problem {
_id: string
id: number
@@ -93,7 +98,7 @@ export interface Problem {
io_mode: string
}
spj: boolean
spj_language: null
spj_language: string
rule_type: string
difficulty: "Low" | "Mid" | "High"
source: string