fix: 链接对话框站内搜索收进高级选项

默认只显示网址与文字;展开高级选项后再搜站内内容,点选时同步更新链接文字。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-01 21:33:24 +08:00
parent d5aa36416c
commit ea9e0058d4
2 changed files with 105 additions and 54 deletions

View File

@@ -14,7 +14,7 @@ import { api } from '@/api/client';
import type { PostItem, SitePageSummary } from '@/api/types'; import type { PostItem, SitePageSummary } from '@/api/types';
import { pagePath, postPath } from '@/utils/permalink'; import { pagePath, postPath } from '@/utils/permalink';
import { useForumLimits } from '@/hooks/useForumLimits'; import { useForumLimits } from '@/hooks/useForumLimits';
import { Loader2 } from 'lucide-react'; import { ChevronDown, Loader2 } from 'lucide-react';
export interface ArticleLinkConfirm { export interface ArticleLinkConfirm {
url: string; url: string;
@@ -69,6 +69,7 @@ export function ArticleLinkDialog({
const { limits } = useForumLimits(); const { limits } = useForumLimits();
const [url, setUrl] = useState(''); const [url, setUrl] = useState('');
const [text, setText] = useState(''); const [text, setText] = useState('');
const [advancedOpen, setAdvancedOpen] = useState(false);
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [debouncedQuery, setDebouncedQuery] = useState(''); const [debouncedQuery, setDebouncedQuery] = useState('');
const [posts, setPosts] = useState<PostItem[]>([]); const [posts, setPosts] = useState<PostItem[]>([]);
@@ -80,15 +81,19 @@ export function ArticleLinkDialog({
if (!open) return; if (!open) return;
setUrl(initialUrl || ''); setUrl(initialUrl || '');
setText(initialText || ''); setText(initialText || '');
setAdvancedOpen(false);
setQuery(''); setQuery('');
setDebouncedQuery(''); setDebouncedQuery('');
setPosts([]);
setPages([]);
setLoaded(false);
}, [open, initialUrl, initialText]); }, [open, initialUrl, initialText]);
useEffect(() => { useEffect(() => {
if (!open) return; if (!open || !advancedOpen) return;
const t = window.setTimeout(() => setDebouncedQuery(query.trim()), SEARCH_DEBOUNCE_MS); const t = window.setTimeout(() => setDebouncedQuery(query.trim()), SEARCH_DEBOUNCE_MS);
return () => window.clearTimeout(t); return () => window.clearTimeout(t);
}, [query, open]); }, [query, open, advancedOpen]);
const loadSiteHits = useCallback(async (keyword: string) => { const loadSiteHits = useCallback(async (keyword: string) => {
setLoading(true); setLoading(true);
@@ -120,10 +125,11 @@ export function ArticleLinkDialog({
} }
}, []); }, []);
// 仅展开高级选项时再请求站内内容,避免默认打开就打接口
useEffect(() => { useEffect(() => {
if (!open) return; if (!open || !advancedOpen) return;
void loadSiteHits(debouncedQuery); void loadSiteHits(debouncedQuery);
}, [open, debouncedQuery, loadSiteHits]); }, [open, advancedOpen, debouncedQuery, loadSiteHits]);
const hits: SiteHit[] = useMemo(() => { const hits: SiteHit[] = useMemo(() => {
const postHits: SiteHit[] = posts.map(p => ({ const postHits: SiteHit[] = posts.map(p => ({
@@ -143,7 +149,8 @@ export function ArticleLinkDialog({
const pickHit = (hit: SiteHit) => { const pickHit = (hit: SiteHit) => {
setUrl(hit.url); setUrl(hit.url);
setText(prev => (prev.trim() ? prev : hit.title)); // 选站内内容时同步用标题作为链接文字
setText(hit.title);
}; };
const handleConfirm = () => { const handleConfirm = () => {
@@ -171,7 +178,6 @@ export function ArticleLinkDialog({
</DialogHeader> </DialogHeader>
<section className="article-link-dialog__section"> <section className="article-link-dialog__section">
<h3 className="article-link-dialog__section-title"> URL</h3>
<div className="article-link-dialog__field"> <div className="article-link-dialog__field">
<Label htmlFor="article-link-url"></Label> <Label htmlFor="article-link-url"></Label>
<Input <Input
@@ -206,55 +212,68 @@ export function ArticleLinkDialog({
</div> </div>
</section> </section>
<section className="article-link-dialog__section"> <section className="article-link-dialog__section article-link-dialog__advanced">
<h3 className="article-link-dialog__section-title"></h3> <button
<div className="article-link-dialog__field"> type="button"
<Label htmlFor="article-link-search"></Label> className={`article-link-dialog__advanced-toggle${advancedOpen ? ' is-open' : ''}`}
<Input aria-expanded={advancedOpen}
id="article-link-search" onClick={() => setAdvancedOpen(v => !v)}
type="search" >
value={query} <span></span>
placeholder="搜索帖子或单页…" <ChevronDown size={16} aria-hidden className="article-link-dialog__advanced-chevron" />
onChange={e => setQuery(e.target.value)} </button>
/> {advancedOpen ? (
</div> <div className="article-link-dialog__advanced-body">
<div className="article-link-dialog__hits"> <h3 className="article-link-dialog__section-title"></h3>
<p className="article-link-dialog__hits-hint"> <div className="article-link-dialog__field">
{debouncedQuery <Label htmlFor="article-link-search"></Label>
? `搜索「${debouncedQuery}` <Input
: '未指定搜索条件。自动显示最近发布条目。'} id="article-link-search"
</p> type="search"
{loading && !loaded ? ( value={query}
<div className="article-link-dialog__hits-empty"> placeholder="搜索帖子或单页…"
<Loader2 size={18} className="article-link-dialog__spin" /> onChange={e => setQuery(e.target.value)}
<span></span> />
</div> </div>
) : !hits.length ? ( <div className="article-link-dialog__hits">
<div className="article-link-dialog__hits-empty"></div> <p className="article-link-dialog__hits-hint">
) : ( {debouncedQuery
<ul className="article-link-dialog__hit-list"> ? `搜索「${debouncedQuery}`
{hits.map(hit => ( : '未指定搜索条件。自动显示最近发布条目。'}
<li key={hit.key}> </p>
<button {loading && !loaded ? (
type="button" <div className="article-link-dialog__hits-empty">
className={`article-link-dialog__hit${url === hit.url ? ' is-selected' : ''}`} <Loader2 size={18} className="article-link-dialog__spin" />
onClick={() => pickHit(hit)} <span></span>
> </div>
<span className="article-link-dialog__hit-title">{hit.title}</span> ) : !hits.length ? (
<span className="article-link-dialog__hit-kind"> <div className="article-link-dialog__hits-empty"></div>
{hit.kind === 'post' ? '帖子' : '单页'} ) : (
</span> <ul className="article-link-dialog__hit-list">
</button> {hits.map(hit => (
</li> <li key={hit.key}>
))} <button
</ul> type="button"
)} className={`article-link-dialog__hit${url === hit.url ? ' is-selected' : ''}`}
{loading && loaded ? ( onClick={() => pickHit(hit)}
<div className="article-link-dialog__hits-loading"> >
<Loader2 size={14} className="article-link-dialog__spin" /> <span className="article-link-dialog__hit-title">{hit.title}</span>
<span className="article-link-dialog__hit-kind">
{hit.kind === 'post' ? '帖子' : '单页'}
</span>
</button>
</li>
))}
</ul>
)}
{loading && loaded ? (
<div className="article-link-dialog__hits-loading">
<Loader2 size={14} className="article-link-dialog__spin" />
</div>
) : null}
</div> </div>
) : null} </div>
</div> ) : null}
</section> </section>
<DialogFooter className="article-link-dialog__footer"> <DialogFooter className="article-link-dialog__footer">

View File

@@ -10990,6 +10990,38 @@ button.profile-stat:hover strong {
font-size: 12px; font-size: 12px;
color: var(--color-text-2); color: var(--color-text-2);
} }
.article-link-dialog__advanced {
padding-bottom: 8px;
}
.article-link-dialog__advanced-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0;
padding: 0;
border: none;
background: transparent;
font-size: 13px;
font-weight: 600;
color: var(--color-text-2);
cursor: pointer;
user-select: none;
}
.article-link-dialog__advanced-toggle:hover {
color: var(--color-text-1);
}
.article-link-dialog__advanced-chevron {
transition: transform 0.15s ease;
}
.article-link-dialog__advanced-toggle.is-open .article-link-dialog__advanced-chevron {
transform: rotate(180deg);
}
.article-link-dialog__advanced-body {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 12px;
}
.article-link-dialog__hits { .article-link-dialog__hits {
position: relative; position: relative;
border: 1px solid var(--j13-border); border: 1px solid var(--j13-border);