feat: 优化单页编辑、列表留白与友链申请体验
单页全屏编辑并禁用内容门控;对齐 Feed 留白;友链申请区前置并收敛弹框样式;略增大帖子列表标题字号。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
||||
cycleMarkdownHeading,
|
||||
insertMarkdownMembersOnly,
|
||||
insertMarkdownReplyOnly,
|
||||
insertMarkdownPointsOnly,
|
||||
insertMarkdownLink,
|
||||
} from '../utils/markdownFormat';
|
||||
import { countWords } from '../utils/text';
|
||||
@@ -63,6 +64,11 @@ interface Props {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
placeholder?: string;
|
||||
/**
|
||||
* 是否启用登录/回复/积分可见区块。
|
||||
* 发帖默认 true;自定义单页等场景应关闭。
|
||||
*/
|
||||
enableContentGates?: boolean;
|
||||
}
|
||||
|
||||
type EditorMode = 'rich' | 'markdown';
|
||||
@@ -211,7 +217,7 @@ function renderToolButtons(tools: ToolBtn[]) {
|
||||
}
|
||||
|
||||
const ArticleEditor = forwardRef<ArticleEditorHandle, Props>(function ArticleEditor(
|
||||
{ value, onChange, placeholder = '在此撰写正文…' },
|
||||
{ value, onChange, placeholder = '在此撰写正文…', enableContentGates = true },
|
||||
ref,
|
||||
) {
|
||||
const isInternalUpdate = useRef(false);
|
||||
@@ -274,9 +280,7 @@ const ArticleEditor = forwardRef<ArticleEditorHandle, Props>(function ArticleEdi
|
||||
},
|
||||
includeChildren: true,
|
||||
}),
|
||||
MembersOnly,
|
||||
ReplyOnly,
|
||||
PointsOnly,
|
||||
...(enableContentGates ? [MembersOnly, ReplyOnly, PointsOnly] : []),
|
||||
TabIndent,
|
||||
],
|
||||
content: sanitizeHtml(value) || '',
|
||||
@@ -698,65 +702,81 @@ const ArticleEditor = forwardRef<ArticleEditorHandle, Props>(function ArticleEdi
|
||||
);
|
||||
}
|
||||
|
||||
tools.push(
|
||||
{
|
||||
icon: <LockKeyhole size={15} />,
|
||||
title: '登录可见',
|
||||
hint: '插入或包裹;区块内 Ctrl+Enter 退出',
|
||||
active: editor.isActive('membersOnly'),
|
||||
className: 'article-tool-btn--members',
|
||||
action: wrapMembersOnly,
|
||||
},
|
||||
{
|
||||
icon: <MessageSquareLock size={15} />,
|
||||
title: '回复可见',
|
||||
hint: '读者回复后才可见;区块内 Ctrl+Enter 退出',
|
||||
active: editor.isActive('replyOnly'),
|
||||
className: 'article-tool-btn--reply',
|
||||
action: wrapReplyOnly,
|
||||
},
|
||||
{
|
||||
icon: <Coins size={15} />,
|
||||
title: '积分可见',
|
||||
hint: '读者花费积分解锁;可设价格',
|
||||
active: editor.isActive('pointsOnly'),
|
||||
className: 'article-tool-btn--points',
|
||||
action: wrapPointsOnly,
|
||||
},
|
||||
);
|
||||
if (enableContentGates) {
|
||||
tools.push(
|
||||
{
|
||||
icon: <LockKeyhole size={15} />,
|
||||
title: '登录可见',
|
||||
hint: '插入或包裹;区块内 Ctrl+Enter 退出',
|
||||
active: editor.isActive('membersOnly'),
|
||||
className: 'article-tool-btn--members',
|
||||
action: wrapMembersOnly,
|
||||
},
|
||||
{
|
||||
icon: <MessageSquareLock size={15} />,
|
||||
title: '回复可见',
|
||||
hint: '读者回复后才可见;区块内 Ctrl+Enter 退出',
|
||||
active: editor.isActive('replyOnly'),
|
||||
className: 'article-tool-btn--reply',
|
||||
action: wrapReplyOnly,
|
||||
},
|
||||
{
|
||||
icon: <Coins size={15} />,
|
||||
title: '积分可见',
|
||||
hint: '读者花费积分解锁;可设价格',
|
||||
active: editor.isActive('pointsOnly'),
|
||||
className: 'article-tool-btn--points',
|
||||
action: wrapPointsOnly,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return tools;
|
||||
}, [editor, openLinkDialog, openCodeBlockDialog, openTableDialog, setImage, wrapMembersOnly, wrapReplyOnly, wrapPointsOnly, wrapSelectedAsGroup, setImageDisplay]);
|
||||
}, [editor, enableContentGates, openLinkDialog, openCodeBlockDialog, openTableDialog, setImage, wrapMembersOnly, wrapReplyOnly, wrapPointsOnly, wrapSelectedAsGroup, setImageDisplay]);
|
||||
|
||||
const buildMarkdownTools = useCallback((): ToolBtn[] => [
|
||||
{ icon: <strong>H</strong>, title: '标题', hint: 'H2 → H6 循环', action: withMarkdown(cycleMarkdownHeading) },
|
||||
{ icon: <Bold size={15} />, title: '加粗', action: withMarkdown((ta, v, ch) => wrapMarkdownSelection(ta, v, '**', '**', '加粗文字', ch)) },
|
||||
{ icon: <Italic size={15} />, title: '斜体', action: withMarkdown((ta, v, ch) => wrapMarkdownSelection(ta, v, '*', '*', '斜体文字', ch)) },
|
||||
{ icon: <UnderlineIcon size={15} />, title: '下划线', action: withMarkdown((ta, v, ch) => wrapMarkdownSelection(ta, v, '<u>', '</u>', '下划线文字', ch)) },
|
||||
{ icon: <Strikethrough size={15} />, title: '删除线', action: withMarkdown((ta, v, ch) => wrapMarkdownSelection(ta, v, '~~', '~~', '删除线文字', ch)) },
|
||||
{ icon: <Minus size={15} />, title: '分割线', action: withMarkdown((ta, v, ch) => insertAtCursor(ta, v, '\n\n---\n\n', ch)) },
|
||||
{ icon: <Quote size={15} />, title: '引用', action: withMarkdown((ta, v, ch) => prefixMarkdownLines(ta, v, '> ', ch)) },
|
||||
{ icon: <List size={15} />, title: '无序列表', action: withMarkdown((ta, v, ch) => prefixMarkdownLines(ta, v, '- ', ch)) },
|
||||
{ icon: <ListOrdered size={15} />, title: '有序列表', action: withMarkdown((ta, v, ch) => prefixMarkdownLines(ta, v, '1. ', ch)) },
|
||||
{ icon: <Code size={15} />, title: '代码块', hint: '语言、行号与折叠', action: () => openCodeBlockDialog('markdown') },
|
||||
{ icon: <TableIcon size={15} />, title: '表格', hint: '插入 GFM 管道表', action: () => openTableDialog('markdown') },
|
||||
{ icon: <LinkIcon size={15} />, title: '链接', action: () => openLinkDialog('markdown') },
|
||||
{ icon: <ImageIcon size={15} />, title: '上传图片', action: insertMarkdownImage },
|
||||
{
|
||||
icon: <LockKeyhole size={15} />,
|
||||
title: '登录可见',
|
||||
hint: '插入 <members-only> 区块',
|
||||
className: 'article-tool-btn--members',
|
||||
action: withMarkdown(insertMarkdownMembersOnly),
|
||||
},
|
||||
{
|
||||
icon: <MessageSquareLock size={15} />,
|
||||
title: '回复可见',
|
||||
hint: '插入 <reply-only> 区块',
|
||||
className: 'article-tool-btn--reply',
|
||||
action: withMarkdown(insertMarkdownReplyOnly),
|
||||
},
|
||||
], [withMarkdown, openLinkDialog, openCodeBlockDialog, openTableDialog, insertMarkdownImage]);
|
||||
const buildMarkdownTools = useCallback((): ToolBtn[] => {
|
||||
const tools: ToolBtn[] = [
|
||||
{ icon: <strong>H</strong>, title: '标题', hint: 'H2 → H6 循环', action: withMarkdown(cycleMarkdownHeading) },
|
||||
{ icon: <Bold size={15} />, title: '加粗', action: withMarkdown((ta, v, ch) => wrapMarkdownSelection(ta, v, '**', '**', '加粗文字', ch)) },
|
||||
{ icon: <Italic size={15} />, title: '斜体', action: withMarkdown((ta, v, ch) => wrapMarkdownSelection(ta, v, '*', '*', '斜体文字', ch)) },
|
||||
{ icon: <UnderlineIcon size={15} />, title: '下划线', action: withMarkdown((ta, v, ch) => wrapMarkdownSelection(ta, v, '<u>', '</u>', '下划线文字', ch)) },
|
||||
{ icon: <Strikethrough size={15} />, title: '删除线', action: withMarkdown((ta, v, ch) => wrapMarkdownSelection(ta, v, '~~', '~~', '删除线文字', ch)) },
|
||||
{ icon: <Minus size={15} />, title: '分割线', action: withMarkdown((ta, v, ch) => insertAtCursor(ta, v, '\n\n---\n\n', ch)) },
|
||||
{ icon: <Quote size={15} />, title: '引用', action: withMarkdown((ta, v, ch) => prefixMarkdownLines(ta, v, '> ', ch)) },
|
||||
{ icon: <List size={15} />, title: '无序列表', action: withMarkdown((ta, v, ch) => prefixMarkdownLines(ta, v, '- ', ch)) },
|
||||
{ icon: <ListOrdered size={15} />, title: '有序列表', action: withMarkdown((ta, v, ch) => prefixMarkdownLines(ta, v, '1. ', ch)) },
|
||||
{ icon: <Code size={15} />, title: '代码块', hint: '语言、行号与折叠', action: () => openCodeBlockDialog('markdown') },
|
||||
{ icon: <TableIcon size={15} />, title: '表格', hint: '插入 GFM 管道表', action: () => openTableDialog('markdown') },
|
||||
{ icon: <LinkIcon size={15} />, title: '链接', action: () => openLinkDialog('markdown') },
|
||||
{ icon: <ImageIcon size={15} />, title: '上传图片', action: insertMarkdownImage },
|
||||
];
|
||||
if (enableContentGates) {
|
||||
tools.push(
|
||||
{
|
||||
icon: <LockKeyhole size={15} />,
|
||||
title: '登录可见',
|
||||
hint: '插入 <members-only> 区块',
|
||||
className: 'article-tool-btn--members',
|
||||
action: withMarkdown(insertMarkdownMembersOnly),
|
||||
},
|
||||
{
|
||||
icon: <MessageSquareLock size={15} />,
|
||||
title: '回复可见',
|
||||
hint: '插入 <reply-only> 区块',
|
||||
className: 'article-tool-btn--reply',
|
||||
action: withMarkdown(insertMarkdownReplyOnly),
|
||||
},
|
||||
{
|
||||
icon: <Coins size={15} />,
|
||||
title: '积分可见',
|
||||
hint: '插入 <points-only> 区块',
|
||||
className: 'article-tool-btn--points',
|
||||
action: withMarkdown(insertMarkdownPointsOnly),
|
||||
},
|
||||
);
|
||||
}
|
||||
return tools;
|
||||
}, [enableContentGates, withMarkdown, openLinkDialog, openCodeBlockDialog, openTableDialog, insertMarkdownImage]);
|
||||
|
||||
const tools = mode === 'rich' ? buildRichTools() : buildMarkdownTools();
|
||||
const words = mode === 'markdown'
|
||||
|
||||
@@ -188,35 +188,43 @@ export default function FriendLinkApplyDialog({ open, onOpenChange, editApply, o
|
||||
>
|
||||
友链在我的网站首页
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={linkPlacement === 'custom'}
|
||||
<div
|
||||
className={cn(
|
||||
'friend-link-apply-placement__option',
|
||||
linkPlacement === 'custom' && 'friend-link-apply-placement__option--active',
|
||||
'friend-link-apply-placement__custom',
|
||||
linkPlacement === 'custom' && 'friend-link-apply-placement__custom--open',
|
||||
)}
|
||||
onClick={() => setLinkPlacement('custom')}
|
||||
>
|
||||
友链在其它页面
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={linkPlacement === 'custom'}
|
||||
className={cn(
|
||||
'friend-link-apply-placement__option',
|
||||
linkPlacement === 'custom' && 'friend-link-apply-placement__option--active',
|
||||
)}
|
||||
onClick={() => setLinkPlacement('custom')}
|
||||
>
|
||||
友链在其它页面
|
||||
</button>
|
||||
<div className="friend-link-apply-placement__custom-panel" aria-hidden={linkPlacement !== 'custom'}>
|
||||
<div className="friend-link-apply-placement__custom-inner">
|
||||
<Label htmlFor="friend-link-reciprocal">添加我方链接的页面地址</Label>
|
||||
<Input
|
||||
id="friend-link-reciprocal"
|
||||
value={reciprocalPageURL}
|
||||
onChange={e => setReciprocalPageURL(e.target.value)}
|
||||
placeholder="如:https://您的域名/link.htm"
|
||||
maxLength={512}
|
||||
tabIndex={linkPlacement === 'custom' ? 0 : -1}
|
||||
/>
|
||||
<p className="friend-link-apply-field__hint">
|
||||
请填写实际放置本站友链的页面,提交后将在后台检测该页面
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{linkPlacement === 'custom' && (
|
||||
<div className="friend-link-apply-field">
|
||||
<Label htmlFor="friend-link-reciprocal">添加我方链接的页面地址</Label>
|
||||
<Input
|
||||
id="friend-link-reciprocal"
|
||||
value={reciprocalPageURL}
|
||||
onChange={e => setReciprocalPageURL(e.target.value)}
|
||||
placeholder="如:https://您的域名/link.htm"
|
||||
maxLength={512}
|
||||
/>
|
||||
<p className="friend-link-apply-field__hint">
|
||||
请填写实际放置本站友链的页面,提交后将在后台检测该页面
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="friend-link-apply-field">
|
||||
<Label htmlFor="friend-link-logo">填写或上传网站 LOGO</Label>
|
||||
<div className="friend-link-apply-logo-row">
|
||||
|
||||
@@ -32,23 +32,11 @@ export default function FriendLinkSiteInfo() {
|
||||
</div>
|
||||
<div className="friend-link-site-info__item">
|
||||
<dt>地址</dt>
|
||||
<dd>
|
||||
{siteURL ? (
|
||||
<a href={siteURL} target="_blank" rel="noopener noreferrer">{siteURL}</a>
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</dd>
|
||||
<dd>{siteURL || '—'}</dd>
|
||||
</div>
|
||||
<div className="friend-link-site-info__item">
|
||||
<dt>LOGO</dt>
|
||||
<dd>
|
||||
{siteLogoURL ? (
|
||||
<a href={siteLogoURL} target="_blank" rel="noopener noreferrer">{siteLogoURL}</a>
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</dd>
|
||||
<dd>{siteLogoURL || '—'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user