新增 pc-client:畅联 PC 端工程(Electron+React,按确认效果图改造,B-59)

- 基于 openim-electron-demo 改造:工号+密码登录(自研账号服务)、会话列表/聊天窗/通讯录界面重做
- 文字/语音/文件/图片消息、文件拖拽发送、表情面板、一对一语音通话(LiveKit)
- RTC 令牌对接账号服务带鉴权版 /api/rtc_token(Bearer imToken + {room,identity})
- account-service: CORS Allow-Headers 补 authorization(浏览器/渲染进程跨域调 rtc_token 需要)
- 附本地 mock 账号服务(scripts/mock-account-server.js)与截图联调脚本
This commit is contained in:
KIMI
2026-08-09 09:11:06 +08:00
parent b95159f7eb
commit 7ce40e356f
284 changed files with 31599 additions and 7 deletions
+252
View File
@@ -0,0 +1,252 @@
import { ApplicationHandleResult } from "@openim/wasm-client-sdk";
import {
BlackUserItem,
FriendApplicationItem,
FriendUserItem,
GroupApplicationItem,
GroupItem,
} from "@openim/wasm-client-sdk/lib/types/entity";
import { t } from "i18next";
import { create } from "zustand";
import { IMSDK } from "@/layout/MainContentWrap";
import { feedbackToast } from "@/utils/common";
import { ContactStore } from "./type";
export const useContactStore = create<ContactStore>()((set, get) => ({
friendList: [],
blackList: [],
groupList: [],
recvFriendApplicationList: [],
sendFriendApplicationList: [],
recvGroupApplicationList: [],
sendGroupApplicationList: [],
unHandleFriendApplicationCount: 0,
unHandleGroupApplicationCount: 0,
getFriendListByReq: async () => {
try {
let offset = 0;
let tmpList = [] as FriendUserItem[];
let initialFetch = true;
// eslint-disable-next-line
while (true) {
const count = initialFetch ? 10000 : 1000;
const { data } = await IMSDK.getFriendListPage({
offset,
count,
filterBlack: true,
});
tmpList = [...tmpList, ...data];
offset += count;
if (data.length < count) break;
initialFetch = false;
}
set(() => ({
friendList: [...tmpList],
}));
} catch (error) {
feedbackToast({ error, msg: t("toast.getFriendListFailed") });
}
},
setFriendList: (list: FriendUserItem[]) => {
set(() => ({ friendList: list }));
},
updateFriend: (friend: FriendUserItem, remove?: boolean) => {
const tmpList = [...get().friendList];
const idx = tmpList.findIndex((f) => f.userID === friend.userID);
if (idx < 0) {
return;
}
if (remove) {
tmpList.splice(idx, 1);
} else {
tmpList[idx] = { ...friend };
}
set(() => ({ friendList: tmpList }));
},
pushNewFriend: (friend: FriendUserItem) => {
set((state) => ({ friendList: [...state.friendList, friend] }));
},
getBlackListByReq: async () => {
try {
const { data } = await IMSDK.getBlackList();
set(() => ({ blackList: data }));
} catch (error) {
feedbackToast({ error, msg: t("toast.getBlackListFailed") });
}
},
updateBlack: (black: BlackUserItem, remove?: boolean) => {
const tmpList = [...get().blackList];
const idx = tmpList.findIndex((b) => b.userID === black.userID);
if (idx < 0) {
return;
}
if (remove) {
tmpList.splice(idx, 1);
} else {
tmpList[idx] = { ...black };
}
set(() => ({ blackList: tmpList }));
},
pushNewBlack: (black: BlackUserItem) => {
const isFriend = get().friendList.find((f) => f.userID === black.userID);
set((state) => ({
blackList: [...state.blackList, black],
friendList: !isFriend
? state.friendList
: state.friendList.filter((f) => f.userID !== black.userID),
}));
},
getGroupListByReq: async () => {
try {
let offset = 0;
let tmpList = [] as GroupItem[];
// eslint-disable-next-line
while (true) {
const { data } = await IMSDK.getJoinedGroupListPage({ offset, count: 1000 });
tmpList = [...tmpList, ...data];
offset += 1000;
if (data.length < 1000) break;
}
// const { data } = await IMSDK.getJoinedGroupList();
set(() => ({ groupList: tmpList }));
} catch (error) {
feedbackToast({ error, msg: t("toast.getGroupListFailed") });
}
},
setGroupList: (list: GroupItem[]) => {
set(() => ({ groupList: list }));
},
updateGroup: (group: GroupItem, remove?: boolean) => {
const tmpList = [...get().groupList];
const idx = tmpList.findIndex((g) => g.groupID === group.groupID);
if (idx < 0) {
return;
}
if (remove) {
tmpList.splice(idx, 1);
} else {
tmpList[idx] = { ...group };
}
set(() => ({ groupList: tmpList }));
},
pushNewGroup: (group: GroupItem) => {
set((state) => ({ groupList: [...state.groupList, group] }));
},
getRecvFriendApplicationListByReq: async () => {
try {
const { data } = await IMSDK.getFriendApplicationListAsRecipient();
set(() => ({ recvFriendApplicationList: data }));
} catch (error) {
console.error(error);
}
},
updateRecvFriendApplication: async (application: FriendApplicationItem) => {
let tmpList = [...get().recvFriendApplicationList];
let isHandleResultUpdate = false;
const idx = tmpList.findIndex((a) => a.fromUserID === application.fromUserID);
if (idx < 0) {
tmpList = [...tmpList, application];
} else {
isHandleResultUpdate = true;
tmpList[idx] = { ...application };
}
if (idx < 0 || isHandleResultUpdate) {
const unHandleFriendApplicationCount = tmpList.filter(
(application) =>
application.handleResult === 0,
).length;
set(() => ({
recvFriendApplicationList: tmpList,
unHandleFriendApplicationCount,
}));
return;
}
set(() => ({ recvFriendApplicationList: tmpList }));
},
getSendFriendApplicationListByReq: async () => {
try {
const { data } = await IMSDK.getFriendApplicationListAsApplicant();
set(() => ({ sendFriendApplicationList: data }));
} catch (error) {
console.error(error);
}
},
updateSendFriendApplication: (application: FriendApplicationItem) => {
let tmpList = [...get().sendFriendApplicationList];
const idx = tmpList.findIndex((a) => a.toUserID === application.toUserID);
if (idx < 0) {
tmpList = [...tmpList, application];
} else {
tmpList[idx] = { ...application };
}
set(() => ({ sendFriendApplicationList: tmpList }));
},
getRecvGroupApplicationListByReq: async () => {
try {
const { data } = await IMSDK.getGroupApplicationListAsRecipient();
set(() => ({ recvGroupApplicationList: data }));
} catch (error) {
console.error(error);
}
},
updateRecvGroupApplication: async (application: GroupApplicationItem) => {
let tmpList = [...get().recvGroupApplicationList];
let isHandleResultUpdate = false;
const idx = tmpList.findIndex((a) => a.userID === application.userID);
if (idx < 0) {
tmpList = [...tmpList, application];
} else {
isHandleResultUpdate = true;
tmpList[idx] = { ...application };
}
if (idx < 0 || application.handleResult === ApplicationHandleResult.Unprocessed) {
const unHandleGroupApplicationCount = tmpList.filter(
(application) =>
application.handleResult === 0
).length;
set(() => ({ recvGroupApplicationList: tmpList, unHandleGroupApplicationCount }));
return;
}
set(() => ({ recvGroupApplicationList: tmpList }));
},
getSendGroupApplicationListByReq: async () => {
try {
const { data } = await IMSDK.getGroupApplicationListAsApplicant();
set(() => ({ sendGroupApplicationList: data }));
} catch (error) {
console.error(error);
}
},
updateSendGroupApplication: (application: GroupApplicationItem) => {
let tmpList = [...get().sendGroupApplicationList];
const idx = tmpList.findIndex((a) => a.groupID === application.groupID);
if (idx < 0) {
tmpList = [...tmpList, application];
} else {
tmpList[idx] = { ...application };
}
set(() => ({ sendGroupApplicationList: tmpList }));
},
updateUnHandleFriendApplicationCount: (num: number) => {
set(() => ({ unHandleFriendApplicationCount: num }));
},
updateUnHandleGroupApplicationCount: (num: number) => {
set(() => ({ unHandleGroupApplicationCount: num }));
},
clearContactStore: () => {
set(() => ({
friendList: [],
blackList: [],
groupList: [],
recvFriendApplicationList: [],
sendFriendApplicationList: [],
recvGroupApplicationList: [],
sendGroupApplicationList: [],
unHandleFriendApplicationCount: 0,
unHandleGroupApplicationCount: 0,
}));
},
}));
+159
View File
@@ -0,0 +1,159 @@
import {
ConversationItem,
GroupItem,
GroupMemberItem,
MessageItem,
} from "@openim/wasm-client-sdk/lib/types/entity";
import { t } from "i18next";
import { create } from "zustand";
import { IMSDK } from "@/layout/MainContentWrap";
import { feedbackToast } from "@/utils/common";
import { conversationSort, isGroupSession } from "@/utils/imCommon";
import { ConversationListUpdateType, ConversationStore } from "./type";
import { useUserStore } from "./user";
const CONVERSATION_SPLIT_COUNT = 500;
export const useConversationStore = create<ConversationStore>()((set, get) => ({
conversationList: [],
currentConversation: undefined,
unReadCount: 0,
currentGroupInfo: undefined,
currentMemberInGroup: undefined,
getConversationListByReq: async (isOffset?: boolean) => {
let tmpConversationList = [] as ConversationItem[];
try {
const { data } = await IMSDK.getConversationListSplit({
offset: isOffset ? get().conversationList.length : 0,
count: CONVERSATION_SPLIT_COUNT,
});
tmpConversationList = data;
} catch (error) {
feedbackToast({ error, msg: t("toast.getConversationFailed") });
return true;
}
set((state) => ({
conversationList: [
...(isOffset ? state.conversationList : []),
...tmpConversationList,
],
}));
return tmpConversationList.length === CONVERSATION_SPLIT_COUNT;
},
updateConversationList: (
list: ConversationItem[],
type: ConversationListUpdateType,
) => {
const idx = list.findIndex(
(c) => c.conversationID === get().currentConversation?.conversationID,
);
if (idx > -1) get().updateCurrentConversation(list[idx]);
if (type === "filter") {
set((state) => ({
conversationList: conversationSort(
[...list, ...state.conversationList],
state.conversationList,
),
}));
return;
}
let filterArr: ConversationItem[] = [];
const chids = list.map((ch) => ch.conversationID);
filterArr = get().conversationList.filter(
(tc) => !chids.includes(tc.conversationID),
);
set(() => ({ conversationList: conversationSort([...list, ...filterArr]) }));
},
updateCurrentConversation: async (
conversation?: ConversationItem,
isJump?: boolean,
) => {
if (!conversation) {
set(() => ({
currentConversation: undefined,
quoteMessage: undefined,
currentGroupInfo: undefined,
currentMemberInGroup: undefined,
}));
return;
}
const prevConversation = get().currentConversation;
const toggleNewConversation =
conversation.conversationID !== prevConversation?.conversationID;
if (toggleNewConversation && isGroupSession(conversation.conversationType)) {
get().getCurrentGroupInfoByReq(conversation.groupID);
await get().getCurrentMemberInGroupByReq(conversation.groupID);
}
set(() => ({ currentConversation: { ...conversation } }));
},
getUnReadCountByReq: async () => {
try {
const { data } = await IMSDK.getTotalUnreadMsgCount();
set(() => ({ unReadCount: data }));
return data;
} catch (error) {
console.error(error);
return 0;
}
},
updateUnReadCount: (count: number) => {
set(() => ({ unReadCount: count }));
},
getCurrentGroupInfoByReq: async (groupID: string) => {
let groupInfo: GroupItem;
try {
const { data } = await IMSDK.getSpecifiedGroupsInfo([groupID]);
groupInfo = data[0];
} catch (error) {
feedbackToast({ error, msg: t("toast.getGroupInfoFailed") });
return;
}
set(() => ({ currentGroupInfo: { ...groupInfo } }));
},
updateCurrentGroupInfo: (groupInfo: GroupItem) => {
set(() => ({ currentGroupInfo: { ...groupInfo } }));
},
getCurrentMemberInGroupByReq: async (groupID: string) => {
let memberInfo: GroupMemberItem;
const selfID = useUserStore.getState().selfInfo.userID;
try {
const { data } = await IMSDK.getSpecifiedGroupMembersInfo({
groupID,
userIDList: [selfID],
});
memberInfo = data[0];
} catch (error) {
set(() => ({ currentMemberInGroup: undefined }));
feedbackToast({ error, msg: t("toast.getGroupMemberFailed") });
return;
}
set(() => ({ currentMemberInGroup: memberInfo ? { ...memberInfo } : undefined }));
},
setCurrentMemberInGroup: (memberInfo?: GroupMemberItem) => {
set(() => ({ currentMemberInGroup: memberInfo }));
},
tryUpdateCurrentMemberInGroup: (member: GroupMemberItem) => {
const currentMemberInGroup = get().currentMemberInGroup;
if (
member.groupID === currentMemberInGroup?.groupID &&
member.userID === currentMemberInGroup?.userID
) {
set(() => ({ currentMemberInGroup: { ...member } }));
}
},
clearConversationStore: () => {
set(() => ({
conversationList: [],
currentConversation: undefined,
unReadCount: 0,
currentGroupInfo: undefined,
currentMemberInGroup: undefined,
quoteMessage: undefined,
}));
},
}));
+3
View File
@@ -0,0 +1,3 @@
export * from "./contact";
export * from "./conversation";
export * from "./user";
+104
View File
@@ -0,0 +1,104 @@
import {
AtTextElem,
BlackUserItem,
ConversationItem,
FriendApplicationItem,
FriendUserItem,
GroupApplicationItem,
GroupItem,
GroupMemberItem,
MessageItem,
} from "@openim/wasm-client-sdk/lib/types/entity";
import { BusinessUserInfo } from "@/api/login";
export type IMConnectState = "success" | "loading" | "failed";
export interface UserStore {
syncState: IMConnectState;
progress: number;
reinstall: boolean;
isLogining: boolean;
connectState: IMConnectState;
selfInfo: BusinessUserInfo;
appSettings: AppSettings;
updateSyncState: (syncState: IMConnectState) => void;
updateProgressState: (progress: number) => void;
updateReinstallState: (reinstall: boolean) => void;
updateIsLogining: (isLogining: boolean) => void;
updateConnectState: (connectState: IMConnectState) => void;
updateSelfInfo: (info: Partial<BusinessUserInfo>) => void;
getSelfInfoByReq: () => void;
updateAppSettings: (settings: Partial<AppSettings>) => void;
userLogout: (force?: boolean) => Promise<void>;
}
export interface AppSettings {
locale: LocaleString;
closeAction: "miniSize" | "quit";
}
export type LocaleString = "zh-CN" | "en-US";
export type ConversationListUpdateType = "push" | "filter";
export interface ConversationStore {
conversationList: ConversationItem[];
currentConversation?: ConversationItem;
unReadCount: number;
currentGroupInfo?: GroupItem;
currentMemberInGroup?: GroupMemberItem;
getConversationListByReq: (
isOffset?: boolean
) => Promise<boolean>;
updateConversationList: (
list: ConversationItem[],
type: ConversationListUpdateType,
) => void;
updateCurrentConversation: (
conversation?: ConversationItem,
isJump?: boolean,
) => Promise<void>;
getUnReadCountByReq: () => Promise<number>;
updateUnReadCount: (count: number) => void;
getCurrentGroupInfoByReq: (groupID: string) => Promise<void>;
updateCurrentGroupInfo: (groupInfo: GroupItem) => void;
getCurrentMemberInGroupByReq: (groupID: string) => Promise<void>;
setCurrentMemberInGroup: (memberInfo?: GroupMemberItem) => void;
tryUpdateCurrentMemberInGroup: (member: GroupMemberItem) => void;
clearConversationStore: () => void;
}
export interface ContactStore {
friendList: FriendUserItem[];
blackList: BlackUserItem[];
groupList: GroupItem[];
recvFriendApplicationList: FriendApplicationItem[];
sendFriendApplicationList: FriendApplicationItem[];
recvGroupApplicationList: GroupApplicationItem[];
sendGroupApplicationList: GroupApplicationItem[];
unHandleFriendApplicationCount: number;
unHandleGroupApplicationCount: number;
getFriendListByReq: () => Promise<void>;
setFriendList: (list: FriendUserItem[]) => void;
updateFriend: (friend: FriendUserItem, remove?: boolean) => void;
pushNewFriend: (friend: FriendUserItem) => void;
getBlackListByReq: () => Promise<void>;
updateBlack: (black: BlackUserItem, remove?: boolean) => void;
pushNewBlack: (black: BlackUserItem) => void;
getGroupListByReq: () => Promise<void>;
setGroupList: (list: GroupItem[]) => void;
updateGroup: (group: GroupItem, remove?: boolean) => void;
pushNewGroup: (group: GroupItem) => void;
getRecvFriendApplicationListByReq: () => Promise<void>;
updateRecvFriendApplication: (application: FriendApplicationItem) => Promise<void>;
getSendFriendApplicationListByReq: () => Promise<void>;
updateSendFriendApplication: (application: FriendApplicationItem) => void;
getRecvGroupApplicationListByReq: () => Promise<void>;
updateRecvGroupApplication: (application: GroupApplicationItem) => Promise<void>;
getSendGroupApplicationListByReq: () => Promise<void>;
updateSendGroupApplication: (application: GroupApplicationItem) => void;
updateUnHandleFriendApplicationCount: (num: number) => void;
updateUnHandleGroupApplicationCount: (num: number) => void;
clearContactStore: () => void;
}
+70
View File
@@ -0,0 +1,70 @@
import { t } from "i18next";
import { create } from "zustand";
import { BusinessUserInfo, getBusinessUserInfo } from "@/api/login";
import { IMSDK } from "@/layout/MainContentWrap";
import router from "@/routes";
import { feedbackToast } from "@/utils/common";
import { clearIMProfile, getLocale, setLocale } from "@/utils/storage";
import { useContactStore } from "./contact";
import { useConversationStore } from "./conversation";
import { AppSettings, IMConnectState, UserStore } from "./type";
export const useUserStore = create<UserStore>()((set, get) => ({
syncState: "success",
progress: 0,
reinstall: true,
isLogining: false,
connectState: "success",
selfInfo: {} as BusinessUserInfo,
appSettings: {
locale: getLocale(),
closeAction: "miniSize",
},
updateSyncState: (syncState: IMConnectState) => {
set({ syncState });
},
updateProgressState: (progress: number) => {
set({ progress });
},
updateReinstallState: (reinstall: boolean) => {
set({ reinstall });
},
updateIsLogining: (isLogining: boolean) => {
set({ isLogining });
},
updateConnectState: (connectState: IMConnectState) => {
set({ connectState });
},
getSelfInfoByReq: () => {
IMSDK.getSelfUserInfo()
.then(({ data }) => {
set(() => ({ selfInfo: data as unknown as BusinessUserInfo }));
getBusinessUserInfo([data.userID]).then(({ data: { users } }) =>
set((state) => ({ selfInfo: { ...state.selfInfo, ...users[0] } })),
);
})
.catch((error) => {
feedbackToast({ error, msg: t("toast.getSelfInfoFailed") });
get().userLogout();
});
},
updateSelfInfo: (info: Partial<BusinessUserInfo>) => {
set((state) => ({ selfInfo: { ...state.selfInfo, ...info } }));
},
updateAppSettings: (settings: Partial<AppSettings>) => {
if (settings.locale) {
setLocale(settings.locale);
}
set((state) => ({ appSettings: { ...state.appSettings, ...settings } }));
},
userLogout: async (force?: boolean) => {
if (!force) await IMSDK.logout();
clearIMProfile();
set({ selfInfo: {} as BusinessUserInfo, progress: 0 });
useContactStore.getState().clearContactStore();
useConversationStore.getState().clearConversationStore();
router.navigate("/login");
},
}));