fix(手机): 收起键盘后「提交」按钮不回来;比赛领奖台第一名被挤出屏幕;iPhone 点输入框整页放大

- 题目页:安卓按返回键收键盘时编辑器不失焦,符号键一直贴底、「运行例子 / 提交」不回来。
  改成看键盘真弹着没有(可视视口变矮),不只看焦点
- 比赛排名:结束后的领奖台三块共 646px,手机上第一名被切掉、第三名看不见;改成平分一行
- iPhone:点 14/15px 的输入框和代码编辑器,Safari 会整页放大且不缩回;只给 iOS 加 maximum-scale=1
- 登录:手机上选完班不再自动聚焦搜索框(键盘弹出来盖住半张名单)
- 题单:窄屏上题目拆成两张没标题的卡片,像分了两组;放不下两栏时合成一张
- 「中等」在题目页是蓝色、在题目列表是橙色,统一成橙色
- 零碎:自学四列表格在 360 宽上收窄格子;统计「错得最多的题」表头不再挤成竖排;
  「第 N 名」「14 道」不再把最后一个字甩到下一行

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-15 12:36:33 +08:00
1 parent 90ce1ab0f5
commit a5d5c39411
12 files changed
+177 -28

No files matched your search

+6 -1
View File
@@ -20,7 +20,12 @@ const cells = computed(() => {
const percent = r.solved ? Math.round((r.firstTry / r.solved) * 100) : 0
const firstSub = `一次就对 · 占 ${percent}%`
const list: Cell[] = [
{ big: r.solved, unit: "道", sub: c ? `做对 · 班上中间那位 ${c.medianSolved} 道` : "做对" },
// 数和「道」之间用不换行空格:手机上两列挤,单个「道」会被甩到下一行
{
big: r.solved,
unit: "道",
sub: c ? `做对 · 班上中间那位 ${c.medianSolved}\u00a0道` : "做对",
},
{
big: r.firstTry,
unit: "道",
+50 -3
View File
@@ -137,9 +137,13 @@ const columns = computed(
</svg>
<template v-if="podium[place - 1]">
<UserName class="podium-name" :username="podium[place - 1]!.username" />
<span class="muted small">
{{ classLabel(podium[place - 1]!.className) }} · {{ podium[place - 1]!.solved }} 道 ·
{{ secondsToDuration(podium[place - 1]!.totalTime) }}
<span class="muted small podium-meta">
<span>{{ classLabel(podium[place - 1]!.className) }}</span
><span class="sep"> · </span
><span
>{{ podium[place - 1]!.solved }} 道 ·
{{ secondsToDuration(podium[place - 1]!.totalTime) }}</span
>
</span>
</template>
<div class="block">{{ place }}</div>
@@ -534,4 +538,47 @@ const columns = computed(
.empty {
padding: 32px var(--oj-pad-x);
}
/* 手机:三级台阶一共 646px 宽,原样摆第一名就被挤到屏幕外了。三块平分一行,
名字截断、班级和用时折成两行 */
@media (max-width: 640px) {
.podium-row {
gap: 16px;
}
.podium {
width: 100%;
gap: 6px;
}
.step {
flex: 1 1 0;
width: auto;
min-width: 0;
}
.podium-name,
.p1 .podium-name {
font-size: 15px;
max-width: 100%;
}
.podium-meta {
font-size: var(--oj-fs-meta);
text-align: center;
}
.podium-meta > span {
display: block;
white-space: nowrap;
}
.podium-meta > .sep {
display: none;
}
.mine {
width: 100%;
}
}
</style>
@@ -75,6 +75,14 @@ function inline(text: string) {
.body :deep(.oj-markdown th) {
white-space: nowrap;
}
/* 360 宽的安卓机上四列表差三十来像素,最后一列被切掉、得横着滑才看得到:格子收窄一点 */
@media (max-width: 480px) {
.body :deep(.oj-markdown th),
.body :deep(.oj-markdown td) {
min-width: 3.5em;
padding: 6px 8px;
}
}
.goals {
margin: 4px 0 8px;
padding: 12px 16px;
@@ -5,7 +5,7 @@ import { useCodeStore } from "oj/store/code"
import { useProblemStore } from "oj/store/problem"
import { useSubmissionStore } from "oj/store/submission"
import SubmitCode from "./SubmitCode.vue"
import { editorFocused } from "../utils/symbolKeys"
import { typing } from "../utils/symbolKeys"
const SubmitFlowchart = defineAsyncComponent(() => import("./SubmitFlowchart.vue"))
@@ -30,8 +30,9 @@ const canRunSamples = computed(
</script>
<template>
<!-- 写代码时(键盘弹着)让位给符号键:两条都贴底会叠在一起,安卓上还会被键盘顶上来 -->
<div v-show="!editorFocused" class="mobile-actions">
<!-- 写代码时(键盘弹着)让位给符号键:两条都贴底会叠在一起,安卓上还会被键盘顶上来。
按返回键收了键盘、编辑器还有焦点时要回来,所以看 typing 不看焦点 -->
<div v-show="!typing" class="mobile-actions">
<n-button
v-if="canRunSamples"
size="large"
@@ -1,7 +1,13 @@
<script setup lang="ts">
import { useThemeVars } from "naive-ui"
import { useCodeStore } from "oj/store/code"
import { editorFocused, SYMBOL_KEYS, typeSymbol } from "../utils/symbolKeys"
import {
editorFocused,
measureKeyboard,
SYMBOL_KEYS,
typeSymbol,
typing,
} from "../utils/symbolKeys"
/**
* 手机上贴着键盘顶的一排符号键,编辑器有焦点(键盘弹着)时才出来。
@@ -29,9 +35,19 @@ function place() {
top.value = viewport ? viewport.offsetTop + viewport.height - BAR_HEIGHT : null
}
useEventListener(() => window.visualViewport, "resize", place)
// 键盘弹起之前就得量出「没键盘时多高」,所以挂上就量
onMounted(measureKeyboard)
useEventListener("resize", measureKeyboard)
useEventListener(
() => window.visualViewport,
"resize",
() => {
measureKeyboard()
place()
},
)
useEventListener(() => window.visualViewport, "scroll", place)
watch(editorFocused, (focused) => focused && place(), { immediate: true })
watch(typing, (on) => on && place(), { immediate: true })
// 键盘弹着时点了别处换题、离开页面,编辑器直接卸载,不一定等得到失焦
onBeforeUnmount(() => {
editorFocused.value = false
@@ -40,7 +56,7 @@ onBeforeUnmount(() => {
<template>
<div
v-if="editorFocused && keys.length"
v-if="typing && keys.length"
class="symbol-keys"
:style="top === null ? { bottom: '0px' } : { top: `${top}px` }"
role="toolbar"
+31 -2
View File
@@ -1,6 +1,6 @@
import { insertBracket } from "@codemirror/autocomplete"
import { EditorView } from "@codemirror/view"
import { ref } from "vue"
import { computed, ref } from "vue"
import { currentEditorView } from "./errorMark"
/**
@@ -8,7 +8,7 @@ import { currentEditorView } from "./errorMark"
* 一行 print(int(input())) 来回切七八次。
*/
/** 题目页主编辑器有没有焦点 —— 有焦点就是键盘弹着,这时才摆符号键 */
/** 题目页主编辑器有没有焦点 */
export const editorFocused = ref(false)
export const editorFocusTracker = EditorView.focusChangeEffect.of((_state, focusing) => {
@@ -16,6 +16,35 @@ export const editorFocusTracker = EditorView.focusChangeEffect.of((_state, focus
return null
})
/**
* 软键盘是不是弹着。光看焦点不够:安卓按返回键收键盘,编辑器**不失焦**,
* 符号键就一直贴在屏幕底下,「运行例子 / 提交」也不回来,学生找不到地方交。
*
* 键盘弹起时可视视口变矮(iOS 和 Chrome 108+ 布局视口不变;更老的安卓连
* innerHeight 一起变矮),所以和这个宽度下见过的最高值比。宽度变了(转屏)重新量。
* 乘 scale 是为了双指放大时别误判成键盘弹着。
*/
const keyboardUp = ref(false)
let baseline = 0
let baselineWidth = 0
export function measureKeyboard() {
const viewport = window.visualViewport
if (!viewport) return
if (window.innerWidth !== baselineWidth) {
baselineWidth = window.innerWidth
baseline = 0
}
const visible = viewport.height * viewport.scale
baseline = Math.max(baseline, window.innerHeight, visible)
keyboardUp.value = visible < baseline - 120
}
/** 正在打字:编辑器有焦点、键盘也弹着。这时才摆符号键,底栏让位 */
export const typing = computed(
() => editorFocused.value && (!window.visualViewport || keyboardUp.value),
)
export interface SymbolKey {
label: string
/** 插进去的字;不写就是 label */
+11 -6
View File
@@ -83,7 +83,12 @@ const spent = computed(() => {
return `${Math.round(minutes / 1440)} 天`
})
// 两栏放得下(每栏 420 起)才拆;拆完在窄屏上叠起来,就成了两张没有标题的卡片,
// 看着像题单分了两组
const twoColumns = useMediaQuery("(min-width: 960px)")
const columns = computed(() => {
if (!twoColumns.value) return [problems.value]
const half = Math.ceil(problems.value.length / 2)
return [problems.value.slice(0, half), problems.value.slice(half)].filter((col) => col.length)
})
@@ -232,7 +237,7 @@ const purple = { color: "#5b3fa8", background: "rgba(122, 95, 208, 0.12)" }
<ClassView v-if="userStore.isTeacherOrAbove && tab === 'class'" :problem-set-id="set.id" />
<template v-else>
<div v-if="problems.length" class="cols">
<div v-if="problems.length" class="cols" :class="{ two: twoColumns }">
<section v-for="(col, ci) in columns" :key="ci" class="card list">
<component
:is="canOpen ? 'router-link' : 'div'"
@@ -461,11 +466,15 @@ const purple = { color: "#5b3fa8", background: "rgba(122, 95, 208, 0.12)" }
.cols {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
grid-template-columns: minmax(0, 1fr);
gap: 16px;
align-items: start;
}
.cols.two {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.card {
border: 1px solid v-bind("theme.borderColor");
border-radius: var(--oj-radius);
@@ -622,10 +631,6 @@ a.prow:hover {
}
@media (max-width: 640px) {
.cols {
grid-template-columns: minmax(0, 1fr);
}
.band {
padding: 16px;
}
+3 -2
View File
@@ -53,13 +53,14 @@ const headline = computed(() => {
return props.lastListed ? `再做对 ${toPass(me, props.lastListed)} 道就进前 ${props.cap}` : ""
if (me.rank === 1) return "你是第一名"
if (me.rank <= 3 && ahead)
return `你在领奖台上 · 再做对 ${toPass(me, ahead)} 道就是第 ${ahead.rank} 名`
// 「第 N 名」用不换行空格连住:手机上这一句正好折行,单个「名」会掉到下一行
return `你在领奖台上 · 再做对 ${toPass(me, ahead)} 道就是第\u00a0${ahead.rank}\u00a0名`
if (third) {
const need = toPass(me, third)
if (need <= 3) return `再做对 ${need} 道,你就上领奖台`
}
if ((me.change ?? 0) >= 3) return `比周一升了 ${me.change} 名`
if (ahead) return `再做对 ${toPass(me, ahead)} 道就是第 ${ahead.rank} 名`
if (ahead) return `再做对 ${toPass(me, ahead)} 道就是第\u00a0${ahead.rank}\u00a0名`
return ""
})
@@ -572,5 +572,16 @@ function percent(value: number, total: number) {
display: block;
overflow-x: auto;
}
/* 表横着滑,表头别被挤成竖排(「提 / 交」) */
.hard th {
white-space: nowrap;
}
.section-title {
flex-wrap: wrap;
row-gap: 2px;
}
.section-title b {
flex: none;
}
}
</style>