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,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>