From 6d417dae2ad8534329600abe18f81953ff28608a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AE=B8=E6=B0=B8=E5=B9=B3?= Date: Wed, 9 Jul 2025 20:07:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=9D=E5=AD=98=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/api/content.ts | 69 +++++ .../ContentLibrarySelectionDialog.tsx | 174 ++++++++++++ .../src/components/DeviceSelectionDialog.tsx | 184 +++++++++++++ nkebao/src/components/ui/checkbox.tsx | 73 ++--- nkebao/src/components/ui/dialog.tsx | 4 +- .../src/pages/workspace/moments-sync/new.tsx | 249 ++++++++++++------ 6 files changed, 639 insertions(+), 114 deletions(-) create mode 100644 nkebao/src/api/content.ts create mode 100644 nkebao/src/components/ContentLibrarySelectionDialog.tsx create mode 100644 nkebao/src/components/DeviceSelectionDialog.tsx diff --git a/nkebao/src/api/content.ts b/nkebao/src/api/content.ts new file mode 100644 index 00000000..e7cd0e63 --- /dev/null +++ b/nkebao/src/api/content.ts @@ -0,0 +1,69 @@ +import { get, post, put, del } from './request'; +import type { ApiResponse, PaginatedResponse } from '@/types/common'; + +// 内容库类型定义 +export interface ContentLibrary { + id: string; + name: string; + sourceType: number; + creatorName: string; + updateTime: string; + status: number; +} + +// 内容库列表响应 +export interface ContentLibraryListResponse { + code: number; + msg: string; + data: { + list: ContentLibrary[]; + total: number; + page: number; + limit: number; + }; +} + +// 获取内容库列表 +export const fetchContentLibraryList = async ( + page: number = 1, + limit: number = 100, + keyword?: string +): Promise => { + const params = new URLSearchParams(); + params.append('page', page.toString()); + params.append('limit', limit.toString()); + + if (keyword) { + params.append('keyword', keyword); + } + + return get(`/v1/content/library/list?${params.toString()}`); +}; + +// 内容库API对象 +export const contentLibraryApi = { + // 获取内容库列表 + async getList(page: number = 1, limit: number = 100, keyword?: string): Promise { + return fetchContentLibraryList(page, limit, keyword); + }, + + // 创建内容库 + async create(params: { name: string; sourceType: number }): Promise> { + return post>('/v1/content/library', params); + }, + + // 更新内容库 + async update(id: string, params: Partial): Promise> { + return put>(`/v1/content/library/${id}`, params); + }, + + // 删除内容库 + async delete(id: string): Promise> { + return del>(`/v1/content/library/${id}`); + }, + + // 获取内容库详情 + async getById(id: string): Promise> { + return get>(`/v1/content/library/${id}`); + }, +}; \ No newline at end of file diff --git a/nkebao/src/components/ContentLibrarySelectionDialog.tsx b/nkebao/src/components/ContentLibrarySelectionDialog.tsx new file mode 100644 index 00000000..dcbbc0a1 --- /dev/null +++ b/nkebao/src/components/ContentLibrarySelectionDialog.tsx @@ -0,0 +1,174 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Search, RefreshCw, Loader2 } from 'lucide-react'; +import { fetchContentLibraryList } from '@/api/content'; +import { ContentLibrary } from '@/api/content'; +import { useToast } from '@/components/ui/toast'; + +interface ContentLibrarySelectionDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + selectedLibraries: string[]; + onSelect: (libraries: string[]) => void; +} + +export function ContentLibrarySelectionDialog({ + open, + onOpenChange, + selectedLibraries, + onSelect, +}: ContentLibrarySelectionDialogProps) { + const { toast } = useToast(); + const [searchQuery, setSearchQuery] = useState(''); + const [loading, setLoading] = useState(false); + const [libraries, setLibraries] = useState([]); + const [tempSelected, setTempSelected] = useState([]); + + // 获取内容库列表 + const fetchLibraries = useCallback(async () => { + setLoading(true); + try { + const response = await fetchContentLibraryList(1, 100, searchQuery); + if (response.code === 200 && response.data) { + setLibraries(response.data.list); + } else { + toast({ title: '获取内容库列表失败', description: response.msg, variant: 'destructive' }); + } + } catch (error) { + console.error('获取内容库列表失败:', error); + toast({ title: '获取内容库列表失败', description: '请检查网络连接', variant: 'destructive' }); + } finally { + setLoading(false); + } + }, [searchQuery, toast]); + + useEffect(() => { + if (open) { + fetchLibraries(); + setTempSelected(selectedLibraries); + } + }, [open, searchQuery, selectedLibraries, fetchLibraries]); + + const handleRefresh = () => { + fetchLibraries(); + }; + + const handleSelectAll = () => { + if (tempSelected.length === libraries.length) { + setTempSelected([]); + } else { + setTempSelected(libraries.map(lib => lib.id)); + } + }; + + const handleLibraryToggle = (libraryId: string) => { + setTempSelected(prev => + prev.includes(libraryId) + ? prev.filter(id => id !== libraryId) + : [...prev, libraryId] + ); + }; + + const handleDialogOpenChange = (open: boolean) => { + if (!open) { + setTempSelected(selectedLibraries); + } + onOpenChange(open); + }; + + const handleConfirm = () => { + onSelect(tempSelected); + onOpenChange(false); + }; + + return ( + + + + 选择内容库 + + +
+
+ + setSearchQuery(e.target.value)} + /> +
+ +
+ +
+
+ 已选择 {tempSelected.length} 个内容库 +
+
+ +
+
+ +
+
+ {loading ? ( +
+ 加载中... +
+ ) : libraries.length === 0 ? ( +
+ 暂无数据 +
+ ) : ( + libraries.map((library) => ( + + )) + )} +
+
+ +
+
+ 已选择 {tempSelected.length} 个内容库 +
+
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/nkebao/src/components/DeviceSelectionDialog.tsx b/nkebao/src/components/DeviceSelectionDialog.tsx new file mode 100644 index 00000000..ddb8af33 --- /dev/null +++ b/nkebao/src/components/DeviceSelectionDialog.tsx @@ -0,0 +1,184 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Search, RefreshCw, Loader2 } from 'lucide-react'; +import { fetchDeviceList } from '@/api/devices'; +import { ServerDevice } from '@/types/device'; +import { useToast } from '@/components/ui/toast'; + +interface Device { + id: string; + name: string; + imei: string; + wxid: string; + status: 'online' | 'offline'; + usedInPlans: number; +} + +interface DeviceSelectionDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + selectedDevices: string[]; + onSelect: (devices: string[]) => void; +} + +export function DeviceSelectionDialog({ + open, + onOpenChange, + selectedDevices, + onSelect +}: DeviceSelectionDialogProps) { + const { toast } = useToast(); + const [searchQuery, setSearchQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [loading, setLoading] = useState(false); + const [devices, setDevices] = useState([]); + + // 获取设备列表 + const fetchDevices = useCallback(async () => { + setLoading(true); + try { + const response = await fetchDeviceList(1, 100, searchQuery); + if (response.code === 200 && response.data) { + // 转换服务端数据格式为组件需要的格式 + const convertedDevices: Device[] = response.data.list.map((serverDevice: ServerDevice) => ({ + id: serverDevice.id.toString(), + name: serverDevice.memo || `设备 ${serverDevice.id}`, + imei: serverDevice.imei, + wxid: serverDevice.wechatId || '', + status: serverDevice.alive === 1 ? 'online' : 'offline', + usedInPlans: 0, // 这个字段需要从其他API获取 + })); + setDevices(convertedDevices); + } else { + toast({ title: '获取设备列表失败', description: response.msg, variant: 'destructive' }); + } + } catch (error) { + console.error('获取设备列表失败:', error); + toast({ title: '获取设备列表失败', description: '请检查网络连接', variant: 'destructive' }); + } finally { + setLoading(false); + } + }, [searchQuery, toast]); + + useEffect(() => { + if (open) { + fetchDevices(); + } + }, [open, searchQuery, fetchDevices]); + + const filteredDevices = devices.filter((device) => { + const matchesSearch = + device.name.toLowerCase().includes(searchQuery.toLowerCase()) || + device.imei.toLowerCase().includes(searchQuery.toLowerCase()) || + device.wxid.toLowerCase().includes(searchQuery.toLowerCase()); + + const matchesStatus = + statusFilter === 'all' || + (statusFilter === 'online' && device.status === 'online') || + (statusFilter === 'offline' && device.status === 'offline'); + + return matchesSearch && matchesStatus; + }); + + const handleDeviceSelect = (deviceId: string) => { + if (selectedDevices.includes(deviceId)) { + onSelect(selectedDevices.filter(id => id !== deviceId)); + } else { + onSelect([...selectedDevices, deviceId]); + } + }; + + return ( + + + + 选择设备 + + +
+
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> +
+ + +
+ +
+
+ {loading ? ( +
+ 加载中... +
+ ) : filteredDevices.length === 0 ? ( +
+ 暂无数据 +
+ ) : ( + filteredDevices.map((device) => ( + + )) + )} +
+
+ +
+
+ 已选择 {selectedDevices.length} 个设备 +
+
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/nkebao/src/components/ui/checkbox.tsx b/nkebao/src/components/ui/checkbox.tsx index f5f8daaf..974936a4 100644 --- a/nkebao/src/components/ui/checkbox.tsx +++ b/nkebao/src/components/ui/checkbox.tsx @@ -1,36 +1,39 @@ -import React from 'react'; - -interface CheckboxProps { - checked?: boolean; - onCheckedChange?: (checked: boolean) => void; - onChange?: (checked: boolean) => void; - disabled?: boolean; - className?: string; - id?: string; -} - -export function Checkbox({ - checked = false, - onCheckedChange, - onChange, - disabled = false, - className = '', - id -}: CheckboxProps) { - const handleChange = (e: React.ChangeEvent) => { - const newChecked = e.target.checked; - onCheckedChange?.(newChecked); - onChange?.(newChecked); - }; - - return ( - - ); +import React from 'react'; + +interface CheckboxProps { + checked?: boolean; + onCheckedChange?: (checked: boolean) => void; + onChange?: (checked: boolean) => void; + disabled?: boolean; + className?: string; + id?: string; + onClick?: (e: React.MouseEvent) => void; +} + +export function Checkbox({ + checked = false, + onCheckedChange, + onChange, + disabled = false, + className = '', + id, + onClick +}: CheckboxProps) { + const handleChange = (e: React.ChangeEvent) => { + const newChecked = e.target.checked; + onCheckedChange?.(newChecked); + onChange?.(newChecked); + }; + + return ( + + ); } \ No newline at end of file diff --git a/nkebao/src/components/ui/dialog.tsx b/nkebao/src/components/ui/dialog.tsx index e73f67d1..c90f07ab 100644 --- a/nkebao/src/components/ui/dialog.tsx +++ b/nkebao/src/components/ui/dialog.tsx @@ -27,7 +27,7 @@ export function Dialog({ open, onOpenChange, children }: DialogProps) { className="fixed inset-0 bg-black bg-opacity-50" onClick={() => onOpenChange(false)} /> -
+
{children}
@@ -41,7 +41,7 @@ interface DialogContentProps { export function DialogContent({ children, className = '' }: DialogContentProps) { return ( -
+
{children}
); diff --git a/nkebao/src/pages/workspace/moments-sync/new.tsx b/nkebao/src/pages/workspace/moments-sync/new.tsx index ba99a21a..e96bbae8 100644 --- a/nkebao/src/pages/workspace/moments-sync/new.tsx +++ b/nkebao/src/pages/workspace/moments-sync/new.tsx @@ -8,6 +8,8 @@ import { createMomentsSyncTask } from '@/api/momentsSync'; import { ChevronLeft, Clock, Plus, Minus, Search } from 'lucide-react'; import Layout from '@/components/Layout'; import BottomNav from '@/components/BottomNav'; +import { DeviceSelectionDialog } from '@/components/DeviceSelectionDialog'; +import { ContentLibrarySelectionDialog } from '@/components/ContentLibrarySelectionDialog'; // 步骤指示器组件 interface StepIndicatorProps { @@ -23,6 +25,17 @@ function StepIndicator({ currentStep }: StepIndicatorProps) { return (
+ {/* 背景连线 */} +
+
1 ? `${((currentStep - 1) / (steps.length - 1)) * 100}%` : '0%' + }} + /> +
+ + {/* 步骤圆圈 */} {steps.map((step) => (
= step.id ? "bg-blue-600 text-white shadow-sm" - : "bg-white border border-gray-200 text-gray-400" + : "bg-white border-2 border-gray-200 text-gray-400" }`} > {step.id} @@ -42,12 +55,6 @@ function StepIndicator({ currentStep }: StepIndicatorProps) {
{step.subtitle}
))} -
-
-
); } @@ -59,8 +66,12 @@ interface BasicSettingsProps { startTime: string; endTime: string; syncCount: number; + interval: number; accountType: "business" | "personal"; enabled: boolean; + contentTypes: ('text' | 'image' | 'video')[]; + targetTags: string[]; + filterKeywords: string[]; }; onChange: (data: Partial) => void; onNext: () => void; @@ -112,23 +123,47 @@ function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps) { 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" + className="h-12 w-12 rounded-xl bg-white border-gray-200 text-xl font-bold" > - + - {formData.syncCount} 条朋友圈
+
+
同步时间间隔
+
+ + {formData.interval} + + 分钟 +
+
+
账号类型
@@ -161,6 +196,32 @@ function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps) {
+
+
内容类型
+
+ {(['text', 'image', 'video'] as const).map((type) => ( +
+ +
+ ))} +
+
+
是否启用 ) => { @@ -230,13 +297,13 @@ export default function NewMomentsSyncTask() { devices: formData.selectedDevices, contentLib: formData.selectedLibraries.join(','), syncMode: formData.accountType === 'business' ? 'auto' : 'manual', - interval: 30, + interval: formData.interval, maxSync: formData.syncCount, startTime: formData.startTime, endTime: formData.endTime, - targetTags: [], - contentTypes: ['text', 'image', 'video'], - filterKeywords: [], + targetTags: formData.targetTags, + contentTypes: formData.contentTypes, + filterKeywords: formData.filterKeywords, friends: [], }); toast({ title: '创建成功' }); @@ -272,72 +339,100 @@ export default function NewMomentsSyncTask() { )} - {currentStep === 2 && ( -
-
- - { - // TODO: 打开设备选择弹窗 - toast({ title: '设备选择功能开发中' }); - }} - readOnly - /> -
- - {formData.selectedDevices.length > 0 && ( -
已选设备:{formData.selectedDevices.length} 个
- )} - -
- - -
+ {currentStep === 2 && ( +
+
+ + setDeviceDialogOpen(true)} + readOnly + />
- )} - {currentStep === 3 && ( -
-
- - { - // TODO: 打开内容库选择弹窗 - toast({ title: '内容库选择功能开发中' }); - }} - readOnly - /> + {formData.selectedDevices.length > 0 && ( +
+ 已选设备:{formData.selectedDevices.length} 个 +
+ {formData.selectedDevices.map(id => { + // 这里可以根据实际API获取设备名称 + return `设备 ${id}`; + }).join(', ')} +
+ )} - {formData.selectedLibraries.length > 0 && ( -
已选内容库:{formData.selectedLibraries.join(', ')}
- )} - -
- - -
+
+ +
- )} + + { + handleUpdateFormData({ selectedDevices: devices }); + }} + /> +
+ )} + + {currentStep === 3 && ( +
+
+ + setContentLibraryDialogOpen(true)} + readOnly + /> +
+ + {formData.selectedLibraries.length > 0 && ( +
+ 已选内容库:{formData.selectedLibraries.length} 个 +
+ {formData.selectedLibraries.map(id => { + // 这里可以根据实际API获取内容库名称 + return `朋友圈内容库${id}`; + }).join(', ')} +
+
+ )} + +
+ + +
+ + { + handleUpdateFormData({ selectedLibraries: libraries }); + }} + /> +
+ )}