fix(web): 审稿面板去重标题/去英文 slug + 伏笔待登记数实时递减 + 移动端决策栏 + 对比度修复
This commit is contained in:
@@ -142,7 +142,7 @@ export function AcceptPanel({
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<p className="mt-2 text-[11px] text-ink-soft/70">
|
<p className="mt-2 text-2xs text-ink-soft">
|
||||||
以上为预期,最终以验收回执为准。
|
以上为预期,最终以验收回执为准。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRef, type ReactNode, type RefObject } from "react";
|
import { useRef, type ReactNode, type RefObject } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { PenLine } from "lucide-react";
|
||||||
|
|
||||||
|
import { EmptyState } from "@/components/ui/EmptyState";
|
||||||
import { locateInDraft } from "@/lib/review/locate";
|
import { locateInDraft } from "@/lib/review/locate";
|
||||||
|
import { buttonClass, proseBody } from "@/lib/ui/variants";
|
||||||
import type { ReviewConflict } from "@/lib/review/sse";
|
import type { ReviewConflict } from "@/lib/review/sse";
|
||||||
|
|
||||||
interface AnnotatedTextProps {
|
interface AnnotatedTextProps {
|
||||||
@@ -12,14 +16,16 @@ interface AnnotatedTextProps {
|
|||||||
// 当前聚焦的冲突下标(来自报告卡「跳转」/锚点),用于锚点高亮联动。
|
// 当前聚焦的冲突下标(来自报告卡「跳转」/锚点),用于锚点高亮联动。
|
||||||
focusedIndex: number | null;
|
focusedIndex: number | null;
|
||||||
onAnchorClick: (index: number) => void;
|
onAnchorClick: (index: number) => void;
|
||||||
|
// 空待审正文时,引导回写作页起草本章。
|
||||||
|
projectId: string;
|
||||||
|
chapterNo: number;
|
||||||
// 转发给可编辑 textarea,供父组件 setSelectionRange 定位/高亮问题区域。
|
// 转发给可编辑 textarea,供父组件 setSelectionRange 定位/高亮问题区域。
|
||||||
editorRef?: RefObject<HTMLTextAreaElement | null>;
|
editorRef?: RefObject<HTMLTextAreaElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 高亮叠层编辑器:背景层渲染带 <mark> 高亮的正文,前景透明 textarea 负责编辑。
|
// 高亮叠层编辑器:背景层渲染带 <mark> 高亮的正文,前景透明 textarea 负责编辑。
|
||||||
// 两层必须共享同一盒模型/排版常量,否则高亮与文字像素错位。
|
// 两层必须共享同一盒模型/排版常量,否则高亮与文字像素错位;正文排版与写作页 Editor 一致(proseBody)。
|
||||||
const TYPO =
|
const TYPO = `box-border w-full whitespace-pre-wrap break-words ${proseBody}`;
|
||||||
"box-border w-full font-serif text-[17px] leading-[1.9] tracking-normal whitespace-pre-wrap break-words";
|
|
||||||
|
|
||||||
// 把终稿按已定位的冲突区间切成 [纯文本 | <mark> | …];<mark> 带 id 供父组件滚动定位。
|
// 把终稿按已定位的冲突区间切成 [纯文本 | <mark> | …];<mark> 带 id 供父组件滚动定位。
|
||||||
// 区间按 start 排序,重叠区间保留先到者(跳过后者),避免标签嵌套。
|
// 区间按 start 排序,重叠区间保留先到者(跳过后者),避免标签嵌套。
|
||||||
@@ -61,6 +67,8 @@ export function AnnotatedText({
|
|||||||
conflicts,
|
conflicts,
|
||||||
focusedIndex,
|
focusedIndex,
|
||||||
onAnchorClick,
|
onAnchorClick,
|
||||||
|
projectId,
|
||||||
|
chapterNo,
|
||||||
editorRef,
|
editorRef,
|
||||||
}: AnnotatedTextProps) {
|
}: AnnotatedTextProps) {
|
||||||
const backdropRef = useRef<HTMLDivElement>(null);
|
const backdropRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -69,6 +77,26 @@ export function AnnotatedText({
|
|||||||
const anchorable = conflicts
|
const anchorable = conflicts
|
||||||
.map((c, i) => ({ c, i }))
|
.map((c, i) => ({ c, i }))
|
||||||
.filter(({ c }) => locateInDraft(value, c) !== null);
|
.filter(({ c }) => locateInDraft(value, c) !== null);
|
||||||
|
|
||||||
|
// 空待审正文:不渲染高亮叠层,直接引导回写作页起草本章(避免对着空编辑器发懵)。
|
||||||
|
if (!value) {
|
||||||
|
return (
|
||||||
|
<EmptyState
|
||||||
|
icon={PenLine}
|
||||||
|
title="本章还没有待审正文"
|
||||||
|
description="先到写作页起草本章,写完回到这里审稿、裁决与验收。"
|
||||||
|
action={
|
||||||
|
<Link
|
||||||
|
href={`/projects/${projectId}/write?chapter=${chapterNo}`}
|
||||||
|
className={buttonClass({ variant: "primary" })}
|
||||||
|
>
|
||||||
|
<PenLine className="h-4 w-4" aria-hidden="true" />
|
||||||
|
去写作页起草本章
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{anchorable.length > 0 ? (
|
{anchorable.length > 0 ? (
|
||||||
@@ -104,13 +132,7 @@ export function AnnotatedText({
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={`${TYPO} pointer-events-none relative select-none text-ink`}
|
className={`${TYPO} pointer-events-none relative select-none text-ink`}
|
||||||
>
|
>
|
||||||
{value ? (
|
{segments}
|
||||||
segments
|
|
||||||
) : (
|
|
||||||
<span className="text-ink-soft/60">
|
|
||||||
(无待审正文,先去写作页起草本章)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{"\n"}
|
{"\n"}
|
||||||
</div>
|
</div>
|
||||||
{/* 前景编辑层:文字透明(由背景层显示),仅保留光标;无内部滚动(页面滚动)。 */}
|
{/* 前景编辑层:文字透明(由背景层显示),仅保留光标;无内部滚动(页面滚动)。 */}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface BeatMapProps {
|
|||||||
export function BeatMap({ beatMap }: BeatMapProps) {
|
export function BeatMap({ beatMap }: BeatMapProps) {
|
||||||
const cells = toBeatCells(beatMap);
|
const cells = toBeatCells(beatMap);
|
||||||
if (cells.length === 0) {
|
if (cells.length === 0) {
|
||||||
return <span className="text-xs text-ink-soft/60">无节拍数据</span>;
|
return <span className="text-xs text-ink-soft">无节拍数据</span>;
|
||||||
}
|
}
|
||||||
const spark = toSparkline(beatMap);
|
const spark = toSparkline(beatMap);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ interface ForeshadowSuggestionsProps {
|
|||||||
incomplete: boolean;
|
incomplete: boolean;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
chapterNo: number;
|
chapterNo: number;
|
||||||
|
// 把「仍待处理(未登记/未回收)」的建议数上抛给报告页:
|
||||||
|
// 喂 AcceptPanel 的验收预览与伏笔分区徽标,随登记/回收操作实时递减。
|
||||||
|
onRemainingChange?: (count: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RegisterFormState {
|
interface RegisterFormState {
|
||||||
@@ -39,6 +42,7 @@ export function ForeshadowSuggestions({
|
|||||||
incomplete,
|
incomplete,
|
||||||
projectId,
|
projectId,
|
||||||
chapterNo,
|
chapterNo,
|
||||||
|
onRemainingChange,
|
||||||
}: ForeshadowSuggestionsProps) {
|
}: ForeshadowSuggestionsProps) {
|
||||||
const fs = useForeshadow([]);
|
const fs = useForeshadow([]);
|
||||||
const [openIdx, setOpenIdx] = useState<number | null>(null);
|
const [openIdx, setOpenIdx] = useState<number | null>(null);
|
||||||
@@ -145,20 +149,24 @@ export function ForeshadowSuggestions({
|
|||||||
.map((s, i) => ({ s, i }))
|
.map((s, i) => ({ s, i }))
|
||||||
.filter(({ s, i }) => !isAlreadySaved(s, i));
|
.filter(({ s, i }) => !isAlreadySaved(s, i));
|
||||||
|
|
||||||
|
// 上抛「仍待处理」建议数(已登记/已回收/历史已存的都已从 visible 过滤),
|
||||||
|
// 让报告页的验收预览与伏笔徽标随作者操作递减(父传入稳定 setter)。
|
||||||
|
const remaining = visible.length;
|
||||||
|
useEffect(() => {
|
||||||
|
onRemainingChange?.(remaining);
|
||||||
|
}, [remaining, onRemainingChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mb-4">
|
<section>
|
||||||
<h2 className="text-xs font-semibold uppercase tracking-wide text-ink-soft">
|
|
||||||
伏笔 (foreshadow-analyst)
|
|
||||||
</h2>
|
|
||||||
{incomplete ? (
|
{incomplete ? (
|
||||||
<Badge variant="warning" className="mt-1">
|
<Badge variant="warning">
|
||||||
<CircleDashed className="h-3 w-3" aria-hidden="true" />
|
<CircleDashed className="h-3 w-3" aria-hidden="true" />
|
||||||
未完成
|
未完成
|
||||||
</Badge>
|
</Badge>
|
||||||
) : visible.length === 0 ? (
|
) : visible.length === 0 ? (
|
||||||
<p className="mt-1 text-xs text-ink-soft">无伏笔建议。</p>
|
<p className="text-xs text-ink-soft">无伏笔建议。</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="mt-2 space-y-2">
|
<ul className="space-y-2">
|
||||||
{visible.map(({ s, i }) => {
|
{visible.map(({ s, i }) => {
|
||||||
const code = s.code;
|
const code = s.code;
|
||||||
const isResolved = s.kind === "resolved";
|
const isResolved = s.kind === "resolved";
|
||||||
|
|||||||
@@ -14,19 +14,16 @@ interface PacePanelProps {
|
|||||||
// 节奏报告区(UX §6.4 ④):节拍图 ▁▃▅ + 注水段列表 + 章末钩子徽标。只读建议。
|
// 节奏报告区(UX §6.4 ④):节拍图 ▁▃▅ + 注水段列表 + 章末钩子徽标。只读建议。
|
||||||
export function PacePanel({ pace, incomplete }: PacePanelProps) {
|
export function PacePanel({ pace, incomplete }: PacePanelProps) {
|
||||||
return (
|
return (
|
||||||
<section className="mb-4">
|
<section>
|
||||||
<h2 className="text-xs font-semibold uppercase tracking-wide text-ink-soft">
|
|
||||||
节奏 (pace-checker)
|
|
||||||
</h2>
|
|
||||||
{incomplete ? (
|
{incomplete ? (
|
||||||
<Badge variant="warning" className="mt-1">
|
<Badge variant="warning">
|
||||||
<CircleDashed className="h-3 w-3" aria-hidden="true" />
|
<CircleDashed className="h-3 w-3" aria-hidden="true" />
|
||||||
未完成
|
未完成
|
||||||
</Badge>
|
</Badge>
|
||||||
) : pace === null ? (
|
) : pace === null ? (
|
||||||
<p className="mt-1 text-xs text-ink-soft">无节奏报告。</p>
|
<p className="text-xs text-ink-soft">无节奏报告。</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-2 space-y-2 text-xs">
|
<div className="space-y-2 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-1 text-ink-soft">爽点节拍图</p>
|
<p className="mb-1 text-ink-soft">爽点节拍图</p>
|
||||||
<BeatMap beatMap={pace.beat_map} />
|
<BeatMap beatMap={pace.beat_map} />
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import { useRouter } from "next/navigation";
|
|||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
ChevronDown,
|
||||||
|
ClipboardCheck,
|
||||||
|
LocateFixed,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
Square,
|
Square,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -101,6 +104,11 @@ export function ReviewReport({
|
|||||||
const editorRef = useRef<HTMLTextAreaElement>(null);
|
const editorRef = useRef<HTMLTextAreaElement>(null);
|
||||||
// 回炉中漂移段在终稿里定位到的原文(null=未打开 RefineView)。内容锚定位,非位置 idx。
|
// 回炉中漂移段在终稿里定位到的原文(null=未打开 RefineView)。内容锚定位,非位置 idx。
|
||||||
const [refineText, setRefineText] = useState<string | null>(null);
|
const [refineText, setRefineText] = useState<string | null>(null);
|
||||||
|
// 伏笔「仍待处理」数:由 ForeshadowSuggestions 上抛(已登记/已回收的已扣除)。
|
||||||
|
// null=尚未上报,回退到原始建议数,避免首帧闪「无建议」。
|
||||||
|
const [foreshadowRemaining, setForeshadowRemaining] = useState<number | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const seededRef = useRef(false);
|
const seededRef = useRef(false);
|
||||||
|
|
||||||
// 进页一次性把历史留痕(冲突 + 伏笔建议 + 节奏)种入流状态(无需重审即可裁决/查看)。
|
// 进页一次性把历史留痕(冲突 + 伏笔建议 + 节奏)种入流状态(无需重审即可裁决/查看)。
|
||||||
@@ -143,6 +151,8 @@ export function ReviewReport({
|
|||||||
const foreshadow = review.state.foreshadow;
|
const foreshadow = review.state.foreshadow;
|
||||||
const pace = review.state.pace;
|
const pace = review.state.pace;
|
||||||
const style = review.state.style;
|
const style = review.state.style;
|
||||||
|
// 伏笔徽标/验收预览统一用「仍待处理数」(随登记/回收递减);未上报前回退原始建议数。
|
||||||
|
const foreshadowOpen = foreshadowRemaining ?? foreshadow.length;
|
||||||
const sectionStatus = (name: string): string | undefined =>
|
const sectionStatus = (name: string): string | undefined =>
|
||||||
review.state.sections.find((s) => s.name === name)?.status;
|
review.state.sections.find((s) => s.name === name)?.status;
|
||||||
|
|
||||||
@@ -469,6 +479,8 @@ export function ReviewReport({
|
|||||||
conflicts={conflicts}
|
conflicts={conflicts}
|
||||||
focusedIndex={focusedIndex}
|
focusedIndex={focusedIndex}
|
||||||
onAnchorClick={focusRegion}
|
onAnchorClick={focusRegion}
|
||||||
|
projectId={project.id}
|
||||||
|
chapterNo={chapterNo}
|
||||||
editorRef={editorRef}
|
editorRef={editorRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -490,7 +502,6 @@ export function ReviewReport({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<ReviewSectionPanel
|
<ReviewSectionPanel
|
||||||
title="一致性"
|
title="一致性"
|
||||||
subtitle="continuity"
|
|
||||||
statusLabel={
|
statusLabel={
|
||||||
reviewing
|
reviewing
|
||||||
? "审稿中"
|
? "审稿中"
|
||||||
@@ -537,7 +548,7 @@ export function ReviewReport({
|
|||||||
<details
|
<details
|
||||||
key={group.type}
|
key={group.type}
|
||||||
open={unresolvedInGroup > 0}
|
open={unresolvedInGroup > 0}
|
||||||
className="rounded border border-line/70 bg-bg/35 p-2"
|
className="group rounded border border-line/70 bg-bg/35 p-2"
|
||||||
>
|
>
|
||||||
<summary className="flex cursor-pointer list-none items-center justify-between gap-2 text-xs font-semibold text-ink">
|
<summary className="flex cursor-pointer list-none items-center justify-between gap-2 text-xs font-semibold text-ink">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
@@ -550,11 +561,17 @@ export function ReviewReport({
|
|||||||
{group.items.length}
|
{group.items.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</span>
|
</span>
|
||||||
<span className="font-mono text-[11px] text-ink-soft">
|
<span className="flex items-center gap-1.5">
|
||||||
|
<span className="font-mono text-2xs text-ink-soft">
|
||||||
{unresolvedInGroup > 0
|
{unresolvedInGroup > 0
|
||||||
? `${unresolvedInGroup} 未裁决`
|
? `${unresolvedInGroup} 未裁决`
|
||||||
: "已处理"}
|
: "已处理"}
|
||||||
</span>
|
</span>
|
||||||
|
<ChevronDown
|
||||||
|
className="h-4 w-4 text-ink-soft transition-transform group-open:rotate-180"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
<ul className="mt-3 space-y-3">
|
<ul className="mt-3 space-y-3">
|
||||||
{group.items.map(({ conflict: c, index: i }) => (
|
{group.items.map(({ conflict: c, index: i }) => (
|
||||||
@@ -583,18 +600,17 @@ export function ReviewReport({
|
|||||||
|
|
||||||
<ReviewSectionPanel
|
<ReviewSectionPanel
|
||||||
title="伏笔"
|
title="伏笔"
|
||||||
subtitle="foreshadow-analyst"
|
|
||||||
statusLabel={
|
statusLabel={
|
||||||
sectionStatus("foreshadow") === "incomplete"
|
sectionStatus("foreshadow") === "incomplete"
|
||||||
? "未完成"
|
? "未完成"
|
||||||
: foreshadow.length > 0
|
: foreshadowOpen > 0
|
||||||
? `${foreshadow.length} 建议`
|
? `${foreshadowOpen} 待登记`
|
||||||
: "无建议"
|
: "无建议"
|
||||||
}
|
}
|
||||||
statusVariant={
|
statusVariant={
|
||||||
sectionStatus("foreshadow") === "incomplete"
|
sectionStatus("foreshadow") === "incomplete"
|
||||||
? "warning"
|
? "warning"
|
||||||
: foreshadow.length > 0
|
: foreshadowOpen > 0
|
||||||
? "accent"
|
? "accent"
|
||||||
: "success"
|
: "success"
|
||||||
}
|
}
|
||||||
@@ -605,11 +621,11 @@ export function ReviewReport({
|
|||||||
incomplete={sectionStatus("foreshadow") === "incomplete"}
|
incomplete={sectionStatus("foreshadow") === "incomplete"}
|
||||||
projectId={project.id}
|
projectId={project.id}
|
||||||
chapterNo={chapterNo}
|
chapterNo={chapterNo}
|
||||||
|
onRemainingChange={setForeshadowRemaining}
|
||||||
/>
|
/>
|
||||||
</ReviewSectionPanel>
|
</ReviewSectionPanel>
|
||||||
<ReviewSectionPanel
|
<ReviewSectionPanel
|
||||||
title="节奏"
|
title="节奏"
|
||||||
subtitle="pace-checker"
|
|
||||||
statusLabel={
|
statusLabel={
|
||||||
sectionStatus("pace") === "incomplete"
|
sectionStatus("pace") === "incomplete"
|
||||||
? "未完成"
|
? "未完成"
|
||||||
@@ -633,7 +649,6 @@ export function ReviewReport({
|
|||||||
</ReviewSectionPanel>
|
</ReviewSectionPanel>
|
||||||
<ReviewSectionPanel
|
<ReviewSectionPanel
|
||||||
title="文风"
|
title="文风"
|
||||||
subtitle="style"
|
|
||||||
statusLabel={
|
statusLabel={
|
||||||
sectionStatus("style") === "incomplete"
|
sectionStatus("style") === "incomplete"
|
||||||
? "未完成"
|
? "未完成"
|
||||||
@@ -667,20 +682,52 @@ export function ReviewReport({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="mt-4">
|
<section id="accept-panel" className="mt-4 scroll-mt-4">
|
||||||
<AcceptPanel
|
<AcceptPanel
|
||||||
projectId={project.id}
|
projectId={project.id}
|
||||||
chapterNo={chapterNo}
|
chapterNo={chapterNo}
|
||||||
reviewed={initialReview !== undefined || review.state.phase === "done"}
|
reviewed={initialReview !== undefined || review.state.phase === "done"}
|
||||||
conflictCount={conflictCount}
|
conflictCount={conflictCount}
|
||||||
unresolvedCount={resolved ? 0 : unresolved}
|
unresolvedCount={resolved ? 0 : unresolved}
|
||||||
foreshadowCount={foreshadow.length}
|
foreshadowCount={foreshadowOpen}
|
||||||
accepting={accept.status === "accepting"}
|
accepting={accept.status === "accepting"}
|
||||||
result={accept.result}
|
result={accept.result}
|
||||||
onAccept={() => void onAccept()}
|
onAccept={() => void onAccept()}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
{/* 移动端(<lg 单列)快捷条:编辑器在上、审稿/验收在下,滚一整屏才够得着——
|
||||||
|
固定底栏直接「跳到未裁决」或「去验收」,免去长滚。桌面端隐藏(右栏常驻可见)。 */}
|
||||||
|
{hasReport && !accept.result ? (
|
||||||
|
<div className="fixed inset-x-0 bottom-0 z-30 flex items-center gap-2 border-t border-line bg-panel/95 px-4 py-2 backdrop-blur supports-[backdrop-filter]:bg-panel/80 lg:hidden">
|
||||||
|
<span className="flex-1 truncate text-xs text-ink-soft">
|
||||||
|
{unresolved > 0 ? `${unresolved} 项冲突待裁决` : "可验收本章"}
|
||||||
|
</span>
|
||||||
|
{unresolved > 0 ? (
|
||||||
|
<Button
|
||||||
|
onClick={jumpToNextUnresolved}
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<LocateFixed className="h-4 w-4" aria-hidden="true" />
|
||||||
|
跳到未裁决
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
document
|
||||||
|
.getElementById("accept-panel")
|
||||||
|
?.scrollIntoView({ behavior: "smooth", block: "center" })
|
||||||
|
}
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<ClipboardCheck className="h-4 w-4" aria-hidden="true" />
|
||||||
|
去验收
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user