feat: 本次提交更新内容如下
存一波
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user