|
|
@@ -0,0 +1,480 @@
|
|
|
+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);
|
|
|
+ // 新增:下拉选中值状态
|
|
|
+ const [selectedBase, setSelectedBase] = useState("羽田ベース");
|
|
|
+ // 新增:下拉假数据选项
|
|
|
+ const baseOptions = ["羽田ベース", "成田ベース", "大阪ベース", "福岡ベース"];
|
|
|
+
|
|
|
+ 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",
|
|
|
+ position: "relative",
|
|
|
+ } as React.CSSProperties;
|
|
|
+
|
|
|
+ const contentStyle = {
|
|
|
+ marginBottom: "0",
|
|
|
+ lineHeight: "1.5",
|
|
|
+ fontSize: "14px",
|
|
|
+ textAlign: "left",
|
|
|
+ paddingTop: "5px",
|
|
|
+ } 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;
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
|
+ }, [messageList]);
|
|
|
+
|
|
|
+ // 新增:下拉选项改变时的处理函数
|
|
|
+ const handleBaseChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
|
+ setSelectedBase(e.target.value);
|
|
|
+ // 这里可扩展选中后要执行的逻辑,比如调用接口等,当前仅打印示例
|
|
|
+ console.log("当前选中的ベース:", e.target.value);
|
|
|
+ };
|
|
|
+
|
|
|
+ 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>
|
|
|
+
|
|
|
+ {/* 新增的下拉列表部分 */}
|
|
|
+ <div style={{ marginBottom: "10px", marginLeft: "15px" }}>
|
|
|
+ <label htmlFor="baseSelect" style={{ marginRight: "5px" }}>ベース選択:</label>
|
|
|
+ <select
|
|
|
+ id="baseSelect"
|
|
|
+ style={{ padding: "6px 12px", border: "1px solid #ccc" }}
|
|
|
+ value={selectedBase}
|
|
|
+ onChange={handleBaseChange}
|
|
|
+ >
|
|
|
+ {baseOptions.map((option) => (
|
|
|
+ <option key={option} value={option}>{option}</option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {messageList.map((msg) => (
|
|
|
+ <div
|
|
|
+ key={msg.id}
|
|
|
+ style={{
|
|
|
+ ...baseMessageStyle,
|
|
|
+ justifyContent: msg.sender === "other"? "flex-end" : "flex-start",
|
|
|
+ display: "flex",
|
|
|
+ flexDirection: "column",
|
|
|
+ alignItems: msg.sender === "other"? "flex-end" : "flex-start",
|
|
|
+ paddingLeft: "40px",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {msg.sender === "other" && (
|
|
|
+ <div style={senderInfoStyle}>
|
|
|
+ <span style={senderNameStyle}>AAA株式会社</span>
|
|
|
+ <img
|
|
|
+ src="/partner.png"
|
|
|
+ alt="AAA株式会社图标"
|
|
|
+ style={iconStyle}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {msg.sender === "me" && (
|
|
|
+ <div style={senderInfoStyle}>
|
|
|
+ <img
|
|
|
+ src="/base.png"
|
|
|
+ alt="羽田ベース图标"
|
|
|
+ style={{...iconStyle, marginLeft: "4px", marginRight: "0" }}
|
|
|
+ />
|
|
|
+ <span style={senderNameStyle}>羽田ベース</span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {msg.content.trim()!== "" && (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ ...bubbleStyle,
|
|
|
+ backgroundColor: msg.sender === "other"? "#d9d9d9" : "#deebf7",
|
|
|
+ marginBottom: "4px",
|
|
|
+ marginTop: "15px",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {msg.sender === "other" && (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ content: '""',
|
|
|
+ position: "absolute",
|
|
|
+ right: "12px",
|
|
|
+ top: "-23px",
|
|
|
+ borderTop: "20px solid transparent",
|
|
|
+ borderBottom: "20px solid transparent",
|
|
|
+ borderRight: "15px solid #d9d9d9",
|
|
|
+ transform: "translateY(20%)",
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ {msg.sender === "me" && (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ content: '""',
|
|
|
+ position: "absolute",
|
|
|
+ left: "12px",
|
|
|
+ top: "-23px",
|
|
|
+ borderTop: "20px solid transparent",
|
|
|
+ borderBottom: "20px solid transparent",
|
|
|
+ borderLeft: "15px solid #deebf7",
|
|
|
+ transform: "translateY(20%)",
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ <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>
|
|
|
+ )}
|
|
|
+ {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} />
|
|
|
+ </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%",
|
|
|
+ margin: "0",
|
|
|
+ padding: "16px",
|
|
|
+ display: "flex",
|
|
|
+ flexDirection: "column",
|
|
|
+ gap: "8px",
|
|
|
+ boxSizing: "border-box",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <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",
|
|
|
+ boxSizing: "border-box",
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ {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 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',
|
|
|
+ margin: 0,
|
|
|
+ padding: 0
|
|
|
+ }}>
|
|
|
+ <div className="chat-messages-container" style={{
|
|
|
+ padding: '16px',
|
|
|
+ boxSizing: 'border-box',
|
|
|
+ flex: 1,
|
|
|
+ overflow: 'auto'
|
|
|
+ }}>
|
|
|
+ <ChatComponent messageList={messages} onClose={onClose} />
|
|
|
+ </div>
|
|
|
+ <InputArea onSendMessage={handleSendMessage} />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default Chat;
|