fix(web): 转发选择器勾选样式改用矢量图标,好友/群聊可分别展开收起
原来的勾选图标是Material Icons字体里的"check"字形,在20px圆圈里塞 14px字体图标,字体本身没有针对这么小的尺寸做过精细调校,稍微放大 一点看就是个不规整的糊状图案。换成手绘SVG勾选路径(矢量、任何缩放 比例下都清晰),并去掉了选中态多余的一层border(原来选中时背景色 和边框色叠在一起,等于多画了一圈没必要的描边)。 好友、群聊两个分组标题现在都可以点击收起/展开(各自独立),列表长 的时候可以先收起不relevant的那一类,收起状态下已经选中的项不会被 清空,"全选"按钮无论展开收起都能点。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
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 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user