feat: 本次提交更新内容如下
梳理了icon ,增加了layout的loading
This commit is contained in:
250
nkebao/src/pages/scenarios/ScenarioList.module.scss
Normal file
250
nkebao/src/pages/scenarios/ScenarioList.module.scss
Normal file
@@ -0,0 +1,250 @@
|
||||
.scenario-list-page {
|
||||
padding: 16px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.new-plan-btn {
|
||||
font-size: 14px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60vh;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.scenario-card {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.scenario-info {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.scenario-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.scenario-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.scenario-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
|
||||
svg {
|
||||
font-size: 16px;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
align-items: center;
|
||||
|
||||
.adm-input {
|
||||
flex: 1;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.plan-list {
|
||||
background: transparent;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.plan-item {
|
||||
background: white;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.plan-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.plan-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.plan-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.plan-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.plan-actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.create-first-btn {
|
||||
height: 40px;
|
||||
padding: 0 24px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.api-dialog {
|
||||
background: white;
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.api-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.adm-input {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-dialog {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
width: 300px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.qr-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
gap: 16px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.qr-image {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.qr-error {
|
||||
text-align: center;
|
||||
color: #ff4d4f;
|
||||
font-size: 14px;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
@@ -1,8 +1,618 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const ScenarioList: React.FC = () => {
|
||||
return <PlaceholderPage title="场景列表" />;
|
||||
};
|
||||
|
||||
export default ScenarioList;
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import { useParams, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
NavBar,
|
||||
List,
|
||||
Button,
|
||||
Toast,
|
||||
SpinLoading,
|
||||
Dialog,
|
||||
Input,
|
||||
Popup,
|
||||
Card,
|
||||
Tag,
|
||||
Space,
|
||||
Divider,
|
||||
} from "antd-mobile";
|
||||
import {
|
||||
PlusOutlined,
|
||||
UserOutlined,
|
||||
CalendarOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
SettingOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
QrcodeOutlined,
|
||||
UpOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
import {
|
||||
getPlanList,
|
||||
getPlanDetail,
|
||||
copyPlan,
|
||||
deletePlan,
|
||||
getWxMinAppCode,
|
||||
} from "./api";
|
||||
import style from "./ScenarioList.module.scss";
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
name: string;
|
||||
status: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
enabled: boolean;
|
||||
total_customers?: number;
|
||||
today_customers?: 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;
|
||||
}
|
||||
|
||||
const ScenarioList: React.FC = () => {
|
||||
const { scenarioId, scenarioName } = useParams<{
|
||||
scenarioId: string;
|
||||
scenarioName: string;
|
||||
}>();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [scenario, setScenario] = useState<ScenarioData | null>(null);
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [showApiDialog, setShowApiDialog] = useState(false);
|
||||
const [currentApiSettings, setCurrentApiSettings] = useState<ApiSettings>({
|
||||
apiKey: "",
|
||||
webhookUrl: "",
|
||||
taskId: "",
|
||||
});
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [loadingTasks, setLoadingTasks] = useState(false);
|
||||
const [showQrDialog, setShowQrDialog] = useState(false);
|
||||
const [qrLoading, setQrLoading] = useState(false);
|
||||
const [qrImg, setQrImg] = useState("");
|
||||
|
||||
// 获取渠道中文名称
|
||||
const getChannelName = (channel: string) => {
|
||||
const channelMap: Record<string, string> = {
|
||||
douyin: "抖音直播获客",
|
||||
kuaishou: "快手直播获客",
|
||||
xiaohongshu: "小红书种草获客",
|
||||
weibo: "微博话题获客",
|
||||
haibao: "海报扫码获客",
|
||||
phone: "电话号码获客",
|
||||
gongzhonghao: "公众号引流获客",
|
||||
weixinqun: "微信群裂变获客",
|
||||
payment: "付款码获客",
|
||||
api: "API接口获客",
|
||||
};
|
||||
return channelMap[channel] || `${channel}获客`;
|
||||
};
|
||||
|
||||
// 获取场景描述
|
||||
const getScenarioDescription = (channel: string) => {
|
||||
const descriptions: Record<string, string> = {
|
||||
douyin: "通过抖音平台进行精准获客,利用短视频内容吸引目标用户",
|
||||
xiaohongshu: "利用小红书平台进行内容营销获客,通过优质内容建立品牌形象",
|
||||
gongzhonghao: "通过微信公众号进行获客,建立私域流量池",
|
||||
haibao: "通过海报分享进行获客,快速传播品牌信息",
|
||||
phone: "通过电话营销进行获客,直接与客户沟通",
|
||||
weixinqun: "通过微信群进行获客,利用社交裂变效应",
|
||||
payment: "通过付款码进行获客,便捷的支付方式",
|
||||
api: "通过API接口进行获客,支持第三方系统集成",
|
||||
};
|
||||
return descriptions[channel] || "通过该平台进行获客";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchScenarioData = async () => {
|
||||
if (!scenarioId) return;
|
||||
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
// 获取计划列表
|
||||
const response = await getPlanList(scenarioId, 1, 20);
|
||||
|
||||
// 设置计划列表
|
||||
if (response && response.data && response.data.list) {
|
||||
setTasks(response.data.list);
|
||||
} else {
|
||||
setTasks([]);
|
||||
}
|
||||
|
||||
// 构建场景数据
|
||||
const scenarioData: ScenarioData = {
|
||||
id: scenarioId,
|
||||
name: scenarioName || "",
|
||||
image: "",
|
||||
description: getScenarioDescription(scenarioId),
|
||||
totalPlans: response?.data?.list?.length || 0,
|
||||
totalCustomers: 0,
|
||||
todayCustomers: 0,
|
||||
growth: "",
|
||||
};
|
||||
|
||||
setScenario(scenarioData);
|
||||
} 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([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchScenarioData();
|
||||
}, [scenarioId]);
|
||||
|
||||
// 获取场景名称
|
||||
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]);
|
||||
|
||||
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",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletePlan = async (taskId: string) => {
|
||||
const taskToDelete = tasks.find((task) => task.id === taskId);
|
||||
if (!taskToDelete) return;
|
||||
|
||||
const result = await Dialog.confirm({
|
||||
content: `确定要删除"${taskToDelete.name}"吗?`,
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
});
|
||||
|
||||
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",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.show({
|
||||
content: "删除失败,请重试",
|
||||
position: "top",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenApiSettings = async (taskId: string) => {
|
||||
try {
|
||||
const response = await getPlanDetail(taskId);
|
||||
if (response && response.code === 200 && response.data) {
|
||||
const detail = response.data;
|
||||
setCurrentApiSettings({
|
||||
apiKey: detail.apiKey || "",
|
||||
webhookUrl: detail.webhookUrl || "",
|
||||
taskId: taskId,
|
||||
});
|
||||
setShowApiDialog(true);
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.show({
|
||||
content: "获取API设置失败",
|
||||
position: "top",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyApiUrl = (url: string) => {
|
||||
navigator.clipboard.writeText(url);
|
||||
Toast.show({
|
||||
content: "API地址已复制到剪贴板",
|
||||
position: "top",
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateNewPlan = () => {
|
||||
navigate(`/scenarios/new/${scenarioId}`);
|
||||
};
|
||||
|
||||
const handleShowQrCode = async (taskId: string) => {
|
||||
setQrLoading(true);
|
||||
setShowQrDialog(true);
|
||||
|
||||
try {
|
||||
const response = await getWxMinAppCode(taskId);
|
||||
if (response && response.code === 200 && response.data) {
|
||||
setQrImg(response.data.qrCode || "");
|
||||
} else {
|
||||
Toast.show({
|
||||
content: "获取二维码失败",
|
||||
position: "top",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.show({
|
||||
content: "获取二维码失败",
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
setQrLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: number) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return "success";
|
||||
case 0:
|
||||
return "default";
|
||||
case -1:
|
||||
return "danger";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return "运行中";
|
||||
case 0:
|
||||
return "已暂停";
|
||||
case -1:
|
||||
return "已停止";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setLoadingTasks(true);
|
||||
try {
|
||||
const response = await getPlanList(scenarioId!, 1, 20);
|
||||
if (response && response.data && response.data.list) {
|
||||
setTasks(response.data.list);
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.show({
|
||||
content: "刷新失败",
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
setLoadingTasks(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredTasks = tasks.filter((task) =>
|
||||
task.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar back={null} style={{ background: "#fff" }}>
|
||||
<div className={style["nav-title"]}>场景详情</div>
|
||||
</NavBar>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div className={style["loading"]}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||
<div className={style["loading-text"]}>加载场景数据中...</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar
|
||||
back={null}
|
||||
style={{ background: "#fff" }}
|
||||
left={<div className={style["nav-title"]}>{scenario?.name}</div>}
|
||||
right={
|
||||
<Button
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={handleCreateNewPlan}
|
||||
className={style["new-plan-btn"]}
|
||||
>
|
||||
<PlusOutlined /> 新建计划
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div className={style["scenario-list-page"]}>
|
||||
{/* 场景信息卡片 */}
|
||||
{scenario && (
|
||||
<Card className={style["scenario-card"]}>
|
||||
<div className={style["scenario-info"]}>
|
||||
<div className={style["scenario-name"]}>{scenario.name}</div>
|
||||
<div className={style["scenario-desc"]}>
|
||||
{scenario.description}
|
||||
</div>
|
||||
<div className={style["scenario-stats"]}>
|
||||
<div className={style["stat-item"]}>
|
||||
<UserOutlined />
|
||||
<span>计划数: {scenario.totalPlans}</span>
|
||||
</div>
|
||||
<div className={style["stat-item"]}>
|
||||
<CalendarOutlined />
|
||||
<span>今日获客: {scenario.todayCustomers}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 搜索栏 */}
|
||||
<div className={style["search-bar"]}>
|
||||
<div className={style["search-input-wrapper"]}>
|
||||
<SearchOutlined className={style["search-icon"]} />
|
||||
<Input
|
||||
placeholder="搜索计划名称"
|
||||
value={searchTerm}
|
||||
onChange={setSearchTerm}
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleRefresh}
|
||||
loading={loadingTasks}
|
||||
className={style["refresh-btn"]}
|
||||
>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 计划列表 */}
|
||||
<List className={style["plan-list"]}>
|
||||
{filteredTasks.length === 0 ? (
|
||||
<div className={style["empty-state"]}>
|
||||
<div className={style["empty-text"]}>
|
||||
{searchTerm ? "没有找到匹配的计划" : "暂无计划"}
|
||||
</div>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={handleCreateNewPlan}
|
||||
className={style["create-first-btn"]}
|
||||
>
|
||||
<PlusOutlined /> 创建第一个计划
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
filteredTasks.map((task) => (
|
||||
<List.Item
|
||||
key={task.id}
|
||||
className={style["plan-item"]}
|
||||
onClick={() => navigate(`/scenarios/edit/${task.id}`)}
|
||||
>
|
||||
<div className={style["plan-content"]}>
|
||||
<div className={style["plan-header"]}>
|
||||
<div className={style["plan-name"]}>{task.name}</div>
|
||||
<Tag color={getStatusColor(task.status)}>
|
||||
{getStatusText(task.status)}
|
||||
</Tag>
|
||||
</div>
|
||||
<div className={style["plan-meta"]}>
|
||||
<span>创建时间: {task.created_at}</span>
|
||||
{task.total_customers !== undefined && (
|
||||
<span>总获客: {task.total_customers}</span>
|
||||
)}
|
||||
{task.today_customers !== undefined && (
|
||||
<span>今日获客: {task.today_customers}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["plan-actions"]}>
|
||||
<Space>
|
||||
<Button
|
||||
size="mini"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCopyPlan(task.id);
|
||||
}}
|
||||
>
|
||||
<CopyOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
size="mini"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleShowQrCode(task.id);
|
||||
}}
|
||||
>
|
||||
<QrcodeOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
size="mini"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOpenApiSettings(task.id);
|
||||
}}
|
||||
>
|
||||
<SettingOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
size="mini"
|
||||
color="danger"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeletePlan(task.id);
|
||||
}}
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</List.Item>
|
||||
))
|
||||
)}
|
||||
</List>
|
||||
|
||||
{/* API设置弹窗 */}
|
||||
<Popup
|
||||
visible={showApiDialog}
|
||||
onMaskClick={() => setShowApiDialog(false)}
|
||||
position="bottom"
|
||||
bodyStyle={{ height: "60vh" }}
|
||||
>
|
||||
<div className={style["api-dialog"]}>
|
||||
<div className={style["dialog-header"]}>
|
||||
<h3>API设置</h3>
|
||||
<Button size="small" onClick={() => setShowApiDialog(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
<div className={style["dialog-content"]}>
|
||||
<div className={style["api-item"]}>
|
||||
<label>API Key:</label>
|
||||
<div className={style["input-with-button"]}>
|
||||
<Input value={currentApiSettings.apiKey} readOnly />
|
||||
<Button
|
||||
size="mini"
|
||||
onClick={() => handleCopyApiUrl(currentApiSettings.apiKey)}
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["api-item"]}>
|
||||
<label>Webhook URL:</label>
|
||||
<div className={style["input-with-button"]}>
|
||||
<Input value={currentApiSettings.webhookUrl} readOnly />
|
||||
<Button
|
||||
size="mini"
|
||||
onClick={() =>
|
||||
handleCopyApiUrl(currentApiSettings.webhookUrl)
|
||||
}
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
{/* 二维码弹窗 */}
|
||||
<Popup
|
||||
visible={showQrDialog}
|
||||
onMaskClick={() => setShowQrDialog(false)}
|
||||
position="bottom"
|
||||
>
|
||||
<div className={style["qr-dialog"]}>
|
||||
<div className={style["dialog-header"]}>
|
||||
<h3>小程序二维码</h3>
|
||||
<Button size="small" onClick={() => setShowQrDialog(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
<div className={style["dialog-content"]}>
|
||||
{qrLoading ? (
|
||||
<div className={style["qr-loading"]}>
|
||||
<SpinLoading color="primary" />
|
||||
<div>生成二维码中...</div>
|
||||
</div>
|
||||
) : qrImg ? (
|
||||
<img
|
||||
src={qrImg}
|
||||
alt="小程序二维码"
|
||||
className={style["qr-image"]}
|
||||
/>
|
||||
) : (
|
||||
<div className={style["qr-error"]}>二维码生成失败</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScenarioList;
|
||||
|
||||
51
nkebao/src/pages/scenarios/api.ts
Normal file
51
nkebao/src/pages/scenarios/api.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import request from "@/api/request";
|
||||
|
||||
// 获取计划列表
|
||||
export const getPlanList = async (scenarioId: string, page: number = 1, limit: number = 20) => {
|
||||
return request(`/api/scenarios/${scenarioId}/plans`, { page, limit }, 'GET');
|
||||
};
|
||||
|
||||
// 获取计划详情
|
||||
export const getPlanDetail = async (planId: string) => {
|
||||
return request(`/api/scenarios/plans/${planId}`, undefined, 'GET');
|
||||
};
|
||||
|
||||
// 复制计划
|
||||
export const copyPlan = async (planId: string) => {
|
||||
return request(`/api/scenarios/plans/${planId}/copy`, undefined, 'POST');
|
||||
};
|
||||
|
||||
// 删除计划
|
||||
export const deletePlan = async (planId: string) => {
|
||||
return request(`/api/scenarios/plans/${planId}`, undefined, 'DELETE');
|
||||
};
|
||||
|
||||
// 创建计划
|
||||
export const createPlan = async (data: any) => {
|
||||
return request('/api/scenarios/plans', data, 'POST');
|
||||
};
|
||||
|
||||
// 更新计划
|
||||
export const updatePlan = async (planId: string, data: any) => {
|
||||
return request(`/api/scenarios/plans/${planId}`, data, 'PUT');
|
||||
};
|
||||
|
||||
// 获取小程序二维码
|
||||
export const getWxMinAppCode = async (planId: string) => {
|
||||
return request(`/api/scenarios/plans/${planId}/qrcode`, undefined, 'GET');
|
||||
};
|
||||
|
||||
// 获取场景类型列表
|
||||
export const getScenarioTypes = async () => {
|
||||
return request('/api/scenarios/types', undefined, 'GET');
|
||||
};
|
||||
|
||||
// 获取设备列表
|
||||
export const getDevices = async () => {
|
||||
return request('/api/devices', undefined, 'GET');
|
||||
};
|
||||
|
||||
// 获取海报列表
|
||||
export const getPosters = async () => {
|
||||
return request('/api/posters', undefined, 'GET');
|
||||
};
|
||||
43
nkebao/src/pages/scenarios/new/page.module.scss
Normal file
43
nkebao/src/pages/scenarios/new/page.module.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
.new-plan-page {
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60vh;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.steps-container {
|
||||
background: white;
|
||||
padding: 20px 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
padding: 0 16px;
|
||||
}
|
||||
@@ -1,8 +1,293 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const NewPlan: React.FC = () => {
|
||||
return <PlaceholderPage title="新建场景" />;
|
||||
};
|
||||
|
||||
export default NewPlan;
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { NavBar, Button, Toast, SpinLoading, Steps, Popup } from "antd-mobile";
|
||||
import { LeftOutline } from "antd-mobile-icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
import BasicSettings from "./steps/BasicSettings";
|
||||
import FriendRequestSettings from "./steps/FriendRequestSettings";
|
||||
import MessageSettings from "./steps/MessageSettings";
|
||||
import {
|
||||
getScenarioTypes,
|
||||
createPlan,
|
||||
updatePlan,
|
||||
getPlanDetail,
|
||||
} from "../api";
|
||||
import style from "./page.module.scss";
|
||||
|
||||
// 步骤定义
|
||||
const steps = [
|
||||
{ id: 1, title: "步骤一", subtitle: "基础设置" },
|
||||
{ id: 2, title: "步骤二", subtitle: "好友申请设置" },
|
||||
{ id: 3, title: "步骤三", subtitle: "消息设置" },
|
||||
];
|
||||
|
||||
// 类型定义
|
||||
interface FormData {
|
||||
name: string;
|
||||
scenario: number;
|
||||
posters: any[];
|
||||
device: string[];
|
||||
remarkType: string;
|
||||
greeting: string;
|
||||
addInterval: number;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
enabled: boolean;
|
||||
sceneId: string | number;
|
||||
remarkFormat: string;
|
||||
addFriendInterval: number;
|
||||
}
|
||||
|
||||
const NewPlan: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: "",
|
||||
scenario: 1,
|
||||
posters: [],
|
||||
device: [],
|
||||
remarkType: "phone",
|
||||
greeting: "你好,请通过",
|
||||
addInterval: 1,
|
||||
startTime: "09:00",
|
||||
endTime: "18:00",
|
||||
enabled: true,
|
||||
sceneId: "",
|
||||
remarkFormat: "",
|
||||
addFriendInterval: 1,
|
||||
});
|
||||
const [sceneList, setSceneList] = useState<any[]>([]);
|
||||
const [sceneLoading, setSceneLoading] = useState(true);
|
||||
const { scenarioId, planId } = useParams<{
|
||||
scenarioId: string;
|
||||
planId: string;
|
||||
}>();
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
setSceneLoading(true);
|
||||
try {
|
||||
// 获取场景类型
|
||||
const res = await getScenarioTypes();
|
||||
if (res?.data) {
|
||||
setSceneList(res.data);
|
||||
}
|
||||
|
||||
if (planId) {
|
||||
setIsEdit(true);
|
||||
// 获取计划详情
|
||||
const detailRes = await getPlanDetail(planId);
|
||||
if (detailRes.code === 200 && detailRes.data) {
|
||||
const detail = detailRes.data;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
name: detail.name ?? "",
|
||||
scenario: Number(detail.scenario) || 1,
|
||||
posters: detail.posters ?? [],
|
||||
device: detail.device ?? [],
|
||||
remarkType: detail.remarkType ?? "phone",
|
||||
greeting: detail.greeting ?? "",
|
||||
addInterval: detail.addInterval ?? 1,
|
||||
startTime: detail.startTime ?? "09:00",
|
||||
endTime: detail.endTime ?? "18:00",
|
||||
enabled: detail.enabled ?? true,
|
||||
sceneId: Number(detail.scenario) || 1,
|
||||
remarkFormat: detail.remarkFormat ?? "",
|
||||
addFriendInterval: detail.addFriendInterval ?? 1,
|
||||
}));
|
||||
}
|
||||
} else if (scenarioId) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
scenario: Number(scenarioId) || 1,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.show({
|
||||
content: "加载数据失败",
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
setSceneLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新表单数据
|
||||
const onChange = (data: any) => {
|
||||
setFormData((prev) => ({ ...prev, ...data }));
|
||||
};
|
||||
|
||||
// 处理保存
|
||||
const handleSave = async () => {
|
||||
if (!formData.name.trim()) {
|
||||
Toast.show({
|
||||
content: "请输入计划名称",
|
||||
position: "top",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
let result;
|
||||
if (isEdit && planId) {
|
||||
// 编辑
|
||||
const editData = {
|
||||
...formData,
|
||||
id: Number(planId),
|
||||
planId: Number(planId),
|
||||
};
|
||||
result = await updatePlan(planId, editData);
|
||||
} else {
|
||||
// 新建
|
||||
result = await createPlan(formData);
|
||||
}
|
||||
|
||||
if (result.code === 200) {
|
||||
Toast.show({
|
||||
content: isEdit ? "计划已更新" : "获客计划已创建",
|
||||
position: "top",
|
||||
});
|
||||
const sceneItem = sceneList.find((v) => formData.scenario === v.id);
|
||||
navigate(
|
||||
`/scenarios/list/${formData.sceneId}/${sceneItem?.name || ""}`
|
||||
);
|
||||
} else {
|
||||
Toast.show({
|
||||
content: result.msg || "操作失败",
|
||||
position: "top",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.show({
|
||||
content: isEdit ? "更新计划失败,请重试" : "创建计划失败,请重试",
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 下一步
|
||||
const handleNext = () => {
|
||||
if (currentStep === steps.length) {
|
||||
handleSave();
|
||||
} else {
|
||||
setCurrentStep((prev) => prev + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 上一步
|
||||
const handlePrev = () => {
|
||||
setCurrentStep((prev) => Math.max(prev - 1, 1));
|
||||
};
|
||||
|
||||
// 渲染当前步骤内容
|
||||
const renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return (
|
||||
<BasicSettings
|
||||
formData={formData}
|
||||
onChange={onChange}
|
||||
onNext={handleNext}
|
||||
sceneList={sceneList}
|
||||
sceneLoading={sceneLoading}
|
||||
/>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<FriendRequestSettings
|
||||
formData={formData}
|
||||
onChange={onChange}
|
||||
onNext={handleNext}
|
||||
onPrev={handlePrev}
|
||||
/>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<MessageSettings
|
||||
formData={formData}
|
||||
onChange={onChange}
|
||||
onNext={handleNext}
|
||||
onPrev={handlePrev}
|
||||
saving={saving}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (sceneLoading) {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar back={null} style={{ background: "#fff" }}>
|
||||
<div className={style["nav-title"]}>
|
||||
{isEdit ? "编辑计划" : "新建计划"}
|
||||
</div>
|
||||
</NavBar>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div className={style["loading"]}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||
<div className={style["loading-text"]}>加载数据中...</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar
|
||||
back={null}
|
||||
style={{ background: "#fff" }}
|
||||
left={
|
||||
<div className={style["nav-title"]}>
|
||||
{isEdit ? "编辑计划" : "新建计划"}
|
||||
</div>
|
||||
}
|
||||
right={
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => navigate(-1)}
|
||||
className={style["back-btn"]}
|
||||
>
|
||||
<LeftOutline />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div className={style["new-plan-page"]}>
|
||||
{/* 步骤指示器 */}
|
||||
<div className={style["steps-container"]}>
|
||||
<Steps current={currentStep - 1}>
|
||||
{steps.map((step) => (
|
||||
<Steps.Step
|
||||
key={step.id}
|
||||
title={step.title}
|
||||
description={step.subtitle}
|
||||
/>
|
||||
))}
|
||||
</Steps>
|
||||
</div>
|
||||
|
||||
{/* 步骤内容 */}
|
||||
<div className={style["step-content"]}>{renderStepContent()}</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewPlan;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
.basic-settings {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.adm-form-item-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.adm-input {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.adm-selector {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.time-input {
|
||||
width: 120px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40vh;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
210
nkebao/src/pages/scenarios/new/steps/BasicSettings.tsx
Normal file
210
nkebao/src/pages/scenarios/new/steps/BasicSettings.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Selector,
|
||||
Button,
|
||||
SpinLoading,
|
||||
Toast,
|
||||
Card,
|
||||
Space,
|
||||
} from "antd-mobile";
|
||||
import { getDevices, getPosters } from "../../api";
|
||||
import style from "./BasicSettings.module.scss";
|
||||
|
||||
interface BasicSettingsProps {
|
||||
formData: any;
|
||||
onChange: (data: any) => void;
|
||||
onNext: () => void;
|
||||
sceneList: any[];
|
||||
sceneLoading: boolean;
|
||||
}
|
||||
|
||||
const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
formData,
|
||||
onChange,
|
||||
onNext,
|
||||
sceneList,
|
||||
sceneLoading,
|
||||
}) => {
|
||||
const [devices, setDevices] = useState<any[]>([]);
|
||||
const [posters, setPosters] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 获取设备列表
|
||||
const devicesRes = await getDevices();
|
||||
if (devicesRes?.data) {
|
||||
setDevices(devicesRes.data);
|
||||
}
|
||||
|
||||
// 获取海报列表
|
||||
const postersRes = await getPosters();
|
||||
if (postersRes?.data) {
|
||||
setPosters(postersRes.data);
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.show({
|
||||
content: "加载数据失败",
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (!formData.name.trim()) {
|
||||
Toast.show({
|
||||
content: "请输入计划名称",
|
||||
position: "top",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.scenario) {
|
||||
Toast.show({
|
||||
content: "请选择场景类型",
|
||||
position: "top",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.device.length === 0) {
|
||||
Toast.show({
|
||||
content: "请选择设备",
|
||||
position: "top",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onNext();
|
||||
};
|
||||
|
||||
if (loading || sceneLoading) {
|
||||
return (
|
||||
<div className={style["loading"]}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||
<div className={style["loading-text"]}>加载数据中...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={style["basic-settings"]}>
|
||||
<Card className={style["form-card"]}>
|
||||
<Form layout="vertical">
|
||||
{/* 计划名称 */}
|
||||
<Form.Item label="计划名称" required className={style["form-item"]}>
|
||||
<Input
|
||||
placeholder="请输入计划名称"
|
||||
value={formData.name}
|
||||
onChange={(value) => onChange({ name: value })}
|
||||
clearable
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 场景类型 */}
|
||||
<Form.Item label="场景类型" required className={style["form-item"]}>
|
||||
<Selector
|
||||
options={sceneList.map((scene) => ({
|
||||
label: scene.name,
|
||||
value: scene.id,
|
||||
}))}
|
||||
value={[formData.scenario]}
|
||||
onChange={(value) => {
|
||||
const selectedScene = sceneList.find(
|
||||
(scene) => scene.id === value[0]
|
||||
);
|
||||
onChange({
|
||||
scenario: value[0],
|
||||
sceneId: value[0],
|
||||
name: selectedScene?.name || "",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 选择设备 */}
|
||||
<Form.Item label="选择设备" required className={style["form-item"]}>
|
||||
<Selector
|
||||
options={devices.map((device) => ({
|
||||
label: device.name,
|
||||
value: device.id,
|
||||
}))}
|
||||
value={formData.device}
|
||||
onChange={(value) => onChange({ device: value })}
|
||||
multiple
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 选择海报 */}
|
||||
<Form.Item label="选择海报" className={style["form-item"]}>
|
||||
<Selector
|
||||
options={posters.map((poster) => ({
|
||||
label: poster.name,
|
||||
value: poster.id,
|
||||
}))}
|
||||
value={formData.posters}
|
||||
onChange={(value) => onChange({ posters: value })}
|
||||
multiple
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 工作时间 */}
|
||||
<Form.Item label="工作时间" className={style["form-item"]}>
|
||||
<Space>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.startTime}
|
||||
onChange={(value) => onChange({ startTime: value })}
|
||||
className={style["time-input"]}
|
||||
/>
|
||||
<span>至</span>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.endTime}
|
||||
onChange={(value) => onChange({ endTime: value })}
|
||||
className={style["time-input"]}
|
||||
/>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
|
||||
{/* 添加间隔 */}
|
||||
<Form.Item label="添加间隔(分钟)" className={style["form-item"]}>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入添加间隔"
|
||||
value={formData.addInterval.toString()}
|
||||
onChange={(value) =>
|
||||
onChange({ addInterval: Number(value) || 1 })
|
||||
}
|
||||
min={1}
|
||||
max={60}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className={style["actions"]}>
|
||||
<Button
|
||||
color="primary"
|
||||
size="large"
|
||||
onClick={handleNext}
|
||||
className={style["next-btn"]}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicSettings;
|
||||
@@ -0,0 +1,56 @@
|
||||
.friend-request-settings {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.adm-form-item-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.adm-input {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.adm-selector {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.adm-text-area {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.prev-btn {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
113
nkebao/src/pages/scenarios/new/steps/FriendRequestSettings.tsx
Normal file
113
nkebao/src/pages/scenarios/new/steps/FriendRequestSettings.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Selector,
|
||||
Button,
|
||||
Card,
|
||||
Space,
|
||||
TextArea,
|
||||
} from "antd-mobile";
|
||||
import style from "./FriendRequestSettings.module.scss";
|
||||
|
||||
interface FriendRequestSettingsProps {
|
||||
formData: any;
|
||||
onChange: (data: any) => void;
|
||||
onNext: () => void;
|
||||
onPrev: () => void;
|
||||
}
|
||||
|
||||
const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
|
||||
formData,
|
||||
onChange,
|
||||
onNext,
|
||||
onPrev,
|
||||
}) => {
|
||||
const remarkTypeOptions = [
|
||||
{ label: "手机号", value: "phone" },
|
||||
{ label: "微信号", value: "wechat" },
|
||||
{ label: "QQ号", value: "qq" },
|
||||
{ label: "自定义", value: "custom" },
|
||||
];
|
||||
|
||||
const handleNext = () => {
|
||||
if (!formData.greeting.trim()) {
|
||||
// 可以添加验证逻辑
|
||||
}
|
||||
onNext();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style["friend-request-settings"]}>
|
||||
<Card className={style["form-card"]}>
|
||||
<Form layout="vertical">
|
||||
{/* 备注类型 */}
|
||||
<Form.Item label="备注类型" required className={style["form-item"]}>
|
||||
<Selector
|
||||
options={remarkTypeOptions}
|
||||
value={[formData.remarkType]}
|
||||
onChange={(value) => onChange({ remarkType: value[0] })}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 备注格式 */}
|
||||
{formData.remarkType === "custom" && (
|
||||
<Form.Item label="备注格式" required className={style["form-item"]}>
|
||||
<Input
|
||||
placeholder="请输入备注格式,如:{name}-{phone}"
|
||||
value={formData.remarkFormat}
|
||||
onChange={(value) => onChange({ remarkFormat: value })}
|
||||
clearable
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 打招呼消息 */}
|
||||
<Form.Item label="打招呼消息" required className={style["form-item"]}>
|
||||
<TextArea
|
||||
placeholder="请输入打招呼消息"
|
||||
value={formData.greeting}
|
||||
onChange={(value) => onChange({ greeting: value })}
|
||||
rows={4}
|
||||
maxLength={200}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 好友申请间隔 */}
|
||||
<Form.Item label="好友申请间隔(分钟)" className={style["form-item"]}>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入好友申请间隔"
|
||||
value={formData.addFriendInterval.toString()}
|
||||
onChange={(value) =>
|
||||
onChange({ addFriendInterval: Number(value) || 1 })
|
||||
}
|
||||
min={1}
|
||||
max={60}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className={style["actions"]}>
|
||||
<Space style={{ width: "100%" }}>
|
||||
<Button size="large" onClick={onPrev} className={style["prev-btn"]}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
size="large"
|
||||
onClick={handleNext}
|
||||
className={style["next-btn"]}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FriendRequestSettings;
|
||||
@@ -0,0 +1,64 @@
|
||||
.message-settings {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.adm-form-item-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.adm-input {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.adm-text-area {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.switch-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.prev-btn {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
140
nkebao/src/pages/scenarios/new/steps/MessageSettings.tsx
Normal file
140
nkebao/src/pages/scenarios/new/steps/MessageSettings.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Selector,
|
||||
Button,
|
||||
Card,
|
||||
Space,
|
||||
TextArea,
|
||||
Switch,
|
||||
} from "antd-mobile";
|
||||
import style from "./MessageSettings.module.scss";
|
||||
|
||||
interface MessageSettingsProps {
|
||||
formData: any;
|
||||
onChange: (data: any) => void;
|
||||
onNext: () => void;
|
||||
onPrev: () => void;
|
||||
saving: boolean;
|
||||
}
|
||||
|
||||
const MessageSettings: React.FC<MessageSettingsProps> = ({
|
||||
formData,
|
||||
onChange,
|
||||
onNext,
|
||||
onPrev,
|
||||
saving,
|
||||
}) => {
|
||||
const handleSave = () => {
|
||||
onNext();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style["message-settings"]}>
|
||||
<Card className={style["form-card"]}>
|
||||
<Form layout="vertical">
|
||||
{/* 启用状态 */}
|
||||
<Form.Item label="启用状态" className={style["form-item"]}>
|
||||
<div className={style["switch-item"]}>
|
||||
<span>启用计划</span>
|
||||
<Switch
|
||||
checked={formData.enabled}
|
||||
onChange={(checked) => onChange({ enabled: checked })}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
{/* 自动回复消息 */}
|
||||
<Form.Item label="自动回复消息" className={style["form-item"]}>
|
||||
<TextArea
|
||||
placeholder="请输入自动回复消息"
|
||||
value={formData.autoReply || ""}
|
||||
onChange={(value) => onChange({ autoReply: value })}
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 关键词回复 */}
|
||||
<Form.Item label="关键词回复" className={style["form-item"]}>
|
||||
<TextArea
|
||||
placeholder="请输入关键词回复规则,格式:关键词=回复内容"
|
||||
value={formData.keywordReply || ""}
|
||||
onChange={(value) => onChange({ keywordReply: value })}
|
||||
rows={4}
|
||||
maxLength={1000}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 群发消息 */}
|
||||
<Form.Item label="群发消息" className={style["form-item"]}>
|
||||
<TextArea
|
||||
placeholder="请输入群发消息内容"
|
||||
value={formData.groupMessage || ""}
|
||||
onChange={(value) => onChange({ groupMessage: value })}
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 消息发送间隔 */}
|
||||
<Form.Item label="消息发送间隔(秒)" className={style["form-item"]}>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入消息发送间隔"
|
||||
value={formData.messageInterval?.toString() || "5"}
|
||||
onChange={(value) =>
|
||||
onChange({ messageInterval: Number(value) || 5 })
|
||||
}
|
||||
min={1}
|
||||
max={300}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 每日发送限制 */}
|
||||
<Form.Item label="每日发送限制" className={style["form-item"]}>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入每日发送限制数量"
|
||||
value={formData.dailyLimit?.toString() || "100"}
|
||||
onChange={(value) =>
|
||||
onChange({ dailyLimit: Number(value) || 100 })
|
||||
}
|
||||
min={1}
|
||||
max={1000}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className={style["actions"]}>
|
||||
<Space style={{ width: "100%" }}>
|
||||
<Button
|
||||
size="large"
|
||||
onClick={onPrev}
|
||||
className={style["prev-btn"]}
|
||||
disabled={saving}
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
size="large"
|
||||
onClick={handleSave}
|
||||
className={style["save-btn"]}
|
||||
loading={saving}
|
||||
>
|
||||
{saving ? "保存中..." : "保存计划"}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageSettings;
|
||||
Reference in New Issue
Block a user