From ad3dbdcc8648c46bd58a9391804fd8ce5967cdfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B7=AB=E5=87=A4=E8=90=8D?= Date: Wed, 12 Aug 2026 20:44:57 +0800 Subject: [PATCH] feat: collapse mobile note after first backfill --- koc-portal/app/globals.css | 63 +++++++++++++++++++++++++ koc-portal/app/page.tsx | 59 +++++++++++++++++++++-- koc-portal/tests/rendered-html.test.mjs | 10 +++- 3 files changed, 128 insertions(+), 4 deletions(-) diff --git a/koc-portal/app/globals.css b/koc-portal/app/globals.css index d0747f9..890ba84 100644 --- a/koc-portal/app/globals.css +++ b/koc-portal/app/globals.css @@ -1050,6 +1050,11 @@ footer { padding: 34px 38px; } +.mobile-note-summary, +.mobile-note-collapse-trigger { + display: none; +} + .note-document-meta { display: flex; gap: 8px; @@ -1810,6 +1815,64 @@ footer { border-radius: 16px; } + .note-document.mobile-collapsed { + padding: 16px; + } + + .note-document.mobile-collapsed .note-document-content { + display: none; + } + + .note-document.mobile-collapsed .mobile-note-summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + } + + .mobile-note-summary > div { + display: grid; + min-width: 0; + gap: 4px; + } + + .mobile-note-summary span { + color: var(--green); + font-size: 9px; + font-weight: 750; + } + + .mobile-note-summary strong { + overflow: hidden; + color: var(--ink); + font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; + } + + .mobile-note-summary small { + color: #899590; + font-size: 8px; + } + + .mobile-note-summary button, + .mobile-note-collapse-trigger { + flex: 0 0 auto; + min-height: 34px; + padding: 0 12px; + border: 1px solid #cfe1d9; + border-radius: 9px; + color: var(--green-deep); + background: #f2f8f5; + font-size: 9px; + font-weight: 700; + } + + .mobile-note-collapse-trigger { + display: block; + margin: 14px 0 0 auto; + } + .note-document h1 { font-size: 28px; } diff --git a/koc-portal/app/page.tsx b/koc-portal/app/page.tsx index 4054de1..68999cd 100644 --- a/koc-portal/app/page.tsx +++ b/koc-portal/app/page.tsx @@ -1,7 +1,7 @@ "use client"; import { zipSync } from "fflate"; -import { ChangeEvent, FormEvent, useCallback, useEffect, useMemo, useState } from "react"; +import { ChangeEvent, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { formatShanghaiDate as formatDate, parseStoredDate, @@ -96,6 +96,13 @@ function partnerApi(path: "/api/partner" | "/api/partner-upload") { return `${resolveAdminOrigin()}${path}`; } +function isMobilePortalViewport() { + return ( + typeof window !== "undefined" && + window.matchMedia("(max-width: 620px)").matches + ); +} + function statusLabel(item: Assignment, taskType = "content_publish") { if (taskType === "screenshot_collect") { return item.result_submitted_at ? "已提交" : "待提交"; @@ -280,6 +287,8 @@ export default function Home() { src: string; alt: string; } | null>(null); + const [noteContentCollapsed, setNoteContentCollapsed] = useState(false); + const submitCardRef = useRef(null); const publishScreenshotPreview = useFilePreview(screenshot); const creatorScreenshotPreview = useFilePreview(creatorScreenshot); const taskResultPreviews = useMemo( @@ -384,6 +393,9 @@ export default function Home() { selected.exposure === null ? "" : String(selected.exposure), ); setCreatorViews(selected.views === null ? "" : String(selected.views)); + setNoteContentCollapsed( + Boolean(selected.publish_url) && isMobilePortalViewport(), + ); }, 0); return () => window.clearTimeout(timer); }, [selected]); @@ -794,6 +806,7 @@ export default function Home() { const submitNote = async (event: FormEvent) => { event.preventDefault(); if (!selected) return; + const isFirstBackfill = !selected.publish_url; try { setWorking(true); if (screenshot) { @@ -818,6 +831,15 @@ export default function Home() { if (!response.ok) throw new Error(result.error || "回填失败"); await loadTask(taskToken, claimToken, delegationToken); setScreenshot(null); + if (isFirstBackfill && isMobilePortalViewport()) { + setNoteContentCollapsed(true); + window.setTimeout(() => { + submitCardRef.current?.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + }, 120); + } setToast("这篇笔记已回填,不会与其他笔记错配"); } catch (reason) { setToast(reason instanceof Error ? reason.message : "回填失败"); @@ -1104,11 +1126,41 @@ export default function Home() {
-
+
+
+
+ 笔记内容已收起 + {selected.title} + + {selected.images.length} 张配图 · 已完成首次回填 + +
+ +
+
笔记 {activeBatch.assignments.findIndex((item) => item.id === selected.id) + 1} 飞书源行 {selected.source_row ?? "—"}
+ {selected.publish_url && ( + + )}

{selected.title}

-
+

逐篇回填

diff --git a/koc-portal/tests/rendered-html.test.mjs b/koc-portal/tests/rendered-html.test.mjs index 25d6ad9..d4a3aef 100644 --- a/koc-portal/tests/rendered-html.test.mjs +++ b/koc-portal/tests/rendered-html.test.mjs @@ -18,12 +18,13 @@ test("builds the branded external task shell", async () => { }); test("keeps claiming minimal and backfill one-to-one", async () => { - const [page, layout, packageJson, nextConfig] = + const [page, layout, packageJson, nextConfig, styles] = await Promise.all([ readFile(new URL("../app/page.tsx", import.meta.url), "utf8"), readFile(new URL("../app/layout.tsx", import.meta.url), "utf8"), readFile(new URL("../package.json", import.meta.url), "utf8"), readFile(new URL("../next.config.ts", import.meta.url), "utf8"), + readFile(new URL("../app/globals.css", import.meta.url), "utf8"), ]); assert.match(page, /微信号\s*\/\s*手机号/); @@ -53,6 +54,13 @@ test("keeps claiming minimal and backfill one-to-one", async () => { assert.match(page, /action:\s*"recover"/); assert.match(page, /同一任务多次领取会分批展示/); assert.match(page, /这篇笔记已回填,不会与其他笔记错配/); + assert.match(page, /笔记内容已收起/); + assert.match(page, /展开笔记内容/); + assert.match(page, /收起笔记内容/); + assert.match(page, /isFirstBackfill/); + assert.match(page, /scrollIntoView/); + assert.match(page, /matchMedia\("\(max-width: 620px\)"\)/); + assert.match(styles, /\.note-document\.mobile-collapsed/); assert.doesNotMatch(page, /批量回填/); assert.doesNotMatch(page, /复制标题和正文/); assert.match(page, /CONFIGURED_ADMIN_ORIGIN/);