From 5a981bf0386c31f7eabee962af87155fee74a623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AE=B8=E6=B0=B8=E5=B9=B3?= Date: Sat, 5 Jul 2025 22:49:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AD=98=E5=82=A8=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/scripts/update-page-headers.js | 87 +++++++++ nkebao/src/api/utils.ts | 66 ++++++- nkebao/src/components/BackButton.tsx | 92 +++++++++ nkebao/src/components/PageHeader.tsx | 86 +++++++++ nkebao/src/components/ThrottledButton.tsx | 4 +- nkebao/src/hooks/useBackNavigation.ts | 182 ++++++++++++++++++ nkebao/src/pages/devices/DeviceDetail.tsx | 38 ++-- nkebao/src/pages/devices/Devices.tsx | 24 +-- nkebao/src/pages/plans/Plans.tsx | 30 +-- nkebao/src/pages/scenarios/ScenarioDetail.tsx | 27 +-- nkebao/src/pages/scenarios/Scenarios.tsx | 30 +-- .../wechat-accounts/WechatAccountDetail.tsx | 18 +- .../pages/wechat-accounts/WechatAccounts.tsx | 23 +-- 13 files changed, 593 insertions(+), 114 deletions(-) create mode 100644 nkebao/scripts/update-page-headers.js create mode 100644 nkebao/src/components/BackButton.tsx create mode 100644 nkebao/src/components/PageHeader.tsx create mode 100644 nkebao/src/hooks/useBackNavigation.ts diff --git a/nkebao/scripts/update-page-headers.js b/nkebao/scripts/update-page-headers.js new file mode 100644 index 00000000..1e299f28 --- /dev/null +++ b/nkebao/scripts/update-page-headers.js @@ -0,0 +1,87 @@ +const fs = require('fs'); +const path = require('path'); + +// 需要更新的页面文件列表 +const pagesToUpdate = [ + 'src/pages/scenarios/ScenarioDetail.tsx', + 'src/pages/scenarios/NewPlan.tsx', + 'src/pages/plans/Plans.tsx', + 'src/pages/plans/PlanDetail.tsx', + 'src/pages/orders/Orders.tsx', + 'src/pages/profile/Profile.tsx', + 'src/pages/content/Content.tsx', + 'src/pages/contact-import/ContactImport.tsx', + 'src/pages/traffic-pool/TrafficPool.tsx', + 'src/pages/workspace/Workspace.tsx' +]; + +// 更新规则 +const updateRules = [ + { + // 替换旧的header结构 + pattern: /
\s*
\s*
\s*]*onClick=\{\(\) => navigate\(-1\)\}[^>]*>\s*]*\/>\s*<\/button>\s*]*>([^<]*)<\/h1>\s*<\/div>\s*(?:]*>([\s\S]*?)<\/div>)?\s*<\/div>\s*<\/header>/g, + replacement: (match, title, rightContent) => { + const rightContentStr = rightContent ? `\n rightContent={\n ${rightContent.trim()}\n }` : ''; + return ``; + } + }, + { + // 替换简单的header结构 + pattern: /
\s*
\s*]*>([^<]*)<\/h1>\s*<\/div>\s*<\/header>/g, + replacement: (match, title) => { + return ``; + } + }, + { + // 添加PageHeader导入 + pattern: /import React[^;]+;/, + replacement: (match) => { + return `${match}\nimport PageHeader from '@/components/PageHeader';`; + } + } +]; + +function updateFile(filePath) { + try { + const fullPath = path.join(process.cwd(), filePath); + if (!fs.existsSync(fullPath)) { + console.log(`文件不存在: ${filePath}`); + return; + } + + let content = fs.readFileSync(fullPath, 'utf8'); + let updated = false; + + // 应用更新规则 + updateRules.forEach(rule => { + const newContent = content.replace(rule.pattern, rule.replacement); + if (newContent !== content) { + content = newContent; + updated = true; + } + }); + + if (updated) { + fs.writeFileSync(fullPath, content, 'utf8'); + console.log(`✅ 已更新: ${filePath}`); + } else { + console.log(`⏭️ 无需更新: ${filePath}`); + } + } catch (error) { + console.error(`❌ 更新失败: ${filePath}`, error.message); + } +} + +// 执行批量更新 +console.log('🚀 开始批量更新页面Header...\n'); + +pagesToUpdate.forEach(filePath => { + updateFile(filePath); +}); + +console.log('\n✨ 批量更新完成!'); +console.log('\n📝 注意事项:'); +console.log('1. 请检查更新后的文件是否正确'); +console.log('2. 可能需要手动调整一些特殊的header结构'); +console.log('3. 确保所有页面都正确导入了PageHeader组件'); +console.log('4. 运行 npm run build 检查是否有编译错误'); \ No newline at end of file diff --git a/nkebao/src/api/utils.ts b/nkebao/src/api/utils.ts index f53ddfd9..585d6395 100644 --- a/nkebao/src/api/utils.ts +++ b/nkebao/src/api/utils.ts @@ -97,4 +97,68 @@ export const isTokenExpired = (): boolean => { console.error('解析token过期时间失败:', error); return true; } -}; \ No newline at end of file +}; + +// 请求去重器 +class RequestDeduplicator { + private pendingRequests = new Map>(); + + async deduplicate(key: string, requestFn: () => Promise): Promise { + if (this.pendingRequests.has(key)) { + return this.pendingRequests.get(key)!; + } + + const promise = requestFn(); + this.pendingRequests.set(key, promise); + + try { + const result = await promise; + return result; + } finally { + this.pendingRequests.delete(key); + } + } + + getPendingCount(): number { + return this.pendingRequests.size; + } + + clear(): void { + this.pendingRequests.clear(); + } +} + +// 请求取消管理器 +class RequestCancelManager { + private abortControllers = new Map(); + + createController(key: string): AbortController { + // 取消之前的请求 + this.cancelRequest(key); + + const controller = new AbortController(); + this.abortControllers.set(key, controller); + return controller; + } + + cancelRequest(key: string): void { + const controller = this.abortControllers.get(key); + if (controller) { + controller.abort(); + this.abortControllers.delete(key); + } + } + + cancelAllRequests(): void { + this.abortControllers.forEach(controller => controller.abort()); + this.abortControllers.clear(); + } + + getController(key: string): AbortController | undefined { + return this.abortControllers.get(key); + } +} + +// 导出单例实例 +export const requestDeduplicator = new RequestDeduplicator(); +export const requestCancelManager = new RequestCancelManager(); \ No newline at end of file diff --git a/nkebao/src/components/BackButton.tsx b/nkebao/src/components/BackButton.tsx new file mode 100644 index 00000000..9acfb8af --- /dev/null +++ b/nkebao/src/components/BackButton.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { ChevronLeft, ArrowLeft } from 'lucide-react'; + +interface BackButtonProps { + /** 返回按钮的样式变体 */ + variant?: 'icon' | 'button' | 'text'; + /** 自定义返回逻辑,如果不提供则使用navigate(-1) */ + onBack?: () => void; + /** 按钮文本,仅在button和text变体时使用 */ + text?: string; + /** 自定义CSS类名 */ + className?: string; + /** 图标大小 */ + iconSize?: number; + /** 是否显示图标 */ + showIcon?: boolean; + /** 自定义图标 */ + icon?: React.ReactNode; +} + +/** + * 通用返回上一页按钮组件 + * 使用React Router的navigate方法实现返回功能 + */ +export const BackButton: React.FC = ({ + variant = 'icon', + onBack, + text = '返回', + className = '', + iconSize = 20, + showIcon = true, + icon +}) => { + const navigate = useNavigate(); + + const handleBack = () => { + if (onBack) { + onBack(); + } else { + navigate(-1); + } + }; + + const defaultIcon = variant === 'icon' ? ( + + ) : ( + + ); + + const buttonIcon = icon || (showIcon ? defaultIcon : null); + + switch (variant) { + case 'icon': + return ( + + ); + + case 'button': + return ( + + ); + + case 'text': + return ( + + ); + + default: + return null; + } +}; + +export default BackButton; \ No newline at end of file diff --git a/nkebao/src/components/PageHeader.tsx b/nkebao/src/components/PageHeader.tsx new file mode 100644 index 00000000..a786fcb4 --- /dev/null +++ b/nkebao/src/components/PageHeader.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import BackButton from './BackButton'; +import { useSimpleBack } from '@/hooks/useBackNavigation'; + +interface PageHeaderProps { + /** 页面标题 */ + title: string; + /** 返回按钮文本 */ + backText?: string; + /** 自定义返回逻辑 */ + onBack?: () => void; + /** 默认返回路径 */ + defaultBackPath?: string; + /** 是否显示返回按钮 */ + showBack?: boolean; + /** 右侧扩展内容 */ + rightContent?: React.ReactNode; + /** 自定义CSS类名 */ + className?: string; + /** 标题样式类名 */ + titleClassName?: string; + /** 返回按钮样式变体 */ + backButtonVariant?: 'icon' | 'button' | 'text'; + /** 返回按钮自定义样式类名 */ + backButtonClassName?: string; + /** 是否固定在顶部 */ + fixed?: boolean; + /** 是否显示底部边框 */ + showBorder?: boolean; +} + +/** + * 通用页面Header组件 + * 支持返回按钮、标题和右侧扩展插槽 + */ +export const PageHeader: React.FC = ({ + title, + backText = '返回', + onBack, + defaultBackPath = '/', + showBack = true, + rightContent, + className = '', + titleClassName = '', + backButtonVariant = 'icon', + backButtonClassName = '', + fixed = true, + showBorder = true +}) => { + const { goBack } = useSimpleBack(defaultBackPath); + + const handleBack = onBack || goBack; + + const baseClasses = `bg-white ${showBorder ? 'border-b border-gray-200' : ''} ${fixed ? 'fixed top-0 left-0 right-0 z-20' : ''}`; + const headerClasses = `${baseClasses} ${className}`; + // 默认小号按钮样式 + const defaultBackBtnClass = 'text-sm px-2 py-1 h-8 min-h-0'; + + return ( +
+
+
+ {showBack && ( + + )} +

+ {title} +

+
+ + {rightContent && ( +
+ {rightContent} +
+ )} +
+
+ ); +}; + +export default PageHeader; \ No newline at end of file diff --git a/nkebao/src/components/ThrottledButton.tsx b/nkebao/src/components/ThrottledButton.tsx index ca08791d..09d545cf 100644 --- a/nkebao/src/components/ThrottledButton.tsx +++ b/nkebao/src/components/ThrottledButton.tsx @@ -151,7 +151,7 @@ const DebounceButtonContent: React.FC<{ errorText?: string; children: React.ReactNode; }> = ({ onClick, delay, loadingText, showLoadingText, disabled, className, errorText, children }) => { - const { debouncedRequest, loading, error } = useThrottledRequestWithError(onClick, delay); + const { throttledRequest, loading, error } = useThrottledRequestWithError(onClick, delay); const getButtonText = () => { return loading && showLoadingText ? loadingText : children; @@ -174,7 +174,7 @@ const DebounceButtonContent: React.FC<{ return (
+
); @@ -457,22 +458,15 @@ export default function DeviceDetail() { return (
{/* 固定header */} -
-
-
- -

设备详情

-
+ -
-
+ } + /> {/* 内容区域 */}
diff --git a/nkebao/src/pages/devices/Devices.tsx b/nkebao/src/pages/devices/Devices.tsx index 422ae292..1fe3a956 100644 --- a/nkebao/src/pages/devices/Devices.tsx +++ b/nkebao/src/pages/devices/Devices.tsx @@ -1,8 +1,9 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import { ChevronLeft, Plus, Search, RefreshCw, QrCode, Smartphone, Loader2, AlertTriangle, Trash2, X } from 'lucide-react'; +import { Plus, Search, RefreshCw, QrCode, Smartphone, Loader2, AlertTriangle, Trash2, X } from 'lucide-react'; import { devicesApi } from '@/api'; import { useToast } from '@/components/ui/toast'; +import PageHeader from '@/components/PageHeader'; // 设备接口 interface Device { @@ -375,18 +376,11 @@ export default function Devices() { return (
- {/* 固定header */} -
-
-
- -

设备管理

-
+ {/* 页面Header */} + 添加设备 -
-
+ } + /> {/* 内容区域 */}
diff --git a/nkebao/src/pages/plans/Plans.tsx b/nkebao/src/pages/plans/Plans.tsx index 1b8a7d34..eab6c75d 100644 --- a/nkebao/src/pages/plans/Plans.tsx +++ b/nkebao/src/pages/plans/Plans.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Plus, Calendar } from 'lucide-react'; +import PageHeader from '@/components/PageHeader'; interface Plan { id: string; @@ -98,11 +99,10 @@ export default function Plans() { if (loading) { return (
-
-
-

获客计划

-
-
+
加载中...
@@ -113,11 +113,10 @@ export default function Plans() { if (error) { return (
-
-
-

获客计划

-
-
+
{error}
); @@ -125,9 +124,10 @@ export default function Plans() { return (
-
-
-

获客计划

+ navigate('/scenarios/new')} className="flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm" @@ -135,8 +135,8 @@ export default function Plans() { 新建计划 -
-
+ } + />
{plans.length === 0 ? ( diff --git a/nkebao/src/pages/scenarios/ScenarioDetail.tsx b/nkebao/src/pages/scenarios/ScenarioDetail.tsx index 653570ac..fb6679a7 100644 --- a/nkebao/src/pages/scenarios/ScenarioDetail.tsx +++ b/nkebao/src/pages/scenarios/ScenarioDetail.tsx @@ -1,6 +1,8 @@ import React, { useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { ArrowLeft, Plus, Users, TrendingUp, Calendar } from 'lucide-react'; +import PageHeader from '@/components/PageHeader'; +import { useSimpleBack } from '@/hooks/useBackNavigation'; +import { Plus, Users, TrendingUp, Calendar } from 'lucide-react'; interface Plan { id: string; @@ -26,6 +28,7 @@ interface ScenarioData { export default function ScenarioDetail() { const { scenarioId } = useParams<{ scenarioId: string }>(); const navigate = useNavigate(); + const { goBack } = useSimpleBack('/scenarios'); const [scenario, setScenario] = useState(null); const [plans, setPlans] = useState([]); const [loading, setLoading] = useState(true); @@ -166,20 +169,10 @@ export default function ScenarioDetail() { return (
-
-
-
- -
- {scenario.name} -

{scenario.name}

-
-
+ navigate(`/scenarios/new?scenario=${scenarioId}`)} className="flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm" @@ -187,8 +180,8 @@ export default function ScenarioDetail() { 新建计划 -
-
+ } + />
{/* 场景描述 */} diff --git a/nkebao/src/pages/scenarios/Scenarios.tsx b/nkebao/src/pages/scenarios/Scenarios.tsx index 528213b7..8b15bea9 100644 --- a/nkebao/src/pages/scenarios/Scenarios.tsx +++ b/nkebao/src/pages/scenarios/Scenarios.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Plus, TrendingUp } from 'lucide-react'; +import PageHeader from '@/components/PageHeader'; interface Scenario { id: string; @@ -107,11 +108,10 @@ export default function Scenarios() { if (loading) { return (
-
-
-

场景获客

-
-
+
加载中...
@@ -122,11 +122,10 @@ export default function Scenarios() { if (error) { return (
-
-
-

场景获客

-
-
+
{error}
); @@ -134,9 +133,10 @@ export default function Scenarios() { return (
-
-
-

场景获客

+ navigate('/scenarios/new')} className="flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm" @@ -144,8 +144,8 @@ export default function Scenarios() { 新建计划 -
-
+ } + />
diff --git a/nkebao/src/pages/wechat-accounts/WechatAccountDetail.tsx b/nkebao/src/pages/wechat-accounts/WechatAccountDetail.tsx index 7b449083..14782d55 100644 --- a/nkebao/src/pages/wechat-accounts/WechatAccountDetail.tsx +++ b/nkebao/src/pages/wechat-accounts/WechatAccountDetail.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; +import PageHeader from '@/components/PageHeader'; import { - ChevronLeft, + ChevronLeft, Smartphone, Users, Star, @@ -407,17 +408,10 @@ export default function WechatAccountDetail() { return (
{/* 固定header */} -
-
- -

账号详情

-
-
+ {/* 内容区域 - 添加顶部内边距避免被固定header遮挡 */}
diff --git a/nkebao/src/pages/wechat-accounts/WechatAccounts.tsx b/nkebao/src/pages/wechat-accounts/WechatAccounts.tsx index c5fd8ab8..1cca5b83 100644 --- a/nkebao/src/pages/wechat-accounts/WechatAccounts.tsx +++ b/nkebao/src/pages/wechat-accounts/WechatAccounts.tsx @@ -1,9 +1,10 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import { ChevronLeft, Search, RefreshCw, ArrowRightLeft, AlertCircle, Loader2 } from 'lucide-react'; +import { Search, RefreshCw, ArrowRightLeft, AlertCircle, Loader2 } from 'lucide-react'; import { fetchWechatAccountList, transformWechatAccount } from '@/api/wechat-accounts'; import { useToast } from '@/components/ui/toast'; import { useWechatAccount } from '@/contexts/WechatAccountContext'; +import PageHeader from '@/components/PageHeader'; interface WechatAccount { id: string; @@ -150,24 +151,16 @@ export default function WechatAccounts() { return (
- {/* 固定header */} -
-
- -

微信号

-
-
+ {/* 内容区域 */} -
+
{/* 搜索和操作栏 */} -
+