feat(compose): 正文内嵌隐藏块并升级 Markdown 编辑器
去掉整帖可见性表单,改为 :::hide 按块脱敏;工具栏补齐代码块、链接/图片对话框与粘贴拖拽上传。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user