更新首页列表

This commit is contained in:
2023-11-01 19:03:01 +08:00
parent 9cf73331ea
commit 710f4b5eb9
35 changed files with 261 additions and 241 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "oj-next",
"version": "0.0.1",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "oj-next",
"version": "0.0.1",
"version": "1.0.0",
"dependencies": {
"@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-python": "^6.1.3",

View File

@@ -1,7 +1,6 @@
{
"name": "oj-next",
"private": true,
"version": "0.0.1",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "vite",

View File

@@ -19,7 +19,7 @@ export async function getProblemList(
limit = 10,
keyword: string,
contestID?: string,
ruleType?: "ACM" | "OI"
ruleType?: "ACM" | "OI",
) {
const endpoint = !!contestID ? "admin/contest/problem" : "admin/problem"
const res = await http.get(endpoint, {
@@ -143,7 +143,7 @@ export function getContest(id: string) {
export function addProblemForContest(
contestID: string,
problemID: number,
displayID: string
displayID: string,
) {
return http.post("admin/contest/add_problem_from_public", {
contest_id: contestID,

View File

@@ -40,7 +40,7 @@ const columns: DataTableColumn<Contest>[] = [
h(
NTag,
{ type: CONTEST_STATUS[row.status]["type"], size: "small" },
() => CONTEST_STATUS[row.status]["name"]
() => CONTEST_STATUS[row.status]["name"],
),
},
{

View File

@@ -18,7 +18,7 @@ async function addProblem() {
await addProblemForContest(
props.contestID,
props.problemID,
displayID.value
displayID.value,
)
emit("added")
} catch (err: any) {

View File

@@ -47,7 +47,7 @@ async function getList() {
query.limit,
query.keyword,
"",
"ACM"
"ACM",
)
total.value = res.total
problems.value = res.results
@@ -56,7 +56,7 @@ watch(
() => props.show,
(value) => {
if (value) getList()
}
},
)
watch(query, getList, { deep: true })
</script>

View File

@@ -33,7 +33,7 @@ const title = computed(
"admin problem edit": "编辑题目",
"admin contest problem create": "新建比赛题目",
"admin contest problem edit": "编辑比赛题目",
}[<string>route.name])
})[<string>route.name],
)
const problem = reactive<BlankProblem>({
_id: "",
@@ -91,7 +91,7 @@ const languageOptions = [
]
const tagOptions = computed(() =>
existingTags.value.map((tag) => ({ label: tag.name, value: tag.name }))
existingTags.value.map((tag) => ({ label: tag.name, value: tag.name })),
)
async function getProblemDetail() {
@@ -233,7 +233,7 @@ function detectProblemCompletion() {
// 样例是空的
else if (
problem.samples.some(
(sample) => sample.output === "" || sample.input === ""
(sample) => sample.output === "" || sample.input === "",
)
) {
message.error("空样例没有删干净")

View File

@@ -20,10 +20,10 @@ const title = computed(
({
"admin problem list": "题目列表",
"admin contest problem list": "比赛题目列表",
}[<string>route.name])
})[<string>route.name],
)
const isContestProblemList = computed(
() => route.name === "admin contest problem list"
() => route.name === "admin contest problem list",
)
const [show, toggleShow] = useToggle()
@@ -77,7 +77,7 @@ async function listProblems() {
offset,
query.limit,
query.keyword,
props.contestID
props.contestID,
)
total.value = res.total
problems.value = res.results

View File

@@ -28,7 +28,7 @@ const testcaseColumns: DataTableColumn<Testcase>[] = [
h(
NButton,
{ size: "small", onClick: () => deleteTestcase(row.id) },
() => "删除"
() => "删除",
),
},
]
@@ -48,7 +48,7 @@ const serverColumns: DataTableColumn<Server>[] = [
h(
NTag,
{ type: statusMap[row.status].color, size: "small" },
() => statusMap[row.status].label
() => statusMap[row.status].label,
),
},
{
@@ -63,7 +63,7 @@ const serverColumns: DataTableColumn<Server>[] = [
disabled: row.status === "normal",
onClick: () => delJudgeServer(row.hostname),
},
() => "删除"
() => "删除",
),
},
{ title: "主机", key: "hostname", width: 130 },
@@ -94,7 +94,7 @@ const testcases = ref<Testcase[]>([])
const token = ref("")
const servers = ref<Server[]>([])
const abnormalServers = computed(() =>
servers.value.filter((item) => item.status === "abnormal")
servers.value.filter((item) => item.status === "abnormal"),
)
const websiteConfig = reactive({
@@ -144,7 +144,7 @@ async function delJudgeServer(hostname: string) {
async function deleteAbnormalServers() {
const dels = abnormalServers.value.map((item) =>
deleteJudgeServer(item.hostname)
deleteJudgeServer(item.hostname),
)
await Promise.all(dels)
message.success("删除成功")

1
src/components.d.ts vendored
View File

@@ -24,6 +24,7 @@ declare module 'vue' {
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
NCode: typeof import('naive-ui')['NCode']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDataTable: typeof import('naive-ui')['NDataTable']
NDatePicker: typeof import('naive-ui')['NDatePicker']

View File

@@ -10,7 +10,7 @@ const router = useRouter()
const learnStore = useLearnStore()
const Mds = Array.from({ length: learnStore.total }, (_, i) => i + 1).map((v) =>
defineAsyncComponent(() => import(`./step-${v}/index.md`))
defineAsyncComponent(() => import(`./step-${v}/index.md`)),
)
watch(
@@ -24,7 +24,7 @@ watch(
router.replace("/learn/step-1")
}
},
{ immediate: true }
{ immediate: true },
)
</script>

View File

@@ -36,7 +36,7 @@ export function getWebsiteConfig() {
export async function getProblemList(
offset = 0,
limit = 10,
searchParams: any = {}
searchParams: any = {},
) {
let params: any = {
paging: true,
@@ -129,7 +129,7 @@ export async function getContestProblems(contestID: string) {
export function getContestRank(
contestID: string,
query: { limit: number; offset: number; force_refresh: "1" | "0" }
query: { limit: number; offset: number; force_refresh: "1" | "0" },
) {
return http.get("contest_rank", {
params: {

View File

@@ -24,7 +24,7 @@ const passwordFormVisible = computed(
() =>
contestStore.isPrivate &&
!contestStore.access &&
!contestStore.isContestAdmin
!contestStore.isContestAdmin,
)
</script>

View File

@@ -37,7 +37,7 @@ const columns: DataTableColumn<Contest>[] = [
h(
NTag,
{ type: CONTEST_STATUS[row.status]["type"] },
() => CONTEST_STATUS[row.status]["name"]
() => CONTEST_STATUS[row.status]["name"],
),
},
{
@@ -95,13 +95,13 @@ watch(
() => {
query.page = 1
routerPush()
}
},
)
watch(
() => route.name === "contests" && route.query,
(newVal) => {
if (newVal) listContests()
}
},
)
function rowProps(row: Contest) {

View File

@@ -33,7 +33,7 @@ const { resume, pause } = useIntervalFn(
10000,
{
immediate: false,
}
},
)
const query = reactive({
@@ -64,7 +64,7 @@ const columns = ref<DataTableColumn<ContestRank>[]>([
type: "info",
onClick: () => router.push("/user?name=" + row.user.username),
},
() => row.user.username
() => row.user.username,
),
},
{
@@ -119,7 +119,7 @@ async function addColumns() {
window.open(data.href, "_blank")
},
},
() => problem.title
() => problem.title,
),
render: (row) => {
if (row.submission_info[problem.id]) {
@@ -134,7 +134,7 @@ async function addColumns() {
h(
NIcon,
{ size: 16, style: "transform: translate(-2px, 3px)" },
() => h(GoldMedal)
() => h(GoldMedal),
),
secondsToDuration(status.ac_time),
]
@@ -143,7 +143,7 @@ async function addColumns() {
errorNumber = h(
"p",
{ style: "margin: 0" },
`(-${status.error_number})`
`(-${status.error_number})`,
)
}
return h("div", [acTime, errorNumber])
@@ -182,7 +182,7 @@ watch(
() => {
query.page = 1
listRanks()
}
},
)
watch(autoRefresh, (checked) => (checked ? resume() : pause()))

View File

@@ -13,7 +13,7 @@ const contestID = !!route.params.contestID ? route.params.contestID : null
const storageKey = computed(
() =>
`problem_${problem.value!._id}_contest_${contestID}_lang_${code.language}`
`problem_${problem.value!._id}_contest_${contestID}_lang_${code.language}`,
)
onMounted(() => {
@@ -26,7 +26,7 @@ onMounted(() => {
})
const editorHeight = computed(() =>
isDesktop.value ? "calc(100vh - 133px)" : "calc(100vh - 172px)"
isDesktop.value ? "calc(100vh - 133px)" : "calc(100vh - 172px)",
)
function changeCode(v: string) {

View File

@@ -23,7 +23,7 @@ const samples = ref<Sample[]>(
msg: "",
status: "not_test",
loading: false,
}))
})),
)
async function test(sample: Sample, index: number) {
@@ -202,7 +202,8 @@ function type(status: ProblemStatus) {
border: 1px solid rgb(239, 239, 245);
word-break: break-word;
box-sizing: border-box;
transition: background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
transition:
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
@@ -219,13 +220,15 @@ function type(status: ProblemStatus) {
width: 100%;
border-spacing: 0;
border: 1px solid rgba(239, 239, 245, 1);
transition: background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
transition:
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.problemContent > .content > table th {
background-color: rgba(250, 250, 252, 1);
transition: background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
transition:
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

View File

@@ -21,7 +21,7 @@ const columns: DataTableColumn<Submission>[] = [
render: (row) =>
parseTime(
row.create_time,
isDesktop ? "YYYY-MM-DD HH:mm:ss" : "M-D hh:mm"
isDesktop ? "YYYY-MM-DD HH:mm:ss" : "M-D hh:mm",
),
},
{
@@ -40,7 +40,7 @@ const columns: DataTableColumn<Submission>[] = [
window.open(data.href, "_blank")
},
},
() => row.id.slice(0, 12)
() => row.id.slice(0, 12),
)
} else {
return row.id.slice(0, 12)

View File

@@ -39,17 +39,21 @@ const { start: fetchSubmission } = useTimeoutFn(
}
},
2000,
{ immediate: false }
{ immediate: false },
)
const judging = computed(
() =>
!!(submission.value && submission.value.result === SubmissionStatus.judging)
!!(
submission.value && submission.value.result === SubmissionStatus.judging
),
)
const pending = computed(
() =>
!!(submission.value && submission.value.result === SubmissionStatus.pending)
!!(
submission.value && submission.value.result === SubmissionStatus.pending
),
)
const submitting = computed(
@@ -57,7 +61,7 @@ const submitting = computed(
!!(
submission.value &&
submission.value.result === SubmissionStatus.submitting
)
),
)
const submitDisabled = computed(() => {
@@ -186,7 +190,7 @@ watch(
size: party.variation.skew(2, 0.3),
})
}
}
},
)
</script>

View File

@@ -7,6 +7,7 @@ import Pagination from "~/shared/Pagination.vue"
import { NSpace, NTag } from "naive-ui"
import ProblemStatus from "./components/ProblemStatus.vue"
import { getProblemTagList } from "~/shared/api"
import { isDesktop } from "~/shared/composables/breakpoints"
interface Tag {
id: number
@@ -36,6 +37,7 @@ const userStore = useUserStore()
const problems = ref<ProblemFiltered[]>([])
const total = ref(0)
const tags = ref<Tag[]>([])
const [showTag, toggleShowTag] = useToggle(isDesktop.value)
const query = reactive<Query>({
keyword: <string>route.query.keyword ?? "",
@@ -112,14 +114,14 @@ watch(
() => {
query.page = 1
routerPush()
}
},
)
watch(
() => route.path === "/" && route.query,
(newVal) => {
if (newVal) listProblems()
}
},
)
// TODO: 这里会在登录时候执行两次有BUG
@@ -130,7 +132,7 @@ onMounted(() => {
listTags()
})
const columns: DataTableColumn<ProblemFiltered>[] = [
const baseColumns: DataTableColumn<ProblemFiltered>[] = [
{
title: "状态",
key: "status",
@@ -157,6 +159,12 @@ const columns: DataTableColumn<ProblemFiltered>[] = [
{ title: "通过率", key: "rate", width: 100 },
]
const columns = computed(() =>
userStore.isAuthed
? baseColumns
: baseColumns.filter((c: any) => c.key !== "status"),
)
function rowProps(row: ProblemFiltered) {
return {
style: "cursor: pointer",
@@ -191,9 +199,10 @@ function rowProps(row: ProblemFiltered) {
</n-space>
</n-form-item>
</n-form>
<n-button @click="toggleShowTag()" quaternary>标签</n-button>
</n-space>
<n-collapse-transition :show="showTag">
<n-space>
<div class="tagTitle">标签</div>
<n-button
@click="chooseTag(tag)"
v-for="tag in tags"
@@ -205,6 +214,7 @@ function rowProps(row: ProblemFiltered) {
{{ tag.name }}
</n-button>
</n-space>
</n-collapse-transition>
<n-data-table
striped
:data="problems"

View File

@@ -47,7 +47,7 @@ const columns: DataTableColumn<Rank>[] = [
type: "info",
onClick: () => router.push("/user?name=" + row.user.username),
},
() => row.user.username
() => row.user.username,
),
},
{ title: "自我介绍", key: "mood", minWidth: 200 },
@@ -67,7 +67,7 @@ watch(
() => {
query.page = 1
listRanks()
}
},
)
onMounted(listRanks)

View File

@@ -48,11 +48,11 @@ export const useContestStore = defineStore("contest", () => {
() =>
userStore.isSuperAdmin ||
(userStore.isAuthed &&
contest.value?.created_by.id === userStore.user!.id)
contest.value?.created_by.id === userStore.user!.id),
)
const isPrivate = computed(
() => contest.value!.contest_type === ContestType.private
() => contest.value!.contest_type === ContestType.private,
)
async function init(contestID: string) {

View File

@@ -102,7 +102,7 @@ watch(
() => {
query.page = 1
routerPush()
}
},
)
watch(
@@ -111,7 +111,7 @@ watch(
route.query,
(newVal) => {
if (newVal) listSubmissions()
}
},
)
const columns = computed(() => {
@@ -123,7 +123,7 @@ const columns = computed(() => {
render: (row) =>
parseTime(
row.create_time,
isDesktop ? "YYYY-MM-DD HH:mm:ss" : "M-D hh:mm"
isDesktop ? "YYYY-MM-DD HH:mm:ss" : "M-D hh:mm",
),
},
{
@@ -139,7 +139,7 @@ const columns = computed(() => {
type: "info",
onClick: () => router.push("/submission/" + row.id),
},
() => row.id.slice(0, 12)
() => row.id.slice(0, 12),
)
} else {
return row.id.slice(0, 12)
@@ -175,7 +175,7 @@ const columns = computed(() => {
}
},
},
() => row.problem
() => row.problem,
),
},
{
@@ -208,7 +208,7 @@ const columns = computed(() => {
type: "info",
onClick: () => router.push("/user?name=" + row.username),
},
() => row.username
() => row.username,
),
},
]
@@ -225,7 +225,7 @@ const columns = computed(() => {
type: "primary",
onClick: () => rejudge(row.id),
},
() => "重新判题"
() => "重新判题",
),
})
}

View File

@@ -39,7 +39,7 @@ watch(
() => props.modelValue,
(v) => {
code.value = v
}
},
)
const emit = defineEmits(["update:modelValue"])

View File

@@ -10,7 +10,7 @@ interface Props {
const props = defineProps<Props>()
const isPrivate = computed(
() => props.contest.contest_type === ContestType.private
() => props.contest.contest_type === ContestType.private,
)
</script>

View File

@@ -6,7 +6,10 @@ import { isDark, toggleDark } from "~/shared/composables/dark"
import { toggleLogin, toggleSignup } from "~/shared/composables/modal"
import { RouterLink } from "vue-router"
import { isDesktop, isMobile } from "~/shared/composables/breakpoints"
import { screenSwitchLabel, switchScreenMode } from "~/shared/composables/switchScreen"
import {
screenSwitchLabel,
switchScreenMode,
} from "~/shared/composables/switchScreen"
import { code } from "~/shared/composables/learn"
import { useLearnStore } from "~/learn/store"

View File

@@ -22,7 +22,7 @@ export function loadChart() {
Colors,
Title,
Tooltip,
Legend
Legend,
)
isLoaded.value = true
}

View File

@@ -26,7 +26,7 @@ const options: MenuOption[] = [
h(
RouterLink,
{ to: "/admin/problem/list" },
{ default: () => "题目列表" }
{ default: () => "题目列表" },
),
key: "admin problem list",
},
@@ -35,7 +35,7 @@ const options: MenuOption[] = [
h(
RouterLink,
{ to: "/admin/problem/create" },
{ default: () => "新建题目" }
{ default: () => "新建题目" },
),
key: "admin problem create",
},
@@ -50,7 +50,7 @@ const options: MenuOption[] = [
h(
RouterLink,
{ to: "/admin/user/generate" },
{ default: () => "批量生成" }
{ default: () => "批量生成" },
),
key: "admin user generate",
},
@@ -60,7 +60,7 @@ const options: MenuOption[] = [
h(
RouterLink,
{ to: "/admin/contest/list" },
{ default: () => "比赛列表" }
{ default: () => "比赛列表" },
),
key: "admin contest list",
},
@@ -69,7 +69,7 @@ const options: MenuOption[] = [
h(
RouterLink,
{ to: "/admin/contest/create" },
{ default: () => "新建比赛" }
{ default: () => "新建比赛" },
),
key: "admin contest create",
},
@@ -84,7 +84,7 @@ const options: MenuOption[] = [
h(
RouterLink,
{ to: "/admin/announcement" },
{ default: () => "公告配置" }
{ default: () => "公告配置" },
),
key: "admin announcement",
},

View File

@@ -11,13 +11,13 @@ export const useUserStore = defineStore("user", () => {
const isAdminRole = computed(
() =>
user.value?.admin_type === USER_TYPE.ADMIN ||
user.value?.admin_type === USER_TYPE.SUPER_ADMIN
user.value?.admin_type === USER_TYPE.SUPER_ADMIN,
)
const isSuperAdmin = computed(
() => user.value?.admin_type === USER_TYPE.SUPER_ADMIN
() => user.value?.admin_type === USER_TYPE.SUPER_ADMIN,
)
const hasProblemPermission = computed(
() => user.value?.problem_permission !== PROBLEM_PERMISSION.NONE
() => user.value?.problem_permission !== PROBLEM_PERMISSION.NONE,
)
async function getMyProfile() {

View File

@@ -100,7 +100,7 @@ export const createTheme = ({
},
{
dark: variant === "dark",
}
},
)
const highlightStyle = HighlightStyle.define(styles)

View File

@@ -92,7 +92,7 @@ const oneDarkTheme = EditorView.theme(
},
},
},
{ dark: true }
{ dark: true },
)
/// The highlighting style for code in the One Dark theme.

View File

@@ -12,7 +12,7 @@ async function download(url: string) {
const headers = res.headers
const link = document.createElement("a")
link.href = window.URL.createObjectURL(
new window.Blob([res.data], { type: headers["content-type"] })
new window.Blob([res.data], { type: headers["content-type"] }),
)
link.download = (headers["content-disposition"] || "").split("filename=")[1]
document.body.appendChild(link)

View File

@@ -18,7 +18,7 @@ export function filterEmptyValue(object: any) {
}
export function getTagColor(
tag: "Low" | "Mid" | "High" | "简单" | "中等" | "困难"
tag: "Low" | "Mid" | "High" | "简单" | "中等" | "困难",
) {
return <"success" | "info" | "error">{
Low: "success",
@@ -39,7 +39,7 @@ export function parseTime(utc: Date | string, format = "YYYY年M月D日") {
export function duration(
start: Date | string,
end: Date | string,
showSeconds = false
showSeconds = false,
): string {
const duration = intervalToDuration({
start: getTime(parseISO(start.toString())),
@@ -141,8 +141,8 @@ export function decode(bytes?: string) {
const latin = atob(bytes ?? "")
return new TextDecoder("utf-8").decode(
Uint8Array.from({ length: latin.length }, (_, index) =>
latin.charCodeAt(index)
)
latin.charCodeAt(index),
),
)
}

View File

@@ -19,7 +19,7 @@ http.interceptors.response.use(
},
(err) => {
return Promise.reject(err)
}
},
)
export default http