fix UI
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
2025-09-29 21:19:57 +08:00
parent e291a194a9
commit ad7ea92769
11 changed files with 874 additions and 200 deletions

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import { NSwitch } from "naive-ui"
import Pagination from "~/shared/components/Pagination.vue"
import { parseTime, filterEmptyValue } from "~/utils/functions"
import { usePagination } from "~/shared/composables/pagination"
import { parseTime } from "~/utils/functions"
import { AdminProblemFiltered } from "~/utils/types"
import { getProblemList, toggleProblemVisible } from "../api"
import Actions from "./components/Actions.vue"
@@ -30,10 +31,14 @@ const [show, toggleShow] = useToggle()
const { count, inc } = useCounter(0)
const total = ref(0)
const problems = ref<AdminProblemFiltered[]>([])
const query = reactive({
limit: parseInt(<string>route.query.limit) || 10,
page: parseInt(<string>route.query.page) || 1,
keyword: <string>route.query.keyword ?? "",
interface ProblemQuery {
keyword: string
}
// 使用分页 composable
const { query, clearQuery } = usePagination<ProblemQuery>({
keyword: "",
})
const columns: DataTableColumn<AdminProblemFiltered>[] = [
@@ -71,18 +76,8 @@ const columns: DataTableColumn<AdminProblemFiltered>[] = [
},
]
function routerPush() {
router.push({
path: route.path,
query: filterEmptyValue(query),
})
}
async function listProblems() {
query.keyword = <string>route.query.keyword ?? ""
query.page = parseInt(<string>route.query.page) || 1
query.limit = parseInt(<string>route.query.limit) || 10
if (query.page < 1) query.page = 1
const offset = (query.page - 1) * query.limit
const res = await getProblemList(
@@ -118,27 +113,18 @@ async function selectProblems() {
}
onMounted(listProblems)
watch(() => query.page, routerPush)
watch(
() => [query.limit, query.keyword],
() => {
query.page = 1
routerPush()
},
)
// 监听搜索关键词变化(防抖)
watchDebounced(
() => query.keyword,
() => {
query.page = 1
routerPush()
},
listProblems,
{ debounce: 500, maxWait: 1000 },
)
// 监听其他查询条件变化
watch(
() => route.name === "admin problem list" && route.query,
(newVal) => {
if (newVal) listProblems()
},
() => [query.page, query.limit],
listProblems,
)
</script>

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import { DataTableRowKey, SelectOption } from "naive-ui"
import Pagination from "~/shared/components/Pagination.vue"
import { parseTime, filterEmptyValue } from "~/utils/functions"
import { usePagination } from "~/shared/composables/pagination"
import { parseTime } from "~/utils/functions"
import { User } from "~/utils/types"
import {
deleteUsers,
@@ -15,18 +16,21 @@ import Name from "./components/Name.vue"
import { PROBLEM_PERMISSION, USER_TYPE } from "~/utils/constants"
const message = useMessage()
const router = useRouter()
const route = useRoute()
interface UserQuery {
keyword: string
type: string
}
// 使用分页 composable
const { query, clearQuery } = usePagination<UserQuery>({
keyword: "",
type: "",
})
const total = ref(0)
const users = ref<User[]>([])
const userEditing = ref<User | null>(null)
const query = reactive({
limit: 10,
page: 1,
keyword: "",
type: "",
})
const adminOptions = [
{ label: "全部用户", value: "" },
@@ -97,19 +101,8 @@ const problemPermissionOptions: SelectOption[] = [
{ label: "管理全部题目", value: PROBLEM_PERMISSION.ALL },
]
function routerPush() {
router.push({
path: route.path,
query: filterEmptyValue(query),
})
}
async function listUsers() {
query.keyword = <string>route.query.keyword ?? ""
query.page = parseInt(<string>route.query.page) || 1
query.limit = parseInt(<string>route.query.limit) || 10
query.type = <string>route.query.type ?? ""
if (query.page < 1) query.page = 1
const offset = (query.page - 1) * query.limit
const res = await getUserList(offset, query.limit, query.type, query.keyword)
@@ -203,27 +196,18 @@ async function handleEditUser() {
}
onMounted(listUsers)
watch(() => query.page, routerPush)
watch(
() => [query.limit, query.keyword, query.type],
() => {
query.page = 1
routerPush()
},
)
// 监听搜索关键词变化(防抖)
watchDebounced(
() => query.keyword,
() => {
query.page = 1
routerPush()
},
listUsers,
{ debounce: 500, maxWait: 1000 },
)
// 监听其他查询条件变化
watch(
() => route.name === "admin user list" && route.query,
(newVal) => {
if (newVal) listUsers()
},
() => [query.page, query.limit, query.type],
listUsers,
)
</script>

View File

@@ -1,25 +1,32 @@
<script setup lang="ts">
import { NTag } from "naive-ui"
import { getContestList } from "oj/api"
import { duration, filterEmptyValue, parseTime } from "utils/functions"
import { duration, parseTime } from "utils/functions"
import { Contest } from "utils/types"
import ContestTitle from "~/shared/components/ContestTitle.vue"
import Pagination from "~/shared/components/Pagination.vue"
import { toggleLogin } from "~/shared/composables/modal"
import { usePagination } from "~/shared/composables/pagination"
import { useUserStore } from "~/shared/store/user"
import { CONTEST_STATUS, ContestType } from "~/utils/constants"
import { renderTableTitle } from "~/utils/renders"
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const query = reactive({
page: parseInt(<string>route.query.page) || 1,
limit: parseInt(<string>route.query.limit) || 10,
keyword: <string>route.query.keyword ?? "",
status: <string>route.query.status ?? "",
tag: <string>route.query.tag ?? "",
interface ContestQuery {
keyword: string
status: string
tag: string
}
// 使用分页 composable
const { query, clearQuery } = usePagination<ContestQuery>({
keyword: "",
status: "",
tag: "",
})
const data = ref<Contest[]>([])
const total = ref(0)
@@ -88,45 +95,28 @@ async function listContests() {
total.value = res.data.total
}
function routerPush() {
router.push({
path: route.path,
query: filterEmptyValue(query),
})
}
function search(value: string) {
query.keyword = value
}
function clear() {
query.keyword = ""
query.status = ""
query.tag = ""
clearQuery()
}
onMounted(listContests)
watch(() => query.page, routerPush)
watch(
() => [query.limit, query.status, query.tag],
() => {
query.page = 1
routerPush()
},
)
// 监听搜索关键词变化(防抖)
watchDebounced(
() => query.keyword,
() => {
query.page = 1
routerPush()
},
listContests,
{ debounce: 500, maxWait: 1000 },
)
// 监听其他查询条件变化
watch(
() => route.name === "contests" && route.query,
(newVal) => {
if (newVal) listContests()
},
() => [query.page, query.limit, query.status, query.tag],
listContests,
)
function rowProps(row: Contest) {

View File

@@ -2,12 +2,13 @@
import { Icon } from "@iconify/vue"
import { NSpace, NTag } from "naive-ui"
import { getProblemList } from "oj/api"
import { filterEmptyValue, getTagColor } from "utils/functions"
import { getTagColor } from "utils/functions"
import { ProblemFiltered } from "utils/types"
import { getProblemTagList } from "~/shared/api"
import Hitokoto from "~/shared/components/Hitokoto.vue"
import Pagination from "~/shared/components/Pagination.vue"
import { isDesktop } from "~/shared/composables/breakpoints"
import { usePagination } from "~/shared/composables/pagination"
import { useUserStore } from "~/shared/store/user"
import { renderTableTitle } from "~/utils/renders"
import ProblemStatus from "./components/ProblemStatus.vue"
@@ -18,12 +19,10 @@ interface Tag {
checked: boolean
}
interface Query {
interface ProblemQuery {
keyword: string
difficulty: string
tag: string
page: number
limit: number
}
const difficultyOptions = [
@@ -34,7 +33,6 @@ const difficultyOptions = [
]
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const problems = ref<ProblemFiltered[]>([])
@@ -42,21 +40,14 @@ const total = ref(0)
const tags = ref<Tag[]>([])
const [showTag, toggleShowTag] = useToggle(isDesktop.value)
const query = reactive<Query>({
keyword: <string>route.query.keyword ?? "",
difficulty: <string>route.query.difficulty ?? "",
tag: <string>route.query.tag ?? "",
page: parseInt(<string>route.query.page) || 1,
limit: parseInt(<string>route.query.limit) || 10,
// 使用分页 composable
const { query, clearQuery } = usePagination<ProblemQuery>({
keyword: "",
difficulty: "",
tag: "",
})
async function listProblems() {
query.keyword = <string>route.query.keyword ?? ""
query.difficulty = <string>route.query.difficulty ?? ""
query.tag = <string>route.query.tag ?? ""
query.page = parseInt(<string>route.query.page) || 1
query.limit = parseInt(<string>route.query.limit) || 10
if (query.page < 1) query.page = 1
const offset = (query.page - 1) * query.limit
const res = await getProblemList(offset, query.limit, {
@@ -76,12 +67,6 @@ async function listTags() {
}))
}
function routerPush() {
router.push({
path: route.path,
query: filterEmptyValue(query),
})
}
function search(value: string) {
query.keyword = value
@@ -100,9 +85,7 @@ function chooseTag(tag: Tag) {
}
function clear() {
query.keyword = ""
query.tag = ""
query.difficulty = ""
clearQuery()
}
// async function getRandom() {
@@ -110,22 +93,20 @@ function clear() {
// router.push("/problem/" + res.data)
// }
watch(() => query.page, routerPush)
watch(
() => [query.tag, query.difficulty, query.limit],
() => {
query.page = 1
routerPush()
},
)
// 监听搜索关键词变化(防抖)
watchDebounced(
() => query.keyword,
() => {
query.page = 1
routerPush()
},
{ debounce: 500, maxWait: 1000 },
listProblems,
{ debounce: 500, maxWait: 1000 }
)
// 监听其他查询条件变化
watch(
() => [query.tag, query.difficulty, query.limit, query.page],
listProblems
)
// 监听标签变化,更新标签选中状态
watch(
() => query.tag,
() => {
@@ -135,15 +116,16 @@ watch(
}))
},
)
watch(
() => route.path === "/" && route.query,
(newVal) => {
if (newVal) listProblems()
},
)
// TODO: 这里会在登录时候执行两次有BUG
watch(() => userStore.isFinished && userStore.isAuthed, listProblems)
// 监听用户认证状态变化,只在认证完成且已登录时刷新问题列表
watch(
() => userStore.isFinished && userStore.isAuthed,
(isAuthenticatedAndFinished) => {
if (isAuthenticatedAndFinished) {
listProblems()
}
}
)
onMounted(() => {
listProblems()

View File

@@ -1,11 +1,12 @@
<script setup lang="ts">
import { NButton, NH2, NText } from "naive-ui"
import { adminRejudge, getSubmissions, getTodaySubmissionCount } from "oj/api"
import { filterEmptyValue, parseTime } from "utils/functions"
import { parseTime } from "utils/functions"
import { LANGUAGE, SubmissionListItem } from "utils/types"
import Pagination from "~/shared/components/Pagination.vue"
import SubmissionResultTag from "~/shared/components/SubmissionResultTag.vue"
import { isDesktop } from "~/shared/composables/breakpoints"
import { usePagination } from "~/shared/composables/pagination"
import { useUserStore } from "~/shared/store/user"
import { LANGUAGE_SHOW_VALUE } from "~/utils/constants"
import { renderTableTitle } from "~/utils/renders"
@@ -14,11 +15,9 @@ import StatisticsPanel from "./components/StatisticsPanel.vue"
import SubmissionLink from "./components/SubmissionLink.vue"
import SubmissionDetail from "./detail.vue"
interface Query {
interface SubmissionQuery {
username: string
result: string
limit: number
page: number
myself: boolean
problem: string
language: LANGUAGE | ""
@@ -32,14 +31,14 @@ const message = useMessage()
const submissions = ref<SubmissionListItem[]>([])
const total = ref(0)
const todayCount = ref(0)
const query = reactive<Query>({
result: <string>route.query.result ?? "",
page: parseInt(<string>route.query.page) || 1,
limit: parseInt(<string>route.query.limit) || 10,
username: <string>route.query.username ?? "",
myself: route.query.myself === "1",
problem: <string>route.query.problem ?? "",
language: <LANGUAGE | "">route.query.language ?? "",
// 使用分页 composable
const { query, clearQuery } = usePagination<SubmissionQuery>({
username: "",
result: "",
myself: false,
problem: "",
language: "",
})
const submissionID = ref("")
const problemDisplayID = ref("")
@@ -61,14 +60,6 @@ const languageOptions: SelectOption[] = [
{ label: "C++", value: "C++" },
]
async function listSubmissions() {
query.result = <string>route.query.result ?? ""
query.page = parseInt(<string>route.query.page) || 1
query.limit = parseInt(<string>route.query.limit) || 10
query.username = <string>route.query.username ?? ""
query.myself = route.query.myself === "1"
query.problem = <string>route.query.problem ?? ""
query.language = <LANGUAGE>route.query.language ?? ""
if (query.page < 1) query.page = 1
const offset = query.limit * (query.page - 1)
try {
@@ -76,7 +67,7 @@ async function listSubmissions() {
...query,
myself: query.myself ? "1" : "0",
offset,
problem_id: <string>route.query.problem ?? "",
problem_id: query.problem,
contest_id: <string>route.params.contestID ?? "",
language: query.language,
})
@@ -101,16 +92,6 @@ onMounted(() => {
}
})
function routerPush() {
const newQuery = {
...query,
myself: query.myself ? "1" : "0",
}
router.push({
path: route.path,
query: filterEmptyValue(newQuery),
})
}
function search(username: string, problem: string) {
query.username = username
@@ -118,11 +99,7 @@ function search(username: string, problem: string) {
}
function clear() {
query.username = ""
query.myself = false
query.result = ""
query.problem = ""
query.language = ""
clearQuery()
}
async function rejudge(submissionID: string) {
@@ -151,25 +128,19 @@ function showCodePanel(id: string, problem: string) {
problemDisplayID.value = problem
}
watch(() => query.page, routerPush)
watch(
() => [query.limit, query.myself, query.result, query.language],
() => {
query.page = 1
routerPush()
},
)
// 监听用户名和题号变化(防抖)
watchDebounced(
() => [query.username, query.problem],
() => {
query.page = 1
routerPush()
},
listSubmissions,
{ debounce: 500, maxWait: 1000 },
)
// 监听其他查询条件变化
watch(
() => [query.page, query.limit, query.myself, query.result, query.language],
listSubmissions,
)
watch(
() =>
(route.name === "submissions" || route.name === "contest submissions") &&

View File

@@ -0,0 +1,150 @@
import { filterEmptyValue } from "~/utils/functions"
export interface PaginationQuery {
page: number
limit: number
[key: string]: any
}
export interface UsePaginationOptions {
/** 默认每页条数 */
defaultLimit?: number
/** 默认页码 */
defaultPage?: number
/** 当其他查询条件变化时是否重置页码 */
resetPageOnChange?: boolean
}
/**
* 分页相关的 composable处理分页状态和 URL 同步
* @param initialQuery 初始查询参数对象
* @param options 配置选项
*/
export function usePagination<T extends Record<string, any>>(
initialQuery: Omit<T, "page" | "limit"> = {} as Omit<T, "page" | "limit">,
options: UsePaginationOptions = {},
) {
const {
defaultLimit = 10,
defaultPage = 1,
resetPageOnChange = true,
} = options
const route = useRoute()
const router = useRouter()
// 从 URL 查询参数初始化状态
const query = reactive({
page: parseInt(<string>route.query.page) || defaultPage,
limit: parseInt(<string>route.query.limit) || defaultLimit,
...initialQuery,
}) as unknown as T & PaginationQuery
// 同步 URL 查询参数到本地状态
function syncFromRoute() {
;(query as any).page = parseInt(<string>route.query.page) || defaultPage
;(query as any).limit = parseInt(<string>route.query.limit) || defaultLimit
// 同步其他查询参数
Object.keys(initialQuery).forEach((key) => {
const value = route.query[key]
if (value !== undefined) {
// 处理不同类型的参数
if (typeof initialQuery[key] === "boolean") {
;(query as any)[key] = value === "1" || value === "true"
} else if (typeof initialQuery[key] === "number") {
;(query as any)[key] = parseInt(<string>value) || initialQuery[key]
} else {
;(query as any)[key] = <string>value || initialQuery[key]
}
}
})
}
// 更新 URL
function updateRoute() {
const newQuery = filterEmptyValue(query)
router.push({
path: route.path,
query: newQuery,
})
}
// 重置页码到第一页
function resetPage() {
;(query as any).page = defaultPage
}
// 清空所有查询条件(除了分页参数)
function clearQuery() {
Object.keys(initialQuery).forEach((key) => {
const initialValue = initialQuery[key]
if (typeof initialValue === "string") {
;(query as any)[key] = ""
} else if (typeof initialValue === "boolean") {
;(query as any)[key] = false
} else if (typeof initialValue === "number") {
;(query as any)[key] = 0
} else {
;(query as any)[key] = initialValue
}
})
resetPage()
}
// 监听页码变化,同步到 URL
watch(() => query.page, updateRoute)
// 监听每页条数变化,重置页码并同步到 URL
watch(
() => query.limit,
() => {
if (resetPageOnChange) {
resetPage()
}
updateRoute()
},
)
// 监听其他查询条件变化,重置页码并同步到 URL
if (resetPageOnChange && Object.keys(initialQuery).length > 0) {
const otherQueryKeys = Object.keys(initialQuery)
watch(
() => otherQueryKeys.map((key) => query[key]),
() => {
resetPage()
updateRoute()
},
{ deep: true },
)
}
// 监听路由变化,同步到本地状态
watch(
() => route.query,
() => {
syncFromRoute()
},
{ deep: true },
)
return {
query,
updateRoute,
resetPage,
clearQuery,
syncFromRoute,
}
}
/**
* 简化版本的分页 composable只处理基本的分页逻辑
* @param defaultLimit 默认每页条数
* @param defaultPage 默认页码
*/
export function useSimplePagination(defaultLimit = 10, defaultPage = 1) {
return usePagination(
{},
{ defaultLimit, defaultPage, resetPageOnChange: false },
)
}

View File

@@ -1,4 +1,6 @@
import axios from "axios"
import storage from "./storage"
import { STORAGE_KEY } from "./constants"
const http = axios.create({
baseURL: "/api",
@@ -9,8 +11,9 @@ const http = axios.create({
http.interceptors.response.use(
(res) => {
if (res.data.error) {
// // TODO: 若后端返回为登录则为session失效应退出当前登录用户
if (res.data.data.startsWith("Please login")) {
if (res.data.data && res.data.data.startsWith("Please login")) {
storage.remove(STORAGE_KEY.AUTHED)
window.location.reload()
}
return Promise.reject(res.data)
} else {

View File

@@ -247,7 +247,7 @@ export interface SubmissionListPayload {
username?: string
contest_id?: string
problem_id?: string
language?: LANGUAGE
language: LANGUAGE | ""
page: number
limit: number
offset: number