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:
1 parent
90ce1ab0f5
commit
a5d5c39411
12 files changed
+177
-28
No files matched your search
@@ -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: "道",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user