添加提交
This commit is contained in:
4
components.d.ts
vendored
4
components.d.ts
vendored
@@ -15,6 +15,7 @@ declare module 'vue' {
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCode: typeof import('naive-ui')['NCode']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
@@ -30,6 +31,8 @@ declare module 'vue' {
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NModalProvider: typeof import('naive-ui')['NModalProvider']
|
||||
NPagination: typeof import('naive-ui')['NPagination']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NRate: typeof import('naive-ui')['NRate']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSplit: typeof import('naive-ui')['NSplit']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
@@ -40,6 +43,7 @@ declare module 'vue' {
|
||||
Preview: typeof import('./src/components/Preview.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
TaskTitle: typeof import('./src/components/submissions/TaskTitle.vue')['default']
|
||||
Tutorial: typeof import('./src/components/Tutorial.vue')['default']
|
||||
UserActions: typeof import('./src/components/dashboard/UserActions.vue')['default']
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { onMounted, watch } from "vue"
|
||||
import { Account } from "./api"
|
||||
import { authed, user } from "./store/user"
|
||||
import { STORAGE_KEY } from "./utils/const"
|
||||
import hljs from "highlight.js/lib/core"
|
||||
|
||||
onMounted(async () => {
|
||||
const data = await Account.getMyProfile()
|
||||
@@ -23,7 +24,12 @@ watch(authed, (v) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider class="myContainer" :locale="zhCN" :date-locale="dateZhCN">
|
||||
<n-config-provider
|
||||
class="myContainer"
|
||||
:locale="zhCN"
|
||||
:date-locale="dateZhCN"
|
||||
:hljs="hljs"
|
||||
>
|
||||
<n-modal-provider>
|
||||
<n-message-provider :max="1">
|
||||
<n-dialog-provider>
|
||||
|
||||
31
src/api.ts
31
src/api.ts
@@ -105,3 +105,34 @@ export const Tutorial = {
|
||||
// return text.split("\n").map((item) => Number(item.split(" ")[0]))
|
||||
},
|
||||
}
|
||||
|
||||
export const Submission = {
|
||||
async create(
|
||||
taskId: number,
|
||||
code: {
|
||||
html?: string
|
||||
css?: string
|
||||
js?: string
|
||||
},
|
||||
) {
|
||||
const data = { task_id: taskId, ...code }
|
||||
const res = await http.post("/submission/", data)
|
||||
return res.data
|
||||
},
|
||||
|
||||
async list(query: { page: number }) {
|
||||
const res = await http.get("/submission", {
|
||||
params: query,
|
||||
})
|
||||
return res.data
|
||||
},
|
||||
|
||||
async get(id: string) {
|
||||
const res = await http.get("/submission/" + id)
|
||||
return res.data
|
||||
},
|
||||
|
||||
async updateScore(id: string, score: number) {
|
||||
await http.put(`/submission/${id}/score`, { score })
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
<template>
|
||||
<n-flex align="center" class="corner">
|
||||
<n-button secondary v-if="!show" @click="showTutorial">教程</n-button>
|
||||
<n-button secondary @click="$router.push({ name: 'submissions' })">
|
||||
查看
|
||||
</n-button>
|
||||
<template v-if="user.loaded && authed">
|
||||
<n-button type="primary" secondary @click="submit">提交</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
secondary
|
||||
:disabled="submitDisabled"
|
||||
:loading="submitLoading"
|
||||
@click="submit"
|
||||
>
|
||||
提交
|
||||
</n-button>
|
||||
<n-dropdown :options="menu" @select="clickMenu">
|
||||
<n-button>{{ user.username }}</n-button>
|
||||
</n-dropdown>
|
||||
@@ -18,22 +29,30 @@
|
||||
</n-flex>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, h } from "vue"
|
||||
import { computed, h, ref } from "vue"
|
||||
import { useMessage } from "naive-ui"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { authed, roleNormal, roleSuper, user } from "../store/user"
|
||||
import { loginModal } from "../store/modal"
|
||||
import { show, tutorialSize } from "../store/tutorial"
|
||||
import { Account } from "../api"
|
||||
import { taskId } from "../store/task"
|
||||
import { html, css, js } from "../store/editors"
|
||||
import { Account, Submission } from "../api"
|
||||
import { Role } from "../utils/type"
|
||||
import { router } from "../router"
|
||||
import { ADMIN_URL } from "../utils/const"
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const submitDisabled = computed(() => {
|
||||
return taskId.value === 0
|
||||
})
|
||||
|
||||
const menu = computed(() => [
|
||||
{
|
||||
label: "后台",
|
||||
label: "后台管理",
|
||||
key: "dashboard",
|
||||
show: !roleNormal.value,
|
||||
icon: () =>
|
||||
@@ -42,7 +61,7 @@ const menu = computed(() => [
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "管理",
|
||||
label: "数据管理",
|
||||
key: "admin",
|
||||
show: roleSuper.value,
|
||||
icon: () =>
|
||||
@@ -51,7 +70,15 @@ const menu = computed(() => [
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "退出",
|
||||
label: "我的提交",
|
||||
key: "submissions",
|
||||
icon: () =>
|
||||
h(Icon, {
|
||||
icon: "streamline-emojis:bar-chart",
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "退出账号",
|
||||
key: "logout",
|
||||
icon: () =>
|
||||
h(Icon, {
|
||||
@@ -73,6 +100,9 @@ function clickMenu(name: string) {
|
||||
case "admin":
|
||||
window.open(ADMIN_URL)
|
||||
break
|
||||
case "submissions":
|
||||
router.push({ name: "submissions" })
|
||||
break
|
||||
case "logout":
|
||||
handleLogout()
|
||||
break
|
||||
@@ -89,8 +119,20 @@ async function handleLogout() {
|
||||
user.role = Role.Normal
|
||||
}
|
||||
|
||||
function submit() {
|
||||
message.error("未实装")
|
||||
async function submit() {
|
||||
try {
|
||||
submitLoading.value = true
|
||||
await Submission.create(taskId.value, {
|
||||
html: html.value,
|
||||
css: css.value,
|
||||
js: js.value,
|
||||
})
|
||||
message.success("提交成功")
|
||||
} catch (err) {
|
||||
message.error("提交失败")
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
title="登录失败,请检查用户名和密码"
|
||||
></n-alert>
|
||||
<n-flex>
|
||||
<n-button block :loading="loading" @click="submit" type="primary">登录</n-button>
|
||||
<n-button block :loading="loading" @click="submit" type="primary"
|
||||
>登录</n-button
|
||||
>
|
||||
</n-flex>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
<n-flex>
|
||||
<n-button quaternary @click="download" :disabled="!showDL">下载</n-button>
|
||||
<n-button quaternary @click="open">展示</n-button>
|
||||
<template v-if="!!submission.id">
|
||||
<n-button quaternary @click="emits('showCode')">查看代码</n-button>
|
||||
<n-popover v-if="!submission.score && (roleAdmin || roleSuper)">
|
||||
<template #trigger>
|
||||
<n-button secondary type="primary">手动打分</n-button>
|
||||
</template>
|
||||
<n-rate :size="30" @update:value="updateScore" />
|
||||
</n-popover>
|
||||
<n-button secondary type="info">智能打分</n-button>
|
||||
</template>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
<iframe class="iframe" ref="iframe"></iframe>
|
||||
@@ -14,12 +24,26 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watchDebounced } from "@vueuse/core"
|
||||
import { html, css, js } from "../store/editors"
|
||||
import { computed, onMounted, useTemplateRef } from "vue"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Submission } from "../api"
|
||||
import { submission } from "../store/submission"
|
||||
import { useMessage } from "naive-ui"
|
||||
import { roleAdmin, roleSuper } from "../store/user"
|
||||
|
||||
interface Props {
|
||||
html: string
|
||||
css: string
|
||||
js: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emits = defineEmits(["afterScore", "showCode"])
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const iframe = useTemplateRef<HTMLIFrameElement>("iframe")
|
||||
const showDL = computed(() => html.value || css.value || js.value)
|
||||
const showDL = computed(() => props.html || props.css || props.js)
|
||||
|
||||
function getContent() {
|
||||
return `<!DOCTYPE html>
|
||||
@@ -28,13 +52,13 @@ function getContent() {
|
||||
<meta charset="UTF-8" />
|
||||
<title>预览</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>${css.value}</style>
|
||||
<style>${props.css}</style>
|
||||
<link rel="stylesheet" href="/normalize.min.css" />
|
||||
<script src="/jquery.min.js"><\/script>
|
||||
</head>
|
||||
<body>
|
||||
${html.value}
|
||||
<script type="module">${js.value}<\/script>
|
||||
${props.html}
|
||||
<script type="module">${props.js}<\/script>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
@@ -68,7 +92,21 @@ function open() {
|
||||
newTab.document.close()
|
||||
}
|
||||
|
||||
watchDebounced([html, css, js], preview, { debounce: 500, maxWait: 1000 })
|
||||
async function updateScore(score: number) {
|
||||
try {
|
||||
await Submission.updateScore(submission.value.id, score)
|
||||
message.success("评分成功")
|
||||
submission.value.score = score
|
||||
emits("afterScore")
|
||||
} catch (err: any) {
|
||||
message.error(err.response.data.detail)
|
||||
}
|
||||
}
|
||||
|
||||
watchDebounced(() => [props.html, props.css, props.js], preview, {
|
||||
debounce: 500,
|
||||
maxWait: 1000,
|
||||
})
|
||||
onMounted(preview)
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
@@ -36,6 +36,7 @@ import { step } from "../store/tutorial"
|
||||
import { authed, roleSuper } from "../store/user"
|
||||
import { useStorage } from "@vueuse/core"
|
||||
import { STORAGE_KEY } from "../utils/const"
|
||||
import { taskId } from "../store/task"
|
||||
|
||||
const displays = ref<number[]>([])
|
||||
const content = useStorage(STORAGE_KEY.CONTENT, "")
|
||||
@@ -75,6 +76,7 @@ async function getContent() {
|
||||
step.value = displays.value[0]
|
||||
}
|
||||
const data = await Tutorial.get(step.value)
|
||||
taskId.value = data.task_ptr
|
||||
content.value = await marked.parse(data.content, { async: true })
|
||||
}
|
||||
|
||||
|
||||
30
src/components/submissions/TaskTitle.vue
Normal file
30
src/components/submissions/TaskTitle.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<n-flex>
|
||||
<n-tag
|
||||
size="small"
|
||||
:bordered="false"
|
||||
type="primary"
|
||||
v-if="props.submission.task_type === 'tutorial'"
|
||||
>
|
||||
教程
|
||||
</n-tag>
|
||||
<n-tag
|
||||
:bordered="false"
|
||||
size="small"
|
||||
type="error"
|
||||
v-if="props.submission.task_type === 'challenge'"
|
||||
>
|
||||
挑战
|
||||
</n-tag>
|
||||
<n-button text>{{ props.submission.task_title }}</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { SubmissionOut } from "../../utils/type"
|
||||
|
||||
interface Props {
|
||||
submission: SubmissionOut
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
@@ -14,7 +14,7 @@
|
||||
<Editors />
|
||||
</template>
|
||||
<template #2>
|
||||
<Preview />
|
||||
<Preview :html="html" :css="css" :js="js" />
|
||||
</template>
|
||||
</n-split>
|
||||
</template>
|
||||
@@ -26,6 +26,7 @@ import Editors from "../components/Editors.vue"
|
||||
import Preview from "../components/Preview.vue"
|
||||
import Tutorial from "../components/Tutorial.vue"
|
||||
import { show, tutorialSize } from "../store/tutorial"
|
||||
import { html, css, js } from "../store/editors"
|
||||
|
||||
const { ctrl_s } = useMagicKeys({
|
||||
passive: false,
|
||||
|
||||
158
src/pages/Submissions.vue
Normal file
158
src/pages/Submissions.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<n-grid class="container" x-gap="10" :cols="3">
|
||||
<n-gi :span="1">
|
||||
<n-flex vertical>
|
||||
<n-flex justify="space-between">
|
||||
<n-button quaternary @click="$router.push({ name: 'home' })">
|
||||
返回首页
|
||||
</n-button>
|
||||
<n-pagination
|
||||
v-model:page="query.page"
|
||||
:page-size="10"
|
||||
:item-count="count"
|
||||
simple
|
||||
>
|
||||
<template #prefix>总共 {{ count }} 条</template>
|
||||
</n-pagination>
|
||||
</n-flex>
|
||||
<n-data-table striped :columns="columns" :data="data"></n-data-table>
|
||||
</n-flex>
|
||||
</n-gi>
|
||||
<n-gi :span="2">
|
||||
<Preview
|
||||
v-if="submission.id"
|
||||
:html="html"
|
||||
:css="css"
|
||||
:js="js"
|
||||
@after-score="afterScore"
|
||||
@show-code="toggleShowCode"
|
||||
/>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-modal
|
||||
preset="card"
|
||||
title="前端代码"
|
||||
v-model:show="codeModal"
|
||||
style="max-width: 80%"
|
||||
>
|
||||
<n-grid x-gap="20" :cols="codeCount">
|
||||
<n-gi :span="1" v-if="html">
|
||||
<n-code :code="html" language="html" word-wrap></n-code>
|
||||
</n-gi>
|
||||
<n-gi :span="1" v-if="css">
|
||||
<n-code :code="css" language="css" word-wrap></n-code>
|
||||
</n-gi>
|
||||
<n-gi :span="1" v-if="js">
|
||||
<n-code :code="js" language="javascript" word-wrap></n-code>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-modal>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { NButton, type DataTableColumn } from "naive-ui"
|
||||
import { computed, h, onMounted, onUnmounted, reactive, ref, watch } from "vue"
|
||||
import { Submission } from "../api"
|
||||
import type { SubmissionOut } from "../utils/type"
|
||||
import { parseTime } from "../utils/helper"
|
||||
import TaskTitle from "../components/submissions/TaskTitle.vue"
|
||||
import Preview from "../components/Preview.vue"
|
||||
import { submission } from "../store/submission"
|
||||
|
||||
const data = ref<SubmissionOut[]>([])
|
||||
const count = ref(0)
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
})
|
||||
|
||||
const html = computed(() => submission.value.html)
|
||||
const css = computed(() => submission.value.css)
|
||||
const js = computed(() => submission.value.js)
|
||||
|
||||
const codeCount = computed(
|
||||
() => [html.value, css.value, js.value].filter((c) => !!c).length,
|
||||
)
|
||||
|
||||
const codeModal = ref(false)
|
||||
|
||||
const columns: DataTableColumn<SubmissionOut>[] = [
|
||||
{
|
||||
title: "时间",
|
||||
key: "created",
|
||||
render: (row) => parseTime(row.created, "YYYY-MM-DD HH:mm:ss"),
|
||||
},
|
||||
{
|
||||
title: "提交者",
|
||||
key: "user",
|
||||
render: (row) => row.username,
|
||||
},
|
||||
{
|
||||
title: "任务",
|
||||
key: "task_title",
|
||||
render: (submission) => h(TaskTitle, { submission }),
|
||||
},
|
||||
{
|
||||
title: "得分",
|
||||
key: "score",
|
||||
render: (row) => {
|
||||
if (row.score > 0) return row.score
|
||||
else return "-"
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "效果",
|
||||
key: "code",
|
||||
render: (row) =>
|
||||
h(
|
||||
NButton,
|
||||
{ quaternary: true, onClick: () => getSubmissionByID(row.id) },
|
||||
() => "查看",
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
async function init() {
|
||||
const res = await Submission.list(query)
|
||||
data.value = res.items
|
||||
count.value = res.count
|
||||
}
|
||||
|
||||
async function getSubmissionByID(id: string) {
|
||||
submission.value = await Submission.get(id)
|
||||
}
|
||||
|
||||
function toggleShowCode() {
|
||||
codeModal.value = true
|
||||
}
|
||||
|
||||
function afterScore() {
|
||||
data.value = data.value.map((d) => {
|
||||
if (d.id === submission.value.id) {
|
||||
d.score = submission.value.score
|
||||
}
|
||||
return d
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => query.page, init)
|
||||
onMounted(init)
|
||||
onUnmounted(() => {
|
||||
submission.value = {
|
||||
id: "",
|
||||
userid: 0,
|
||||
username: "",
|
||||
task_title: "",
|
||||
task_type: "tutorial",
|
||||
score: 0,
|
||||
html: "",
|
||||
css: "",
|
||||
js: "",
|
||||
created: new Date(),
|
||||
modified: new Date(),
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -24,7 +24,7 @@
|
||||
simple
|
||||
/>
|
||||
</n-flex>
|
||||
<n-data-table :columns="columns" :data="users"></n-data-table>
|
||||
<n-data-table striped :columns="columns" :data="users"></n-data-table>
|
||||
<n-modal
|
||||
style="width: 300px"
|
||||
:mask-closable="false"
|
||||
@@ -145,7 +145,7 @@ async function batchCreateUsers() {
|
||||
init()
|
||||
} catch (err: any) {
|
||||
batchLoading.value = false
|
||||
message.error(err.detail??"批量创建失败")
|
||||
message.error(err.detail ?? "批量创建失败")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@ import { STORAGE_KEY } from "./utils/const"
|
||||
|
||||
const routes = [
|
||||
{ path: "/", name: "home", component: Home },
|
||||
{
|
||||
path: "/submissions",
|
||||
name: "submissions",
|
||||
component: () => import("./pages/Submissions.vue"),
|
||||
},
|
||||
{
|
||||
path: "/dashboard",
|
||||
name: "dashboard",
|
||||
|
||||
16
src/store/submission.ts
Normal file
16
src/store/submission.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ref } from "vue"
|
||||
import type { SubmissionAll } from "../utils/type"
|
||||
|
||||
export const submission = ref<SubmissionAll>({
|
||||
id: "",
|
||||
userid: 0,
|
||||
username: "",
|
||||
task_title: "",
|
||||
task_type: "tutorial",
|
||||
score: 0,
|
||||
html: "",
|
||||
css: "",
|
||||
js: "",
|
||||
created: new Date(),
|
||||
modified: new Date(),
|
||||
})
|
||||
3
src/store/task.ts
Normal file
3
src/store/task.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { ref } from "vue"
|
||||
|
||||
export const taskId = ref(0)
|
||||
@@ -36,3 +36,28 @@ export interface User {
|
||||
role: Role
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
export interface SubmissionOut {
|
||||
id: string
|
||||
userid: number
|
||||
username: string
|
||||
task_type: string
|
||||
task_title: string
|
||||
score: number
|
||||
created: Date
|
||||
modified: Date
|
||||
}
|
||||
|
||||
export interface SubmissionAll {
|
||||
id: string
|
||||
userid: number
|
||||
username: string
|
||||
task_type: string
|
||||
task_title: string
|
||||
score: number
|
||||
html: ""
|
||||
css: ""
|
||||
js: ""
|
||||
created: Date
|
||||
modified: Date
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user