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;
}
}
// 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 { Layout, Input, Button, Modal, Spin } from "antd";
import { Layout, Input, Button, Modal, message } from "antd";
import {
SendOutlined,
FolderOutlined,
@@ -7,7 +7,6 @@ import {
ExportOutlined,
CloseOutlined,
MessageOutlined,
LoadingOutlined,
} from "@ant-design/icons";
import { ContractData, weChatGroup, ChatRecord } from "@/pages/pc/ckbox/data";
import { useWebSocketStore } from "@/store/module/websocket/websocket";
@@ -47,12 +46,27 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
state => state.quoteMessageContent,
);
const isLoadingAiChat = useWeChatStore(state => state.isLoadingAiChat);
const updateIsLoadingAiChat = useWeChatStore(
state => state.updateIsLoadingAiChat,
);
const updateQuoteMessageContent = useWeChatStore(
state => state.updateQuoteMessageContent,
);
useEffect(() => {
if (quoteMessageContent) {
setInputValue(quoteMessageContent);
}
}, [quoteMessageContent]);
// 取消AI生成
const handleCancelAi = () => {
// 停止AI加载状态
updateIsLoadingAiChat(false);
// 清空AI回复内容
updateQuoteMessageContent("");
message.info("已取消AI生成");
};
const handleSend = async () => {
if (!inputValue.trim()) return;
const messageId = +Date.now();
@@ -199,115 +213,156 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
<>
{/* 聊天输入 */}
<Footer className={styles.chatFooter}>
{["common"].includes(EnterModule) && (
<div className={styles.inputContainer}>
<div className={styles.inputToolbar}>
<div className={styles.leftTool}>
<EmojiPicker onEmojiSelect={handleEmojiSelect} />
<SimpleFileUpload
onFileUploaded={filePath =>
handleFileUploaded(filePath, FileType.FILE)
}
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;
{isLoadingAiChat ? (
<div className={styles.aiLoadingContainer}>
<div className={styles.aiLoadingContent}>
<div className={styles.aiLoadingIcon}>
{/* WiFi式波纹 - 左侧 */}
<div className={styles.waveLeft}>
<div className={styles.wave1}></div>
<div className={styles.wave2}></div>
<div className={styles.wave3}></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>
{isLoadingAiChat ? (
<Spin
indicator={<LoadingOutlined spin />}
spinning={isLoadingAiChat}
>
<div style={{ height: "100px" }}>
<div>Ai思考中...</div>
</div>
</Spin>
) : (
<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>
) : (
<>
{["common"].includes(EnterModule) && (
<div className={styles.inputContainer}>
<div className={styles.inputToolbar}>
<div className={styles.leftTool}>
<EmojiPicker onEmojiSelect={handleEmojiSelect} />
<SimpleFileUpload
onFileUploaded={filePath =>
handleFileUploaded(filePath, FileType.FILE)
}
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 />}
/>
}
/>
<div className={styles.sendButtonArea}>
<Button
type="primary"
icon={<SendOutlined />}
onClick={handleSend}
disabled={!inputValue.trim()}
className={styles.sendButton}
>
</Button>
<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>
<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>
)}
{["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}>
Ctrl+Enter换行Enter发送
</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.actionButton} onClick={handleCancelAction}>
<CloseOutlined className={styles.actionIcon} />
<span className={styles.actionText}></span>
</div>
</div>
<div
className={styles.actionButton}
onClick={handleCancelAction}
>
<CloseOutlined className={styles.actionIcon} />
<span className={styles.actionText}></span>
</div>
</div>
)}
</>
)}
</Footer>