add create a problem.
This commit is contained in:
@@ -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 : ""
|
||||
}
|
||||
|
||||
@@ -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
3
src/components.d.ts
vendored
@@ -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']
|
||||
|
||||
@@ -51,10 +51,6 @@ export async function getProblemList(
|
||||
}
|
||||
}
|
||||
|
||||
export function getProblemTagList() {
|
||||
return http.get("problem/tags")
|
||||
}
|
||||
|
||||
export function getRandomProblemID() {
|
||||
return http.get("pickone")
|
||||
}
|
||||
|
||||
@@ -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
91
src/shared/TextEditor.vue
Normal 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>
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user