Compare commits
2
Commits
55bdf317f1
...
95dc2f24e6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95dc2f24e6 | ||
|
|
e511d82f3b |
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
import { useDark } from '@vueuse/core'
|
import { useDark } from 'shared/composables/dark'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 排名页的几种颜色(设计稿「排名重设计」G1)。用户说「颜色可以保留」:你绿、前一名蓝、
|
* 排名页的几种颜色(设计稿「排名重设计」G1)。用户说「颜色可以保留」:你绿、前一名蓝、
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useThemeVars } from 'naive-ui'
|
import { useThemeVars } from 'naive-ui'
|
||||||
import { useDark } from '@vueuse/core'
|
import { useDark } from 'shared/composables/dark'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 只读的代码区:行号 + 高亮 + 可选地标红一行(编译失败 / 运行时错误报了行号时)。
|
* 只读的代码区:行号 + 高亮 + 可选地标红一行(编译失败 / 运行时错误报了行号时)。
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import UserName from 'shared/components/UserName.vue'
|
|||||||
*/
|
*/
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
row: SubmissionListItem
|
row: SubmissionListItem
|
||||||
|
/** 按 ↑ ↓ 会落到的上一条、下一条:这条加载完顺手把它们也拉回来,换条时不用等网络 */
|
||||||
|
neighbors?: SubmissionListItem[]
|
||||||
/** 老师那套:重新判题、逐个测试点、复制两项收进「⋯」 */
|
/** 老师那套:重新判题、逐个测试点、复制两项收进「⋯」 */
|
||||||
teacher: boolean
|
teacher: boolean
|
||||||
/** 在比赛的提交列表里(不能重判,题目链接走比赛) */
|
/** 在比赛的提交列表里(不能重判,题目链接走比赛) */
|
||||||
@@ -61,7 +63,42 @@ const detail = ref<Submission | null>(null)
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const failed = ref(false)
|
const failed = ref(false)
|
||||||
|
|
||||||
const judging = computed(() => props.row.result === SubmissionStatus.pending || props.row.result === SubmissionStatus.judging)
|
function isJudging(row: SubmissionListItem) {
|
||||||
|
return row.result === SubmissionStatus.pending || row.result === SubmissionStatus.judging
|
||||||
|
}
|
||||||
|
const judging = computed(() => isJudging(props.row))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同一条只发一次请求:预取的还在路上时按到了它,接着等那一个,不再发第二个
|
||||||
|
*/
|
||||||
|
const inflight = new Map<string, Promise<Submission>>()
|
||||||
|
function fetchDetail(id: string) {
|
||||||
|
let request = inflight.get(id)
|
||||||
|
if (!request) {
|
||||||
|
request = getSubmission(id).finally(() => inflight.delete(id))
|
||||||
|
inflight.set(id, request)
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预取上一条、下一条。只在这一条落定之后才做(按住 ↓ 一路划过去的那些行不预取),
|
||||||
|
* 进缓存的口径和 load() 一样:还在判的不要,列表和详情的结果对不上也不要。
|
||||||
|
*
|
||||||
|
* 没预取的时候,换到没看过的一条要先把代码区卸掉、转圈、再整个重新挂上 ——
|
||||||
|
* 2026-11 实测(生产构建、CPU 降速 6 倍)每次卡 60–100ms;走缓存的只换内容,不卡。
|
||||||
|
*/
|
||||||
|
function prefetch() {
|
||||||
|
for (const row of props.neighbors ?? []) {
|
||||||
|
if (!row.showLink || isJudging(row) || cache.get(row.id)?.result === row.result) continue
|
||||||
|
fetchDetail(row.id).then(
|
||||||
|
(res) => {
|
||||||
|
if (res.result === row.result) cache.set(row.id, res)
|
||||||
|
},
|
||||||
|
() => {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 每次加载一个序号:同一条先按「判题中」拉了一次、判完又拉一次时,晚回来的旧请求不能盖掉新的
|
// 每次加载一个序号:同一条先按「判题中」拉了一次、判完又拉一次时,晚回来的旧请求不能盖掉新的
|
||||||
let loadSeq = 0
|
let loadSeq = 0
|
||||||
@@ -78,15 +115,17 @@ async function load() {
|
|||||||
const hit = cache.get(row.id)
|
const hit = cache.get(row.id)
|
||||||
if (hit && hit.result === row.result) {
|
if (hit && hit.result === row.result) {
|
||||||
detail.value = hit
|
detail.value = hit
|
||||||
|
prefetch()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getSubmission(row.id)
|
const res = await fetchDetail(row.id)
|
||||||
if (seq !== loadSeq) return
|
if (seq !== loadSeq) return
|
||||||
// 列表那边还在判、详情这边已经判完了(或反过来)都不缓存,等两边对上
|
// 列表那边还在判、详情这边已经判完了(或反过来)都不缓存,等两边对上
|
||||||
if (!judging.value && res.result === props.row.result) cache.set(row.id, res)
|
if (!judging.value && res.result === props.row.result) cache.set(row.id, res)
|
||||||
detail.value = res
|
detail.value = res
|
||||||
|
prefetch()
|
||||||
} catch {
|
} catch {
|
||||||
if (seq === loadSeq) failed.value = true
|
if (seq === loadSeq) failed.value = true
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -35,6 +35,13 @@ export function useRowCursor(options: {
|
|||||||
const index = navigable.value.findIndex((row) => row.id === selectedId.value)
|
const index = navigable.value.findIndex((row) => row.id === selectedId.value)
|
||||||
return index === -1 ? null : { index: index + 1, count: navigable.value.length }
|
return index === -1 ? null : { index: index + 1, count: navigable.value.length }
|
||||||
})
|
})
|
||||||
|
/** 按 ↑ ↓ 会落到的上一条、下一条(本页之内),右栏拿去预取 */
|
||||||
|
const neighbors = computed(() => {
|
||||||
|
const list = navigable.value
|
||||||
|
const index = list.findIndex((row) => row.id === selectedId.value)
|
||||||
|
if (index === -1) return []
|
||||||
|
return [list[index + 1], list[index - 1]].filter((row): row is Row => row !== undefined)
|
||||||
|
})
|
||||||
|
|
||||||
// 翻页是异步的,先记下「落地后停在哪一头」,新一页回来再选
|
// 翻页是异步的,先记下「落地后停在哪一头」,新一页回来再选
|
||||||
let pendingJump: 'first' | 'last' | null = null
|
let pendingJump: 'first' | 'last' | null = null
|
||||||
@@ -125,5 +132,5 @@ export function useRowCursor(options: {
|
|||||||
move(e.key === 'ArrowUp' ? -1 : 1)
|
move(e.key === 'ArrowUp' ? -1 : 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
return { selectedId, selectedRow, position, maxPage, select, move, settleSelection, forgetJump }
|
return { selectedId, selectedRow, position, neighbors, maxPage, select, move, settleSelection, forgetJump }
|
||||||
}
|
}
|
||||||
@@ -263,7 +263,7 @@ watch(
|
|||||||
|
|
||||||
// ==================== 选中与翻阅 ====================
|
// ==================== 选中与翻阅 ====================
|
||||||
|
|
||||||
const { selectedId, selectedRow, position, maxPage, select, move, settleSelection, forgetJump } = useRowCursor({
|
const { selectedId, selectedRow, position, neighbors, maxPage, select, move, settleSelection, forgetJump } = useRowCursor({
|
||||||
rows,
|
rows,
|
||||||
total,
|
total,
|
||||||
query,
|
query,
|
||||||
@@ -858,6 +858,7 @@ function dayBreak(index: number) {
|
|||||||
<SubmissionPane
|
<SubmissionPane
|
||||||
v-else
|
v-else
|
||||||
:row="selectedRow as SubmissionListItem"
|
:row="selectedRow as SubmissionListItem"
|
||||||
|
:neighbors="neighbors as SubmissionListItem[]"
|
||||||
:teacher="teacher"
|
:teacher="teacher"
|
||||||
:class-name="query.className"
|
:class-name="query.className"
|
||||||
:contest="inContest"
|
:contest="inContest"
|
||||||
@@ -893,6 +894,7 @@ function dayBreak(index: number) {
|
|||||||
<SubmissionPane
|
<SubmissionPane
|
||||||
v-else
|
v-else
|
||||||
:row="selectedRow as SubmissionListItem"
|
:row="selectedRow as SubmissionListItem"
|
||||||
|
:neighbors="neighbors as SubmissionListItem[]"
|
||||||
:teacher="teacher"
|
:teacher="teacher"
|
||||||
:class-name="query.className"
|
:class-name="query.className"
|
||||||
:contest="inContest"
|
:contest="inContest"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDark } from '@vueuse/core'
|
import { useDark } from 'shared/composables/dark'
|
||||||
import { rgba } from 'shared/composables/tone'
|
import { rgba } from 'shared/composables/tone'
|
||||||
import { displayName } from '@oj2/contract'
|
import { displayName } from '@oj2/contract'
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useDark } from '@vueuse/core'
|
import { useDark } from 'shared/composables/dark'
|
||||||
import { Chart as ChartJS } from 'chart.js'
|
import { Chart as ChartJS } from 'chart.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createGlobalState, useDark as useVueUseDark } from '@vueuse/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全站只有一个 `useDark()` 实例。**别再从 `@vueuse/core` 直接取 useDark** ——
|
||||||
|
* auto-import 已经把 `useDark` 指到这里(apps/web/vite.config.ts 的 priority)。
|
||||||
|
*
|
||||||
|
* VueUse 的 useDark 每个实例挂载时都要把 `<html>` 的 class 「同步」一遍,同步的写法是
|
||||||
|
* 往 `<head>` 插一段关掉过渡的 `<style>`、`getComputedStyle()` 逼浏览器当场重算整页样式、
|
||||||
|
* 再删掉。亮色模式下它那句「没什么要改就跳过」永远不成立(要摘的 `dark` 本来就不在,它也算
|
||||||
|
* 一次改动),所以**每个实例挂载一次就是一次整页样式重算**。
|
||||||
|
*
|
||||||
|
* 提交列表每行的结果胶囊(StatusPill → useTone)都调它:一页 30 行就是 30 次,翻页、
|
||||||
|
* 切筛选再来 30 次,右栏换一条又是三四次。2026-11 实测(生产构建、CPU 降速 6 倍)首屏
|
||||||
|
* 光这一处就占了 1.3 秒。全站共用一个之后只在第一次挂载时同步,后面的调用都是读同一个 ref。
|
||||||
|
*/
|
||||||
|
export const useDark = createGlobalState(() => useVueUseDark())
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useDark } from '@vueuse/core'
|
import { useDark } from 'shared/composables/dark'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 圆环从哪儿开始扩散。正常点击就用指针落点;键盘触发(Enter / 空格)时浏览器给的
|
* 圆环从哪儿开始扩散。正常点击就用指针落点;键盘触发(Enter / 空格)时浏览器给的
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useDark } from '@vueuse/core'
|
import { useDark } from 'shared/composables/dark'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { RARITY_TEXT_COLOR } from 'utils/constants'
|
import { RARITY_TEXT_COLOR } from 'utils/constants'
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useThemeVars } from 'naive-ui'
|
import { useThemeVars } from 'naive-ui'
|
||||||
import { useDark } from '@vueuse/core'
|
import { useDark } from 'shared/composables/dark'
|
||||||
|
|
||||||
export type Tone = 'success' | 'error' | 'warning' | 'info' | 'default'
|
export type Tone = 'success' | 'error' | 'warning' | 'info' | 'default'
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,11 @@ export default defineConfig(() => {
|
|||||||
'vue-router',
|
'vue-router',
|
||||||
'@vueuse/core',
|
'@vueuse/core',
|
||||||
'pinia',
|
'pinia',
|
||||||
|
// 盖掉上面 @vueuse/core 的同名项:全站共用一个实例,原因见 shared/composables/dark.ts
|
||||||
|
{
|
||||||
|
from: 'shared/composables/dark',
|
||||||
|
imports: [{ name: 'useDark', priority: 10 }],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'],
|
'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user