Merge branch 'yongpxu-dev' into yongxu-dev3

This commit is contained in:
超级老白兔
2025-10-27 17:13:24 +08:00
2 changed files with 404 additions and 102 deletions

View File

@@ -227,3 +227,250 @@
min-width: 60px; min-width: 60px;
} }
} }
// AI 加载动效样式
.aiLoadingContainer {
display: flex;
align-items: center;
justify-content: center;
height: 190px;
background: linear-gradient(135deg, #f8f9ff 0%, #fef5ff 100%);
border-radius: 8px;
margin: 8px 0;
position: relative;
overflow: hidden;
&::before {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(
45deg,
transparent 30%,
rgba(138, 99, 210, 0.05) 50%,
transparent 70%
);
animation: shimmer 3s infinite;
}
}
@keyframes shimmer {
0% {
transform: translateX(-100%) translateY(-100%) rotate(45deg);
}
100% {
transform: translateX(100%) translateY(100%) rotate(45deg);
}
}
.aiLoadingContent {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
position: relative;
z-index: 1;
}
.aiLoadingIcon {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 0;
height: 60px;
}
.brainIcon {
font-size: 36px;
animation: float 2s ease-in-out infinite;
filter: drop-shadow(0 4px 8px rgba(138, 99, 210, 0.3));
position: relative;
z-index: 2;
margin: 0 12px;
}
@keyframes float {
0%,
100% {
transform: translateY(0) scale(1);
}
50% {
transform: translateY(-8px) scale(1.05);
}
}
// WiFi波纹容器 - 左侧
.waveLeft {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
gap: 5px;
}
// WiFi波纹容器 - 右侧
.waveRight {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 5px;
}
// 波纹条样式
.wave1,
.wave2,
.wave3 {
height: 3px;
background: #8a63d2;
border-radius: 2px;
animation: waveExpand 1.5s ease-out infinite;
}
.wave1 {
width: 12px;
animation-delay: 0s;
}
.wave2 {
width: 20px;
animation-delay: 0.25s;
}
.wave3 {
width: 28px;
animation-delay: 0.5s;
}
@keyframes waveExpand {
0% {
opacity: 0;
transform: scaleX(0.3);
}
40% {
opacity: 1;
}
100% {
opacity: 0;
transform: scaleX(1.2);
}
}
.aiLoadingText {
display: flex;
align-items: center;
gap: 2px;
font-size: 16px;
font-weight: 500;
color: #333;
}
.loadingTextMain {
background: linear-gradient(90deg, #8a63d2 0%, #b794f6 50%, #8a63d2 100%);
background-size: 200% auto;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: textShine 2s linear infinite;
}
@keyframes textShine {
0% {
background-position: 0% center;
}
100% {
background-position: 200% center;
}
}
.loadingDots {
display: inline-flex;
gap: 2px;
span {
animation: dotFlashing 1.4s infinite;
color: #8a63d2;
font-weight: bold;
&:nth-child(1) {
animation-delay: 0s;
}
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
}
}
@keyframes dotFlashing {
0%,
80%,
100% {
opacity: 0.3;
transform: scale(0.8);
}
40% {
opacity: 1;
transform: scale(1.2);
}
}
.aiLoadingSubText {
font-size: 12px;
color: #999;
animation: fadeInOut 2s ease-in-out infinite;
}
@keyframes fadeInOut {
0%,
100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
// 加载文字和取消按钮同行容器
.aiLoadingTextRow {
display: flex;
align-items: center;
gap: 12px;
}
// 取消AI按钮显眼版
.cancelAiButton {
height: 28px;
padding: 0 12px;
font-size: 13px;
border-radius: 6px;
border: 1px solid #ff7875;
background: #fff;
color: #ff4d4f;
font-weight: 500;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(255, 77, 79, 0.15);
&:hover {
color: #fff;
background: #ff4d4f;
border-color: #ff4d4f;
box-shadow: 0 4px 8px rgba(255, 77, 79, 0.3);
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(255, 77, 79, 0.2);
}
.anticon {
font-size: 12px;
}
}

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Layout, Input, Button, Modal, Spin } from "antd"; import { Layout, Input, Button, Modal, message } from "antd";
import { import {
SendOutlined, SendOutlined,
FolderOutlined, FolderOutlined,
@@ -7,7 +7,6 @@ import {
ExportOutlined, ExportOutlined,
CloseOutlined, CloseOutlined,
MessageOutlined, MessageOutlined,
LoadingOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { ContractData, weChatGroup, ChatRecord } from "@/pages/pc/ckbox/data"; import { ContractData, weChatGroup, ChatRecord } from "@/pages/pc/ckbox/data";
import { useWebSocketStore } from "@/store/module/websocket/websocket"; import { useWebSocketStore } from "@/store/module/websocket/websocket";
@@ -47,12 +46,27 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
state => state.quoteMessageContent, state => state.quoteMessageContent,
); );
const isLoadingAiChat = useWeChatStore(state => state.isLoadingAiChat); const isLoadingAiChat = useWeChatStore(state => state.isLoadingAiChat);
const updateIsLoadingAiChat = useWeChatStore(
state => state.updateIsLoadingAiChat,
);
const updateQuoteMessageContent = useWeChatStore(
state => state.updateQuoteMessageContent,
);
useEffect(() => { useEffect(() => {
if (quoteMessageContent) { if (quoteMessageContent) {
setInputValue(quoteMessageContent); setInputValue(quoteMessageContent);
} }
}, [quoteMessageContent]); }, [quoteMessageContent]);
// 取消AI生成
const handleCancelAi = () => {
// 停止AI加载状态
updateIsLoadingAiChat(false);
// 清空AI回复内容
updateQuoteMessageContent("");
message.info("已取消AI生成");
};
const handleSend = async () => { const handleSend = async () => {
if (!inputValue.trim()) return; if (!inputValue.trim()) return;
const messageId = +Date.now(); const messageId = +Date.now();
@@ -199,115 +213,156 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
<> <>
{/* 聊天输入 */} {/* 聊天输入 */}
<Footer className={styles.chatFooter}> <Footer className={styles.chatFooter}>
{["common"].includes(EnterModule) && ( {isLoadingAiChat ? (
<div className={styles.inputContainer}> <div className={styles.aiLoadingContainer}>
<div className={styles.inputToolbar}> <div className={styles.aiLoadingContent}>
<div className={styles.leftTool}> <div className={styles.aiLoadingIcon}>
<EmojiPicker onEmojiSelect={handleEmojiSelect} /> {/* WiFi式波纹 - 左侧 */}
<SimpleFileUpload <div className={styles.waveLeft}>
onFileUploaded={filePath => <div className={styles.wave1}></div>
handleFileUploaded(filePath, FileType.FILE) <div className={styles.wave2}></div>
} <div className={styles.wave3}></div>
maxSize={1}
type={4}
slot={
<Button
className={styles.toolbarButton}
type="text"
icon={<FolderOutlined />}
/>
}
/>
<SimpleFileUpload
onFileUploaded={filePath =>
handleFileUploaded(filePath, FileType.IMAGE)
}
maxSize={1}
type={1}
slot={
<Button
className={styles.toolbarButton}
type="text"
icon={<PictureOutlined />}
/>
}
/>
<AudioRecorder
onAudioUploaded={audioData =>
handleFileUploaded(audioData, FileType.AUDIO)
}
className={styles.toolbarButton}
/>
</div>
<div className={styles.rightTool}>
<ToContract className={styles.rightToolItem} />
<div
style={{
fontSize: "12px",
cursor: "pointer",
color: "#666",
}}
onClick={openChatRecordModel}
>
<MessageOutlined />
&nbsp;
</div> </div>
{/* 中心大脑图标 */}
<div className={styles.brainIcon}>🧠</div>
{/* WiFi式波纹 - 右侧 */}
<div className={styles.waveRight}>
<div className={styles.wave1}></div>
<div className={styles.wave2}></div>
<div className={styles.wave3}></div>
</div>
</div>
<div className={styles.aiLoadingTextRow}>
<div className={styles.aiLoadingText}>
<span className={styles.loadingTextMain}>AI </span>
<span className={styles.loadingDots}>
<span>.</span>
<span>.</span>
<span>.</span>
</span>
</div>
<Button
className={styles.cancelAiButton}
onClick={handleCancelAi}
size="small"
icon={<CloseOutlined />}
>
</Button>
</div>
<div className={styles.aiLoadingSubText}>
</div> </div>
</div> </div>
{isLoadingAiChat ? ( </div>
<Spin ) : (
indicator={<LoadingOutlined spin />} <>
spinning={isLoadingAiChat} {["common"].includes(EnterModule) && (
> <div className={styles.inputContainer}>
<div style={{ height: "100px" }}> <div className={styles.inputToolbar}>
<div>Ai思考中...</div> <div className={styles.leftTool}>
</div> <EmojiPicker onEmojiSelect={handleEmojiSelect} />
</Spin> <SimpleFileUpload
) : ( onFileUploaded={filePath =>
<div className={styles.inputArea}> handleFileUploaded(filePath, FileType.FILE)
<div className={styles.inputWrapper}> }
<TextArea maxSize={1}
value={inputValue} type={4}
onChange={e => setInputValue(e.target.value)} slot={
onKeyDown={handleKeyPress} <Button
placeholder="输入消息..." className={styles.toolbarButton}
className={styles.messageInput} type="text"
autoSize={{ minRows: 2, maxRows: 6 }} icon={<FolderOutlined />}
/> />
}
/>
<SimpleFileUpload
onFileUploaded={filePath =>
handleFileUploaded(filePath, FileType.IMAGE)
}
maxSize={1}
type={1}
slot={
<Button
className={styles.toolbarButton}
type="text"
icon={<PictureOutlined />}
/>
}
/>
<div className={styles.sendButtonArea}> <AudioRecorder
<Button onAudioUploaded={audioData =>
type="primary" handleFileUploaded(audioData, FileType.AUDIO)
icon={<SendOutlined />} }
onClick={handleSend} className={styles.toolbarButton}
disabled={!inputValue.trim()} />
className={styles.sendButton}
>
</Button>
</div> </div>
<div className={styles.rightTool}>
<ToContract className={styles.rightToolItem} />
<div
style={{
fontSize: "12px",
cursor: "pointer",
color: "#666",
}}
onClick={openChatRecordModel}
>
<MessageOutlined />
&nbsp;
</div>
</div>
</div>
<div className={styles.inputArea}>
<div className={styles.inputWrapper}>
<TextArea
value={inputValue}
onChange={e => setInputValue(e.target.value)}
onKeyDown={handleKeyPress}
placeholder="输入消息..."
className={styles.messageInput}
autoSize={{ minRows: 2, maxRows: 6 }}
/>
<div className={styles.sendButtonArea}>
<Button
type="primary"
icon={<SendOutlined />}
onClick={handleSend}
disabled={!inputValue.trim()}
className={styles.sendButton}
>
</Button>
</div>
</div>
</div>
<div className={styles.inputHint}>
Ctrl+Enter换行Enter发送
</div> </div>
</div> </div>
)} )}
{["multipleForwarding"].includes(EnterModule) && (
<div className={styles.multipleForwardingBar}>
<div className={styles.actionButton} onClick={handTurnRignt}>
<ExportOutlined className={styles.actionIcon} />
<span className={styles.actionText}></span>
</div>
<div className={styles.inputHint}> <div
Ctrl+Enter换行Enter发送 className={styles.actionButton}
</div> onClick={handleCancelAction}
</div> >
)} <CloseOutlined className={styles.actionIcon} />
{["multipleForwarding"].includes(EnterModule) && ( <span className={styles.actionText}></span>
<div className={styles.multipleForwardingBar}> </div>
<div className={styles.actionButton} onClick={handTurnRignt}> </div>
<ExportOutlined className={styles.actionIcon} /> )}
<span className={styles.actionText}></span> </>
</div>
<div className={styles.actionButton} onClick={handleCancelAction}>
<CloseOutlined className={styles.actionIcon} />
<span className={styles.actionText}></span>
</div>
</div>
)} )}
</Footer> </Footer>