From c497ccadc8f210388b0993256507adba96f46a14 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Mon, 12 Oct 2026 14:00:29 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E9=A2=98=E7=9B=AE=E9=A1=B5):=20=E4=BE=8B?= =?UTF-8?q?=E5=AD=90=E8=BE=93=E5=87=BA=E9=95=BF=E4=B8=80=E7=82=B9=E5=B0=B1?= =?UTF-8?q?=E5=9C=A8=E5=8D=8A=E6=A0=BC=E9=87=8C=E6=A8=AA=E7=9D=80=E6=BB=9A?= =?UTF-8?q?=EF=BC=8C=E6=9C=BA=E6=88=BF=E5=B1=8F=E5=B9=95=E4=B8=8A=E4=B8=80?= =?UTF-8?q?=E6=88=90=E7=9A=84=E9=A2=98=E7=9C=8B=E4=B8=8D=E5=85=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 机房两种屏上左栏只有 550px 左右,「例子 N | 输入 | 输出」每格只放得下二十来个字符, 而例子输出最长那行的 p90 有 28 个字符宽(中文按两个算),62 道题超过 32。 按这道题最长那行用实际字体量宽度:半格放得下照旧摆表;放不下就整道题的例子改成 上下排,输入、输出各占满宽;整宽也放不下的二十来道才横着滚,滚动条改成 6px 细条。 拖动分栏时实时重算。不自动折行:折出来的行学生分不清是不是真换了行。 Co-Authored-By: Claude Opus 5.5 --- .../oj/problem/components/ProblemContent.vue | 169 +++++++++++++++--- 1 file changed, 140 insertions(+), 29 deletions(-) diff --git a/apps/web/src/oj/problem/components/ProblemContent.vue b/apps/web/src/oj/problem/components/ProblemContent.vue index 3c93429..cd63267 100644 --- a/apps/web/src/oj/problem/components/ProblemContent.vue +++ b/apps/web/src/oj/problem/components/ProblemContent.vue @@ -111,6 +111,64 @@ watch( const samples = computed(() => problem.value?.samples ?? []) const submissionStore = useSubmissionStore() +const SAMPLE_SIDES = ["input", "output"] as const +const SAMPLE_SIDE_LABEL = { input: "输入", output: "输出" } as const + +/* + * 例子默认摆成「例子 N | 输入 | 输出」的表,可机房那两种屏上左栏只有 550px 左右,每格只放得下 + * 二十来个字符,而例子输出最长那行的 p90 就有 28 个字符宽(中文按两个算)—— 一成的题会在 + * 半格里横着滚。所以按这道题最长那行量一下:半格放得下就摆表,放不下这道题的例子就整体改成 + * 上下排(输入一行、输出一行,各占满宽)。整宽还放不下的二十来道才横着滚。不自动折行:折出来 + * 的行学生分不清是不是真换了行,照着它输出就错了。 + * 下面几个尺寸 CSS 里也用(v-bind),量宽度和排版是同一套数。 + */ +const SAMPLE_FONT = "14px Consolas, Monaco, monospace" +const SAMPLE_NO_W = 56 +const SAMPLE_GAP = 8 +const SAMPLE_PAD_L = 10 +const SAMPLE_PAD_R = 4 +const SAMPLE_COPY_W = 44 +const sampleCss = { + font: SAMPLE_FONT, + noW: `${SAMPLE_NO_W}px`, + gap: `${SAMPLE_GAP}px`, + pad: `2px ${SAMPLE_PAD_R}px 2px ${SAMPLE_PAD_L}px`, + copyW: `${SAMPLE_COPY_W}px`, +} + +let measureCtx: CanvasRenderingContext2D | null = null +/** 例子里最长那一行排出来有多宽(px)。等宽字体里中文、表情照样比英文宽,所以按字体真量 */ +const sampleTextWidth = computed(() => { + measureCtx ??= document.createElement("canvas").getContext("2d") + if (!measureCtx) return 0 + measureCtx.font = SAMPLE_FONT + let widest = 0 + for (const sample of samples.value) { + for (const side of SAMPLE_SIDES) { + for (const line of sample[side].split("\n")) { + //
 里 tab 按 8 列排
+        const width = measureCtx.measureText(line.replace(/\t/g, "        ")).width
+        if (width > widest) widest = width
+      }
+    }
+  }
+  return widest
+})
+
+const sampleTable = useTemplateRef("sampleTable")
+const { width: sampleTableWidth } = useElementSize(sampleTable)
+/** 摆成表时,半格里留给文字的宽度 */
+const sampleHalfWidth = computed(
+  () =>
+    (sampleTableWidth.value - SAMPLE_NO_W - SAMPLE_GAP * 2) / 2 -
+    SAMPLE_PAD_L -
+    SAMPLE_PAD_R -
+    SAMPLE_COPY_W,
+)
+const samplesStacked = computed(
+  () => sampleTableWidth.value > 0 && sampleTextWidth.value > sampleHalfWidth.value,
+)
+
 // 文案和配色分类都由后端生成 —— 原来这里有一份 NODE_TARGET_LABELS +
 // ruleDescription + ruleTagType,和判题机那份几乎一模一样,见契约
 // astRequirementSchema。规则原文(engine / target)不下发给学生。
@@ -282,29 +340,38 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
             自己输入数据试试 ›
           
         
-        
- - 输入 - 输出 - + +
@@ -509,14 +576,31 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem padding: 8px; } -/* 例子:例子 N | 输入 | 输出,每格右边一个「复制」 */ +/* + * 例子:例子 N | 输入 | 输出,每格右边一个「复制」。 + * 放不下时(.stacked)每个例子两行:例子 N | 输入 | 那格,下一行 输出 | 那格 + */ .sample-table { + display: flex; + flex-direction: column; + gap: 6px; +} + +.sample-table.stacked { + gap: 12px; +} + +.sample-row { display: grid; - grid-template-columns: 56px minmax(0, 1fr) minmax(0, 1fr); - gap: 6px 8px; + grid-template-columns: v-bind("sampleCss.noW") minmax(0, 1fr) minmax(0, 1fr); + gap: 6px v-bind("sampleCss.gap"); align-items: stretch; } +.stacked .sample-row { + grid-template-columns: v-bind("sampleCss.noW") 32px minmax(0, 1fr); +} + .sample-label, .sample-no { font-size: 12px; @@ -528,11 +612,23 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem align-items: center; } +/* 上下排时「例子 N」竖跨输入、输出两行,和「输入」对齐在顶上 */ +.stacked .sample-no { + grid-row: span 2; + align-items: flex-start; +} + +.stacked .sample-no, +.sample-side { + padding-top: 8px; + line-height: 1.5; +} + .sample-cell { min-width: 0; display: flex; align-items: flex-start; - padding: 2px 4px 2px 10px; + padding: v-bind("sampleCss.pad"); border-radius: 6px; background-color: rgba(128, 128, 128, 0.08); } @@ -542,17 +638,32 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem min-width: 0; margin: 0; padding: 4px 0; - font-family: Consolas, Monaco, monospace; - font-size: 14px; + font: v-bind("sampleCss.font"); + line-height: 1.5; white-space: pre; overflow-x: auto; } +/* 整宽也放不下、只好横着滚的那二十来道:Windows 上默认那根带箭头的粗滚动条比例子本身还显眼 */ +.testcase::-webkit-scrollbar { + height: 6px; +} + +.testcase::-webkit-scrollbar-thumb { + border-radius: 3px; + background-color: rgba(128, 128, 128, 0.35); +} + +.testcase::-webkit-scrollbar-track { + background: transparent; +} + .sample-copy { flex: none; + width: v-bind("sampleCss.copyW"); height: 24px; margin-top: 2px; - padding: 0 6px; + padding: 0; border: 0; border-radius: 4px; background: transparent;