feat(web): 查找聊天记录 history search + media grid in the detail panel

Closes a real visual gap flagged directly: the detail panel jumped straight
from the quick-action buttons to 备注/置顶聊天/etc, skipping the "查找聊天
记录" row + 4-icon media grid (图片/视频/文件/链接) that's prominently
visible in the reference design (README §3.4) -- deliberately deferred in
an earlier pass, but a dead-end-free real implementation was worth building
now rather than leaving a visible hole.

- lib/api/messages.ts: searchTopicMessages/getTopicMedia, endpoint shapes
  from docs/IM-前后端对接文档.md:1251-1254 (verified live: both return real
  200s against the actual backend, not just matching the docs on paper).
- HistorySearchModal: centered modal (README §1.5's 弹层 pattern) with the
  five tabs, debounced search (lib/hooks.ts's useDebouncedValue, ported from
  the old app's use-cached-data.js), empty/loading states.
- DetailColumn: wires the row + grid to open the modal on the right tab.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-08 02:27:12 +08:00
co-authored by Claude Sonnet 5
parent 0b48346d02
commit 80bf0aa41f
4 changed files with 216 additions and 2 deletions
+28 -1
View File
@@ -4,6 +4,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { Icon } from '@/app/Icon';
import { HistorySearchModal } from '@/features/messages/HistorySearchModal';
import {
blockUser,
deleteFriend,
@@ -14,6 +15,7 @@ import {
updateConversationSettings,
updateFriendRemark,
} from '@/lib/api/friends';
import type { MediaType } from '@/lib/api/messages';
import { avatarBgClass, avatarLetter } from '@/lib/avatar';
import { useTinode } from '@/lib/tinode/hooks';
@@ -46,6 +48,7 @@ export function DetailColumn({ topicId, peerName, onClose }: DetailColumnProps)
const queryClient = useQueryClient();
const [remarkEditing, setRemarkEditing] = useState(false);
const [remarkDraft, setRemarkDraft] = useState('');
const [historyTab, setHistoryTab] = useState<'text' | MediaType | null>(null);
const friendsQuery = useQuery({ queryKey: ['friends'], queryFn: () => getFriends() });
const friend = friendsQuery.data?.items.find((f) => f.user_id === topicId);
@@ -131,7 +134,29 @@ export function DetailColumn({ topicId, peerName, onClose }: DetailColumnProps)
</div>
</div>
<div className="flex flex-col px-5 pb-2 text-sm">
<div className="px-5 pb-3 text-sm">
<button type="button" onClick={() => setHistoryTab('text')} className="flex w-full items-center justify-between py-2">
<span className="text-text-body">查找聊天记录</span>
<Icon name="chevron_right" className="text-[18px] text-text-tertiary-2" />
</button>
<div className="mt-1 grid grid-cols-4 gap-2">
{(
[
{ type: 'image' as const, label: '图片', icon: 'image' },
{ type: 'video' as const, label: '视频', icon: 'videocam' },
{ type: 'file' as const, label: '文件', icon: 'description' },
{ type: 'link' as const, label: '链接', icon: 'link' },
] satisfies { type: MediaType; label: string; icon: string }[]
).map((item) => (
<button key={item.type} type="button" onClick={() => setHistoryTab(item.type)} className="flex flex-col items-center gap-1 py-1">
<Icon name={item.icon} className="text-[22px] text-text-tertiary-4" />
<span className="text-xs text-text-secondary">{item.label}</span>
</button>
))}
</div>
</div>
<div className="flex flex-col border-t border-list-item px-5 pb-2 text-sm">
{remarkEditing ? (
<div className="flex items-center gap-2 py-3">
<span className="flex-none text-text-body">备注</span>
@@ -195,6 +220,8 @@ export function DetailColumn({ topicId, peerName, onClose }: DetailColumnProps)
>
删除好友
</button>
{historyTab ? <HistorySearchModal topicId={topicId} initialTab={historyTab} onClose={() => setHistoryTab(null)} /> : null}
</aside>
);
}