fix(pc-client): PC 端统一手机端设计 Token 并补提交 src/components(B-52 视觉阻塞修复)

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
编码工程师
2026-08-16 00:27:52 +08:00
co-authored by multica-agent
parent 9074b5d1d7
commit 2fe8417900
27 changed files with 884 additions and 28 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ function App() {
autoInsertSpaceInButton={false}
locale={locale === "zh-CN" ? zhCN : enUS}
theme={{
token: { colorPrimary: "#0073D9" },
token: { colorPrimary: "#3B87F5", colorError: "#F53F3F" },
}}
>
<QueryClientProvider client={queryClient}>
@@ -0,0 +1,148 @@
import { ApplicationHandleResult } from "@openim/wasm-client-sdk";
import {
FriendApplicationItem,
GroupApplicationItem,
} from "@openim/wasm-client-sdk/lib/types/entity";
import { Button, Spin } from "antd";
import { t } from "i18next";
import { memo, useCallback, useState } from "react";
import arrow from "@/assets/images/contact/arrowTopRight.png";
import OIMAvatar from "@/components/OIMAvatar";
import { IMSDK } from "@/layout/MainContentWrap";
import { emit } from "@/utils/events";
type ApplicationItemSource = FriendApplicationItem & GroupApplicationItem;
export type AccessFunction = (
source: Partial<ApplicationItemSource>,
isRecv: boolean,
) => Promise<void>;
const ApplicationItem = ({
currentUserID,
source,
onAccept,
onReject,
}: {
source: Partial<ApplicationItemSource>;
currentUserID: string;
onAccept: AccessFunction;
onReject: AccessFunction;
}) => {
const [loading, setLoading] = useState(false);
const isRecv = source.userID !== currentUserID && source.fromUserID !== currentUserID;
const isGroup = Boolean(source.groupID);
const showActionBtn = source.handleResult === 0 && isRecv;
const getApplicationDesc = () => {
if (isGroup) {
return t("application.applyToJoin");
}
return isRecv ? t("application.applyToFriend") : t("application.applyToAdd");
};
const getTitle = () => {
if (isGroup) {
return isRecv ? source.nickname : source.groupName;
}
return isRecv ? source.fromNickname : source.toNickname;
};
const getStatusStr = () => {
if (source.handleResult === ApplicationHandleResult.Agree) {
return t("application.agreed");
}
if (source.handleResult === ApplicationHandleResult.Reject) {
return t("application.refused");
}
return t("application.pending");
};
const getAvatarUrl = () => {
if (isGroup) {
return isRecv ? source.userFaceURL : source.groupFaceURL;
}
return isRecv ? source.fromFaceURL : source.toFaceURL;
};
const loadingWrap = async (isAgree: boolean) => {
setLoading(true);
await (isAgree ? onAccept(source, isRecv) : onReject(source, isRecv));
setLoading(false);
};
const tryShowCard = useCallback(async () => {
if (isGroup) {
const { data } = await IMSDK.getSpecifiedGroupsInfo([source.groupID!]);
emit("OPEN_GROUP_CARD", data[0]);
return;
}
window.userClick(isRecv ? source.fromUserID : source.toUserID);
}, []);
return (
<Spin spinning={loading}>
<div className="flex flex-row items-center justify-between p-3.5 transition-colors hover:bg-[var(--primary-active)]">
<div className="flex flex-row">
<OIMAvatar
src={getAvatarUrl()}
text={getTitle()}
isgroup={isGroup && !isRecv}
onClick={tryShowCard}
/>
<div className="ml-3">
<p className="text-sm">{getTitle()}</p>
<p className="pb-2.5 pt-[5px] text-xs ">
{getApplicationDesc()}
{(isGroup || (!isGroup && !isRecv)) && (
<span className="ml-1 text-xs text-[var(--primary)]">
{source.groupName || source.toNickname}
</span>
)}
</p>
<p className="text-xs text-[var(--sub-text)]">
{t("application.information")}:
</p>
<p className="text-xs text-[var(--sub-text)]">{source.reqMsg}</p>
</div>
</div>
{showActionBtn && (
<div className="flex flex-row">
<div className="mr-5.5 h-8 w-[60px]">
<Button
block={true}
size="small"
onClick={() => loadingWrap(false)}
className="!h-full !rounded-md border-2 border-[var(--primary)] text-[var(--primary)]"
>
{t("application.refuse")}
</Button>
</div>
<div className="h-8 w-[60px]">
<Button
block={true}
size="small"
type="primary"
className="!h-full !rounded-md bg-[var(--primary)]"
onClick={() => loadingWrap(true)}
>
{t("application.agree")}
</Button>
</div>
</div>
)}
{!showActionBtn && (
<div className="flex flex-row items-center">
{!isRecv && <img className="mr-2 h-4 w-4" src={arrow} alt="" />}
<p className="text-sm text-[var(--sub-text)]">{getStatusStr()}</p>
</div>
)}
</div>
</Spin>
);
};
export default memo(ApplicationItem);
@@ -0,0 +1,105 @@
.image-inline {
margin: 2px;
img {
aspect-ratio: auto !important;
max-width: 30vw !important;
max-height: 15vh !important;
width: auto !important;
object-fit: contain;
cursor: pointer;
}
}
.ck-content {
padding: 0 18px !important;
border: none !important;
word-break: break-all;
}
.ck-content [draggable="true"] {
pointer-events: none;
}
.ck-content > p {
margin: 10px 0 !important;
}
.ck.ck-toolbar {
border: none !important;
}
.ck-editor__editable {
--ck-inner-shadow: none !important;
--ck-drop-shadow: none !important;
--ck-drop-shadow-active: none !important;
outline: none !important;
}
.ck-editor__editable:focus {
box-shadow: none !important;
}
.ck.ck-editor__editable[role="textbox"]:focus {
border: none;
box-shadow: none !important;
}
.ck-powered-by {
display: none !important;
}
.ck-widget__type-around__button,
.ck-widget__type-around__button_after {
display: none !important;
}
.ck-editor {
flex: 1;
overflow-y: auto;
}
.ck-editor__main,
.ck-content {
height: 100% !important;
}
.ck-image-upload-complete-icon {
display: none !important;
}
.ck-image-upload-complete-icon:after {
display: none !important;
}
.ck-balloon-panel_visible {
border-radius: 10px !important;
border: none !important;
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05) !important;
overflow: hidden !important;
}
.ck-list__item .ck-on {
background-color: #f0f0f0 !important;
}
.ck .ck-widget {
--ck-widget-outline-thickness: 2px !important;
--ck-color-widget-hover-border: transparent !important;
}
.ck .ck-widget.ck-widget_selected {
--ck-widget-outline-thickness: 2px !important;
}
.ck.ck-clipboard-drop-target-line {
--ck-clipboard-drop-target-color: transparent !important;
}
.ck-powered-by-balloon {
z-index: -99;
}
.ck-editor__top {
display: none;
}
+123
View File
@@ -0,0 +1,123 @@
import "./index.scss";
import "ckeditor5/ckeditor5.css";
import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic";
import { Essentials } from "@ckeditor/ckeditor5-essentials";
import { Paragraph } from "@ckeditor/ckeditor5-paragraph";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import {
forwardRef,
ForwardRefRenderFunction,
memo,
useImperativeHandle,
useRef,
} from "react";
export type CKEditorRef = {
focus: (moveToEnd?: boolean) => void;
};
interface CKEditorProps {
value: string;
placeholder?: string;
onChange?: (value: string) => void;
onEnter?: () => void;
}
export interface EmojiData {
src: string;
alt: string;
}
const keyCodes = {
delete: 46,
backspace: 8,
};
const Index: ForwardRefRenderFunction<CKEditorRef, CKEditorProps> = (
{ value, placeholder, onChange, onEnter },
ref,
) => {
const ckEditor = useRef<ClassicEditor | null>(null);
const focus = (moveToEnd = false) => {
const editor = ckEditor.current;
if (editor) {
const model = editor.model;
const view = editor.editing.view;
const root = model.document.getRoot();
if (moveToEnd && root) {
const range = model.createRange(model.createPositionAt(root, "end"));
model.change((writer) => {
writer.setSelection(range);
});
}
view.focus();
}
};
const listenKeydown = (editor: ClassicEditor) => {
editor.editing.view.document.on(
"keydown",
(evt, data) => {
if (data.keyCode === 13 && !data.shiftKey) {
data.preventDefault();
evt.stop();
onEnter?.();
return;
}
if (data.keyCode === keyCodes.backspace || data.keyCode === keyCodes.delete) {
const selection = editor.model.document.selection;
const hasSelectContent = !editor.model.getSelectedContent(selection).isEmpty;
const hasEditorContent = Boolean(editor.getData());
if (!hasEditorContent) {
return;
}
if (hasSelectContent) return;
}
},
{ priority: "high" },
);
};
useImperativeHandle(
ref,
() => ({
focus,
}),
[],
);
return (
<CKEditor
editor={ClassicEditor}
data={value}
config={{
placeholder,
toolbar: [],
image: {
toolbar: [],
insert: {
type: "inline",
},
},
plugins: [Essentials, Paragraph],
}}
onReady={(editor) => {
ckEditor.current = editor;
listenKeydown(editor);
focus(true);
}}
onChange={(event, editor) => {
const data = editor.getData();
onChange?.(data);
}}
/>
);
};
export default memo(forwardRef(Index));
@@ -0,0 +1,46 @@
export const replaceEmoji2Str = (text: string) => {
const parser = new DOMParser();
const doc = parser.parseFromString(text, "text/html");
const emojiEls: HTMLImageElement[] = Array.from(doc.querySelectorAll(".emojione"));
emojiEls.map((face) => {
// @ts-ignore
const escapedOut = face.outerHTML.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
text = text.replace(new RegExp(escapedOut, "g"), face.alt);
});
return text;
};
export const getCleanText = (html: string) => {
let text = replaceEmoji2Str(html);
text = text.replace(/<\/p><p>/g, "\n");
text = text.replace(/<br\s*[/]?>/gi, "\n");
text = text.replace(/<[^>]+>/g, "");
text = convertChar(text);
text = decodeHtmlEntities(text);
return text.trim();
};
let textAreaDom: HTMLTextAreaElement | null = null;
const decodeHtmlEntities = (text: string) => {
if (!textAreaDom) {
textAreaDom = document.createElement("textarea");
}
textAreaDom.innerHTML = text;
return textAreaDom.value;
};
export const convertChar = (text: string) => text.replace(/&nbsp;/gi, " ");
export const getCleanTextExceptImg = (html: string) => {
html = replaceEmoji2Str(html);
const regP = /<\/p><p>/g;
html = html.replace(regP, "</p><br><p>");
const regBr = /<br\s*\/?>/gi;
html = html.replace(regBr, "\n");
const regWithoutHtmlExceptImg = /<(?!img\s*\/?)[^>]+>/gi;
return html.replace(regWithoutHtmlExceptImg, "");
};
@@ -0,0 +1,47 @@
import { Modal, ModalProps } from "antd";
import { FC, memo, useRef, useState } from "react";
import type { DraggableData, DraggableEvent } from "react-draggable";
import Draggable from "react-draggable";
interface IDraggableModalWrapProps extends ModalProps {
ignoreClasses?: string;
}
const DraggableModalWrap: FC<IDraggableModalWrapProps> = (props) => {
const [bounds, setBounds] = useState({ left: 0, top: 0, bottom: 0, right: 0 });
const draggleRef = useRef<HTMLDivElement>(null);
const onStart = (_event: DraggableEvent, uiData: DraggableData) => {
const { clientWidth, clientHeight } = window.document.documentElement;
const targetRect = draggleRef.current?.getBoundingClientRect();
if (!targetRect) {
return;
}
setBounds({
left: -targetRect.left + uiData.x,
right: clientWidth - (targetRect.right - uiData.x),
top: -targetRect.top + uiData.y,
bottom: clientHeight - (targetRect.bottom - uiData.y),
});
};
return (
<Modal
{...props}
modalRender={(modal) => (
<Draggable
allowAnyClick
cancel={props.ignoreClasses}
bounds={bounds}
onStart={(event, uiData) => onStart(event, uiData)}
>
<div ref={draggleRef}>{modal}</div>
</Draggable>
)}
>
{props.children}
</Modal>
);
};
export default memo(DraggableModalWrap);
@@ -0,0 +1,101 @@
import { EnterOutlined } from "@ant-design/icons";
import { useClickAway } from "ahooks";
import { Input, InputRef } from "antd";
import clsx from "clsx";
import { FC, useRef, useState } from "react";
import edit_name from "@/assets/images/chatSetting/edit_name.png";
interface IEditableContentProps {
editable?: boolean;
value?: string;
placeholder?: string;
className?: string;
textClassName?: string;
onChange?: (value: string) => Promise<void>;
}
const EditableContent: FC<IEditableContentProps> = ({
editable,
value,
placeholder,
className,
textClassName,
onChange,
}) => {
const wrapRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<InputRef>(null);
const [editState, setEditState] = useState({
isEdit: false,
loading: false,
innerValue: value,
});
useClickAway(() => {
if (editState.isEdit) {
setEditState({
isEdit: false,
loading: false,
innerValue: value,
});
}
}, [wrapRef]);
const toggleEdit = (e: React.MouseEvent<HTMLImageElement, MouseEvent>) => {
e.stopPropagation();
setEditState({
isEdit: true,
loading: false,
innerValue: value === "-" ? "" : value,
});
setTimeout(() => inputRef.current?.focus());
};
const onPressEnter = async (
e: React.KeyboardEvent<HTMLInputElement> & { target: { value: string } },
) => {
setEditState((state) => ({ ...state, loading: true }));
await onChange?.(e.target.value);
setEditState({
isEdit: false,
loading: false,
innerValue: e.target.value,
});
};
return (
<div ref={wrapRef} className={clsx("ml-3 flex items-center", className)}>
{editState.isEdit ? (
<Input
spellCheck={false}
value={editState.innerValue}
placeholder={placeholder}
maxLength={20}
onChange={(e) =>
setEditState((state) => ({ ...state, innerValue: e.target.value }))
}
ref={inputRef}
onPressEnter={onPressEnter}
suffix={<EnterOutlined rev={undefined} />}
/>
) : (
<>
<div className={clsx("mr-1 max-w-[240px] truncate", textClassName)}>
{value}
</div>
{editable && (
<img
className="cursor-pointer"
width={14}
src={edit_name}
alt="edit name"
onClick={toggleEdit}
/>
)}
</>
)}
</div>
);
};
export default EditableContent;
@@ -0,0 +1,37 @@
import { Component, ErrorInfo, ReactNode } from "react";
type ErrorBoundaryProps = {
logTips?: string;
placeholder: ReactNode;
children: ReactNode;
};
type ErrorBoundaryState = {
hasError: boolean;
logTips?: string;
};
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, logTips: props.logTips };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error(this.state.logTips ?? "ErrorBoundary");
console.error(errorInfo);
console.error(error);
this.setState({ hasError: true });
}
render() {
if (this.state.hasError) {
return this.props.placeholder;
}
return this.props.children;
}
}
export default ErrorBoundary;
@@ -0,0 +1,22 @@
.sider_resize {
width: 300px;
min-width: 240px;
max-width: 45vw;
resize: horizontal;
overflow: scroll;
height: 100%;
opacity: 0;
&::-webkit-scrollbar {
height: calc(100vh - 40px);
}
}
.sider_bar {
position: absolute;
right: 0;
top: 0;
bottom: 0;
border-left: 1px solid var(--gap-text);
pointer-events: none;
}
@@ -0,0 +1,34 @@
import clsx from "clsx";
import * as React from "react";
import styles from "./flexible-sider.module.scss";
const FlexibleSider = ({
needHidden,
children,
wrapClassName,
}: {
needHidden: boolean;
wrapClassName?: string;
children: React.ReactNode;
}) => (
<aside
className={clsx(
"relative bg-white dark:text-white",
{ "max-[600px]:hidden": needHidden },
{ "max-[600px]:!max-w-none max-[600px]:!basis-full": !needHidden },
)}
>
<div
className={`absolute bottom-0 left-0 right-1 top-0 z-10 overflow-hidden ${
wrapClassName ?? ""
}`}
>
{children}
</div>
<div className={styles.sider_resize}></div>
<div className={styles.sider_bar}></div>
</aside>
);
export default FlexibleSider;
@@ -0,0 +1,91 @@
import { Avatar as AntdAvatar, AvatarProps } from "antd";
import clsx from "clsx";
import * as React from "react";
import { useMemo } from "react";
import default_group from "@/assets/images/contact/group.png";
import { avatarList, getDefaultAvatar } from "@/utils/avatar";
const default_avatars = avatarList.map((item) => item.name);
// 头像文字:两个字以内原样显示;中文名取最后一个字,其他取首字母
const getDisplayText = (text?: string) => {
if (!text) return text;
const trimmed = text.trim();
if (trimmed.length <= 2) return trimmed;
if (/^[一-龥]/.test(trimmed)) return trimmed.slice(-1);
return trimmed[0].toUpperCase();
};
interface IOIMAvatarProps extends AvatarProps {
text?: string;
color?: string;
bgColor?: string;
isgroup?: boolean;
isnotification?: boolean;
size?: number;
}
const OIMAvatar: React.FC<IOIMAvatarProps> = (props) => {
const {
src,
text,
size = 42,
color = "#fff",
bgColor = "#3B87F5",
isgroup = false,
isnotification,
} = props;
const [errorHolder, setErrorHolder] = React.useState<string>();
const getAvatarUrl = useMemo(() => {
if (src) {
if (default_avatars.includes(src as string))
return getDefaultAvatar(src as string);
return src;
}
return isgroup ? default_group : undefined;
}, [src, isgroup, isnotification]);
const avatarProps = { ...props, isgroup: undefined, isnotification: undefined };
React.useEffect(() => {
if (!isgroup) {
setErrorHolder(undefined);
}
}, [isgroup]);
const errorHandler = () => {
if (isgroup) {
setErrorHolder(default_group);
}
};
return (
<AntdAvatar
style={{
backgroundColor: bgColor,
minWidth: `${size}px`,
minHeight: `${size}px`,
lineHeight: `${size - 2}px`,
borderRadius: `${Math.round(size * 0.18)}px`,
color,
}}
shape="square"
{...avatarProps}
className={clsx(
{
"cursor-pointer": Boolean(props.onClick),
},
props.className,
)}
src={errorHolder ?? getAvatarUrl}
onError={errorHandler as any}
>
{getDisplayText(text)}
</AntdAvatar>
);
};
export default OIMAvatar;
@@ -0,0 +1,53 @@
import { Switch } from "antd";
import clsx from "clsx";
import { FC, ReactNode, useState } from "react";
interface ISettingRowProps {
title: string;
value?: boolean;
hidden?: boolean;
className?: string;
children?: ReactNode;
tryChange?: (checked: boolean) => Promise<void>;
rowClick?: () => void;
}
const SettingRow: FC<ISettingRowProps> = ({
title,
value,
hidden,
className,
children,
tryChange,
rowClick,
}) => {
const [loading, setLoading] = useState(false);
const onClick = async (checked: boolean) => {
setLoading(true);
await tryChange?.(checked);
setLoading(false);
};
if (hidden) {
return null;
}
return (
<div
className={clsx("flex items-center justify-between p-4", className)}
onClick={rowClick}
>
<div className="font-medium">{title}</div>
{children ?? (
<Switch
className="bg-[var(--sub-text)]"
loading={loading}
checked={value}
onClick={onClick}
/>
)}
</div>
);
};
export default SettingRow;
@@ -0,0 +1,47 @@
import { Platform } from "@openim/wasm-client-sdk";
import { useKeyPress } from "ahooks";
import win_close from "@/assets/images/topSearchBar/win_close.png";
import win_max from "@/assets/images/topSearchBar/win_max.png";
import win_min from "@/assets/images/topSearchBar/win_min.png";
const WindowControlBar = () => {
useKeyPress("esc", () => {
window.electronAPI?.ipcInvoke("minimizeWindow");
});
if (!window.electronAPI || window.electronAPI?.getPlatform() === Platform.MacOSX) {
return null;
}
return (
<div className="absolute right-3 top-3.5 z-[99999999] flex h-fit items-center">
<div
className="app-no-drag flex h-[14px] cursor-pointer items-center"
onClick={() => window.electronAPI?.ipcInvoke("minimizeWindow")}
>
<img
className="app-no-drag cursor-pointer"
width={14}
src={win_min}
alt="win_min"
/>
</div>
<img
className="app-no-drag mx-3 cursor-pointer"
width={13}
src={win_max}
alt="win_max"
onClick={() => window.electronAPI?.ipcInvoke("maxmizeWindow")}
/>
<img
className="app-no-drag cursor-pointer"
width={12}
src={win_close}
alt="win_close"
onClick={() => window.electronAPI?.ipcInvoke("closeWindow")}
/>
</div>
);
};
export default WindowControlBar;
+1 -1
View File
@@ -136,7 +136,7 @@ export const AboutContent = ({ closeOverlay }: { closeOverlay?: () => void }) =>
<div className="app-drag flex items-center justify-between bg-[var(--gap-text)] p-5">
<span className="text-base font-medium">{t("placeholder.about")}</span>
<CloseOutlined
className="app-no-drag cursor-pointer text-[#8e9aaf]"
className="app-no-drag cursor-pointer text-[var(--sub-text)]"
rev={undefined}
onClick={closeOverlay}
/>
@@ -84,7 +84,7 @@ export const BlackListContent = ({ closeOverlay }: { closeOverlay?: () => void }
<div className="flex items-center justify-between bg-[var(--gap-text)] p-5">
<span className="text-base font-medium">{t("placeholder.blackList")}</span>
<CloseOutlined
className="app-no-drag cursor-pointer text-[#8e9aaf]"
className="app-no-drag cursor-pointer text-[var(--sub-text)]"
rev={undefined}
onClick={closeOverlay}
/>
@@ -64,7 +64,7 @@ export const PersonalSettingsContent = ({
<div className="app-drag flex items-center justify-between bg-[var(--gap-text)] p-5">
<span className="text-base font-medium">{t("placeholder.accountSetting")}</span>
<CloseOutlined
className="app-no-drag cursor-pointer text-[#8e9aaf]"
className="app-no-drag cursor-pointer text-[var(--sub-text)]"
rev={undefined}
onClick={closeOverlay}
/>
+1 -1
View File
@@ -128,7 +128,7 @@ const NavItem = ({ nav: { icon: Icon, icon_active: ActiveIcon, title, path } }:
<NavIcon
className={clsx("text-[22px]", {
"text-[var(--primary)]": isActive,
"text-[#8e9ab0]": !isActive,
"text-[var(--sub-text)]": !isActive,
})}
rev={undefined}
/>
@@ -13,7 +13,7 @@ import ConversationItemComp from "./ConversationItem";
// 断网时顶部的深色提示横条
const OfflineBar = () => (
<div>
<div className="flex h-9 items-center bg-[#4a4a4a] px-3 text-xs text-white">
<div className="flex h-9 items-center bg-[#3a3f47] px-3 text-xs text-white">
<svg
width="14"
height="14"
@@ -153,7 +153,7 @@ const ConversationSider = () => {
<div className="pb-3 text-base font-bold">{t("placeholder.chat")}</div>
<Input
className="app-no-drag mb-2 rounded-md border-none !bg-[#F3F5F7]"
prefix={<SearchOutlined className="text-[#8e9ab0]" rev={undefined} />}
prefix={<SearchOutlined className="text-[var(--sub-text)]" rev={undefined} />}
placeholder={t("placeholder.search")}
allowClear
value={keyword}
@@ -141,7 +141,7 @@ const ChatFooter: ForwardRefRenderFunction<unknown, unknown> = (_, ref) => {
};
const toolIconClass =
"cursor-pointer text-xl text-[#8e9ab0] hover:text-[#515E70]";
"cursor-pointer text-xl text-[var(--sub-text)] hover:text-[#515E70]";
return (
<footer className="relative bg-white">
@@ -186,7 +186,7 @@ const ChatFooter: ForwardRefRenderFunction<unknown, unknown> = (_, ref) => {
{/* 录音条 */}
{recording && (
<div className="mx-4 mt-2 flex items-center rounded-md bg-[#F3F5F7] px-3 py-2">
<span className="mr-2 h-2 w-2 animate-pulse rounded-full bg-[#F4493C]" />
<span className="mr-2 h-2 w-2 animate-pulse rounded-full bg-[var(--warn-text)]" />
<span className="flex-1 text-sm">
{t("placeholder.microphone")} {seconds}
</span>
@@ -17,14 +17,14 @@ const FileIcon = () => (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
<path
d="M6 2.5h8L19 7.5v13a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1v-17a1 1 0 0 1 1-1Z"
stroke="#0073D9"
stroke="var(--primary)"
strokeWidth="1.6"
fill="#fff"
/>
<path d="M13.5 2.5v5.5H19" stroke="#0073D9" strokeWidth="1.6" fill="none" />
<path d="M13.5 2.5v5.5H19" stroke="var(--primary)" strokeWidth="1.6" fill="none" />
<path
d="M8.5 12h7M8.5 15.5h7"
stroke="#0073D9"
stroke="var(--primary)"
strokeWidth="1.4"
strokeLinecap="round"
/>
@@ -88,7 +88,7 @@ const SoundMessageRender: FC<IMessageItemProps> = ({ message, isSender }) => {
<span className={clsx(isSender ? "mr-2" : "ml-2")}>{duration}</span>
</div>
{!isSender && !played && (
<span className="ml-2 h-2 w-2 shrink-0 rounded-full bg-[#F4493C]" />
<span className="ml-2 h-2 w-2 shrink-0 rounded-full bg-[var(--warn-text)]" />
)}
</div>
);
@@ -36,7 +36,7 @@ const AlphabetIndex: ForwardRefRenderFunction<
{indexList.map((letter, idx) => (
<span
className={clsx("my-0.5 cursor-pointer text-xs text-[var(--sub-text)]", {
"!text-[#0073D9]": currentAlphabet === letter,
"!text-[var(--primary)]": currentAlphabet === letter,
})}
key={letter}
onClick={() => jumpToLetter(idx, letter)}
@@ -90,10 +90,10 @@ export const MyFriends = () => {
groupCounts={sectionData.groupCounts}
groupContent={(index) => (
<div>
<div className="bg-white px-3.5 pb-1 text-[13px] text-[#8E9AB0FF]">
<div className="bg-white px-3.5 pb-1 text-[13px] text-[var(--sub-text)]">
{sectionData.indexList[index]}
</div>
<div className="mx-3.5 mb-3 h-px w-full bg-[#E8EAEFFF] bg-white" />
<div className="mx-3.5 mb-3 h-px w-full bg-[var(--gap-text)] bg-white" />
</div>
)}
itemContent={(index) => {
@@ -17,7 +17,7 @@ const GroupListItem = ({
<OIMAvatar size={48} src={source?.faceURL} isgroup />
<div className="ml-3">
<p className="text-sm">{source.groupName}</p>
<p className="text-xs text-[#8E9AB0FF]">{source.memberCount}</p>
<p className="text-xs text-[var(--sub-text)]">{source.memberCount}</p>
</div>
</div>
);
+2 -2
View File
@@ -79,7 +79,7 @@
.ant-drawer-header {
padding: 0 24px;
background-color: #e8eaef;
background-color: var(--gap-text);
border: none;
.ant-drawer-header-title {
@@ -101,7 +101,7 @@
.anticon-close,
.anticon-right {
color: #8e9ab0;
color: var(--sub-text);
}
}
+8 -8
View File
@@ -1,14 +1,14 @@
:root {
--full-height: 100%;
--chat-bubble: #ffffff;
--chat-bubble-sender: #cde6ff;
--base-black: #0c1c33;
--primary: #0073d9;
--primary-active: #eaf4ff;
--top-search-bar: #0073d9;
--sub-text: #8e9ab0;
--gap-text: #e8eaef;
--warn-text: #ff381f;
--chat-bubble-sender: #d6e7fc;
--base-black: #1d2129;
--primary: #3b87f5;
--primary-active: #ebf3fe;
--top-search-bar: #3b87f5;
--sub-text: #86909c;
--gap-text: #eeeeee;
--warn-text: #f53f3f;
--moment-text: #6085b1;
--searchbar-height: 2.5rem;
}