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

记录列表搞好了
This commit is contained in:
笔记本里的永平
2025-07-08 15:55:41 +08:00
parent 53621e41e7
commit 98bcafda50
8 changed files with 511 additions and 663 deletions

View File

@@ -1,90 +1,101 @@
import { get, post, del } from './request';
import {
LikeTask,
CreateLikeTaskData,
UpdateLikeTaskData,
LikeRecord,
ApiResponse,
PaginatedResponse
} from '@/types/auto-like';
// 获取自动点赞任务列表
export async function fetchAutoLikeTasks(): Promise<LikeTask[]> {
try {
const res = await get<ApiResponse<PaginatedResponse<LikeTask>>>('/v1/workbench/list?type=1&page=1&limit=100');
if (res.code === 200 && res.data) {
return res.data.list || [];
}
return [];
} catch (error) {
console.error('获取自动点赞任务失败:', error);
return [];
}
}
// 获取单个任务详情
export async function fetchAutoLikeTaskDetail(id: string): Promise<LikeTask | null> {
try {
const res = await get<ApiResponse<LikeTask>>(`/v1/workbench/detail/${id}`);
if (res.code === 200 && res.data) {
return res.data;
}
return null;
} catch (error) {
console.error('获取任务详情失败:', error);
return null;
}
}
// 创建自动点赞任务
export async function createAutoLikeTask(data: CreateLikeTaskData): Promise<ApiResponse> {
return post('/v1/workbench/create', {
...data,
type: 1 // 自动点赞类型
});
}
// 更新自动点赞任务
export async function updateAutoLikeTask(data: UpdateLikeTaskData): Promise<ApiResponse> {
return post('/v1/workbench/update', {
...data,
type: 1 // 自动点赞类型
});
}
// 删除自动点赞任务
export async function deleteAutoLikeTask(id: string): Promise<ApiResponse> {
return del('/v1/workbench/delete', { params: { id } });
}
// 切换任务状态
export async function toggleAutoLikeTask(id: string, status: string): Promise<ApiResponse> {
return post('/v1/workbench/update-status', { id, status });
}
// 复制自动点赞任务
export async function copyAutoLikeTask(id: string): Promise<ApiResponse> {
return post('/v1/workbench/copy', { id });
}
// 获取点赞记录
export async function fetchLikeRecords(
workbenchId: string,
page: number = 1,
limit: number = 20
): Promise<PaginatedResponse<LikeRecord>> {
try {
const res = await get<ApiResponse<PaginatedResponse<LikeRecord>>>(`/v1/workbench/like-records?workbenchId=${workbenchId}&page=${page}&limit=${limit}`);
if (res.code === 200 && res.data) {
return res.data;
}
return { list: [], total: 0, page, limit };
} catch (error) {
console.error('获取点赞记录失败:', error);
return { list: [], total: 0, page, limit };
}
}
import { get, post, del } from './request';
import {
LikeTask,
CreateLikeTaskData,
UpdateLikeTaskData,
LikeRecord,
ApiResponse,
PaginatedResponse
} from '@/types/auto-like';
// 获取自动点赞任务列表
export async function fetchAutoLikeTasks(): Promise<LikeTask[]> {
try {
const res = await get<ApiResponse<PaginatedResponse<LikeTask>>>('/v1/workbench/list?type=1&page=1&limit=100');
if (res.code === 200 && res.data) {
return res.data.list || [];
}
return [];
} catch (error) {
console.error('获取自动点赞任务失败:', error);
return [];
}
}
// 获取单个任务详情
export async function fetchAutoLikeTaskDetail(id: string): Promise<LikeTask | null> {
try {
const res = await get<ApiResponse<LikeTask>>(`/v1/workbench/detail/${id}`);
if (res.code === 200 && res.data) {
return res.data;
}
return null;
} catch (error) {
console.error('获取任务详情失败:', error);
return null;
}
}
// 创建自动点赞任务
export async function createAutoLikeTask(data: CreateLikeTaskData): Promise<ApiResponse> {
return post('/v1/workbench/create', {
...data,
type: 1 // 自动点赞类型
});
}
// 更新自动点赞任务
export async function updateAutoLikeTask(data: UpdateLikeTaskData): Promise<ApiResponse> {
return post('/v1/workbench/update', {
...data,
type: 1 // 自动点赞类型
});
}
// 删除自动点赞任务
export async function deleteAutoLikeTask(id: string): Promise<ApiResponse> {
return del('/v1/workbench/delete', { params: { id } });
}
// 切换任务状态
export async function toggleAutoLikeTask(id: string, status: string): Promise<ApiResponse> {
return post('/v1/workbench/update-status', { id, status });
}
// 复制自动点赞任务
export async function copyAutoLikeTask(id: string): Promise<ApiResponse> {
return post('/v1/workbench/copy', { id });
}
// 获取点赞记录
export async function fetchLikeRecords(
workbenchId: string,
page: number = 1,
limit: number = 20,
keyword?: string
): Promise<PaginatedResponse<LikeRecord>> {
try {
const params = new URLSearchParams({
workbenchId,
page: page.toString(),
limit: limit.toString()
});
if (keyword) {
params.append('keyword', keyword);
}
const res = await get<ApiResponse<PaginatedResponse<LikeRecord>>>(`/v1/workbench/like-records?${params.toString()}`);
if (res.code === 200 && res.data) {
return res.data;
}
return { list: [], total: 0, page, limit };
} catch (error) {
console.error('获取点赞记录失败:', error);
return { list: [], total: 0, page, limit };
}
}
export type { LikeTask, LikeRecord, CreateLikeTaskData };

View File

@@ -4,7 +4,7 @@ import { requestInterceptor, responseInterceptor, errorInterceptor } from './int
// 创建axios实例
const request: AxiosInstance = axios.create({
baseURL: process.env.REACT_APP_API_BASE_URL || 'http://www.yishi.com',
timeout: 10000,
timeout: 20000,
headers: {
'Content-Type': 'application/json',
},

View File

@@ -18,6 +18,7 @@ const NO_BOTTOM_NAV_PATHS = [
'/workspace/auto-group/',
'/workspace/moments-sync/',
'/workspace/traffic-distribution/',
'/workspace/auto-like',
'/404',
'/500'
];

View File

@@ -0,0 +1,28 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "../../utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

View File

@@ -0,0 +1,15 @@
import { cn } from "../../utils";
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@@ -9,7 +9,7 @@ import {
Search,
RefreshCw,
ThumbsUp,
ChevronDown,
ChevronLeft,
} from "lucide-react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
@@ -209,7 +209,7 @@ export default function AutoLike() {
<div className="flex items-center justify-between p-4">
<div className="flex items-center space-x-3">
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
<ChevronDown className="h-5 w-5" />
<ChevronLeft className="h-5 w-5" />
</Button>
<h1 className="text-lg font-medium"></h1>
</div>

View File

@@ -1,455 +1,248 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Edit,
Trash2,
Copy,
ThumbsUp,
Settings,
Calendar,
Eye,
RefreshCw,
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Switch } from '@/components/ui/switch';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import Layout from '@/components/Layout';
import PageHeader from '@/components/PageHeader';
import BottomNav from '@/components/BottomNav';
import { useToast } from '@/components/ui/toast';
import '@/components/Layout.css';
import {
fetchAutoLikeTaskDetail,
toggleAutoLikeTask,
deleteAutoLikeTask,
copyAutoLikeTask,
fetchLikeRecords,
LikeTask,
LikeRecord,
} from '@/api/autoLike';
export default function AutoLikeDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { toast } = useToast();
const [task, setTask] = useState<LikeTask | null>(null);
const [records, setRecords] = useState<LikeRecord[]>([]);
const [loading, setLoading] = useState(true);
const [recordsLoading, setRecordsLoading] = useState(false);
const fetchTaskDetail = useCallback(async () => {
if (!id) return;
try {
const taskData = await fetchAutoLikeTaskDetail(id);
if (taskData) {
setTask(taskData);
} else {
toast({
title: '任务不存在',
description: '该任务可能已被删除',
variant: 'destructive',
});
navigate('/workspace/auto-like');
}
} catch (error) {
console.error('获取任务详情失败:', error);
toast({
title: '获取失败',
description: '请稍后重试',
variant: 'destructive',
});
} finally {
setLoading(false);
}
}, [id, toast, navigate]);
const fetchRecords = useCallback(async () => {
if (!id) return;
setRecordsLoading(true);
try {
const response = await fetchLikeRecords(id, 1, 20);
setRecords(response.list || []);
} catch (error) {
console.error('获取点赞记录失败:', error);
} finally {
setRecordsLoading(false);
}
}, [id]);
useEffect(() => {
if (id) {
fetchTaskDetail();
fetchRecords();
}
}, [id, fetchTaskDetail, fetchRecords]);
const handleToggleStatus = async () => {
if (!task) return;
// 先更新本地状态
const newStatus = (Number(task.status) === 1 ? 2 : 1) as 1 | 2;
const originalStatus = Number(task.status) as 1 | 2;
setTask(prev => prev ? { ...prev, status: newStatus } : null);
try {
const response = await toggleAutoLikeTask(task.id, String(newStatus));
if (response.code === 200) {
toast({ title: '操作成功' });
// 成功时保持本地状态,不重新获取数据
} else {
// 请求失败,回退本地状态
setTask(prev => prev ? { ...prev, status: originalStatus } : null);
toast({
title: '操作失败',
description: response.msg || '请稍后重试',
variant: 'destructive',
});
}
} catch (error) {
console.error('操作失败:', error);
// 请求异常,回退本地状态
setTask(prev => prev ? { ...prev, status: originalStatus } : null);
toast({
title: '操作失败',
description: '请稍后重试',
variant: 'destructive',
});
}
};
const handleDelete = async () => {
if (!task || !window.confirm('确定要删除该任务吗?')) return;
try {
const response = await deleteAutoLikeTask(task.id);
if (response.code === 200) {
toast({ title: '删除成功' });
navigate('/workspace/auto-like');
} else {
toast({
title: '删除失败',
description: response.msg || '请稍后重试',
variant: 'destructive',
});
}
} catch (error) {
console.error('删除失败:', error);
toast({
title: '删除失败',
description: '请稍后重试',
variant: 'destructive',
});
}
};
const handleCopy = async () => {
if (!task) return;
try {
const response = await copyAutoLikeTask(task.id);
if (response.code === 200) {
toast({ title: '复制成功' });
navigate('/workspace/auto-like');
} else {
toast({
title: '复制失败',
description: response.msg || '请稍后重试',
variant: 'destructive',
});
}
} catch (error) {
console.error('复制失败:', error);
toast({
title: '复制失败',
description: '请稍后重试',
variant: 'destructive',
});
}
};
const getStatusColor = (status: number) => {
switch (status) {
case 1:
return 'bg-green-100 text-green-800';
case 2:
return 'bg-gray-100 text-gray-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getStatusText = (status: number) => {
switch (status) {
case 1:
return '进行中';
case 2:
return '已暂停';
default:
return '未知';
}
};
if (loading) {
return (
<Layout
header={<PageHeader title="任务详情" defaultBackPath="/workspace/auto-like" />}
footer={<BottomNav />}
>
<div className="bg-gray-50 min-h-screen pb-20">
<div className="p-4">
<Card className="p-8 text-center">
<RefreshCw className="h-12 w-12 text-gray-300 mx-auto mb-3 animate-spin" />
<p className="text-gray-500 text-lg font-medium mb-2">...</p>
<p className="text-gray-400 text-sm"></p>
</Card>
</div>
</div>
</Layout>
);
}
if (!task) {
return null;
}
return (
<Layout
header={
<PageHeader
title="任务详情"
defaultBackPath="/workspace/auto-like"
rightContent={
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<Settings className="h-4 w-4 mr-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => navigate(`/workspace/auto-like/${task.id}/edit`)}>
<Edit className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={handleCopy}>
<Copy className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={handleDelete}>
<Trash2 className="h-4 w-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
}
/>
}
footer={<BottomNav />}
>
<div className="bg-gray-50 min-h-screen pb-20">
<div className="p-4 space-y-4">
{/* 任务基本信息 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center">
<ThumbsUp className="h-5 w-5 mr-2" />
{task.name}
</CardTitle>
<div className="flex items-center space-x-2">
<Badge className={getStatusColor(Number(task.status))}>
{getStatusText(Number(task.status))}
</Badge>
<Switch
checked={Number(task.status) === 1}
onCheckedChange={handleToggleStatus}
/>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="text-sm">
<div className="text-gray-500"></div>
<div className="font-medium">{task.createTime}</div>
</div>
<div className="text-sm">
<div className="text-gray-500"></div>
<div className="font-medium">{task.creator}</div>
</div>
<div className="text-sm">
<div className="text-gray-500"></div>
<div className="font-medium">{task.deviceCount} </div>
</div>
<div className="text-sm">
<div className="text-gray-500"></div>
<div className="font-medium">{task.targetGroup}</div>
</div>
</div>
</CardContent>
</Card>
{/* 执行进度 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<Calendar className="h-5 w-5 mr-2" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span className="font-medium">
{task.todayLikeCount} / {task.maxLikesPerDay}
</span>
</div>
<Progress
value={(task.todayLikeCount / task.maxLikesPerDay) * 100}
className="h-2"
/>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<div className="text-gray-500"></div>
<div className="font-medium">{task.totalLikeCount} </div>
</div>
<div>
<div className="text-gray-500"></div>
<div className="font-medium">{task.lastLikeTime}</div>
</div>
</div>
</CardContent>
</Card>
{/* 任务配置 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<Settings className="h-5 w-5 mr-2" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="text-sm">
<div className="text-gray-500"></div>
<div className="font-medium">{task.likeInterval} </div>
</div>
<div className="text-sm">
<div className="text-gray-500"></div>
<div className="font-medium">{task.maxLikesPerDay} </div>
</div>
<div className="text-sm">
<div className="text-gray-500"></div>
<div className="font-medium">{task.friendMaxLikes} </div>
</div>
<div className="text-sm">
<div className="text-gray-500"></div>
<div className="font-medium">
{task.timeRange.start} - {task.timeRange.end}
</div>
</div>
</div>
<div className="space-y-2">
<div className="text-sm text-gray-500"></div>
<div className="flex flex-wrap gap-2">
{task.contentTypes.map((type) => (
<Badge key={type} variant="outline">
{type === 'text' ? '文字' : type === 'image' ? '图片' : type === 'video' ? '视频' : '链接'}
</Badge>
))}
</div>
</div>
{task.targetTags.length > 0 && (
<div className="space-y-2">
<div className="text-sm text-gray-500"></div>
<div className="flex flex-wrap gap-2">
{task.targetTags.map((tag) => (
<Badge key={tag} variant="outline">
{tag}
</Badge>
))}
</div>
</div>
)}
{task.enableFriendTags && task.friendTags && (
<div className="space-y-2">
<div className="text-sm text-gray-500"></div>
<div className="flex flex-wrap gap-2">
<Badge variant="outline">{task.friendTags}</Badge>
</div>
</div>
)}
</CardContent>
</Card>
{/* 点赞记录 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center">
<Eye className="h-5 w-5 mr-2" />
</CardTitle>
<Button variant="outline" size="sm" onClick={fetchRecords}>
<RefreshCw className="h-4 w-4 mr-2" />
</Button>
</div>
</CardHeader>
<CardContent>
{recordsLoading ? (
<div className="text-center py-4">
<RefreshCw className="h-6 w-6 text-gray-400 mx-auto animate-spin" />
<p className="text-gray-500 text-sm mt-2">...</p>
</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>
) : (
<div className="space-y-3">
{records.slice(0, 10).map((record) => (
<div key={record.id} className="flex items-center space-x-3 p-3 border rounded-lg">
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<ThumbsUp className="h-4 w-4 text-blue-600" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">
{record.friendName}
</div>
<div className="text-xs text-gray-500 truncate">
{record.content}
</div>
</div>
<div className="text-xs text-gray-500">
{record.likeTime}
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
</div>
</Layout>
);
import React, { useState, useEffect, useCallback } from 'react';
import { useParams } from 'react-router-dom';
import {
ThumbsUp,
RefreshCw,
Search,
Filter,
} from 'lucide-react';
import { Card, CardContent } 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;
const fetchRecords = useCallback(async (page: number = 1, keyword?: string) => {
if (!id) return;
setRecordsLoading(true);
try {
const response = await fetchLikeRecords(id, page, pageSize, keyword);
setRecords(response.list || []);
setTotal(response.total || 0);
setCurrentPage(page);
} catch (error) {
console.error('获取点赞记录失败:', error);
toast({
title: '获取点赞记录失败',
description: '请稍后重试',
variant: 'destructive',
});
} finally {
setRecordsLoading(false);
}
}, [id, toast]);
useEffect(() => {
if (id) {
fetchRecords(1);
}
}, [id, fetchRecords]); // 加上 fetchRecords 依赖
const handleSearch = () => {
setCurrentPage(1);
fetchRecords(1, searchTerm);
};
const handleRefresh = () => {
fetchRecords(currentPage, searchTerm);
};
const handlePageChange = (newPage: number) => {
fetchRecords(newPage, searchTerm);
};
return (
<Layout
header={
<PageHeader
title="点赞记录"
defaultBackPath="/workspace/auto-like"
/>
}
>
<div className="bg-gray-50 min-h-screen pb-20">
<div className="p-4 space-y-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索好友昵称或内容"
className="pl-9"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
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>
</CardContent>
</Card>
{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>
))}
</>
)}
{/* 分页 */}
{records.length > 0 && total > pageSize && (
<div className="flex justify-center mt-6">
<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>
</div>
</Layout>
);
}

View File

@@ -1,118 +1,118 @@
// 自动点赞任务状态
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[];
devices: string[];
friends?: string[];
friendMaxLikes: number;
friendTags?: string;
enableFriendTags: boolean;
targetTags: string[];
}
// 更新任务数据
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;
// 自动点赞任务状态
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[];
devices: string[];
friends?: string[];
friendMaxLikes: number;
friendTags?: string;
enableFriendTags: boolean;
targetTags: string[];
}
// 更新任务数据
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;
}