Merge branch 'yongpxu-dev' of https://e.coding.net/g-xtcy5189/cunkebao/cunkebao_v3 into yongpxu-dev
This commit is contained in:
96
nkebao/src/pages/workspace/ai-analyzer/index.module.scss
Normal file
96
nkebao/src/pages/workspace/ai-analyzer/index.module.scss
Normal file
@@ -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;
|
||||
}
|
||||
141
nkebao/src/pages/workspace/ai-analyzer/index.tsx
Normal file
141
nkebao/src/pages/workspace/ai-analyzer/index.tsx
Normal file
@@ -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: <span className={styles.statusDone}>已完成</span>,
|
||||
doing: <span className={styles.statusDoing}>分析中</span>,
|
||||
};
|
||||
|
||||
const AiAnalyzer: React.FC = () => {
|
||||
const [tab, setTab] = useState<"all" | "doing" | "done">("all");
|
||||
|
||||
const filteredPlans =
|
||||
tab === "all" ? mockPlans : mockPlans.filter((p) => p.status === tab);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavCommon
|
||||
title="AI数据分析"
|
||||
right={
|
||||
<Button type="primary" size="small" style={{ borderRadius: 6 }}>
|
||||
<PlusOutlined /> 新建计划
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={styles.analyzerPage}>
|
||||
<Tabs
|
||||
activeKey={tab}
|
||||
onChange={(key) => setTab(key as any)}
|
||||
className={styles.tabs}
|
||||
>
|
||||
<Tabs.Tab title="全部计划" key="all" />
|
||||
<Tabs.Tab title="进行中" key="doing" />
|
||||
<Tabs.Tab title="已完成" key="done" />
|
||||
</Tabs>
|
||||
<div className={styles.planList}>
|
||||
{filteredPlans.map((plan) => (
|
||||
<div className={styles.planCard} key={plan.id}>
|
||||
<div className={styles.cardHeader}>
|
||||
<span className={styles.cardTitle}>{plan.title}</span>
|
||||
{statusTag[plan.status as "done" | "doing"]}
|
||||
</div>
|
||||
<div className={styles.cardInfo}>
|
||||
<div>
|
||||
<span className={styles.label}>设备:</span>
|
||||
{plan.device} | 微信号: {plan.wechat}
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.label}>分析类型:</span>
|
||||
{plan.type}
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.label}>关键词:</span>
|
||||
{plan.keywords.map((k) => (
|
||||
<span className={styles.keyword} key={k}>
|
||||
{k}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.label}>创建时间:</span>
|
||||
{plan.createTime}
|
||||
</div>
|
||||
{plan.status === "done" && (
|
||||
<div>
|
||||
<span className={styles.label}>完成时间:</span>
|
||||
{plan.finishTime}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.cardActions}>
|
||||
{plan.status === "done" ? (
|
||||
<>
|
||||
<Button size="small" className={styles.actionBtn}>
|
||||
发送报告
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
className={styles.actionBtn}
|
||||
>
|
||||
查看报告
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
className={styles.actionBtn}
|
||||
>
|
||||
查看进度
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AiAnalyzer;
|
||||
139
nkebao/src/pages/workspace/ai-assistant/AIAssistant.module.scss
Normal file
139
nkebao/src/pages/workspace/ai-assistant/AIAssistant.module.scss
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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 <PlaceholderPage title="AI助手" showBack={false} />;
|
||||
const [messages, setMessages] = useState<Message[]>(initialMessages);
|
||||
const [input, setInput] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const imageInputRef = useRef<HTMLInputElement>(null);
|
||||
const [recognizing, setRecognizing] = useState(false);
|
||||
const recognitionRef = useRef<any>(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<HTMLInputElement>) => {
|
||||
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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<Layout header={<NavCommon title="AI助手" />} loading={false}>
|
||||
<div className={styles.chatContainer}>
|
||||
<div className={styles.messageList}>
|
||||
{messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={
|
||||
msg.from === "user" ? styles.userMessage : styles.aiMessage
|
||||
}
|
||||
>
|
||||
{msg.type === "text" && (
|
||||
<div className={styles.bubble}>{msg.content}</div>
|
||||
)}
|
||||
{msg.type === "image" && (
|
||||
<div className={styles.bubble}>
|
||||
<img
|
||||
src={msg.fileUrl}
|
||||
alt={msg.fileName}
|
||||
className={styles.image}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{msg.type === "file" && (
|
||||
<div className={styles.bubble}>
|
||||
<a
|
||||
href={msg.fileUrl}
|
||||
download={msg.fileName}
|
||||
className={styles.fileLink}
|
||||
>
|
||||
<PaperClipOutlined style={{ marginRight: 6 }} />
|
||||
{msg.fileName}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{/* 语音消息可后续扩展 */}
|
||||
<div className={styles.time}>{msg.time}</div>
|
||||
</div>
|
||||
))}
|
||||
{loading && (
|
||||
<div className={styles.aiMessage}>
|
||||
<div className={styles.bubble}>AI正在输入...</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
<div className={styles.inputBar}>
|
||||
<button
|
||||
className={styles.iconBtn}
|
||||
onClick={() => imageInputRef.current?.click()}
|
||||
title="图片"
|
||||
type="button"
|
||||
>
|
||||
<PictureOutlined />
|
||||
</button>
|
||||
<input
|
||||
ref={imageInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: "none" }}
|
||||
onChange={handleImageChange}
|
||||
/>
|
||||
<button
|
||||
className={styles.iconBtn}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
title="文件"
|
||||
type="button"
|
||||
>
|
||||
<PaperClipOutlined />
|
||||
</button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
style={{ display: "none" }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<button
|
||||
className={styles.iconBtn}
|
||||
onClick={handleVoiceInput}
|
||||
title="语音输入"
|
||||
type="button"
|
||||
style={{ color: recognizing ? "#5bbcff" : undefined }}
|
||||
>
|
||||
<AudioOutlined />
|
||||
</button>
|
||||
<input
|
||||
className={styles.input}
|
||||
type="text"
|
||||
placeholder="输入消息..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleSend();
|
||||
}}
|
||||
disabled={loading}
|
||||
/>
|
||||
<button
|
||||
className={styles.sendButton}
|
||||
onClick={handleSend}
|
||||
disabled={loading || !input.trim()}
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIAssistant;
|
||||
|
||||
@@ -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: <PlaceholderPage title="AI数据分析" />,
|
||||
element: <AiAnalyzer />,
|
||||
auth: true,
|
||||
},
|
||||
// AI策略优化
|
||||
|
||||
Reference in New Issue
Block a user