liuxf 5 ヶ月 前
コミット
dc8a9db252
1 ファイル変更461 行追加0 行削除
  1. 461 0
      src/pages/Base/Order/Chat.tsx

+ 461 - 0
src/pages/Base/Order/Chat.tsx

@@ -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;