diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..a598c05 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/public/static/qq-group.png b/frontend/public/static/qq-group.png new file mode 100644 index 0000000..f6a10e2 Binary files /dev/null and b/frontend/public/static/qq-group.png differ diff --git a/frontend/public/static/wechat-group.png b/frontend/public/static/wechat-group.png new file mode 100644 index 0000000..483f330 Binary files /dev/null and b/frontend/public/static/wechat-group.png differ diff --git a/frontend/public/static/wechat-group1.png b/frontend/public/static/wechat-group1.png new file mode 100644 index 0000000..88238b1 Binary files /dev/null and b/frontend/public/static/wechat-group1.png differ diff --git a/frontend/public/static/wechat.png b/frontend/public/static/wechat.png new file mode 100644 index 0000000..f7f78ae Binary files /dev/null and b/frontend/public/static/wechat.png differ diff --git a/frontend/public/static/xianyu-group.png b/frontend/public/static/xianyu-group.png new file mode 100644 index 0000000..d8edcca Binary files /dev/null and b/frontend/public/static/xianyu-group.png differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c8bdbc9..aa03c3b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import React, { useEffect, useState, useRef } from 'react' import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' import { useAuthStore } from '@/store/authStore' import { MainLayout } from '@/components/layout/MainLayout' @@ -25,24 +25,34 @@ import { verifyToken } from '@/api/auth' // Protected route wrapper function ProtectedRoute({ children }: { children: React.ReactNode }) { - const { isAuthenticated, setAuth, clearAuth } = useAuthStore() + const { isAuthenticated, setAuth, clearAuth, token: storeToken, _hasHydrated } = useAuthStore() const [isChecking, setIsChecking] = useState(true) const [isValid, setIsValid] = useState(false) + const hasCheckedRef = useRef(false) useEffect(() => { + // 等待 zustand persist 完成 hydration + if (!_hasHydrated) return + + // 防止重复检查 + if (hasCheckedRef.current) return + const checkAuth = async () => { - const token = localStorage.getItem('auth_token') + // 优先使用 store 中的 token,其次是 localStorage + const token = storeToken || localStorage.getItem('auth_token') if (!token) { setIsChecking(false) setIsValid(false) + hasCheckedRef.current = true return } - // 如果已经认证,直接通过 - if (isAuthenticated) { + // 如果 store 中已经认证且有用户信息,直接通过 + if (isAuthenticated && storeToken) { setIsChecking(false) setIsValid(true) + hasCheckedRef.current = true return } @@ -65,14 +75,15 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) { setIsValid(false) } finally { setIsChecking(false) + hasCheckedRef.current = true } } checkAuth() - }, [isAuthenticated, setAuth, clearAuth]) + }, [_hasHydrated, isAuthenticated, storeToken, setAuth, clearAuth]) - // 显示加载状态 - if (isChecking) { + // 等待 hydration 或检查完成 + if (!_hasHydrated || isChecking) { return (
diff --git a/frontend/src/api/accounts.ts b/frontend/src/api/accounts.ts index 0d6c761..494867f 100644 --- a/frontend/src/api/accounts.ts +++ b/frontend/src/api/accounts.ts @@ -1,24 +1,73 @@ import { get, post, put, del } from '@/utils/request' import type { Account, AccountDetail, ApiResponse } from '@/types' -// 获取账号列表 -export const getAccounts = (): Promise => { - return get('/cookies') +// 获取账号列表(返回账号ID数组) +export const getAccounts = async (): Promise => { + const ids: string[] = await get('/cookies') + // 后端返回的是账号ID数组,转换为Account对象数组 + return ids.map(id => ({ + id, + cookie: '', + enabled: true, + use_ai_reply: false, + use_default_reply: false, + auto_confirm: false + })) } // 获取账号详情列表 -export const getAccountDetails = (): Promise => { - return get('/cookies/details') +export const getAccountDetails = async (): Promise => { + interface BackendAccountDetail { + id: string + value: string + enabled: boolean + auto_confirm: boolean + remark?: string + pause_duration?: number + } + const data = await get('/cookies/details') + // 后端返回 value 字段,前端使用 cookie 字段 + return data.map((item) => ({ + id: item.id, + cookie: item.value, + enabled: item.enabled, + auto_confirm: item.auto_confirm, + note: item.remark, + pause_duration: item.pause_duration, + use_ai_reply: false, + use_default_reply: false, + })) } // 添加账号 export const addAccount = (data: { id: string; cookie: string }): Promise => { - return post('/cookies', data) + // 后端需要 id 和 value 字段 + return post('/cookies', { id: data.id, value: data.cookie }) } -// 更新账号 -export const updateAccount = (id: string, data: Partial): Promise => { - return put(`/cookies/${id}`, data) +// 更新账号 Cookie 值 +export const updateAccountCookie = (id: string, value: string): Promise => { + return put(`/cookies/${id}`, { id, value }) +} + +// 更新账号启用/禁用状态 +export const updateAccountStatus = (id: string, enabled: boolean): Promise => { + return put(`/cookies/${id}/status`, { enabled }) +} + +// 更新账号备注 +export const updateAccountRemark = (id: string, remark: string): Promise => { + return put(`/cookies/${id}/remark`, { remark }) +} + +// 更新账号自动确认设置 +export const updateAccountAutoConfirm = (id: string, autoConfirm: boolean): Promise => { + return put(`/cookies/${id}/auto-confirm`, { auto_confirm: autoConfirm }) +} + +// 更新账号暂停时间 +export const updateAccountPauseDuration = (id: string, pauseDuration: number): Promise => { + return put(`/cookies/${id}/pause-duration`, { pause_duration: pauseDuration }) } // 删除账号 diff --git a/frontend/src/api/admin.ts b/frontend/src/api/admin.ts index 915b9d0..d0000a6 100644 --- a/frontend/src/api/admin.ts +++ b/frontend/src/api/admin.ts @@ -1,22 +1,33 @@ -import { get, post, put, del } from '@/utils/request' +import { get, post, del } from '@/utils/request' import type { ApiResponse, User } from '@/types' // ========== 用户管理 ========== // 获取用户列表 -export const getUsers = (): Promise<{ success: boolean; data?: User[] }> => { - return get('/admin/users') +export const getUsers = async (): Promise<{ success: boolean; data?: User[] }> => { + const result = await get<{ users: Array<{ + id: number + username: string + email?: string + is_admin: boolean + cookie_count?: number + card_count?: number + }> }>('/admin/users') + // 后端返回 { users: [...] } 格式,转换字段名 + const users: User[] = (result.users || []).map(u => ({ + user_id: u.id, + username: u.username, + email: u.email, + is_admin: u.is_admin, + })) + return { success: true, data: users } } -// 添加用户 -export const addUser = (data: { username: string; password: string; email?: string; is_admin?: boolean }): Promise => { - return post('/admin/users', data) -} +// TODO: 后端暂未实现 POST /admin/users 接口 +// export const addUser = ... -// 更新用户 -export const updateUser = (userId: number, data: Partial): Promise => { - return put(`/admin/users/${userId}`, data) -} +// TODO: 后端暂未实现 PUT /admin/users/{userId} 接口 +// export const updateUser = ... // 删除用户 export const deleteUser = (userId: number): Promise => { @@ -34,12 +45,21 @@ export interface SystemLog { } // 获取系统日志 -export const getSystemLogs = (params?: { page?: number; limit?: number; level?: string }): Promise<{ success: boolean; data?: SystemLog[]; total?: number }> => { +export const getSystemLogs = async (params?: { page?: number; limit?: number; level?: string }): Promise<{ success: boolean; data?: SystemLog[]; total?: number }> => { const query = new URLSearchParams() if (params?.page) query.set('page', String(params.page)) if (params?.limit) query.set('limit', String(params.limit)) if (params?.level) query.set('level', params.level) - return get(`/admin/logs?${query.toString()}`) + const result = await get<{ logs?: string[]; total?: number }>(`/admin/logs?${query.toString()}`) + // 后端返回 { logs: [...] } 格式,转换为 SystemLog 数组 + const logs: SystemLog[] = (result.logs || []).map((log, index) => ({ + id: String(index), + level: log.includes('ERROR') ? 'error' : log.includes('WARNING') ? 'warning' : 'info', + message: log, + module: 'system', + created_at: new Date().toISOString(), + })) + return { success: true, data: logs, total: result.total } } // 清空系统日志 @@ -58,12 +78,32 @@ export interface RiskLog { } // 获取风控日志 -export const getRiskLogs = (params?: { page?: number; limit?: number; cookie_id?: string }): Promise<{ success: boolean; data?: RiskLog[]; total?: number }> => { +export const getRiskLogs = async (params?: { page?: number; limit?: number; cookie_id?: string }): Promise<{ success: boolean; data?: RiskLog[]; total?: number }> => { const query = new URLSearchParams() if (params?.page) query.set('page', String(params.page)) if (params?.limit) query.set('limit', String(params.limit)) if (params?.cookie_id) query.set('cookie_id', params.cookie_id) - return get(`/admin/risk-control-logs?${query.toString()}`) + const result = await get<{ success: boolean; data?: Array<{ + id: number + cookie_id: string + event_type: string + event_description: string + processing_result: string + processing_status: string + error_message: string | null + created_at: string + updated_at: string + cookie_name: string + }>; total?: number }>(`/admin/risk-control-logs?${query.toString()}`) + // 转换后端格式为前端格式 + const logs: RiskLog[] = (result.data || []).map(item => ({ + id: String(item.id), + cookie_id: item.cookie_id || item.cookie_name, + risk_type: item.event_type, + message: item.event_description || item.processing_result, + created_at: item.created_at, + })) + return { success: true, data: logs, total: result.total } } // 清空风控日志 diff --git a/frontend/src/api/cards.ts b/frontend/src/api/cards.ts index 444a8c2..d01567d 100644 --- a/frontend/src/api/cards.ts +++ b/frontend/src/api/cards.ts @@ -2,9 +2,12 @@ import { get, post, del } from '@/utils/request' import type { ApiResponse, Card } from '@/types' // 获取卡券列表 -export const getCards = (accountId?: string): Promise<{ success: boolean; data?: Card[] }> => { +export const getCards = async (accountId?: string): Promise<{ success: boolean; data?: Card[] }> => { const url = accountId ? `/cards?cookie_id=${accountId}` : '/cards' - return get(url) + const result = await get(url) + // 后端可能返回数组或 { cards: [...] } 格式 + const data = Array.isArray(result) ? result : (result.cards || []) + return { success: true, data } } // 获取账号的卡券列表 diff --git a/frontend/src/api/delivery.ts b/frontend/src/api/delivery.ts index 2e0714e..280320f 100644 --- a/frontend/src/api/delivery.ts +++ b/frontend/src/api/delivery.ts @@ -2,24 +2,26 @@ import { get, post, put, del } from '@/utils/request' import type { ApiResponse, DeliveryRule } from '@/types' // 获取发货规则列表 -export const getDeliveryRules = (accountId?: string): Promise<{ success: boolean; data?: DeliveryRule[] }> => { - const url = accountId ? `/api/delivery-rules?cookie_id=${accountId}` : '/api/delivery-rules' - return get(url) +export const getDeliveryRules = async (): Promise<{ success: boolean; data?: DeliveryRule[] }> => { + const result = await get('/delivery-rules') + // 后端可能返回数组或 { rules: [...] } 格式 + const data = Array.isArray(result) ? result : (result.rules || []) + return { success: true, data } } // 添加发货规则 export const addDeliveryRule = (data: Partial): Promise => { - return post('/api/delivery-rules', data) + return post('/delivery-rules', data) } // 更新发货规则 export const updateDeliveryRule = (ruleId: string, data: Partial): Promise => { - return put(`/api/delivery-rules/${ruleId}`, data) + return put(`/delivery-rules/${ruleId}`, data) } // 删除发货规则 export const deleteDeliveryRule = (ruleId: string): Promise => { - return del(`/api/delivery-rules/${ruleId}`) + return del(`/delivery-rules/${ruleId}`) } // 获取账号的发货规则 diff --git a/frontend/src/api/items.ts b/frontend/src/api/items.ts index 922a3e9..0dfe6c4 100644 --- a/frontend/src/api/items.ts +++ b/frontend/src/api/items.ts @@ -2,9 +2,12 @@ import { get, post, put, del } from '@/utils/request' import type { Item, ItemReply, ApiResponse } from '@/types' // 获取商品列表 -export const getItems = (cookieId?: string): Promise<{ success: boolean; data: Item[] }> => { - const params = cookieId ? `?cookie_id=${cookieId}` : '' - return get(`/items${params}`) +export const getItems = async (cookieId?: string): Promise<{ success: boolean; data: Item[] }> => { + const url = cookieId ? `/items/cookie/${cookieId}` : '/items' + const result = await get<{ items?: Item[] } | Item[]>(url) + // 后端返回 { items: [...] } 或直接返回数组 + const items = Array.isArray(result) ? result : (result.items || []) + return { success: true, data: items } } // 删除商品 diff --git a/frontend/src/api/notifications.ts b/frontend/src/api/notifications.ts index f7062c2..3b93431 100644 --- a/frontend/src/api/notifications.ts +++ b/frontend/src/api/notifications.ts @@ -5,47 +5,61 @@ import type { ApiResponse, NotificationChannel, MessageNotification } from '@/ty // 获取通知渠道列表 export const getNotificationChannels = (): Promise<{ success: boolean; data?: NotificationChannel[] }> => { - return get('/api/notification-channels') + return get('/notification-channels') } // 添加通知渠道 export const addNotificationChannel = (data: Partial): Promise => { - return post('/api/notification-channels', data) + return post('/notification-channels', data) } // 更新通知渠道 export const updateNotificationChannel = (channelId: string, data: Partial): Promise => { - return put(`/api/notification-channels/${channelId}`, data) + return put(`/notification-channels/${channelId}`, data) } // 删除通知渠道 export const deleteNotificationChannel = (channelId: string): Promise => { - return del(`/api/notification-channels/${channelId}`) + return del(`/notification-channels/${channelId}`) } // 测试通知渠道 export const testNotificationChannel = (channelId: string): Promise => { - return post(`/api/notification-channels/${channelId}/test`) + return post(`/notification-channels/${channelId}/test`) } // ========== 消息通知 ========== -// 获取消息通知列表 -export const getMessageNotifications = (): Promise<{ success: boolean; data?: MessageNotification[] }> => { - return get('/api/message-notifications') +// 获取所有消息通知配置 +// 后端返回格式: { cookie_id: { channel_id: { enabled: boolean, channel_name: string } } } +export const getMessageNotifications = async (): Promise<{ success: boolean; data?: MessageNotification[] }> => { + const result = await get>>('/message-notifications') + // 将嵌套对象转换为数组 + const notifications: MessageNotification[] = [] + for (const [cookieId, channels] of Object.entries(result || {})) { + for (const [channelId, config] of Object.entries(channels || {})) { + notifications.push({ + cookie_id: cookieId, + channel_id: Number(channelId), + channel_name: config.channel_name, + enabled: config.enabled, + }) + } + } + return { success: true, data: notifications } } -// 添加消息通知 -export const addMessageNotification = (data: Partial): Promise => { - return post('/api/message-notifications', data) -} - -// 更新消息通知 -export const updateMessageNotification = (notificationId: string, data: Partial): Promise => { - return put(`/api/message-notifications/${notificationId}`, data) +// 设置消息通知 - 后端接口需要 cookie_id 作为路径参数 +export const setMessageNotification = (cookieId: string, channelId: number, enabled: boolean): Promise => { + return post(`/message-notifications/${cookieId}`, { channel_id: channelId, enabled }) } // 删除消息通知 export const deleteMessageNotification = (notificationId: string): Promise => { - return del(`/api/message-notifications/${notificationId}`) + return del(`/message-notifications/${notificationId}`) +} + +// 删除账号的所有消息通知 +export const deleteAccountNotifications = (cookieId: string): Promise => { + return del(`/message-notifications/account/${cookieId}`) } diff --git a/frontend/src/api/search.ts b/frontend/src/api/search.ts index 3d55650..16b0c27 100644 --- a/frontend/src/api/search.ts +++ b/frontend/src/api/search.ts @@ -1,7 +1,35 @@ import { post } from '@/utils/request' -import type { Item } from '@/types' + +// 搜索结果项类型 +export interface SearchResultItem { + item_id: string + title: string + price: string + seller_name?: string + item_url?: string + main_image?: string + publish_time?: string + tags?: string[] + area?: string + want_count?: number +} // 搜索商品 -export const searchItems = (keyword: string, accountId?: string): Promise<{ success: boolean; data?: Item[] }> => { - return post('/api/items/search', { keyword, cookie_id: accountId }) +export const searchItems = async ( + keyword: string, + page: number = 1, + pageSize: number = 20 +): Promise<{ success: boolean; data: SearchResultItem[]; total?: number; error?: string }> => { + const result = await post<{ + success: boolean + data?: SearchResultItem[] + total?: number + error?: string + }>('/items/search', { keyword, page, page_size: pageSize }) + return { + success: result.success, + data: result.data || [], + total: result.total, + error: result.error + } } diff --git a/frontend/src/api/settings.ts b/frontend/src/api/settings.ts index 64609b9..7e80f6e 100644 --- a/frontend/src/api/settings.ts +++ b/frontend/src/api/settings.ts @@ -1,18 +1,34 @@ -import { get, post, put } from '@/utils/request' +import { get, put } from '@/utils/request' import type { ApiResponse, SystemSettings } from '@/types' // 获取系统设置 -export const getSystemSettings = (): Promise<{ success: boolean; data?: SystemSettings }> => { - return get('/system-settings') +export const getSystemSettings = async (): Promise<{ success: boolean; data?: SystemSettings }> => { + const data = await get>('/system-settings') + // 将字符串 'true'/'false' 转换为布尔值 + const booleanFields = ['registration_enabled', 'show_login_info', 'login_captcha_enabled', 'show_default_login'] + const converted: SystemSettings = {} + for (const [key, value] of Object.entries(data)) { + if (booleanFields.includes(key)) { + converted[key] = value === true || value === 'true' + } else { + converted[key] = value + } + } + return { success: true, data: converted } } // 更新系统设置 -export const updateSystemSettings = (data: Partial): Promise => { - // 逐个更新设置项 - const promises = Object.entries(data).map(([key, value]) => - put(`/system-settings/${key}`, { value }) - ) - return Promise.all(promises).then(() => ({ success: true, message: '设置已保存' })) +export const updateSystemSettings = async (data: Partial): Promise => { + // 逐个更新设置项,确保 value 是字符串 + const promises = Object.entries(data).map(([key, value]) => { + // 将布尔值和数字转换为字符串 + const stringValue = typeof value === 'boolean' ? (value ? 'true' : 'false') + : typeof value === 'number' ? String(value) + : value + return put(`/system-settings/${key}`, { value: stringValue }) + }) + await Promise.all(promises) + return { success: true, message: '设置已保存' } } // 获取 AI 设置 @@ -25,9 +41,11 @@ export const updateAISettings = (data: Record): Promise => { - return post('/ai-reply-test/default') +// TODO: 测试 AI 连接需要指定 cookie_id,后端接口为 POST /ai-reply-test/{cookie_id} +// 系统设置页面的测试按钮暂时无法使用,需要先选择账号 +export const testAIConnection = async (): Promise => { + // 后端需要有效的 cookie_id,这里返回提示信息 + return { success: false, message: 'AI 测试需要先选择一个账号,请在账号管理页面的 AI 设置中测试' } } // 获取邮件设置 @@ -43,7 +61,8 @@ export const updateEmailSettings = (data: Record): Promise ({ success: true, message: '设置已保存' })) } -// 测试邮件发送 -export const testEmailSend = (email: string): Promise => { - return post('/send-verification-code', { email, type: 'test' }) +// TODO: 测试邮件发送功能需要后端支持 type: 'test' 参数 +// 当前后端的 /send-verification-code 接口只支持 'register' 和 'login' 类型 +export const testEmailSend = async (_email: string): Promise => { + return { success: false, message: '邮件测试功能暂未实现,请检查 SMTP 配置后直接保存' } } diff --git a/frontend/src/components/common/Loading.tsx b/frontend/src/components/common/Loading.tsx index 684cbd0..7a52358 100644 --- a/frontend/src/components/common/Loading.tsx +++ b/frontend/src/components/common/Loading.tsx @@ -21,17 +21,17 @@ export function Loading({ size = 'md', fullScreen = false, text }: LoadingProps) animate={{ rotate: 360 }} transition={{ duration: 1, repeat: Infinity, ease: 'linear' }} > - + {text && ( -

{text}

+

{text}

)}
) if (fullScreen) { return ( -
+
{content}
) diff --git a/frontend/src/components/common/Select.tsx b/frontend/src/components/common/Select.tsx new file mode 100644 index 0000000..195ecb0 --- /dev/null +++ b/frontend/src/components/common/Select.tsx @@ -0,0 +1,160 @@ +import { useState, useRef, useEffect } from 'react' +import { ChevronDown, Check } from 'lucide-react' +import { cn } from '@/utils/cn' + +export interface SelectOption { + value: string + label: string + disabled?: boolean +} + +interface SelectProps { + value: string + onChange: (value: string) => void + options: SelectOption[] + placeholder?: string + disabled?: boolean + className?: string +} + +export function Select({ + value, + onChange, + options, + placeholder = '请选择', + disabled = false, + className, +}: SelectProps) { + const [isOpen, setIsOpen] = useState(false) + const selectRef = useRef(null) + + const selectedOption = options.find((opt) => opt.value === value) + + // 点击外部关闭 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (selectRef.current && !selectRef.current.contains(event.target as Node)) { + setIsOpen(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) + + // 键盘导航 + const handleKeyDown = (e: React.KeyboardEvent) => { + if (disabled) return + + switch (e.key) { + case 'Enter': + case ' ': + e.preventDefault() + setIsOpen(!isOpen) + break + case 'Escape': + setIsOpen(false) + break + case 'ArrowDown': + e.preventDefault() + if (!isOpen) { + setIsOpen(true) + } else { + const currentIndex = options.findIndex((opt) => opt.value === value) + const nextIndex = Math.min(currentIndex + 1, options.length - 1) + onChange(options[nextIndex].value) + } + break + case 'ArrowUp': + e.preventDefault() + if (isOpen) { + const currentIndex = options.findIndex((opt) => opt.value === value) + const prevIndex = Math.max(currentIndex - 1, 0) + onChange(options[prevIndex].value) + } + break + } + } + + return ( +
+ {/* 触发器 */} + + + {/* 下拉菜单 */} + {isOpen && ( +
+ {options.length === 0 ? ( +
+ 暂无选项 +
+ ) : ( + options.map((option) => ( + + )) + )} +
+ )} +
+ ) +} diff --git a/frontend/src/components/layout/MainLayout.tsx b/frontend/src/components/layout/MainLayout.tsx index 8aa976e..703cbd9 100644 --- a/frontend/src/components/layout/MainLayout.tsx +++ b/frontend/src/components/layout/MainLayout.tsx @@ -11,11 +11,14 @@ export function MainLayout() { {/* Main content area */}
- {/* Top navbar */} - - - {/* Tabs bar */} - + {/* Fixed header area */} +
+ {/* Top navbar */} + + + {/* Tabs bar */} + +
{/* Page content */}
diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 8023a2c..58fe17c 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -74,8 +74,9 @@ export function Sidebar() { className={({ isActive }) => cn( 'flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-all duration-150', - 'text-slate-400 hover:text-white hover:bg-white/10', - isActive && 'bg-blue-600 text-white shadow-sm' + isActive + ? 'bg-blue-600 text-white dark:text-white hover:text-white hover:bg-blue-700 shadow-sm' + : 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-white/10' ) } > @@ -106,24 +107,25 @@ export function Sidebar() { }} className={cn( 'fixed top-0 left-0 h-screen w-56 z-50', - 'bg-[#001529] text-white', + 'bg-white dark:bg-[#001529]', 'flex flex-col', 'transition-transform duration-200 ease-out', + 'border-r border-slate-200 dark:border-slate-700', 'lg:translate-x-0', !sidebarMobileOpen && '-translate-x-full lg:translate-x-0' )} > {/* Header */} -
+
- 闲鱼管理系统 + 闲鱼管理系统
@@ -139,7 +141,7 @@ export function Sidebar() { {user?.is_admin && ( <>
-

+

管理员

diff --git a/frontend/src/components/layout/TabsBar.tsx b/frontend/src/components/layout/TabsBar.tsx index 77d04cd..e0fa751 100644 --- a/frontend/src/components/layout/TabsBar.tsx +++ b/frontend/src/components/layout/TabsBar.tsx @@ -23,7 +23,7 @@ const routeTitles: Record = { '/dashboard': '仪表盘', '/accounts': '账号管理', '/items': '商品管理', - '/keywords': '关键词管理', + '/keywords': '自动回复', '/item-replies': '指定商品回复', '/orders': '订单管理', '/cards': '卡券管理', diff --git a/frontend/src/pages/about/About.tsx b/frontend/src/pages/about/About.tsx index 233c6c0..ac5969d 100644 --- a/frontend/src/pages/about/About.tsx +++ b/frontend/src/pages/about/About.tsx @@ -1,128 +1,217 @@ - -import { MessageSquare, Github, Globe, Users, Heart } from 'lucide-react' +import { useState } from 'react' +import { MessageSquare, Github, Heart, Code, MessageCircle, Users, UserCheck, Bot, Truck, Bell, BarChart3, X } from 'lucide-react' export function About() { + const [previewImage, setPreviewImage] = useState(null) + return ( -
+
{/* Header */} -
-
+
- +
- +

闲鱼自动回复管理系统 - - +

+

智能管理您的闲鱼店铺,提升客服效率 - +

+
+ + {/* Contact Groups */} +
+
+
+

+ + 微信群 +

+
+
+
setPreviewImage('/static/wechat-group.png')} + > + 微信群二维码 { + (e.target as HTMLImageElement).style.display = 'none' + const parent = (e.target as HTMLImageElement).parentElement + if (parent) { + parent.innerHTML = '

二维码未配置

' + } + }} + /> +
+

扫码加入微信技术交流群

+
+
+
+
+

+ + QQ群 +

+
+
+
setPreviewImage('/static/qq-group.png')} + > + QQ群二维码 { + (e.target as HTMLImageElement).style.display = 'none' + const parent = (e.target as HTMLImageElement).parentElement + if (parent) { + parent.innerHTML = '

二维码未配置

' + } + }} + /> +
+

扫码加入QQ技术交流群

+
+
{/* Features */} -
-

主要功能

-
- {[ - { title: '多账号管理', desc: '支持同时管理多个闲鱼账号' }, - { title: '智能自动回复', desc: '基于关键词的智能消息回复' }, - { title: 'AI 助手', desc: '接入 AI 模型,智能处理复杂问题' }, - { title: '自动发货', desc: '订单自动发货,支持卡密发货' }, - { title: '消息通知', desc: '多渠道消息推送通知' }, - { title: '数据统计', desc: '订单、商品数据统计分析' }, - ].map((feature, index) => ( -
-
-
-

{feature.title}

-

{feature.desc}

+
+
+

主要功能

+
+
+
+ {[ + { title: '多账号管理', desc: '同时管理多个账号', icon: UserCheck, color: 'text-blue-500' }, + { title: '智能回复', desc: '关键词自动回复', icon: MessageSquare, color: 'text-green-500' }, + { title: 'AI 助手', desc: '智能处理复杂问题', icon: Bot, color: 'text-purple-500' }, + { title: '自动发货', desc: '支持卡密发货', icon: Truck, color: 'text-orange-500' }, + { title: '消息通知', desc: '多渠道推送', icon: Bell, color: 'text-pink-500' }, + { title: '数据统计', desc: '订单商品分析', icon: BarChart3, color: 'text-cyan-500' }, + ].map((feature, index) => ( +
+
+ +
+
+

{feature.title}

+

{feature.desc}

+
-
- ))} + ))} +
+
+
+ + {/* Contributors */} + {/* Links */} -
-

相关链接

-
- - - GitHub - - - - 官方网站 - - - - 交流群 - +
+
+

相关链接

+
+
{/* Footer */} -
+

- Made with by Open Source Community + Made with by Open Source Community

-

- 赞助商:划算云服务器{' '} +

+ 赞助商: - www.hsykj.com + 划算云服务器

+ + {/* 图片预览弹窗 */} + {previewImage && ( +
setPreviewImage(null)} + > +
+ + 预览 e.stopPropagation()} + /> +
+
+ )}
) } diff --git a/frontend/src/pages/accounts/Accounts.tsx b/frontend/src/pages/accounts/Accounts.tsx index 14c0afb..53f78fc 100644 --- a/frontend/src/pages/accounts/Accounts.tsx +++ b/frontend/src/pages/accounts/Accounts.tsx @@ -1,8 +1,9 @@ import { useState, useEffect, useRef, useCallback } from 'react' import type { FormEvent } from 'react' -import { Plus, RefreshCw, QrCode, Key, Edit2, Trash2, Power, PowerOff, X, Loader2 } from 'lucide-react' -import { getAccountDetails, deleteAccount, updateAccount, addAccount, generateQRLogin, checkQRLoginStatus, passwordLogin } from '@/api/accounts' +import { Plus, RefreshCw, QrCode, Key, Edit2, Trash2, Power, PowerOff, X, Loader2, Copy } from 'lucide-react' +import { getAccountDetails, deleteAccount, updateAccountCookie, updateAccountStatus, updateAccountRemark, addAccount, generateQRLogin, checkQRLoginStatus, passwordLogin } from '@/api/accounts' import { useUIStore } from '@/store/uiStore' +import { useAuthStore } from '@/store/authStore' import { PageLoading } from '@/components/common/Loading' import type { AccountDetail } from '@/types' @@ -10,6 +11,7 @@ type ModalType = 'qrcode' | 'password' | 'manual' | 'edit' | null export function Accounts() { const { addToast } = useUIStore() + const { isAuthenticated, token, _hasHydrated } = useAuthStore() const [loading, setLoading] = useState(true) const [accounts, setAccounts] = useState([]) const [activeModal, setActiveModal] = useState(null) @@ -34,12 +36,11 @@ export function Accounts() { // 编辑账号状态 const [editingAccount, setEditingAccount] = useState(null) const [editNote, setEditNote] = useState('') - const [editUseAI, setEditUseAI] = useState(false) - const [editUseDefault, setEditUseDefault] = useState(false) const [editCookie, setEditCookie] = useState('') const [editSaving, setEditSaving] = useState(false) const loadAccounts = async () => { + if (!_hasHydrated || !isAuthenticated || !token) return try { setLoading(true) const data = await getAccountDetails() @@ -52,8 +53,9 @@ export function Accounts() { } useEffect(() => { + if (!_hasHydrated || !isAuthenticated || !token) return loadAccounts() - }, []) + }, [_hasHydrated, isAuthenticated, token]) // 清理扫码检查定时器 const clearQrCheck = useCallback(() => { @@ -111,14 +113,22 @@ export function Accounts() { case 'scanned': setQrStatus('scanned') break + case 'processing': + // 正在处理中,显示已扫描状态 + setQrStatus('scanned') + break case 'success': + case 'already_processed': + // 登录成功或已处理完成 setQrStatus('success') clearQrCheck() addToast({ type: 'success', message: result.account_info?.is_new_account ? `新账号 ${result.account_info.account_id} 添加成功` - : `账号 ${result.account_info?.account_id} 登录成功`, + : result.account_info?.account_id + ? `账号 ${result.account_info.account_id} 登录成功` + : '账号登录成功', }) setTimeout(() => { closeModal() @@ -227,7 +237,7 @@ export function Accounts() { const handleToggleEnabled = async (account: AccountDetail) => { try { - await updateAccount(account.id, { enabled: !account.enabled }) + await updateAccountStatus(account.id, !account.enabled) addToast({ type: 'success', message: account.enabled ? '账号已禁用' : '账号已启用' }) loadAccounts() } catch { @@ -250,8 +260,6 @@ export function Accounts() { const openEditModal = (account: AccountDetail) => { setEditingAccount(account) setEditNote(account.note || '') - setEditUseAI(account.use_ai_reply || false) - setEditUseDefault(account.use_default_reply || false) setEditCookie(account.cookie || '') setActiveModal('edit') } @@ -262,12 +270,20 @@ export function Accounts() { setEditSaving(true) try { - await updateAccount(editingAccount.id, { - note: editNote.trim() || undefined, - use_ai_reply: editUseAI, - use_default_reply: editUseDefault, - cookie: editCookie.trim() || undefined, - }) + // 分别调用不同的 API 更新不同字段 + const promises: Promise[] = [] + + // 更新备注 + if (editNote.trim() !== (editingAccount.note || '')) { + promises.push(updateAccountRemark(editingAccount.id, editNote.trim())) + } + + // 更新 Cookie 值 + if (editCookie.trim() && editCookie.trim() !== editingAccount.cookie) { + promises.push(updateAccountCookie(editingAccount.id, editCookie.trim())) + } + + await Promise.all(promises) addToast({ type: 'success', message: '账号信息已更新' }) closeModal() loadAccounts() @@ -314,45 +330,45 @@ export function Accounts() { {/* 扫码登录 */} {/* 账号密码登录 */} {/* 手动输入 */}
@@ -383,7 +399,7 @@ export function Accounts() {
-

暂无账号,请添加新账号

+

暂无账号,请添加新账号

@@ -392,9 +408,23 @@ export function Accounts() { {account.id} - - {account.cookie?.substring(0, 25)}... - +
+ + {account.cookie ? account.cookie.substring(0, 30) + '...' : '无'} + + {account.cookie && ( + + )} +
@@ -416,31 +446,33 @@ export function Accounts() { {account.note || '-'} -
+
@@ -466,14 +498,14 @@ export function Accounts() { {qrStatus === 'loading' && (
-

正在生成二维码...

+

正在生成二维码...

)} {qrStatus === 'ready' && (
登录二维码 -

请使用闲鱼APP扫描二维码

-

二维码有效期约5分钟

+

请使用闲鱼APP扫描二维码

+

二维码有效期约5分钟

)} {qrStatus === 'scanned' && ( @@ -495,7 +527,7 @@ export function Accounts() { )} {qrStatus === 'expired' && (
-

二维码已过期

+

二维码已过期

@@ -547,12 +579,12 @@ export function Accounts() { placeholder="请输入密码" />
-
@@ -672,30 +704,17 @@ export function Accounts() {