Merge branch 'yongpxu-dev' into yongxu-dev3
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
聊天记录
|
||||
{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 />
|
||||
聊天记录
|
||||
</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user