fix: 链接对话框站内搜索收进高级选项
默认只显示网址与文字;展开高级选项后再搜站内内容,点选时同步更新链接文字。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user