feat: 暂时可以了
This commit is contained in:
4
nkebao/package-lock.json
generated
4
nkebao/package-lock.json
generated
@@ -30,7 +30,7 @@
|
||||
"@radix-ui/react-scroll-area": "latest",
|
||||
"@radix-ui/react-select": "latest",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slider": "latest",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "latest",
|
||||
"@radix-ui/react-tabs": "latest",
|
||||
@@ -70,7 +70,7 @@
|
||||
"recharts": "latest",
|
||||
"regenerator-runtime": "latest",
|
||||
"sonner": "^1.7.4",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tdesign-mobile-react": "^0.16.0",
|
||||
"vaul": "^0.9.6",
|
||||
|
||||
@@ -21,6 +21,7 @@ import AutoGroupDetail from './pages/workspace/auto-group/Detail';
|
||||
import GroupPush from './pages/workspace/group-push/GroupPush';
|
||||
import MomentsSync from './pages/workspace/moments-sync/MomentsSync';
|
||||
import MomentsSyncDetail from './pages/workspace/moments-sync/Detail';
|
||||
import NewMomentsSync from './pages/workspace/moments-sync/new';
|
||||
import EditMomentsSync from './pages/workspace/moments-sync/edit';
|
||||
import AIAssistant from './pages/workspace/ai-assistant/AIAssistant';
|
||||
import TrafficDistribution from './pages/workspace/traffic-distribution/TrafficDistribution';
|
||||
@@ -66,6 +67,7 @@ function App() {
|
||||
<Route path="/workspace/auto-group/:id" element={<AutoGroupDetail />} />
|
||||
<Route path="/workspace/group-push" element={<GroupPush />} />
|
||||
<Route path="/workspace/moments-sync" element={<MomentsSync />} />
|
||||
<Route path="/workspace/moments-sync/new" element={<NewMomentsSync />} />
|
||||
<Route path="/workspace/moments-sync/:id" element={<MomentsSyncDetail />} />
|
||||
<Route path="/workspace/moments-sync/edit/:id" element={<EditMomentsSync />} />
|
||||
<Route path="/workspace/ai-assistant" element={<AIAssistant />} />
|
||||
|
||||
@@ -13,6 +13,11 @@ interface InputProps {
|
||||
type?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
name?: string;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
autoComplete?: string;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
export function Input({
|
||||
@@ -27,7 +32,12 @@ export function Input({
|
||||
id,
|
||||
type = 'text',
|
||||
min,
|
||||
max
|
||||
max,
|
||||
name,
|
||||
required = false,
|
||||
disabled = false,
|
||||
autoComplete,
|
||||
step
|
||||
}: InputProps) {
|
||||
const isReadOnly = readOnly || readonly;
|
||||
|
||||
@@ -43,6 +53,11 @@ export function Input({
|
||||
readOnly={isReadOnly}
|
||||
min={min}
|
||||
max={max}
|
||||
name={name}
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
autoComplete={autoComplete}
|
||||
step={step}
|
||||
className={`flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
Send,
|
||||
Settings,
|
||||
@@ -45,7 +45,6 @@ interface Conversation {
|
||||
}
|
||||
|
||||
export default function AIAssistant() {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||
const [currentConversation, setCurrentConversation] = useState<Conversation | null>(null);
|
||||
|
||||
@@ -12,20 +12,16 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
|
||||
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 { fetchDeviceList } from '@/api/devices';
|
||||
import type { Device } from '@/types/device';
|
||||
|
||||
import { get } from '@/api/request';
|
||||
|
||||
interface TagGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
|
||||
// 用于设备选择弹窗的简化设备类型
|
||||
interface DeviceSelectionItem {
|
||||
@@ -177,7 +173,7 @@ export default function NewAutoLike() {
|
||||
|
||||
// 处理状态字段,使用双等号允许类型自动转换
|
||||
const status = taskAny.status;
|
||||
setAutoEnabled(status == 1 || status == 'running');
|
||||
setAutoEnabled(status === 1 || status === 'running');
|
||||
} else {
|
||||
toast({
|
||||
title: '获取任务详情失败',
|
||||
@@ -199,44 +195,7 @@ export default function NewAutoLike() {
|
||||
}
|
||||
};
|
||||
|
||||
// 标签组数据
|
||||
const [tagGroups] = useState<TagGroup[]>([
|
||||
{
|
||||
id: 'intention',
|
||||
name: '意向度',
|
||||
tags: ['高意向', '中意向', '低意向'],
|
||||
},
|
||||
{
|
||||
id: 'customer',
|
||||
name: '客户类型',
|
||||
tags: ['新客户', '老客户', 'VIP客户'],
|
||||
},
|
||||
{
|
||||
id: 'gender',
|
||||
name: '性别',
|
||||
tags: ['男性', '女性'],
|
||||
},
|
||||
{
|
||||
id: 'age',
|
||||
name: '年龄段',
|
||||
tags: ['年轻人', '中年人', '老年人'],
|
||||
},
|
||||
{
|
||||
id: 'location',
|
||||
name: '地区',
|
||||
tags: ['城市', '农村'],
|
||||
},
|
||||
{
|
||||
id: 'income',
|
||||
name: '收入',
|
||||
tags: ['高收入', '中等收入', '低收入'],
|
||||
},
|
||||
{
|
||||
id: 'interaction',
|
||||
name: '互动频率',
|
||||
tags: ['高频互动', '中频互动', '低频互动'],
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
const handleUpdateFormData = (data: Partial<CreateLikeTaskDataLocal>) => {
|
||||
setFormData((prev) => ({ ...prev, ...data }));
|
||||
@@ -827,203 +786,7 @@ function DeviceSelectionDialog({ open, onOpenChange, selectedDevices, onSelect,
|
||||
);
|
||||
}
|
||||
|
||||
// 标签选择器组件
|
||||
interface TagSelectorProps {
|
||||
selectedTags: string[];
|
||||
tagOperator: 'and' | 'or';
|
||||
onTagsChange: (tags: string[]) => void;
|
||||
onOperatorChange: (operator: 'and' | 'or') => void;
|
||||
onBack: () => void;
|
||||
onComplete: () => void;
|
||||
tagGroups: TagGroup[];
|
||||
}
|
||||
|
||||
function TagSelector({
|
||||
selectedTags,
|
||||
tagOperator,
|
||||
onTagsChange,
|
||||
onOperatorChange,
|
||||
onBack,
|
||||
onComplete,
|
||||
tagGroups,
|
||||
}: TagSelectorProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [customTag, setCustomTag] = useState('');
|
||||
|
||||
const toggleTag = (tag: string) => {
|
||||
if (selectedTags.includes(tag)) {
|
||||
onTagsChange(selectedTags.filter((t) => t !== tag));
|
||||
} else {
|
||||
onTagsChange([...selectedTags, tag]);
|
||||
}
|
||||
};
|
||||
|
||||
const addCustomTag = () => {
|
||||
if (customTag.trim() && !selectedTags.includes(customTag.trim())) {
|
||||
onTagsChange([...selectedTags, customTag.trim()]);
|
||||
setCustomTag('');
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (tag: string) => {
|
||||
onTagsChange(selectedTags.filter((t) => t !== tag));
|
||||
};
|
||||
|
||||
const filteredTagGroups = tagGroups
|
||||
.map((group) => ({
|
||||
...group,
|
||||
tags: group.tags.filter((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase())),
|
||||
}))
|
||||
.filter((group) => group.tags.length > 0);
|
||||
|
||||
return (
|
||||
<Card className="mb-6">
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-medium">选择目标人群标签</h3>
|
||||
</div>
|
||||
|
||||
<div className="relative mb-4">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索标签"
|
||||
className="pl-9"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="intention" className="mb-6">
|
||||
<TabsList className="grid grid-cols-4 mb-4">
|
||||
{tagGroups.slice(0, 4).map((group) => (
|
||||
<TabsTrigger key={group.id} value={group.id}>
|
||||
{group.name}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{tagGroups.map((group) => (
|
||||
<TabsContent key={group.id} value={group.id} className="mt-0">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{group.tags.map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant={selectedTags.includes(tag) ? 'default' : 'outline'}
|
||||
className="cursor-pointer py-1 px-3"
|
||||
onClick={() => toggleTag(tag)}
|
||||
>
|
||||
{selectedTags.includes(tag) && <Check className="h-3 w-3 mr-1" />}
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
<ScrollArea className="h-48 border rounded-md p-4 mb-4">
|
||||
<div className="space-y-4">
|
||||
{filteredTagGroups.length > 0 ? (
|
||||
filteredTagGroups.map((group) => (
|
||||
<div key={group.id} className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-gray-500">{group.name}</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{group.tags.map((tag) => (
|
||||
<div key={tag} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`tag-${tag}`}
|
||||
checked={selectedTags.includes(tag)}
|
||||
onCheckedChange={() => toggleTag(tag)}
|
||||
/>
|
||||
<Label htmlFor={`tag-${tag}`} className="text-sm font-normal">
|
||||
{tag}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center text-gray-500">没有找到匹配的标签</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<div className="flex space-x-2 mt-2">
|
||||
<div className="relative flex-1">
|
||||
<TagIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
value={customTag}
|
||||
onChange={(e) => setCustomTag(e.target.value)}
|
||||
className="pl-9"
|
||||
placeholder="添加自定义标签"
|
||||
onKeyDown={(e) => e.key === 'Enter' && addCustomTag()}
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={addCustomTag} disabled={!customTag.trim()}>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-medium">标签匹配逻辑</h3>
|
||||
<p className="text-sm text-muted-foreground mb-2">选择多个标签之间的匹配关系</p>
|
||||
|
||||
<RadioGroup
|
||||
value={tagOperator}
|
||||
onValueChange={(value: string) => onOperatorChange(value as 'and' | 'or')}
|
||||
className="flex flex-col space-y-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="and" id="and-operator" />
|
||||
<Label htmlFor="and-operator" className="font-normal">
|
||||
所有标签都必须匹配(AND)
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="or" id="or-operator" />
|
||||
<Label htmlFor="or-operator" className="font-normal">
|
||||
匹配任意一个标签即可(OR)
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-base font-medium mb-2">已选择的标签</h3>
|
||||
<div className="min-h-[60px] border rounded-md p-3">
|
||||
{selectedTags.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">未选择任何标签</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedTags.map((tag) => (
|
||||
<Badge key={tag} className="flex items-center gap-1 py-1 px-2">
|
||||
{tag}
|
||||
<Button variant="ghost" size="icon" className="h-4 w-4 p-0 ml-1" onClick={() => removeTag(tag)}>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between space-x-4">
|
||||
<Button variant="outline" className="flex-1" onClick={onBack}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button className="flex-1" onClick={onComplete} disabled={selectedTags.length === 0}>
|
||||
完成设置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 微信好友选择弹窗组件
|
||||
interface FriendSelectionDialogProps {
|
||||
@@ -1096,27 +859,7 @@ function FriendSelectionDialog({ open, onOpenChange, selectedFriends = [], onSel
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
// 选择当前页面上所有好友
|
||||
const currentPageIds = filteredFriends.map(friend => friend.id);
|
||||
const newSelectedFriends = [...selectedFriends];
|
||||
|
||||
// 添加未选中的好友
|
||||
currentPageIds.forEach(id => {
|
||||
if (!selectedFriends.includes(id)) {
|
||||
newSelectedFriends.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
onSelect(newSelectedFriends);
|
||||
};
|
||||
|
||||
const handleUnselectAll = () => {
|
||||
// 取消当前页面上所有好友的选择
|
||||
const currentPageIds = filteredFriends.map(friend => friend.id);
|
||||
const newSelectedFriends = selectedFriends.filter(id => !currentPageIds.includes(id));
|
||||
onSelect(newSelectedFriends);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
onOpenChange(false);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
syncMoments
|
||||
} from '@/api/momentsSync';
|
||||
import { MomentsSyncTask } from '@/types/moments-sync';
|
||||
import { ChevronLeft, Edit2, RefreshCw } from 'lucide-react';
|
||||
import { ChevronLeft, Edit2, RefreshCw, Clock, Database, Smartphone } from 'lucide-react';
|
||||
import Layout from '@/components/Layout';
|
||||
import BottomNav from '@/components/BottomNav';
|
||||
|
||||
export default function MomentsSyncDetail() {
|
||||
const { id } = useParams();
|
||||
@@ -20,13 +22,7 @@ export default function MomentsSyncDetail() {
|
||||
const [task, setTask] = useState<MomentsSyncTask | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchTaskDetail();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const fetchTaskDetail = async () => {
|
||||
const fetchTaskDetail = useCallback(async () => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -39,9 +35,13 @@ export default function MomentsSyncDetail() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
}, [id, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchTaskDetail();
|
||||
}
|
||||
}, [id, fetchTaskDetail]);
|
||||
|
||||
const handleToggleStatus = async () => {
|
||||
if (!task || !id) return;
|
||||
@@ -74,106 +74,178 @@ export default function MomentsSyncDetail() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex-1 bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">加载中...</p>
|
||||
<Layout>
|
||||
<div className="flex-1 bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
return (
|
||||
<div className="flex-1 bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-600">任务不存在</p>
|
||||
<Button onClick={() => navigate('/workspace/moments-sync')} className="mt-4">
|
||||
返回列表
|
||||
</Button>
|
||||
<Layout>
|
||||
<div className="flex-1 bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-600">任务不存在</p>
|
||||
<Button onClick={() => navigate('/workspace/moments-sync')} className="mt-4">
|
||||
返回列表
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-gray-50 min-h-screen">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate('/workspace/moments-sync')}>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-lg font-medium">朋友圈同步任务详情</h1>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="outline" size="sm" onClick={handleEdit}>
|
||||
<Edit2 className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleSync}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
立即同步
|
||||
</Button>
|
||||
</div>
|
||||
const header = (
|
||||
<div className="sticky top-0 z-10 bg-white border-b">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate('/workspace/moments-sync')}>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-lg font-medium">朋友圈同步任务详情</h1>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="outline" size="sm" onClick={handleEdit}>
|
||||
<Edit2 className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleSync}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
立即同步
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="p-4 max-w-7xl mx-auto space-y-6">
|
||||
{/* 基本信息卡片 */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h2 className="text-2xl font-bold">{task.name}</h2>
|
||||
<Badge variant={task.status === 1 ? "default" : "secondary"}>
|
||||
{task.status === 1 ? "进行中" : "已暂停"}
|
||||
</Badge>
|
||||
</div>
|
||||
<Switch checked={task.status === 1} onCheckedChange={handleToggleStatus} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">任务详情</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<p>推送设备:{task.deviceCount} 个</p>
|
||||
<p>内容库:{task.contentLib || '未设置'}</p>
|
||||
<p>已同步:{task.syncCount} 条</p>
|
||||
<p>创建人:{task.creator}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">时间信息</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<p>创建时间:{task.createTime}</p>
|
||||
<p>上次同步:{task.lastSyncTime || '暂无'}</p>
|
||||
<p>更新时间:{task.updateTime}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">同步设置</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<p>同步间隔:{task.syncInterval} 秒</p>
|
||||
<p>每日最大:{task.maxSyncPerDay} 条</p>
|
||||
<p>时间范围:{task.timeRange.start} - {task.timeRange.end}</p>
|
||||
<p>同步模式:{task.syncMode === 'auto' ? '自动' : '手动'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">今日统计</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<p>今日同步:{task.todaySyncCount} 条</p>
|
||||
<p>总同步数:{task.totalSyncCount} 条</p>
|
||||
<p>目标标签:{task.targetTags.length} 个</p>
|
||||
<p>内容类型:{task.contentTypes.join(', ')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout header={header} footer={<BottomNav />}>
|
||||
<div className="flex-1 bg-gray-50 min-h-screen pb-20">
|
||||
<div className="p-4 max-w-4xl mx-auto space-y-6">
|
||||
{/* 基本信息卡片 */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<h2 className="text-2xl font-bold">{task.name}</h2>
|
||||
<Badge variant={task.status === 1 ? "default" : "secondary"}>
|
||||
{task.status === 1 ? "进行中" : "已暂停"}
|
||||
</Badge>
|
||||
</div>
|
||||
<Switch checked={task.status === 1} onCheckedChange={handleToggleStatus} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* 任务详情 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold flex items-center">
|
||||
<Database className="h-5 w-5 mr-2 text-blue-600" />
|
||||
任务详情
|
||||
</h3>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">推送设备</span>
|
||||
<span className="font-medium">{task.deviceCount} 个</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">内容库</span>
|
||||
<span className="font-medium">{task.contentLib || '未设置'}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">已同步</span>
|
||||
<span className="font-medium">{task.syncCount} 条</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">创建人</span>
|
||||
<span className="font-medium">{task.creator}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 时间信息 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold flex items-center">
|
||||
<Clock className="h-5 w-5 mr-2 text-green-600" />
|
||||
时间信息
|
||||
</h3>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">创建时间</span>
|
||||
<span className="font-medium">{task.createTime}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">上次同步</span>
|
||||
<span className="font-medium">{task.lastSyncTime || '暂无'}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">更新时间</span>
|
||||
<span className="font-medium">{task.updateTime || '暂无'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 同步设置卡片 */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<Smartphone className="h-5 w-5 mr-2 text-purple-600" />
|
||||
同步设置
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">同步间隔</span>
|
||||
<span className="font-medium">{task.syncInterval} 秒</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">每日最大</span>
|
||||
<span className="font-medium">{task.maxSyncPerDay || 100} 条</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">时间范围</span>
|
||||
<span className="font-medium">{task.timeRange.start} - {task.timeRange.end}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">同步模式</span>
|
||||
<span className="font-medium">{task.syncMode === 'auto' ? '自动' : '手动'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">今日同步</span>
|
||||
<span className="font-medium">{task.todaySyncCount || 0} 条</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">总同步数</span>
|
||||
<span className="font-medium">{task.totalSyncCount || task.syncCount || 0} 条</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">目标标签</span>
|
||||
<span className="font-medium">{task.targetTags.length} 个</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">内容类型</span>
|
||||
<span className="font-medium">{task.contentTypes.join(', ')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 同步记录卡片 */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">最近同步记录</h3>
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500">暂无同步记录</p>
|
||||
<p className="text-sm text-gray-400 mt-2">任务开始执行后将显示同步记录</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -27,13 +27,7 @@ export default function EditMomentsSyncTask() {
|
||||
filterKeywords: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchTaskDetail();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const fetchTaskDetail = async () => {
|
||||
const fetchTaskDetail = useCallback(async () => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -59,7 +53,13 @@ export default function EditMomentsSyncTask() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [id, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchTaskDetail();
|
||||
}
|
||||
}, [id, fetchTaskDetail]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
@@ -2,64 +2,243 @@ import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import { createMomentsSyncTask } from '@/api/momentsSync';
|
||||
import { ChevronLeft, Clock, Plus, Minus, Search } from 'lucide-react';
|
||||
import Layout from '@/components/Layout';
|
||||
import BottomNav from '@/components/BottomNav';
|
||||
|
||||
// 步骤指示器组件
|
||||
interface StepIndicatorProps {
|
||||
currentStep: number;
|
||||
}
|
||||
|
||||
function StepIndicator({ currentStep }: StepIndicatorProps) {
|
||||
const steps = [
|
||||
{ id: 1, title: "步骤 1", subtitle: "基础设置" },
|
||||
{ id: 2, title: "步骤 2", subtitle: "设备选择" },
|
||||
{ id: 3, title: "步骤 3", subtitle: "选择内容库" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="relative flex justify-between px-6">
|
||||
{steps.map((step) => (
|
||||
<div
|
||||
key={step.id}
|
||||
className={`flex flex-col items-center relative z-10 transition-colors ${
|
||||
currentStep >= step.id ? "text-blue-600" : "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center transition-all ${
|
||||
currentStep >= step.id
|
||||
? "bg-blue-600 text-white shadow-sm"
|
||||
: "bg-white border border-gray-200 text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{step.id}
|
||||
</div>
|
||||
<div className="text-xs mt-2 font-medium">{step.subtitle}</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="absolute top-4 left-0 right-0 h-[1px] bg-gray-100 -z-10">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
// 基础设置组件
|
||||
interface BasicSettingsProps {
|
||||
formData: {
|
||||
taskName: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
syncCount: number;
|
||||
accountType: "business" | "personal";
|
||||
enabled: boolean;
|
||||
};
|
||||
onChange: (data: Partial<BasicSettingsProps["formData"]>) => void;
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps) {
|
||||
return (
|
||||
<div className="space-y-6 p-4">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<div className="text-base font-medium mb-2">任务名称</div>
|
||||
<Input
|
||||
value={formData.taskName}
|
||||
onChange={(e) => onChange({ taskName: e.target.value })}
|
||||
placeholder="请输入任务名称"
|
||||
className="h-12 border-0 border-b border-gray-200 rounded-none focus-visible:ring-0 focus-visible:border-blue-600 px-0 text-base"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-base font-medium mb-2">允许发布时间段</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.startTime}
|
||||
onChange={(e) => onChange({ startTime: e.target.value })}
|
||||
className="h-12 pl-10 rounded-xl border-gray-200 text-base"
|
||||
/>
|
||||
<Clock className="absolute left-3 top-4 h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
<span className="text-gray-500">至</span>
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.endTime}
|
||||
onChange={(e) => onChange({ endTime: e.target.value })}
|
||||
className="h-12 pl-10 rounded-xl border-gray-200 text-base"
|
||||
/>
|
||||
<Clock className="absolute left-3 top-4 h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-base font-medium mb-2">每日同步数量</div>
|
||||
<div className="flex items-center space-x-5">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={() => onChange({ syncCount: Math.max(1, formData.syncCount - 1) })}
|
||||
className="h-12 w-12 rounded-xl bg-white border-gray-200"
|
||||
>
|
||||
<Minus className="h-5 w-5" />
|
||||
</Button>
|
||||
<span className="w-8 text-center text-lg font-medium">{formData.syncCount}</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={() => onChange({ syncCount: formData.syncCount + 1 })}
|
||||
className="h-12 w-12 rounded-xl bg-white border-gray-200"
|
||||
>
|
||||
<Plus className="h-5 w-5" />
|
||||
</Button>
|
||||
<span className="text-gray-500">条朋友圈</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-base font-medium mb-2">账号类型</div>
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onChange({ accountType: "business" })}
|
||||
className={`w-full h-12 justify-between rounded-lg ${
|
||||
formData.accountType === "business"
|
||||
? "bg-blue-600 hover:bg-blue-600 text-white"
|
||||
: "bg-white hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
业务号
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onChange({ accountType: "personal" })}
|
||||
className={`w-full h-12 justify-between rounded-lg ${
|
||||
formData.accountType === "personal"
|
||||
? "bg-blue-600 hover:bg-blue-600 text-white"
|
||||
: "bg-white hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
人设号
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-base font-medium">是否启用</span>
|
||||
<Switch
|
||||
checked={formData.enabled}
|
||||
onCheckedChange={(checked) => onChange({ enabled: checked })}
|
||||
className="data-[state=checked]:bg-blue-600 h-7 w-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={onNext}
|
||||
className="w-full h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base font-medium shadow-sm"
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NewMomentsSyncTask() {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
deviceIds: '', // 逗号分隔
|
||||
contentLib: '',
|
||||
syncMode: 'auto',
|
||||
syncInterval: '30',
|
||||
maxSyncPerDay: '100',
|
||||
timeStart: '08:00',
|
||||
timeEnd: '22:00',
|
||||
targetTags: '', // 逗号分隔
|
||||
contentTypes: '', // 逗号分隔
|
||||
filterKeywords: '', // 逗号分隔
|
||||
const [formData, setFormData] = useState({
|
||||
taskName: '',
|
||||
startTime: '06:00',
|
||||
endTime: '23:59',
|
||||
syncCount: 5,
|
||||
accountType: 'business' as 'business' | 'personal',
|
||||
enabled: true,
|
||||
selectedDevices: [] as string[],
|
||||
selectedLibraries: [] as string[],
|
||||
});
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
const handleUpdateFormData = (data: Partial<typeof formData>) => {
|
||||
setFormData((prev) => ({ ...prev, ...data }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!form.name.trim()) {
|
||||
const handleNext = () => {
|
||||
setCurrentStep((prev) => Math.min(prev + 1, 3));
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
setCurrentStep((prev) => Math.max(prev - 1, 1));
|
||||
};
|
||||
|
||||
const handleComplete = async () => {
|
||||
if (!formData.taskName.trim()) {
|
||||
toast({ title: '请输入任务名称', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
if (!form.deviceIds.trim()) {
|
||||
toast({ title: '请输入推送设备', variant: 'destructive' });
|
||||
if (formData.selectedDevices.length === 0) {
|
||||
toast({ title: '请选择设备', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
if (!form.contentLib.trim()) {
|
||||
toast({ title: '请输入内容库', variant: 'destructive' });
|
||||
if (formData.selectedLibraries.length === 0) {
|
||||
toast({ title: '请选择内容库', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await createMomentsSyncTask({
|
||||
name: form.name,
|
||||
devices: form.deviceIds.split(',').map(s => s.trim()).filter(Boolean),
|
||||
contentLib: form.contentLib,
|
||||
syncMode: form.syncMode as 'auto' | 'manual',
|
||||
interval: Number(form.syncInterval),
|
||||
maxSync: Number(form.maxSyncPerDay),
|
||||
startTime: form.timeStart,
|
||||
endTime: form.timeEnd,
|
||||
targetTags: form.targetTags.split(',').map(s => s.trim()).filter(Boolean),
|
||||
contentTypes: form.contentTypes.split(',').map(s => s.trim()).filter(Boolean) as ('text' | 'image' | 'video' | 'link')[],
|
||||
filterKeywords: form.filterKeywords.split(',').map(s => s.trim()).filter(Boolean),
|
||||
});
|
||||
await createMomentsSyncTask({
|
||||
name: formData.taskName,
|
||||
devices: formData.selectedDevices,
|
||||
contentLib: formData.selectedLibraries.join(','),
|
||||
syncMode: formData.accountType === 'business' ? 'auto' : 'manual',
|
||||
interval: 30,
|
||||
maxSync: formData.syncCount,
|
||||
startTime: formData.startTime,
|
||||
endTime: formData.endTime,
|
||||
targetTags: [],
|
||||
contentTypes: ['text', 'image', 'video'],
|
||||
filterKeywords: [],
|
||||
friends: [],
|
||||
});
|
||||
toast({ title: '创建成功' });
|
||||
navigate('/workspace/moments-sync');
|
||||
} catch (error) {
|
||||
@@ -69,67 +248,97 @@ export default function NewMomentsSyncTask() {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-gray-50 min-h-screen pb-20">
|
||||
<div className="max-w-xl mx-auto bg-white rounded shadow p-6 mt-8">
|
||||
<h2 className="text-lg font-bold mb-6">新建朋友圈同步任务</h2>
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label className="block mb-1 font-medium">任务名称</label>
|
||||
<Input value={form.name} onChange={handleChange} placeholder="请输入任务名称" name="name" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1 font-medium">推送设备ID(逗号分隔)</label>
|
||||
<Input value={form.deviceIds} onChange={handleChange} placeholder="如:dev1,dev2" name="deviceIds" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1 font-medium">内容库</label>
|
||||
<Input value={form.contentLib} onChange={handleChange} placeholder="请输入内容库名称" name="contentLib" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1 font-medium">同步模式</label>
|
||||
<select name="syncMode" value={form.syncMode} onChange={handleChange} className="w-full border rounded px-3 py-2">
|
||||
<option value="auto">自动</option>
|
||||
<option value="manual">手动</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1">
|
||||
<label className="block mb-1 font-medium">同步间隔(秒)</label>
|
||||
<Input value={form.syncInterval} onChange={handleChange} name="syncInterval" type="number" min={1} required />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="block mb-1 font-medium">每日最大同步数</label>
|
||||
<Input value={form.maxSyncPerDay} onChange={handleChange} name="maxSyncPerDay" type="number" min={1} required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1">
|
||||
<label className="block mb-1 font-medium">开始时间</label>
|
||||
<Input value={form.timeStart} onChange={handleChange} name="timeStart" type="time" required />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="block mb-1 font-medium">结束时间</label>
|
||||
<Input value={form.timeEnd} onChange={handleChange} name="timeEnd" type="time" required />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1 font-medium">目标人群标签(逗号分隔)</label>
|
||||
<Input value={form.targetTags} onChange={handleChange} placeholder="如:重要客户,活跃用户" name="targetTags" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1 font-medium">内容类型(逗号分隔,支持text,image,video)</label>
|
||||
<Input value={form.contentTypes} onChange={handleChange} placeholder="如:text,image,video" name="contentTypes" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1 font-medium">关键词过滤(逗号分隔,可选)</label>
|
||||
<Input value={form.filterKeywords} onChange={handleChange} placeholder="如:产品,服务,优惠" name="filterKeywords" />
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<Button type="submit" loading={loading} className="w-full">{loading ? '提交中...' : '创建任务'}</Button>
|
||||
</div>
|
||||
</form>
|
||||
const header = (
|
||||
<div className="sticky top-0 z-10 bg-white">
|
||||
<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">新建朋友圈同步</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout header={header} footer={<BottomNav />}>
|
||||
<div className="min-h-screen bg-[#F8F9FA] pb-20">
|
||||
<div className="mt-8">
|
||||
<StepIndicator currentStep={currentStep} />
|
||||
|
||||
<div className="mt-8">
|
||||
{currentStep === 1 && (
|
||||
<BasicSettings formData={formData} onChange={handleUpdateFormData} onNext={handleNext} />
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
<div className="space-y-6 px-6">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-4 h-5 w-5 text-gray-400" />
|
||||
<Input
|
||||
placeholder="选择设备"
|
||||
className="h-12 pl-11 rounded-xl border-gray-200 text-base"
|
||||
onClick={() => {
|
||||
// TODO: 打开设备选择弹窗
|
||||
toast({ title: '设备选择功能开发中' });
|
||||
}}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.selectedDevices.length > 0 && (
|
||||
<div className="text-base text-gray-500">已选设备:{formData.selectedDevices.length} 个</div>
|
||||
)}
|
||||
|
||||
<div className="flex space-x-4 pt-4">
|
||||
<Button variant="outline" onClick={handlePrev} className="flex-1 h-12 rounded-xl text-base">
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 3 && (
|
||||
<div className="space-y-6 px-6">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-4 h-5 w-5 text-gray-400" />
|
||||
<Input
|
||||
placeholder="选择内容库"
|
||||
className="h-12 pl-11 rounded-xl border-gray-200 text-base"
|
||||
onClick={() => {
|
||||
// TODO: 打开内容库选择弹窗
|
||||
toast({ title: '内容库选择功能开发中' });
|
||||
}}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.selectedLibraries.length > 0 && (
|
||||
<div className="text-base text-gray-500">已选内容库:{formData.selectedLibraries.join(', ')}</div>
|
||||
)}
|
||||
|
||||
<div className="flex space-x-4 pt-4">
|
||||
<Button variant="outline" onClick={handlePrev} className="flex-1 h-12 rounded-xl text-base">
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleComplete}
|
||||
loading={loading}
|
||||
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
|
||||
>
|
||||
{loading ? '创建中...' : '完成'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Edit,
|
||||
Trash2,
|
||||
Pause,
|
||||
Users,
|
||||
|
||||
Share2,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
@@ -18,7 +18,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
|
||||
// 不再使用 DropdownMenu 组件
|
||||
// import {
|
||||
// DropdownMenu,
|
||||
@@ -101,26 +101,7 @@ export default function TrafficDistribution() {
|
||||
navigate(`/workspace/traffic-distribution/${ruleId}/edit`);
|
||||
};
|
||||
|
||||
const handleView = (ruleId: string) => {
|
||||
navigate(`/workspace/traffic-distribution/${ruleId}`);
|
||||
};
|
||||
|
||||
const handleCopy = (ruleId: string) => {
|
||||
const ruleToCopy = tasks.find((rule) => rule.id === ruleId);
|
||||
if (ruleToCopy) {
|
||||
const newRule = {
|
||||
...ruleToCopy,
|
||||
id: `${Date.now()}`,
|
||||
name: `${ruleToCopy.name} (复制)`,
|
||||
createTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
|
||||
};
|
||||
setTasks([...tasks, newRule]);
|
||||
toast({
|
||||
title: '复制成功',
|
||||
description: '已成功复制分发规则',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRuleStatus = (ruleId: string) => {
|
||||
const rule = tasks.find((r) => r.id === ruleId);
|
||||
@@ -206,31 +187,7 @@ export default function TrafficDistribution() {
|
||||
rule.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return 'bg-green-100 text-green-800';
|
||||
case 'paused':
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
case 'completed':
|
||||
return 'bg-blue-100 text-blue-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return '进行中';
|
||||
case 'paused':
|
||||
return '已暂停';
|
||||
case 'completed':
|
||||
return '已完成';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
// 模拟加载数据
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2405,7 +2405,7 @@
|
||||
dependencies:
|
||||
"@radix-ui/react-primitive" "2.1.3"
|
||||
|
||||
"@radix-ui/react-slider@latest":
|
||||
"@radix-ui/react-slider@^1.3.5":
|
||||
version "1.3.5"
|
||||
resolved "https://registry.npmmirror.com/@radix-ui/react-slider/-/react-slider-1.3.5.tgz"
|
||||
integrity sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==
|
||||
@@ -10898,7 +10898,7 @@ symbol-tree@^3.2.4:
|
||||
resolved "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz"
|
||||
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
|
||||
|
||||
tailwind-merge@^2.5.5:
|
||||
tailwind-merge@^2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz"
|
||||
integrity sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==
|
||||
|
||||
Reference in New Issue
Block a user