Files
tongxunruanjian/pc-client/src/pages/contact/myFriends/index.tsx
T
bbc4dce0b6 fix: 验收包写入公司内网地址,避免 Windows 空 VITE 与 Android 假域名
Windows 补 .env.production 与 endpoints 兜底;Android 默认 host/端口改为内网,启动后重写 HttpUtil/ApiService,Urls 改为 getter。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 16:21:40 +08:00

128 lines
4.0 KiB
TypeScript

import { FriendUserItem } from "@openim/wasm-client-sdk/lib/types/entity";
import axios from "axios";
import { Empty, Spin } from "antd";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { GroupedVirtuoso } from "react-virtuoso";
import { ACCOUNT_URL } from "@/config/endpoints";
import { useContactStore } from "@/store";
import { emit } from "@/utils/events";
import { getIMToken } from "@/utils/storage";
import FriendListItem from "./FriendListItem";
type DirInfo = { title: string; department: string };
export const MyFriends = () => {
const { t } = useTranslation();
const friendList = useContactStore((state) => state.friendList);
const [dirMap, setDirMap] = useState<Record<string, DirInfo>>({});
const [dirReady, setDirReady] = useState(false);
useEffect(() => {
let cancelled = false;
const loadTitles = async () => {
try {
const token = await getIMToken();
if (!token) {
setDirReady(true);
return;
}
const { data } = await axios.get(
`${ACCOUNT_URL}/api/directory`,
{
params: { keyword: "", limit: 100 },
headers: { Authorization: `Bearer ${token}` },
},
);
if (cancelled) return;
if (data?.code === 0) {
const map: Record<string, DirInfo> = {};
for (const item of data.data?.items ?? []) {
if (item.userID) {
map[item.userID] = {
title: item.title || "",
department: item.department || "",
};
}
}
setDirMap(map);
}
} catch {
// 目录不可用时职务行留空、部门归入未分组
} finally {
if (!cancelled) setDirReady(true);
}
};
void loadTitles();
return () => {
cancelled = true;
};
}, []);
const sectionData = useMemo(() => {
const ungrouped = t("placeholder.ungrouped");
const map = new Map<string, FriendUserItem[]>();
for (const friend of friendList) {
const dept = dirMap[friend.userID]?.department?.trim() || ungrouped;
const list = map.get(dept) ?? [];
list.push(friend);
map.set(dept, list);
}
const names = [...map.keys()].sort((a, b) => {
if (a === ungrouped) return 1;
if (b === ungrouped) return -1;
return a.localeCompare(b, "zh");
});
return {
groupCounts: names.map((name) => map.get(name)!.length),
indexList: names,
totalList: names.flatMap((name) => map.get(name)!),
};
}, [friendList, dirMap, t]);
const showUserCard = useCallback((userID: string) => {
emit("OPEN_USER_CARD", {
userID,
});
}, []);
return (
<div className="flex h-full w-full flex-col overflow-hidden bg-white">
<div className="m-5.5 text-base font-extrabold">{t("placeholder.myFriend")}</div>
{!dirReady && friendList.length === 0 ? (
<Spin />
) : !friendList.length ? (
<Empty className="mt-[30%]" image={Empty.PRESENTED_IMAGE_SIMPLE} />
) : (
<div className="ml-4 mt-4 flex-1 overflow-auto pr-4">
<GroupedVirtuoso
groupCounts={sectionData.groupCounts}
groupContent={(index) => (
<div
className="flex items-center bg-[var(--chat-bg)] px-3.5 text-xs text-[var(--sub-text)]"
style={{ height: 28 }}
>
{sectionData.indexList[index]}
</div>
)}
itemContent={(index) => {
const friend = sectionData.totalList[index];
return (
<FriendListItem
key={friend.userID}
friend={friend}
title={dirMap[friend.userID]?.title}
showUserCard={showUserCard}
/>
);
}}
className="no-scrollbar h-full overflow-x-hidden"
/>
</div>
)}
</div>
);
};