FEAT => 本次更新项目为:

选择社群构建完成
This commit is contained in:
超级老白兔
2025-08-07 17:43:20 +08:00
parent 67c76d8b04
commit 504e8ff9b6
5 changed files with 463 additions and 353 deletions

View File

@@ -1,6 +1,5 @@
import React, { useState } from "react";
import { Input, Button, Card, Switch } from "antd";
import { MinusOutlined, PlusOutlined } from "@ant-design/icons";
import React, { useImperativeHandle, forwardRef } from "react";
import { Input, Button, Card, Switch, Form, InputNumber } from "antd";
interface BasicSettingsProps {
defaultValues?: {
@@ -18,194 +17,199 @@ interface BasicSettingsProps {
loading?: boolean;
}
const BasicSettings: React.FC<BasicSettingsProps> = ({
defaultValues = {
name: "",
pushTimeStart: "06:00",
pushTimeEnd: "23:59",
dailyPushCount: 20,
pushOrder: "latest",
isLoopPush: false,
isImmediatePush: false,
isEnabled: false,
},
onNext,
onSave,
loading = false,
}) => {
const [values, setValues] = useState(defaultValues);
export interface BasicSettingsRef {
validate: () => Promise<boolean>;
getValues: () => any;
}
const handleChange = (field: string, value: any) => {
setValues(prev => ({ ...prev, [field]: value }));
};
const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
(
{
defaultValues = {
name: "",
pushTimeStart: "06:00",
pushTimeEnd: "23:59",
dailyPushCount: 20,
pushOrder: "latest",
isLoopPush: false,
isImmediatePush: false,
isEnabled: false,
},
},
ref,
) => {
const [form] = Form.useForm();
const handleCountChange = (increment: boolean) => {
setValues(prev => ({
...prev,
dailyPushCount: increment
? prev.dailyPushCount + 1
: Math.max(1, prev.dailyPushCount - 1),
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
validate: async () => {
try {
await form.validateFields();
return true;
} catch (error) {
console.log("BasicSettings 表单验证失败:", error);
return false;
}
},
getValues: () => {
return form.getFieldsValue();
},
}));
};
return (
<div style={{ marginBottom: 24 }}>
<Card>
<div>
{/* 任务名称 */}
<div style={{ marginBottom: 16 }}>
<span style={{ color: "red", marginRight: 4 }}>*</span>:
<Input
value={values.name}
onChange={e => handleChange("name", e.target.value)}
placeholder="请输入任务名称"
style={{ marginTop: 4 }}
/>
</div>
{/* 允许推送的时间段 */}
<div style={{ marginBottom: 16 }}>
<span>:</span>
<div style={{ display: "flex", gap: 8, marginTop: 4 }}>
<Input
type="time"
value={values.pushTimeStart}
onChange={e => handleChange("pushTimeStart", e.target.value)}
style={{ width: 120 }}
/>
<span style={{ color: "#888" }}></span>
<Input
type="time"
value={values.pushTimeEnd}
onChange={e => handleChange("pushTimeEnd", e.target.value)}
style={{ width: 120 }}
/>
</div>
</div>
{/* 每日推送 */}
<div style={{ marginBottom: 16 }}>
<span>:</span>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
marginTop: 4,
}}
return (
<div style={{ marginBottom: 24 }}>
<Card>
<Form
form={form}
layout="vertical"
initialValues={defaultValues}
onValuesChange={(changedValues, allValues) => {
// 可以在这里处理表单值变化
}}
>
{/* 任务名称 */}
<Form.Item
label="任务名称"
name="name"
rules={[
{ required: true, message: "请输入任务名称" },
{ min: 2, max: 50, message: "任务名称长度在2-50个字符之间" },
]}
>
<Button
icon={<MinusOutlined />}
onClick={() => handleCountChange(false)}
disabled={loading}
/>
<Input
type="number"
value={values.dailyPushCount}
onChange={e =>
handleChange(
"dailyPushCount",
Number.parseInt(e.target.value) || 1,
)
}
style={{ width: 80, textAlign: "center" }}
<Input placeholder="请输入任务名称" />
</Form.Item>
{/* 允许推送的时间段 */}
<Form.Item label="允许推送的时间段">
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<Form.Item
name="pushTimeStart"
noStyle
rules={[{ required: true, message: "请选择开始时间" }]}
>
<Input type="time" style={{ width: 120 }} />
</Form.Item>
<span style={{ color: "#888" }}></span>
<Form.Item
name="pushTimeEnd"
noStyle
rules={[{ required: true, message: "请选择结束时间" }]}
>
<Input type="time" style={{ width: 120 }} />
</Form.Item>
</div>
</Form.Item>
{/* 每日推送 */}
<Form.Item
label="每日推送"
name="dailyPushCount"
rules={[
{ required: true, message: "请输入每日推送数量" },
{
type: "number",
min: 1,
max: 100,
message: "每日推送数量在1-100之间",
},
]}
>
<InputNumber
min={1}
disabled={loading}
max={100}
style={{ width: 120 }}
addonAfter="条内容"
/>
<Button
icon={<PlusOutlined />}
onClick={() => handleCountChange(true)}
disabled={loading}
/>
<span style={{ color: "#888" }}></span>
</div>
</div>
{/* 推送顺序 */}
<div style={{ marginBottom: 16 }}>
<span>:</span>
<div style={{ display: "flex" }}>
<Button
type={values.pushOrder === "earliest" ? "primary" : "default"}
onClick={() => handleChange("pushOrder", "earliest")}
disabled={loading}
style={{ borderRadius: "6px 0 0 6px" }}
>
</Button>
<Button
type={values.pushOrder === "latest" ? "primary" : "default"}
onClick={() => handleChange("pushOrder", "latest")}
disabled={loading}
style={{ borderRadius: "0 6px 6px 0", marginLeft: -1 }}
>
</Button>
</div>
</div>
{/* 是否循环推送 */}
<div
style={{
marginBottom: 16,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span>:</span>
<Switch
checked={values.isLoopPush}
onChange={checked => handleChange("isLoopPush", checked)}
disabled={loading}
/>
</div>
{/* 是否立即推送 */}
<div
style={{
marginBottom: 16,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span>:</span>
<Switch
checked={values.isImmediatePush}
onChange={checked => handleChange("isImmediatePush", checked)}
disabled={loading}
/>
</div>
{values.isImmediatePush && (
<div
style={{
background: "#fffbe6",
border: "1px solid #ffe58f",
borderRadius: 4,
padding: 8,
color: "#ad8b00",
marginBottom: 16,
}}
</Form.Item>
{/* 推送顺序 */}
<Form.Item
label="推送顺序"
name="pushOrder"
rules={[{ required: true, message: "请选择推送顺序" }]}
>
</div>
)}
{/* 是否启用 */}
<div
style={{
marginBottom: 16,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span>:</span>
<Switch
checked={values.isEnabled}
onChange={checked => handleChange("isEnabled", checked)}
disabled={loading}
/>
</div>
</div>
</Card>
</div>
);
};
<div style={{ display: "flex" }}>
<Button
type="default"
style={{ borderRadius: "6px 0 0 6px" }}
onClick={() => form.setFieldValue("pushOrder", "earliest")}
className={
form.getFieldValue("pushOrder") === "earliest"
? "ant-btn-primary"
: ""
}
>
</Button>
<Button
type="default"
style={{ borderRadius: "0 6px 6px 0", marginLeft: -1 }}
onClick={() => form.setFieldValue("pushOrder", "latest")}
className={
form.getFieldValue("pushOrder") === "latest"
? "ant-btn-primary"
: ""
}
>
</Button>
</div>
</Form.Item>
{/* 是否循环推送 */}
<Form.Item
label="是否循环推送"
name="isLoopPush"
valuePropName="checked"
>
<Switch />
</Form.Item>
{/* 是否立即推送 */}
<Form.Item
label="是否立即推送"
name="isImmediatePush"
valuePropName="checked"
>
<Switch />
</Form.Item>
{/* 是否启用 */}
<Form.Item
label="是否启用"
name="isEnabled"
valuePropName="checked"
>
<Switch />
</Form.Item>
{/* 立即推送提示 */}
<Form.Item noStyle shouldUpdate>
{() => {
const isImmediatePush = form.getFieldValue("isImmediatePush");
return isImmediatePush ? (
<div
style={{
background: "#fffbe6",
border: "1px solid #ffe58f",
borderRadius: 4,
padding: 8,
color: "#ad8b00",
marginBottom: 16,
}}
>
</div>
) : null;
}}
</Form.Item>
</Form>
</Card>
</div>
);
},
);
BasicSettings.displayName = "BasicSettings";
export default BasicSettings;

View File

@@ -1,4 +1,5 @@
import React, { useState } from "react";
import React, { useImperativeHandle, forwardRef } from "react";
import { Form, Card } from "antd";
import ContentLibrarySelection from "@/components/ContentLibrarySelection";
interface ContentLibrary {
@@ -19,60 +20,116 @@ interface ContentSelectorProps {
loading?: boolean;
}
const ContentSelector: React.FC<ContentSelectorProps> = ({
selectedLibraries,
onLibrariesChange,
onPrevious,
onNext,
onSave,
loading = false,
}) => {
// 将 ContentLibrary[] 转换为 string[] 用于 ContentLibrarySelection
const selectedLibraryIds = selectedLibraries.map(lib => lib.id);
export interface ContentSelectorRef {
validate: () => Promise<boolean>;
getValues: () => any;
}
// 处理选择变化
const handleLibrariesChange = (libraryIds: string[]) => {
// 这里需要根据选中的ID重新构建ContentLibrary对象
// 由于ContentLibrarySelection只返回ID我们需要从原始数据中获取完整信息
// 暂时使用简化的处理方式
const newSelectedLibraries = libraryIds.map(id => ({
id,
name: `内容库 ${id}`, // 这里应该从API获取完整信息
targets: [], // 这里应该从API获取完整信息
const ContentSelector = forwardRef<ContentSelectorRef, ContentSelectorProps>(
(
{
selectedLibraries,
onLibrariesChange,
onPrevious,
onNext,
onSave,
loading = false,
},
ref,
) => {
const [form] = Form.useForm();
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
validate: async () => {
try {
await form.validateFields();
return true;
} catch (error) {
console.log("ContentSelector 表单验证失败:", error);
return false;
}
},
getValues: () => {
return form.getFieldsValue();
},
}));
onLibrariesChange(newSelectedLibraries);
};
// 处理选择详情变化
const handleSelectDetail = (libraries: any[]) => {
// 将API返回的数据转换为ContentLibrary格式
const convertedLibraries = libraries.map(lib => ({
id: lib.id,
name: lib.name,
targets: [], // 这里需要根据实际情况获取targets数据
}));
onLibrariesChange(convertedLibraries);
};
// 将 ContentLibrary[] 转换为 string[] 用于 ContentLibrarySelection
const selectedLibraryIds = selectedLibraries.map(lib => lib.id);
return (
<div style={{ marginBottom: 24 }}>
<div>
<div style={{ marginBottom: 16 }}>
<span>:</span>
</div>
<ContentLibrarySelection
selectedLibraries={selectedLibraryIds}
onSelect={handleLibrariesChange}
onSelectDetail={handleSelectDetail}
placeholder="选择内容库"
showInput={true}
showSelectedList={true}
readonly={loading}
selectedListMaxHeight={320}
/>
// 处理选择变化
const handleLibrariesChange = (libraryIds: string[]) => {
// 这里需要根据选中的ID重新构建ContentLibrary对象
// 由于ContentLibrarySelection只返回ID我们需要从原始数据中获取完整信息
// 暂时使用简化的处理方式
const newSelectedLibraries = libraryIds.map(id => ({
id,
name: `内容库 ${id}`, // 这里应该从API获取完整信息
targets: [], // 这里应该从API获取完整信息
}));
onLibrariesChange(newSelectedLibraries);
form.setFieldValue("contentLibraries", libraryIds);
};
// 处理选择详情变化
const handleSelectDetail = (libraries: any[]) => {
// 将API返回的数据转换为ContentLibrary格式
const convertedLibraries = libraries.map(lib => ({
id: lib.id,
name: lib.name,
targets: [], // 这里需要根据实际情况获取targets数据
}));
onLibrariesChange(convertedLibraries);
form.setFieldValue(
"contentLibraries",
libraries.map(lib => lib.id),
);
};
return (
<div style={{ marginBottom: 24 }}>
<Card>
<Form
form={form}
layout="vertical"
initialValues={{ contentLibraries: selectedLibraryIds }}
>
<div style={{ marginBottom: 16 }}>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
</h2>
<p style={{ margin: "8px 0 0 0", color: "#666", fontSize: 14 }}>
</p>
</div>
<Form.Item
name="contentLibraries"
rules={[
{ required: true, message: "请选择至少一个内容库" },
{ type: "array", min: 1, message: "请选择至少一个内容库" },
{ type: "array", max: 20, message: "最多只能选择20个内容库" },
]}
>
<ContentLibrarySelection
selectedLibraries={selectedLibraryIds}
onSelect={handleLibrariesChange}
onSelectDetail={handleSelectDetail}
placeholder="选择内容库"
showInput={true}
showSelectedList={true}
readonly={loading}
selectedListMaxHeight={320}
/>
</Form.Item>
</Form>
</Card>
</div>
</div>
);
};
);
},
);
ContentSelector.displayName = "ContentSelector";
export default ContentSelector;

View File

@@ -1,59 +1,95 @@
import React, { useState } from "react";
import React, { useImperativeHandle, forwardRef } from "react";
import { Form, Card } from "antd";
import GroupSelection from "@/components/GroupSelection";
import { GroupSelectionItem } from "@/components/GroupSelection/data";
interface GroupSelectorProps {
selectedGroups: string[];
onGroupsChange: (groups: string[]) => void;
selectedGroups: GroupSelectionItem[];
onPrevious: () => void;
onNext: () => void;
onSave: () => void;
loading?: boolean;
onNext: (data: {
wechatGroups: string[];
wechatGroupsOptions: GroupSelectionItem[];
}) => void;
}
const GroupSelector: React.FC<GroupSelectorProps> = ({
selectedGroups,
onGroupsChange,
onPrevious,
onNext,
onSave,
loading = false,
}) => {
// 将string[]转换为GroupSelectionItem[]
const selectedGroupItems: GroupSelectionItem[] = selectedGroups.map(id => ({
id,
name: `群组 ${id}`,
avatar: "",
chatroomId: id,
}));
export interface GroupSelectorRef {
validate: () => Promise<boolean>;
getValues: () => any;
}
const handleGroupSelect = (groupItems: GroupSelectionItem[]) => {
// 将GroupSelectionItem[]转换回string[]
const groupIds = groupItems.map(item => item.id);
onGroupsChange(groupIds);
};
const GroupSelector = forwardRef<GroupSelectorRef, GroupSelectorProps>(
({ selectedGroups, onNext }, ref) => {
const [form] = Form.useForm();
return (
<div style={{ padding: 20 }}>
<div style={{ marginBottom: 20 }}>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
</h2>
<p style={{ margin: "8px 0 0 0", color: "#666", fontSize: 14 }}>
</p>
</div>
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
validate: async () => {
try {
form.setFieldsValue({
wechatGroups: selectedGroups.map(item => item.id),
});
await form.validateFields();
return true;
} catch (error) {
console.log("GroupSelector 表单验证失败:", error);
return false;
}
},
getValues: () => {
return form.getFieldsValue();
},
}));
<GroupSelection
selectedGroups={selectedGroupItems}
onSelect={handleGroupSelect}
placeholder="选择要推送的群组"
readonly={false}
showSelectedList={true}
selectedListMaxHeight={300}
/>
</div>
);
};
// 群组选择
const handleGroupSelect = (wechatGroupsOptions: GroupSelectionItem[]) => {
const wechatGroups = wechatGroupsOptions.map(item => item.id);
form.setFieldValue("wechatGroups", wechatGroups);
onNext({ wechatGroups, wechatGroupsOptions });
};
return (
<Card>
<Form
form={form}
layout="vertical"
initialValues={{ groups: selectedGroups }}
>
<div style={{ marginBottom: 20 }}>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
</h2>
<p style={{ margin: "8px 0 0 0", color: "#666", fontSize: 14 }}>
</p>
</div>
<Form.Item
name="wechatGroups"
rules={[
{
required: true,
type: "array",
min: 1,
message: "请选择至少一个群组",
},
{ type: "array", max: 50, message: "最多只能选择50个群组" },
]}
>
<GroupSelection
selectedGroups={selectedGroups}
onSelect={handleGroupSelect}
placeholder="选择要推送的群组"
readonly={false}
showSelectedList={true}
selectedListMaxHeight={300}
/>
</Form.Item>
</Form>
</Card>
);
},
);
GroupSelector.displayName = "GroupSelector";
export default GroupSelector;

View File

@@ -27,6 +27,7 @@ export interface FormData {
isLoopPush: boolean;
isImmediatePush: boolean;
isEnabled: boolean;
groups: WechatGroup[];
contentLibraries: ContentLibrary[];
wechatGroups: string[];
[key: string]: any;
}

View File

@@ -1,14 +1,17 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Button } from "antd";
import { createGroupPushTask } from "./index.api";
import Layout from "@/components/Layout/Layout";
import StepIndicator from "@/components/StepIndicator";
import BasicSettings from "./components/BasicSettings";
import GroupSelector from "./components/GroupSelector";
import ContentSelector from "./components/ContentSelector";
import BasicSettings, { BasicSettingsRef } from "./components/BasicSettings";
import GroupSelector, { GroupSelectorRef } from "./components/GroupSelector";
import ContentSelector, {
ContentSelectorRef,
} from "./components/ContentSelector";
import type { ContentLibrary, FormData } from "./index.data";
import NavCommon from "@/components/NavCommon";
import { GroupSelectionItem } from "@/components/GroupSelection/data";
const steps = [
{ id: 1, title: "步骤 1", subtitle: "基础设置" },
{ id: 2, title: "步骤 2", subtitle: "选择社群" },
@@ -21,6 +24,9 @@ const NewGroupPush: React.FC = () => {
const navigate = useNavigate();
const [currentStep, setCurrentStep] = useState(1);
const [loading, setLoading] = useState(false);
const [wechatGroupsOptions, setWechatGroupsOptions] = useState<
GroupSelectionItem[]
>([]);
const [formData, setFormData] = useState<FormData>({
name: "",
pushTimeStart: "06:00",
@@ -30,11 +36,16 @@ const NewGroupPush: React.FC = () => {
isLoopPush: false,
isImmediatePush: false,
isEnabled: false,
groups: [],
wechatGroups: [],
contentLibraries: [],
});
const [isEditMode, setIsEditMode] = useState(false);
// 创建子组件的ref
const basicSettingsRef = useRef<BasicSettingsRef>(null);
const groupSelectorRef = useRef<GroupSelectorRef>(null);
const contentSelectorRef = useRef<ContentSelectorRef>(null);
useEffect(() => {
if (!id) return;
setIsEditMode(true);
@@ -44,19 +55,16 @@ const NewGroupPush: React.FC = () => {
setFormData(prev => ({ ...prev, ...values }));
};
const handleGroupsChange = (groups: string[]) => {
// 将string[]转换为WechatGroup[]
const convertedGroups = groups.map(id => ({
id,
name: `群组 ${id}`,
avatar: "",
serviceAccount: {
id: "",
name: "",
avatar: "",
},
//群组选择
const handleGroupsChange = (data: {
wechatGroups: string[];
wechatGroupsOptions: GroupSelectionItem[];
}) => {
setFormData(prev => ({
...prev,
wechatGroups: data.wechatGroups,
}));
setFormData(prev => ({ ...prev, groups: convertedGroups }));
setWechatGroupsOptions(data.wechatGroupsOptions);
};
const handleLibrariesChange = (contentLibraries: ContentLibrary[]) => {
@@ -119,64 +127,66 @@ const NewGroupPush: React.FC = () => {
}
};
const handleNext = () => {
const handleNext = async () => {
if (currentStep < 4) {
setCurrentStep(currentStep + 1);
}
};
try {
let isValid = false;
const canGoNext = () => {
switch (currentStep) {
case 1: {
return formData.name.trim() !== "";
switch (currentStep) {
case 1:
// 调用 BasicSettings 的表单校验
isValid = (await basicSettingsRef.current?.validate()) || false;
if (isValid) {
const values = basicSettingsRef.current?.getValues();
if (values) {
handleBasicSettingsChange(values);
}
setCurrentStep(2);
}
break;
case 2:
// 调用 GroupSelector 的表单校验
isValid = (await groupSelectorRef.current?.validate()) || false;
if (isValid) {
setCurrentStep(3);
}
break;
case 3:
// 调用 ContentSelector 的表单校验
isValid = (await contentSelectorRef.current?.validate()) || false;
if (isValid) {
setCurrentStep(4);
}
break;
default:
setCurrentStep(currentStep + 1);
}
} catch (error) {
console.log("表单验证失败:", error);
}
case 2: {
// 选择社群:检查是否选择了群组
const groupsValid =
formData.groups.length > 0 && formData.groups.length <= 50; // 添加上限检查
return groupsValid;
}
case 3: {
// 选择内容库:检查是否选择了内容库
const librariesValid =
formData.contentLibraries.length > 0 &&
formData.contentLibraries.length <= 20; // 添加上限检查
return librariesValid;
}
case 4: {
// 京东联盟:可以进入下一步(保存)
// 这里可以添加京东联盟相关的验证逻辑
return true;
}
default:
return false;
}
};
const renderFooter = () => {
if (currentStep === 4) {
return (
<div className="footer-btn-group">
<Button size="large" onClick={handlePrevious}>
</Button>
<Button type="primary" size="large" onClick={handleSave}>
</Button>
</div>
);
}
return (
<div className="footer-btn-group">
{currentStep > 1 && (
<Button size="large" type="primary" onClick={handlePrevious}>
<Button size="large" onClick={handlePrevious}>
</Button>
)}
<Button size="large" type="primary" onClick={handleNext}>
</Button>
{currentStep === 4 ? (
<Button size="large" type="primary" onClick={handleSave}>
</Button>
) : (
<Button size="large" type="primary" onClick={handleNext}>
</Button>
)}
</div>
);
};
@@ -186,11 +196,14 @@ const NewGroupPush: React.FC = () => {
header={<NavCommon title={isEditMode ? "编辑任务" : "新建任务"} />}
footer={renderFooter()}
>
<div style={{ maxWidth: 600, margin: "0 auto", padding: 16 }}>
<StepIndicator currentStep={currentStep} steps={steps} />
<div style={{ marginTop: 32 }}>
<div style={{ padding: 12 }}>
<div style={{ marginBottom: 12 }}>
<StepIndicator currentStep={currentStep} steps={steps} />
</div>
<div>
{currentStep === 1 && (
<BasicSettings
ref={basicSettingsRef}
defaultValues={{
name: formData.name,
pushTimeStart: formData.pushTimeStart,
@@ -208,16 +221,15 @@ const NewGroupPush: React.FC = () => {
)}
{currentStep === 2 && (
<GroupSelector
selectedGroups={formData.groups.map(g => g.id)}
onGroupsChange={handleGroupsChange}
ref={groupSelectorRef}
selectedGroups={wechatGroupsOptions}
onPrevious={() => setCurrentStep(1)}
onNext={() => setCurrentStep(3)}
onSave={handleSave}
loading={loading}
onNext={handleGroupsChange}
/>
)}
{currentStep === 3 && (
<ContentSelector
ref={contentSelectorRef}
selectedLibraries={formData.contentLibraries}
onLibrariesChange={handleLibrariesChange}
onPrevious={() => setCurrentStep(2)}