fix(web): 第9轮审查批次3——通讯录域一批问题
- 手机号搜索加好友时来源永远上报"search"(账号搜索),NewFriendsView 早就有phone的文案映射但一直用不到——现在按searchType正确区分 - 添加好友弹层三个快捷按钮布局改成横向(原来是纵向堆叠,和设计稿方向 不一致),图标颜色改成强调色 - "已选择"状态标签的圆角从胶囊改成6px小圆角,和同弹层其它按钮风格一致 - 通讯录头部"加好友"按钮补上移动端尺寸(40px/桌面36px),之前和旁边 FIXED_ENTRIES的断点处理不一致 - 我的二维码/名片二维码生成失败没有重试入口,只能关掉弹层重开——补上 重试按钮,同时处理"铸造失败"和"canvas绘制失败"两种情况 - 通讯录域几乎所有列表(好友列表/新朋友/群聊列表/黑名单/好友主页/加 好友搜索)都把请求失败误判成"没有数据",统一补上isError态+重试按 钮;好友主页额外区分403(资料保护)/404(真的不存在)/其它错误三种情况, 之前网络错误也会显示"未找到该用户" Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8f4b0de636
commit
18d21a03bb
@@ -47,7 +47,7 @@ export function AddFriendModal({ onClose, onSent, initialTarget }: AddFriendModa
|
||||
// Tracks HOW `selected` was populated so the eventual sendFriendRequest can report the real
|
||||
// provenance instead of always claiming "search" -- NewFriendsView's own SOURCE_LABEL map has a
|
||||
// dedicated "扫码添加" entry for the qr case that was otherwise dead/unreachable.
|
||||
const [selectedSource, setSelectedSource] = useState<'search' | 'qr'>('search');
|
||||
const [selectedSource, setSelectedSource] = useState<'search' | 'phone' | 'qr'>('search');
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [sent, setSent] = useState(false);
|
||||
@@ -194,9 +194,9 @@ export function AddFriendModal({ onClose, onSent, initialTarget }: AddFriendModa
|
||||
key={action.label}
|
||||
type="button"
|
||||
onClick={action.onClick}
|
||||
className="flex flex-col items-center gap-1.5 rounded-lg bg-surface-input py-3 text-xs text-text-secondary"
|
||||
className="flex items-center justify-center gap-2 rounded-lg bg-surface-input py-3 text-xs text-text-secondary"
|
||||
>
|
||||
<Icon name={action.icon} className="text-xl text-text-tertiary-1" />
|
||||
<Icon name={action.icon} className="text-xl text-accent" />
|
||||
{action.label}
|
||||
</button>
|
||||
))}
|
||||
@@ -208,6 +208,13 @@ export function AddFriendModal({ onClose, onSent, initialTarget }: AddFriendModa
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
{searchQuery.isLoading ? (
|
||||
<div className="py-6 text-center text-sm text-text-tertiary-4">搜索中…</div>
|
||||
) : searchQuery.isError ? (
|
||||
<div className="flex flex-col items-center gap-2 py-6 text-sm text-danger-text">
|
||||
<span>搜索失败,请重试</span>
|
||||
<button type="button" onClick={() => searchQuery.refetch()} className="text-accent">
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
) : !searchQuery.data?.items.length ? (
|
||||
<div className="py-6 text-center text-sm text-text-tertiary-4">未找到相关用户</div>
|
||||
) : (
|
||||
@@ -220,7 +227,12 @@ export function AddFriendModal({ onClose, onSent, initialTarget }: AddFriendModa
|
||||
type="button"
|
||||
disabled={!selectable}
|
||||
onClick={() => {
|
||||
setSelectedSource('search');
|
||||
// `searchType` (already computed above from the query itself) is the real
|
||||
// signal for which channel actually found this person -- NewFriendsView's
|
||||
// own SOURCE_LABEL table already has a `phone` entry ("通过手机号搜索")
|
||||
// that this path never reached because every search result was tagged
|
||||
// 'search' regardless of how it was found.
|
||||
setSelectedSource(searchType === 'phone' ? 'phone' : 'search');
|
||||
setSelected(item);
|
||||
}}
|
||||
className="flex items-center gap-3 rounded-lg px-2 py-2 text-left disabled:opacity-70"
|
||||
@@ -231,7 +243,7 @@ export function AddFriendModal({ onClose, onSent, initialTarget }: AddFriendModa
|
||||
{item.public_account ? <span className="truncate text-xs text-text-tertiary-2">ID: {item.public_account}</span> : null}
|
||||
</span>
|
||||
{selectable ? (
|
||||
<span className={`flex-none rounded-full px-2.5 py-1 text-xs ${isSelected ? 'bg-accent-soft-bg text-accent' : 'bg-surface-input text-text-tertiary-2'}`}>
|
||||
<span className={`flex-none rounded-md px-2.5 py-1 text-xs ${isSelected ? 'bg-accent-soft-bg text-accent' : 'bg-surface-input text-text-tertiary-2'}`}>
|
||||
{isSelected ? '已选择' : '选择'}
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@@ -103,6 +103,13 @@ export function BlocklistView() {
|
||||
<div className="flex flex-col gap-2.5 p-6">
|
||||
{query.isLoading ? (
|
||||
<div className="py-10 text-center text-sm text-text-tertiary-4">加载中…</div>
|
||||
) : query.isError ? (
|
||||
<div className="flex flex-col items-center gap-2 py-10 text-sm text-danger-text">
|
||||
<span>加载失败,请重试</span>
|
||||
<button type="button" onClick={() => query.refetch()} className="text-accent">
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="py-10 text-center text-sm text-text-tertiary-4">黑名单是空的</div>
|
||||
) : (
|
||||
|
||||
@@ -65,7 +65,7 @@ export function ContactsListColumn() {
|
||||
<aside className="flex h-full w-full flex-none flex-col border-r border-border-panel bg-white md:w-[300px] lg:w-80">
|
||||
<header className="flex h-15 flex-none items-center justify-between px-5">
|
||||
<h1 className="m-0 text-xl font-semibold text-text-heading">通讯录</h1>
|
||||
<button type="button" onClick={() => setAddingFriend(true)} className="grid h-8 w-8 place-items-center text-text-heading">
|
||||
<button type="button" onClick={() => setAddingFriend(true)} className="grid h-10 w-10 place-items-center text-text-heading md:h-9 md:w-9">
|
||||
<Icon name="person_add_alt" />
|
||||
</button>
|
||||
</header>
|
||||
@@ -119,6 +119,13 @@ export function ContactsListColumn() {
|
||||
)}
|
||||
{friendsQuery.isLoading ? (
|
||||
<div className="flex flex-1 items-center justify-center text-sm text-text-tertiary-4">加载中…</div>
|
||||
) : friendsQuery.isError ? (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-sm text-danger-text">
|
||||
<span>加载失败,请重试</span>
|
||||
<button type="button" onClick={() => friendsQuery.refetch()} className="text-accent">
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-2 py-10 text-text-tertiary-4">
|
||||
<Icon name={trimmedQuery ? 'search_off' : 'people_outline'} className="!text-4xl" />
|
||||
|
||||
@@ -70,12 +70,28 @@ export function FriendProfileView() {
|
||||
// Confirmed live 2026-09-09: a 403 here comes back with reason PERMISSION_DENIED and message
|
||||
// "profile is private" -- a real privacy setting on the other side, not a missing/deleted
|
||||
// account. Collapsing it into "未找到该用户" would misrepresent an intentional privacy choice
|
||||
// as if the user doesn't exist; every other failure (404, network error) keeps that fallback.
|
||||
// as if the user doesn't exist.
|
||||
const isPrivate = query.error instanceof ImApiError && query.error.status === 403;
|
||||
// A genuine 404 is the only case that should actually read as "this user doesn't exist" --
|
||||
// every OTHER failure (network error, 5xx, a transient timeout) used to fall into that same
|
||||
// "未找到该用户" bucket too, telling the user an account is gone/never existed when the real
|
||||
// problem is just that this one request failed and could succeed on retry.
|
||||
const isNotFound = query.error instanceof ImApiError && query.error.status === 404;
|
||||
return (
|
||||
<div className="relative flex h-full w-full items-center justify-center bg-surface-content text-sm text-text-tertiary-4">
|
||||
<div className="relative flex h-full w-full flex-col items-center justify-center gap-2 bg-surface-content text-sm text-text-tertiary-4">
|
||||
{backLink}
|
||||
{isPrivate ? '对方设置了资料保护,暂时无法查看' : '未找到该用户'}
|
||||
{isPrivate ? (
|
||||
'对方设置了资料保护,暂时无法查看'
|
||||
) : isNotFound ? (
|
||||
'未找到该用户'
|
||||
) : (
|
||||
<>
|
||||
<span>加载失败,请重试</span>
|
||||
<button type="button" onClick={() => query.refetch()} className="text-accent">
|
||||
重试
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,13 @@ export function GroupListView() {
|
||||
<div className="flex flex-col gap-2.5 p-6">
|
||||
{query.isLoading ? (
|
||||
<div className="py-10 text-center text-sm text-text-tertiary-4">加载中…</div>
|
||||
) : query.isError ? (
|
||||
<div className="flex flex-col items-center gap-2 py-10 text-sm text-danger-text">
|
||||
<span>加载失败,请重试</span>
|
||||
<button type="button" onClick={() => query.refetch()} className="text-accent">
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="py-10 text-center text-sm text-text-tertiary-4">暂无群聊</div>
|
||||
) : (
|
||||
|
||||
@@ -69,6 +69,13 @@ export function NewFriendsView() {
|
||||
<div className="flex flex-col gap-3 p-6">
|
||||
{query.isLoading ? (
|
||||
<div className="py-10 text-center text-sm text-text-tertiary-4">加载中…</div>
|
||||
) : query.isError ? (
|
||||
<div className="flex flex-col items-center gap-2 py-10 text-sm text-danger-text">
|
||||
<span>加载失败,请重试</span>
|
||||
<button type="button" onClick={() => query.refetch()} className="text-accent">
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="py-10 text-center text-sm text-text-tertiary-4">暂无新朋友申请</div>
|
||||
) : (
|
||||
|
||||
@@ -35,15 +35,23 @@ export function QRCodeModal({ identifier, groupTopic, onClose }: QRCodeModalProp
|
||||
queryFn: (): Promise<MyQRCode | GroupQRCode> => (groupTopic ? createGroupQRCode(groupTopic) : identifier ? createUserQRCode(identifier) : createMyQRCode()),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const draw = () => {
|
||||
if (!query.data || !canvasRef.current) return;
|
||||
const canvas = canvasRef.current;
|
||||
setDrawError('');
|
||||
// Dynamically imported (like agora-rtc-sdk-ng in agora-client.ts) so a QR-generating library
|
||||
// isn't in every visitor's main bundle when most sessions never open this modal.
|
||||
import('qrcode')
|
||||
.then(({ default: QRCode }) => QRCode.toCanvas(canvas, query.data.qr_payload, { width: 200, margin: 1, color: { dark: '#22262d', light: '#ffffff' } }))
|
||||
.catch(() => setDrawError('二维码生成失败'));
|
||||
}, [query.data]);
|
||||
};
|
||||
|
||||
useEffect(draw, [query.data]);
|
||||
|
||||
const retry = () => {
|
||||
if (query.isError) query.refetch();
|
||||
else draw();
|
||||
};
|
||||
|
||||
const profile = query.data && 'display_profile' in query.data ? query.data.display_profile : undefined;
|
||||
const group = query.data && 'group_snapshot' in query.data ? query.data.group_snapshot : undefined;
|
||||
@@ -117,6 +125,9 @@ export function QRCodeModal({ identifier, groupTopic, onClose }: QRCodeModalProp
|
||||
<div className="flex flex-col items-center gap-2 px-4 text-center">
|
||||
<Icon name="error_outline" className="text-2xl text-danger-text" />
|
||||
<span className="text-xs text-text-tertiary-4">{drawError || '二维码生成失败,请重试'}</span>
|
||||
<button type="button" onClick={retry} className="text-xs text-accent">
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<canvas ref={canvasRef} />
|
||||
|
||||
Reference in New Issue
Block a user