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

@@ -17,13 +17,11 @@ import {
} from "@/lib/api";
import {
POST_TYPES,
CONTENT_ACCESS,
BOUNTY_EXPIRE_DAYS,
LOTTERY_EXPIRE_DAYS,
POLL_EXPIRE_DAYS,
normalizePostType,
type PostTypeValue,
type ContentAccessValue,
} from "@/lib/postMeta";
import { toast } from "@/lib/toast";
import MarkdownEditor from "@/components/MarkdownEditor";
@@ -44,8 +42,6 @@ type ComposeDraft = {
tags: string;
boardId: number;
postType?: string;
contentAccess?: string;
accessPoints?: number;
typeMeta?: string;
updatedAt: number;
};
@@ -128,8 +124,6 @@ type EditProps = {
initialContent: string;
initialTags?: string;
initialPostType?: string;
initialContentAccess?: string;
initialAccessPoints?: number;
initialTypeMeta?: string;
initialAttachments?: PostAttachment[];
/** 投票帖:有票后禁止改选项 */
@@ -162,14 +156,6 @@ export default function PostComposer(props: PostComposerProps) {
const [postType, setPostType] = useState<PostTypeValue>(
normalizePostType(isCreate ? "discussion" : props.initialPostType)
);
const [contentAccess, setContentAccess] = useState<ContentAccessValue>(
(isCreate
? "public"
: (props.initialContentAccess as ContentAccessValue) || "public") as ContentAccessValue
);
const [accessPoints, setAccessPoints] = useState(
isCreate ? 10 : props.initialAccessPoints || 10
);
const [typeMeta, setTypeMeta] = useState(
isCreate ? "" : props.initialTypeMeta || ""
);
@@ -364,8 +350,6 @@ export default function PostComposer(props: PostComposerProps) {
tags: serializeTags(tagList),
boardId,
postType,
contentAccess,
accessPoints,
typeMeta,
updatedAt: Date.now(),
});
@@ -380,8 +364,6 @@ export default function PostComposer(props: PostComposerProps) {
tagList,
boardId,
postType,
contentAccess,
accessPoints,
typeMeta,
]);
@@ -392,10 +374,6 @@ export default function PostComposer(props: PostComposerProps) {
setTagList(parseTags(draftBanner.tags || ""));
if (draftBanner.boardId) setBoardId(draftBanner.boardId);
if (draftBanner.postType) setPostType(normalizePostType(draftBanner.postType));
if (draftBanner.contentAccess) {
setContentAccess(draftBanner.contentAccess as ContentAccessValue);
}
if (draftBanner.accessPoints) setAccessPoints(draftBanner.accessPoints);
if (draftBanner.typeMeta) setTypeMeta(draftBanner.typeMeta);
setDraftBanner(null);
skipDraftSave.current = false;
@@ -533,7 +511,6 @@ export default function PostComposer(props: PostComposerProps) {
title.trim().length > 0 &&
content.trim().length > 0 &&
(isCreate ? boardsReady && boardId > 0 : true) &&
(contentAccess !== "points" || accessPoints > 0) &&
(postType !== "poll" ||
pollOptions.map((o) => o.trim()).filter(Boolean).length >= 2) &&
(postType !== "bounty" ||
@@ -609,8 +586,6 @@ export default function PostComposer(props: PostComposerProps) {
content: content.trim(),
tags,
post_type: postType,
content_access: contentAccess,
access_points: contentAccess === "points" ? accessPoints : 0,
type_meta: meta,
attachment_ids: attIds,
});
@@ -634,8 +609,6 @@ export default function PostComposer(props: PostComposerProps) {
title: title.trim(),
content: content.trim(),
tags,
content_access: contentAccess,
access_points: contentAccess === "points" ? accessPoints : 0,
type_meta: meta,
attachment_ids: attIds,
});
@@ -818,42 +791,6 @@ export default function PostComposer(props: PostComposerProps) {
/>
</div>
</div>
<div className="j13-compose-meta-access">
<label className="field-label" htmlFor="compose-access">
正文可见性
</label>
<select
id="compose-access"
className="j13-compose-field"
value={contentAccess}
disabled={loading}
onChange={(e) => setContentAccess(e.target.value as ContentAccessValue)}
>
{CONTENT_ACCESS.map((a) => (
<option key={a.value} value={a.value}>
{a.label}
</option>
))}
</select>
{contentAccess === "points" && (
<div className="mt-2 flex items-center gap-2">
<label className="meta text-[12.5px] shrink-0" htmlFor="compose-access-pts">
所需积分
</label>
<input
id="compose-access-pts"
type="number"
min={1}
max={100000}
value={accessPoints}
disabled={loading}
onChange={(e) => setAccessPoints(Number(e.target.value) || 0)}
className="j13-compose-field !py-1.5 w-28"
/>
</div>
)}
</div>
</div>
</section>
@@ -1241,7 +1178,7 @@ export default function PostComposer(props: PostComposerProps) {
disabled={loading}
rows={18}
placeholder={
"从这里开始写正文…\n\n支持 Markdown;可用工具栏插入格式与图片。可点全屏沉浸写作。"
"从这里开始写正文…\n\n工具栏可插入格式、代码块、链接、图片与隐藏内容(登录/回复/积分可见)。支持粘贴与拖拽上传图片。"
}
onSubmitShortcut={() => {
if (canSubmit) void handleSubmit();