feat(compose): 正文内嵌隐藏块并升级 Markdown 编辑器

去掉整帖可见性表单,改为 :::hide 按块脱敏;工具栏补齐代码块、链接/图片对话框与粘贴拖拽上传。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-17 04:58:20 +08:00
parent d1155cbd64
commit 01e2fd05ca
19 changed files with 1867 additions and 471 deletions

View File

@@ -1320,9 +1320,137 @@ body {
color: var(--accent);
}
.j13-md-tool:disabled { opacity: 0.4; }
.j13-md-tool:focus-visible {
.j13-md-tool.is-active {
background: var(--accent-soft);
color: var(--accent);
}
.j13-md-sep {
width: 1px;
height: 18px;
margin: 0 4px;
background: var(--line);
flex-shrink: 0;
}
.j13-md-popover-anchor {
position: relative;
}
.j13-md-popover {
position: absolute;
top: calc(100% + 6px);
left: 0;
z-index: 40;
min-width: 168px;
padding: 6px;
border-radius: 12px;
background: var(--panel);
border: 1px solid var(--line);
box-shadow: var(--shadow-lg);
display: flex;
flex-direction: column;
gap: 2px;
}
.j13-md-popover-wide {
min-width: 200px;
max-height: 280px;
}
.j13-md-popover-form {
min-width: 260px;
padding: 10px;
gap: 8px;
}
.j13-md-popover button[role="menuitem"],
.j13-md-menu-item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
text-align: left;
padding: 8px 10px;
border-radius: 8px;
font-size: 13px;
color: var(--ink);
background: transparent;
}
.j13-md-popover button[role="menuitem"]:hover,
.j13-md-menu-item:hover {
background: var(--accent-soft);
color: var(--accent);
}
.j13-md-popover-input {
width: 100%;
height: 34px;
padding: 0 10px;
border-radius: 8px;
border: 1px solid var(--line);
background: var(--bg);
color: var(--ink);
font-size: 13px;
}
.j13-md-popover-input:focus {
outline: 2px solid var(--accent);
outline-offset: 1px;
outline-offset: 0;
}
.j13-md-popover-list {
max-height: 200px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1px;
}
.j13-md-popover-list button {
text-align: left;
padding: 7px 10px;
border-radius: 8px;
font-size: 12.5px;
font-family: var(--font-mono, ui-monospace, monospace);
color: var(--ink-2);
}
.j13-md-popover-list button:hover {
background: var(--accent-soft);
color: var(--accent);
}
.j13-md-field {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
color: var(--ink-3);
}
.j13-md-popover-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 2px;
}
.btn-sm {
height: 30px;
padding: 0 12px;
font-size: 12.5px;
}
.j13-md-write-wrap {
position: relative;
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 0;
}
.j13-md-write-wrap.is-dragover .j13-md-textarea {
opacity: 0.35;
}
.j13-md-drop-mask {
position: absolute;
inset: 8px 0;
z-index: 5;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
border: 2px dashed var(--accent);
background: color-mix(in srgb, var(--accent-soft) 80%, transparent);
color: var(--accent);
font-weight: 600;
font-size: 14px;
pointer-events: none;
}
.j13-md-mode {
display: inline-flex;
@@ -1394,6 +1522,66 @@ body {
background: transparent;
}
/* 正文内嵌隐藏块 */
.j13-hide-block {
margin: 1.1rem 0;
border-radius: 14px;
border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--line));
background: color-mix(in srgb, var(--accent-soft) 55%, var(--panel));
overflow: hidden;
}
.j13-hide-block-bar {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
border-bottom: 1px solid color-mix(in srgb, var(--accent) 14%, var(--line));
}
.j13-hide-badge {
display: inline-flex;
align-items: center;
height: 22px;
padding: 0 9px;
border-radius: 999px;
font-size: 11.5px;
font-weight: 600;
color: var(--accent);
background: var(--accent-soft);
}
.j13-hide-block-body {
padding: 12px 16px 14px;
}
.j13-hide-block-body > :first-child { margin-top: 0; }
.j13-hide-block-body > :last-child { margin-bottom: 0; }
.j13-hide-gate {
margin: 1.1rem 0;
border-radius: 14px;
padding: 1.35rem 1.25rem;
text-align: center;
background: var(--accent-soft);
border: 1px dashed color-mix(in srgb, var(--accent) 35%, transparent);
}
.j13-hide-gate-icon {
margin: 0 auto 0.55rem;
color: var(--accent);
}
.j13-hide-gate-title {
font-weight: 700;
font-size: 15px;
color: var(--ink);
margin-bottom: 0.25rem;
}
.j13-hide-gate-desc {
margin-bottom: 0.9rem;
font-size: 13px;
}
.j13-hide-gate-actions {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
.j13-compose-shell {
background: var(--panel);
border: 1px solid var(--line);

View File

@@ -9,8 +9,6 @@ type Props = {
initialContent: string;
initialTags: string;
initialPostType?: string;
initialContentAccess?: string;
initialAccessPoints?: number;
initialTypeMeta?: string;
initialAttachments?: PostAttachment[];
canEditPollOptions?: boolean;
@@ -26,8 +24,6 @@ export default function EditPostClient({
initialContent,
initialTags,
initialPostType,
initialContentAccess,
initialAccessPoints,
initialTypeMeta,
initialAttachments,
canEditPollOptions,
@@ -44,8 +40,6 @@ export default function EditPostClient({
initialContent={initialContent}
initialTags={initialTags}
initialPostType={initialPostType}
initialContentAccess={initialContentAccess}
initialAccessPoints={initialAccessPoints}
initialTypeMeta={initialTypeMeta}
initialAttachments={initialAttachments}
canEditPollOptions={canEditPollOptions}

View File

@@ -60,8 +60,6 @@ export default async function EditPostPage({ params }: PageProps) {
initialContent={post.content}
initialTags={post.tags || ""}
initialPostType={post.post_type}
initialContentAccess={post.content_access}
initialAccessPoints={post.access_points}
initialTypeMeta={post.type_meta}
initialAttachments={post.attachments || []}
canEditPollOptions={post.poll?.can_edit_options !== false}

View File

@@ -34,7 +34,7 @@ import MobilePostBar from "@/components/MobilePostBar";
import Avatar from "@/components/Avatar";
import RoleBadge from "@/components/RoleBadge";
import MarkdownBody from "@/components/MarkdownBody";
import ContentLockGate from "@/components/ContentLockGate";
import { publicMarkdownForSeo } from "@/lib/hideBlocks";
import PostAttachments from "@/components/PostAttachments";
import PostInteractPanels, { QuestionStatusCard } from "@/components/PostInteractPanels";
import AcceptedAnswerCard from "@/components/AcceptedAnswerCard";
@@ -60,9 +60,10 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
robots: { index: false, follow: false },
};
}
const desc = post.content_locked
? post.access_hint || "部分内容需满足条件后可见"
: (post.content || "").slice(0, 160);
const publicBody = publicMarkdownForSeo(post.content || "");
const desc = publicBody
? publicBody.slice(0, 160)
: post.access_hint || "部分内容需满足条件后可见";
return {
title: post.title,
description: desc,
@@ -481,10 +482,11 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
const related = (relatedData?.posts ?? []).filter((p) => p.id !== post.id).slice(0, 5);
const authorPosts = (profile?.posts ?? []).filter((p) => p.id !== post.id).slice(0, 5);
const toc = post.content_locked ? [] : extractToc(post.content);
const publicBody = publicMarkdownForSeo(post.content || "");
const toc = extractToc(publicBody);
const readingMinutes = Math.max(
1,
Math.ceil((post.content_locked ? post.title.length : post.content.length) / 400)
Math.ceil((publicBody.length > 0 ? publicBody.length : post.title.length) / 400)
);
const edited =
new Date(post.updated_at).getTime() - new Date(post.created_at).getTime() > 60_000;
@@ -527,8 +529,8 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
"@context": "https://schema.org",
"@type": "DiscussionForumPosting",
headline: post.title,
// 锁定正文不进 JSON-LD,避免 SEO 泄露付费/回复可见内容
articleBody: post.content_locked ? undefined : post.content,
// 仅公开片段进 JSON-LD,避免 SEO 泄露隐藏块原文
articleBody: publicBody || undefined,
datePublished: post.created_at,
dateModified: post.updated_at,
author: { "@type": "Person", name: post.user.nickname },
@@ -541,7 +543,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
...(allowComments &&
acceptedAnswer &&
!acceptedAnswer.deleted &&
!post.content_locked && {
!!publicBody && {
acceptedAnswer: {
"@type": "Comment",
text: acceptedAnswer.content,
@@ -718,24 +720,14 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
</div>
)}
{post.content_locked ? (
<ContentLockGate
postId={post.id}
hint={post.access_hint}
access={post.content_access}
points={post.access_points}
loggedIn={!!me.user}
/>
) : (
<MarkdownBody
content={post.content}
components={{ h2: headingRenderer("h2"), h3: headingRenderer("h3") }}
/>
)}
<MarkdownBody
content={post.content}
postId={post.id}
loggedIn={!!me.user}
components={{ h2: headingRenderer("h2"), h3: headingRenderer("h3") }}
/>
{!post.content_locked && (
<PostAttachments postId={post.id} items={post.attachments || []} />
)}
<PostAttachments postId={post.id} items={post.attachments || []} />
{/* 类型特殊块(投票/悬赏/抽奖/问答)统一放正文最下方 */}
<PostInteractPanels
@@ -745,7 +737,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
lottery={post.lottery}
loggedIn={!!me.user}
/>
{!post.content_locked && allowComments && acceptedAnswer ? (
{allowComments && acceptedAnswer ? (
<AcceptedAnswerCard
answer={acceptedAnswer}
variant={acceptedAnswerVariant}