|
|
@@ -0,0 +1,461 @@
|
|
|
+import React, { useState, useRef, useEffect } from "react";
|
|
|
+
|
|
|
+// 聊天组件 props 类型
|
|
|
+type ChatProps = {
|
|
|
+ onClose: () => void;
|
|
|
+};
|
|
|
+
|
|
|
+// 聊天消息类型定义
|
|
|
+type ChatMessage = {
|
|
|
+ id: string;
|
|
|
+ sender: "other" | "me";
|
|
|
+ content: string;
|
|
|
+ timestamp: string;
|
|
|
+ attachment?: {
|
|
|
+ name: string;
|
|
|
+ url: string;
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+// 聊天组件 props 类型
|
|
|
+type ChatComponentProps = {
|
|
|
+ messageList: ChatMessage[];
|
|
|
+ onClose: () => void;
|
|
|
+};
|
|
|
+
|
|
|
+const ChatComponent: React.FC<ChatComponentProps> = ({ messageList, onClose }) => {
|
|
|
+ const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
|
+
|
|
|
+ // 自动滚动到最新消息
|
|
|
+ useEffect(() => {
|
|
|
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
|
+ }, [messageList]);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div>
|
|
|
+ {/* 返回按钮区域 */}
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ marginBottom: "10px",
|
|
|
+ display: "flex",
|
|
|
+ alignItems: "center",
|
|
|
+ marginLeft: "-10px",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <img
|
|
|
+ src="/return.svg"
|
|
|
+ alt="返回箭头"
|
|
|
+ style={{ width: "25px", height: "25px" }}
|
|
|
+ />
|
|
|
+ <span
|
|
|
+ style={{ fontSize: "14px", cursor: "pointer" }}
|
|
|
+ onClick={onClose}
|
|
|
+ > 戻る </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 消息列表 */}
|
|
|
+ {messageList.map((msg) => (
|
|
|
+ <div
|
|
|
+ key={msg.id}
|
|
|
+ style={{
|
|
|
+ marginBottom: "12px",
|
|
|
+ width: "100%",
|
|
|
+ display: "flex",
|
|
|
+ flexDirection: "column",
|
|
|
+ alignItems: msg.sender === "other" ? "flex-start" : "flex-end",
|
|
|
+ paddingLeft: "40px",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {/* 发送者信息 */}
|
|
|
+ {msg.sender === "other" && (
|
|
|
+ <div style={senderInfoStyle}>
|
|
|
+ <img
|
|
|
+ src="/partner.png"
|
|
|
+ alt="AAA株式会社图标"
|
|
|
+ style={iconStyle}
|
|
|
+ />
|
|
|
+ <span style={senderNameStyle}>AAA株式会社</span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {msg.sender === "me" && (
|
|
|
+ <div style={senderInfoStyle}>
|
|
|
+ <span style={senderNameStyle}>羽田ベース</span>
|
|
|
+ <img
|
|
|
+ src="/base.png"
|
|
|
+ alt="羽田ベース图标"
|
|
|
+ style={{ ...iconStyle, marginLeft: "4px", marginRight: "0" }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 聊天内容气泡 - 使用div包裹以便添加伪元素 */}
|
|
|
+ {msg.content.trim() !== "" && (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ position: "relative",
|
|
|
+ marginBottom: "4px"
|
|
|
+ }}
|
|
|
+ className={msg.sender === "other" ? "other-bubble" : "my-bubble"}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ ...bubbleStyle,
|
|
|
+ backgroundColor: msg.sender === "other" ? "#d9d9d9" : "#deebf7",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div style={contentStyle}>
|
|
|
+ {msg.content.split("\n").map((line, index) => (
|
|
|
+ <React.Fragment key={index}>
|
|
|
+ {line}
|
|
|
+ {index < msg.content.split("\n").length - 1 && <br />}
|
|
|
+ </React.Fragment>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 三角指向通过CSS伪元素实现,见下方style标签 */}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 附件区域 */}
|
|
|
+ {msg.attachment && (
|
|
|
+ <div style={attachmentContainerStyle}>
|
|
|
+ <div style={fileNameWrapStyle}>
|
|
|
+ <span style={attachmentNameStyle}>{msg.attachment.name}</span>
|
|
|
+ </div>
|
|
|
+ <a
|
|
|
+ href={msg.attachment.url}
|
|
|
+ download={msg.attachment.name}
|
|
|
+ style={downloadLinkStyle}
|
|
|
+ >
|
|
|
+ ダウンロード
|
|
|
+ </a>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 时间戳 */}
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ ...timestampStyle,
|
|
|
+ marginLeft: msg.sender === "other" ? "10px" : "0",
|
|
|
+ marginRight: msg.sender === "me" ? "10px" : "0",
|
|
|
+ textAlign: msg.sender === "other" ? "left" : "right",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {msg.timestamp}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+
|
|
|
+ {/* 用于自动滚动的参考点 */}
|
|
|
+ <div ref={messagesEndRef} />
|
|
|
+
|
|
|
+ {/* 气泡三角样式 - 使用CSS伪元素实现 */}
|
|
|
+ <style jsx global>{`
|
|
|
+ /* 对方消息气泡的三角指向(左侧) */
|
|
|
+ .other-bubble::before {
|
|
|
+ content: "";
|
|
|
+ position: absolute;
|
|
|
+ top: 10px;
|
|
|
+ left: -8px;
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+ border-top: 8px solid transparent;
|
|
|
+ border-right: 8px solid #d9d9d9; /* 与对方气泡背景色一致 */
|
|
|
+ border-bottom: 8px solid transparent;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 自己消息气泡的三角指向(右侧) */
|
|
|
+ .my-bubble::before {
|
|
|
+ content: "";
|
|
|
+ position: absolute;
|
|
|
+ top: 10px;
|
|
|
+ right: -8px;
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+ border-top: 8px solid transparent;
|
|
|
+ border-left: 8px solid #deebf7; /* 与自己气泡背景色一致 */
|
|
|
+ border-bottom: 8px solid transparent;
|
|
|
+ }
|
|
|
+ `}</style>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+// 输入区域组件
|
|
|
+const InputArea: React.FC<{ onSendMessage: (content: string, file: File | null) => void }> = ({ onSendMessage }) => {
|
|
|
+ const [inputValue, setInputValue] = useState("");
|
|
|
+ const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
|
+
|
|
|
+ const handleSend = () => {
|
|
|
+ if (inputValue.trim() || selectedFile) {
|
|
|
+ onSendMessage(inputValue.trim(), selectedFile);
|
|
|
+ setInputValue("");
|
|
|
+ setSelectedFile(null);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
+ if (e.target.files && e.target.files.length > 0) {
|
|
|
+ setSelectedFile(e.target.files[0]);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleRemoveFile = () => {
|
|
|
+ setSelectedFile(null);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
|
+ if (e.key === 'Enter' && !e.shiftKey) {
|
|
|
+ e.preventDefault();
|
|
|
+ handleSend();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ width: "100%",
|
|
|
+ maxWidth: "600px",
|
|
|
+ margin: "0 auto",
|
|
|
+ padding: "16px",
|
|
|
+ display: "flex",
|
|
|
+ flexDirection: "column",
|
|
|
+ gap: "8px",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <textarea
|
|
|
+ value={inputValue}
|
|
|
+ onChange={(e) => setInputValue(e.target.value)}
|
|
|
+ onKeyDown={handleKeyDown}
|
|
|
+ placeholder="メッセージを入力する..."
|
|
|
+ style={{
|
|
|
+ width: "100%",
|
|
|
+ height: "180px",
|
|
|
+ padding: "8px",
|
|
|
+ borderRadius: "4px",
|
|
|
+ border: "1px solid #ccc",
|
|
|
+ resize: "none",
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ {selectedFile && (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ display: "flex",
|
|
|
+ alignItems: "center",
|
|
|
+ justifyContent: "space-between",
|
|
|
+ backgroundColor: "#f9f9f9",
|
|
|
+ padding: "8px",
|
|
|
+ borderRadius: "4px",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div style={{ display: "flex", alignItems: "center" }}>
|
|
|
+ <span style={{ marginRight: "8px", color: "#0066cc" }}>
|
|
|
+ {selectedFile.name.split('.').pop()?.toUpperCase() || 'FILE'}
|
|
|
+ </span>
|
|
|
+ <span>{selectedFile.name}</span>
|
|
|
+ </div>
|
|
|
+ <span
|
|
|
+ style={{
|
|
|
+ cursor: "pointer",
|
|
|
+ color: "#000",
|
|
|
+ fontWeight: "bold",
|
|
|
+ fontSize: "16px",
|
|
|
+ }}
|
|
|
+ onClick={handleRemoveFile}
|
|
|
+ >
|
|
|
+ ×
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ display: "flex",
|
|
|
+ alignItems: "center",
|
|
|
+ justifyContent: "space-between",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <label
|
|
|
+ htmlFor="fileInput"
|
|
|
+ style={{
|
|
|
+ color: "#0066cc",
|
|
|
+ textDecoration: "underline",
|
|
|
+ cursor: "pointer",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ ファイルアップロード
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ type="file"
|
|
|
+ id="fileInput"
|
|
|
+ onChange={handleFileChange}
|
|
|
+ style={{
|
|
|
+ display: "none",
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <button
|
|
|
+ style={{
|
|
|
+ padding: "8px 16px",
|
|
|
+ backgroundColor: "#000",
|
|
|
+ color: "#fff",
|
|
|
+ border: "none",
|
|
|
+ borderRadius: "4px",
|
|
|
+ cursor: "pointer",
|
|
|
+ }}
|
|
|
+ onClick={handleSend}
|
|
|
+ >
|
|
|
+ 送信
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+// 样式定义
|
|
|
+const baseMessageStyle = {
|
|
|
+ display: "flex",
|
|
|
+ marginBottom: "12px",
|
|
|
+ width: "100%",
|
|
|
+} as React.CSSProperties;
|
|
|
+
|
|
|
+const senderInfoStyle = {
|
|
|
+ display: "flex",
|
|
|
+ alignItems: "center",
|
|
|
+ marginBottom: "4px",
|
|
|
+} as React.CSSProperties;
|
|
|
+
|
|
|
+const iconStyle = {
|
|
|
+ width: "20px",
|
|
|
+ height: "20px",
|
|
|
+ marginRight: "4px",
|
|
|
+} as React.CSSProperties;
|
|
|
+
|
|
|
+const senderNameStyle = {
|
|
|
+ fontSize: "14px",
|
|
|
+ fontWeight: "bold",
|
|
|
+} as React.CSSProperties;
|
|
|
+
|
|
|
+const bubbleStyle = {
|
|
|
+ padding: "10px 14px",
|
|
|
+ borderRadius: "8px",
|
|
|
+ boxShadow: "0 1px 2px rgba(0,0,0,0.1)",
|
|
|
+ maxWidth: "70%",
|
|
|
+ wordWrap: "break-word",
|
|
|
+} as React.CSSProperties;
|
|
|
+
|
|
|
+const contentStyle = {
|
|
|
+ marginBottom: "0",
|
|
|
+ lineHeight: "1.5",
|
|
|
+ fontSize: "14px",
|
|
|
+ textAlign: "left",
|
|
|
+} as React.CSSProperties;
|
|
|
+
|
|
|
+const attachmentContainerStyle = {
|
|
|
+ display: "flex",
|
|
|
+ alignItems: "center",
|
|
|
+ marginBottom: "4px",
|
|
|
+ gap: "8px",
|
|
|
+} as React.CSSProperties;
|
|
|
+
|
|
|
+const fileNameWrapStyle = {
|
|
|
+ border: "1px solid #ccc",
|
|
|
+ borderRadius: "4px",
|
|
|
+ padding: "6px 8px",
|
|
|
+ display: "inline-block",
|
|
|
+} as React.CSSProperties;
|
|
|
+
|
|
|
+const attachmentNameStyle = {
|
|
|
+ fontSize: "14px",
|
|
|
+ color: "#000",
|
|
|
+ whiteSpace: "nowrap",
|
|
|
+ overflow: "hidden",
|
|
|
+ textOverflow: "ellipsis",
|
|
|
+ maxWidth: "200px",
|
|
|
+} as React.CSSProperties;
|
|
|
+
|
|
|
+const downloadLinkStyle = {
|
|
|
+ fontSize: "14px",
|
|
|
+ color: "#0066cc",
|
|
|
+ textDecoration: "underline",
|
|
|
+ cursor: "pointer",
|
|
|
+ whiteSpace: "nowrap",
|
|
|
+} as React.CSSProperties;
|
|
|
+
|
|
|
+const timestampStyle = {
|
|
|
+ fontSize: "14px",
|
|
|
+ color: "#000",
|
|
|
+} as React.CSSProperties;
|
|
|
+
|
|
|
+// 示例数据
|
|
|
+const initialMessages: ChatMessage[] = [
|
|
|
+ {
|
|
|
+ id: "1",
|
|
|
+ sender: "other",
|
|
|
+ content: "実積を確認しました。\n21日は追加で稼働していませんが、実積に反映されていません。",
|
|
|
+ timestamp: "2025-7-1 12:21",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: "2",
|
|
|
+ sender: "me",
|
|
|
+ content: "ご確認ありがとうございます。\n恐れ入りますが、運行の証跡がかかるものをアップロードいただけますでしょうか。",
|
|
|
+ timestamp: "2025-7-1 12:25",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: "3",
|
|
|
+ sender: "other",
|
|
|
+ content: "1ファイルを送ります。\nこちらでご確認お願いします。",
|
|
|
+ timestamp: "2025-7-1 13:07",
|
|
|
+ attachment: {
|
|
|
+ name: "6月運行証明.xlsx",
|
|
|
+ url: "#",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: "4",
|
|
|
+ sender: "me",
|
|
|
+ content: "ありがとうございます。\n内容確認して、再度ご連絡させていただきます。",
|
|
|
+ timestamp: "2025-7-1 13:31",
|
|
|
+ },
|
|
|
+];
|
|
|
+
|
|
|
+const Chat: React.FC<ChatProps> = ({ onClose }) => {
|
|
|
+ const [messages, setMessages] = useState<ChatMessage[]>(initialMessages);
|
|
|
+
|
|
|
+ const handleSendMessage = (content: string, file: File | null) => {
|
|
|
+ const newMessage: ChatMessage = {
|
|
|
+ id: Date.now().toString(),
|
|
|
+ sender: "me",
|
|
|
+ content,
|
|
|
+ timestamp: new Date()
|
|
|
+ .toLocaleString("ja-JP", {
|
|
|
+ year: "numeric",
|
|
|
+ month: "long",
|
|
|
+ day: "numeric",
|
|
|
+ hour: "2-digit",
|
|
|
+ minute: "2-digit",
|
|
|
+ })
|
|
|
+ .replace(/[年月]/g, "-")
|
|
|
+ .replace("日", ""),
|
|
|
+ };
|
|
|
+
|
|
|
+ if (file) {
|
|
|
+ newMessage.attachment = {
|
|
|
+ name: file.name,
|
|
|
+ url: "#", // 实际需替换为后端返回的下载链接
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ setMessages([...messages, newMessage]);
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
|
+ <div className="chat-messages-container">
|
|
|
+ <ChatComponent messageList={messages} onClose={onClose} />
|
|
|
+ </div>
|
|
|
+ <InputArea onSendMessage={handleSendMessage} />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default Chat;
|