Merge branch 'yongpxu-dev' into yongpxu-dev2

This commit is contained in:
笔记本里的永平
2025-07-23 11:38:35 +08:00
16 changed files with 1989 additions and 840 deletions

View File

@@ -0,0 +1,8 @@
{
"hash": "efe0acf4",
"configHash": "2bed34b3",
"lockfileHash": "ef01d341",
"browserHash": "91bd3b2c",
"optimized": {},
"chunks": {}
}

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

33
nkebao/src/api/common.ts Normal file
View File

@@ -0,0 +1,33 @@
import request from "./request";
/**
* 通用文件上传方法(支持图片、文件)
* @param {File} file - 要上传的文件对象
* @param {string} [uploadUrl='/v1/attachment/upload'] - 上传接口地址
* @returns {Promise<string>} - 上传成功后返回文件url
*/
export async function uploadFile(
file: File,
uploadUrl: string = "/v1/attachment/upload"
): Promise<string> {
try {
// 创建 FormData 对象用于文件上传
const formData = new FormData();
formData.append("file", file);
// 使用 request 方法上传文件,设置正确的 Content-Type
const res = await request(uploadUrl, formData, "POST", {
headers: {
"Content-Type": "multipart/form-data",
},
});
// 检查响应结果
if (res?.code === 200 && res?.data?.url) {
return res.data.url;
} else {
throw new Error(res?.msg || "文件上传失败");
}
} catch (e: any) {
throw new Error(e?.message || "文件上传失败");
}
}

View File

@@ -0,0 +1,53 @@
import request from "@/api/request";
// 获取场景类型列表
export function getScenarioTypes() {
return request("/v1/scenarios/types", undefined, "GET");
}
// 创建计划
export function createPlan(data: any) {
return request("/v1/scenarios/plans", data, "POST");
}
// 更新计划
export function updatePlan(planId: string, data: any) {
return request(`/v1/scenarios/plans/${planId}`, data, "PUT");
}
// 获取计划详情
export function getPlanDetail(planId: string) {
return request(`/v1/scenarios/plans/${planId}`, undefined, "GET");
}
// PlanDetail 类型定义(可根据实际接口返回结构补充字段)
export interface PlanDetail {
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;
// 其它字段可扩展
[key: string]: any;
}
// 兼容旧代码的接口命名
export function getPlanScenes() {
return getScenarioTypes();
}
export function createScenarioPlan(data: any) {
return createPlan(data);
}
export function fetchPlanDetail(planId: string) {
return getPlanDetail(planId);
}
export function updateScenarioPlan(planId: string, data: any) {
return updatePlan(planId, data);
}

View File

@@ -1,21 +1,20 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { NavBar, Button, Toast, SpinLoading, Steps, Popup } from "antd-mobile";
import { ArrowLeftOutlined } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import { LeftOutlined } from "@ant-design/icons";
import { Button, Steps, message } from "antd";
import BasicSettings from "./steps/BasicSettings";
import FriendRequestSettings from "./steps/FriendRequestSettings";
import MessageSettings from "./steps/MessageSettings";
import Layout from "@/components/Layout/Layout";
import {
getScenarioTypes,
createPlan,
updatePlan,
getPlanDetail,
} from "./page.api";
import style from "./page.module.scss";
getPlanScenes,
createScenarioPlan,
fetchPlanDetail,
PlanDetail,
updateScenarioPlan,
} from "./index.api";
// 步骤定义
// 步骤定义 - 只保留三个步骤
const steps = [
{ id: 1, title: "步骤一", subtitle: "基础设置" },
{ id: 2, title: "步骤二", subtitle: "好友申请设置" },
@@ -26,7 +25,7 @@ const steps = [
interface FormData {
name: string;
scenario: number;
posters: any[];
posters: any[]; // 后续可替换为具体Poster类型
device: string[];
remarkType: string;
greeting: string;
@@ -39,8 +38,8 @@ interface FormData {
addFriendInterval: number;
}
const NewPlan: React.FC = () => {
const navigate = useNavigate();
export default function NewPlan() {
const router = useNavigate();
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState<FormData>({
name: "",
@@ -64,57 +63,53 @@ const NewPlan: React.FC = () => {
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) {
//获取场景类型
getPlanScenes()
.then((data) => {
setSceneList(data || []);
})
.catch((err) => {
message.error(err.message || "获取场景类型失败");
})
.finally(() => setSceneLoading(false));
if (planId) {
setIsEdit(true);
//获取计划详情
try {
const detail = await fetchPlanDetail(planId);
setFormData((prev) => ({
...prev,
scenario: Number(scenarioId) || 1,
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,
tips: detail.tips ?? "",
}));
} catch (err) {
message.error(err.message || "获取计划详情失败");
}
} else {
if (scenarioId) {
setFormData((prev) => ({
...prev,
...{ scenario: Number(scenarioId) || 1 },
}));
}
} catch (error) {
Toast.show({
content: "加载数据失败",
position: "top",
});
} finally {
setSceneLoading(false);
}
};
@@ -125,52 +120,35 @@ const NewPlan: React.FC = () => {
// 处理保存
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);
result = await updateScenarioPlan(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",
});
result = await createScenarioPlan(formData);
}
message.success(isEdit ? "计划已更新" : "获客计划已创建");
const sceneItem = sceneList.find((v) => formData.scenario === v.id);
router(`/scenarios/list/${formData.sceneId}/${sceneItem.name}`);
} catch (error) {
Toast.show({
content: isEdit ? "更新计划失败,请重试" : "创建计划失败,请重试",
position: "top",
});
} finally {
setSaving(false);
message.error(
error instanceof Error
? error.message
: typeof error === "string"
? error
: isEdit
? "更新计划失败,请重试"
: "创建计划失败,请重试"
);
}
};
@@ -194,6 +172,7 @@ const NewPlan: React.FC = () => {
case 1:
return (
<BasicSettings
isEdit={isEdit}
formData={formData}
onChange={onChange}
onNext={handleNext}
@@ -215,9 +194,8 @@ const NewPlan: React.FC = () => {
<MessageSettings
formData={formData}
onChange={onChange}
onNext={handleNext}
onNext={handleSave}
onPrev={handlePrev}
saving={saving}
/>
);
default:
@@ -225,50 +203,25 @@ const NewPlan: React.FC = () => {
}
};
if (sceneLoading) {
return (
<Layout
header={
<NavBar back={null} style={{ background: "#fff" }}>
<div className={style["nav-title"]}>
{isEdit ? "编辑计划" : "新建计划"}
</div>
</NavBar>
}
>
<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="nav-title">
<ArrowLeftOutlined
twoToneColor="#1677ff"
onClick={() => navigate(-1)}
<header className="sticky top-0 z-10 bg-white border-b">
<div className="flex items-center justify-between h-14 px-4">
<div className="flex items-center">
<Button
type="text"
shape="circle"
icon={<LeftOutlined />}
onClick={() => router(-1)}
/>
</div>
}
>
<span className="nav-title">
{isEdit ? "编辑计划" : "新建计划"}
</span>
</NavBar>
{/* 步骤指示器 */}
<div className={style["steps-container"]}>
</div>
</header>
<div className="px-4 py-6">
<Steps current={currentStep - 1}>
{steps.map((step) => (
{steps.map((step, idx) => (
<Steps.Step
key={step.id}
title={step.title}
@@ -280,12 +233,7 @@ const NewPlan: React.FC = () => {
</>
}
>
<div className={style["new-plan-page"]}>
{/* 步骤内容 */}
<div className={style["step-content"]}>{renderStepContent()}</div>
</div>
<div className="p-4">{renderStepContent()}</div>
</Layout>
);
};
export default NewPlan;
}

View File

@@ -1,20 +0,0 @@
import request from "@/api/request";
// 获取场景类型列表
export function getScenarioTypes() {
return request("/api/scenarios/types", undefined, "GET");
}
// 创建计划
export function createPlan(data: any) {
return request("/api/scenarios/plans", data, "POST");
}
// 更新计划
export function updatePlan(planId: string, data: any) {
return request(`/api/scenarios/plans/${planId}`, data, "PUT");
}
// 获取计划详情
export function getPlanDetail(planId: string) {
return request(`/api/scenarios/plans/${planId}`, undefined, "GET");
}

View File

@@ -1,39 +0,0 @@
.new-plan-page {
}
.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: #ffffff;
margin-bottom: 12px;
}
.step-content {
flex: 1;
padding: 0 16px;
}

View File

@@ -1,63 +0,0 @@
.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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +0,0 @@
.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;
}

View File

@@ -1,113 +1,259 @@
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;
"use client";
import React, { useState, useEffect } from "react";
import {
Form,
Input,
Button,
Checkbox,
Modal,
Alert,
Select,
message,
} from "antd";
import { QuestionCircleOutlined, MessageOutlined } from "@ant-design/icons";
import DeviceSelection from "@/components/DeviceSelection";
interface FriendRequestSettingsProps {
formData: any;
onChange: (data: any) => void;
onNext: () => void;
onPrev: () => void;
}
// 招呼语模板
const greetingTemplates = [
"你好,请通过",
"你好,了解XX,请通过",
"你好我是XX产品的客服请通过",
"你好,感谢关注我们的产品",
"你好,很高兴为您服务",
];
// 备注类型选项
const remarkTypes = [
{ value: "phone", label: "手机号" },
{ value: "nickname", label: "昵称" },
{ value: "source", label: "来源" },
];
const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
formData,
onChange,
onNext,
onPrev,
}) => {
const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false);
const [hasWarnings, setHasWarnings] = useState(false);
const [selectedDevices, setSelectedDevices] = useState<any[]>(
formData.selectedDevices || []
);
const [showRemarkTip, setShowRemarkTip] = useState(false);
// 获取场景标题
const getScenarioTitle = () => {
switch (formData.scenario) {
case "douyin":
return "抖音直播";
case "xiaohongshu":
return "小红书";
case "weixinqun":
return "微信群";
case "gongzhonghao":
return "公众号";
default:
return formData.name || "获客计划";
}
};
// 使用useEffect设置默认值
useEffect(() => {
if (!formData.greeting) {
onChange({
...formData,
greeting: "你好,请通过",
remarkType: "phone", // 默认选择手机号
remarkFormat: `手机号+${getScenarioTitle()}`, // 默认备注格式
addFriendInterval: 1,
});
}
}, [formData, formData.greeting, onChange]);
// 检查是否有未完成的必填项
useEffect(() => {
const hasIncompleteFields = !formData.greeting?.trim();
setHasWarnings(hasIncompleteFields);
}, [formData]);
const handleTemplateSelect = (template: string) => {
onChange({ ...formData, greeting: template });
setIsTemplateDialogOpen(false);
};
const handleNext = () => {
// 即使有警告也允许进入下一步,但会显示提示
onNext();
};
return (
<>
<div className="space-y-6">
<div>
<span className="font-medium text-base"></span>
<div className="mt-2">
<DeviceSelection
selectedDevices={selectedDevices.map((d) => d.id)}
onSelect={(deviceIds) => {
const newSelectedDevices = deviceIds.map((id) => ({
id,
name: `设备 ${id}`,
status: "online",
}));
setSelectedDevices(newSelectedDevices);
onChange({ ...formData, device: deviceIds });
}}
placeholder="选择设备"
/>
</div>
</div>
<div className="mb-4">
<div className="flex items-center space-x-2 mb-1 relative">
<span className="font-medium text-base"></span>
<span
className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-gray-200 text-gray-500 text-xs cursor-pointer hover:bg-gray-300 transition-colors"
onMouseEnter={() => setShowRemarkTip(true)}
onMouseLeave={() => setShowRemarkTip(false)}
onClick={() => setShowRemarkTip((v) => !v)}
>
?
</span>
{showRemarkTip && (
<div className="absolute left-24 top-0 z-20 w-64 p-3 bg-white border border-gray-200 rounded shadow-lg text-sm text-gray-700">
<div></div>
<div className="mt-2 text-xs text-gray-500"></div>
<div className="mt-1 text-blue-600">
{formData.remarkType === "phone" &&
`138****1234+${getScenarioTitle()}`}
{formData.remarkType === "nickname" &&
`小红书用户2851+${getScenarioTitle()}`}
{formData.remarkType === "source" &&
`抖音直播+${getScenarioTitle()}`}
</div>
</div>
)}
</div>
<Select
value={formData.remarkType || "phone"}
onChange={(value) => onChange({ ...formData, remarkType: value })}
className="w-full mt-2"
>
{remarkTypes.map((type) => (
<Select.Option key={type.value} value={type.value}>
{type.label}
</Select.Option>
))}
</Select>
</div>
<div>
<div className="flex items-center justify-between">
<span className="font-medium text-base"></span>
<Button
onClick={() => setIsTemplateDialogOpen(true)}
className="text-blue-500"
>
<MessageOutlined className="h-4 w-4 mr-2" />
</Button>
</div>
<Input
value={formData.greeting}
onChange={(e) =>
onChange({ ...formData, greeting: e.target.value })
}
placeholder="请输入招呼语"
className="mt-2"
/>
</div>
<div>
<span className="font-medium text-base"></span>
<div className="flex items-center space-x-2 mt-2">
<Input
type="number"
value={formData.addFriendInterval || 1}
onChange={(e) =>
onChange({
...formData,
addFriendInterval: Number(e.target.value),
})
}
/>
<div className="w-10"></div>
</div>
</div>
<div>
<span className="font-medium text-base"></span>
<div className="flex items-center space-x-2 mt-2">
<Input
type="time"
value={formData.addFriendTimeStart || "09:00"}
onChange={(e) =>
onChange({ ...formData, addFriendTimeStart: e.target.value })
}
className="w-32"
/>
<span></span>
<Input
type="time"
value={formData.addFriendTimeEnd || "18:00"}
onChange={(e) =>
onChange({ ...formData, addFriendTimeEnd: e.target.value })
}
className="w-32"
/>
</div>
</div>
{hasWarnings && (
<Alert
message="警告"
description="您有未完成的设置项,建议完善后再进入下一步。"
type="warning"
showIcon
className="bg-amber-50 border-amber-200"
/>
)}
<div className="flex justify-between pt-4">
<Button onClick={onPrev}></Button>
<Button type="primary" onClick={handleNext}>
</Button>
</div>
</div>
<Modal
open={isTemplateDialogOpen}
onCancel={() => setIsTemplateDialogOpen(false)}
footer={null}
>
<div className="space-y-2">
{greetingTemplates.map((template, index) => (
<Button
key={index}
onClick={() => handleTemplateSelect(template)}
style={{ width: "100%", marginBottom: 8 }}
>
{template}
</Button>
))}
</div>
</Modal>
</>
);
};
export default FriendRequestSettings;

View File

@@ -1,64 +0,0 @@
.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;
}

View File

@@ -1,140 +1,603 @@
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;
import React, { useState } from "react";
import { Form, Input, Button, Tabs, Modal, Alert, Upload, message } from "antd";
import {
PlusOutlined,
CloseOutlined,
UploadOutlined,
ClockCircleOutlined,
MessageOutlined,
PictureOutlined,
VideoCameraOutlined,
FileOutlined,
AppstoreOutlined,
LinkOutlined,
TeamOutlined,
} from "@ant-design/icons";
interface MessageContent {
id: string;
type: "text" | "image" | "video" | "file" | "miniprogram" | "link" | "group";
content: string;
sendInterval?: number;
intervalUnit?: "seconds" | "minutes";
scheduledTime?: {
hour: number;
minute: number;
second: number;
};
title?: string;
description?: string;
address?: string;
coverImage?: string;
groupId?: string;
linkUrl?: string;
}
interface DayPlan {
day: number;
messages: MessageContent[];
}
interface MessageSettingsProps {
formData: any;
onChange: (data: any) => void;
onNext: () => void;
onPrev: () => void;
}
// 消息类型配置
const messageTypes = [
{ id: "text", icon: MessageOutlined, label: "文本" },
{ id: "image", icon: PictureOutlined, label: "图片" },
{ id: "video", icon: VideoCameraOutlined, label: "视频" },
{ id: "file", icon: FileOutlined, label: "文件" },
{ id: "miniprogram", icon: AppstoreOutlined, label: "小程序" },
{ id: "link", icon: LinkOutlined, label: "链接" },
{ id: "group", icon: TeamOutlined, label: "邀请入群" },
];
// 模拟群组数据
const mockGroups = [
{ id: "1", name: "产品交流群1", memberCount: 156 },
{ id: "2", name: "产品交流群2", memberCount: 234 },
{ id: "3", name: "产品交流群3", memberCount: 89 },
];
const MessageSettings: React.FC<MessageSettingsProps> = ({
formData,
onChange,
onNext,
onPrev,
}) => {
const [dayPlans, setDayPlans] = useState<DayPlan[]>([
{
day: 0,
messages: [
{
id: "1",
type: "text",
content: "",
sendInterval: 5,
intervalUnit: "seconds",
},
],
},
]);
const [isAddDayPlanOpen, setIsAddDayPlanOpen] = useState(false);
const [isGroupSelectOpen, setIsGroupSelectOpen] = useState(false);
const [selectedGroupId, setSelectedGroupId] = useState("");
// 添加新消息
const handleAddMessage = (dayIndex: number, type = "text") => {
const updatedPlans = [...dayPlans];
const newMessage: MessageContent = {
id: Date.now().toString(),
type: type as MessageContent["type"],
content: "",
};
if (dayPlans[dayIndex].day === 0) {
newMessage.sendInterval = 5;
newMessage.intervalUnit = "seconds";
} else {
newMessage.scheduledTime = {
hour: 9,
minute: 0,
second: 0,
};
}
updatedPlans[dayIndex].messages.push(newMessage);
setDayPlans(updatedPlans);
onChange({ ...formData, messagePlans: updatedPlans });
};
// 更新消息内容
const handleUpdateMessage = (
dayIndex: number,
messageIndex: number,
updates: Partial<MessageContent>
) => {
const updatedPlans = [...dayPlans];
updatedPlans[dayIndex].messages[messageIndex] = {
...updatedPlans[dayIndex].messages[messageIndex],
...updates,
};
setDayPlans(updatedPlans);
onChange({ ...formData, messagePlans: updatedPlans });
};
// 删除消息
const handleRemoveMessage = (dayIndex: number, messageIndex: number) => {
const updatedPlans = [...dayPlans];
updatedPlans[dayIndex].messages.splice(messageIndex, 1);
setDayPlans(updatedPlans);
onChange({ ...formData, messagePlans: updatedPlans });
};
// 切换时间单位
const toggleIntervalUnit = (dayIndex: number, messageIndex: number) => {
const message = dayPlans[dayIndex].messages[messageIndex];
const newUnit = message.intervalUnit === "minutes" ? "seconds" : "minutes";
handleUpdateMessage(dayIndex, messageIndex, { intervalUnit: newUnit });
};
// 添加新的天数计划
const handleAddDayPlan = () => {
const newDay = dayPlans.length;
setDayPlans([
...dayPlans,
{
day: newDay,
messages: [
{
id: Date.now().toString(),
type: "text",
content: "",
scheduledTime: {
hour: 9,
minute: 0,
second: 0,
},
},
],
},
]);
setIsAddDayPlanOpen(false);
message.success(`已添加第${newDay}天的消息计划`);
};
// 选择群组
const handleSelectGroup = (groupId: string) => {
setSelectedGroupId(groupId);
setIsGroupSelectOpen(false);
message.success(
`已选择群组:${mockGroups.find((g) => g.id === groupId)?.name}`
);
};
// 处理文件上传
const handleFileUpload = (
dayIndex: number,
messageIndex: number,
type: "image" | "video" | "file"
) => {
message.success(
`${
type === "image" ? "图片" : type === "video" ? "视频" : "文件"
}上传成功`
);
};
const items = dayPlans.map((plan, dayIndex) => ({
key: plan.day.toString(),
label: plan.day === 0 ? "即时消息" : `${plan.day}`,
children: (
<div className="space-y-4">
{plan.messages.map((message, messageIndex) => (
<div key={message.id} className="space-y-4 p-4 bg-gray-50 rounded-lg">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
{plan.day === 0 ? (
<>
<div className="w-10"></div>
<div className="w-40">
<Input
type="number"
value={String(message.sendInterval)}
onChange={(e) =>
handleUpdateMessage(dayIndex, messageIndex, {
sendInterval: Number(e.target.value),
})
}
/>
</div>
<Button
onClick={() => toggleIntervalUnit(dayIndex, messageIndex)}
className="flex items-center space-x-1"
>
<ClockCircleOutlined className="h-3 w-3" />
<span>
{message.intervalUnit === "minutes" ? "分钟" : "秒"}
</span>
</Button>
</>
) : (
<>
<div className="font-medium"></div>
<div className="flex items-center space-x-1">
<Input
type="number"
min={0}
max={23}
value={String(message.scheduledTime?.hour || 0)}
onChange={(e) =>
handleUpdateMessage(dayIndex, messageIndex, {
scheduledTime: {
...(message.scheduledTime || {
hour: 0,
minute: 0,
second: 0,
}),
hour: Number(e.target.value),
},
})
}
className="w-16"
/>
<span>:</span>
<Input
type="number"
min={0}
max={59}
value={String(message.scheduledTime?.minute || 0)}
onChange={(e) =>
handleUpdateMessage(dayIndex, messageIndex, {
scheduledTime: {
...(message.scheduledTime || {
hour: 0,
minute: 0,
second: 0,
}),
minute: Number(e.target.value),
},
})
}
className="w-16"
/>
<span>:</span>
<Input
type="number"
min={0}
max={59}
value={String(message.scheduledTime?.second || 0)}
onChange={(e) =>
handleUpdateMessage(dayIndex, messageIndex, {
scheduledTime: {
...(message.scheduledTime || {
hour: 0,
minute: 0,
second: 0,
}),
second: Number(e.target.value),
},
})
}
className="w-16"
/>
</div>
</>
)}
</div>
<Button
onClick={() => handleRemoveMessage(dayIndex, messageIndex)}
>
<CloseOutlined className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center space-x-2 bg-white p-2 rounded-lg">
{messageTypes.map((type) => (
<Button
key={type.id}
type={message.type === type.id ? "primary" : "default"}
onClick={() =>
handleUpdateMessage(dayIndex, messageIndex, {
type: type.id as any,
})
}
className="flex flex-col items-center p-2 h-auto"
>
<type.icon className="h-4 w-4" />
</Button>
))}
</div>
{message.type === "text" && (
<Input.TextArea
value={message.content}
onChange={(e) =>
handleUpdateMessage(dayIndex, messageIndex, {
content: e.target.value,
})
}
placeholder="请输入消息内容"
className="min-h-[100px]"
/>
)}
{message.type === "miniprogram" && (
<div className="space-y-4">
<div className="space-y-2">
<div className="font-medium">
<span className="text-red-500">*</span>
</div>
<Input
value={message.title}
onChange={(e) =>
handleUpdateMessage(dayIndex, messageIndex, {
title: e.target.value,
})
}
placeholder="请输入小程序标题"
/>
</div>
<div className="space-y-2">
<div className="font-medium"></div>
<Input
value={message.description}
onChange={(e) =>
handleUpdateMessage(dayIndex, messageIndex, {
description: e.target.value,
})
}
placeholder="请输入小程序描述"
/>
</div>
<div className="space-y-2">
<div className="font-medium">
<span className="text-red-500">*</span>
</div>
<Input
value={message.address}
onChange={(e) =>
handleUpdateMessage(dayIndex, messageIndex, {
address: e.target.value,
})
}
placeholder="请输入小程序路径"
/>
</div>
<div className="space-y-2">
<div className="font-medium">
<span className="text-red-500">*</span>
</div>
<div className="border-2 border-dashed rounded-lg p-4 text-center">
{message.coverImage ? (
<div className="relative">
<img
src={message.coverImage || "/placeholder.svg"}
alt="封面"
className="max-w-[200px] mx-auto rounded-lg"
/>
<Button
onClick={() =>
handleUpdateMessage(dayIndex, messageIndex, {
coverImage: undefined,
})
}
>
<CloseOutlined className="h-4 w-4" />
</Button>
</div>
) : (
<Button
onClick={() =>
handleFileUpload(dayIndex, messageIndex, "image")
}
>
<UploadOutlined className="h-4 w-4 mr-2" />
</Button>
)}
</div>
</div>
</div>
)}
{message.type === "link" && (
<div className="space-y-4">
<div className="space-y-2">
<div className="font-medium">
<span className="text-red-500">*</span>
</div>
<Input
value={message.title}
onChange={(e) =>
handleUpdateMessage(dayIndex, messageIndex, {
title: e.target.value,
})
}
placeholder="请输入链接标题"
/>
</div>
<div className="space-y-2">
<div className="font-medium"></div>
<Input
value={message.description}
onChange={(e) =>
handleUpdateMessage(dayIndex, messageIndex, {
description: e.target.value,
})
}
placeholder="请输入链接描述"
/>
</div>
<div className="space-y-2">
<div className="font-medium">
<span className="text-red-500">*</span>
</div>
<Input
value={message.linkUrl}
onChange={(e) =>
handleUpdateMessage(dayIndex, messageIndex, {
linkUrl: e.target.value,
})
}
placeholder="请输入链接地址"
/>
</div>
<div className="space-y-2">
<div className="font-medium">
<span className="text-red-500">*</span>
</div>
<div className="border-2 border-dashed rounded-lg p-4 text-center">
{message.coverImage ? (
<div className="relative">
<img
src={message.coverImage || "/placeholder.svg"}
alt="封面"
className="max-w-[200px] mx-auto rounded-lg"
/>
<Button
onClick={() =>
handleUpdateMessage(dayIndex, messageIndex, {
coverImage: undefined,
})
}
>
<CloseOutlined className="h-4 w-4" />
</Button>
</div>
) : (
<Button
onClick={() =>
handleFileUpload(dayIndex, messageIndex, "image")
}
>
<UploadOutlined className="h-4 w-4 mr-2" />
</Button>
)}
</div>
</div>
</div>
)}
{message.type === "group" && (
<div className="space-y-2">
<div className="font-medium">
<span className="text-red-500">*</span>
</div>
<Button onClick={() => setIsGroupSelectOpen(true)}>
{selectedGroupId
? mockGroups.find((g) => g.id === selectedGroupId)?.name
: "选择邀请入的群"}
</Button>
</div>
)}
{(message.type === "image" ||
message.type === "video" ||
message.type === "file") && (
<div className="border-2 border-dashed rounded-lg p-4 text-center">
<Button
onClick={() =>
handleFileUpload(
dayIndex,
messageIndex,
message.type as any
)
}
>
<UploadOutlined className="h-4 w-4 mr-2" />
{message.type === "image"
? "图片"
: message.type === "video"
? "视频"
: "文件"}
</Button>
</div>
)}
</div>
))}
<Button onClick={() => handleAddMessage(dayIndex)} className="w-full">
<PlusOutlined className="w-4 h-4 mr-2" />
</Button>
</div>
),
}));
return (
<>
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold"></h2>
<Button onClick={() => setIsAddDayPlanOpen(true)}>
<PlusOutlined className="h-4 w-4" />
</Button>
</div>
<Tabs defaultActiveKey="0" items={items} />
<div className="flex justify-between pt-4">
<Button onClick={onPrev}></Button>
<Button type="primary" onClick={onNext}>
</Button>
</div>
</div>
{/* 添加天数计划弹窗 */}
<Modal
title="添加消息计划"
open={isAddDayPlanOpen}
onCancel={() => setIsAddDayPlanOpen(false)}
onOk={() => {
handleAddDayPlan();
setIsAddDayPlanOpen(false);
}}
>
<p className="text-sm text-gray-500 mb-4"></p>
<Button onClick={handleAddDayPlan} className="w-full">
{dayPlans.length}
</Button>
</Modal>
{/* 选择群聊弹窗 */}
<Modal
title="选择群聊"
open={isGroupSelectOpen}
onCancel={() => setIsGroupSelectOpen(false)}
onOk={() => {
handleSelectGroup(selectedGroupId);
setIsGroupSelectOpen(false);
}}
>
<div className="space-y-2">
{mockGroups.map((group) => (
<div
key={group.id}
className={`p-4 rounded-lg cursor-pointer hover:bg-gray-100 ${
selectedGroupId === group.id
? "bg-blue-50 border border-blue-200"
: ""
}`}
onClick={() => handleSelectGroup(group.id)}
>
<div className="font-medium">{group.name}</div>
<div className="text-sm text-gray-500">
{group.memberCount}
</div>
</div>
))}
</div>
</Modal>
</>
);
};
export default MessageSettings;