From 19a7cd687e226c18c6c670eda918b164ad307f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Thu, 24 Jul 2025 16:14:35 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20Ai=E5=8A=A9=E6=89=8B=E7=95=8C=E9=9D=A2=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai-assistant/AIAssistant.module.scss | 139 ++++++++++ .../workspace/ai-assistant/AIAssistant.tsx | 262 +++++++++++++++++- 2 files changed, 398 insertions(+), 3 deletions(-) create mode 100644 nkebao/src/pages/workspace/ai-assistant/AIAssistant.module.scss diff --git a/nkebao/src/pages/workspace/ai-assistant/AIAssistant.module.scss b/nkebao/src/pages/workspace/ai-assistant/AIAssistant.module.scss new file mode 100644 index 000000000..d5a7cd33d --- /dev/null +++ b/nkebao/src/pages/workspace/ai-assistant/AIAssistant.module.scss @@ -0,0 +1,139 @@ +.chatContainer { + display: flex; + flex-direction: column; +} + +.messageList { + flex: 1; + overflow-y: auto; + padding: 16px 12px 80px 12px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.userMessage { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.aiMessage { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.bubble { + max-width: 80%; + padding: 10px 14px; + border-radius: 18px; + font-size: 15px; + line-height: 1.6; + word-break: break-word; + background: #fff; + color: #222; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); +} + +.userMessage .bubble { + background: linear-gradient(135deg, #a7e0ff 0%, #5bbcff 100%); + color: #222; + border-bottom-right-radius: 6px; +} + +.aiMessage .bubble { + background: #fff; + color: #222; + border-bottom-left-radius: 6px; +} + +.time { + font-size: 11px; + color: #aaa; + margin: 4px 8px 0 8px; + align-self: flex-end; +} + +.inputBar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + background: #fff; + padding: 10px 12px 10px 12px; + box-shadow: 0 -2px 8px rgba(0,0,0,0.04); + z-index: 10; +} + +.input { + flex: 1; + border: none; + outline: none; + background: #f3f4f6; + border-radius: 18px; + padding: 10px 14px; + font-size: 15px; + margin-right: 8px; +} + +.sendButton { + background: var(--primary-gradient, linear-gradient(135deg, #a7e0ff 0%, #5bbcff 100%)); + color: #fff; + border: none; + border-radius: 18px; + padding: 8px 18px; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} + +.sendButton:disabled { + background: #e5e7eb; + color: #aaa; + cursor: not-allowed; +} + +.iconBtn { + background: none; + border: none; + outline: none; + margin-right: 6px; + font-size: 20px; + color: #888; + cursor: pointer; + padding: 4px; + border-radius: 50%; + transition: background 0.2s, color 0.2s; +} + +.iconBtn:hover, .iconBtn:active { + background: #f3f4f6; + color: #5bbcff; +} + +.image { + max-width: 180px; + max-height: 180px; + border-radius: 10px; + display: block; +} + +.fileLink { + color: #5bbcff; + text-decoration: none; + font-size: 15px; + word-break: break-all; + display: flex; + align-items: center; +} + +.nav-title { + color: var(--primary-color); + font-weight: 700; + font-size: 18px; + text-shadow: 0 2px 4px rgba(24, 142, 238, 0.2); +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/ai-assistant/AIAssistant.tsx b/nkebao/src/pages/workspace/ai-assistant/AIAssistant.tsx index 2727f4534..4e60367e5 100644 --- a/nkebao/src/pages/workspace/ai-assistant/AIAssistant.tsx +++ b/nkebao/src/pages/workspace/ai-assistant/AIAssistant.tsx @@ -1,8 +1,264 @@ -import React from "react"; -import PlaceholderPage from "@/components/PlaceholderPage"; +import React, { useRef, useState, useEffect } from "react"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; +import { + PictureOutlined, + PaperClipOutlined, + AudioOutlined, +} from "@ant-design/icons"; +import styles from "./AIAssistant.module.scss"; + +interface Message { + id: string; + content: string; + from: "user" | "ai"; + time: string; + type?: "text" | "image" | "file" | "audio"; + fileName?: string; + fileUrl?: string; +} + +const initialMessages: Message[] = [ + { + id: "1", + content: "你好!我是你的AI助手,有什么可以帮助你的吗?", + from: "ai", + time: "15:29", + type: "text", + }, +]; const AIAssistant: React.FC = () => { - return ; + const [messages, setMessages] = useState(initialMessages); + const [input, setInput] = useState(""); + const [loading, setLoading] = useState(false); + const messagesEndRef = useRef(null); + const fileInputRef = useRef(null); + const imageInputRef = useRef(null); + const [recognizing, setRecognizing] = useState(false); + const recognitionRef = useRef(null); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + // 语音识别初始化 + useEffect(() => { + if (!("webkitSpeechRecognition" in window)) return; + const SpeechRecognition = (window as any).webkitSpeechRecognition; + recognitionRef.current = new SpeechRecognition(); + recognitionRef.current.continuous = false; + recognitionRef.current.interimResults = false; + recognitionRef.current.lang = "zh-CN"; + recognitionRef.current.onresult = (event: any) => { + const transcript = event.results[0][0].transcript; + setInput((prev) => prev + transcript); + setRecognizing(false); + }; + recognitionRef.current.onerror = () => setRecognizing(false); + recognitionRef.current.onend = () => setRecognizing(false); + }, []); + + const handleSend = async () => { + if (!input.trim()) return; + const userMsg: Message = { + id: Date.now().toString(), + content: input, + from: "user", + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + type: "text", + }; + setMessages((prev) => [...prev, userMsg]); + setInput(""); + setLoading(true); + setTimeout(() => { + setMessages((prev) => [ + ...prev, + { + id: Date.now().toString() + "-ai", + content: "AI正在思考...(此处可接入真实API)", + from: "ai", + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + type: "text", + }, + ]); + setLoading(false); + }, 1200); + }; + + // 图片上传 + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const url = URL.createObjectURL(file); + setMessages((prev) => [ + ...prev, + { + id: Date.now().toString(), + content: url, + from: "user", + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + type: "image", + fileName: file.name, + fileUrl: url, + }, + ]); + } + e.target.value = ""; + }; + + // 文件上传 + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const url = URL.createObjectURL(file); + setMessages((prev) => [ + ...prev, + { + id: Date.now().toString(), + content: file.name, + from: "user", + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + type: "file", + fileName: file.name, + fileUrl: url, + }, + ]); + } + e.target.value = ""; + }; + + // 语音输入 + const handleVoiceInput = () => { + if (!recognitionRef.current) return alert("当前浏览器不支持语音输入"); + if (recognizing) { + recognitionRef.current.stop(); + setRecognizing(false); + } else { + recognitionRef.current.start(); + setRecognizing(true); + } + }; + + return ( + } loading={false}> +
+
+ {messages.map((msg) => ( +
+ {msg.type === "text" && ( +
{msg.content}
+ )} + {msg.type === "image" && ( +
+ {msg.fileName} +
+ )} + {msg.type === "file" && ( + + )} + {/* 语音消息可后续扩展 */} +
{msg.time}
+
+ ))} + {loading && ( +
+
AI正在输入...
+
+ )} +
+
+
+ + + + + + setInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleSend(); + }} + disabled={loading} + /> + +
+
+ + ); }; export default AIAssistant; From aadc6d9a9e14a4f47b4a3699fa66f247ab2c4d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Thu, 24 Jul 2025 17:35:10 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20Ai=E5=88=86=E6=9E=90=E6=95=B0=E6=8D=AE=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workspace/ai-analyzer/index.module.scss | 96 ++++++++++++ .../src/pages/workspace/ai-analyzer/index.tsx | 141 ++++++++++++++++++ nkebao/src/router/module/workspace.tsx | 3 +- 3 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 nkebao/src/pages/workspace/ai-analyzer/index.module.scss create mode 100644 nkebao/src/pages/workspace/ai-analyzer/index.tsx diff --git a/nkebao/src/pages/workspace/ai-analyzer/index.module.scss b/nkebao/src/pages/workspace/ai-analyzer/index.module.scss new file mode 100644 index 000000000..1fe010a2e --- /dev/null +++ b/nkebao/src/pages/workspace/ai-analyzer/index.module.scss @@ -0,0 +1,96 @@ +.analyzerPage { + + +} + +.tabs { + background: #fff; + padding: 0 12px; + border-radius: 0 0 12px 12px; + margin-bottom: 8px; +} + +.planList { + display: flex; + flex-direction: column; + gap: 16px; + padding: 0 12px 16px 12px; +} + +.planCard { + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); + padding: 16px 14px 12px 14px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.cardHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; +} + +.cardTitle { + font-size: 16px; + font-weight: 700; + color: #222; +} + +.statusDone { + background: #e6f9e6; + color: #22c55e; + font-size: 12px; + border-radius: 8px; + padding: 2px 10px; + font-weight: 600; +} + +.statusDoing { + background: #e0f2fe; + color: #1677ff; + font-size: 12px; + border-radius: 8px; + padding: 2px 10px; + font-weight: 600; +} + +.cardInfo { + font-size: 13px; + color: #444; + display: flex; + flex-direction: column; + gap: 4px; +} + +.label { + color: #888; + font-size: 12px; + margin-right: 2px; +} + +.keyword { + display: inline-block; + background: #f3f4f6; + color: #1677ff; + border-radius: 6px; + padding: 2px 8px; + font-size: 12px; + margin-right: 6px; + margin-bottom: 2px; +} + +.cardActions { + display: flex; + gap: 10px; + margin-top: 8px; +} + +.actionBtn { + border-radius: 6px !important; + font-size: 13px !important; + padding: 0 12px !important; +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/ai-analyzer/index.tsx b/nkebao/src/pages/workspace/ai-analyzer/index.tsx new file mode 100644 index 000000000..09224bd6b --- /dev/null +++ b/nkebao/src/pages/workspace/ai-analyzer/index.tsx @@ -0,0 +1,141 @@ +import React, { useState } from "react"; +import NavCommon from "@/components/NavCommon"; +import Layout from "@/components/Layout/Layout"; +import { Tabs } from "antd-mobile"; +import { Button } from "antd"; +import styles from "./index.module.scss"; +import { PlusOutlined } from "@ant-design/icons"; + +const mockPlans = [ + { + id: "1", + title: "美妆用户分析", + status: "done", + device: "设备1", + wechat: "wxid_abc123", + type: "综合分析", + keywords: ["美妆", "护肤", "彩妆"], + createTime: "2023/12/15 18:30:00", + finishTime: "2023/12/15 19:45:00", + }, + { + id: "2", + title: "健身爱好者分析", + status: "doing", + device: "设备2", + wechat: "wxid_fit456", + type: "好友信息分析", + keywords: ["健身", "运动", "健康"], + createTime: "2023/12/16 17:15:00", + finishTime: "", + }, +]; + +const statusMap = { + all: "全部计划", + doing: "进行中", + done: "已完成", +}; + +const statusTag = { + done: 已完成, + doing: 分析中, +}; + +const AiAnalyzer: React.FC = () => { + const [tab, setTab] = useState<"all" | "doing" | "done">("all"); + + const filteredPlans = + tab === "all" ? mockPlans : mockPlans.filter((p) => p.status === tab); + + return ( + + 新建计划 + + } + /> + } + > +
+ setTab(key as any)} + className={styles.tabs} + > + + + + +
+ {filteredPlans.map((plan) => ( +
+
+ {plan.title} + {statusTag[plan.status as "done" | "doing"]} +
+
+
+ 设备: + {plan.device} | 微信号: {plan.wechat} +
+
+ 分析类型: + {plan.type} +
+
+ 关键词: + {plan.keywords.map((k) => ( + + {k} + + ))} +
+
+ 创建时间: + {plan.createTime} +
+ {plan.status === "done" && ( +
+ 完成时间: + {plan.finishTime} +
+ )} +
+
+ {plan.status === "done" ? ( + <> + + + + ) : ( + + )} +
+
+ ))} +
+
+
+ ); +}; + +export default AiAnalyzer; diff --git a/nkebao/src/router/module/workspace.tsx b/nkebao/src/router/module/workspace.tsx index ee4ca0c51..036d28116 100644 --- a/nkebao/src/router/module/workspace.tsx +++ b/nkebao/src/router/module/workspace.tsx @@ -16,6 +16,7 @@ import TrafficDistribution from "@/pages/workspace/traffic-distribution/TrafficD import TrafficDistributionDetail from "@/pages/workspace/traffic-distribution/Detail"; import NewDistribution from "@/pages/workspace/traffic-distribution/NewDistribution"; import PlaceholderPage from "@/components/PlaceholderPage"; +import AiAnalyzer from "@/pages/workspace/ai-analyzer"; const workspaceRoutes = [ { @@ -116,7 +117,7 @@ const workspaceRoutes = [ // AI数据分析 { path: "/workspace/ai-analyzer", - element: , + element: , auth: true, }, // AI策略优化