feat: collapse mobile note after first backfill
This commit is contained in:
@@ -1050,6 +1050,11 @@ footer {
|
|||||||
padding: 34px 38px;
|
padding: 34px 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-note-summary,
|
||||||
|
.mobile-note-collapse-trigger {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.note-document-meta {
|
.note-document-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -1810,6 +1815,64 @@ footer {
|
|||||||
border-radius: 16px;
|
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 {
|
.note-document h1 {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { zipSync } from "fflate";
|
import { zipSync } from "fflate";
|
||||||
import { ChangeEvent, FormEvent, useCallback, useEffect, useMemo, useState } from "react";
|
import { ChangeEvent, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
formatShanghaiDate as formatDate,
|
formatShanghaiDate as formatDate,
|
||||||
parseStoredDate,
|
parseStoredDate,
|
||||||
@@ -96,6 +96,13 @@ function partnerApi(path: "/api/partner" | "/api/partner-upload") {
|
|||||||
return `${resolveAdminOrigin()}${path}`;
|
return `${resolveAdminOrigin()}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMobilePortalViewport() {
|
||||||
|
return (
|
||||||
|
typeof window !== "undefined" &&
|
||||||
|
window.matchMedia("(max-width: 620px)").matches
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function statusLabel(item: Assignment, taskType = "content_publish") {
|
function statusLabel(item: Assignment, taskType = "content_publish") {
|
||||||
if (taskType === "screenshot_collect") {
|
if (taskType === "screenshot_collect") {
|
||||||
return item.result_submitted_at ? "已提交" : "待提交";
|
return item.result_submitted_at ? "已提交" : "待提交";
|
||||||
@@ -280,6 +287,8 @@ export default function Home() {
|
|||||||
src: string;
|
src: string;
|
||||||
alt: string;
|
alt: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [noteContentCollapsed, setNoteContentCollapsed] = useState(false);
|
||||||
|
const submitCardRef = useRef<HTMLFormElement | null>(null);
|
||||||
const publishScreenshotPreview = useFilePreview(screenshot);
|
const publishScreenshotPreview = useFilePreview(screenshot);
|
||||||
const creatorScreenshotPreview = useFilePreview(creatorScreenshot);
|
const creatorScreenshotPreview = useFilePreview(creatorScreenshot);
|
||||||
const taskResultPreviews = useMemo(
|
const taskResultPreviews = useMemo(
|
||||||
@@ -384,6 +393,9 @@ export default function Home() {
|
|||||||
selected.exposure === null ? "" : String(selected.exposure),
|
selected.exposure === null ? "" : String(selected.exposure),
|
||||||
);
|
);
|
||||||
setCreatorViews(selected.views === null ? "" : String(selected.views));
|
setCreatorViews(selected.views === null ? "" : String(selected.views));
|
||||||
|
setNoteContentCollapsed(
|
||||||
|
Boolean(selected.publish_url) && isMobilePortalViewport(),
|
||||||
|
);
|
||||||
}, 0);
|
}, 0);
|
||||||
return () => window.clearTimeout(timer);
|
return () => window.clearTimeout(timer);
|
||||||
}, [selected]);
|
}, [selected]);
|
||||||
@@ -794,6 +806,7 @@ export default function Home() {
|
|||||||
const submitNote = async (event: FormEvent) => {
|
const submitNote = async (event: FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
|
const isFirstBackfill = !selected.publish_url;
|
||||||
try {
|
try {
|
||||||
setWorking(true);
|
setWorking(true);
|
||||||
if (screenshot) {
|
if (screenshot) {
|
||||||
@@ -818,6 +831,15 @@ export default function Home() {
|
|||||||
if (!response.ok) throw new Error(result.error || "回填失败");
|
if (!response.ok) throw new Error(result.error || "回填失败");
|
||||||
await loadTask(taskToken, claimToken, delegationToken);
|
await loadTask(taskToken, claimToken, delegationToken);
|
||||||
setScreenshot(null);
|
setScreenshot(null);
|
||||||
|
if (isFirstBackfill && isMobilePortalViewport()) {
|
||||||
|
setNoteContentCollapsed(true);
|
||||||
|
window.setTimeout(() => {
|
||||||
|
submitCardRef.current?.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "start",
|
||||||
|
});
|
||||||
|
}, 120);
|
||||||
|
}
|
||||||
setToast("这篇笔记已回填,不会与其他笔记错配");
|
setToast("这篇笔记已回填,不会与其他笔记错配");
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
setToast(reason instanceof Error ? reason.message : "回填失败");
|
setToast(reason instanceof Error ? reason.message : "回填失败");
|
||||||
@@ -1104,11 +1126,41 @@ export default function Home() {
|
|||||||
</header>
|
</header>
|
||||||
<button className="back-link" onClick={closeNote}>← 返回我的笔记</button>
|
<button className="back-link" onClick={closeNote}>← 返回我的笔记</button>
|
||||||
<div className="detail-grid">
|
<div className="detail-grid">
|
||||||
<article className="note-document">
|
<article
|
||||||
|
className={`note-document ${
|
||||||
|
noteContentCollapsed ? "mobile-collapsed" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="mobile-note-summary">
|
||||||
|
<div>
|
||||||
|
<span>笔记内容已收起</span>
|
||||||
|
<strong>{selected.title}</strong>
|
||||||
|
<small>
|
||||||
|
{selected.images.length} 张配图 · 已完成首次回填
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-expanded={!noteContentCollapsed}
|
||||||
|
onClick={() => setNoteContentCollapsed(false)}
|
||||||
|
>
|
||||||
|
展开笔记内容
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="note-document-content">
|
||||||
<div className="note-document-meta">
|
<div className="note-document-meta">
|
||||||
<span>笔记 {activeBatch.assignments.findIndex((item) => item.id === selected.id) + 1}</span>
|
<span>笔记 {activeBatch.assignments.findIndex((item) => item.id === selected.id) + 1}</span>
|
||||||
<span>飞书源行 {selected.source_row ?? "—"}</span>
|
<span>飞书源行 {selected.source_row ?? "—"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{selected.publish_url && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mobile-note-collapse-trigger"
|
||||||
|
onClick={() => setNoteContentCollapsed(true)}
|
||||||
|
>
|
||||||
|
收起笔记内容
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div className="note-title-row">
|
<div className="note-title-row">
|
||||||
<h1>{selected.title}</h1>
|
<h1>{selected.title}</h1>
|
||||||
<button
|
<button
|
||||||
@@ -1185,9 +1237,10 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<form className="submit-card" onSubmit={submitNote}>
|
<form ref={submitCardRef} className="submit-card" onSubmit={submitNote}>
|
||||||
<div className="submit-heading">
|
<div className="submit-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="micro">逐篇回填</p>
|
<p className="micro">逐篇回填</p>
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ test("builds the branded external task shell", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("keeps claiming minimal and backfill one-to-one", 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([
|
await Promise.all([
|
||||||
readFile(new URL("../app/page.tsx", import.meta.url), "utf8"),
|
readFile(new URL("../app/page.tsx", import.meta.url), "utf8"),
|
||||||
readFile(new URL("../app/layout.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("../package.json", import.meta.url), "utf8"),
|
||||||
readFile(new URL("../next.config.ts", 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*手机号/);
|
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, /action:\s*"recover"/);
|
||||||
assert.match(page, /同一任务多次领取会分批展示/);
|
assert.match(page, /同一任务多次领取会分批展示/);
|
||||||
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.doesNotMatch(page, /复制标题和正文/);
|
assert.doesNotMatch(page, /复制标题和正文/);
|
||||||
assert.match(page, /CONFIGURED_ADMIN_ORIGIN/);
|
assert.match(page, /CONFIGURED_ADMIN_ORIGIN/);
|
||||||
|
|||||||
Reference in New Issue
Block a user