FEAT => 本次更新项目为:统一组件接口,将属性名称从 selectedGroups 更改为 selectedOptions,以提升代码一致性和可读性,并对相关组件进行了相应调整。

This commit is contained in:
超级老白兔
2025-08-11 10:49:02 +08:00
parent 0af7969fcc
commit 8c9a660afd
7 changed files with 155 additions and 920 deletions

View File

@@ -202,7 +202,7 @@ export default function ContentForm() {
</Tabs.Tab>
<Tabs.Tab title="选择聊天群" key="groups">
<GroupSelection
selectedGroups={selectedGroupsOptions}
selectedOptions={selectedGroupsOptions}
onSelect={handleGroupsChange}
placeholder="选择聊天群"
/>

View File

@@ -438,7 +438,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
<div className={styles["basic-group-selection"]}>
<div className={styles["basic-label"]}></div>
<GroupSelection
selectedGroups={formData.groupSelected || []}
selectedOptions={formData.groupSelected || []}
onSelect={groups =>
onChange({ ...formData, groupSelected: groups })
}

View File

@@ -450,7 +450,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
{message.type === "group" && (
<div style={{ marginBottom: 8 }}>
<GroupSelection
selectedGroups={message.groupIds || []}
selectedOptions={message.groupIds || []}
onSelect={groupIds =>
handleUpdateMessage(dayIndex, messageIndex, {
groupIds: groupIds,

View File

@@ -1,594 +0,0 @@
import React, { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
ChevronLeft,
Plus,
Minus,
Check,
X,
Tag as TagIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
createAutoLikeTask,
updateAutoLikeTask,
fetchAutoLikeTaskDetail,
} from "@/api/autoLike";
import { ContentType } from "@/types/auto-like";
import { useToast } from "@/components/ui/toast";
import Layout from "@/components/Layout";
import DeviceSelection from "@/components/DeviceSelection";
import FriendSelection from "@/components/FriendSelection";
// 修改CreateLikeTaskData接口确保friends字段不是可选的
interface CreateLikeTaskDataLocal {
name: string;
interval: number;
maxLikes: number;
startTime: string;
endTime: string;
contentTypes: ContentType[];
devices: string[];
friends: string[];
friendMaxLikes: number;
friendTags: string;
enableFriendTags: boolean;
targetTags: string[];
}
export default function NewAutoLike() {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const isEditMode = !!id;
const { toast } = useToast();
const [currentStep, setCurrentStep] = useState(1);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isLoading, setIsLoading] = useState(isEditMode);
const [formData, setFormData] = useState<CreateLikeTaskDataLocal>({
name: "",
interval: 5,
maxLikes: 200,
startTime: "08:00",
endTime: "22:00",
contentTypes: ["text", "image", "video"],
devices: [],
friends: [], // 确保初始化为空数组而不是undefined
targetTags: [],
friendMaxLikes: 10,
enableFriendTags: false,
friendTags: "",
});
// 新增自动开启的独立状态
const [autoEnabled, setAutoEnabled] = useState(false);
// 如果是编辑模式,获取任务详情
useEffect(() => {
if (isEditMode && id) {
fetchTaskDetail();
}
}, [id, isEditMode]);
// 获取任务详情
const fetchTaskDetail = async () => {
try {
const taskDetail = await fetchAutoLikeTaskDetail(id!);
console.log("Task detail response:", taskDetail); // 添加日志用于调试
if (taskDetail) {
// 使用类型断言处理可能的字段名称差异
const taskAny = taskDetail as any;
// 处理可能的嵌套结构
const config = taskAny.config || taskAny;
setFormData({
name: taskDetail.name || "",
interval: config.likeInterval || config.interval || 5,
maxLikes: config.maxLikesPerDay || config.maxLikes || 200,
startTime: config.timeRange?.start || config.startTime || "08:00",
endTime: config.timeRange?.end || config.endTime || "22:00",
contentTypes: config.contentTypes || ["text", "image", "video"],
devices: config.devices || [],
friends: config.friends || [],
targetTags: config.targetTags || [],
friendMaxLikes: config.friendMaxLikes || 10,
enableFriendTags: config.enableFriendTags || false,
friendTags: config.friendTags || "",
});
// 处理状态字段,使用双等号允许类型自动转换
const status = taskAny.status;
setAutoEnabled(status === 1 || status === "running");
} else {
toast({
title: "获取任务详情失败",
description: "无法找到该任务",
variant: "destructive",
});
navigate("/workspace/auto-like");
}
} catch (error) {
console.error("获取任务详情出错:", error); // 添加错误日志
toast({
title: "获取任务详情失败",
description: "请检查网络连接后重试",
variant: "destructive",
});
navigate("/workspace/auto-like");
} finally {
setIsLoading(false);
}
};
const handleUpdateFormData = (data: Partial<CreateLikeTaskDataLocal>) => {
setFormData(prev => ({ ...prev, ...data }));
};
const handleNext = () => {
setCurrentStep(prev => Math.min(prev + 1, 3));
// 滚动到顶部
const mainElement = document.querySelector("main");
if (mainElement) {
mainElement.scrollTo({ top: 0, behavior: "smooth" });
}
};
const handlePrev = () => {
setCurrentStep(prev => Math.max(prev - 1, 1));
// 滚动到顶部
const mainElement = document.querySelector("main");
if (mainElement) {
mainElement.scrollTo({ top: 0, behavior: "smooth" });
}
};
const handleComplete = async () => {
if (isSubmitting) return;
setIsSubmitting(true);
try {
// 转换为API需要的格式
const apiFormData = {
...formData,
// 如果API需要其他转换可以在这里添加
};
let response;
if (isEditMode) {
// 编辑模式调用更新API
response = await updateAutoLikeTask({
...apiFormData,
id: id!,
});
} else {
// 新建模式调用创建API
response = await createAutoLikeTask(apiFormData);
}
if (response.code === 200) {
toast({
title: isEditMode ? "更新成功" : "创建成功",
description: isEditMode
? "自动点赞任务已更新"
: "自动点赞任务已创建并开始执行",
});
navigate("/workspace/auto-like");
} else {
toast({
title: isEditMode ? "更新失败" : "创建失败",
description: response.msg || "请稍后重试",
variant: "destructive",
});
}
} catch (error) {
toast({
title: isEditMode ? "更新失败" : "创建失败",
description: "请检查网络连接后重试",
variant: "destructive",
});
} finally {
setIsSubmitting(false);
}
};
const header = (
<div className="sticky top-0 z-10 bg-white pb-4">
<div className="flex items-center h-14 px-4">
<Button
variant="ghost"
size="icon"
onClick={() => navigate(-1)}
className="hover:bg-gray-50"
>
<ChevronLeft className="h-6 w-6" />
</Button>
<h1 className="ml-2 text-lg font-medium">
{isEditMode ? "编辑自动点赞" : "新建自动点赞"}
</h1>
</div>
<StepIndicator currentStep={currentStep} />
</div>
);
if (isLoading) {
return (
<Layout header={header}>
<div className="flex items-center justify-center h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
<p className="mt-4 text-gray-500">...</p>
</div>
</div>
</Layout>
);
}
return (
<Layout header={header}>
<div className="min-h-screen bg-[#F8F9FA]">
<div className="pt-4">
<div className="mt-8">
{currentStep === 1 && (
<BasicSettings
formData={formData}
onChange={handleUpdateFormData}
onNext={handleNext}
autoEnabled={autoEnabled}
setAutoEnabled={setAutoEnabled}
/>
)}
{currentStep === 2 && (
<div className="space-y-6 px-6">
<DeviceSelection
selectedOptions={formData.devices}
onSelect={devices => handleUpdateFormData({ devices })}
placeholder="选择设备"
/>
<div className="flex space-x-4">
<Button
variant="outline"
className="flex-1 h-12 rounded-xl text-base"
onClick={handlePrev}
>
</Button>
<Button
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
onClick={handleNext}
disabled={formData.devices.length === 0}
>
</Button>
</div>
</div>
)}
{currentStep === 3 && (
<div className="px-6 space-y-6">
<FriendSelection
selectedFriends={formData.friends || []}
onSelect={friends => handleUpdateFormData({ friends })}
deviceIds={formData.devices}
placeholder="选择微信好友"
/>
<div className="flex space-x-4">
<Button
variant="outline"
className="flex-1 h-12 rounded-xl text-base"
onClick={handlePrev}
>
</Button>
<Button
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
onClick={handleComplete}
>
</Button>
</div>
</div>
)}
</div>
</div>
</div>
</Layout>
);
}
// 步骤指示器组件
interface StepIndicatorProps {
currentStep: number;
}
function StepIndicator({ currentStep }: StepIndicatorProps) {
const steps = [
{ title: "基础设置", description: "设置点赞规则" },
{ title: "设备选择", description: "选择执行设备" },
{ title: "人群选择", description: "选择目标人群" },
];
return (
<div className="px-6">
<div className="relative">
<div className="flex items-center justify-between">
{steps.map((step, index) => (
<div
key={index}
className="flex flex-col items-center relative z-10"
>
<div
className={`flex items-center justify-center w-8 h-8 rounded-full ${
index < currentStep
? "bg-blue-600 text-white"
: index === currentStep
? "border-2 border-blue-600 text-blue-600"
: "border-2 border-gray-300 text-gray-300"
}`}
>
{index < currentStep ? (
<Check className="w-5 h-5" />
) : (
index + 1
)}
</div>
<div className="text-center mt-2">
<div
className={`text-sm font-medium ${index <= currentStep ? "text-gray-900" : "text-gray-400"}`}
>
{step.title}
</div>
<div className="text-xs text-gray-500 mt-1">
{step.description}
</div>
</div>
</div>
))}
</div>
<div className="absolute top-4 left-0 w-full h-0.5 bg-gray-200 -translate-y-1/2 z-0">
<div
className="absolute top-0 left-0 h-full bg-blue-600 transition-all duration-300"
style={{
width: `${((currentStep - 1) / (steps.length - 1)) * 100}%`,
}}
></div>
</div>
</div>
</div>
);
}
// 基础设置组件
interface BasicSettingsProps {
formData: CreateLikeTaskDataLocal;
onChange: (data: Partial<CreateLikeTaskDataLocal>) => void;
onNext: () => void;
autoEnabled: boolean;
setAutoEnabled: (v: boolean) => void;
}
function BasicSettings({
formData,
onChange,
onNext,
autoEnabled,
setAutoEnabled,
}: BasicSettingsProps) {
const handleContentTypeChange = (type: ContentType) => {
const currentTypes = [...formData.contentTypes];
if (currentTypes.includes(type)) {
onChange({ contentTypes: currentTypes.filter(t => t !== type) });
} else {
onChange({ contentTypes: [...currentTypes, type] });
}
};
const incrementInterval = () => {
onChange({ interval: Math.min(formData.interval + 5, 60) });
};
const decrementInterval = () => {
onChange({ interval: Math.max(formData.interval - 5, 5) });
};
const incrementMaxLikes = () => {
onChange({ maxLikes: Math.min(formData.maxLikes + 10, 500) });
};
const decrementMaxLikes = () => {
onChange({ maxLikes: Math.max(formData.maxLikes - 10, 10) });
};
return (
<div className="space-y-6 px-6">
<div className="space-y-2">
<Label htmlFor="task-name"></Label>
<Input
id="task-name"
placeholder="请输入任务名称"
value={formData.name}
onChange={e => onChange({ name: e.target.value })}
className="h-12 rounded-xl border-gray-200"
/>
</div>
<div className="space-y-2">
<Label htmlFor="like-interval"></Label>
<div className="flex items-center">
<Button
type="button"
variant="outline"
size="icon"
className="h-12 w-12 rounded-l-xl border-gray-200 bg-white hover:bg-gray-50"
onClick={decrementInterval}
>
<Minus className="h-5 w-5" />
</Button>
<div className="relative flex-1">
<Input
id="like-interval"
type="number"
min={5}
max={60}
value={formData.interval.toString()}
onChange={e =>
onChange({ interval: Number.parseInt(e.target.value) || 5 })
}
className="h-12 rounded-none border-x-0 border-gray-200 text-center"
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none text-gray-500">
</div>
</div>
<Button
type="button"
variant="outline"
size="icon"
className="h-12 w-12 rounded-r-xl border-gray-200 bg-white hover:bg-gray-50"
onClick={incrementInterval}
>
<Plus className="h-5 w-5" />
</Button>
</div>
<p className="text-xs text-gray-500"></p>
</div>
<div className="space-y-2">
<Label htmlFor="max-likes"></Label>
<div className="flex items-center">
<Button
type="button"
variant="outline"
size="icon"
className="h-12 w-12 rounded-l-xl border-gray-200 bg-white hover:bg-gray-50"
onClick={decrementMaxLikes}
>
<Minus className="h-5 w-5" />
</Button>
<div className="relative flex-1">
<Input
id="max-likes"
type="number"
min={10}
max={500}
value={formData.maxLikes.toString()}
onChange={e =>
onChange({ maxLikes: Number.parseInt(e.target.value) || 10 })
}
className="h-12 rounded-none border-x-0 border-gray-200 text-center"
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none text-gray-500">
/
</div>
</div>
<Button
type="button"
variant="outline"
size="icon"
className="h-12 w-12 rounded-r-xl border-gray-200 bg-white hover:bg-gray-50"
onClick={incrementMaxLikes}
>
<Plus className="h-5 w-5" />
</Button>
</div>
<p className="text-xs text-gray-500"></p>
</div>
<div className="space-y-2">
<Label></Label>
<div className="grid grid-cols-2 gap-4">
<div>
<Input
type="time"
value={formData.startTime}
onChange={e => onChange({ startTime: e.target.value })}
className="h-12 rounded-xl border-gray-200"
/>
</div>
<div>
<Input
type="time"
value={formData.endTime}
onChange={e => onChange({ endTime: e.target.value })}
className="h-12 rounded-xl border-gray-200"
/>
</div>
</div>
<p className="text-xs text-gray-500"></p>
</div>
<div className="space-y-2">
<Label></Label>
<div className="grid grid-cols-3 gap-2">
{[
{ id: "text" as ContentType, label: "文字" },
{ id: "image" as ContentType, label: "图片" },
{ id: "video" as ContentType, label: "视频" },
].map(type => (
<div
key={type.id}
className={`flex items-center justify-center h-12 rounded-xl border cursor-pointer ${
formData.contentTypes.includes(type.id)
? "border-blue-500 bg-blue-50 text-blue-600"
: "border-gray-200 text-gray-600"
}`}
onClick={() => handleContentTypeChange(type.id)}
>
{type.label}
</div>
))}
</div>
<p className="text-xs text-gray-500"></p>
</div>
<div className="space-y-2 rounded-xl">
<div className="flex items-center justify-between">
<Label htmlFor="enable-friend-tags" className="cursor-pointer">
</Label>
<Switch
id="enable-friend-tags"
checked={formData.enableFriendTags}
onCheckedChange={checked => onChange({ enableFriendTags: checked })}
/>
</div>
{formData.enableFriendTags && (
<>
<div className="space-y-2 mt-4">
<Label htmlFor="friend-tags"></Label>
<Input
id="friend-tags"
placeholder="请输入标签"
value={formData.friendTags || ""}
onChange={e => onChange({ friendTags: e.target.value })}
className="h-12 rounded-xl border-gray-200"
/>
<p className="text-xs text-gray-500"></p>
</div>
</>
)}
</div>
<div className="flex items-center justify-between py-2">
<Label htmlFor="auto-enabled" className="cursor-pointer">
</Label>
<Switch
id="auto-enabled"
checked={autoEnabled}
onCheckedChange={setAutoEnabled}
/>
</div>
<Button
onClick={onNext}
className="w-full h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
>
</Button>
</div>
);
}

View File

@@ -0,0 +1,125 @@
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
import { FriendSelectionItem } from "@/components/FriendSelection/data";
// 自动点赞任务状态
export type LikeTaskStatus = 1 | 2; // 1: 开启, 2: 关闭
// 内容类型
export type ContentType = "text" | "image" | "video" | "link";
// 设备信息
export interface Device {
id: string;
name: string;
status: "online" | "offline";
lastActive: string;
}
// 好友信息
export interface Friend {
id: string;
nickname: string;
wechatId: string;
avatar: string;
tags: string[];
region: string;
source: string;
}
// 点赞记录
export interface LikeRecord {
id: string;
workbenchId: string;
momentsId: string;
snsId: string;
wechatAccountId: string;
wechatFriendId: string;
likeTime: string;
content: string;
resUrls: string[];
momentTime: string;
userName: string;
operatorName: string;
operatorAvatar: string;
friendName: string;
friendAvatar: string;
}
// 自动点赞任务
export interface LikeTask {
id: string;
name: string;
status: LikeTaskStatus;
deviceCount: number;
targetGroup: string;
likeCount: number;
lastLikeTime: string;
createTime: string;
creator: string;
likeInterval: number;
maxLikesPerDay: number;
timeRange: { start: string; end: string };
contentTypes: ContentType[];
targetTags: string[];
devices: string[];
friends: string[];
friendMaxLikes: number;
friendTags: string;
enableFriendTags: boolean;
todayLikeCount: number;
totalLikeCount: number;
updateTime: string;
}
// 创建任务数据
export interface CreateLikeTaskData {
name: string;
interval: number;
maxLikes: number;
startTime: string;
endTime: string;
contentTypes: ContentType[];
deveiceGroups: string[];
deveiceGroupsOptions: DeviceSelectionItem[];
friendsGroups: string[];
friendsGroupsOptions: FriendSelectionItem[];
friendMaxLikes: number;
friendTags?: string;
enableFriendTags: boolean;
targetTags: string[];
[key: string]: any;
}
// 更新任务数据
export interface UpdateLikeTaskData extends CreateLikeTaskData {
id: string;
}
// 任务配置
export interface TaskConfig {
interval: number;
maxLikes: number;
startTime: string;
endTime: string;
contentTypes: ContentType[];
devices: string[];
friends: string[];
friendMaxLikes: number;
friendTags: string;
enableFriendTags: boolean;
}
// API响应类型
export interface ApiResponse<T = any> {
code: number;
msg: string;
data: T;
}
// 分页响应类型
export interface PaginatedResponse<T> {
list: T[];
total: number;
page: number;
limit: number;
}

View File

@@ -12,17 +12,14 @@ import {
updateAutoLikeTask,
fetchAutoLikeTaskDetail,
} from "./api";
import {
CreateLikeTaskData,
ContentType,
} from "@/pages/workspace/auto-like/record/data";
import { CreateLikeTaskData, ContentType } from "./data";
import style from "./new.module.scss";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
const contentTypeLabels: Record<ContentType, string> = {
text: "文字",
image: "图片",
video: "视频",
link: "链接",
};
const steps = [
@@ -46,8 +43,10 @@ const NewAutoLike: React.FC = () => {
startTime: "08:00",
endTime: "22:00",
contentTypes: ["text", "image", "video"],
devices: [],
friends: [],
deveiceGroups: [],
deveiceGroupsOptions: [],
friendsGroups: [],
friendsGroupsOptions: [],
targetTags: [],
friendMaxLikes: 10,
enableFriendTags: false,
@@ -55,10 +54,10 @@ const NewAutoLike: React.FC = () => {
});
useEffect(() => {
if (isEditMode && id) {
if (id) {
fetchTaskDetail();
}
}, [id, isEditMode]);
}, [id]);
const fetchTaskDetail = async () => {
setIsLoading(true);
@@ -73,8 +72,10 @@ const NewAutoLike: React.FC = () => {
startTime: config.timeRange?.start || config.startTime || "08:00",
endTime: config.timeRange?.end || config.endTime || "22:00",
contentTypes: config.contentTypes || ["text", "image", "video"],
devices: config.devices || [],
friends: config.friends || [],
deveiceGroups: config.deveicegroups || [],
deveiceGroupsOptions: config.deveiceGroupsOptions || [],
friendsGroups: config.friendsgroups || [],
friendsGroupsOptions: config.friendsGroupsOptions || [],
targetTags: config.targetTags || [],
friendMaxLikes: config.friendMaxLikes || 10,
enableFriendTags: config.enableFriendTags || false,
@@ -120,13 +121,13 @@ const NewAutoLike: React.FC = () => {
message.warning("请输入任务名称");
return;
}
if (!formData.devices || formData.devices.length === 0) {
if (!formData.deveicegroups || formData.deveicegroups.length === 0) {
message.warning("请选择执行设备");
return;
}
setIsSubmitting(true);
try {
if (isEditMode && id) {
if (isEditMode) {
await updateAutoLikeTask({ ...formData, id });
message.success("更新成功");
} else {
@@ -328,7 +329,7 @@ const NewAutoLike: React.FC = () => {
<div className={style.basicSection}>
<div className={style.formItem}>
<DeviceSelection
selectedOptions={formData.devices}
selectedOptions={formData.deveicegroups}
onSelect={devices => handleUpdateFormData({ devices })}
showInput={true}
showSelectedList={true}
@@ -347,7 +348,7 @@ const NewAutoLike: React.FC = () => {
onClick={handleNext}
className={style.nextBtn}
size="large"
disabled={formData.devices.length === 0}
disabled={formData.deveicegroups.length === 0}
>
</Button>
@@ -359,9 +360,14 @@ const NewAutoLike: React.FC = () => {
<div className={style.basicSection}>
<div className={style.formItem}>
<FriendSelection
selectedFriends={formData.friends || []}
onSelect={friends => handleUpdateFormData({ friends })}
deviceIds={formData.devices}
selectedOptions={formData.friendsGroupsOptions || []}
onSelect={friends =>
handleUpdateFormData({
friendsGroups: friends.map(f => String(f.id)),
friendsGroupsOptions: friends,
})
}
deviceIds={formData.deveiceGroups}
/>
</div>
<Button
@@ -378,7 +384,9 @@ const NewAutoLike: React.FC = () => {
className={style.completeBtn}
size="large"
loading={isSubmitting}
disabled={!formData.friends || formData.friends.length === 0}
disabled={
!formData.friendsgroups || formData.friendsgroups.length === 0
}
>
{isEditMode ? "更新任务" : "创建任务"}
</Button>

View File

@@ -1,304 +0,0 @@
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { ThumbsUp, RefreshCw, Search } from "lucide-react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Avatar } from "@/components/ui/avatar";
import { Skeleton } from "@/components/ui/skeleton";
import { Separator } from "@/components/ui/separator";
import Layout from "@/components/Layout";
import PageHeader from "@/components/PageHeader";
import { useToast } from "@/components/ui/toast";
import "@/components/Layout.css";
import { fetchLikeRecords, LikeRecord } from "@/api/autoLike";
// 格式化日期
const formatDate = (dateString: string) => {
try {
const date = new Date(dateString);
return date.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
} catch (error) {
return dateString;
}
};
export default function AutoLikeDetail() {
const { id } = useParams<{ id: string }>();
const { toast } = useToast();
const [records, setRecords] = useState<LikeRecord[]>([]);
const [recordsLoading, setRecordsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [total, setTotal] = useState(0);
const pageSize = 10;
useEffect(() => {
if (!id) return;
setRecordsLoading(true);
fetchLikeRecords(id, 1, pageSize)
.then(response => {
setRecords(response.list || []);
setTotal(response.total || 0);
setCurrentPage(1);
})
.catch(() => {
toast({
title: "获取点赞记录失败",
description: "请稍后重试",
variant: "destructive",
});
})
.finally(() => setRecordsLoading(false));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
const handleSearch = () => {
setCurrentPage(1);
fetchLikeRecords(id!, 1, pageSize, searchTerm)
.then(response => {
setRecords(response.list || []);
setTotal(response.total || 0);
setCurrentPage(1);
})
.catch(() => {
toast({
title: "获取点赞记录失败",
description: "请稍后重试",
variant: "destructive",
});
});
};
const handleRefresh = () => {
fetchLikeRecords(id!, currentPage, pageSize, searchTerm)
.then(response => {
setRecords(response.list || []);
setTotal(response.total || 0);
})
.catch(() => {
toast({
title: "获取点赞记录失败",
description: "请稍后重试",
variant: "destructive",
});
});
};
const handlePageChange = (newPage: number) => {
fetchLikeRecords(id!, newPage, pageSize, searchTerm)
.then(response => {
setRecords(response.list || []);
setTotal(response.total || 0);
setCurrentPage(newPage);
})
.catch(() => {
toast({
title: "获取点赞记录失败",
description: "请稍后重试",
variant: "destructive",
});
});
};
return (
<Layout
header={
<>
<PageHeader title="点赞记录" defaultBackPath="/workspace/auto-like" />
<div className="flex items-center space-x-2 px-4 py-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索好友昵称或内容"
className="pl-9"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
onKeyDown={e => e.key === "Enter" && handleSearch()}
/>
</div>
<Button
variant="outline"
size="icon"
onClick={handleRefresh}
disabled={recordsLoading}
>
<RefreshCw
className={`h-4 w-4 ${recordsLoading ? "animate-spin" : ""}`}
/>
</Button>
</div>
</>
}
footer={
<>
{records.length > 0 && total > pageSize && (
<div className="flex justify-center py-4">
<Button
variant="outline"
size="sm"
disabled={currentPage === 1}
onClick={() => handlePageChange(currentPage - 1)}
className="mx-1"
>
</Button>
<span className="mx-4 py-2 text-sm text-gray-500">
{currentPage} {Math.ceil(total / pageSize)}
</span>
<Button
variant="outline"
size="sm"
disabled={currentPage >= Math.ceil(total / pageSize)}
onClick={() => handlePageChange(currentPage + 1)}
className="mx-1"
>
</Button>
</div>
)}
</>
}
>
<div className="bg-gray-50 min-h-screen pb-20">
<div className="p-4 space-y-4">
{recordsLoading ? (
<div className="space-y-4">
{Array.from({ length: 3 }).map((_, index) => (
<Card key={index} className="p-4">
<div className="flex items-center space-x-3 mb-3">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-16" />
</div>
</div>
<Separator className="my-3" />
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
<div className="flex space-x-2 mt-3">
<Skeleton className="h-20 w-20" />
<Skeleton className="h-20 w-20" />
</div>
</div>
</Card>
))}
</div>
) : records.length === 0 ? (
<div className="text-center py-8">
<ThumbsUp className="h-12 w-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500"></p>
</div>
) : (
<>
{records.map(record => (
<div
key={record.id}
className="p-4 mb-4 bg-white rounded-2xl shadow-sm"
>
<div className="flex items-start justify-between">
<div className="flex items-center space-x-3 max-w-[65%]">
<Avatar>
<img
src={
record.friendAvatar ||
"https://api.dicebear.com/7.x/avataaars/svg?seed=fallback"
}
alt={record.friendName}
className="w-10 h-10 rounded-full"
/>
</Avatar>
<div className="min-w-0">
<div
className="font-medium truncate"
title={record.friendName}
>
{record.friendName}
</div>
<div className="text-sm text-gray-500"></div>
</div>
</div>
<Badge
variant="outline"
className="bg-blue-50 whitespace-nowrap shrink-0"
>
{formatDate(record.momentTime || record.likeTime)}
</Badge>
</div>
<Separator className="my-3" />
<div className="mb-3">
{record.content && (
<p className="text-gray-700 mb-3 whitespace-pre-line">
{record.content}
</p>
)}
{Array.isArray(record.resUrls) &&
record.resUrls.length > 0 && (
<div
className={`grid gap-2 ${
record.resUrls.length === 1
? "grid-cols-1"
: record.resUrls.length === 2
? "grid-cols-2"
: record.resUrls.length <= 3
? "grid-cols-3"
: record.resUrls.length <= 6
? "grid-cols-3 grid-rows-2"
: "grid-cols-3 grid-rows-3"
}`}
>
{record.resUrls
.slice(0, 9)
.map((image: string, idx: number) => (
<div
key={idx}
className="relative aspect-square rounded-md overflow-hidden"
>
<img
src={image}
alt={`内容图片 ${idx + 1}`}
className="object-cover w-full h-full"
/>
</div>
))}
</div>
)}
</div>
<div className="flex items-center mt-4 p-2 bg-gray-50 rounded-md">
<Avatar className="h-8 w-8 mr-2 shrink-0">
<img
src={
record.operatorAvatar ||
"https://api.dicebear.com/7.x/avataaars/svg?seed=operator"
}
alt={record.operatorName}
className="w-8 h-8 rounded-full"
/>
</Avatar>
<div className="text-sm min-w-0">
<span
className="font-medium truncate inline-block max-w-full"
title={record.operatorName}
>
{record.operatorName}
</span>
<span className="text-gray-500 ml-2"></span>
</div>
</div>
</div>
))}
</>
)}
</div>
</div>
</Layout>
);
}