feat: 本次提交更新内容如下

定版本转移2025年7月17日
This commit is contained in:
笔记本里的永平
2025-07-17 10:22:38 +08:00
parent 0f860d01e4
commit 92a3d407a7
645 changed files with 30755 additions and 118800 deletions

View File

@@ -1,305 +0,0 @@
import React, { useState } from "react";
import {
MoreVertical,
Eye,
Edit,
Copy,
Trash2,
Plus,
Search,
RefreshCw,
ThumbsUp,
ChevronLeft,
} from "lucide-react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { useNavigate } from "react-router-dom";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/toast";
import { fetchAutoLikeTasks, deleteAutoLikeTask, toggleAutoLikeTask, copyAutoLikeTask, LikeTask } from '@/api/autoLike';
type CardMenuProps = {
onView: () => void;
onEdit: () => void;
onCopy: () => void;
onDelete: () => void;
};
function CardMenu({ onView, onEdit, onCopy, onDelete }: CardMenuProps) {
const [open, setOpen] = React.useState(false);
const menuRef = React.useRef<HTMLDivElement | null>(null);
React.useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpen(false);
}
}
if (open) document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [open]);
return (
<div style={{ position: "relative" }}>
<button onClick={() => setOpen((v) => !v)} style={{ background: "none", border: "none", padding: 0, margin: 0, cursor: "pointer" }}>
<MoreVertical className="h-4 w-4" />
</button>
{open && (
<div
ref={menuRef}
style={{
position: "absolute",
right: 0,
top: 28,
background: "#fff",
borderRadius: 8,
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
zIndex: 100,
minWidth: 120,
padding: 4,
}}
>
<div onClick={() => { onView(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}>
<Eye className="h-4 w-4 mr-2" />
</div>
<div onClick={() => { onEdit(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}>
<Edit className="h-4 w-4 mr-2" />
</div>
<div onClick={() => { onCopy(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}>
<Copy className="h-4 w-4 mr-2" />
</div>
<div onClick={() => { onDelete(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, color: "#e53e3e", transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}>
<Trash2 className="h-4 w-4 mr-2" />
</div>
</div>
)}
</div>
);
}
export default function AutoLike() {
const navigate = useNavigate();
const { toast } = useToast();
const [tasks, setTasks] = React.useState<LikeTask[]>([]);
const [searchTerm, setSearchTerm] = useState("");
const [loading, setLoading] = useState(false);
// 1. fetchTasks 不用 useCallback直接定义
async function fetchTasks() {
setLoading(true);
try {
const list = await fetchAutoLikeTasks();
// 确保数据字段与旧项目一致
const mappedTasks = list.map(task => ({
...task,
// 确保字段名称和格式与旧项目一致
status: task.status || 2, // 默认为关闭状态
deviceCount: task.deviceCount || 0,
targetGroup: task.targetGroup || '默认人群',
likeCount: task.todayLikeCount || task.likeCount || 0,
creator: task.creator || '未知',
lastLikeTime: task.lastLikeTime || '暂无',
createTime: task.createTime || '未知',
likeInterval: task.likeInterval || 5,
maxLikesPerDay: task.maxLikesPerDay || 200,
timeRange: task.timeRange || { start: '08:00', end: '22:00' },
contentTypes: task.contentTypes || ['text', 'image', 'video'],
targetTags: task.targetTags || []
}));
setTasks(mappedTasks);
} catch (error) {
toast({ title: "获取任务失败", variant: "destructive" });
} finally {
setLoading(false);
}
}
// 2. useEffect 里直接调用
React.useEffect(() => {
fetchTasks();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const handleDelete = async (id: string) => {
if (!window.confirm("确定要删除该任务吗?")) return;
try {
const response = await deleteAutoLikeTask(id);
if (response.code === 200) {
toast({ title: "删除成功" });
fetchTasks();
} else {
toast({ title: "删除失败", description: response.msg || "请稍后重试", variant: "destructive" });
}
} catch (error) {
toast({ title: "删除失败", description: "请稍后重试", variant: "destructive" });
}
};
const handleEdit = (taskId: string) => {
navigate(`/workspace/auto-like/${taskId}/edit`);
};
const handleView = (taskId: string) => {
navigate(`/workspace/auto-like/${taskId}`);
};
const handleCopy = async (id: string) => {
try {
const response = await copyAutoLikeTask(id);
if (response.code === 200) {
toast({ title: "复制成功" });
fetchTasks();
} else {
toast({ title: "复制失败", description: response.msg || "请稍后重试", variant: "destructive" });
}
} catch (error) {
toast({ title: "复制失败", description: "请稍后重试", variant: "destructive" });
}
};
const toggleTaskStatus = async (id: string, status: number) => {
// 先更新本地状态
const newStatus = (status === 1 ? 2 : 1) as 1 | 2;
setTasks(prevTasks =>
prevTasks.map(task =>
task.id === id ? { ...task, status: newStatus } : task
)
);
try {
const response = await toggleAutoLikeTask(id, String(newStatus));
if (response.code === 200) {
toast({ title: "操作成功" });
// 成功时不刷新列表,保持本地状态
} else {
// 请求失败,回退本地状态
setTasks(prevTasks =>
prevTasks.map(task =>
task.id === id ? { ...task, status: status as 1 | 2 } : task
)
);
toast({ title: "操作失败", description: response.msg || "请稍后重试", variant: "destructive" });
}
} catch (error) {
// 请求异常,回退本地状态
setTasks(prevTasks =>
prevTasks.map(task =>
task.id === id ? { ...task, status: status as 1 | 2 } : task
)
);
toast({ title: "操作失败", description: "请稍后重试", variant: "destructive" });
}
};
const handleCreateNew = () => {
navigate("/workspace/auto-like/new");
};
const filteredTasks = tasks.filter((task) =>
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
return (
<div className="flex-1 bg-gray-50 min-h-screen pb-20">
<header className="sticky top-0 z-10 bg-white border-b">
<div className="flex items-center justify-between p-4">
<div className="flex items-center space-x-3">
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
<ChevronLeft className="h-5 w-5" />
</Button>
<h1 className="text-lg font-medium"></h1>
</div>
<Button onClick={handleCreateNew}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</header>
<div className="p-4">
<Card className="p-4 mb-4">
<div className="flex items-center space-x-2">
<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)} />
</div>
<Button variant="outline" size="icon" onClick={fetchTasks} disabled={loading}>
{loading ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
</Button>
</div>
</Card>
<div className="space-y-4">
{filteredTasks.map((task) => (
<Card key={task.id} className="p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<h3 className="font-medium">{task.name}</h3>
<Badge variant={Number(task.status) === 1 ? "success" : "secondary"}>
{Number(task.status) === 1 ? "进行中" : "已暂停"}
</Badge>
</div>
<div className="flex items-center space-x-2">
<Switch checked={Number(task.status) === 1} onCheckedChange={() => toggleTaskStatus(task.id, Number(task.status))} />
<CardMenu
onView={() => handleView(task.id)}
onEdit={() => handleEdit(task.id)}
onCopy={() => handleCopy(task.id)}
onDelete={() => handleDelete(task.id)}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-6 mb-6">
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500 font-medium"></span>
<span className="text-sm text-gray-900 font-semibold">{task.deviceCount} </span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500 font-medium"></span>
<span className="text-sm text-gray-900 font-semibold">{task.targetGroup}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500 font-medium"></span>
<span className="text-sm text-gray-900 font-semibold">{task.updateTime}</span>
</div>
</div>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500 font-medium"></span>
<span className="text-sm text-gray-900 font-semibold">{task.likeInterval} </span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500 font-medium"></span>
<span className="text-sm text-gray-900 font-semibold">{task.maxLikesPerDay} </span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500 font-medium"></span>
<span className="text-sm text-gray-900 font-semibold">{task.createTime}</span>
</div>
</div>
</div>
<div className="flex items-center justify-between text-sm text-gray-500 border-t pt-5">
<div className="flex items-center space-x-2">
<ThumbsUp className="w-4 h-4 text-blue-500" />
<span className="font-medium"></span>
<span className="text-gray-900 font-semibold">{task.lastLikeTime}</span>
</div>
<div className="flex items-center space-x-2">
<ThumbsUp className="w-4 h-4 text-green-500" />
<span className="font-medium"></span>
<span className="text-gray-900 font-semibold">{task.totalLikeCount || 0}</span>
</div>
</div>
</Card>
))}
</div>
</div>
</div>
);
}

View File

@@ -1,281 +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>
);
}

View File

@@ -1,552 +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
selectedDevices={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>
);
}