diff --git a/Cunkebao/src/pages/Home.tsx b/Cunkebao/src/pages/Home.tsx index 52994a85..140727eb 100644 --- a/Cunkebao/src/pages/Home.tsx +++ b/Cunkebao/src/pages/Home.tsx @@ -1,21 +1,30 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Bell, Smartphone, Users, Activity, MessageSquare, TrendingUp } from 'lucide-react'; -import Chart from 'chart.js/auto'; -import Layout from '@/components/Layout'; -import BottomNav from '@/components/BottomNav'; -import UnifiedHeader, { HeaderPresets } from '@/components/UnifiedHeader'; -import { Card } from '@/components/ui/card'; -import { Progress } from '@/components/ui/progress'; -import '@/components/Layout.css'; +import React, { useEffect, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { + Bell, + Smartphone, + Users, + Activity, + MessageSquare, + TrendingUp, +} from "lucide-react"; +import Chart from "chart.js/auto"; +import Layout from "@/components/Layout"; +import BottomNav from "@/components/BottomNav"; +import UnifiedHeader, { HeaderPresets } from "@/components/UnifiedHeader"; +import { Card } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import "@/components/Layout.css"; // API接口定义 -const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || "https://ckbapi.quwanzhi.com"; +const API_BASE_URL = + process.env.REACT_APP_API_BASE_URL || "https://ckbapi.quwanzhi.com"; // 统一的API请求客户端 async function apiRequest(url: string): Promise { try { - const token = typeof window !== "undefined" ? localStorage.getItem("token") : null; + const token = + typeof window !== "undefined" ? localStorage.getItem("token") : null; const headers: Record = { "Content-Type": "application/json", Accept: "application/json", @@ -99,7 +108,7 @@ export default function Home() { growth: 12, }, { - id: "xiaohongshu", + id: "xiaohongshu", name: "小红书获客", icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png", color: "bg-red-100 text-red-600", @@ -135,7 +144,7 @@ export default function Home() { }, { title: "群发任务", - value: "8", + value: "8", icon: , color: "text-orange-600", path: "/workspace/group-push", @@ -180,10 +189,11 @@ export default function Home() { // 尝试请求API数据 try { // 并行请求多个接口 - const [deviceStatsResult, wechatStatsResult] = await Promise.allSettled([ - apiRequest(`${API_BASE_URL}/v1/dashboard/device-stats`), - apiRequest(`${API_BASE_URL}/v1/dashboard/wechat-stats`), - ]); + const [deviceStatsResult, wechatStatsResult] = + await Promise.allSettled([ + apiRequest(`${API_BASE_URL}/v1/dashboard/device-stats`), + apiRequest(`${API_BASE_URL}/v1/dashboard/wechat-stats`), + ]); const newStats = { totalDevices: 0, @@ -213,7 +223,9 @@ export default function Home() { setStats(newStats); } catch (apiError) { console.warn("API请求失败,使用默认数据:", apiError); - setApiError(apiError instanceof Error ? apiError.message : "API连接失败"); + setApiError( + apiError instanceof Error ? apiError.message : "API连接失败" + ); // 使用默认数据 setStats({ @@ -247,11 +259,11 @@ export default function Home() { }, []); // 移除stats依赖 const handleDevicesClick = () => { - navigate('/profile/devices'); + navigate("/profile/devices"); }; const handleWechatClick = () => { - navigate('/wechat-accounts'); + navigate("/wechat-accounts"); }; // 使用Chart.js创建图表 @@ -263,7 +275,7 @@ export default function Home() { } const ctx = chartRef.current.getContext("2d"); - + // 添加null检查 if (!ctx) return; @@ -391,9 +403,12 @@ export default function Home() {
设备数量
- {stats.totalDevices} + + {stats.totalDevices} +
+
@@ -402,22 +417,31 @@ export default function Home() {
微信号数量
- {stats.totalWechatAccounts} + + {stats.totalWechatAccounts} +
+
在线微信号
- {stats.onlineWechatAccounts} + + {stats.onlineWechatAccounts} +
0 ? (stats.onlineWechatAccounts / stats.totalWechatAccounts) * 100 : 0 + stats.totalWechatAccounts > 0 + ? (stats.onlineWechatAccounts / + stats.totalWechatAccounts) * + 100 + : 0 } className="h-1" /> @@ -435,16 +459,30 @@ export default function Home() { .sort((a, b) => b.value - a.value) .slice(0, 4) // 只显示前4个 .map((scenario) => ( -
navigate(`/scenarios/${scenario.id}?name=${encodeURIComponent(scenario.name)}`)} + onClick={() => + navigate( + `/scenarios/${scenario.id}?name=${encodeURIComponent( + scenario.name + )}` + ) + } >
-
- {scenario.name} +
+ {scenario.name} +
+
+ {scenario.value}
-
{scenario.value}
{scenario.name}
@@ -466,7 +504,9 @@ export default function Home() { className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100 transition-colors" onClick={() => stat.path && navigate(stat.path)} > -
{stat.icon}
+
+ {stat.icon} +
{stat.value}
{stat.title}
@@ -487,4 +527,4 @@ export default function Home() {
); -} \ No newline at end of file +} diff --git a/nkebao.code-workspace b/nkebao.code-workspace new file mode 100644 index 00000000..042d95e5 --- /dev/null +++ b/nkebao.code-workspace @@ -0,0 +1,14 @@ +{ + "folders": [ + { + "path": "nkebao" + }, + { + "path": "Cunkebao" + }, + { + "path": "../../MySelf/好版登项目/好版登小程序" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/nkebao/src/pages/scenarios/list/index.module.scss b/nkebao/src/pages/scenarios/list/index.module.scss index e7c76ea5..e6a3ae82 100644 --- a/nkebao/src/pages/scenarios/list/index.module.scss +++ b/nkebao/src/pages/scenarios/list/index.module.scss @@ -199,10 +199,6 @@ font-size: 12px; color: #888; text-align: center; - margin-bottom: 6px; - line-height: 1.4; - min-height: 32px; - max-height: 32px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; @@ -262,9 +258,6 @@ // 响应式设计 @media (max-width: 480px) { - .scene-page { - padding: 8px; - } .scenario-card { padding: 14px 16px; @@ -307,20 +300,18 @@ padding: 12px 4px 10px 4px; } .card-img-bg { - width: 40px; - height: 40px; + width: 60px; + height: 60px; } .card-img { - width: 26px; - height: 26px; + width: 40px; + height: 40px; } .card-title { font-size: 15px; } .card-desc { font-size: 11px; - min-height: 24px; - max-height: 24px; } .card-count { font-size: 12px; diff --git a/nkebao/src/pages/scenarios/list/index.tsx b/nkebao/src/pages/scenarios/list/index.tsx index 48785bec..97be9b46 100644 --- a/nkebao/src/pages/scenarios/list/index.tsx +++ b/nkebao/src/pages/scenarios/list/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { NavBar, Button, Toast } from "antd-mobile"; -import { PlusOutlined, UpOutlined } from "@ant-design/icons"; +import { PlusOutlined, RiseOutlined } from "@ant-design/icons"; import MeauMobile from "@/components/MeauMobile/MeauMoible"; import Layout from "@/components/Layout/Layout"; import { getScenarios } from "./api"; @@ -162,7 +162,7 @@ const Scene: React.FC = () => { 今日: {scenario.count} - {scenario.growth} diff --git a/nkebao/src/pages/scenarios/plan/list/data.ts b/nkebao/src/pages/scenarios/plan/list/data.ts new file mode 100644 index 00000000..0f36a6c9 --- /dev/null +++ b/nkebao/src/pages/scenarios/plan/list/data.ts @@ -0,0 +1,22 @@ +export interface Task { + id: string; + name: string; + status: number; + created_at: string; + updated_at: string; + enabled: boolean; + total_customers?: number; + today_customers?: number; + lastUpdated?: string; + stats?: { + devices?: number; + acquired?: number; + added?: number; + }; +} + +export interface ApiSettings { + apiKey: string; + webhookUrl: string; + taskId: string; +} diff --git a/nkebao/src/pages/scenarios/plan/list/index.module.scss b/nkebao/src/pages/scenarios/plan/list/index.module.scss index b3696f7b..50366c87 100644 --- a/nkebao/src/pages/scenarios/plan/list/index.module.scss +++ b/nkebao/src/pages/scenarios/plan/list/index.module.scss @@ -41,22 +41,12 @@ position: relative; flex: 1; - .adm-input { - padding-left: 40px; + .ant-input { border-radius: 8px; + height: 40px; } } -.search-icon { - position: absolute; - left: 12px; - top: 50%; - transform: translateY(-50%); - color: #999; - font-size: 16px; - z-index: 1; -} - .refresh-btn { height: 40px; width: 40px; @@ -87,7 +77,7 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 12px; + margin-bottom: 16px; } .plan-name { @@ -98,21 +88,64 @@ margin-right: 12px; } -.plan-meta { +.plan-header-right { display: flex; - flex-direction: column; + align-items: center; gap: 8px; - margin-bottom: 12px; - padding-bottom: 12px; - border-bottom: 1px solid #f0f0f0; } -.meta-item { +.more-btn { + padding: 4px; + min-width: auto; + height: 28px; + width: 28px; + border-radius: 4px; + + &:hover { + background-color: #f5f5f5; + } +} + +.stats-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin-bottom: 16px; +} + +.stat-item { + background: #f8f9fa; + border-radius: 8px; + padding: 12px; + text-align: center; + border: 1px solid #e9ecef; +} + +.stat-label { + font-size: 12px; + color: #666; + margin-bottom: 4px; + font-weight: 500; +} + +.stat-value { + font-size: 18px; + font-weight: 600; + color: #333; + line-height: 1.2; +} + +.plan-footer { + border-top: 1px solid #f0f0f0; + padding-top: 12px; +} + +.last-execution { display: flex; align-items: center; gap: 6px; font-size: 12px; - color: #666; + color: #999; svg { font-size: 14px; @@ -120,12 +153,6 @@ } } -.plan-actions { - display: flex; - justify-content: flex-end; - gap: 8px; -} - .empty-state { display: flex; flex-direction: column; @@ -147,6 +174,48 @@ border-radius: 20px; } +.action-menu-dialog { + background: white; + border-radius: 16px 16px 0 0; + padding: 20px; + max-height: 60vh; + display: flex; + flex-direction: column; +} + +.action-menu-item { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s ease; + + &:hover { + background-color: #f5f5f5; + } + + &.danger { + color: #ff4d4f; + + &:hover { + background-color: #fff2f0; + } + } +} + +.action-icon { + font-size: 16px; + width: 20px; + text-align: center; +} + +.action-text { + font-size: 16px; + font-weight: 500; +} + .api-dialog { background: white; border-radius: 16px 16px 0 0; @@ -188,7 +257,7 @@ font-weight: 500; } - .adm-input { + .ant-input { border-radius: 8px; } } @@ -198,7 +267,7 @@ gap: 8px; align-items: center; - .adm-input { + .ant-input { flex: 1; } } diff --git a/nkebao/src/pages/scenarios/plan/list/index.tsx b/nkebao/src/pages/scenarios/plan/list/index.tsx index 5d02c359..9ec0f8df 100644 --- a/nkebao/src/pages/scenarios/plan/list/index.tsx +++ b/nkebao/src/pages/scenarios/plan/list/index.tsx @@ -7,12 +7,12 @@ import { Toast, SpinLoading, Dialog, - Input, Popup, Card, Tag, Space, } from "antd-mobile"; +import { Input } from "antd"; import { PlusOutlined, UserOutlined, @@ -24,6 +24,8 @@ import { ReloadOutlined, QrcodeOutlined, EditOutlined, + MoreOutlined, + ClockCircleOutlined, } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; @@ -36,40 +38,7 @@ import { getWxMinAppCode, } from "./api"; import style from "./index.module.scss"; - -interface Task { - id: string; - name: string; - status: number; - created_at: string; - updated_at: string; - enabled: boolean; - total_customers?: number; - today_customers?: number; - lastUpdated?: string; - stats?: { - devices?: number; - acquired?: number; - added?: number; - }; -} - -interface ScenarioData { - id: string; - name: string; - image: string; - description: string; - totalPlans: number; - totalCustomers: number; - todayCustomers: number; - growth: string; -} - -interface ApiSettings { - apiKey: string; - webhookUrl: string; - taskId: string; -} +import { Task, ApiSettings } from "./data"; const ScenarioList: React.FC = () => { const { scenarioId, scenarioName } = useParams<{ @@ -78,10 +47,9 @@ const ScenarioList: React.FC = () => { }>(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); - const [scenario, setScenario] = useState(null); + const [pageTitle, setPageTitle] = useState(""); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); - const [error, setError] = useState(""); const [showApiDialog, setShowApiDialog] = useState(false); const [currentApiSettings, setCurrentApiSettings] = useState({ apiKey: "", @@ -93,6 +61,7 @@ const ScenarioList: React.FC = () => { const [showQrDialog, setShowQrDialog] = useState(false); const [qrLoading, setQrLoading] = useState(false); const [qrImg, setQrImg] = useState(""); + const [showActionMenu, setShowActionMenu] = useState(null); // 获取渠道中文名称 const getChannelName = (channel: string) => { @@ -111,27 +80,19 @@ const ScenarioList: React.FC = () => { return channelMap[channel] || `${channel}获客`; }; - // 获取场景描述 - const getScenarioDescription = (channel: string) => { - const descriptions: Record = { - douyin: "通过抖音平台进行精准获客,利用短视频内容吸引目标用户", - xiaohongshu: "利用小红书平台进行内容营销获客,通过优质内容建立品牌形象", - gongzhonghao: "通过微信公众号进行获客,建立私域流量池", - haibao: "通过海报分享进行获客,快速传播品牌信息", - phone: "通过电话营销进行获客,直接与客户沟通", - weixinqun: "通过微信群进行获客,利用社交裂变效应", - payment: "通过付款码进行获客,便捷的支付方式", - api: "通过API接口进行获客,支持第三方系统集成", - }; - return descriptions[channel] || "通过该平台进行获客"; - }; + // 获取场景名称 + const getScenarioName = useCallback(() => { + const urlName = searchParams.get("name"); + if (urlName) { + return urlName; + } + return getChannelName(scenarioId || ""); + }, [searchParams, scenarioId]); useEffect(() => { const fetchScenarioData = async () => { if (!scenarioId) return; - setLoading(true); - setError(""); try { // 获取计划列表 @@ -142,98 +103,37 @@ const ScenarioList: React.FC = () => { }); // 设置计划列表 - if (response && response.data && response.data.list) { - setTasks(response.data.list); - } else { - setTasks([]); - } + setTasks(response.list); - // 构建场景数据 - const scenarioData: ScenarioData = { - id: scenarioId, - name: scenarioName || "", - image: "", - description: getScenarioDescription(scenarioId), - totalPlans: response?.data?.list?.length || 0, - totalCustomers: 0, - todayCustomers: 0, - growth: "", - }; - - setScenario(scenarioData); + // 设置页面标题 + setPageTitle(getScenarioName()); } catch (error) { console.error("获取场景数据失败:", error); - // 即使API失败也要创建基本的场景数据 - const scenarioData: ScenarioData = { - id: scenarioId, - name: getScenarioName(), - image: "", - description: getScenarioDescription(scenarioId), - totalPlans: 0, - totalCustomers: 0, - todayCustomers: 0, - growth: "", - }; - setScenario(scenarioData); setTasks([]); + setPageTitle(getScenarioName()); } finally { setLoading(false); } }; fetchScenarioData(); - }, [scenarioId]); + }, [scenarioId, getScenarioName]); - // 获取场景名称 - const getScenarioName = useCallback(() => { - const urlName = searchParams.get("name"); - if (urlName) { - return urlName; - } - return getChannelName(scenarioId || ""); - }, [searchParams, scenarioId]); - - // 更新场景数据中的名称 + // 更新页面标题 useEffect(() => { - setScenario((prev) => - prev - ? { - ...prev, - name: (() => { - const urlName = searchParams.get("name"); - if (urlName) return urlName; - return getChannelName(scenarioId || ""); - })(), - } - : null - ); - }, [searchParams, scenarioId]); + setPageTitle(getScenarioName()); + }, [getScenarioName]); const handleCopyPlan = async (taskId: string) => { const taskToCopy = tasks.find((task) => task.id === taskId); if (!taskToCopy) return; - - try { - const response = await copyPlan(taskId); - if (response && response.code === 200) { - Toast.show({ - content: `已成功复制"${taskToCopy.name}"`, - position: "top", - }); - // 刷新列表 - handleRefresh(); - } else { - Toast.show({ - content: response?.msg || "复制失败", - position: "top", - }); - } - } catch (error) { - Toast.show({ - content: "复制失败,请重试", - position: "top", - }); - } + await copyPlan(taskId); + Toast.show({ + content: `已成功复制"${taskToCopy.name}"`, + position: "top", + }); + // 刷新列表 + handleRefresh(); }; const handleDeletePlan = async (taskId: string) => { @@ -343,7 +243,7 @@ const ScenarioList: React.FC = () => { const getStatusText = (status: number) => { switch (status) { case 1: - return "运行中"; + return "进行中"; case 0: return "已暂停"; case -1: @@ -357,13 +257,11 @@ const ScenarioList: React.FC = () => { setLoadingTasks(true); try { const response = await getPlanList({ - scenarioId: scenarioId!, + sceneId: scenarioId!, page: 1, limit: 20, }); - if (response && response.data && response.data.list) { - setTasks(response.data.list); - } + setTasks(response.list); } catch (error) { Toast.show({ content: "刷新失败", @@ -378,6 +276,77 @@ const ScenarioList: React.FC = () => { task.name.toLowerCase().includes(searchTerm.toLowerCase()) ); + // 计算通过率 + const calculatePassRate = (acquired: number, added: number) => { + if (added === 0) return "0.00%"; + return `${((acquired / added) * 100).toFixed(2)}%`; + }; + + // 格式化时间 + const formatTime = (timeString: string) => { + if (!timeString) return ""; + const date = new Date(timeString); + return date + .toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }) + .replace(/\//g, "-"); + }; + + // 生成操作菜单 + const getActionMenu = (task: Task) => [ + { + key: "edit", + text: "编辑", + icon: , + onClick: () => { + setShowActionMenu(null); + navigate(`/scenarios/edit/${task.id}`); + }, + }, + { + key: "settings", + text: "API设置", + icon: , + onClick: () => { + setShowActionMenu(null); + handleOpenApiSettings(task.id); + }, + }, + { + key: "copy", + text: "复制", + icon: , + onClick: () => { + setShowActionMenu(null); + handleCopyPlan(task.id); + }, + }, + { + key: "qrcode", + text: "二维码", + icon: , + onClick: () => { + setShowActionMenu(null); + handleShowQrCode(task.id); + }, + }, + { + key: "delete", + text: "删除", + icon: , + onClick: () => { + setShowActionMenu(null); + handleDeletePlan(task.id); + }, + danger: true, + }, + ]; + if (loading) { return ( { {scenario?.name}
} + left={
{pageTitle}
} right={ +
- {/* 中部:更新时间和统计 */} -
-
- - 最后更新: {task.updated_at || task.created_at} + {/* 统计数据网格 */} +
+
+
设备数
+
+ {task.stats?.devices || 0} +
-
- - - 设备: {task.stats?.devices || 0} | 获客:{" "} - {task.stats?.acquired || 0} | 添加:{" "} +
+
已获客
+
+ {task.stats?.acquired || 0} +
+
+
+
已添加
+
{task.stats?.added || 0} +
+
+
+
通过率
+
{task.passRate}%
+
+
+ + {/* 底部:上次执行时间 */} +
+
+ + + 上次执行: {formatTime(task.updated_at || task.created_at)}
- - {/* 底部:操作按钮 */} -
- - - - - - - -
)) )} @@ -537,7 +503,7 @@ const ScenarioList: React.FC = () => {
- +
+ {/* 操作菜单弹窗 */} + setShowActionMenu(null)} + position="bottom" + bodyStyle={{ height: "auto", maxHeight: "60vh" }} + > +
+
+

操作菜单

+ +
+
+ {showActionMenu && + getActionMenu(tasks.find((t) => t.id === showActionMenu)!).map( + (item) => ( +
+ {item.icon} + {item.text} +
+ ) + )} +
+
+
+ {/* 二维码弹窗 */}