完善 TipTap 编辑器与个人资料页:头像裁剪、Tab 缩进、文章内链、Markdown 工具与 Tooltip,并更新样式与 Feed 缓存。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
freefire
2026-06-16 05:57:15 +08:00
parent e4d1dd139e
commit 57172eb053
56 changed files with 3128 additions and 531 deletions

View File

@@ -5,10 +5,26 @@ import {
NodeViewContent,
type NodeViewProps,
} from '@tiptap/react';
import { LockKeyhole } from 'lucide-react';
import { LockKeyhole, LogOut } from 'lucide-react';
/** 查找光标所在的登录可见节点深度 */
function findMembersOnlyDepth($pos: { depth: number; node: (d: number) => { type: { name: string } } }): number {
for (let d = $pos.depth; d > 0; d -= 1) {
if ($pos.node(d).type.name === 'membersOnly') return d;
}
return -1;
}
/** 编辑态「登录可见」区块视图 */
function MembersOnlyView({ selected }: NodeViewProps) {
function MembersOnlyView({ selected, editor }: NodeViewProps) {
const handleExit = () => {
editor.chain().focus().exitMembersOnly().run();
};
const handleUnwrap = () => {
editor.chain().focus().unwrapMembersOnly().run();
};
return (
<NodeViewWrapper
as="members-only"
@@ -19,6 +35,26 @@ function MembersOnlyView({ selected }: NodeViewProps) {
<LockKeyhole size={12} />
</span>
<span></span>
<button
type="button"
className="post-members-only__exit-btn"
title="Ctrl+Enter 退出到公开区域"
onMouseDown={e => e.preventDefault()}
onClick={handleExit}
>
<LogOut size={11} />
退
</button>
<span className="post-members-only__shortcut-hint">Ctrl+Enter 退</span>
<button
type="button"
className="post-members-only__unwrap-btn"
title="取消登录可见包裹,保留正文"
onMouseDown={e => e.preventDefault()}
onClick={handleUnwrap}
>
</button>
</div>
<NodeViewContent className="post-members-only__body" />
</NodeViewWrapper>
@@ -30,6 +66,8 @@ declare module '@tiptap/core' {
membersOnly: {
insertMembersOnly: () => ReturnType;
wrapMembersOnly: () => ReturnType;
exitMembersOnly: () => ReturnType;
unwrapMembersOnly: () => ReturnType;
};
}
}
@@ -54,15 +92,37 @@ export const MembersOnly = Node.create({
return ReactNodeViewRenderer(MembersOnlyView);
},
addKeyboardShortcuts() {
return {
// 在区块末尾空行按 Enter 时退出到公开区域
Enter: ({ editor }) => {
const { $from, empty } = editor.state.selection;
if (!empty) return false;
const depth = findMembersOnlyDepth($from);
if (depth < 0) return false;
const parent = $from.parent;
const atBlockEnd = $from.parentOffset === parent.content.size;
const isEmptyBlock = parent.textContent.trim().length === 0;
if (!atBlockEnd || !isEmptyBlock) return false;
return editor.commands.exitMembersOnly();
},
// Ctrl+Enter / Cmd+Enter 退出到公开区域
'Mod-Enter': ({ editor }) => {
if (!editor.isActive('membersOnly')) return false;
return editor.commands.exitMembersOnly();
},
};
},
addCommands() {
return {
insertMembersOnly: () => ({ chain }) => chain()
.insertContent({
type: this.name,
content: [{
type: 'paragraph',
content: [{ type: 'text', text: '在此输入仅登录用户可见的内容…' }],
}],
content: [{ type: 'paragraph' }],
})
.run(),
@@ -79,6 +139,33 @@ export const MembersOnly = Node.create({
}
return true;
},
exitMembersOnly: () => ({ state, chain }) => {
const { $from } = state.selection;
const depth = findMembersOnlyDepth($from);
if (depth < 0) return false;
const pos = $from.before(depth);
const node = $from.node(depth);
const end = pos + node.nodeSize;
return chain()
.insertContentAt(end, { type: 'paragraph' })
.setTextSelection(end + 1)
.run();
},
unwrapMembersOnly: () => ({ tr, state, dispatch }) => {
const { $from } = state.selection;
const depth = findMembersOnlyDepth($from);
if (depth < 0) return false;
const pos = $from.before(depth);
const node = $from.node(depth);
tr.replaceWith(pos, pos + node.nodeSize, node.content);
if (dispatch) dispatch(tr);
return true;
},
};
},
});