fix(web): 转发选择器勾选样式改用矢量图标,好友/群聊可分别展开收起

原来的勾选图标是Material Icons字体里的"check"字形,在20px圆圈里塞
14px字体图标,字体本身没有针对这么小的尺寸做过精细调校,稍微放大
一点看就是个不规整的糊状图案。换成手绘SVG勾选路径(矢量、任何缩放
比例下都清晰),并去掉了选中态多余的一层border(原来选中时背景色
和边框色叠在一起,等于多画了一圈没必要的描边)。

好友、群聊两个分组标题现在都可以点击收起/展开(各自独立),列表长
的时候可以先收起不relevant的那一类,收起状态下已经选中的项不会被
清空,"全选"按钮无论展开收起都能点。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-10 19:25:36 +08:00
co-authored by Claude Sonnet 5
parent e3217a5de9
commit 3280b92a09
@@ -14,6 +14,19 @@ interface ForwardPickerModalProps {
onClose: () => void; onClose: () => void;
} }
/** A hand-drawn checkmark instead of the Material Icons font glyph this row used to use -- the font
* glyph's hinting isn't tuned for a 14px render inside a 20px circle, so at anything above 1x pixel
* density it comes out visibly soft/asymmetric where a vector path stays crisp at any scale. Not a
* systemic swap (`Icon` is still how the rest of the app draws icons) -- scoped to this one
* selection indicator, which is the one a user actually screenshotted and flagged as rough-looking. */
function CheckMark({ className = '' }: { className?: string }) {
return (
<svg viewBox="0 0 16 16" fill="none" className={className} aria-hidden="true">
<path d="M3.5 8.5L6.5 11.5L12.5 4.5" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
/** /**
* README §3.6: multi-select targets + an optional comment, sent in one action. Used by the message * README §3.6: multi-select targets + an optional comment, sent in one action. Used by the message
* context menu's single-message "转发", the multi-select bulk bar's "转发", and favorites' bulk * context menu's single-message "转发", the multi-select bulk bar's "转发", and favorites' bulk
@@ -30,6 +43,9 @@ export function ForwardPickerModal({ onPick, onClose }: ForwardPickerModalProps)
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [selected, setSelected] = useState<Set<string>>(new Set()); const [selected, setSelected] = useState<Set<string>>(new Set());
const [comment, setComment] = useState(''); const [comment, setComment] = useState('');
// Both start expanded -- collapsing is for a long list getting in the way of the OTHER section,
// not a default state anyone would want on first open.
const [collapsed, setCollapsed] = useState<{ friends: boolean; groups: boolean }>({ friends: false, groups: false });
useEscapeKey(onClose); useEscapeKey(onClose);
const filtered = contacts.filter((c) => !query.trim() || (c.public?.fn || '').includes(query.trim())); const filtered = contacts.filter((c) => !query.trim() || (c.public?.fn || '').includes(query.trim()));
@@ -75,6 +91,10 @@ export function ForwardPickerModal({ onPick, onClose }: ForwardPickerModalProps)
}); });
}; };
const toggleCollapsed = (section: 'friends' | 'groups') => {
setCollapsed((prev) => ({ ...prev, [section]: !prev[section] }));
};
const send = () => { const send = () => {
if (selected.size === 0) return; if (selected.size === 0) return;
onPick([...selected], comment.trim()); onPick([...selected], comment.trim());
@@ -96,14 +116,26 @@ export function ForwardPickerModal({ onPick, onClose }: ForwardPickerModalProps)
</span> </span>
<span className="min-w-0 flex-1 truncate text-sm text-text-body">{c.public?.fn || c.topic}</span> <span className="min-w-0 flex-1 truncate text-sm text-text-body">{c.public?.fn || c.topic}</span>
<span <span
className={`grid h-5 w-5 flex-none place-items-center rounded-full border ${isSelected ? 'border-accent bg-accent' : 'border-[#d5d9df]'}`} className={`grid h-5 w-5 flex-none place-items-center rounded-full ${isSelected ? 'bg-accent' : 'border-[1.5px] border-[#c5cad2]'}`}
> >
{isSelected ? <Icon name="check" className="text-[14px] text-white" /> : null} {isSelected ? <CheckMark className="h-3 w-3 text-white" /> : null}
</span> </span>
</button> </button>
); );
}; };
const renderSectionHeader = (label: string, section: 'friends' | 'groups', topics: string[], allSelected: boolean) => (
<div className="flex items-center justify-between px-3 pb-1 pt-2">
<button type="button" onClick={() => toggleCollapsed(section)} className="flex items-center gap-0.5 text-xs text-text-tertiary-2">
<Icon name="chevron_right" className={`text-sm transition-transform ${collapsed[section] ? '' : 'rotate-90'}`} />
{label}
</button>
<button type="button" onClick={() => toggleSectionSelectAll(topics, allSelected)} className="text-xs text-accent">
{allSelected ? '取消全选' : '全选'}
</button>
</div>
);
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[#22262d]/25" onClick={onClose}> <div className="fixed inset-0 z-50 flex items-center justify-center bg-[#22262d]/25" onClick={onClose}>
<div className="flex h-[560px] w-[420px] flex-col rounded-xl bg-white shadow-modal" onClick={(e) => e.stopPropagation()}> <div className="flex h-[560px] w-[420px] flex-col rounded-xl bg-white shadow-modal" onClick={(e) => e.stopPropagation()}>
@@ -130,32 +162,14 @@ export function ForwardPickerModal({ onPick, onClose }: ForwardPickerModalProps)
<div className="flex-1 overflow-y-auto px-2 py-2"> <div className="flex-1 overflow-y-auto px-2 py-2">
{friends.length ? ( {friends.length ? (
<> <>
<div className="flex items-center justify-between px-3 pb-1 pt-2"> {renderSectionHeader('好友', 'friends', friendTopics, allFriendsSelected)}
<span className="text-xs text-text-tertiary-2">好友</span> {collapsed.friends ? null : friends.map(renderRow)}
<button
type="button"
onClick={() => toggleSectionSelectAll(friendTopics, allFriendsSelected)}
className="text-xs text-accent"
>
{allFriendsSelected ? '取消全选' : '全选'}
</button>
</div>
{friends.map(renderRow)}
</> </>
) : null} ) : null}
{groups.length ? ( {groups.length ? (
<> <>
<div className="flex items-center justify-between px-3 pb-1 pt-2"> {renderSectionHeader('群聊', 'groups', groupTopics, allGroupsSelected)}
<span className="text-xs text-text-tertiary-2">群聊</span> {collapsed.groups ? null : groups.map(renderRow)}
<button
type="button"
onClick={() => toggleSectionSelectAll(groupTopics, allGroupsSelected)}
className="text-xs text-accent"
>
{allGroupsSelected ? '取消全选' : '全选'}
</button>
</div>
{groups.map(renderRow)}
</> </>
) : null} ) : null}
{!friends.length && !groups.length ? ( {!friends.length && !groups.length ? (