diff --git a/docs/acceptance/pc-boundary.md b/docs/acceptance/pc-boundary.md index 33d2f6a..45aa58b 100644 --- a/docs/acceptance/pc-boundary.md +++ b/docs/acceptance/pc-boundary.md @@ -1,36 +1,37 @@ # PC 端可改边界封存(docs/acceptance) -> 只读核对,本卡不改 PC 界面。依据架构方案 B-90 的 PC 目录边界,对当前 PC 工程做只读核对,封存后续允许改动目录。 +> 依据架构方案 B-90 的 PC 目录边界。B-185 第二批只改界面层,通信核心仍冻结。 -## 核对结论(2026-08-19,只读) - -PC 工程 `pc-client/` 当前核心边界确认如下: - -### 不可动(通信核心,冻结) +## 不可动(通信核心,冻结) | 目录/文件 | 内容 | 理由 | |---|---|---| -| `pc-client/electron/` | 主进程(`main/`)、预加载(`preload/`)、SDK 资产与打包核心 | 进程/打包核心,改动即破坏客户端稳定性 | -| `pc-client/src/store/` | 会话(`conversation.ts`)、联系人(`contact.ts`)、用户(`user.ts`)状态 | 会话/联系人/用户状态唯一来源 | -| `pc-client/src/layout/MainContentWrap.tsx` | SDK 初始化与连接(`@openim/electron-client-sdk` 实例化、`IMSDK`、登录检查) | SDK 初始化与连接入口 | -| `pc-client/src/pages/chat/queryChat/ChatFooter/useSendMessage.ts` | 消息发送(`IMSDK.sendMessage`) | 消息发送核心逻辑 | -| `pc-client/src/pages/chat/queryChat/useHistoryMessageList.tsx` | 历史消息(`IMSDK.getAdvancedHistoryMessageList`) | 历史消息核心逻辑 | -| SDK 版本 | `@openim/electron-client-sdk ^3.8.3-patch.10`、`@openim/wasm-client-sdk ^3.8.3-patch.10`(`package.json`) | 锁定版本,不做无收益升级 | +| `pc-client/electron/` | 主进程、预加载、SDK 资产与打包核心 | 进程/打包核心。特批仅 `windowManage.ts` 的 `minWidth=1080`、`minHeight=680` | +| `pc-client/src/store/` | 会话 / 联系人 / 用户状态 | 状态唯一来源 | +| `pc-client/src/layout/MainContentWrap.tsx` | SDK 初始化与连接 | SDK 入口 | +| `pc-client/src/pages/chat/queryChat/ChatFooter/useSendMessage.ts` | 消息发送 | 发送核心 | +| `pc-client/src/pages/chat/queryChat/useHistoryMessageList.tsx` | 历史消息 | 历史核心 | +| SDK 版本 | `@openim/electron-client-sdk ^3.8.3-patch.10`、`@openim/wasm-client-sdk ^3.8.3-patch.10` | 锁定版本 | -### 可改(界面层,后续重整范围) +## 可改(界面层) | 目录 | 内容 | |---|---| | `pc-client/src/layout/LeftNavBar/` | 导航外观 | -| `pc-client/src/pages/contact/**` | 联系人展示与入口 | +| `pc-client/src/pages/login/**` | 登录窗展示(校验三态、窗内顶栏提示;不改登录接口) | +| `pc-client/src/pages/chat/**` | 会话栏、空会话页、聊天窗**展示**(顶栏/气泡/输入区样式、右键置顶删除、大图预览样式) | +| `pc-client/src/pages/contact/**` | 联系人展示、职务行、部门分组 | +| `pc-client/src/pages/common/AddColleague.tsx` | 添加同事弹窗五态与文案 | | `pc-client/src/components/**` | 通用 UI 组件 | -| `pc-client/src/styles/` | 统一视觉 Token 唯一来源(依据视觉规范 v2) | +| `pc-client/src/styles/` | 统一视觉 Token 唯一来源 | +| `pc-client/src/i18n/resources/` | 界面文案 | -### 明确禁止 +聊天目录中下列文件仍冻结,即使位于 `pages/chat/**`: -- 不建设 PC 语音通话:不提供发起/接听/来电弹窗;对手机发来的通话自定义消息静默过滤,不显示原始 JSON。 -- 不改 `electron/`、`src/store/`、SDK 初始化、消息发送与历史逻辑。 +- `queryChat/ChatFooter/useSendMessage.ts` +- `queryChat/useHistoryMessageList.tsx` -## 回退与复核 +## 明确禁止 -- 本卡未改动任何 PC 文件(只读核对);上述边界已记录,作为后续 PC 界面重整(阶段 4)的唯一改动态范围。 +- 不建设 PC 语音通话:不提供发起/接听/来电入口;本批已从聊天顶栏去掉「语音通话」。RTC 死代码按 B-90 保留以便回滚。 +- 不改 `electron/`(窗口下限两常量除外)、`src/store/`、SDK 初始化、消息发送与历史逻辑。 diff --git a/pc-client/src/components/ApplicationItem/index.tsx b/pc-client/src/components/ApplicationItem/index.tsx index 2620935..a2e2528 100644 --- a/pc-client/src/components/ApplicationItem/index.tsx +++ b/pc-client/src/components/ApplicationItem/index.tsx @@ -50,12 +50,12 @@ const ApplicationItem = ({ const getStatusStr = () => { if (source.handleResult === ApplicationHandleResult.Agree) { - return "已添加"; + return t("application.agreed"); } if (source.handleResult === ApplicationHandleResult.Reject) { - return "已拒绝"; + return t("application.refused"); } - return isRecv ? "待处理" : "等待对方通过"; + return isRecv ? t("application.waitingHandle") : t("application.pending"); }; const getAvatarUrl = () => { @@ -80,9 +80,15 @@ const ApplicationItem = ({ window.userClick(isRecv ? source.fromUserID : source.toUserID); }, []); + const btnStyle = { + height: 32, + flex: 1, + borderRadius: 8, + }; + return ( -
+
-
)} diff --git a/pc-client/src/i18n/resources/en.json b/pc-client/src/i18n/resources/en.json index af9151a..67b2aad 100644 --- a/pc-client/src/i18n/resources/en.json +++ b/pc-client/src/i18n/resources/en.json @@ -113,7 +113,12 @@ "publishFailed": "Failed to publish!", "notCanAddFriend": "The other party has been set up not to be added as a friend!", "fileTypeError": "Unsupported file type or folder!", - "inputStaffNo": "请输入工号", + "inputStaffNo": "Please enter staff number", + "staffOrPasswordWrong": "Incorrect staff number or password", + "networkRetry": "Network error, please try again later", + "inputPeerStaffNo": "Please enter the colleague's staff number", + "addColleagueSent": "Request sent, waiting for approval", + "addColleagueFailed": "Failed to send. Check the staff number and network", "noMicrophone": "No microphone detected, cannot record", "recordTooShort": "Recording is too short" }, @@ -188,6 +193,15 @@ "qrCodeLoginTitle": "Please scan the QR code using OpenCorp mobile", "welcome": "欢迎使用畅联", "title": "畅联", + "internalComms": "Internal company messaging", + "loggingIn": "Signing in", + "agreeInternal": "By signing in you agree to internal usage rules", + "emptyChat": "Select a conversation on the left to start chatting", + "ungrouped": "Ungrouped", + "colleagueStaffNo": "Staff number", + "verifyMessageOptional": "Verification message (optional)", + "addColleagueDefaultMsg": "Hi, I would like to add you as a colleague", + "sendApplication": "Send request", "subTitle": "公司内部通讯工具", "myCreated": "I created", "myJoined": "I joined", @@ -468,6 +482,7 @@ "refused": "Refused", "information": "Verification Information", "pending": "Pending Verification", + "waitingHandle": "Pending", "applyToJoin": "Apply to Join", "applyToAdd": "Apply to Add", "applyToFriend": "Applied to Add You as a Friend" diff --git a/pc-client/src/i18n/resources/zh.json b/pc-client/src/i18n/resources/zh.json index 80bbebd..9ab0605 100644 --- a/pc-client/src/i18n/resources/zh.json +++ b/pc-client/src/i18n/resources/zh.json @@ -112,6 +112,11 @@ "inputVerifyCode": "请输入您的验证码", "inputPassword": "请输入您的密码", "inputStaffNo": "请输入工号", + "staffOrPasswordWrong": "工号或密码不正确", + "networkRetry": "网络异常,请稍后重试", + "inputPeerStaffNo": "请输入对方工号", + "addColleagueSent": "申请已发送,等对方通过", + "addColleagueFailed": "发送失败,请确认工号正确、网络正常", "sendFreiendRequestSuccess": "发送好友请求成功!", "registerSuccess": "注册成功!", "inputInvitationCode": "请输入您的邀请码", @@ -200,6 +205,15 @@ "qrCodeLoginTitle": "请使用OpenCorp移动端扫描二维码", "welcome": "欢迎使用畅联", "title": "畅联", + "internalComms": "企业内部通讯", + "loggingIn": "登录中", + "agreeInternal": "登录即代表同意内部使用规范", + "emptyChat": "从左边选一个会话,开始聊天", + "ungrouped": "未分组", + "colleagueStaffNo": "同事工号", + "verifyMessageOptional": "验证消息(可不填)", + "addColleagueDefaultMsg": "你好,我想加你为同事", + "sendApplication": "发送申请", "subTitle": "公司内部通讯工具", "myCreated": "我创建的", "myJoined": "我加入的", @@ -480,6 +494,7 @@ "refused": "已拒绝", "information": "验证信息", "pending": "等待对方通过", + "waitingHandle": "待处理", "applyToJoin": "申请加入", "applyToAdd": "申请添加", "applyToFriend": "申请添加您为好友" diff --git a/pc-client/src/pages/chat/ConversationSider/ConversationItem.tsx b/pc-client/src/pages/chat/ConversationSider/ConversationItem.tsx index 5dec3d6..3e2a07b 100644 --- a/pc-client/src/pages/chat/ConversationSider/ConversationItem.tsx +++ b/pc-client/src/pages/chat/ConversationSider/ConversationItem.tsx @@ -2,13 +2,15 @@ import type { ConversationItem as ConversationItemType, MessageItem, } from "@openim/wasm-client-sdk/lib/types/entity"; -import { Badge } from "antd"; +import { Badge, Dropdown } from "antd"; import clsx from "clsx"; import { t } from "i18next"; import { memo, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import OIMAvatar from "@/components/OIMAvatar"; +import { IMSDK } from "@/layout/MainContentWrap"; import { useConversationStore, useUserStore } from "@/store"; +import { feedbackToast } from "@/utils/common"; import { formatConversionTime, getConversationContent } from "@/utils/imCommon"; import styles from "./conversation-item.module.scss"; @@ -50,47 +52,90 @@ const ConversationItem = ({ isActive, conversation }: IConversationProps) => { const latestMessageTime = formatConversionTime(conversation.latestMsgSendTime); + const onMenuClick = async ({ key }: { key: string }) => { + try { + if (key === "pin") { + await IMSDK.pinConversation({ + conversationID: conversation.conversationID, + isPinned: !conversation.isPinned, + }); + return; + } + if (key === "delete") { + await IMSDK.deleteConversationAndDeleteAllMsg(conversation.conversationID); + if (isActive) { + navigate("/chat"); + } + } + } catch (error) { + feedbackToast({ + error, + msg: + key === "pin" + ? t("toast.pinConversationFailed") + : t("toast.deleteConversationFailed"), + }); + } + }; + return ( -
- +
+ -
-
-
- {conversation.showName} +
+
+
+ {conversation.showName} +
+
+ {latestMessageTime} +
-
{latestMessageTime}
-
-
-
-
+
+
+
+
+
-
-
+ ); }; diff --git a/pc-client/src/pages/chat/ConversationSider/conversation-item.module.scss b/pc-client/src/pages/chat/ConversationSider/conversation-item.module.scss index ddeadfe..61a2dae 100644 --- a/pc-client/src/pages/chat/ConversationSider/conversation-item.module.scss +++ b/pc-client/src/pages/chat/ConversationSider/conversation-item.module.scss @@ -1,16 +1,5 @@ .conversation-item { - @apply relative mx-2 my-0.5 flex cursor-pointer items-center rounded-lg px-3 py-2.5; - - &-pined { - &::after { - content: ""; - position: absolute; - right: 0; - top: 0; - border: 4px solid; - border-color: var(--primary) var(--primary) transparent transparent; - } - } + @apply relative mx-2 my-0 flex cursor-pointer items-center rounded-lg px-3; :global(.emojione) { width: 16px; diff --git a/pc-client/src/pages/chat/ConversationSider/index.tsx b/pc-client/src/pages/chat/ConversationSider/index.tsx index 86870c2..eeee7f7 100644 --- a/pc-client/src/pages/chat/ConversationSider/index.tsx +++ b/pc-client/src/pages/chat/ConversationSider/index.tsx @@ -1,7 +1,7 @@ import { SearchOutlined, PlusOutlined } from "@ant-design/icons"; import { Button, Input, Spin } from "antd"; import { t } from "i18next"; -import { useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; @@ -11,43 +11,90 @@ import { useConversationStore, useUserStore } from "@/store"; import ConversationItemComp from "./ConversationItem"; -// 断网时顶部的深色提示横条 -const OfflineBar = () => ( -
-
- + typeof window !== "undefined" && + window.matchMedia("(prefers-reduced-motion: reduce)").matches; + +const OfflineBar = ({ visible }: { visible: boolean }) => { + const [shown, setShown] = useState(visible); + const [leaving, setLeaving] = useState(false); + + useEffect(() => { + if (visible) { + setShown(true); + setLeaving(false); + return; + } + if (!shown) return; + if (prefersReducedMotion()) { + setShown(false); + setLeaving(false); + return; + } + setLeaving(true); + const timer = window.setTimeout(() => { + setShown(false); + setLeaving(false); + }, 300); + return () => window.clearTimeout(timer); + }, [visible, shown]); + + if (!shown) return null; + + return ( +
+
- - - - - {t("states.offline")} + + + + + + {t("states.offline")} +
+
+ {t("states.offlineTip")} +
-
- {t("states.offlineTip")} -
-
+ ); +}; + +const LinearChatIcon = () => ( + + + ); -const EmptyIcon = () => ( - +const LinearFailIcon = () => ( + - - - + + ); @@ -96,15 +143,12 @@ const ConversationSider = () => { if (syncState === "failed" && conversationList.length === 0) { return (
- +
{t("states.loadFailed")}
{t("states.loadFailedTip")}
-
@@ -113,7 +157,7 @@ const ConversationSider = () => { if (conversationList.length === 0) { return (
- +
{t("states.noMessage")}
{t("states.noMessageTip")} @@ -150,12 +194,13 @@ const ConversationSider = () => { needHidden={Boolean(conversationID)} wrapClassName="flex flex-col border-r border-[var(--gap-text)] bg-white" > - {connectState === "failed" && } +
{t("placeholder.chat")}
} placeholder={t("placeholder.search")} allowClear @@ -163,7 +208,7 @@ const ConversationSider = () => { onChange={(e) => setKeyword(e.target.value)} />
setAddColleagueOpen(true)} > diff --git a/pc-client/src/pages/chat/EmptyChat.tsx b/pc-client/src/pages/chat/EmptyChat.tsx index 50c265d..a175acd 100644 --- a/pc-client/src/pages/chat/EmptyChat.tsx +++ b/pc-client/src/pages/chat/EmptyChat.tsx @@ -1,23 +1,26 @@ import { Layout } from "antd"; import { useTranslation } from "react-i18next"; +const LinearChatIcon = () => ( + + + +); + export const EmptyChat = () => { const { t } = useTranslation(); return ( - +
-
- - - -
-
{t("placeholder.title")}
-
- {t("placeholder.subTitle")} + +
+ {t("placeholder.emptyChat")}
diff --git a/pc-client/src/pages/chat/queryChat/ChatFooter/index.tsx b/pc-client/src/pages/chat/queryChat/ChatFooter/index.tsx index 62e9f2b..4769284 100644 --- a/pc-client/src/pages/chat/queryChat/ChatFooter/index.tsx +++ b/pc-client/src/pages/chat/queryChat/ChatFooter/index.tsx @@ -33,7 +33,7 @@ const EmojiPanel = ({ onSelect }: { onSelect: (emoji: string) => void }) => ( {EMOJI_LIST.map((emoji) => (
onSelect(emoji)} > {emoji} @@ -42,7 +42,7 @@ const EmojiPanel = ({ onSelect }: { onSelect: (emoji: string) => void }) => (
); -const ChatFooter: ForwardRefRenderFunction = (_, ref) => { +const ChatFooter: ForwardRefRenderFunction = (_, _ref) => { const [text, setText] = useState(""); const [sending, setSending] = useState(false); const [emojiVisible, setEmojiVisible] = useState(false); @@ -141,7 +141,7 @@ const ChatFooter: ForwardRefRenderFunction = (_, ref) => { }; const toolIconClass = - "cursor-pointer text-xl text-[var(--sub-text)] hover:text-[#515E70]"; + "flex h-7 w-7 cursor-pointer items-center justify-center rounded-lg text-[20px] text-[var(--sub-text)] transition-colors duration-100 ease-out hover:bg-[var(--primary-active)] hover:text-[var(--primary)]"; return (