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

存一波
This commit is contained in:
笔记本里的永平
2025-07-22 13:55:20 +08:00
parent 28059d7e2b
commit 7e848b2081
7 changed files with 1238 additions and 368 deletions

View 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>
);
}

View File

@@ -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>
);

View File

@@ -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;
}
}

View 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>
);
}

View File

@@ -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"]}
/>
)}