feat: 暂时可以了

This commit is contained in:
许永平
2025-07-09 19:13:23 +08:00
parent d78698aa6b
commit c75b8d596d
10 changed files with 518 additions and 521 deletions

View File

@@ -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",

View File

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

View File

@@ -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}`}
/>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(() => {

View File

@@ -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==