出题人

This commit is contained in:
2025-10-03 02:03:01 +08:00
parent c16059a2ee
commit b0229cb264
9 changed files with 95 additions and 58 deletions

17
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-python": "^6.2.1", "@codemirror/lang-python": "^6.2.1",
"@vueuse/core": "^13.9.0", "@vueuse/core": "^13.9.0",
"@vueuse/router": "^13.9.0",
"@wangeditor-next/editor": "^5.6.46", "@wangeditor-next/editor": "^5.6.46",
"@wangeditor-next/editor-for-vue": "^5.1.14", "@wangeditor-next/editor-for-vue": "^5.1.14",
"axios": "^1.12.2", "axios": "^1.12.2",
@@ -1530,6 +1531,22 @@
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
} }
}, },
"node_modules/@vueuse/router": {
"version": "13.9.0",
"resolved": "https://registry.npmmirror.com/@vueuse/router/-/router-13.9.0.tgz",
"integrity": "sha512-7AYay8Pv/0fC4D0eygbIyZuLyVs+9D7dsnO5D8aqat9qcOz91v/XFWR667WE1+p+OkU0ib+FjQUdnTVBNoIw8g==",
"license": "MIT",
"dependencies": {
"@vueuse/shared": "13.9.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0",
"vue-router": "^4.0.0"
}
},
"node_modules/@vueuse/shared": { "node_modules/@vueuse/shared": {
"version": "13.9.0", "version": "13.9.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.9.0.tgz", "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.9.0.tgz",

View File

@@ -13,6 +13,7 @@
"@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-python": "^6.2.1", "@codemirror/lang-python": "^6.2.1",
"@vueuse/core": "^13.9.0", "@vueuse/core": "^13.9.0",
"@vueuse/router": "^13.9.0",
"@wangeditor-next/editor": "^5.6.46", "@wangeditor-next/editor": "^5.6.46",
"@wangeditor-next/editor-for-vue": "^5.1.14", "@wangeditor-next/editor-for-vue": "^5.1.14",
"axios": "^1.12.2", "axios": "^1.12.2",

View File

@@ -7,6 +7,7 @@ import { AdminProblemFiltered } from "~/utils/types"
import { getProblemList, toggleProblemVisible } from "../api" import { getProblemList, toggleProblemVisible } from "../api"
import Actions from "./components/Actions.vue" import Actions from "./components/Actions.vue"
import Modal from "./components/Modal.vue" import Modal from "./components/Modal.vue"
import { useRouteQuery } from "@vueuse/router"
interface Props { interface Props {
contestID?: string contestID?: string
@@ -38,7 +39,7 @@ interface ProblemQuery {
// 使用分页 composable // 使用分页 composable
const { query, clearQuery } = usePagination<ProblemQuery>({ const { query, clearQuery } = usePagination<ProblemQuery>({
keyword: "", keyword: useRouteQuery("keyword", "").value,
}) })
const columns: DataTableColumn<AdminProblemFiltered>[] = [ const columns: DataTableColumn<AdminProblemFiltered>[] = [
@@ -152,7 +153,7 @@ watch(
从题库中选择 从题库中选择
</n-button> </n-button>
<div> <div>
<n-input v-model:value="query.keyword" placeholder="输入标题关键字" /> <n-input v-model:value="query.keyword" placeholder="输入标题关键字" clearable @clear="clearQuery" />
</div> </div>
</n-flex> </n-flex>
</n-flex> </n-flex>

View File

@@ -56,6 +56,10 @@ export async function getProblemList(
} }
} }
export function getAuthors() {
return http.get("problem/author")
}
export function getRandomProblemID() { export function getRandomProblemID() {
return http.get("pickone") return http.get("pickone")
} }
@@ -84,7 +88,7 @@ export function submitCode(data: SubmitCodePayload) {
return http.post("submission", data) return http.post("submission", data)
} }
export function getSubmissions(params: SubmissionListPayload) { export function getSubmissions(params: Partial<SubmissionListPayload>) {
const endpoint = !!params.contest_id ? "contest_submissions" : "submissions" const endpoint = !!params.contest_id ? "contest_submissions" : "submissions"
return http.get(endpoint, { params }) return http.get(endpoint, { params })
} }
@@ -249,13 +253,10 @@ export function getAIDetailData(start: string, end: string) {
return http.get("ai/detail", { params: { start, end } }) return http.get("ai/detail", { params: { start, end } })
} }
export function getAIWeeklyData( export function getAIWeeklyData(end: string, duration: string) {
end: string,
duration: string,
) {
return http.get("ai/weekly", { params: { end, duration } }) return http.get("ai/weekly", { params: { end, duration } })
} }
export function getAIHeatmapData() { export function getAIHeatmapData() {
return http.get("ai/heatmap") return http.get("ai/heatmap")
} }

View File

@@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRouteQuery } from "@vueuse/router"
import { NTag } from "naive-ui" import { NTag } from "naive-ui"
import { getContestList } from "oj/api" import { getContestList } from "oj/api"
import { duration, parseTime } from "utils/functions" import { duration, parseTime } from "utils/functions"
@@ -22,9 +23,9 @@ interface ContestQuery {
// 使用分页 composable // 使用分页 composable
const { query, clearQuery } = usePagination<ContestQuery>({ const { query, clearQuery } = usePagination<ContestQuery>({
keyword: "", keyword: useRouteQuery("keyword", "").value,
status: "", status: useRouteQuery("status", "").value,
tag: "", tag: useRouteQuery("tag", "").value,
}) })
const data = ref<Contest[]>([]) const data = ref<Contest[]>([])

View File

@@ -153,7 +153,7 @@ watch(query, listSubmissions)
</n-flex> </n-flex>
</template> </template>
</n-alert> </n-alert>
<n-alert class="tip" type="error" :show-icon="false" v-else> <n-alert class="tip" type="error" :show-icon="false" v-if="rank === -1 && class_ac_count > 0">
<template #header> <template #header>
<n-flex> <n-flex>
<div> <div>
@@ -212,7 +212,7 @@ watch(query, listSubmissions)
</n-flex> </n-flex>
</template> </template>
</n-alert> </n-alert>
<n-alert class="tip" type="error" :show-icon="false" v-else> <n-alert class="tip" type="error" :show-icon="false" v-if="rank === -1 && all_ac_count > 0">
<template #header> <template #header>
<n-flex align="center"> <n-flex align="center">
<div> <div>

View File

@@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { NSpace, NTag } from "naive-ui" import { NSpace, NTag } from "naive-ui"
import { getProblemList } from "oj/api" import { useRouteQuery } from "@vueuse/router"
import { getAuthors, getProblemList, getRandomProblemID } from "oj/api"
import { getTagColor } from "utils/functions" import { getTagColor } from "utils/functions"
import { ProblemFiltered } from "utils/types" import { ProblemFiltered } from "utils/types"
import { getProblemTagList } from "~/shared/api" import { getProblemTagList } from "~/shared/api"
@@ -23,6 +24,7 @@ interface ProblemQuery {
keyword: string keyword: string
difficulty: string difficulty: string
tag: string tag: string
author: string
} }
const difficultyOptions = [ const difficultyOptions = [
@@ -32,6 +34,8 @@ const difficultyOptions = [
{ label: "困难", value: "High" }, { label: "困难", value: "High" },
] ]
const authorOptions = ref([{label: "全部", value: ""}])
const router = useRouter() const router = useRouter()
const userStore = useUserStore() const userStore = useUserStore()
@@ -42,11 +46,22 @@ const [showTag, toggleShowTag] = useToggle(isDesktop.value)
// 使用分页 composable // 使用分页 composable
const { query, clearQuery } = usePagination<ProblemQuery>({ const { query, clearQuery } = usePagination<ProblemQuery>({
keyword: "", keyword: useRouteQuery("keyword", "").value,
difficulty: "", difficulty: useRouteQuery("difficulty", "").value,
tag: "", tag: useRouteQuery("tag", "").value,
author: useRouteQuery("author", "").value,
}) })
async function getAuthorOptions() {
authorOptions.value = [{label: "全部", value: ""}]
const res = await getAuthors()
const remotes = res.data.map((item: {username: string, problem_count: number}) => ({
label: `${item.username} (${item.problem_count})`,
value: item.username,
}))
authorOptions.value = [...authorOptions.value, ...remotes]
}
async function listProblems() { async function listProblems() {
if (query.page < 1) query.page = 1 if (query.page < 1) query.page = 1
const offset = (query.page - 1) * query.limit const offset = (query.page - 1) * query.limit
@@ -54,6 +69,7 @@ async function listProblems() {
keyword: query.keyword, keyword: query.keyword,
tag: query.tag, tag: query.tag,
difficulty: query.difficulty, difficulty: query.difficulty,
author: query.author,
}) })
total.value = res.total total.value = res.total
problems.value = res.results problems.value = res.results
@@ -67,11 +83,6 @@ async function listTags() {
})) }))
} }
function search(value: string) {
query.keyword = value
}
function chooseTag(tag: Tag) { function chooseTag(tag: Tag) {
query.tag = tag.checked ? "" : tag.name query.tag = tag.checked ? "" : tag.name
tags.value = tags.value.map((t) => { tags.value = tags.value.map((t) => {
@@ -84,26 +95,21 @@ function chooseTag(tag: Tag) {
}) })
} }
function clear() { async function getRandom() {
clearQuery() const res = await getRandomProblemID()
router.push("/problem/" + res.data)
} }
// async function getRandom() {
// const res = await getRandomProblemID()
// router.push("/problem/" + res.data)
// }
// 监听搜索关键词变化(防抖) // 监听搜索关键词变化(防抖)
watchDebounced( watchDebounced(() => query.keyword, listProblems, {
() => query.keyword, debounce: 500,
listProblems, maxWait: 1000,
{ debounce: 500, maxWait: 1000 } })
)
// 监听其他查询条件变化 // 监听其他查询条件变化
watch( watch(
() => [query.tag, query.difficulty, query.limit, query.page], () => [query.tag, query.difficulty, query.limit, query.page, query.author],
listProblems listProblems,
) )
// 监听标签变化,更新标签选中状态 // 监听标签变化,更新标签选中状态
@@ -117,14 +123,13 @@ watch(
}, },
) )
// 监听用户认证状态变化,只在认证完成且已登录时刷新问题列表
watch( watch(
() => userStore.isFinished && userStore.isAuthed, () => userStore.isFinished && userStore.isAuthed,
(isAuthenticatedAndFinished) => { (isAuthenticatedAndFinished) => {
if (isAuthenticatedAndFinished) { if (isAuthenticatedAndFinished) {
listProblems() listProblems()
} }
} },
) )
onMounted(() => { onMounted(() => {
@@ -212,12 +217,22 @@ function rowProps(row: ProblemFiltered) {
:options="difficultyOptions" :options="difficultyOptions"
/> />
</n-form-item> </n-form-item>
<n-form-item label="出题者">
<n-select
style="width: 160px"
v-model:value="query.author"
remote
@update:show="getAuthorOptions"
@update:value="(val) => (query.author = val)"
:options="authorOptions"
/>
</n-form-item>
<n-form-item> <n-form-item>
<n-input <n-input
clearable clearable
style="width: 200px" style="width: 200px"
v-model:value="query.keyword" v-model:value="query.keyword"
placeholder="号或者标题" placeholder="号或者标题"
/> />
</n-form-item> </n-form-item>
</n-form> </n-form>
@@ -226,9 +241,8 @@ function rowProps(row: ProblemFiltered) {
<n-form :show-feedback="false" inline label-placement="left"> <n-form :show-feedback="false" inline label-placement="left">
<n-form-item> <n-form-item>
<n-flex align="center"> <n-flex align="center">
<n-button @click="search(query.keyword)">搜索</n-button> <n-button @click="clearQuery" quaternary>重置</n-button>
<n-button @click="clear" quaternary>重置</n-button> <n-button @click="getRandom" quaternary>试试手气</n-button>
<!-- <n-button @click="getRandom" quaternary>试试手气</n-button> -->
</n-flex> </n-flex>
</n-form-item> </n-form-item>
</n-form> </n-form>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { NButton, NH2, NText } from "naive-ui" import { NButton, NH2, NText } from "naive-ui"
import { useRouteQuery } from "@vueuse/router"
import { adminRejudge, getSubmissions, getTodaySubmissionCount } from "oj/api" import { adminRejudge, getSubmissions, getTodaySubmissionCount } from "oj/api"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import { LANGUAGE, SubmissionListItem } from "utils/types" import { LANGUAGE, SubmissionListItem } from "utils/types"
@@ -18,7 +19,7 @@ import SubmissionDetail from "./detail.vue"
interface SubmissionQuery { interface SubmissionQuery {
username: string username: string
result: string result: string
myself: boolean myself: "0" | "1"
problem: string problem: string
language: LANGUAGE | "" language: LANGUAGE | ""
} }
@@ -34,11 +35,11 @@ const todayCount = ref(0)
// 使用分页 composable // 使用分页 composable
const { query, clearQuery } = usePagination<SubmissionQuery>({ const { query, clearQuery } = usePagination<SubmissionQuery>({
username: "", username: useRouteQuery("username", "").value,
result: "", result: useRouteQuery("result", "").value,
myself: false, myself: useRouteQuery("myself", "0").value,
problem: "", problem: useRouteQuery("problem", "").value,
language: "", language: useRouteQuery("language", "").value,
}) })
const submissionID = ref("") const submissionID = ref("")
const problemDisplayID = ref("") const problemDisplayID = ref("")
@@ -65,7 +66,6 @@ async function listSubmissions() {
try { try {
const res = await getSubmissions({ const res = await getSubmissions({
...query, ...query,
myself: query.myself ? "1" : "0",
offset, offset,
problem_id: query.problem, problem_id: query.problem,
contest_id: <string>route.params.contestID ?? "", contest_id: <string>route.params.contestID ?? "",
@@ -92,7 +92,6 @@ onMounted(() => {
} }
}) })
function search(username: string, problem: string) { function search(username: string, problem: string) {
query.username = username query.username = username
query.problem = problem query.problem = problem
@@ -129,11 +128,10 @@ function showCodePanel(id: string, problem: string) {
} }
// 监听用户名和题号变化(防抖) // 监听用户名和题号变化(防抖)
watchDebounced( watchDebounced(() => [query.username, query.problem], listSubmissions, {
() => [query.username, query.problem], debounce: 500,
listSubmissions, maxWait: 1000,
{ debounce: 500, maxWait: 1000 }, })
)
// 监听其他查询条件变化 // 监听其他查询条件变化
watch( watch(
@@ -272,7 +270,11 @@ const columns = computed(() => {
/> />
</n-form-item> </n-form-item>
<n-form-item v-if="userStore.isAuthed" label="只看自己"> <n-form-item v-if="userStore.isAuthed" label="只看自己">
<n-switch v-model:value="query.myself" /> <n-switch
v-model:value="query.myself"
checked-value="1"
unchecked-value="0"
/>
</n-form-item> </n-form-item>
</n-form> </n-form>
<n-form :show-feedback="false" inline label-placement="left"> <n-form :show-feedback="false" inline label-placement="left">

View File

@@ -15,9 +15,9 @@ async function receive() {
onMounted(receive) onMounted(receive)
</script> </script>
<template> <template>
<div class="hitokoto" @click="receive"> <div class="hitokoto" @click="receive" v-if="hitokoto.sentence">
<div class="sentence">{{ hitokoto.sentence }}</div> <div class="sentence">{{ hitokoto.sentence }}</div>
<div class="from">{{ "from " + hitokoto.from }}</div> <div class="from">{{ "来自 " + hitokoto.from }}</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>