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-scroll-area": "latest",
|
||||||
"@radix-ui/react-select": "latest",
|
"@radix-ui/react-select": "latest",
|
||||||
"@radix-ui/react-separator": "^1.1.1",
|
"@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-slot": "^1.1.1",
|
||||||
"@radix-ui/react-switch": "latest",
|
"@radix-ui/react-switch": "latest",
|
||||||
"@radix-ui/react-tabs": "latest",
|
"@radix-ui/react-tabs": "latest",
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
"recharts": "latest",
|
"recharts": "latest",
|
||||||
"regenerator-runtime": "latest",
|
"regenerator-runtime": "latest",
|
||||||
"sonner": "^1.7.4",
|
"sonner": "^1.7.4",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tdesign-mobile-react": "^0.16.0",
|
"tdesign-mobile-react": "^0.16.0",
|
||||||
"vaul": "^0.9.6",
|
"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 GroupPush from './pages/workspace/group-push/GroupPush';
|
||||||
import MomentsSync from './pages/workspace/moments-sync/MomentsSync';
|
import MomentsSync from './pages/workspace/moments-sync/MomentsSync';
|
||||||
import MomentsSyncDetail from './pages/workspace/moments-sync/Detail';
|
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 EditMomentsSync from './pages/workspace/moments-sync/edit';
|
||||||
import AIAssistant from './pages/workspace/ai-assistant/AIAssistant';
|
import AIAssistant from './pages/workspace/ai-assistant/AIAssistant';
|
||||||
import TrafficDistribution from './pages/workspace/traffic-distribution/TrafficDistribution';
|
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/auto-group/:id" element={<AutoGroupDetail />} />
|
||||||
<Route path="/workspace/group-push" element={<GroupPush />} />
|
<Route path="/workspace/group-push" element={<GroupPush />} />
|
||||||
<Route path="/workspace/moments-sync" element={<MomentsSync />} />
|
<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/:id" element={<MomentsSyncDetail />} />
|
||||||
<Route path="/workspace/moments-sync/edit/:id" element={<EditMomentsSync />} />
|
<Route path="/workspace/moments-sync/edit/:id" element={<EditMomentsSync />} />
|
||||||
<Route path="/workspace/ai-assistant" element={<AIAssistant />} />
|
<Route path="/workspace/ai-assistant" element={<AIAssistant />} />
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ interface InputProps {
|
|||||||
type?: string;
|
type?: string;
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
|
name?: string;
|
||||||
|
required?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
autoComplete?: string;
|
||||||
|
step?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Input({
|
export function Input({
|
||||||
@@ -27,7 +32,12 @@ export function Input({
|
|||||||
id,
|
id,
|
||||||
type = 'text',
|
type = 'text',
|
||||||
min,
|
min,
|
||||||
max
|
max,
|
||||||
|
name,
|
||||||
|
required = false,
|
||||||
|
disabled = false,
|
||||||
|
autoComplete,
|
||||||
|
step
|
||||||
}: InputProps) {
|
}: InputProps) {
|
||||||
const isReadOnly = readOnly || readonly;
|
const isReadOnly = readOnly || readonly;
|
||||||
|
|
||||||
@@ -43,6 +53,11 @@ export function Input({
|
|||||||
readOnly={isReadOnly}
|
readOnly={isReadOnly}
|
||||||
min={min}
|
min={min}
|
||||||
max={max}
|
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}`}
|
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 React, { useState, useRef, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import {
|
import {
|
||||||
Send,
|
Send,
|
||||||
Settings,
|
Settings,
|
||||||
@@ -45,7 +45,6 @@ interface Conversation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AIAssistant() {
|
export default function AIAssistant() {
|
||||||
const navigate = useNavigate();
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||||
const [currentConversation, setCurrentConversation] = useState<Conversation | null>(null);
|
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 { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
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 { createAutoLikeTask, updateAutoLikeTask, fetchAutoLikeTaskDetail } from '@/api/autoLike';
|
||||||
import { ContentType } from '@/types/auto-like';
|
import { ContentType } from '@/types/auto-like';
|
||||||
import { useToast } from '@/components/ui/toast';
|
import { useToast } from '@/components/ui/toast';
|
||||||
import Layout from '@/components/Layout';
|
import Layout from '@/components/Layout';
|
||||||
import { fetchDeviceList } from '@/api/devices';
|
import { fetchDeviceList } from '@/api/devices';
|
||||||
import type { Device } from '@/types/device';
|
|
||||||
import { get } from '@/api/request';
|
import { get } from '@/api/request';
|
||||||
|
|
||||||
interface TagGroup {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用于设备选择弹窗的简化设备类型
|
// 用于设备选择弹窗的简化设备类型
|
||||||
interface DeviceSelectionItem {
|
interface DeviceSelectionItem {
|
||||||
@@ -177,7 +173,7 @@ export default function NewAutoLike() {
|
|||||||
|
|
||||||
// 处理状态字段,使用双等号允许类型自动转换
|
// 处理状态字段,使用双等号允许类型自动转换
|
||||||
const status = taskAny.status;
|
const status = taskAny.status;
|
||||||
setAutoEnabled(status == 1 || status == 'running');
|
setAutoEnabled(status === 1 || status === 'running');
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
title: '获取任务详情失败',
|
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>) => {
|
const handleUpdateFormData = (data: Partial<CreateLikeTaskDataLocal>) => {
|
||||||
setFormData((prev) => ({ ...prev, ...data }));
|
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 {
|
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 = () => {
|
const handleConfirm = () => {
|
||||||
onOpenChange(false);
|
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 { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
@@ -11,7 +11,9 @@ import {
|
|||||||
syncMoments
|
syncMoments
|
||||||
} from '@/api/momentsSync';
|
} from '@/api/momentsSync';
|
||||||
import { MomentsSyncTask } from '@/types/moments-sync';
|
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() {
|
export default function MomentsSyncDetail() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
@@ -20,13 +22,7 @@ export default function MomentsSyncDetail() {
|
|||||||
const [task, setTask] = useState<MomentsSyncTask | null>(null);
|
const [task, setTask] = useState<MomentsSyncTask | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchTaskDetail = useCallback(async () => {
|
||||||
if (id) {
|
|
||||||
fetchTaskDetail();
|
|
||||||
}
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
const fetchTaskDetail = async () => {
|
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -39,9 +35,13 @@ export default function MomentsSyncDetail() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [id, toast]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
fetchTaskDetail();
|
||||||
|
}
|
||||||
|
}, [id, fetchTaskDetail]);
|
||||||
|
|
||||||
const handleToggleStatus = async () => {
|
const handleToggleStatus = async () => {
|
||||||
if (!task || !id) return;
|
if (!task || !id) return;
|
||||||
@@ -74,106 +74,178 @@ export default function MomentsSyncDetail() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 bg-gray-50 min-h-screen flex items-center justify-center">
|
<Layout>
|
||||||
<div className="text-center">
|
<div className="flex-1 bg-gray-50 min-h-screen flex items-center justify-center">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
<div className="text-center">
|
||||||
<p className="text-gray-600">加载中...</p>
|
<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>
|
||||||
</div>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!task) {
|
if (!task) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 bg-gray-50 min-h-screen flex items-center justify-center">
|
<Layout>
|
||||||
<div className="text-center">
|
<div className="flex-1 bg-gray-50 min-h-screen flex items-center justify-center">
|
||||||
<p className="text-gray-600">任务不存在</p>
|
<div className="text-center">
|
||||||
<Button onClick={() => navigate('/workspace/moments-sync')} className="mt-4">
|
<p className="text-gray-600">任务不存在</p>
|
||||||
返回列表
|
<Button onClick={() => navigate('/workspace/moments-sync')} className="mt-4">
|
||||||
</Button>
|
返回列表
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const header = (
|
||||||
<div className="flex-1 bg-gray-50 min-h-screen">
|
<div className="sticky top-0 z-10 bg-white border-b">
|
||||||
{/* Header */}
|
<div className="flex items-center justify-between p-4">
|
||||||
<header className="sticky top-0 z-10 bg-white border-b">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="flex items-center justify-between p-4">
|
<Button variant="ghost" size="icon" onClick={() => navigate('/workspace/moments-sync')}>
|
||||||
<div className="flex items-center space-x-3">
|
<ChevronLeft className="h-5 w-5" />
|
||||||
<Button variant="ghost" size="icon" onClick={() => navigate('/workspace/moments-sync')}>
|
</Button>
|
||||||
<ChevronLeft className="h-5 w-5" />
|
<h1 className="text-lg font-medium">朋友圈同步任务详情</h1>
|
||||||
</Button>
|
</div>
|
||||||
<h1 className="text-lg font-medium">朋友圈同步任务详情</h1>
|
<div className="flex items-center space-x-2">
|
||||||
</div>
|
<Button variant="outline" size="sm" onClick={handleEdit}>
|
||||||
<div className="flex items-center space-x-2">
|
<Edit2 className="h-4 w-4 mr-2" />
|
||||||
<Button variant="outline" size="sm" onClick={handleEdit}>
|
编辑
|
||||||
<Edit2 className="h-4 w-4 mr-2" />
|
</Button>
|
||||||
编辑
|
<Button variant="outline" size="sm" onClick={handleSync}>
|
||||||
</Button>
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
<Button variant="outline" size="sm" onClick={handleSync}>
|
立即同步
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
</Button>
|
||||||
立即同步
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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>
|
||||||
</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 { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -27,13 +27,7 @@ export default function EditMomentsSyncTask() {
|
|||||||
filterKeywords: '',
|
filterKeywords: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchTaskDetail = useCallback(async () => {
|
||||||
if (id) {
|
|
||||||
fetchTaskDetail();
|
|
||||||
}
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
const fetchTaskDetail = async () => {
|
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -59,7 +53,13 @@ export default function EditMomentsSyncTask() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [id, toast]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
fetchTaskDetail();
|
||||||
|
}
|
||||||
|
}, [id, fetchTaskDetail]);
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
|
|||||||
@@ -2,64 +2,243 @@ import React, { useState } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { useToast } from '@/components/ui/toast';
|
import { useToast } from '@/components/ui/toast';
|
||||||
import { createMomentsSyncTask } from '@/api/momentsSync';
|
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() {
|
export default function NewMomentsSyncTask() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const [currentStep, setCurrentStep] = useState(1);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [form, setForm] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
taskName: '',
|
||||||
deviceIds: '', // 逗号分隔
|
startTime: '06:00',
|
||||||
contentLib: '',
|
endTime: '23:59',
|
||||||
syncMode: 'auto',
|
syncCount: 5,
|
||||||
syncInterval: '30',
|
accountType: 'business' as 'business' | 'personal',
|
||||||
maxSyncPerDay: '100',
|
enabled: true,
|
||||||
timeStart: '08:00',
|
selectedDevices: [] as string[],
|
||||||
timeEnd: '22:00',
|
selectedLibraries: [] as string[],
|
||||||
targetTags: '', // 逗号分隔
|
|
||||||
contentTypes: '', // 逗号分隔
|
|
||||||
filterKeywords: '', // 逗号分隔
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
const handleUpdateFormData = (data: Partial<typeof formData>) => {
|
||||||
const { name, value } = e.target;
|
setFormData((prev) => ({ ...prev, ...data }));
|
||||||
setForm(prev => ({
|
|
||||||
...prev,
|
|
||||||
[name]: value,
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleNext = () => {
|
||||||
e.preventDefault();
|
setCurrentStep((prev) => Math.min(prev + 1, 3));
|
||||||
if (!form.name.trim()) {
|
};
|
||||||
|
|
||||||
|
const handlePrev = () => {
|
||||||
|
setCurrentStep((prev) => Math.max(prev - 1, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleComplete = async () => {
|
||||||
|
if (!formData.taskName.trim()) {
|
||||||
toast({ title: '请输入任务名称', variant: 'destructive' });
|
toast({ title: '请输入任务名称', variant: 'destructive' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!form.deviceIds.trim()) {
|
if (formData.selectedDevices.length === 0) {
|
||||||
toast({ title: '请输入推送设备', variant: 'destructive' });
|
toast({ title: '请选择设备', variant: 'destructive' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!form.contentLib.trim()) {
|
if (formData.selectedLibraries.length === 0) {
|
||||||
toast({ title: '请输入内容库', variant: 'destructive' });
|
toast({ title: '请选择内容库', variant: 'destructive' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await createMomentsSyncTask({
|
await createMomentsSyncTask({
|
||||||
name: form.name,
|
name: formData.taskName,
|
||||||
devices: form.deviceIds.split(',').map(s => s.trim()).filter(Boolean),
|
devices: formData.selectedDevices,
|
||||||
contentLib: form.contentLib,
|
contentLib: formData.selectedLibraries.join(','),
|
||||||
syncMode: form.syncMode as 'auto' | 'manual',
|
syncMode: formData.accountType === 'business' ? 'auto' : 'manual',
|
||||||
interval: Number(form.syncInterval),
|
interval: 30,
|
||||||
maxSync: Number(form.maxSyncPerDay),
|
maxSync: formData.syncCount,
|
||||||
startTime: form.timeStart,
|
startTime: formData.startTime,
|
||||||
endTime: form.timeEnd,
|
endTime: formData.endTime,
|
||||||
targetTags: form.targetTags.split(',').map(s => s.trim()).filter(Boolean),
|
targetTags: [],
|
||||||
contentTypes: form.contentTypes.split(',').map(s => s.trim()).filter(Boolean) as ('text' | 'image' | 'video' | 'link')[],
|
contentTypes: ['text', 'image', 'video'],
|
||||||
filterKeywords: form.filterKeywords.split(',').map(s => s.trim()).filter(Boolean),
|
filterKeywords: [],
|
||||||
});
|
friends: [],
|
||||||
|
});
|
||||||
toast({ title: '创建成功' });
|
toast({ title: '创建成功' });
|
||||||
navigate('/workspace/moments-sync');
|
navigate('/workspace/moments-sync');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -69,67 +248,97 @@ export default function NewMomentsSyncTask() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const header = (
|
||||||
<div className="flex-1 bg-gray-50 min-h-screen pb-20">
|
<div className="sticky top-0 z-10 bg-white">
|
||||||
<div className="max-w-xl mx-auto bg-white rounded shadow p-6 mt-8">
|
<div className="flex items-center h-14 px-4">
|
||||||
<h2 className="text-lg font-bold mb-6">新建朋友圈同步任务</h2>
|
<Button variant="ghost" size="icon" onClick={() => navigate(-1)} className="hover:bg-gray-50">
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
<ChevronLeft className="h-6 w-6" />
|
||||||
<div>
|
</Button>
|
||||||
<label className="block mb-1 font-medium">任务名称</label>
|
<h1 className="ml-2 text-lg font-medium">新建朋友圈同步</h1>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</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,
|
Edit,
|
||||||
Trash2,
|
Trash2,
|
||||||
Pause,
|
Pause,
|
||||||
Users,
|
|
||||||
Share2,
|
Share2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
@@ -18,7 +18,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Progress } from '@/components/ui/progress';
|
|
||||||
// 不再使用 DropdownMenu 组件
|
// 不再使用 DropdownMenu 组件
|
||||||
// import {
|
// import {
|
||||||
// DropdownMenu,
|
// DropdownMenu,
|
||||||
@@ -101,26 +101,7 @@ export default function TrafficDistribution() {
|
|||||||
navigate(`/workspace/traffic-distribution/${ruleId}/edit`);
|
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 toggleRuleStatus = (ruleId: string) => {
|
||||||
const rule = tasks.find((r) => r.id === ruleId);
|
const rule = tasks.find((r) => r.id === ruleId);
|
||||||
@@ -206,31 +187,7 @@ export default function TrafficDistribution() {
|
|||||||
rule.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
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(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -2405,7 +2405,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-primitive" "2.1.3"
|
"@radix-ui/react-primitive" "2.1.3"
|
||||||
|
|
||||||
"@radix-ui/react-slider@latest":
|
"@radix-ui/react-slider@^1.3.5":
|
||||||
version "1.3.5"
|
version "1.3.5"
|
||||||
resolved "https://registry.npmmirror.com/@radix-ui/react-slider/-/react-slider-1.3.5.tgz"
|
resolved "https://registry.npmmirror.com/@radix-ui/react-slider/-/react-slider-1.3.5.tgz"
|
||||||
integrity sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==
|
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"
|
resolved "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz"
|
||||||
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
|
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
|
||||||
|
|
||||||
tailwind-merge@^2.5.5:
|
tailwind-merge@^2.6.0:
|
||||||
version "2.6.0"
|
version "2.6.0"
|
||||||
resolved "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz"
|
resolved "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz"
|
||||||
integrity sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==
|
integrity sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==
|
||||||
|
|||||||
Reference in New Issue
Block a user