Merge branch 'yongpxu-dev' of https://e.coding.net/g-xtcy5189/cunkebao/cunkebao_v3 into yongpxu-dev

This commit is contained in:
笔记本里的永平
2025-07-24 20:30:44 +08:00
5 changed files with 637 additions and 4 deletions

View 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;
}

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

View 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);
}

View File

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

View File

@@ -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策略优化