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

View File

@@ -6,6 +6,12 @@ import {
ClockCircleOutline,
SendOutline,
StarOutline,
Bell,
Smartphone,
Users,
Activity,
MessageSquare,
TrendingUp,
} from "antd-mobile-icons";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
import Layout from "@/components/Layout/Layout";
@@ -23,160 +29,358 @@ const Home: React.FC = () => {
const [todayStats, setTodayStats] = useState<any[]>([]);
const [dashboard, setDashboard] = 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(() => {
getDashboard().then((res: any) => {
setDashboard(res);
});
getPlanStats({ num: 4 }).then((res: any) => {
setSceneStats(res);
});
getSevenDayStats().then((res: any) => {
setSevenDayStats(res);
});
getTodayStats().then((res: any) => {
const todayStatsData = [
{
label: "同步朋友圈",
value: res.momentsNum,
icon: (
<ClockCircleOutline style={{ fontSize: 16, color: "#ff6b35" }} />
),
color: "#ff6b35",
},
{
label: "群发任务",
value: res.groupPushNum,
icon: <SendOutline style={{ fontSize: 16, color: "#ffd700" }} />,
color: "#ffd700",
},
{
label: "获客转化率",
value: res.passRate,
icon: <StarOutline style={{ fontSize: 16, color: "#4caf50" }} />,
color: "#4caf50",
},
{
label: "系统活跃度",
value: res.sysActive,
icon: (
<ClockCircleOutline style={{ fontSize: 16, color: "#2196f3" }} />
),
color: "#2196f3",
},
];
setTodayStats(todayStatsData);
});
const fetchData = async () => {
try {
setIsLoading(true);
setApiError("");
// 并行请求多个接口
const [dashboardResult, planStatsResult, sevenDayResult, todayResult] =
await Promise.allSettled([
getDashboard(),
getPlanStats({ num: 4 }),
getSevenDayStats(),
getTodayStats(),
]);
// 处理仪表板数据
if (dashboardResult.status === "fulfilled") {
setDashboard(dashboardResult.value);
} else {
console.warn("仪表板API失败:", dashboardResult.reason);
setApiError("API连接异常显示默认数据");
}
// 处理计划统计数据
if (planStatsResult.status === "fulfilled") {
setSceneStats(planStatsResult.value);
} else {
console.warn("计划统计API失败:", planStatsResult.reason);
}
// 处理七天统计数据
if (sevenDayResult.status === "fulfilled") {
setSevenDayStats(sevenDayResult.value);
} else {
console.warn("七天统计API失败:", sevenDayResult.reason);
}
// 处理今日统计数据
if (todayResult.status === "fulfilled") {
const 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 (
<Layout
header={
<NavBar back={null} style={{ background: "#fff" }}>
<div className={style["nav-title"]}>
<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>
</NavBar>
}
footer={<MeauMobile />}
>
<div className={style["home-page"]}>
{/* 顶部统计卡片 */}
<div className={style["stats-grid"]}>
<div className={style["stat-card"]}>
<div className={style["stat-icon"]}>
<AppOutline
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.deviceNum}</div>
</div>
</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 className="bg-gray-50">
<div className="p-4 space-y-4">
{/* 统计卡片 */}
<div className="grid grid-cols-3 gap-3">
<div className="cursor-pointer" onClick={handleDevicesClick}>
<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.deviceNum || 42}
</span>
<Smartphone className="w-5 h-5 text-blue-600" />
</div>
<div className="h-2"></div>
</div>
<div className={style["scene-value"]}>{item.allNum}</div>
<div className={style["scene-label"]}>{item.name}</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 className={style["section"]}>
<div className={style["section-header"]}>
<span className={style["section-title"]}></span>
{/* 场景获客统计 */}
<div className="p-4 bg-white rounded-lg">
<div className="flex justify-between items-center mb-3">
<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 className={style["today-grid"]}>
{todayStats.map((item) => (
<div key={item.label} className={style["today-item"]}>
<div className={style["today-icon"]}>{item.icon}</div>
{/* 今日数据统计 */}
<div className="p-4 bg-white rounded-lg">
<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
className={style["today-value"]}
style={{ color: item.color }}
key={index}
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 className={style["today-label"]}>{item.label}</div>
</div>
))}
))}
</div>
</div>
</div>
{/* 趋势图表 */}
<div className={style["section"]}>
<div className={style["section-header"]}>
<span className={style["section-title"]}></span>
</div>
<div className={style["chart-container"]}>
<LineChart
xData={sevenDayStats.date}
yData={sevenDayStats.allNum}
/>
{/* 趋势图表 - 保持原有实现 */}
<div className={style["section"]}>
<div className={style["section-header"]}>
<span className={style["section-title"]}></span>
</div>
<div className={style["chart-container"]}>
<LineChart
xData={sevenDayStats.date}
yData={sevenDayStats.allNum}
/>
</div>
</div>
</div>
</div>
</Layout>
);
};
export default Home;

View File

@@ -1,27 +1,32 @@
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");
}
// 获取计划详情
export function getPlanDetail(planId: string) {
return request(`/api/scenarios/plans/${planId}`, {}, "GET");
export function getPlanDetail(planId: string): Promise<PlanDetail> {
return request(`/v1/plan/detail`, { planId }, "GET");
}
// 复制计划
export function copyPlan(planId: string) {
return request(`/api/scenarios/plans/${planId}/copy`, {}, "POST");
export function copyPlan(planId: string): Promise<ApiResponse<any>> {
return request(`/v1/plan/copy`, { planId }, "GET");
}
// 删除计划
export function deletePlan(planId: string) {
return request(`/api/scenarios/plans/${planId}`, {}, "DELETE");
export function deletePlan(planId: string): Promise<ApiResponse<any>> {
return request(`/v1/plan/delete`, { planId }, "DELETE");
}
// 获取小程序二维码
export function getWxMinAppCode(planId: string) {
return request(`/api/scenarios/plans/${planId}/qrcode`, {}, "GET");
export function getWxMinAppCode(planId: string): Promise<ApiResponse<string>> {
return request(`/v1/plan/getWxMinAppCode`, { taskId: planId }, "GET");
}

View File

@@ -27,3 +27,33 @@ export interface ApiSettings {
webhookUrl: 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;
}
// 加载更多按钮样式
.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 {
background: white;
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 {
background: white;
border-radius: 16px;
padding: 20px;
width: 300px;
max-width: 90vw;
}
.qr-loading {

View File

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