FEAT => 本次更新项目为:重构上传组件的使用说明文档,统一代码风格,增加聊天文件上传组件的功能描述,并删除不再使用的样式和组件,以提升代码的可维护性和用户体验。
This commit is contained in:
254
nkebao/src/components/Upload/ChatFileUpload/example.tsx
Normal file
254
nkebao/src/components/Upload/ChatFileUpload/example.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
import React, { useState } from "react";
|
||||
import { Input, Button, Card, Space, Typography, Divider } from "antd";
|
||||
import { SendOutlined } from "@ant-design/icons";
|
||||
import ChatFileUpload from "./index";
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Text } = Typography;
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
type: "text" | "file";
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
fileInfo?: {
|
||||
url: string;
|
||||
name: string;
|
||||
type: string;
|
||||
size: number;
|
||||
};
|
||||
}
|
||||
|
||||
const ChatFileUploadExample: React.FC = () => {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
// 处理文件上传
|
||||
const handleFileUploaded = (fileInfo: {
|
||||
url: string;
|
||||
name: string;
|
||||
type: string;
|
||||
size: number;
|
||||
}) => {
|
||||
const newMessage: ChatMessage = {
|
||||
id: Date.now().toString(),
|
||||
type: "file",
|
||||
content: `文件: ${fileInfo.name}`,
|
||||
timestamp: new Date(),
|
||||
fileInfo,
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, newMessage]);
|
||||
};
|
||||
|
||||
// 处理文本发送
|
||||
const handleSendText = () => {
|
||||
if (!inputValue.trim()) return;
|
||||
|
||||
const newMessage: ChatMessage = {
|
||||
id: Date.now().toString(),
|
||||
type: "text",
|
||||
content: inputValue,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, newMessage]);
|
||||
setInputValue("");
|
||||
};
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
// 获取文件类型图标
|
||||
const getFileTypeIcon = (type: string, name: string) => {
|
||||
const lowerType = type.toLowerCase();
|
||||
const lowerName = name.toLowerCase();
|
||||
|
||||
if (lowerType.startsWith("image/")) {
|
||||
return "🖼️";
|
||||
} else if (lowerType.startsWith("video/")) {
|
||||
return "🎥";
|
||||
} else if (lowerType.startsWith("audio/")) {
|
||||
return "🎵";
|
||||
} else if (lowerType === "application/pdf") {
|
||||
return "📄";
|
||||
} else if (lowerName.endsWith(".doc") || lowerName.endsWith(".docx")) {
|
||||
return "📝";
|
||||
} else if (lowerName.endsWith(".xls") || lowerName.endsWith(".xlsx")) {
|
||||
return "📊";
|
||||
} else if (lowerName.endsWith(".ppt") || lowerName.endsWith(".pptx")) {
|
||||
return "📈";
|
||||
} else {
|
||||
return "📎";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 600, margin: "0 auto", padding: 20 }}>
|
||||
<Card title="聊天文件上传示例" style={{ marginBottom: 20 }}>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Text>功能特点:</Text>
|
||||
<ul>
|
||||
<li>点击文件按钮直接唤醒文件选择框</li>
|
||||
<li>选择文件后自动上传</li>
|
||||
<li>上传成功后自动发送到聊天框</li>
|
||||
<li>支持各种文件类型和大小限制</li>
|
||||
<li>显示文件图标和大小信息</li>
|
||||
</ul>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* 聊天消息区域 */}
|
||||
<Card
|
||||
title="聊天记录"
|
||||
style={{
|
||||
height: 400,
|
||||
marginBottom: 20,
|
||||
overflowY: "auto",
|
||||
}}
|
||||
bodyStyle={{ height: 320, overflowY: "auto" }}
|
||||
>
|
||||
{messages.length === 0 ? (
|
||||
<div style={{ textAlign: "center", color: "#999", marginTop: 100 }}>
|
||||
暂无消息,开始聊天吧!
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{messages.map(message => (
|
||||
<div key={message.id} style={{ marginBottom: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
background: "#f0f0f0",
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
maxWidth: "80%",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{message.type === "text" ? (
|
||||
<div>{message.content}</div>
|
||||
) : (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{getFileTypeIcon(
|
||||
message.fileInfo!.type,
|
||||
message.fileInfo!.name,
|
||||
)}
|
||||
</span>
|
||||
<Text strong>{message.fileInfo!.name}</Text>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#666" }}>
|
||||
大小: {formatFileSize(message.fileInfo!.size)}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#666" }}>
|
||||
类型: {message.fileInfo!.type}
|
||||
</div>
|
||||
<a
|
||||
href={message.fileInfo!.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ fontSize: 12, color: "#1890ff" }}
|
||||
>
|
||||
查看文件
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: "#999",
|
||||
marginTop: 4,
|
||||
textAlign: "right",
|
||||
}}
|
||||
>
|
||||
{message.timestamp.toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 输入区域 */}
|
||||
<Card title="发送消息">
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<TextArea
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
placeholder="输入消息内容..."
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
onPressEnter={e => {
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendText();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
{/* 文件上传组件 */}
|
||||
<ChatFileUpload
|
||||
onFileUploaded={handleFileUploaded}
|
||||
maxSize={50} // 最大50MB
|
||||
accept="*/*" // 接受所有文件类型
|
||||
buttonText="文件"
|
||||
buttonIcon={<span>📎</span>}
|
||||
/>
|
||||
|
||||
{/* 图片上传组件 */}
|
||||
<ChatFileUpload
|
||||
onFileUploaded={handleFileUploaded}
|
||||
maxSize={10} // 最大10MB
|
||||
accept="image/*" // 只接受图片
|
||||
buttonText="图片"
|
||||
buttonIcon={<span>🖼️</span>}
|
||||
/>
|
||||
|
||||
{/* 文档上传组件 */}
|
||||
<ChatFileUpload
|
||||
onFileUploaded={handleFileUploaded}
|
||||
maxSize={20} // 最大20MB
|
||||
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx" // 只接受文档
|
||||
buttonText="文档"
|
||||
buttonIcon={<span>📄</span>}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
onClick={handleSendText}
|
||||
disabled={!inputValue.trim()}
|
||||
>
|
||||
发送
|
||||
</Button>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatFileUploadExample;
|
||||
@@ -0,0 +1,48 @@
|
||||
.chatFileUpload {
|
||||
display: inline-block;
|
||||
|
||||
.uploadButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
background-color: rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.chatFileUpload {
|
||||
.uploadButton {
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
|
||||
.anticon {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
189
nkebao/src/components/Upload/ChatFileUpload/index.tsx
Normal file
189
nkebao/src/components/Upload/ChatFileUpload/index.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Button, message } from "antd";
|
||||
import {
|
||||
PaperClipOutlined,
|
||||
LoadingOutlined,
|
||||
FileOutlined,
|
||||
FileImageOutlined,
|
||||
FileVideoOutlined,
|
||||
FileAudioOutlined,
|
||||
FilePdfOutlined,
|
||||
FileWordOutlined,
|
||||
FileExcelOutlined,
|
||||
FilePptOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { uploadFile } from "@/api/common";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
interface ChatFileUploadProps {
|
||||
onFileUploaded?: (fileInfo: {
|
||||
url: string;
|
||||
name: string;
|
||||
type: string;
|
||||
size: number;
|
||||
}) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
maxSize?: number; // 最大文件大小(MB)
|
||||
accept?: string; // 接受的文件类型
|
||||
buttonText?: string;
|
||||
buttonIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ChatFileUpload: React.FC<ChatFileUploadProps> = ({
|
||||
onFileUploaded,
|
||||
disabled = false,
|
||||
className,
|
||||
maxSize = 50, // 默认50MB
|
||||
accept = "*/*", // 默认接受所有文件类型
|
||||
buttonText = "发送文件",
|
||||
buttonIcon = <PaperClipOutlined />,
|
||||
}) => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
// 获取文件图标
|
||||
const getFileIcon = (file: File) => {
|
||||
const type = file.type.toLowerCase();
|
||||
const name = file.name.toLowerCase();
|
||||
|
||||
if (type.startsWith("image/")) {
|
||||
return <FileImageOutlined />;
|
||||
} else if (type.startsWith("video/")) {
|
||||
return <FileVideoOutlined />;
|
||||
} else if (type.startsWith("audio/")) {
|
||||
return <FileAudioOutlined />;
|
||||
} else if (type === "application/pdf") {
|
||||
return <FilePdfOutlined />;
|
||||
} else if (name.endsWith(".doc") || name.endsWith(".docx")) {
|
||||
return <FileWordOutlined />;
|
||||
} else if (name.endsWith(".xls") || name.endsWith(".xlsx")) {
|
||||
return <FileExcelOutlined />;
|
||||
} else if (name.endsWith(".ppt") || name.endsWith(".pptx")) {
|
||||
return <FilePptOutlined />;
|
||||
} else {
|
||||
return <FileOutlined />;
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
// 验证文件
|
||||
const validateFile = (file: File): boolean => {
|
||||
// 检查文件大小
|
||||
if (file.size > maxSize * 1024 * 1024) {
|
||||
message.error(`文件大小不能超过 ${maxSize}MB`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件类型(如果指定了accept)
|
||||
if (accept !== "*/*") {
|
||||
const acceptTypes = accept.split(",").map(type => type.trim());
|
||||
const fileType = file.type;
|
||||
const fileName = file.name.toLowerCase();
|
||||
|
||||
const isValidType = acceptTypes.some(type => {
|
||||
if (type.startsWith(".")) {
|
||||
// 扩展名匹配
|
||||
return fileName.endsWith(type);
|
||||
} else if (type.includes("*")) {
|
||||
// MIME类型通配符匹配
|
||||
const baseType = type.replace("*", "");
|
||||
return fileType.startsWith(baseType);
|
||||
} else {
|
||||
// 精确MIME类型匹配
|
||||
return fileType === type;
|
||||
}
|
||||
});
|
||||
|
||||
if (!isValidType) {
|
||||
message.error(`不支持的文件类型: ${file.type}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileSelect = async (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const files = event.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
|
||||
// 验证文件
|
||||
if (!validateFile(file)) {
|
||||
// 清空input值,允许重新选择同一文件
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
|
||||
try {
|
||||
// 上传文件
|
||||
const fileUrl = await uploadFile(file);
|
||||
|
||||
// 调用回调函数,传递文件信息
|
||||
onFileUploaded?.({
|
||||
url: fileUrl,
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
});
|
||||
|
||||
message.success("文件上传成功");
|
||||
} catch (error: any) {
|
||||
message.error(error.message || "文件上传失败");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
// 清空input值,允许重新选择同一文件
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 触发文件选择
|
||||
const handleClick = () => {
|
||||
if (disabled || uploading) return;
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${style.chatFileUpload} ${className || ""}`}>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept={accept}
|
||||
onChange={handleFileSelect}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="text"
|
||||
icon={uploading ? <LoadingOutlined /> : buttonIcon}
|
||||
onClick={handleClick}
|
||||
disabled={disabled || uploading}
|
||||
className={style.uploadButton}
|
||||
title={buttonText}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatFileUpload;
|
||||
65
nkebao/src/components/Upload/ChatFileUpload/test.tsx
Normal file
65
nkebao/src/components/Upload/ChatFileUpload/test.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
import { Card, Space, Typography } from "antd";
|
||||
import ChatFileUpload from "./index";
|
||||
import ChatFileUploadExample from "./example";
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
const ChatFileUploadTest: React.FC = () => {
|
||||
const handleFileUploaded = (fileInfo: {
|
||||
url: string;
|
||||
name: string;
|
||||
type: string;
|
||||
size: number;
|
||||
}) => {
|
||||
console.log("文件上传成功:", fileInfo);
|
||||
alert(
|
||||
`文件上传成功!\n文件名: ${fileInfo.name}\n大小: ${(fileInfo.size / 1024 / 1024).toFixed(2)}MB\n类型: ${fileInfo.type}\nURL: ${fileInfo.url}`,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<Title level={2}>ChatFileUpload 组件测试</Title>
|
||||
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
{/* 基础用法 */}
|
||||
<Card title="基础用法" size="small">
|
||||
<Paragraph>
|
||||
点击下面的按钮选择文件,选择后会自动上传并显示文件信息。
|
||||
</Paragraph>
|
||||
<Space>
|
||||
<ChatFileUpload
|
||||
onFileUploaded={handleFileUploaded}
|
||||
buttonText="选择文件"
|
||||
/>
|
||||
<ChatFileUpload
|
||||
onFileUploaded={handleFileUploaded}
|
||||
maxSize={10}
|
||||
accept="image/*"
|
||||
buttonText="选择图片"
|
||||
buttonIcon={<span>🖼️</span>}
|
||||
/>
|
||||
<ChatFileUpload
|
||||
onFileUploaded={handleFileUploaded}
|
||||
maxSize={20}
|
||||
accept=".pdf,.doc,.docx"
|
||||
buttonText="选择文档"
|
||||
buttonIcon={<span>📄</span>}
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* 完整聊天示例 */}
|
||||
<Card title="完整聊天示例" size="small">
|
||||
<Paragraph>
|
||||
下面的示例展示了如何在聊天界面中使用ChatFileUpload组件。
|
||||
</Paragraph>
|
||||
<ChatFileUploadExample />
|
||||
</Card>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatFileUploadTest;
|
||||
@@ -9,6 +9,7 @@
|
||||
### 1. MainImgUpload 主图封面上传组件
|
||||
|
||||
#### 功能特点
|
||||
|
||||
- 只支持上传一张图片作为主图封面
|
||||
- 上传后右上角显示删除按钮
|
||||
- 支持图片预览功能
|
||||
@@ -19,10 +20,10 @@
|
||||
#### 使用方法
|
||||
|
||||
```tsx
|
||||
import MainImgUpload from '@/components/Upload/MainImgUpload';
|
||||
import MainImgUpload from "@/components/Upload/MainImgUpload";
|
||||
|
||||
const MyComponent = () => {
|
||||
const [mainImage, setMainImage] = useState<string>('');
|
||||
const [mainImage, setMainImage] = useState<string>("");
|
||||
|
||||
return (
|
||||
<MainImgUpload
|
||||
@@ -37,27 +38,32 @@ const MyComponent = () => {
|
||||
```
|
||||
|
||||
#### 编辑模式数据回显
|
||||
|
||||
```tsx
|
||||
// 编辑模式下,传入已有的图片URL
|
||||
const [mainImage, setMainImage] = useState<string>('https://example.com/image.jpg');
|
||||
const [mainImage, setMainImage] = useState<string>(
|
||||
"https://example.com/image.jpg",
|
||||
);
|
||||
|
||||
<MainImgUpload
|
||||
value={mainImage} // 会自动显示已上传的图片
|
||||
onChange={setMainImage}
|
||||
/>
|
||||
/>;
|
||||
```
|
||||
|
||||
### 2. ImageUpload 多图上传组件
|
||||
|
||||
#### 功能特点
|
||||
|
||||
- 支持多张图片上传
|
||||
- 可设置最大上传数量
|
||||
- 支持图片预览和删除
|
||||
- **支持数据回显**:编辑时自动显示已上传的图片数组
|
||||
|
||||
#### 使用方法
|
||||
|
||||
```tsx
|
||||
import ImageUpload from '@/components/Upload/ImageUpload/ImageUpload';
|
||||
import ImageUpload from "@/components/Upload/ImageUpload/ImageUpload";
|
||||
|
||||
const MyComponent = () => {
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
@@ -74,22 +80,24 @@ const MyComponent = () => {
|
||||
```
|
||||
|
||||
#### 编辑模式数据回显
|
||||
|
||||
```tsx
|
||||
// 编辑模式下,传入已有的图片URL数组
|
||||
const [images, setImages] = useState<string[]>([
|
||||
'https://example.com/image1.jpg',
|
||||
'https://example.com/image2.jpg'
|
||||
"https://example.com/image1.jpg",
|
||||
"https://example.com/image2.jpg",
|
||||
]);
|
||||
|
||||
<ImageUpload
|
||||
value={images} // 会自动显示已上传的图片
|
||||
onChange={setImages}
|
||||
/>
|
||||
/>;
|
||||
```
|
||||
|
||||
### 3. VideoUpload 视频上传组件
|
||||
|
||||
#### 功能特点
|
||||
|
||||
- 支持视频文件上传
|
||||
- 支持单个或多个视频
|
||||
- 视频预览功能
|
||||
@@ -97,11 +105,12 @@ const [images, setImages] = useState<string[]>([
|
||||
- **支持数据回显**:编辑时自动显示已上传的视频
|
||||
|
||||
#### 使用方法
|
||||
|
||||
```tsx
|
||||
import VideoUpload from '@/components/Upload/VideoUpload';
|
||||
import VideoUpload from "@/components/Upload/VideoUpload";
|
||||
|
||||
const MyComponent = () => {
|
||||
const [videoUrl, setVideoUrl] = useState<string>('');
|
||||
const [videoUrl, setVideoUrl] = useState<string>("");
|
||||
|
||||
return (
|
||||
<VideoUpload
|
||||
@@ -115,67 +124,77 @@ const MyComponent = () => {
|
||||
```
|
||||
|
||||
#### 编辑模式数据回显
|
||||
|
||||
```tsx
|
||||
// 编辑模式下,传入已有的视频URL
|
||||
const [videoUrl, setVideoUrl] = useState<string>('https://example.com/video.mp4');
|
||||
const [videoUrl, setVideoUrl] = useState<string>(
|
||||
"https://example.com/video.mp4",
|
||||
);
|
||||
|
||||
<VideoUpload
|
||||
value={videoUrl} // 会自动显示已上传的视频
|
||||
onChange={setVideoUrl}
|
||||
/>
|
||||
/>;
|
||||
```
|
||||
|
||||
### 4. FileUpload 文件上传组件
|
||||
|
||||
#### 功能特点
|
||||
|
||||
- 支持Excel、Word、PPT等文档文件
|
||||
- 可配置接受的文件类型
|
||||
- 文件预览和下载
|
||||
- **支持数据回显**:编辑时自动显示已上传的文件
|
||||
|
||||
#### 使用方法
|
||||
|
||||
```tsx
|
||||
import FileUpload from '@/components/Upload/FileUpload';
|
||||
import FileUpload from "@/components/Upload/FileUpload";
|
||||
|
||||
const MyComponent = () => {
|
||||
const [fileUrl, setFileUrl] = useState<string>('');
|
||||
const [fileUrl, setFileUrl] = useState<string>("");
|
||||
|
||||
return (
|
||||
<FileUpload
|
||||
value={fileUrl}
|
||||
onChange={setFileUrl}
|
||||
maxSize={10} // 最大10MB
|
||||
acceptTypes={['excel', 'word', 'ppt']}
|
||||
acceptTypes={["excel", "word", "ppt"]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### 编辑模式数据回显
|
||||
|
||||
```tsx
|
||||
// 编辑模式下,传入已有的文件URL
|
||||
const [fileUrl, setFileUrl] = useState<string>('https://example.com/document.xlsx');
|
||||
const [fileUrl, setFileUrl] = useState<string>(
|
||||
"https://example.com/document.xlsx",
|
||||
);
|
||||
|
||||
<FileUpload
|
||||
value={fileUrl} // 会自动显示已上传的文件
|
||||
onChange={setFileUrl}
|
||||
/>
|
||||
/>;
|
||||
```
|
||||
|
||||
### 5. AvatarUpload 头像上传组件
|
||||
|
||||
#### 功能特点
|
||||
|
||||
- 专门的头像上传组件
|
||||
- 圆形头像显示
|
||||
- 支持删除和重新上传
|
||||
- **支持数据回显**:编辑时自动显示已上传的头像
|
||||
|
||||
#### 使用方法
|
||||
|
||||
```tsx
|
||||
import AvatarUpload from '@/components/Upload/AvatarUpload';
|
||||
import AvatarUpload from "@/components/Upload/AvatarUpload";
|
||||
|
||||
const MyComponent = () => {
|
||||
const [avatarUrl, setAvatarUrl] = useState<string>('');
|
||||
const [avatarUrl, setAvatarUrl] = useState<string>("");
|
||||
|
||||
return (
|
||||
<AvatarUpload
|
||||
@@ -188,19 +207,176 @@ const MyComponent = () => {
|
||||
```
|
||||
|
||||
#### 编辑模式数据回显
|
||||
|
||||
```tsx
|
||||
// 编辑模式下,传入已有的头像URL
|
||||
const [avatarUrl, setAvatarUrl] = useState<string>('https://example.com/avatar.jpg');
|
||||
const [avatarUrl, setAvatarUrl] = useState<string>(
|
||||
"https://example.com/avatar.jpg",
|
||||
);
|
||||
|
||||
<AvatarUpload
|
||||
value={avatarUrl} // 会自动显示已上传的头像
|
||||
onChange={setAvatarUrl}
|
||||
/>;
|
||||
```
|
||||
|
||||
### 6. ChatFileUpload 聊天文件上传组件
|
||||
|
||||
#### 功能特点
|
||||
|
||||
- 专门为聊天场景设计的文件上传组件
|
||||
- 点击按钮直接唤醒文件选择框
|
||||
- 选择文件后自动上传
|
||||
- 上传成功后自动发送到聊天框
|
||||
- 支持各种文件类型和大小限制
|
||||
- 显示文件图标和大小信息
|
||||
- 支持自定义按钮文本和图标
|
||||
|
||||
#### 使用方法
|
||||
|
||||
```tsx
|
||||
import ChatFileUpload from "@/components/Upload/ChatFileUpload";
|
||||
|
||||
const ChatComponent = () => {
|
||||
const handleFileUploaded = (fileInfo: {
|
||||
url: string;
|
||||
name: string;
|
||||
type: string;
|
||||
size: number;
|
||||
}) => {
|
||||
// 处理上传成功的文件
|
||||
console.log("文件上传成功:", fileInfo);
|
||||
// 发送到聊天框
|
||||
sendMessage({
|
||||
type: "file",
|
||||
content: fileInfo,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ChatFileUpload
|
||||
onFileUploaded={handleFileUploaded}
|
||||
maxSize={50} // 最大50MB
|
||||
accept="*/*" // 接受所有文件类型
|
||||
buttonText="发送文件"
|
||||
buttonIcon={<span>📎</span>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### 不同文件类型的配置示例
|
||||
|
||||
```tsx
|
||||
// 图片上传
|
||||
<ChatFileUpload
|
||||
onFileUploaded={handleFileUploaded}
|
||||
maxSize={10}
|
||||
accept="image/*"
|
||||
buttonText="图片"
|
||||
buttonIcon={<span>🖼️</span>}
|
||||
/>
|
||||
|
||||
// 文档上传
|
||||
<ChatFileUpload
|
||||
onFileUploaded={handleFileUploaded}
|
||||
maxSize={20}
|
||||
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"
|
||||
buttonText="文档"
|
||||
buttonIcon={<span>📄</span>}
|
||||
/>
|
||||
|
||||
// 视频上传
|
||||
<ChatFileUpload
|
||||
onFileUploaded={handleFileUploaded}
|
||||
maxSize={100}
|
||||
accept="video/*"
|
||||
buttonText="视频"
|
||||
buttonIcon={<span>🎥</span>}
|
||||
/>
|
||||
```
|
||||
|
||||
#### 在聊天界面中的完整使用示例
|
||||
|
||||
```tsx
|
||||
import React, { useState } from "react";
|
||||
import { Input, Button } from "antd";
|
||||
import ChatFileUpload from "@/components/Upload/ChatFileUpload";
|
||||
|
||||
const ChatInterface = () => {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
const handleFileUploaded = fileInfo => {
|
||||
const newMessage = {
|
||||
id: Date.now(),
|
||||
type: "file",
|
||||
content: fileInfo,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages(prev => [...prev, newMessage]);
|
||||
};
|
||||
|
||||
const handleSendText = () => {
|
||||
if (!inputValue.trim()) return;
|
||||
|
||||
const newMessage = {
|
||||
id: Date.now(),
|
||||
type: "text",
|
||||
content: inputValue,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages(prev => [...prev, newMessage]);
|
||||
setInputValue("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 聊天消息区域 */}
|
||||
<div className="chat-messages">
|
||||
{messages.map(msg => (
|
||||
<div key={msg.id} className="message">
|
||||
{msg.type === "file" ? (
|
||||
<div>
|
||||
<div>📎 {msg.content.name}</div>
|
||||
<div>大小: {formatFileSize(msg.content.size)}</div>
|
||||
<a href={msg.content.url} target="_blank">
|
||||
查看文件
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div>{msg.content}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 输入区域 */}
|
||||
<div className="chat-input">
|
||||
<Input.TextArea
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
placeholder="输入消息..."
|
||||
/>
|
||||
<div className="input-actions">
|
||||
<ChatFileUpload
|
||||
onFileUploaded={handleFileUploaded}
|
||||
maxSize={50}
|
||||
accept="*/*"
|
||||
buttonText="文件"
|
||||
/>
|
||||
<Button onClick={handleSendText}>发送</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 数据回显机制
|
||||
|
||||
### 工作原理
|
||||
|
||||
所有Upload组件都通过以下机制实现数据回显:
|
||||
|
||||
1. **useEffect监听value变化**:当传入的value发生变化时,自动更新内部状态
|
||||
@@ -209,11 +385,13 @@ const [avatarUrl, setAvatarUrl] = useState<string>('https://example.com/avatar.j
|
||||
4. **UI更新**:根据文件列表自动更新界面显示
|
||||
|
||||
### 使用场景
|
||||
|
||||
- **新增模式**:value为空或未定义,显示上传按钮
|
||||
- **编辑模式**:value包含已上传文件的URL,自动显示文件
|
||||
- **混合模式**:支持部分文件已上传,部分文件待上传
|
||||
|
||||
### 注意事项
|
||||
|
||||
1. **URL格式**:确保传入的URL是有效的文件访问地址
|
||||
2. **权限验证**:确保文件URL在编辑时仍然可访问
|
||||
3. **状态同步**:value和onChange需要正确配合使用
|
||||
@@ -222,6 +400,7 @@ const [avatarUrl, setAvatarUrl] = useState<string>('https://example.com/avatar.j
|
||||
## 技术实现
|
||||
|
||||
### 核心特性
|
||||
|
||||
- 基于 antd Upload 组件
|
||||
- 使用 antd-mobile 的 Toast 提示
|
||||
- 支持 FormData 上传
|
||||
@@ -230,6 +409,7 @@ const [avatarUrl, setAvatarUrl] = useState<string>('https://example.com/avatar.j
|
||||
- **完整的数据回显支持**
|
||||
|
||||
### 文件结构
|
||||
|
||||
```
|
||||
src/components/Upload/
|
||||
├── MainImgUpload/ # 主图上传组件
|
||||
@@ -237,11 +417,17 @@ src/components/Upload/
|
||||
├── VideoUpload/ # 视频上传组件
|
||||
├── FileUpload/ # 文件上传组件
|
||||
├── AvatarUpload/ # 头像上传组件
|
||||
├── ChatFileUpload/ # 聊天文件上传组件
|
||||
│ ├── index.tsx # 主组件文件
|
||||
│ ├── index.module.scss # 样式文件
|
||||
│ └── example.tsx # 使用示例
|
||||
└── README.md # 使用说明文档
|
||||
```
|
||||
|
||||
### 统一的数据回显模式
|
||||
|
||||
所有组件都遵循相同的数据回显模式:
|
||||
|
||||
```tsx
|
||||
// 1. 接收value属性
|
||||
interface Props {
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
.chatFooter {
|
||||
background: #fff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 16px;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
min-height: auto;
|
||||
flex-shrink: 0;
|
||||
@@ -94,43 +94,114 @@
|
||||
.inputContainer {
|
||||
.inputToolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: #fafafa;
|
||||
justify-content: space-between;
|
||||
|
||||
.leftTool {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.rightTool {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.toolbarButton {
|
||||
color: #8c8c8c;
|
||||
color: #666;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 18px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
background: #f5f5f5;
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #bae7ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inputArea {
|
||||
display: flex;
|
||||
padding: 12px 16px;
|
||||
gap: 8px;
|
||||
align-items: flex-end;
|
||||
background: #fff;
|
||||
|
||||
.messageInput {
|
||||
flex: 1;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
resize: none;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
min-height: 36px;
|
||||
max-height: 120px;
|
||||
background: #fff;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:focus {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
|
||||
.sendButton {
|
||||
border-radius: 8px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
background: #1890ff;
|
||||
border: 1px solid #1890ff;
|
||||
color: #fff;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #40a9ff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #096dd9;
|
||||
border-color: #096dd9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #f5f5f5;
|
||||
border-color: #d9d9d9;
|
||||
color: #bfbfbf;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inputHint {
|
||||
padding: 4px 16px 8px;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,9 +309,7 @@
|
||||
|
||||
.contactInfo {
|
||||
.contactItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
@@ -255,9 +324,8 @@
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
.contactItemText {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,13 @@ import {
|
||||
Tag,
|
||||
Row,
|
||||
Col,
|
||||
Modal,
|
||||
} from "antd";
|
||||
import {
|
||||
ShareAltOutlined,
|
||||
SendOutlined,
|
||||
SmileOutlined,
|
||||
PaperClipOutlined,
|
||||
FolderOutlined,
|
||||
PhoneOutlined,
|
||||
VideoCameraOutlined,
|
||||
MoreOutlined,
|
||||
@@ -29,11 +31,16 @@ import {
|
||||
EnvironmentOutlined,
|
||||
CalendarOutlined,
|
||||
BankOutlined,
|
||||
IdcardOutlined,
|
||||
CloseOutlined,
|
||||
StarOutlined,
|
||||
EnvironmentOutlined as LocationOutlined,
|
||||
AudioOutlined,
|
||||
AudioOutlined as AudioHoldOutlined,
|
||||
CodeSandboxOutlined,
|
||||
MessageOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import dayjs from "dayjs";
|
||||
import { ChatSession, MessageData, MessageType } from "../data";
|
||||
import { ChatSession, MessageData, MessageType } from "../../data";
|
||||
// import { getChatHistory, sendMessage } from "../api";
|
||||
import styles from "./ChatWindow.module.scss";
|
||||
|
||||
@@ -56,6 +63,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
const [messages, setMessages] = useState<MessageData[]>([]);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showMaterialModal, setShowMaterialModal] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -140,6 +148,46 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 素材菜单项
|
||||
const materialMenuItems = [
|
||||
{
|
||||
key: "text",
|
||||
label: "文字素材",
|
||||
icon: <span>📝</span>,
|
||||
},
|
||||
{
|
||||
key: "audio",
|
||||
label: "语音素材",
|
||||
icon: <span>🎵</span>,
|
||||
},
|
||||
{
|
||||
key: "image",
|
||||
label: "图片素材",
|
||||
icon: <span>🖼️</span>,
|
||||
},
|
||||
{
|
||||
key: "video",
|
||||
label: "视频素材",
|
||||
icon: <span>🎬</span>,
|
||||
},
|
||||
{
|
||||
key: "link",
|
||||
label: "链接素材",
|
||||
icon: <span>🔗</span>,
|
||||
},
|
||||
{
|
||||
key: "card",
|
||||
label: "名片素材",
|
||||
icon: <span>📇</span>,
|
||||
},
|
||||
];
|
||||
|
||||
const handleMaterialSelect = (key: string) => {
|
||||
console.log("选择素材类型:", key);
|
||||
setShowMaterialModal(true);
|
||||
// 这里可以根据不同的素材类型显示不同的模态框
|
||||
};
|
||||
|
||||
const renderMessage = (msg: MessageData) => {
|
||||
const isOwn = msg.senderId === "me";
|
||||
return (
|
||||
@@ -278,20 +326,94 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
<Footer className={styles.chatFooter}>
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.inputToolbar}>
|
||||
<Tooltip title="表情">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<SmileOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="附件">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PaperClipOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<div className={styles.leftTool}>
|
||||
<Tooltip title="表情">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<SmileOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="上传附件">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<FolderOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="收藏">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<StarOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="位置">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<LocationOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="语音">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<AudioOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="按住说话">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<AudioHoldOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "2px",
|
||||
right: "2px",
|
||||
fontSize: "8px",
|
||||
color: "#52c41a",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
H
|
||||
</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu
|
||||
items={materialMenuItems}
|
||||
onClick={({ key }) => handleMaterialSelect(key)}
|
||||
style={{
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
trigger={["click"]}
|
||||
placement="topLeft"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CodeSandboxOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className={styles.rightTool}>
|
||||
<div className={styles.rightToolItem}>
|
||||
<ShareAltOutlined />
|
||||
转给他人
|
||||
</div>
|
||||
<div className={styles.rightToolItem}>
|
||||
<MessageOutlined />
|
||||
聊天记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.inputArea}>
|
||||
<TextArea
|
||||
@@ -312,6 +434,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
发送
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.inputHint}>按下Ctrl+Enter换行</div>
|
||||
</div>
|
||||
</Footer>
|
||||
</Layout>
|
||||
@@ -355,23 +478,33 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
<div className={styles.contactInfo}>
|
||||
<div className={styles.contactItem}>
|
||||
<PhoneOutlined />
|
||||
<span>{contactInfo.phone}</span>
|
||||
<span className={styles.contactItemText}>
|
||||
{contactInfo.phone}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.contactItem}>
|
||||
<MailOutlined />
|
||||
<span>{contactInfo.email}</span>
|
||||
<span className={styles.contactItemText}>
|
||||
{contactInfo.email}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.contactItem}>
|
||||
<EnvironmentOutlined />
|
||||
<span>{contactInfo.location}</span>
|
||||
<span className={styles.contactItemText}>
|
||||
{contactInfo.location}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.contactItem}>
|
||||
<BankOutlined />
|
||||
<span>{contactInfo.company}</span>
|
||||
<span className={styles.contactItemText}>
|
||||
{contactInfo.company}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.contactItem}>
|
||||
<CalendarOutlined />
|
||||
<span>入职时间:{contactInfo.joinDate}</span>
|
||||
<span className={styles.contactItemText}>
|
||||
入职时间:{contactInfo.joinDate}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -409,6 +542,88 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
</div>
|
||||
</Sider>
|
||||
)}
|
||||
|
||||
{/* 素材选择模态框 */}
|
||||
<Modal
|
||||
title="选择素材"
|
||||
open={showMaterialModal}
|
||||
onCancel={() => setShowMaterialModal(false)}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setShowMaterialModal(false)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="confirm"
|
||||
type="primary"
|
||||
onClick={() => setShowMaterialModal(false)}
|
||||
>
|
||||
确定
|
||||
</Button>,
|
||||
]}
|
||||
width={800}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<div style={{ display: "flex", height: "400px" }}>
|
||||
{/* 左侧素材分类 */}
|
||||
<div
|
||||
style={{
|
||||
width: "200px",
|
||||
background: "#f5f5f5",
|
||||
borderRight: "1px solid #e8e8e8",
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: "16px", borderBottom: "1px solid #e8e8e8" }}>
|
||||
<h4 style={{ margin: 0, color: "#262626" }}>公共素材</h4>
|
||||
</div>
|
||||
<div style={{ padding: "8px 0" }}>
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
background: "#e6f7ff",
|
||||
borderLeft: "3px solid #1890ff",
|
||||
color: "#1890ff",
|
||||
}}
|
||||
>
|
||||
暗黑4
|
||||
</div>
|
||||
<div style={{ padding: "8px 16px", cursor: "pointer" }}>
|
||||
针对老客户的...
|
||||
</div>
|
||||
<div style={{ padding: "8px 16px", cursor: "pointer" }}>
|
||||
D2辅助
|
||||
</div>
|
||||
<div style={{ padding: "8px 16px", cursor: "pointer" }}>
|
||||
ROS反馈演示...
|
||||
</div>
|
||||
<div style={{ padding: "8px 16px", cursor: "pointer" }}>
|
||||
一键宏产品素...
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: "16px", borderTop: "1px solid #e8e8e8" }}>
|
||||
<h4 style={{ margin: 0, color: "#262626" }}>部门素材</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧内容区域 */}
|
||||
<div style={{ flex: 1, padding: "16px" }}>
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<Input.Search placeholder="昵称" style={{ width: "100%" }} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "300px",
|
||||
color: "#8c8c8c",
|
||||
}}
|
||||
>
|
||||
请选择左侧素材分类
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { List, Avatar, Badge } from "antd";
|
||||
import { UserOutlined } from "@ant-design/icons";
|
||||
import { ContactData } from "../data";
|
||||
import { ContactData } from "../../data";
|
||||
import styles from "./ContactList.module.scss";
|
||||
|
||||
interface ContactListProps {
|
||||
@@ -32,9 +32,9 @@ import {
|
||||
} from "@ant-design/icons";
|
||||
import dayjs from "dayjs";
|
||||
import { ContactData, MessageData, ChatSession } from "./data";
|
||||
import ChatWindow from "./components/ChatWindow";
|
||||
import ContactList from "./components/ContactList";
|
||||
import MessageList from "./components/MessageList";
|
||||
import ChatWindow from "./components/ChatWindow/index";
|
||||
import ContactList from "./components/ContactList/index";
|
||||
import MessageList from "./components/MessageList/index";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
|
||||
Reference in New Issue
Block a user