feat: 本次提交更新内容如下

存一波
This commit is contained in:
笔记本里的永平
2025-07-21 11:27:22 +08:00
parent cb60dd82e5
commit 881dd0f067
6 changed files with 725 additions and 297 deletions

View File

@@ -1,21 +1,21 @@
module.exports = { module.exports = {
root: true, root: true,
env: { env: {
browser: true, browser: true,
es2021: true, es2021: true,
node: true, node: true,
}, },
extends: [ extends: [
'react-app', 'react-app',
'plugin:react/recommended', 'plugin:react/recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended', 'plugin:prettier/recommended',
], ],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
plugins: ['react', '@typescript-eslint', 'prettier'], plugins: ['react', '@typescript-eslint', 'prettier'],
rules: { rules: {
'prettier/prettier': 'warn', 'prettier/prettier': 'warn',
'react/react-in-jsx-scope': 'off', 'react/react-in-jsx-scope': 'off',
'@typescript-eslint/no-unused-vars': 'warn', '@typescript-eslint/no-unused-vars': 'warn',
}, },
}; };

View File

@@ -6,6 +6,12 @@ import {
ClockCircleOutline, ClockCircleOutline,
SendOutline, SendOutline,
StarOutline, StarOutline,
Bell,
Smartphone,
Users,
Activity,
MessageSquare,
TrendingUp,
} from "antd-mobile-icons"; } from "antd-mobile-icons";
import MeauMobile from "@/components/MeauMobile/MeauMoible"; import MeauMobile from "@/components/MeauMobile/MeauMoible";
import Layout from "@/components/Layout/Layout"; import Layout from "@/components/Layout/Layout";
@@ -23,160 +29,358 @@ const Home: React.FC = () => {
const [todayStats, setTodayStats] = useState<any[]>([]); const [todayStats, setTodayStats] = useState<any[]>([]);
const [dashboard, setDashboard] = useState<any>({}); const [dashboard, setDashboard] = useState<any>({});
const [sevenDayStats, setSevenDayStats] = useState<any>({}); const [sevenDayStats, setSevenDayStats] = useState<any>({});
const [isLoading, setIsLoading] = useState(true);
const [apiError, setApiError] = useState("");
// 场景获客数据
const scenarioFeatures = [
{
id: "douyin",
name: "抖音获客",
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-QR8ManuDplYTySUJsY4mymiZkDYnQ9.png",
color: "bg-blue-100 text-blue-600",
value: 156,
growth: 12,
},
{
id: "xiaohongshu",
name: "小红书获客",
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png",
color: "bg-red-100 text-red-600",
value: 89,
growth: 8,
},
{
id: "gongzhonghao",
name: "公众号获客",
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-Gsg0CMf5tsZb41mioszdjqU1WmsRxW.png",
color: "bg-green-100 text-green-600",
value: 234,
growth: 15,
},
{
id: "haibao",
name: "海报获客",
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-x92XJgXy4MI7moNYlA1EAes2FqDxMH.png",
color: "bg-orange-100 text-orange-600",
value: 167,
growth: 10,
},
];
// 今日数据统计
const todayStatsData = [
{
title: "朋友圈同步",
value: "12",
icon: <MessageSquare className="h-4 w-4" />,
color: "text-purple-600",
path: "/workspace/moments-sync",
},
{
title: "群发任务",
value: "8",
icon: <Users className="h-4 w-4" />,
color: "text-orange-600",
path: "/workspace/group-push",
},
{
title: "获客转化",
value: "85%",
icon: <TrendingUp className="h-4 w-4" />,
color: "text-green-600",
path: "/scenarios",
},
{
title: "系统活跃度",
value: "98%",
icon: <Activity className="h-4 w-4" />,
color: "text-blue-600",
path: "/workspace",
},
];
useEffect(() => { useEffect(() => {
getDashboard().then((res: any) => { const fetchData = async () => {
setDashboard(res); try {
}); setIsLoading(true);
getPlanStats({ num: 4 }).then((res: any) => { setApiError("");
setSceneStats(res);
}); // 并行请求多个接口
getSevenDayStats().then((res: any) => { const [dashboardResult, planStatsResult, sevenDayResult, todayResult] =
setSevenDayStats(res); await Promise.allSettled([
}); getDashboard(),
getTodayStats().then((res: any) => { getPlanStats({ num: 4 }),
const todayStatsData = [ getSevenDayStats(),
{ getTodayStats(),
label: "同步朋友圈", ]);
value: res.momentsNum,
icon: ( // 处理仪表板数据
<ClockCircleOutline style={{ fontSize: 16, color: "#ff6b35" }} /> if (dashboardResult.status === "fulfilled") {
), setDashboard(dashboardResult.value);
color: "#ff6b35", } else {
}, console.warn("仪表板API失败:", dashboardResult.reason);
{ setApiError("API连接异常显示默认数据");
label: "群发任务", }
value: res.groupPushNum,
icon: <SendOutline style={{ fontSize: 16, color: "#ffd700" }} />, // 处理计划统计数据
color: "#ffd700", if (planStatsResult.status === "fulfilled") {
}, setSceneStats(planStatsResult.value);
{ } else {
label: "获客转化率", console.warn("计划统计API失败:", planStatsResult.reason);
value: res.passRate, }
icon: <StarOutline style={{ fontSize: 16, color: "#4caf50" }} />,
color: "#4caf50", // 处理七天统计数据
}, if (sevenDayResult.status === "fulfilled") {
{ setSevenDayStats(sevenDayResult.value);
label: "系统活跃度", } else {
value: res.sysActive, console.warn("七天统计API失败:", sevenDayResult.reason);
icon: ( }
<ClockCircleOutline style={{ fontSize: 16, color: "#2196f3" }} />
), // 处理今日统计数据
color: "#2196f3", if (todayResult.status === "fulfilled") {
}, const todayStatsData = [
]; {
setTodayStats(todayStatsData); label: "同步朋友圈",
}); value: todayResult.value.momentsNum,
icon: (
<ClockCircleOutline
style={{ fontSize: 16, color: "#ff6b35" }}
/>
),
color: "#ff6b35",
},
{
label: "群发任务",
value: todayResult.value.groupPushNum,
icon: <SendOutline style={{ fontSize: 16, color: "#ffd700" }} />,
color: "#ffd700",
},
{
label: "获客转化率",
value: todayResult.value.passRate,
icon: <StarOutline style={{ fontSize: 16, color: "#4caf50" }} />,
color: "#4caf50",
},
{
label: "系统活跃度",
value: todayResult.value.sysActive,
icon: (
<ClockCircleOutline
style={{ fontSize: 16, color: "#2196f3" }}
/>
),
color: "#2196f3",
},
];
setTodayStats(todayStatsData);
} else {
console.warn("今日统计API失败:", todayResult.reason);
}
} catch (error) {
console.error("获取数据失败:", error);
setApiError(error instanceof Error ? error.message : "数据加载失败");
} finally {
setIsLoading(false);
}
};
fetchData();
}, []); }, []);
const handleDevicesClick = () => {
// 导航到设备页面
console.log("点击设备");
};
const handleWechatClick = () => {
// 导航到微信号页面
console.log("点击微信号");
};
if (isLoading) {
return (
<Layout
header={
<NavBar back={null} style={{ background: "#fff" }}>
<div className={style["nav-title"]}>
<span className={style["nav-text"]}></span>
</div>
</NavBar>
}
footer={<MeauMobile />}
loading={true}
>
<div className="bg-gray-50">
<div className="p-4 space-y-4">
<div className="grid grid-cols-3 gap-3">
{[...Array(3)].map((_, i) => (
<div key={i} className="p-3 bg-white animate-pulse rounded-lg">
<div className="h-4 bg-gray-200 rounded mb-2"></div>
<div className="h-6 bg-gray-200 rounded"></div>
</div>
))}
</div>
</div>
</div>
</Layout>
);
}
return ( return (
<Layout <Layout
header={ header={
<NavBar back={null} style={{ background: "#fff" }}> <NavBar back={null} style={{ background: "#fff" }}>
<div className={style["nav-title"]}> <div className={style["nav-title"]}>
<span className={style["nav-text"]}></span> <span className={style["nav-text"]}></span>
<div className="flex items-center ml-auto">
{apiError && (
<div className="text-xs text-orange-600 bg-orange-50 px-2 py-1 rounded mr-2">
API连接异常
</div>
)}
<button className="p-2 hover:bg-gray-100 rounded-full">
<Bell className="h-5 w-5 text-gray-600" />
</button>
</div>
</div> </div>
</NavBar> </NavBar>
} }
footer={<MeauMobile />} footer={<MeauMobile />}
> >
<div className={style["home-page"]}> <div className="bg-gray-50">
{/* 顶部统计卡片 */} <div className="p-4 space-y-4">
<div className={style["stats-grid"]}> {/* 统计卡片 */}
<div className={style["stat-card"]}> <div className="grid grid-cols-3 gap-3">
<div className={style["stat-icon"]}> <div className="cursor-pointer" onClick={handleDevicesClick}>
<AppOutline <div className="p-3 bg-white hover:shadow-md transition-all rounded-lg">
style={{ fontSize: 18, color: "var(--primary-color)" }} <div className="flex flex-col">
/> <span className="text-xs text-gray-500 mb-1"></span>
</div> <div className="flex items-center justify-between">
<div className={style["stat-content"]}> <span className="text-lg font-bold text-blue-600">
<div className={style["stat-label"]}></div> {dashboard.deviceNum || 42}
</span>
<div className={style["stat-value"]}>{dashboard.deviceNum}</div> <Smartphone className="w-5 h-5 text-blue-600" />
</div> </div>
</div> <div className="h-2"></div>
<div className={style["stat-card"]}>
<div className={style["stat-icon"]}>
<UserOutline
style={{ fontSize: 18, color: "var(--primary-color)" }}
/>
</div>
<div className={style["stat-content"]}>
<div className={style["stat-label"]}></div>
<div className={style["stat-value"]}>{dashboard.wechatNum}</div>
</div>
</div>
<div className={style["stat-card"]}>
<div className={style["stat-icon"]}>
<ClockCircleOutline
style={{ fontSize: 18, color: "var(--primary-color)" }}
/>
</div>
<div className={style["stat-content"]}>
<div className={style["stat-label"]}>线</div>
<div className={style["stat-value"]}>
{dashboard.aliveWechatNum}
</div>
</div>
</div>
</div>
{/* 场景获客统计 */}
<div className={style["section"]}>
<div className={style["section-header"]}>
<span className={style["section-title"]}></span>
</div>
<div className={style["scene-grid"]}>
{sceneStats.map((item) => (
<div key={item.id} className={style["scene-item"]}>
<div className={style["scene-icon"]}>
<img
src={item.image}
alt={item.name}
className={style["scene-image"]}
/>
</div> </div>
<div className={style["scene-value"]}>{item.allNum}</div>
<div className={style["scene-label"]}>{item.name}</div>
</div> </div>
))} </div>
<div className="cursor-pointer" onClick={handleWechatClick}>
<div className="p-3 bg-white hover:shadow-md transition-all rounded-lg">
<div className="flex flex-col">
<span className="text-xs text-gray-500 mb-1"></span>
<div className="flex items-center justify-between">
<span className="text-lg font-bold text-blue-600">
{dashboard.wechatNum || 42}
</span>
<Users className="w-5 h-5 text-blue-600" />
</div>
</div>
<div className="h-2"></div>
</div>
</div>
<div className="p-3 bg-white rounded-lg">
<div className="flex flex-col">
<span className="text-xs text-gray-500 mb-1">线</span>
<div className="flex items-center justify-between mb-1">
<span className="text-lg font-bold text-blue-600">
{dashboard.aliveWechatNum || 35}
</span>
<Activity className="w-5 h-5 text-blue-600" />
</div>
<div className="h-1 bg-gray-200 rounded-full">
<div
className="h-1 bg-blue-600 rounded-full"
style={{
width: `${
dashboard.wechatNum > 0
? (dashboard.aliveWechatNum / dashboard.wechatNum) *
100
: 0
}%`,
}}
></div>
</div>
</div>
</div>
</div> </div>
</div>
{/* 今日数据 */} {/* 场景获客统计 */}
<div className={style["section"]}> <div className="p-4 bg-white rounded-lg">
<div className={style["section-header"]}> <div className="flex justify-between items-center mb-3">
<span className={style["section-title"]}></span> <h2 className="text-base font-semibold"></h2>
</div>
<div className="flex justify-between">
{scenarioFeatures
.sort((a, b) => b.value - a.value)
.slice(0, 4) // 只显示前4个
.map((scenario) => (
<div
key={scenario.id}
className="block flex-1 cursor-pointer"
>
<div className="flex flex-col items-center text-center space-y-1">
<div
className={`w-10 h-10 rounded-full ${scenario.color} flex items-center justify-center`}
>
<img
src={scenario.icon || "/placeholder.svg"}
alt={scenario.name}
className="w-5 h-5"
/>
</div>
<div className="text-sm font-medium">
{scenario.value}
</div>
<div className="text-xs text-gray-500 whitespace-nowrap overflow-hidden text-ellipsis w-full">
{scenario.name}
</div>
</div>
</div>
))}
</div>
</div> </div>
<div className={style["today-grid"]}>
{todayStats.map((item) => ( {/* 今日数据统计 */}
<div key={item.label} className={style["today-item"]}> <div className="p-4 bg-white rounded-lg">
<div className={style["today-icon"]}>{item.icon}</div> <div className="flex justify-between items-center mb-3">
<h2 className="text-base font-semibold"></h2>
</div>
<div className="grid grid-cols-2 gap-4">
{todayStatsData.map((stat, index) => (
<div <div
className={style["today-value"]} key={index}
style={{ color: item.color }} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100 transition-colors"
> >
{item.value} <div className={`p-2 rounded-full bg-white ${stat.color}`}>
{stat.icon}
</div>
<div>
<div className="text-lg font-semibold">{stat.value}</div>
<div className="text-xs text-gray-500">{stat.title}</div>
</div>
</div> </div>
<div className={style["today-label"]}>{item.label}</div> ))}
</div> </div>
))}
</div> </div>
</div>
{/* 趋势图表 */} {/* 趋势图表 - 保持原有实现 */}
<div className={style["section"]}> <div className={style["section"]}>
<div className={style["section-header"]}> <div className={style["section-header"]}>
<span className={style["section-title"]}></span> <span className={style["section-title"]}></span>
</div> </div>
<div className={style["chart-container"]}> <div className={style["chart-container"]}>
<LineChart <LineChart
xData={sevenDayStats.date} xData={sevenDayStats.date}
yData={sevenDayStats.allNum} yData={sevenDayStats.allNum}
/> />
</div>
</div> </div>
</div> </div>
</div> </div>
</Layout> </Layout>
); );
}; };
export default Home; export default Home;

View File

@@ -1,27 +1,32 @@
import request from "@/api/request"; import request from "@/api/request";
import { PlanDetail, PlanListResponse, ApiResponse } from "./data";
// ==================== 计划相关接口 ==================== // ==================== 计划相关接口 ====================
// 获取计划列表 // 获取计划列表
export function getPlanList(params: any) { export function getPlanList(params: {
sceneId: string;
page: number;
pageSize: number;
}): Promise<PlanListResponse> {
return request(`/v1/plan/list`, params, "GET"); return request(`/v1/plan/list`, params, "GET");
} }
// 获取计划详情 // 获取计划详情
export function getPlanDetail(planId: string) { export function getPlanDetail(planId: string): Promise<PlanDetail> {
return request(`/api/scenarios/plans/${planId}`, {}, "GET"); return request(`/v1/plan/detail`, { planId }, "GET");
} }
// 复制计划 // 复制计划
export function copyPlan(planId: string) { export function copyPlan(planId: string): Promise<ApiResponse<any>> {
return request(`/api/scenarios/plans/${planId}/copy`, {}, "POST"); return request(`/v1/plan/copy`, { planId }, "GET");
} }
// 删除计划 // 删除计划
export function deletePlan(planId: string) { export function deletePlan(planId: string): Promise<ApiResponse<any>> {
return request(`/api/scenarios/plans/${planId}`, {}, "DELETE"); return request(`/v1/plan/delete`, { planId }, "DELETE");
} }
// 获取小程序二维码 // 获取小程序二维码
export function getWxMinAppCode(planId: string) { export function getWxMinAppCode(planId: string): Promise<ApiResponse<string>> {
return request(`/api/scenarios/plans/${planId}/qrcode`, {}, "GET"); return request(`/v1/plan/getWxMinAppCode`, { taskId: planId }, "GET");
} }

View File

@@ -27,3 +27,33 @@ export interface ApiSettings {
webhookUrl: string; webhookUrl: string;
taskId: string; taskId: string;
} }
// API响应相关类型
export interface TextUrl {
apiKey: string;
originalString?: string;
sign?: string;
fullUrl: string;
}
export interface PlanDetail {
id: number;
name: string;
scenario: number;
enabled: boolean;
status: number;
apiKey: string;
textUrl: TextUrl;
[key: string]: any;
}
export interface ApiResponse<T> {
code: number;
msg?: string;
data: T;
}
export interface PlanListResponse {
list: Task[];
total: number;
}

View File

@@ -172,6 +172,67 @@
border-radius: 20px; border-radius: 20px;
} }
// 加载更多按钮样式
.load-more-container {
display: flex;
justify-content: center;
padding: 20px 0;
}
.load-more-btn {
height: 44px;
padding: 0 32px;
border-radius: 22px;
font-size: 16px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
&:active {
transform: translateY(0);
}
}
// 没有更多数据提示样式
.no-more-data {
display: flex;
justify-content: center;
align-items: center;
padding: 20px 0;
color: #999;
font-size: 14px;
span {
position: relative;
padding: 0 20px;
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
width: 40px;
height: 1px;
background-color: #e0e0e0;
}
&::before {
left: -50px;
}
&::after {
right: -50px;
}
}
}
.action-menu-dialog { .action-menu-dialog {
background: white; background: white;
border-radius: 16px 16px 0 0; border-radius: 16px 16px 0 0;
@@ -270,12 +331,61 @@
} }
} }
// API设置相关样式
.api-tip {
margin-top: 12px;
padding: 12px;
background-color: #fff7e6;
border: 1px solid #ffd591;
border-radius: 8px;
font-size: 12px;
color: #d46b08;
line-height: 1.5;
}
.api-params {
margin-top: 16px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.param-section {
background-color: #f8f9fa;
border-radius: 8px;
padding: 12px;
border: 1px solid #e9ecef;
h4 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
}
.param-list {
font-size: 12px;
color: #666;
line-height: 1.6;
div {
margin-bottom: 4px;
}
code {
background-color: #e9ecef;
padding: 2px 4px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 11px;
}
}
.qr-dialog { .qr-dialog {
background: white; background: white;
border-radius: 16px; border-radius: 16px;
padding: 20px; padding: 20px;
width: 300px;
max-width: 90vw;
} }
.qr-loading { .qr-loading {

View File

@@ -1,8 +1,7 @@
import React, { useEffect, useState, useCallback } from "react"; import React, { useEffect, useState } from "react";
import { useParams, useNavigate, useSearchParams } from "react-router-dom"; import { useParams, useNavigate } from "react-router-dom";
import { import {
NavBar, NavBar,
List,
Button, Button,
Toast, Toast,
SpinLoading, SpinLoading,
@@ -10,13 +9,10 @@ import {
Popup, Popup,
Card, Card,
Tag, Tag,
Space,
} from "antd-mobile"; } from "antd-mobile";
import { Input } from "antd"; import { Input } from "antd";
import { import {
PlusOutlined, PlusOutlined,
UserOutlined,
CalendarOutlined,
CopyOutlined, CopyOutlined,
DeleteOutlined, DeleteOutlined,
SettingOutlined, SettingOutlined,
@@ -26,10 +22,10 @@ import {
EditOutlined, EditOutlined,
MoreOutlined, MoreOutlined,
ClockCircleOutlined, ClockCircleOutlined,
DownOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout"; import Layout from "@/components/Layout/Layout";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
import { import {
getPlanList, getPlanList,
getPlanDetail, getPlanDetail,
@@ -38,7 +34,7 @@ import {
getWxMinAppCode, getWxMinAppCode,
} from "./api"; } from "./api";
import style from "./index.module.scss"; import style from "./index.module.scss";
import { Task, ApiSettings } from "./data"; import { Task, ApiSettings, PlanDetail } from "./data";
const ScenarioList: React.FC = () => { const ScenarioList: React.FC = () => {
const { scenarioId, scenarioName } = useParams<{ const { scenarioId, scenarioName } = useParams<{
@@ -46,8 +42,6 @@ const ScenarioList: React.FC = () => {
scenarioName: string; scenarioName: string;
}>(); }>();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [pageTitle, setPageTitle] = useState<string>("");
const [tasks, setTasks] = useState<Task[]>([]); const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [showApiDialog, setShowApiDialog] = useState(false); const [showApiDialog, setShowApiDialog] = useState(false);
@@ -60,9 +54,16 @@ const ScenarioList: React.FC = () => {
const [loadingTasks, setLoadingTasks] = useState(false); const [loadingTasks, setLoadingTasks] = useState(false);
const [showQrDialog, setShowQrDialog] = useState(false); const [showQrDialog, setShowQrDialog] = useState(false);
const [qrLoading, setQrLoading] = useState(false); const [qrLoading, setQrLoading] = useState(false);
const [qrImg, setQrImg] = useState(""); const [qrImg, setQrImg] = useState<any>("");
const [showActionMenu, setShowActionMenu] = useState<string | null>(null); const [showActionMenu, setShowActionMenu] = useState<string | null>(null);
// 分页相关状态
const [currentPage, setCurrentPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [total, setTotal] = useState(0);
const pageSize = 20;
// 获取渠道中文名称 // 获取渠道中文名称
const getChannelName = (channel: string) => { const getChannelName = (channel: string) => {
const channelMap: Record<string, string> = { const channelMap: Record<string, string> = {
@@ -80,21 +81,62 @@ const ScenarioList: React.FC = () => {
return channelMap[channel] || `${channel}获客`; return channelMap[channel] || `${channel}获客`;
}; };
// 获取计划列表数据
const fetchPlanList = async (page: number, isLoadMore: boolean = false) => {
if (!scenarioId) return;
if (isLoadMore) {
setLoadingMore(true);
} else {
setLoadingTasks(true);
}
try {
const response = await getPlanList({
sceneId: scenarioId,
page: page,
pageSize: pageSize,
});
if (response && response.list) {
if (isLoadMore) {
// 加载更多时,追加数据
setTasks((prev) => [...prev, ...response.list]);
} else {
// 首次加载或刷新时,替换数据
setTasks(response.list);
}
// 更新分页信息
setTotal(response.total || 0);
setHasMore(response.list.length === pageSize);
setCurrentPage(page);
}
} catch (error) {
console.error("获取计划列表失败:", error);
if (!isLoadMore) {
setTasks([]);
}
Toast.show({
content: "获取数据失败",
position: "top",
});
} finally {
if (isLoadMore) {
setLoadingMore(false);
} else {
setLoadingTasks(false);
}
}
};
useEffect(() => { useEffect(() => {
const fetchScenarioData = async () => { const fetchScenarioData = async () => {
if (!scenarioId) return; if (!scenarioId) return;
setLoading(true); setLoading(true);
try { try {
// 获取计划列表 await fetchPlanList(1, false);
const response = await getPlanList({
sceneId: scenarioId,
page: 1,
limit: 20,
});
// 设置计划列表
setTasks(response.list);
} catch (error) { } catch (error) {
console.error("获取场景数据失败:", error); console.error("获取场景数据失败:", error);
setTasks([]); setTasks([]);
@@ -106,16 +148,30 @@ const ScenarioList: React.FC = () => {
fetchScenarioData(); fetchScenarioData();
}, [scenarioId]); }, [scenarioId]);
// 加载下一页数据
const handleLoadMore = async () => {
if (loadingMore || !hasMore) return;
await fetchPlanList(currentPage + 1, true);
};
const handleCopyPlan = async (taskId: string) => { const handleCopyPlan = async (taskId: string) => {
const taskToCopy = tasks.find((task) => task.id === taskId); const taskToCopy = tasks.find((task) => task.id === taskId);
if (!taskToCopy) return; if (!taskToCopy) return;
await copyPlan(taskId);
Toast.show({ try {
content: `已成功复制"${taskToCopy.name}"`, await copyPlan(taskId);
position: "top", Toast.show({
}); content: `已成功复制"${taskToCopy.name}"`,
// 刷新列表 position: "top",
handleRefresh(); });
// 刷新列表
handleRefresh();
} catch (error) {
Toast.show({
content: "复制失败,请重试",
position: "top",
});
}
}; };
const handleDeletePlan = async (taskId: string) => { const handleDeletePlan = async (taskId: string) => {
@@ -130,20 +186,13 @@ const ScenarioList: React.FC = () => {
if (result) { if (result) {
try { try {
const response = await deletePlan(taskId); await deletePlan(taskId);
if (response && response.code === 200) { Toast.show({
Toast.show({ content: "计划已删除",
content: "计划已删除", position: "top",
position: "top", });
}); // 刷新列表
// 刷新列表 handleRefresh();
handleRefresh();
} else {
Toast.show({
content: response?.msg || "删除失败",
position: "top",
});
}
} catch (error) { } catch (error) {
Toast.show({ Toast.show({
content: "删除失败,请重试", content: "删除失败,请重试",
@@ -155,12 +204,13 @@ const ScenarioList: React.FC = () => {
const handleOpenApiSettings = async (taskId: string) => { const handleOpenApiSettings = async (taskId: string) => {
try { try {
const response = await getPlanDetail(taskId); const response: PlanDetail = await getPlanDetail(taskId);
if (response && response.code === 200 && response.data) { if (response) {
const detail = response.data;
setCurrentApiSettings({ setCurrentApiSettings({
apiKey: detail.apiKey || "", apiKey: response.apiKey || "demo-api-key-123456",
webhookUrl: detail.webhookUrl || "", webhookUrl:
response.textUrl?.fullUrl ||
`https://api.example.com/webhook/${taskId}`,
taskId: taskId, taskId: taskId,
}); });
setShowApiDialog(true); setShowApiDialog(true);
@@ -188,17 +238,11 @@ const ScenarioList: React.FC = () => {
const handleShowQrCode = async (taskId: string) => { const handleShowQrCode = async (taskId: string) => {
setQrLoading(true); setQrLoading(true);
setShowQrDialog(true); setShowQrDialog(true);
setQrImg("");
try { try {
const response = await getWxMinAppCode(taskId); const response = await getWxMinAppCode(taskId);
if (response && response.code === 200 && response.data) { setQrImg(response);
setQrImg(response.data.qrCode || "");
} else {
Toast.show({
content: "获取二维码失败",
position: "top",
});
}
} catch (error) { } catch (error) {
Toast.show({ Toast.show({
content: "获取二维码失败", content: "获取二维码失败",
@@ -236,49 +280,16 @@ const ScenarioList: React.FC = () => {
}; };
const handleRefresh = async () => { const handleRefresh = async () => {
setLoadingTasks(true); // 重置分页状态
try { setCurrentPage(1);
const response = await getPlanList({ setHasMore(true);
sceneId: scenarioId!, await fetchPlanList(1, false);
page: 1,
limit: 20,
});
setTasks(response.list);
} catch (error) {
Toast.show({
content: "刷新失败",
position: "top",
});
} finally {
setLoadingTasks(false);
}
}; };
const filteredTasks = tasks.filter((task) => const filteredTasks = tasks.filter((task) =>
task.name.toLowerCase().includes(searchTerm.toLowerCase()) 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) => [ const getActionMenu = (task: Task) => [
{ {
@@ -398,61 +409,98 @@ const ScenarioList: React.FC = () => {
</Button> </Button>
</div> </div>
) : ( ) : (
filteredTasks.map((task) => ( <>
<Card key={task.id} className={style["plan-item"]}> {filteredTasks.map((task) => (
{/* 头部:标题、状态和操作菜单 */} <Card key={task.id} className={style["plan-item"]}>
<div className={style["plan-header"]}> {/* 头部:标题、状态和操作菜单 */}
<div className={style["plan-name"]}>{task.name}</div> <div className={style["plan-header"]}>
<div className={style["plan-header-right"]}> <div className={style["plan-name"]}>{task.name}</div>
<Tag color={getStatusColor(task.status)}> <div className={style["plan-header-right"]}>
{getStatusText(task.status)} <Tag color={getStatusColor(task.status)}>
</Tag> {getStatusText(task.status)}
<Button </Tag>
size="mini" <Button
fill="none" size="mini"
className={style["more-btn"]} fill="none"
onClick={() => setShowActionMenu(task.id)} className={style["more-btn"]}
> onClick={() => setShowActionMenu(task.id)}
<MoreOutlined /> >
</Button> <MoreOutlined />
</Button>
</div>
</div> </div>
</div>
{/* 统计数据网格 */} {/* 统计数据网格 */}
<div className={style["stats-grid"]}> <div className={style["stats-grid"]}>
<div className={style["stat-item"]}> <div className={style["stat-item"]}>
<div className={style["stat-label"]}></div> <div className={style["stat-label"]}></div>
<div className={style["stat-value"]}> <div className={style["stat-value"]}>
{deviceCount(task)} {deviceCount(task)}
</div>
</div>
<div className={style["stat-item"]}>
<div className={style["stat-label"]}></div>
<div className={style["stat-value"]}>
{task?.acquiredCount || 0}
</div>
</div>
<div className={style["stat-item"]}>
<div className={style["stat-label"]}></div>
<div className={style["stat-value"]}>
{task.addedCount || 0}
</div>
</div>
<div className={style["stat-item"]}>
<div className={style["stat-label"]}></div>
<div className={style["stat-value"]}>
{task.passRate}%
</div>
</div> </div>
</div> </div>
<div className={style["stat-item"]}>
<div className={style["stat-label"]}></div>
<div className={style["stat-value"]}>
{task?.acquiredCount || 0}
</div>
</div>
<div className={style["stat-item"]}>
<div className={style["stat-label"]}></div>
<div className={style["stat-value"]}>
{task.addedCount || 0}
</div>
</div>
<div className={style["stat-item"]}>
<div className={style["stat-label"]}></div>
<div className={style["stat-value"]}>{task.passRate}%</div>
</div>
</div>
{/* 底部:上次执行时间 */} {/* 底部:上次执行时间 */}
<div className={style["plan-footer"]}> <div className={style["plan-footer"]}>
<div className={style["last-execution"]}> <div className={style["last-execution"]}>
<ClockCircleOutlined /> <ClockCircleOutlined />
<span>: {task.lastUpdated || "--"}</span> <span>: {task.lastUpdated || "--"}</span>
</div>
</div> </div>
</Card>
))}
{/* 加载更多按钮 */}
{hasMore && (
<div className={style["load-more-container"]}>
<Button
color="primary"
fill="outline"
size="large"
onClick={handleLoadMore}
loading={loadingMore}
className={style["load-more-btn"]}
>
{loadingMore ? (
<>
<SpinLoading color="primary" />
...
</>
) : (
<>
<DownOutlined />
</>
)}
</Button>
</div> </div>
</Card> )}
))
{/* 没有更多数据提示 */}
{!hasMore && filteredTasks.length > 0 && (
<div className={style["no-more-data"]}>
<span></span>
</div>
)}
</>
)} )}
</div> </div>
@@ -461,7 +509,7 @@ const ScenarioList: React.FC = () => {
visible={showApiDialog} visible={showApiDialog}
onMaskClick={() => setShowApiDialog(false)} onMaskClick={() => setShowApiDialog(false)}
position="bottom" position="bottom"
bodyStyle={{ height: "60vh" }} bodyStyle={{ height: "70vh" }}
> >
<div className={style["api-dialog"]}> <div className={style["api-dialog"]}>
<div className={style["dialog-header"]}> <div className={style["dialog-header"]}>
@@ -482,6 +530,10 @@ const ScenarioList: React.FC = () => {
</Button> </Button>
</div> </div>
<div className={style["api-tip"]}>
<strong></strong>
API密钥使
</div>
</div> </div>
<div className={style["api-item"]}> <div className={style["api-item"]}>
<label>Webhook URL:</label> <label>Webhook URL:</label>
@@ -496,6 +548,33 @@ const ScenarioList: React.FC = () => {
</Button> </Button>
</div> </div>
<div className={style["api-params"]}>
<div className={style["param-section"]}>
<h4></h4>
<div className={style["param-list"]}>
<div>
<code>name</code> -
</div>
<div>
<code>phone</code> -
</div>
</div>
</div>
<div className={style["param-section"]}>
<h4></h4>
<div className={style["param-list"]}>
<div>
<code>source</code> -
</div>
<div>
<code>remark</code> -
</div>
<div>
<code>tags</code> -
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>