feat: 本次提交更新内容如下
存一波
This commit is contained in:
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
552
nkebao/src/pages/workspace/auto-like/new/NewAutoLike.tsx
Normal file
552
nkebao/src/pages/workspace/auto-like/new/NewAutoLike.tsx
Normal file
@@ -0,0 +1,552 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Button, Input, Switch, message, Spin } from "antd";
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
PlusOutlined,
|
||||
MinusOutlined,
|
||||
ArrowLeftOutlined,
|
||||
CheckOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Button, Input, Switch, message, Spin } from "antd";
|
||||
import { NavBar } from "antd-mobile";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import {
|
||||
@@ -13,11 +14,7 @@ import {
|
||||
updateAutoLikeTask,
|
||||
fetchAutoLikeTaskDetail,
|
||||
} from "./api";
|
||||
import {
|
||||
CreateLikeTaskData,
|
||||
UpdateLikeTaskData,
|
||||
ContentType,
|
||||
} from "@/types/auto-like";
|
||||
import { ContentType } from "@/types/auto-like";
|
||||
import style from "./new.module.scss";
|
||||
|
||||
const contentTypeLabels: Record<ContentType, string> = {
|
||||
@@ -27,28 +24,32 @@ const contentTypeLabels: Record<ContentType, string> = {
|
||||
link: "链接",
|
||||
};
|
||||
|
||||
const steps = ["基础设置", "设备选择", "人群选择"];
|
||||
|
||||
const defaultForm = {
|
||||
name: "",
|
||||
interval: 5,
|
||||
maxLikes: 200,
|
||||
startTime: "08:00",
|
||||
endTime: "22:00",
|
||||
contentTypes: ["text", "image", "video"] as ContentType[],
|
||||
devices: [] as string[],
|
||||
friends: [] as string[],
|
||||
targetTags: [] as string[],
|
||||
friendMaxLikes: 10,
|
||||
enableFriendTags: false,
|
||||
friendTags: "",
|
||||
};
|
||||
|
||||
const NewAutoLike: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const isEditMode = !!id;
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(isEditMode);
|
||||
const [autoEnabled, setAutoEnabled] = useState(false);
|
||||
const [formData, setFormData] = useState<CreateLikeTaskData>({
|
||||
name: "",
|
||||
interval: 5,
|
||||
maxLikes: 200,
|
||||
startTime: "08:00",
|
||||
endTime: "22:00",
|
||||
contentTypes: ["text", "image", "video"],
|
||||
devices: [],
|
||||
friends: [],
|
||||
targetTags: [],
|
||||
friendMaxLikes: 10,
|
||||
enableFriendTags: false,
|
||||
friendTags: "",
|
||||
});
|
||||
const [formData, setFormData] = useState({ ...defaultForm });
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode && id) {
|
||||
@@ -89,12 +90,12 @@ const NewAutoLike: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateFormData = (data: Partial<CreateLikeTaskData>) => {
|
||||
const handleUpdateFormData = (data: Partial<typeof formData>) => {
|
||||
setFormData((prev) => ({ ...prev, ...data }));
|
||||
};
|
||||
|
||||
const handleNext = () => setCurrentStep((prev) => Math.min(prev + 1, 3));
|
||||
const handlePrev = () => setCurrentStep((prev) => Math.max(prev - 1, 1));
|
||||
const handleNext = () => setCurrentStep((prev) => Math.min(prev + 1, 2));
|
||||
const handlePrev = () => setCurrentStep((prev) => Math.max(prev - 1, 0));
|
||||
|
||||
const handleComplete = async () => {
|
||||
if (!formData.name.trim()) {
|
||||
@@ -124,90 +125,104 @@ const NewAutoLike: React.FC = () => {
|
||||
|
||||
// 步骤1:基础设置
|
||||
const renderBasicSettings = () => (
|
||||
<div className={style["form-section"]}>
|
||||
<div className={style["form-item"]}>
|
||||
<label className={style["form-label"]}>任务名称</label>
|
||||
<div className={style.formStep}>
|
||||
<div className={style.formItem}>
|
||||
<div className={style.formLabel}>任务名称</div>
|
||||
<Input
|
||||
placeholder="请输入任务名称"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleUpdateFormData({ name: e.target.value })}
|
||||
className={style["form-input"]}
|
||||
className={style.input}
|
||||
/>
|
||||
</div>
|
||||
<div className={style["form-item"]}>
|
||||
<label className={style["form-label"]}>点赞间隔</label>
|
||||
<div className={style["stepper-group"]}>
|
||||
<Button
|
||||
icon={<MinusOutlined />}
|
||||
|
||||
<div className={style.formItem}>
|
||||
<div className={style.formLabel}>点赞间隔</div>
|
||||
<div className={style.counterRow}>
|
||||
<button
|
||||
type="button"
|
||||
className={style.counterBtn}
|
||||
onClick={() =>
|
||||
handleUpdateFormData({
|
||||
interval: Math.max(1, formData.interval - 1),
|
||||
})
|
||||
}
|
||||
className={style["stepper-btn"]}
|
||||
/>
|
||||
<span className={style["stepper-value"]}>{formData.interval} 秒</span>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
<MinusOutlined />
|
||||
</button>
|
||||
<span className={style.counterValue}>{formData.interval} 秒</span>
|
||||
<button
|
||||
type="button"
|
||||
className={style.counterBtn}
|
||||
onClick={() =>
|
||||
handleUpdateFormData({ interval: formData.interval + 1 })
|
||||
}
|
||||
className={style["stepper-btn"]}
|
||||
/>
|
||||
>
|
||||
<PlusOutlined />
|
||||
</button>
|
||||
</div>
|
||||
<div className={style.counterTip}>设置两次点赞之间的最小时间间隔</div>
|
||||
</div>
|
||||
<div className={style["form-item"]}>
|
||||
<label className={style["form-label"]}>每日上限</label>
|
||||
<div className={style["stepper-group"]}>
|
||||
<Button
|
||||
icon={<MinusOutlined />}
|
||||
|
||||
<div className={style.formItem}>
|
||||
<div className={style.formLabel}>每日最大点赞数</div>
|
||||
<div className={style.counterRow}>
|
||||
<button
|
||||
type="button"
|
||||
className={style.counterBtn}
|
||||
onClick={() =>
|
||||
handleUpdateFormData({
|
||||
maxLikes: Math.max(1, formData.maxLikes - 10),
|
||||
})
|
||||
}
|
||||
className={style["stepper-btn"]}
|
||||
/>
|
||||
<span className={style["stepper-value"]}>{formData.maxLikes} 次</span>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
<MinusOutlined />
|
||||
</button>
|
||||
<span className={style.counterValue}>{formData.maxLikes} 次</span>
|
||||
<button
|
||||
type="button"
|
||||
className={style.counterBtn}
|
||||
onClick={() =>
|
||||
handleUpdateFormData({ maxLikes: formData.maxLikes + 10 })
|
||||
}
|
||||
className={style["stepper-btn"]}
|
||||
/>
|
||||
>
|
||||
<PlusOutlined />
|
||||
</button>
|
||||
</div>
|
||||
<div className={style.counterTip}>设置每天最多点赞的次数</div>
|
||||
</div>
|
||||
<div className={style["form-item"]}>
|
||||
<label className={style["form-label"]}>执行时间</label>
|
||||
<div className={style["time-range"]}>
|
||||
|
||||
<div className={style.formItem}>
|
||||
<div className={style.formLabel}>点赞时间范围</div>
|
||||
<div className={style.timeRow}>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.startTime}
|
||||
onChange={(e) =>
|
||||
handleUpdateFormData({ startTime: e.target.value })
|
||||
}
|
||||
className={style["time-input"]}
|
||||
className={style.inputTime}
|
||||
/>
|
||||
<span className={style["time-separator"]}>至</span>
|
||||
<span className={style.timeTo}>至</span>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.endTime}
|
||||
onChange={(e) => handleUpdateFormData({ endTime: e.target.value })}
|
||||
className={style["time-input"]}
|
||||
className={style.inputTime}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["form-item"]}>
|
||||
<label className={style["form-label"]}>内容类型</label>
|
||||
<div className={style["content-types"]}>
|
||||
{(["text", "image", "video", "link"] as ContentType[]).map((type) => (
|
||||
|
||||
<div className={style.formItem}>
|
||||
<div className={style.formLabel}>点赞内容类型</div>
|
||||
<div className={style.contentTypes}>
|
||||
{(["text", "image", "video"] as ContentType[]).map((type) => (
|
||||
<span
|
||||
key={type}
|
||||
className={
|
||||
formData.contentTypes.includes(type)
|
||||
? style["content-type-tag-active"]
|
||||
: style["content-type-tag"]
|
||||
? style.contentTypeTagActive
|
||||
: style.contentTypeTag
|
||||
}
|
||||
onClick={() => {
|
||||
const newTypes = formData.contentTypes.includes(type)
|
||||
@@ -221,78 +236,109 @@ const NewAutoLike: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["form-item"]}>
|
||||
<label className={style["form-label"]}>自动开启</label>
|
||||
<Switch checked={autoEnabled} onChange={setAutoEnabled} />
|
||||
|
||||
<div className={style.formItem}>
|
||||
<div className={style.switchRow}>
|
||||
<span className={style.switchLabel}>启用好友标签</span>
|
||||
<Switch
|
||||
checked={formData.enableFriendTags}
|
||||
onChange={(checked) =>
|
||||
handleUpdateFormData({ enableFriendTags: checked })
|
||||
}
|
||||
className={style.switch}
|
||||
/>
|
||||
</div>
|
||||
{formData.enableFriendTags && (
|
||||
<div className={style.formItem}>
|
||||
<div className={style.formLabel}>好友标签</div>
|
||||
<Input
|
||||
placeholder="请输入标签"
|
||||
value={formData.friendTags}
|
||||
onChange={(e) =>
|
||||
handleUpdateFormData({ friendTags: e.target.value })
|
||||
}
|
||||
className={style.input}
|
||||
/>
|
||||
<div className={style.counterTip}>只给有此标签的好友点赞</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={style["form-actions"]}>
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
onClick={handleNext}
|
||||
size="large"
|
||||
className={style["main-btn"]}
|
||||
>
|
||||
|
||||
<div className={style.formItem}>
|
||||
<div className={style.switchRow}>
|
||||
<span className={style.switchLabel}>自动开启</span>
|
||||
<Switch
|
||||
checked={autoEnabled}
|
||||
onChange={setAutoEnabled}
|
||||
className={style.switch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.formStepBtnRow}>
|
||||
<Button type="primary" onClick={handleNext} className={style.nextBtn}>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 步骤2:设备选择(占位)
|
||||
// 步骤2:设备选择
|
||||
const renderDeviceSelection = () => (
|
||||
<div className={style["form-section"]}>
|
||||
<div className={style["placeholder-content"]}>
|
||||
<span className={style["placeholder-icon"]}>[设备选择组件占位]</span>
|
||||
<div className={style["placeholder-text"]}>设备选择功能开发中...</div>
|
||||
<div className={style["placeholder-subtext"]}>
|
||||
当前已选择 {formData.devices?.length || 0} 个设备
|
||||
</div>
|
||||
<div className={style.formStep}>
|
||||
<div className={style.formItem}>
|
||||
<div className={style.formLabel}>选择设备</div>
|
||||
<Input
|
||||
placeholder="请选择设备"
|
||||
value={formData.devices.join(", ")}
|
||||
readOnly
|
||||
onClick={() => message.info("这里应弹出设备选择器")}
|
||||
className={style.input}
|
||||
/>
|
||||
{formData.devices.length > 0 && (
|
||||
<div className={style.selectedTip}>
|
||||
已选设备: {formData.devices.length}个
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={style["form-actions"]}>
|
||||
<Button
|
||||
onClick={handlePrev}
|
||||
size="large"
|
||||
className={style["secondary-btn"]}
|
||||
>
|
||||
<div className={style.formStepBtnRow}>
|
||||
<Button onClick={handlePrev} className={style.prevBtn}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleNext}
|
||||
size="large"
|
||||
className={style["main-btn"]}
|
||||
>
|
||||
<Button type="primary" onClick={handleNext} className={style.nextBtn}>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 步骤3:好友设置(占位)
|
||||
const renderFriendSettings = () => (
|
||||
<div className={style["form-section"]}>
|
||||
<div className={style["placeholder-content"]}>
|
||||
<span className={style["placeholder-icon"]}>[好友选择组件占位]</span>
|
||||
<div className={style["placeholder-text"]}>好友设置功能开发中...</div>
|
||||
<div className={style["placeholder-subtext"]}>
|
||||
当前已选择 {formData.friends?.length || 0} 个好友
|
||||
</div>
|
||||
// 步骤3:人群选择
|
||||
const renderFriendSelection = () => (
|
||||
<div className={style.formStep}>
|
||||
<div className={style.formItem}>
|
||||
<div className={style.formLabel}>选择微信好友</div>
|
||||
<Input
|
||||
placeholder="请选择微信好友"
|
||||
value={formData.friends.join(", ")}
|
||||
readOnly
|
||||
onClick={() => message.info("这里应弹出好友选择器")}
|
||||
className={style.input}
|
||||
/>
|
||||
{formData.friends.length > 0 && (
|
||||
<div className={style.selectedTip}>
|
||||
已选好友: {formData.friends.length}个
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={style["form-actions"]}>
|
||||
<Button
|
||||
onClick={handlePrev}
|
||||
size="large"
|
||||
className={style["secondary-btn"]}
|
||||
>
|
||||
<div className={style.formStepBtnRow}>
|
||||
<Button onClick={handlePrev} className={style.prevBtn}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleComplete}
|
||||
size="large"
|
||||
loading={isSubmitting}
|
||||
className={style["main-btn"]}
|
||||
className={style.completeBtn}
|
||||
>
|
||||
{isEditMode ? "更新任务" : "创建任务"}
|
||||
</Button>
|
||||
@@ -321,21 +367,39 @@ const NewAutoLike: React.FC = () => {
|
||||
</NavBar>
|
||||
}
|
||||
>
|
||||
<div className={style["new-page-bg"]}>
|
||||
<div className={style["new-page-center"]}>
|
||||
{/* 步骤器保留新项目的 */}
|
||||
{/* 你可以在这里插入新项目的步骤器组件 */}
|
||||
<div className={style["form-card"]}>
|
||||
{currentStep === 1 && renderBasicSettings()}
|
||||
{currentStep === 2 && renderDeviceSelection()}
|
||||
{currentStep === 3 && renderFriendSettings()}
|
||||
{isLoading && (
|
||||
<div className={style["loading"]}>
|
||||
<Spin />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={style.formBg}>
|
||||
<div className={style.formSteps}>
|
||||
{steps.map((s, i) => (
|
||||
<div
|
||||
key={s}
|
||||
className={
|
||||
style.formStepIndicator +
|
||||
" " +
|
||||
(i === currentStep
|
||||
? style.formStepActive
|
||||
: i < currentStep
|
||||
? style.formStepDone
|
||||
: "")
|
||||
}
|
||||
>
|
||||
<span className={style.formStepNum}>
|
||||
{i < currentStep ? <CheckOutlined /> : i + 1}
|
||||
</span>
|
||||
<span>{s}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className={style.formLoading}>
|
||||
<Spin />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{currentStep === 0 && renderBasicSettings()}
|
||||
{currentStep === 1 && renderDeviceSelection()}
|
||||
{currentStep === 2 && renderFriendSelection()}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -1,250 +1,227 @@
|
||||
.new-page-bg {
|
||||
.formBg {
|
||||
background: #f8f6f3;
|
||||
min-height: 100vh;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 56px;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 0 #f0f0f0;
|
||||
padding: 0 24px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.nav-back-btn {
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 20px;
|
||||
color: #222;
|
||||
margin-right: 8px;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.new-page-center {
|
||||
padding: 32px 0 32px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.formSteps {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.formStepIndicator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #bbb;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.formStepActive {
|
||||
color: #188eee;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.formStepDone {
|
||||
color: #19c37d;
|
||||
}
|
||||
|
||||
.formStepNum {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: #e5e7eb;
|
||||
color: #888;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.formStepActive .formStepNum {
|
||||
background: #188eee;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.formStepDone .formStepNum {
|
||||
background: #19c37d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.formStep {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
padding: 32px 24px 24px 24px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
margin: 0 auto 24px auto;
|
||||
}
|
||||
|
||||
.formItem {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.formLabel {
|
||||
font-size: 15px;
|
||||
color: #222;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.input {
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.timeRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inputTime {
|
||||
width: 90px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.timeTo {
|
||||
margin: 0 8px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.counterRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.counterBtn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
font-size: 16px;
|
||||
color: #188eee;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
|
||||
.counterBtn:hover {
|
||||
border: 1px solid #188eee;
|
||||
}
|
||||
|
||||
.counterValue {
|
||||
width: 48px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.counterTip {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.contentTypes {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.contentTypeTag {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.contentTypeTag:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.contentTypeTagActive {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
background: #e6f7ff;
|
||||
color: #188eee;
|
||||
border: 1px solid #91d5ff;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.switchRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.switchLabel {
|
||||
font-size: 15px;
|
||||
color: #222;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.switch {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.selectedTip {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.formStepBtnRow {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 2px 16px rgba(0,0,0,0.06);
|
||||
padding: 36px 32px 32px 32px;
|
||||
min-width: 340px;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
border-radius: 10px !important;
|
||||
.prevBtn {
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
padding-left: 14px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e5e6eb;
|
||||
transition: border 0.2s;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: #1890ff;
|
||||
background: #fff;
|
||||
.nextBtn {
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.stepper-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
.completeBtn {
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.stepper-btn {
|
||||
border-radius: 8px !important;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 18px;
|
||||
background: #f5f6fa;
|
||||
border: 1px solid #e5e6eb;
|
||||
color: #222;
|
||||
.formLoading {
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
|
||||
.stepper-btn:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.stepper-value {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.time-range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.time-input {
|
||||
border-radius: 10px !important;
|
||||
height: 44px;
|
||||
font-size: 15px;
|
||||
padding-left: 14px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e5e6eb;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.time-separator {
|
||||
font-size: 15px;
|
||||
color: #888;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content-types {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.content-type-tag {
|
||||
border-radius: 8px;
|
||||
background: #f5f6fa;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
padding: 6px 18px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #e5e6eb;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.content-type-tag-active {
|
||||
border-radius: 8px;
|
||||
background: #e6f4ff;
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
padding: 6px 18px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #1890ff;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.main-btn {
|
||||
border-radius: 10px !important;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
background: #1890ff;
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px rgba(24,144,255,0.08);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.main-btn:hover {
|
||||
background: #1677ff;
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
border-radius: 10px !important;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
background: #fff;
|
||||
border: 1.5px solid #e5e6eb;
|
||||
color: #222;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
|
||||
.secondary-btn:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
padding: 40px 0 24px 0;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 32px;
|
||||
color: #d9d9d9;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.placeholder-subtext {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.form-card {
|
||||
min-width: 0;
|
||||
max-width: 100vw;
|
||||
padding: 18px 6px 18px 6px;
|
||||
}
|
||||
.new-page-center {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
281
nkebao/src/pages/workspace/auto-like/record/AutoLikeDetail.tsx
Normal file
281
nkebao/src/pages/workspace/auto-like/record/AutoLikeDetail.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -125,9 +125,8 @@ const AutoLikeDetail: React.FC = () => {
|
||||
"https://api.dicebear.com/7.x/avataaars/svg?seed=fallback"
|
||||
}
|
||||
className={style["user-avatar"]}
|
||||
>
|
||||
<UserOutlined />
|
||||
</Avatar>
|
||||
fallback={<UserOutlined />}
|
||||
/>
|
||||
<div className={style["user-details"]}>
|
||||
<div className={style["user-name"]} title={record.friendName}>
|
||||
{record.friendName}
|
||||
@@ -167,9 +166,8 @@ const AutoLikeDetail: React.FC = () => {
|
||||
"https://api.dicebear.com/7.x/avataaars/svg?seed=operator"
|
||||
}
|
||||
className={style["operator-avatar"]}
|
||||
>
|
||||
<UserOutlined />
|
||||
</Avatar>
|
||||
fallback={<UserOutlined />}
|
||||
/>
|
||||
<div className={style["like-text"]}>
|
||||
<span className={style["operator-name"]} title={record.operatorName}>
|
||||
{record.operatorName}
|
||||
@@ -194,7 +192,7 @@ const AutoLikeDetail: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
footer={<MeauMobile activeKey="workspace" />}
|
||||
>
|
||||
<div className={style["detail-page"]}>
|
||||
{/* 任务信息卡片 */}
|
||||
@@ -284,7 +282,7 @@ const AutoLikeDetail: React.FC = () => {
|
||||
data={records}
|
||||
renderItem={renderRecordItem}
|
||||
hasMore={hasMore}
|
||||
loadMore={handleLoadMore}
|
||||
onLoadMore={handleLoadMore}
|
||||
className={style["records-list"]}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user