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>
128 lines
4.0 KiB
TypeScript
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>
|
|
);
|
|
};
|