添加提交

This commit is contained in:
2025-03-18 19:16:08 +08:00
parent 3618b5a4b2
commit e4e7507f85
15 changed files with 382 additions and 19 deletions

4
components.d.ts vendored
View File

@@ -15,6 +15,7 @@ declare module 'vue' {
NAlert: typeof import('naive-ui')['NAlert'] NAlert: typeof import('naive-ui')['NAlert']
NButton: typeof import('naive-ui')['NButton'] NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard'] NCard: typeof import('naive-ui')['NCard']
NCode: typeof import('naive-ui')['NCode']
NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDataTable: typeof import('naive-ui')['NDataTable'] NDataTable: typeof import('naive-ui')['NDataTable']
NDialogProvider: typeof import('naive-ui')['NDialogProvider'] NDialogProvider: typeof import('naive-ui')['NDialogProvider']
@@ -30,6 +31,8 @@ declare module 'vue' {
NModal: typeof import('naive-ui')['NModal'] NModal: typeof import('naive-ui')['NModal']
NModalProvider: typeof import('naive-ui')['NModalProvider'] NModalProvider: typeof import('naive-ui')['NModalProvider']
NPagination: typeof import('naive-ui')['NPagination'] NPagination: typeof import('naive-ui')['NPagination']
NPopover: typeof import('naive-ui')['NPopover']
NRate: typeof import('naive-ui')['NRate']
NSelect: typeof import('naive-ui')['NSelect'] NSelect: typeof import('naive-ui')['NSelect']
NSplit: typeof import('naive-ui')['NSplit'] NSplit: typeof import('naive-ui')['NSplit']
NSwitch: typeof import('naive-ui')['NSwitch'] NSwitch: typeof import('naive-ui')['NSwitch']
@@ -40,6 +43,7 @@ declare module 'vue' {
Preview: typeof import('./src/components/Preview.vue')['default'] Preview: typeof import('./src/components/Preview.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
TaskTitle: typeof import('./src/components/submissions/TaskTitle.vue')['default']
Tutorial: typeof import('./src/components/Tutorial.vue')['default'] Tutorial: typeof import('./src/components/Tutorial.vue')['default']
UserActions: typeof import('./src/components/dashboard/UserActions.vue')['default'] UserActions: typeof import('./src/components/dashboard/UserActions.vue')['default']
} }

View File

@@ -5,6 +5,7 @@ import { onMounted, watch } from "vue"
import { Account } from "./api" import { Account } from "./api"
import { authed, user } from "./store/user" import { authed, user } from "./store/user"
import { STORAGE_KEY } from "./utils/const" import { STORAGE_KEY } from "./utils/const"
import hljs from "highlight.js/lib/core"
onMounted(async () => { onMounted(async () => {
const data = await Account.getMyProfile() const data = await Account.getMyProfile()
@@ -23,7 +24,12 @@ watch(authed, (v) => {
</script> </script>
<template> <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-modal-provider>
<n-message-provider :max="1"> <n-message-provider :max="1">
<n-dialog-provider> <n-dialog-provider>

View File

@@ -105,3 +105,34 @@ export const Tutorial = {
// return text.split("\n").map((item) => Number(item.split(" ")[0])) // 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 })
},
}

View File

@@ -1,8 +1,19 @@
<template> <template>
<n-flex align="center" class="corner"> <n-flex align="center" class="corner">
<n-button secondary v-if="!show" @click="showTutorial">教程</n-button> <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"> <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-dropdown :options="menu" @select="clickMenu">
<n-button>{{ user.username }}</n-button> <n-button>{{ user.username }}</n-button>
</n-dropdown> </n-dropdown>
@@ -18,22 +29,30 @@
</n-flex> </n-flex>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, h } from "vue" import { computed, h, ref } from "vue"
import { useMessage } from "naive-ui" import { useMessage } from "naive-ui"
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { authed, roleNormal, roleSuper, user } from "../store/user" import { authed, roleNormal, roleSuper, user } from "../store/user"
import { loginModal } from "../store/modal" import { loginModal } from "../store/modal"
import { show, tutorialSize } from "../store/tutorial" 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 { Role } from "../utils/type"
import { router } from "../router" import { router } from "../router"
import { ADMIN_URL } from "../utils/const" import { ADMIN_URL } from "../utils/const"
const message = useMessage() const message = useMessage()
const submitLoading = ref(false)
const submitDisabled = computed(() => {
return taskId.value === 0
})
const menu = computed(() => [ const menu = computed(() => [
{ {
label: "后台", label: "后台管理",
key: "dashboard", key: "dashboard",
show: !roleNormal.value, show: !roleNormal.value,
icon: () => icon: () =>
@@ -42,7 +61,7 @@ const menu = computed(() => [
}), }),
}, },
{ {
label: "管理", label: "数据管理",
key: "admin", key: "admin",
show: roleSuper.value, show: roleSuper.value,
icon: () => icon: () =>
@@ -51,7 +70,15 @@ const menu = computed(() => [
}), }),
}, },
{ {
label: "退出", label: "我的提交",
key: "submissions",
icon: () =>
h(Icon, {
icon: "streamline-emojis:bar-chart",
}),
},
{
label: "退出账号",
key: "logout", key: "logout",
icon: () => icon: () =>
h(Icon, { h(Icon, {
@@ -73,6 +100,9 @@ function clickMenu(name: string) {
case "admin": case "admin":
window.open(ADMIN_URL) window.open(ADMIN_URL)
break break
case "submissions":
router.push({ name: "submissions" })
break
case "logout": case "logout":
handleLogout() handleLogout()
break break
@@ -89,8 +119,20 @@ async function handleLogout() {
user.role = Role.Normal user.role = Role.Normal
} }
function submit() { async function submit() {
message.error("未实装") 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> </script>
<style scoped> <style scoped>

View File

@@ -23,7 +23,9 @@
title="登录失败,请检查用户名和密码" title="登录失败,请检查用户名和密码"
></n-alert> ></n-alert>
<n-flex> <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-flex>
</n-form> </n-form>
</n-modal> </n-modal>

View File

@@ -7,6 +7,16 @@
<n-flex> <n-flex>
<n-button quaternary @click="download" :disabled="!showDL">下载</n-button> <n-button quaternary @click="download" :disabled="!showDL">下载</n-button>
<n-button quaternary @click="open">展示</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>
</n-flex> </n-flex>
<iframe class="iframe" ref="iframe"></iframe> <iframe class="iframe" ref="iframe"></iframe>
@@ -14,12 +24,26 @@
<script lang="ts" setup> <script lang="ts" setup>
import { watchDebounced } from "@vueuse/core" import { watchDebounced } from "@vueuse/core"
import { html, css, js } from "../store/editors"
import { computed, onMounted, useTemplateRef } from "vue" import { computed, onMounted, useTemplateRef } from "vue"
import { Icon } from "@iconify/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 iframe = useTemplateRef<HTMLIFrameElement>("iframe")
const showDL = computed(() => html.value || css.value || js.value) const showDL = computed(() => props.html || props.css || props.js)
function getContent() { function getContent() {
return `<!DOCTYPE html> return `<!DOCTYPE html>
@@ -28,13 +52,13 @@ function getContent() {
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>预览</title> <title>预览</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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" /> <link rel="stylesheet" href="/normalize.min.css" />
<script src="/jquery.min.js"><\/script> <script src="/jquery.min.js"><\/script>
</head> </head>
<body> <body>
${html.value} ${props.html}
<script type="module">${js.value}<\/script> <script type="module">${props.js}<\/script>
</body> </body>
</html>` </html>`
} }
@@ -68,7 +92,21 @@ function open() {
newTab.document.close() 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) onMounted(preview)
</script> </script>
<style scoped> <style scoped>

View File

@@ -36,6 +36,7 @@ import { step } from "../store/tutorial"
import { authed, roleSuper } from "../store/user" import { authed, roleSuper } from "../store/user"
import { useStorage } from "@vueuse/core" import { useStorage } from "@vueuse/core"
import { STORAGE_KEY } from "../utils/const" import { STORAGE_KEY } from "../utils/const"
import { taskId } from "../store/task"
const displays = ref<number[]>([]) const displays = ref<number[]>([])
const content = useStorage(STORAGE_KEY.CONTENT, "") const content = useStorage(STORAGE_KEY.CONTENT, "")
@@ -75,6 +76,7 @@ async function getContent() {
step.value = displays.value[0] step.value = displays.value[0]
} }
const data = await Tutorial.get(step.value) const data = await Tutorial.get(step.value)
taskId.value = data.task_ptr
content.value = await marked.parse(data.content, { async: true }) content.value = await marked.parse(data.content, { async: true })
} }

View 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>

View File

@@ -14,7 +14,7 @@
<Editors /> <Editors />
</template> </template>
<template #2> <template #2>
<Preview /> <Preview :html="html" :css="css" :js="js" />
</template> </template>
</n-split> </n-split>
</template> </template>
@@ -26,6 +26,7 @@ import Editors from "../components/Editors.vue"
import Preview from "../components/Preview.vue" import Preview from "../components/Preview.vue"
import Tutorial from "../components/Tutorial.vue" import Tutorial from "../components/Tutorial.vue"
import { show, tutorialSize } from "../store/tutorial" import { show, tutorialSize } from "../store/tutorial"
import { html, css, js } from "../store/editors"
const { ctrl_s } = useMagicKeys({ const { ctrl_s } = useMagicKeys({
passive: false, passive: false,

158
src/pages/Submissions.vue Normal file
View 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>

View File

@@ -24,7 +24,7 @@
simple simple
/> />
</n-flex> </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 <n-modal
style="width: 300px" style="width: 300px"
:mask-closable="false" :mask-closable="false"

View File

@@ -6,6 +6,11 @@ import { STORAGE_KEY } from "./utils/const"
const routes = [ const routes = [
{ path: "/", name: "home", component: Home }, { path: "/", name: "home", component: Home },
{
path: "/submissions",
name: "submissions",
component: () => import("./pages/Submissions.vue"),
},
{ {
path: "/dashboard", path: "/dashboard",
name: "dashboard", name: "dashboard",

16
src/store/submission.ts Normal file
View 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
View File

@@ -0,0 +1,3 @@
import { ref } from "vue"
export const taskId = ref(0)

View File

@@ -36,3 +36,28 @@ export interface User {
role: Role role: Role
is_active: boolean 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
}