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
|
// 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
|
// 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.
|
// 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 [message, setMessage] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [sent, setSent] = useState(false);
|
const [sent, setSent] = useState(false);
|
||||||
@@ -194,9 +194,9 @@ export function AddFriendModal({ onClose, onSent, initialTarget }: AddFriendModa
|
|||||||
key={action.label}
|
key={action.label}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={action.onClick}
|
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}
|
{action.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -208,6 +208,13 @@ export function AddFriendModal({ onClose, onSent, initialTarget }: AddFriendModa
|
|||||||
<div className="mt-4 flex flex-col gap-2">
|
<div className="mt-4 flex flex-col gap-2">
|
||||||
{searchQuery.isLoading ? (
|
{searchQuery.isLoading ? (
|
||||||
<div className="py-6 text-center text-sm text-text-tertiary-4">搜索中…</div>
|
<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 ? (
|
) : !searchQuery.data?.items.length ? (
|
||||||
<div className="py-6 text-center text-sm text-text-tertiary-4">未找到相关用户</div>
|
<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"
|
type="button"
|
||||||
disabled={!selectable}
|
disabled={!selectable}
|
||||||
onClick={() => {
|
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);
|
setSelected(item);
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-3 rounded-lg px-2 py-2 text-left disabled:opacity-70"
|
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}
|
{item.public_account ? <span className="truncate text-xs text-text-tertiary-2">ID: {item.public_account}</span> : null}
|
||||||
</span>
|
</span>
|
||||||
{selectable ? (
|
{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 ? '已选择' : '选择'}
|
{isSelected ? '已选择' : '选择'}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -103,6 +103,13 @@ export function BlocklistView() {
|
|||||||
<div className="flex flex-col gap-2.5 p-6">
|
<div className="flex flex-col gap-2.5 p-6">
|
||||||
{query.isLoading ? (
|
{query.isLoading ? (
|
||||||
<div className="py-10 text-center text-sm text-text-tertiary-4">加载中…</div>
|
<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 ? (
|
) : items.length === 0 ? (
|
||||||
<div className="py-10 text-center text-sm text-text-tertiary-4">黑名单是空的</div>
|
<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">
|
<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">
|
<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>
|
<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" />
|
<Icon name="person_add_alt" />
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
@@ -119,6 +119,13 @@ export function ContactsListColumn() {
|
|||||||
)}
|
)}
|
||||||
{friendsQuery.isLoading ? (
|
{friendsQuery.isLoading ? (
|
||||||
<div className="flex flex-1 items-center justify-center text-sm text-text-tertiary-4">加载中…</div>
|
<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 ? (
|
) : filtered.length === 0 ? (
|
||||||
<div className="flex flex-1 flex-col items-center justify-center gap-2 py-10 text-text-tertiary-4">
|
<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" />
|
<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
|
// 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
|
// "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
|
// 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;
|
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 (
|
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}
|
{backLink}
|
||||||
{isPrivate ? '对方设置了资料保护,暂时无法查看' : '未找到该用户'}
|
{isPrivate ? (
|
||||||
|
'对方设置了资料保护,暂时无法查看'
|
||||||
|
) : isNotFound ? (
|
||||||
|
'未找到该用户'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>加载失败,请重试</span>
|
||||||
|
<button type="button" onClick={() => query.refetch()} className="text-accent">
|
||||||
|
重试
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ export function GroupListView() {
|
|||||||
<div className="flex flex-col gap-2.5 p-6">
|
<div className="flex flex-col gap-2.5 p-6">
|
||||||
{query.isLoading ? (
|
{query.isLoading ? (
|
||||||
<div className="py-10 text-center text-sm text-text-tertiary-4">加载中…</div>
|
<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 ? (
|
) : filtered.length === 0 ? (
|
||||||
<div className="py-10 text-center text-sm text-text-tertiary-4">暂无群聊</div>
|
<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">
|
<div className="flex flex-col gap-3 p-6">
|
||||||
{query.isLoading ? (
|
{query.isLoading ? (
|
||||||
<div className="py-10 text-center text-sm text-text-tertiary-4">加载中…</div>
|
<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 ? (
|
) : items.length === 0 ? (
|
||||||
<div className="py-10 text-center text-sm text-text-tertiary-4">暂无新朋友申请</div>
|
<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()),
|
queryFn: (): Promise<MyQRCode | GroupQRCode> => (groupTopic ? createGroupQRCode(groupTopic) : identifier ? createUserQRCode(identifier) : createMyQRCode()),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const draw = () => {
|
||||||
if (!query.data || !canvasRef.current) return;
|
if (!query.data || !canvasRef.current) return;
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
|
setDrawError('');
|
||||||
// Dynamically imported (like agora-rtc-sdk-ng in agora-client.ts) so a QR-generating library
|
// 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.
|
// isn't in every visitor's main bundle when most sessions never open this modal.
|
||||||
import('qrcode')
|
import('qrcode')
|
||||||
.then(({ default: QRCode }) => QRCode.toCanvas(canvas, query.data.qr_payload, { width: 200, margin: 1, color: { dark: '#22262d', light: '#ffffff' } }))
|
.then(({ default: QRCode }) => QRCode.toCanvas(canvas, query.data.qr_payload, { width: 200, margin: 1, color: { dark: '#22262d', light: '#ffffff' } }))
|
||||||
.catch(() => setDrawError('二维码生成失败'));
|
.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 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;
|
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">
|
<div className="flex flex-col items-center gap-2 px-4 text-center">
|
||||||
<Icon name="error_outline" className="text-2xl text-danger-text" />
|
<Icon name="error_outline" className="text-2xl text-danger-text" />
|
||||||
<span className="text-xs text-text-tertiary-4">{drawError || '二维码生成失败,请重试'}</span>
|
<span className="text-xs text-text-tertiary-4">{drawError || '二维码生成失败,请重试'}</span>
|
||||||
|
<button type="button" onClick={retry} className="text-xs text-accent">
|
||||||
|
重试
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<canvas ref={canvasRef} />
|
<canvas ref={canvasRef} />
|
||||||
|
|||||||
Reference in New Issue
Block a user