FEAT => 本次更新项目为:统一组件接口,将属性名称从 selectedGroups 更改为 selectedOptions,以提升代码一致性和可读性,并对相关组件进行了相应调整。
This commit is contained in:
@@ -202,7 +202,7 @@ export default function ContentForm() {
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab title="选择聊天群" key="groups">
|
||||
<GroupSelection
|
||||
selectedGroups={selectedGroupsOptions}
|
||||
selectedOptions={selectedGroupsOptions}
|
||||
onSelect={handleGroupsChange}
|
||||
placeholder="选择聊天群"
|
||||
/>
|
||||
|
||||
@@ -438,7 +438,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
<div className={styles["basic-group-selection"]}>
|
||||
<div className={styles["basic-label"]}>选择群聊</div>
|
||||
<GroupSelection
|
||||
selectedGroups={formData.groupSelected || []}
|
||||
selectedOptions={formData.groupSelected || []}
|
||||
onSelect={groups =>
|
||||
onChange({ ...formData, groupSelected: groups })
|
||||
}
|
||||
|
||||
@@ -450,7 +450,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
|
||||
{message.type === "group" && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<GroupSelection
|
||||
selectedGroups={message.groupIds || []}
|
||||
selectedOptions={message.groupIds || []}
|
||||
onSelect={groupIds =>
|
||||
handleUpdateMessage(dayIndex, messageIndex, {
|
||||
groupIds: groupIds,
|
||||
|
||||
@@ -1,594 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
ChevronLeft,
|
||||
Plus,
|
||||
Minus,
|
||||
Check,
|
||||
X,
|
||||
Tag as TagIcon,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
createAutoLikeTask,
|
||||
updateAutoLikeTask,
|
||||
fetchAutoLikeTaskDetail,
|
||||
} from "@/api/autoLike";
|
||||
import { ContentType } from "@/types/auto-like";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
import Layout from "@/components/Layout";
|
||||
import DeviceSelection from "@/components/DeviceSelection";
|
||||
import FriendSelection from "@/components/FriendSelection";
|
||||
|
||||
// 修改CreateLikeTaskData接口,确保friends字段不是可选的
|
||||
interface CreateLikeTaskDataLocal {
|
||||
name: string;
|
||||
interval: number;
|
||||
maxLikes: number;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
contentTypes: ContentType[];
|
||||
devices: string[];
|
||||
friends: string[];
|
||||
friendMaxLikes: number;
|
||||
friendTags: string;
|
||||
enableFriendTags: boolean;
|
||||
targetTags: string[];
|
||||
}
|
||||
|
||||
export default function NewAutoLike() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const isEditMode = !!id;
|
||||
const { toast } = useToast();
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(isEditMode);
|
||||
const [formData, setFormData] = useState<CreateLikeTaskDataLocal>({
|
||||
name: "",
|
||||
interval: 5,
|
||||
maxLikes: 200,
|
||||
startTime: "08:00",
|
||||
endTime: "22:00",
|
||||
contentTypes: ["text", "image", "video"],
|
||||
devices: [],
|
||||
friends: [], // 确保初始化为空数组而不是undefined
|
||||
targetTags: [],
|
||||
friendMaxLikes: 10,
|
||||
enableFriendTags: false,
|
||||
friendTags: "",
|
||||
});
|
||||
// 新增自动开启的独立状态
|
||||
const [autoEnabled, setAutoEnabled] = useState(false);
|
||||
|
||||
// 如果是编辑模式,获取任务详情
|
||||
useEffect(() => {
|
||||
if (isEditMode && id) {
|
||||
fetchTaskDetail();
|
||||
}
|
||||
}, [id, isEditMode]);
|
||||
|
||||
// 获取任务详情
|
||||
const fetchTaskDetail = async () => {
|
||||
try {
|
||||
const taskDetail = await fetchAutoLikeTaskDetail(id!);
|
||||
console.log("Task detail response:", taskDetail); // 添加日志用于调试
|
||||
|
||||
if (taskDetail) {
|
||||
// 使用类型断言处理可能的字段名称差异
|
||||
const taskAny = taskDetail as any;
|
||||
// 处理可能的嵌套结构
|
||||
const config = taskAny.config || taskAny;
|
||||
|
||||
setFormData({
|
||||
name: taskDetail.name || "",
|
||||
interval: config.likeInterval || config.interval || 5,
|
||||
maxLikes: config.maxLikesPerDay || config.maxLikes || 200,
|
||||
startTime: config.timeRange?.start || config.startTime || "08:00",
|
||||
endTime: config.timeRange?.end || config.endTime || "22:00",
|
||||
contentTypes: config.contentTypes || ["text", "image", "video"],
|
||||
devices: config.devices || [],
|
||||
friends: config.friends || [],
|
||||
targetTags: config.targetTags || [],
|
||||
friendMaxLikes: config.friendMaxLikes || 10,
|
||||
enableFriendTags: config.enableFriendTags || false,
|
||||
friendTags: config.friendTags || "",
|
||||
});
|
||||
|
||||
// 处理状态字段,使用双等号允许类型自动转换
|
||||
const status = taskAny.status;
|
||||
setAutoEnabled(status === 1 || status === "running");
|
||||
} else {
|
||||
toast({
|
||||
title: "获取任务详情失败",
|
||||
description: "无法找到该任务",
|
||||
variant: "destructive",
|
||||
});
|
||||
navigate("/workspace/auto-like");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取任务详情出错:", error); // 添加错误日志
|
||||
toast({
|
||||
title: "获取任务详情失败",
|
||||
description: "请检查网络连接后重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
navigate("/workspace/auto-like");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateFormData = (data: Partial<CreateLikeTaskDataLocal>) => {
|
||||
setFormData(prev => ({ ...prev, ...data }));
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
setCurrentStep(prev => Math.min(prev + 1, 3));
|
||||
// 滚动到顶部
|
||||
const mainElement = document.querySelector("main");
|
||||
if (mainElement) {
|
||||
mainElement.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
setCurrentStep(prev => Math.max(prev - 1, 1));
|
||||
// 滚动到顶部
|
||||
const mainElement = document.querySelector("main");
|
||||
if (mainElement) {
|
||||
mainElement.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleComplete = async () => {
|
||||
if (isSubmitting) return;
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
// 转换为API需要的格式
|
||||
const apiFormData = {
|
||||
...formData,
|
||||
// 如果API需要其他转换,可以在这里添加
|
||||
};
|
||||
|
||||
let response;
|
||||
if (isEditMode) {
|
||||
// 编辑模式,调用更新API
|
||||
response = await updateAutoLikeTask({
|
||||
...apiFormData,
|
||||
id: id!,
|
||||
});
|
||||
} else {
|
||||
// 新建模式,调用创建API
|
||||
response = await createAutoLikeTask(apiFormData);
|
||||
}
|
||||
|
||||
if (response.code === 200) {
|
||||
toast({
|
||||
title: isEditMode ? "更新成功" : "创建成功",
|
||||
description: isEditMode
|
||||
? "自动点赞任务已更新"
|
||||
: "自动点赞任务已创建并开始执行",
|
||||
});
|
||||
navigate("/workspace/auto-like");
|
||||
} else {
|
||||
toast({
|
||||
title: isEditMode ? "更新失败" : "创建失败",
|
||||
description: response.msg || "请稍后重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: isEditMode ? "更新失败" : "创建失败",
|
||||
description: "请检查网络连接后重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const header = (
|
||||
<div className="sticky top-0 z-10 bg-white pb-4">
|
||||
<div className="flex items-center h-14 px-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => navigate(-1)}
|
||||
className="hover:bg-gray-50"
|
||||
>
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</Button>
|
||||
<h1 className="ml-2 text-lg font-medium">
|
||||
{isEditMode ? "编辑自动点赞" : "新建自动点赞"}
|
||||
</h1>
|
||||
</div>
|
||||
<StepIndicator currentStep={currentStep} />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Layout header={header}>
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-500">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout header={header}>
|
||||
<div className="min-h-screen bg-[#F8F9FA]">
|
||||
<div className="pt-4">
|
||||
<div className="mt-8">
|
||||
{currentStep === 1 && (
|
||||
<BasicSettings
|
||||
formData={formData}
|
||||
onChange={handleUpdateFormData}
|
||||
onNext={handleNext}
|
||||
autoEnabled={autoEnabled}
|
||||
setAutoEnabled={setAutoEnabled}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
<div className="space-y-6 px-6">
|
||||
<DeviceSelection
|
||||
selectedOptions={formData.devices}
|
||||
onSelect={devices => handleUpdateFormData({ devices })}
|
||||
placeholder="选择设备"
|
||||
/>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 h-12 rounded-xl text-base"
|
||||
onClick={handlePrev}
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
|
||||
onClick={handleNext}
|
||||
disabled={formData.devices.length === 0}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 3 && (
|
||||
<div className="px-6 space-y-6">
|
||||
<FriendSelection
|
||||
selectedFriends={formData.friends || []}
|
||||
onSelect={friends => handleUpdateFormData({ friends })}
|
||||
deviceIds={formData.devices}
|
||||
placeholder="选择微信好友"
|
||||
/>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 h-12 rounded-xl text-base"
|
||||
onClick={handlePrev}
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
|
||||
onClick={handleComplete}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
// 步骤指示器组件
|
||||
interface StepIndicatorProps {
|
||||
currentStep: number;
|
||||
}
|
||||
|
||||
function StepIndicator({ currentStep }: StepIndicatorProps) {
|
||||
const steps = [
|
||||
{ title: "基础设置", description: "设置点赞规则" },
|
||||
{ title: "设备选择", description: "选择执行设备" },
|
||||
{ title: "人群选择", description: "选择目标人群" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="px-6">
|
||||
<div className="relative">
|
||||
<div className="flex items-center justify-between">
|
||||
{steps.map((step, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-col items-center relative z-10"
|
||||
>
|
||||
<div
|
||||
className={`flex items-center justify-center w-8 h-8 rounded-full ${
|
||||
index < currentStep
|
||||
? "bg-blue-600 text-white"
|
||||
: index === currentStep
|
||||
? "border-2 border-blue-600 text-blue-600"
|
||||
: "border-2 border-gray-300 text-gray-300"
|
||||
}`}
|
||||
>
|
||||
{index < currentStep ? (
|
||||
<Check className="w-5 h-5" />
|
||||
) : (
|
||||
index + 1
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center mt-2">
|
||||
<div
|
||||
className={`text-sm font-medium ${index <= currentStep ? "text-gray-900" : "text-gray-400"}`}
|
||||
>
|
||||
{step.title}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{step.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute top-4 left-0 w-full h-0.5 bg-gray-200 -translate-y-1/2 z-0">
|
||||
<div
|
||||
className="absolute top-0 left-0 h-full bg-blue-600 transition-all duration-300"
|
||||
style={{
|
||||
width: `${((currentStep - 1) / (steps.length - 1)) * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 基础设置组件
|
||||
interface BasicSettingsProps {
|
||||
formData: CreateLikeTaskDataLocal;
|
||||
onChange: (data: Partial<CreateLikeTaskDataLocal>) => void;
|
||||
onNext: () => void;
|
||||
autoEnabled: boolean;
|
||||
setAutoEnabled: (v: boolean) => void;
|
||||
}
|
||||
|
||||
function BasicSettings({
|
||||
formData,
|
||||
onChange,
|
||||
onNext,
|
||||
autoEnabled,
|
||||
setAutoEnabled,
|
||||
}: BasicSettingsProps) {
|
||||
const handleContentTypeChange = (type: ContentType) => {
|
||||
const currentTypes = [...formData.contentTypes];
|
||||
if (currentTypes.includes(type)) {
|
||||
onChange({ contentTypes: currentTypes.filter(t => t !== type) });
|
||||
} else {
|
||||
onChange({ contentTypes: [...currentTypes, type] });
|
||||
}
|
||||
};
|
||||
|
||||
const incrementInterval = () => {
|
||||
onChange({ interval: Math.min(formData.interval + 5, 60) });
|
||||
};
|
||||
|
||||
const decrementInterval = () => {
|
||||
onChange({ interval: Math.max(formData.interval - 5, 5) });
|
||||
};
|
||||
|
||||
const incrementMaxLikes = () => {
|
||||
onChange({ maxLikes: Math.min(formData.maxLikes + 10, 500) });
|
||||
};
|
||||
|
||||
const decrementMaxLikes = () => {
|
||||
onChange({ maxLikes: Math.max(formData.maxLikes - 10, 10) });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 px-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="task-name">任务名称</Label>
|
||||
<Input
|
||||
id="task-name"
|
||||
placeholder="请输入任务名称"
|
||||
value={formData.name}
|
||||
onChange={e => onChange({ name: e.target.value })}
|
||||
className="h-12 rounded-xl border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="like-interval">点赞间隔</Label>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-l-xl border-gray-200 bg-white hover:bg-gray-50"
|
||||
onClick={decrementInterval}
|
||||
>
|
||||
<Minus className="h-5 w-5" />
|
||||
</Button>
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
id="like-interval"
|
||||
type="number"
|
||||
min={5}
|
||||
max={60}
|
||||
value={formData.interval.toString()}
|
||||
onChange={e =>
|
||||
onChange({ interval: Number.parseInt(e.target.value) || 5 })
|
||||
}
|
||||
className="h-12 rounded-none border-x-0 border-gray-200 text-center"
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none text-gray-500">
|
||||
秒
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-r-xl border-gray-200 bg-white hover:bg-gray-50"
|
||||
onClick={incrementInterval}
|
||||
>
|
||||
<Plus className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">设置两次点赞之间的最小时间间隔</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="max-likes">每日最大点赞数</Label>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-l-xl border-gray-200 bg-white hover:bg-gray-50"
|
||||
onClick={decrementMaxLikes}
|
||||
>
|
||||
<Minus className="h-5 w-5" />
|
||||
</Button>
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
id="max-likes"
|
||||
type="number"
|
||||
min={10}
|
||||
max={500}
|
||||
value={formData.maxLikes.toString()}
|
||||
onChange={e =>
|
||||
onChange({ maxLikes: Number.parseInt(e.target.value) || 10 })
|
||||
}
|
||||
className="h-12 rounded-none border-x-0 border-gray-200 text-center"
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none text-gray-500">
|
||||
次/天
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-r-xl border-gray-200 bg-white hover:bg-gray-50"
|
||||
onClick={incrementMaxLikes}
|
||||
>
|
||||
<Plus className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">设置每天最多点赞的次数</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>点赞时间范围</Label>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.startTime}
|
||||
onChange={e => onChange({ startTime: e.target.value })}
|
||||
className="h-12 rounded-xl border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.endTime}
|
||||
onChange={e => onChange({ endTime: e.target.value })}
|
||||
className="h-12 rounded-xl border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">设置每天可以点赞的时间段</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>点赞内容类型</Label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{ id: "text" as ContentType, label: "文字" },
|
||||
{ id: "image" as ContentType, label: "图片" },
|
||||
{ id: "video" as ContentType, label: "视频" },
|
||||
].map(type => (
|
||||
<div
|
||||
key={type.id}
|
||||
className={`flex items-center justify-center h-12 rounded-xl border cursor-pointer ${
|
||||
formData.contentTypes.includes(type.id)
|
||||
? "border-blue-500 bg-blue-50 text-blue-600"
|
||||
: "border-gray-200 text-gray-600"
|
||||
}`}
|
||||
onClick={() => handleContentTypeChange(type.id)}
|
||||
>
|
||||
{type.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">选择要点赞的内容类型</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 rounded-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="enable-friend-tags" className="cursor-pointer">
|
||||
启用好友标签
|
||||
</Label>
|
||||
<Switch
|
||||
id="enable-friend-tags"
|
||||
checked={formData.enableFriendTags}
|
||||
onCheckedChange={checked => onChange({ enableFriendTags: checked })}
|
||||
/>
|
||||
</div>
|
||||
{formData.enableFriendTags && (
|
||||
<>
|
||||
<div className="space-y-2 mt-4">
|
||||
<Label htmlFor="friend-tags">好友标签</Label>
|
||||
<Input
|
||||
id="friend-tags"
|
||||
placeholder="请输入标签"
|
||||
value={formData.friendTags || ""}
|
||||
onChange={e => onChange({ friendTags: e.target.value })}
|
||||
className="h-12 rounded-xl border-gray-200"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">只给有此标签的好友点赞</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<Label htmlFor="auto-enabled" className="cursor-pointer">
|
||||
自动开启
|
||||
</Label>
|
||||
<Switch
|
||||
id="auto-enabled"
|
||||
checked={autoEnabled}
|
||||
onCheckedChange={setAutoEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={onNext}
|
||||
className="w-full h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
125
nkebao/src/pages/mobile/workspace/auto-like/new/data.ts
Normal file
125
nkebao/src/pages/mobile/workspace/auto-like/new/data.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||
import { FriendSelectionItem } from "@/components/FriendSelection/data";
|
||||
|
||||
// 自动点赞任务状态
|
||||
export type LikeTaskStatus = 1 | 2; // 1: 开启, 2: 关闭
|
||||
|
||||
// 内容类型
|
||||
export type ContentType = "text" | "image" | "video" | "link";
|
||||
|
||||
// 设备信息
|
||||
export interface Device {
|
||||
id: string;
|
||||
name: string;
|
||||
status: "online" | "offline";
|
||||
lastActive: string;
|
||||
}
|
||||
|
||||
// 好友信息
|
||||
export interface Friend {
|
||||
id: string;
|
||||
nickname: string;
|
||||
wechatId: string;
|
||||
avatar: string;
|
||||
tags: string[];
|
||||
region: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
// 点赞记录
|
||||
export interface LikeRecord {
|
||||
id: string;
|
||||
workbenchId: string;
|
||||
momentsId: string;
|
||||
snsId: string;
|
||||
wechatAccountId: string;
|
||||
wechatFriendId: string;
|
||||
likeTime: string;
|
||||
content: string;
|
||||
resUrls: string[];
|
||||
momentTime: string;
|
||||
userName: string;
|
||||
operatorName: string;
|
||||
operatorAvatar: string;
|
||||
friendName: string;
|
||||
friendAvatar: string;
|
||||
}
|
||||
|
||||
// 自动点赞任务
|
||||
export interface LikeTask {
|
||||
id: string;
|
||||
name: string;
|
||||
status: LikeTaskStatus;
|
||||
deviceCount: number;
|
||||
targetGroup: string;
|
||||
likeCount: number;
|
||||
lastLikeTime: string;
|
||||
createTime: string;
|
||||
creator: string;
|
||||
likeInterval: number;
|
||||
maxLikesPerDay: number;
|
||||
timeRange: { start: string; end: string };
|
||||
contentTypes: ContentType[];
|
||||
targetTags: string[];
|
||||
devices: string[];
|
||||
friends: string[];
|
||||
friendMaxLikes: number;
|
||||
friendTags: string;
|
||||
enableFriendTags: boolean;
|
||||
todayLikeCount: number;
|
||||
totalLikeCount: number;
|
||||
updateTime: string;
|
||||
}
|
||||
|
||||
// 创建任务数据
|
||||
export interface CreateLikeTaskData {
|
||||
name: string;
|
||||
interval: number;
|
||||
maxLikes: number;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
contentTypes: ContentType[];
|
||||
deveiceGroups: string[];
|
||||
deveiceGroupsOptions: DeviceSelectionItem[];
|
||||
friendsGroups: string[];
|
||||
friendsGroupsOptions: FriendSelectionItem[];
|
||||
friendMaxLikes: number;
|
||||
friendTags?: string;
|
||||
enableFriendTags: boolean;
|
||||
targetTags: string[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// 更新任务数据
|
||||
export interface UpdateLikeTaskData extends CreateLikeTaskData {
|
||||
id: string;
|
||||
}
|
||||
|
||||
// 任务配置
|
||||
export interface TaskConfig {
|
||||
interval: number;
|
||||
maxLikes: number;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
contentTypes: ContentType[];
|
||||
devices: string[];
|
||||
friends: string[];
|
||||
friendMaxLikes: number;
|
||||
friendTags: string;
|
||||
enableFriendTags: boolean;
|
||||
}
|
||||
|
||||
// API响应类型
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
// 分页响应类型
|
||||
export interface PaginatedResponse<T> {
|
||||
list: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
@@ -12,17 +12,14 @@ import {
|
||||
updateAutoLikeTask,
|
||||
fetchAutoLikeTaskDetail,
|
||||
} from "./api";
|
||||
import {
|
||||
CreateLikeTaskData,
|
||||
ContentType,
|
||||
} from "@/pages/workspace/auto-like/record/data";
|
||||
import { CreateLikeTaskData, ContentType } from "./data";
|
||||
import style from "./new.module.scss";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
|
||||
const contentTypeLabels: Record<ContentType, string> = {
|
||||
text: "文字",
|
||||
image: "图片",
|
||||
video: "视频",
|
||||
link: "链接",
|
||||
};
|
||||
|
||||
const steps = [
|
||||
@@ -46,8 +43,10 @@ const NewAutoLike: React.FC = () => {
|
||||
startTime: "08:00",
|
||||
endTime: "22:00",
|
||||
contentTypes: ["text", "image", "video"],
|
||||
devices: [],
|
||||
friends: [],
|
||||
deveiceGroups: [],
|
||||
deveiceGroupsOptions: [],
|
||||
friendsGroups: [],
|
||||
friendsGroupsOptions: [],
|
||||
targetTags: [],
|
||||
friendMaxLikes: 10,
|
||||
enableFriendTags: false,
|
||||
@@ -55,10 +54,10 @@ const NewAutoLike: React.FC = () => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode && id) {
|
||||
if (id) {
|
||||
fetchTaskDetail();
|
||||
}
|
||||
}, [id, isEditMode]);
|
||||
}, [id]);
|
||||
|
||||
const fetchTaskDetail = async () => {
|
||||
setIsLoading(true);
|
||||
@@ -73,8 +72,10 @@ const NewAutoLike: React.FC = () => {
|
||||
startTime: config.timeRange?.start || config.startTime || "08:00",
|
||||
endTime: config.timeRange?.end || config.endTime || "22:00",
|
||||
contentTypes: config.contentTypes || ["text", "image", "video"],
|
||||
devices: config.devices || [],
|
||||
friends: config.friends || [],
|
||||
deveiceGroups: config.deveicegroups || [],
|
||||
deveiceGroupsOptions: config.deveiceGroupsOptions || [],
|
||||
friendsGroups: config.friendsgroups || [],
|
||||
friendsGroupsOptions: config.friendsGroupsOptions || [],
|
||||
targetTags: config.targetTags || [],
|
||||
friendMaxLikes: config.friendMaxLikes || 10,
|
||||
enableFriendTags: config.enableFriendTags || false,
|
||||
@@ -120,13 +121,13 @@ const NewAutoLike: React.FC = () => {
|
||||
message.warning("请输入任务名称");
|
||||
return;
|
||||
}
|
||||
if (!formData.devices || formData.devices.length === 0) {
|
||||
if (!formData.deveicegroups || formData.deveicegroups.length === 0) {
|
||||
message.warning("请选择执行设备");
|
||||
return;
|
||||
}
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
if (isEditMode && id) {
|
||||
if (isEditMode) {
|
||||
await updateAutoLikeTask({ ...formData, id });
|
||||
message.success("更新成功");
|
||||
} else {
|
||||
@@ -328,7 +329,7 @@ const NewAutoLike: React.FC = () => {
|
||||
<div className={style.basicSection}>
|
||||
<div className={style.formItem}>
|
||||
<DeviceSelection
|
||||
selectedOptions={formData.devices}
|
||||
selectedOptions={formData.deveicegroups}
|
||||
onSelect={devices => handleUpdateFormData({ devices })}
|
||||
showInput={true}
|
||||
showSelectedList={true}
|
||||
@@ -347,7 +348,7 @@ const NewAutoLike: React.FC = () => {
|
||||
onClick={handleNext}
|
||||
className={style.nextBtn}
|
||||
size="large"
|
||||
disabled={formData.devices.length === 0}
|
||||
disabled={formData.deveicegroups.length === 0}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
@@ -359,9 +360,14 @@ const NewAutoLike: React.FC = () => {
|
||||
<div className={style.basicSection}>
|
||||
<div className={style.formItem}>
|
||||
<FriendSelection
|
||||
selectedFriends={formData.friends || []}
|
||||
onSelect={friends => handleUpdateFormData({ friends })}
|
||||
deviceIds={formData.devices}
|
||||
selectedOptions={formData.friendsGroupsOptions || []}
|
||||
onSelect={friends =>
|
||||
handleUpdateFormData({
|
||||
friendsGroups: friends.map(f => String(f.id)),
|
||||
friendsGroupsOptions: friends,
|
||||
})
|
||||
}
|
||||
deviceIds={formData.deveiceGroups}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
@@ -378,7 +384,9 @@ const NewAutoLike: React.FC = () => {
|
||||
className={style.completeBtn}
|
||||
size="large"
|
||||
loading={isSubmitting}
|
||||
disabled={!formData.friends || formData.friends.length === 0}
|
||||
disabled={
|
||||
!formData.friendsgroups || formData.friendsgroups.length === 0
|
||||
}
|
||||
>
|
||||
{isEditMode ? "更新任务" : "创建任务"}
|
||||
</Button>
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { ThumbsUp, RefreshCw, Search } from "lucide-react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Avatar } from "@/components/ui/avatar";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import Layout from "@/components/Layout";
|
||||
import PageHeader from "@/components/PageHeader";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
import "@/components/Layout.css";
|
||||
import { fetchLikeRecords, LikeRecord } from "@/api/autoLike";
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
export default function AutoLikeDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { toast } = useToast();
|
||||
const [records, setRecords] = useState<LikeRecord[]>([]);
|
||||
const [recordsLoading, setRecordsLoading] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const pageSize = 10;
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
setRecordsLoading(true);
|
||||
fetchLikeRecords(id, 1, pageSize)
|
||||
.then(response => {
|
||||
setRecords(response.list || []);
|
||||
setTotal(response.total || 0);
|
||||
setCurrentPage(1);
|
||||
})
|
||||
.catch(() => {
|
||||
toast({
|
||||
title: "获取点赞记录失败",
|
||||
description: "请稍后重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
})
|
||||
.finally(() => setRecordsLoading(false));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id]);
|
||||
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1);
|
||||
fetchLikeRecords(id!, 1, pageSize, searchTerm)
|
||||
.then(response => {
|
||||
setRecords(response.list || []);
|
||||
setTotal(response.total || 0);
|
||||
setCurrentPage(1);
|
||||
})
|
||||
.catch(() => {
|
||||
toast({
|
||||
title: "获取点赞记录失败",
|
||||
description: "请稍后重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchLikeRecords(id!, currentPage, pageSize, searchTerm)
|
||||
.then(response => {
|
||||
setRecords(response.list || []);
|
||||
setTotal(response.total || 0);
|
||||
})
|
||||
.catch(() => {
|
||||
toast({
|
||||
title: "获取点赞记录失败",
|
||||
description: "请稍后重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
fetchLikeRecords(id!, newPage, pageSize, searchTerm)
|
||||
.then(response => {
|
||||
setRecords(response.list || []);
|
||||
setTotal(response.total || 0);
|
||||
setCurrentPage(newPage);
|
||||
})
|
||||
.catch(() => {
|
||||
toast({
|
||||
title: "获取点赞记录失败",
|
||||
description: "请稍后重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<PageHeader title="点赞记录" defaultBackPath="/workspace/auto-like" />
|
||||
<div className="flex items-center space-x-2 px-4 py-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索好友昵称或内容"
|
||||
className="pl-9"
|
||||
value={searchTerm}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
onKeyDown={e => e.key === "Enter" && handleSearch()}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleRefresh}
|
||||
disabled={recordsLoading}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${recordsLoading ? "animate-spin" : ""}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
{records.length > 0 && total > pageSize && (
|
||||
<div className="flex justify-center py-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
className="mx-1"
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<span className="mx-4 py-2 text-sm text-gray-500">
|
||||
第 {currentPage} 页,共 {Math.ceil(total / pageSize)} 页
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={currentPage >= Math.ceil(total / pageSize)}
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
className="mx-1"
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="bg-gray-50 min-h-screen pb-20">
|
||||
<div className="p-4 space-y-4">
|
||||
{recordsLoading ? (
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Card key={index} className="p-4">
|
||||
<div className="flex items-center space-x-3 mb-3">
|
||||
<Skeleton className="h-10 w-10 rounded-full" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-3 w-16" />
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-3" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<div className="flex space-x-2 mt-3">
|
||||
<Skeleton className="h-20 w-20" />
|
||||
<Skeleton className="h-20 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : records.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<ThumbsUp className="h-12 w-12 text-gray-300 mx-auto mb-3" />
|
||||
<p className="text-gray-500">暂无点赞记录</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{records.map(record => (
|
||||
<div
|
||||
key={record.id}
|
||||
className="p-4 mb-4 bg-white rounded-2xl shadow-sm"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-3 max-w-[65%]">
|
||||
<Avatar>
|
||||
<img
|
||||
src={
|
||||
record.friendAvatar ||
|
||||
"https://api.dicebear.com/7.x/avataaars/svg?seed=fallback"
|
||||
}
|
||||
alt={record.friendName}
|
||||
className="w-10 h-10 rounded-full"
|
||||
/>
|
||||
</Avatar>
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
className="font-medium truncate"
|
||||
title={record.friendName}
|
||||
>
|
||||
{record.friendName}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">内容发布者</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-blue-50 whitespace-nowrap shrink-0"
|
||||
>
|
||||
{formatDate(record.momentTime || record.likeTime)}
|
||||
</Badge>
|
||||
</div>
|
||||
<Separator className="my-3" />
|
||||
<div className="mb-3">
|
||||
{record.content && (
|
||||
<p className="text-gray-700 mb-3 whitespace-pre-line">
|
||||
{record.content}
|
||||
</p>
|
||||
)}
|
||||
{Array.isArray(record.resUrls) &&
|
||||
record.resUrls.length > 0 && (
|
||||
<div
|
||||
className={`grid gap-2 ${
|
||||
record.resUrls.length === 1
|
||||
? "grid-cols-1"
|
||||
: record.resUrls.length === 2
|
||||
? "grid-cols-2"
|
||||
: record.resUrls.length <= 3
|
||||
? "grid-cols-3"
|
||||
: record.resUrls.length <= 6
|
||||
? "grid-cols-3 grid-rows-2"
|
||||
: "grid-cols-3 grid-rows-3"
|
||||
}`}
|
||||
>
|
||||
{record.resUrls
|
||||
.slice(0, 9)
|
||||
.map((image: string, idx: number) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="relative aspect-square rounded-md overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt={`内容图片 ${idx + 1}`}
|
||||
className="object-cover w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center mt-4 p-2 bg-gray-50 rounded-md">
|
||||
<Avatar className="h-8 w-8 mr-2 shrink-0">
|
||||
<img
|
||||
src={
|
||||
record.operatorAvatar ||
|
||||
"https://api.dicebear.com/7.x/avataaars/svg?seed=operator"
|
||||
}
|
||||
alt={record.operatorName}
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
</Avatar>
|
||||
<div className="text-sm min-w-0">
|
||||
<span
|
||||
className="font-medium truncate inline-block max-w-full"
|
||||
title={record.operatorName}
|
||||
>
|
||||
{record.operatorName}
|
||||
</span>
|
||||
<span className="text-gray-500 ml-2">点赞了这条内容</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user