feat: 存储了
This commit is contained in:
87
nkebao/scripts/update-page-headers.js
Normal file
87
nkebao/scripts/update-page-headers.js
Normal file
@@ -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: /<header className="[^"]*fixed[^"]*">\s*<div className="[^"]*">\s*<div className="[^"]*">\s*<button[^>]*onClick=\{\(\) => navigate\(-1\)\}[^>]*>\s*<ChevronLeft[^>]*\/>\s*<\/button>\s*<h1[^>]*>([^<]*)<\/h1>\s*<\/div>\s*(?:<div[^>]*>([\s\S]*?)<\/div>)?\s*<\/div>\s*<\/header>/g,
|
||||||
|
replacement: (match, title, rightContent) => {
|
||||||
|
const rightContentStr = rightContent ? `\n rightContent={\n ${rightContent.trim()}\n }` : '';
|
||||||
|
return `<PageHeader\n title="${title.trim()}"\n defaultBackPath="/"${rightContentStr}\n />`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 替换简单的header结构
|
||||||
|
pattern: /<header className="[^"]*">\s*<div className="[^"]*">\s*<h1[^>]*>([^<]*)<\/h1>\s*<\/div>\s*<\/header>/g,
|
||||||
|
replacement: (match, title) => {
|
||||||
|
return `<PageHeader\n title="${title.trim()}"\n showBack={false}\n />`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 添加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 检查是否有编译错误');
|
||||||
@@ -97,4 +97,68 @@ export const isTokenExpired = (): boolean => {
|
|||||||
console.error('解析token过期时间失败:', error);
|
console.error('解析token过期时间失败:', error);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 请求去重器
|
||||||
|
class RequestDeduplicator {
|
||||||
|
private pendingRequests = new Map<string, Promise<any>>();
|
||||||
|
|
||||||
|
async deduplicate<T>(key: string, requestFn: () => Promise<T>): Promise<T> {
|
||||||
|
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<string, AbortController>();
|
||||||
|
|
||||||
|
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();
|
||||||
92
nkebao/src/components/BackButton.tsx
Normal file
92
nkebao/src/components/BackButton.tsx
Normal file
@@ -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<BackButtonProps> = ({
|
||||||
|
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' ? (
|
||||||
|
<ChevronLeft className={`h-${iconSize} w-${iconSize}`} />
|
||||||
|
) : (
|
||||||
|
<ArrowLeft className={`h-${iconSize} w-${iconSize}`} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const buttonIcon = icon || (showIcon ? defaultIcon : null);
|
||||||
|
|
||||||
|
switch (variant) {
|
||||||
|
case 'icon':
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleBack}
|
||||||
|
className={`p-2 hover:bg-gray-100 rounded-lg transition-colors ${className}`}
|
||||||
|
title="返回上一页"
|
||||||
|
>
|
||||||
|
{buttonIcon}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'button':
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleBack}
|
||||||
|
className={`flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors ${className}`}
|
||||||
|
>
|
||||||
|
{buttonIcon}
|
||||||
|
{text}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'text':
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleBack}
|
||||||
|
className={`flex items-center gap-2 text-blue-600 hover:text-blue-700 transition-colors ${className}`}
|
||||||
|
>
|
||||||
|
{buttonIcon}
|
||||||
|
{text}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BackButton;
|
||||||
86
nkebao/src/components/PageHeader.tsx
Normal file
86
nkebao/src/components/PageHeader.tsx
Normal file
@@ -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<PageHeaderProps> = ({
|
||||||
|
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 (
|
||||||
|
<header className={headerClasses}>
|
||||||
|
<div className="flex items-center justify-between px-4 py-3">
|
||||||
|
<div className="flex items-center ">
|
||||||
|
{showBack && (
|
||||||
|
<BackButton
|
||||||
|
variant={backButtonVariant}
|
||||||
|
text={backText}
|
||||||
|
onBack={handleBack}
|
||||||
|
className={`${defaultBackBtnClass} ${backButtonClassName}`.trim()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<h1 className={`text-lg font-semibold ${titleClassName}`}>
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rightContent && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{rightContent}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageHeader;
|
||||||
@@ -151,7 +151,7 @@ const DebounceButtonContent: React.FC<{
|
|||||||
errorText?: string;
|
errorText?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}> = ({ onClick, delay, loadingText, showLoadingText, disabled, className, errorText, children }) => {
|
}> = ({ onClick, delay, loadingText, showLoadingText, disabled, className, errorText, children }) => {
|
||||||
const { debouncedRequest, loading, error } = useThrottledRequestWithError(onClick, delay);
|
const { throttledRequest, loading, error } = useThrottledRequestWithError(onClick, delay);
|
||||||
|
|
||||||
const getButtonText = () => {
|
const getButtonText = () => {
|
||||||
return loading && showLoadingText ? loadingText : children;
|
return loading && showLoadingText ? loadingText : children;
|
||||||
@@ -174,7 +174,7 @@ const DebounceButtonContent: React.FC<{
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={debouncedRequest}
|
onClick={throttledRequest}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
className={getButtonClassName()}
|
className={getButtonClassName()}
|
||||||
>
|
>
|
||||||
|
|||||||
182
nkebao/src/hooks/useBackNavigation.ts
Normal file
182
nkebao/src/hooks/useBackNavigation.ts
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import { useCallback, useRef, useEffect } from 'react';
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface BackNavigationOptions {
|
||||||
|
/** 默认返回路径,当没有历史记录时使用 */
|
||||||
|
defaultPath?: string;
|
||||||
|
/** 是否在组件卸载时保存当前路径到历史记录 */
|
||||||
|
saveOnUnmount?: boolean;
|
||||||
|
/** 最大历史记录数量 */
|
||||||
|
maxHistoryLength?: number;
|
||||||
|
/** 自定义返回逻辑 */
|
||||||
|
customBackLogic?: (history: string[], currentPath: string) => string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BackNavigationReturn {
|
||||||
|
/** 返回上一页 */
|
||||||
|
goBack: () => void;
|
||||||
|
/** 返回到指定路径 */
|
||||||
|
goTo: (path: string) => void;
|
||||||
|
/** 返回到首页 */
|
||||||
|
goHome: () => void;
|
||||||
|
/** 检查是否可以返回 */
|
||||||
|
canGoBack: () => boolean;
|
||||||
|
/** 获取历史记录 */
|
||||||
|
getHistory: () => string[];
|
||||||
|
/** 清除历史记录 */
|
||||||
|
clearHistory: () => void;
|
||||||
|
/** 当前路径 */
|
||||||
|
currentPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 高级返回导航Hook
|
||||||
|
* 提供更智能的返回逻辑和历史记录管理
|
||||||
|
*/
|
||||||
|
export const useBackNavigation = (options: BackNavigationOptions = {}): BackNavigationReturn => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const historyRef = useRef<string[]>([]);
|
||||||
|
const {
|
||||||
|
defaultPath = '/',
|
||||||
|
saveOnUnmount = true,
|
||||||
|
maxHistoryLength = 10,
|
||||||
|
customBackLogic
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// 保存路径到历史记录
|
||||||
|
const saveToHistory = useCallback((path: string) => {
|
||||||
|
const history = historyRef.current;
|
||||||
|
|
||||||
|
// 如果路径已经存在,移除它
|
||||||
|
const filteredHistory = history.filter(p => p !== path);
|
||||||
|
|
||||||
|
// 添加到开头
|
||||||
|
filteredHistory.unshift(path);
|
||||||
|
|
||||||
|
// 限制历史记录长度
|
||||||
|
if (filteredHistory.length > maxHistoryLength) {
|
||||||
|
filteredHistory.splice(maxHistoryLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
historyRef.current = filteredHistory;
|
||||||
|
}, [maxHistoryLength]);
|
||||||
|
|
||||||
|
// 获取历史记录
|
||||||
|
const getHistory = useCallback(() => {
|
||||||
|
return [...historyRef.current];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 清除历史记录
|
||||||
|
const clearHistory = useCallback(() => {
|
||||||
|
historyRef.current = [];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 检查是否可以返回
|
||||||
|
const canGoBack = useCallback(() => {
|
||||||
|
return historyRef.current.length > 1 || window.history.length > 1;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = useCallback(() => {
|
||||||
|
const history = getHistory();
|
||||||
|
|
||||||
|
// 如果有自定义返回逻辑,使用它
|
||||||
|
if (customBackLogic) {
|
||||||
|
const targetPath = customBackLogic(history, location.pathname);
|
||||||
|
if (targetPath) {
|
||||||
|
navigate(targetPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有历史记录,返回到上一个路径
|
||||||
|
if (history.length > 1) {
|
||||||
|
const previousPath = history[1]; // 当前路径在索引0,上一个在索引1
|
||||||
|
navigate(previousPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果浏览器历史记录有上一页,使用浏览器返回
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
navigate(-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后回退到默认路径
|
||||||
|
navigate(defaultPath);
|
||||||
|
}, [navigate, location.pathname, getHistory, customBackLogic, defaultPath]);
|
||||||
|
|
||||||
|
// 返回到指定路径
|
||||||
|
const goTo = useCallback((path: string) => {
|
||||||
|
navigate(path);
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
// 返回到首页
|
||||||
|
const goHome = useCallback(() => {
|
||||||
|
navigate('/');
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
// 组件挂载时保存当前路径
|
||||||
|
useEffect(() => {
|
||||||
|
saveToHistory(location.pathname);
|
||||||
|
}, [location.pathname, saveToHistory]);
|
||||||
|
|
||||||
|
// 组件卸载时保存路径(可选)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!saveOnUnmount) return;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
saveToHistory(location.pathname);
|
||||||
|
};
|
||||||
|
}, [location.pathname, saveToHistory, saveOnUnmount]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
goBack,
|
||||||
|
goTo,
|
||||||
|
goHome,
|
||||||
|
canGoBack,
|
||||||
|
getHistory,
|
||||||
|
clearHistory,
|
||||||
|
currentPath: location.pathname
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简化的返回Hook,只提供基本的返回功能
|
||||||
|
*/
|
||||||
|
export const useSimpleBack = (defaultPath: string = '/') => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const goBack = useCallback(() => {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
navigate(-1);
|
||||||
|
} else {
|
||||||
|
navigate(defaultPath);
|
||||||
|
}
|
||||||
|
}, [navigate, defaultPath]);
|
||||||
|
|
||||||
|
return { goBack };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带确认的返回Hook
|
||||||
|
*/
|
||||||
|
export const useConfirmBack = (
|
||||||
|
message: string = '确定要离开当前页面吗?',
|
||||||
|
defaultPath: string = '/'
|
||||||
|
) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const goBack = useCallback(() => {
|
||||||
|
if (window.confirm(message)) {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
navigate(-1);
|
||||||
|
} else {
|
||||||
|
navigate(defaultPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [navigate, message, defaultPath]);
|
||||||
|
|
||||||
|
return { goBack };
|
||||||
|
};
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import PageHeader from '@/components/PageHeader';
|
||||||
|
import BackButton from '@/components/BackButton';
|
||||||
|
import { useSimpleBack } from '@/hooks/useBackNavigation';
|
||||||
import { ChevronLeft, Smartphone, Battery, Wifi, MessageCircle, Users, Settings, History, RefreshCw, Loader2 } from 'lucide-react';
|
import { ChevronLeft, Smartphone, Battery, Wifi, MessageCircle, Users, Settings, History, RefreshCw, Loader2 } from 'lucide-react';
|
||||||
import { devicesApi, fetchDeviceDetail, fetchDeviceRelatedAccounts, fetchDeviceHandleLogs, updateDeviceTaskConfig } from '@/api/devices';
|
import { devicesApi, fetchDeviceDetail, fetchDeviceRelatedAccounts, fetchDeviceHandleLogs, updateDeviceTaskConfig } from '@/api/devices';
|
||||||
import { useToast } from '@/components/ui/toast';
|
import { useToast } from '@/components/ui/toast';
|
||||||
@@ -52,7 +55,7 @@ interface HandleLog {
|
|||||||
|
|
||||||
export default function DeviceDetail() {
|
export default function DeviceDetail() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const { goBack } = useSimpleBack('/devices');
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [device, setDevice] = useState<Device | null>(null);
|
const [device, setDevice] = useState<Device | null>(null);
|
||||||
const [activeTab, setActiveTab] = useState("info");
|
const [activeTab, setActiveTab] = useState("info");
|
||||||
@@ -442,13 +445,11 @@ export default function DeviceDetail() {
|
|||||||
<div className="text-sm text-gray-500 text-center">
|
<div className="text-sm text-gray-500 text-center">
|
||||||
无法加载ID为 "{id}" 的设备信息,请检查设备是否存在。
|
无法加载ID为 "{id}" 的设备信息,请检查设备是否存在。
|
||||||
</div>
|
</div>
|
||||||
<button
|
<BackButton
|
||||||
onClick={() => navigate(-1)}
|
variant="button"
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
|
text="返回上一页"
|
||||||
>
|
onBack={goBack}
|
||||||
<ChevronLeft className="h-4 w-4" />
|
/>
|
||||||
返回上一页
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -457,22 +458,15 @@ export default function DeviceDetail() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
{/* 固定header */}
|
{/* 固定header */}
|
||||||
<header className="fixed top-0 left-0 right-0 z-20 bg-white border-b border-gray-200">
|
<PageHeader
|
||||||
<div className="flex items-center justify-between px-4 py-3">
|
title="设备详情"
|
||||||
<div className="flex items-center gap-3">
|
defaultBackPath="/devices"
|
||||||
<button
|
rightContent={
|
||||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
<h1 className="text-lg font-semibold">设备详情</h1>
|
|
||||||
</div>
|
|
||||||
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors">
|
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors">
|
||||||
<Settings className="h-5 w-5" />
|
<Settings className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
}
|
||||||
</header>
|
/>
|
||||||
|
|
||||||
{/* 内容区域 */}
|
{/* 内容区域 */}
|
||||||
<div className="pt-16 pb-20">
|
<div className="pt-16 pb-20">
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 { devicesApi } from '@/api';
|
||||||
import { useToast } from '@/components/ui/toast';
|
import { useToast } from '@/components/ui/toast';
|
||||||
|
import PageHeader from '@/components/PageHeader';
|
||||||
|
|
||||||
// 设备接口
|
// 设备接口
|
||||||
interface Device {
|
interface Device {
|
||||||
@@ -375,18 +376,11 @@ export default function Devices() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
{/* 固定header */}
|
{/* 页面Header */}
|
||||||
<header className="fixed top-0 left-0 right-0 z-20 bg-white border-b border-gray-200">
|
<PageHeader
|
||||||
<div className="flex items-center justify-between px-4 py-3">
|
title="设备管理"
|
||||||
<div className="flex items-center gap-3">
|
defaultBackPath="/"
|
||||||
<button
|
rightContent={
|
||||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
<h1 className="text-lg font-semibold">设备管理</h1>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
|
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
|
||||||
onClick={handleOpenAddDeviceModal}
|
onClick={handleOpenAddDeviceModal}
|
||||||
@@ -394,8 +388,8 @@ export default function Devices() {
|
|||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
添加设备
|
添加设备
|
||||||
</button>
|
</button>
|
||||||
</div>
|
}
|
||||||
</header>
|
/>
|
||||||
|
|
||||||
{/* 内容区域 */}
|
{/* 内容区域 */}
|
||||||
<div className="pt-16 pb-20">
|
<div className="pt-16 pb-20">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Plus, Calendar } from 'lucide-react';
|
import { Plus, Calendar } from 'lucide-react';
|
||||||
|
import PageHeader from '@/components/PageHeader';
|
||||||
|
|
||||||
interface Plan {
|
interface Plan {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -98,11 +99,10 @@ export default function Plans() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50">
|
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50">
|
||||||
<header className="sticky top-0 z-10 bg-white border-b">
|
<PageHeader
|
||||||
<div className="flex items-center justify-between p-4">
|
title="获客计划"
|
||||||
<h1 className="text-xl font-semibold">获客计划</h1>
|
showBack={false}
|
||||||
</div>
|
/>
|
||||||
</header>
|
|
||||||
<div className="flex justify-center items-center h-40">
|
<div className="flex justify-center items-center h-40">
|
||||||
<div className="text-gray-500">加载中...</div>
|
<div className="text-gray-500">加载中...</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,11 +113,10 @@ export default function Plans() {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50">
|
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50">
|
||||||
<header className="sticky top-0 z-10 bg-white border-b">
|
<PageHeader
|
||||||
<div className="flex items-center justify-between p-4">
|
title="获客计划"
|
||||||
<h1 className="text-xl font-semibold">获客计划</h1>
|
showBack={false}
|
||||||
</div>
|
/>
|
||||||
</header>
|
|
||||||
<div className="text-red-500 text-center py-8">{error}</div>
|
<div className="text-red-500 text-center py-8">{error}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -125,9 +124,10 @@ export default function Plans() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50">
|
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50">
|
||||||
<header className="sticky top-0 z-10 bg-white border-b">
|
<PageHeader
|
||||||
<div className="flex items-center justify-between p-4">
|
title="获客计划"
|
||||||
<h1 className="text-xl font-semibold">获客计划</h1>
|
showBack={false}
|
||||||
|
rightContent={
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/scenarios/new')}
|
onClick={() => 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"
|
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() {
|
|||||||
<Plus className="h-4 w-4 mr-1" />
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
新建计划
|
新建计划
|
||||||
</button>
|
</button>
|
||||||
</div>
|
}
|
||||||
</header>
|
/>
|
||||||
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{plans.length === 0 ? (
|
{plans.length === 0 ? (
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
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 {
|
interface Plan {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -26,6 +28,7 @@ interface ScenarioData {
|
|||||||
export default function ScenarioDetail() {
|
export default function ScenarioDetail() {
|
||||||
const { scenarioId } = useParams<{ scenarioId: string }>();
|
const { scenarioId } = useParams<{ scenarioId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { goBack } = useSimpleBack('/scenarios');
|
||||||
const [scenario, setScenario] = useState<ScenarioData | null>(null);
|
const [scenario, setScenario] = useState<ScenarioData | null>(null);
|
||||||
const [plans, setPlans] = useState<Plan[]>([]);
|
const [plans, setPlans] = useState<Plan[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -166,20 +169,10 @@ export default function ScenarioDetail() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50">
|
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50">
|
||||||
<header className="sticky top-0 z-10 bg-white border-b">
|
<PageHeader
|
||||||
<div className="flex items-center justify-between p-4">
|
title={scenario.name}
|
||||||
<div className="flex items-center">
|
defaultBackPath="/scenarios"
|
||||||
<button
|
rightContent={
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
className="mr-3 p-1 hover:bg-gray-100 rounded"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<img src={scenario.image} alt={scenario.name} className="w-8 h-8 mr-3 rounded" />
|
|
||||||
<h1 className="text-xl font-semibold">{scenario.name}</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/scenarios/new?scenario=${scenarioId}`)}
|
onClick={() => 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"
|
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() {
|
|||||||
<Plus className="h-4 w-4 mr-1" />
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
新建计划
|
新建计划
|
||||||
</button>
|
</button>
|
||||||
</div>
|
}
|
||||||
</header>
|
/>
|
||||||
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{/* 场景描述 */}
|
{/* 场景描述 */}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Plus, TrendingUp } from 'lucide-react';
|
import { Plus, TrendingUp } from 'lucide-react';
|
||||||
|
import PageHeader from '@/components/PageHeader';
|
||||||
|
|
||||||
interface Scenario {
|
interface Scenario {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -107,11 +108,10 @@ export default function Scenarios() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50">
|
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50">
|
||||||
<header className="sticky top-0 z-10 bg-white border-b">
|
<PageHeader
|
||||||
<div className="flex items-center justify-between p-4">
|
title="场景获客"
|
||||||
<h1 className="text-xl font-semibold">场景获客</h1>
|
showBack={false}
|
||||||
</div>
|
/>
|
||||||
</header>
|
|
||||||
<div className="flex justify-center items-center h-40">
|
<div className="flex justify-center items-center h-40">
|
||||||
<div className="text-gray-500">加载中...</div>
|
<div className="text-gray-500">加载中...</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,11 +122,10 @@ export default function Scenarios() {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50">
|
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50">
|
||||||
<header className="sticky top-0 z-10 bg-white border-b">
|
<PageHeader
|
||||||
<div className="flex items-center justify-between p-4">
|
title="场景获客"
|
||||||
<h1 className="text-xl font-semibold">场景获客</h1>
|
showBack={false}
|
||||||
</div>
|
/>
|
||||||
</header>
|
|
||||||
<div className="text-red-500 text-center py-8">{error}</div>
|
<div className="text-red-500 text-center py-8">{error}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -134,9 +133,10 @@ export default function Scenarios() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50">
|
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50">
|
||||||
<header className="sticky top-0 z-10 bg-white border-b">
|
<PageHeader
|
||||||
<div className="flex items-center justify-between p-4">
|
title="场景获客"
|
||||||
<h1 className="text-xl font-semibold">场景获客</h1>
|
showBack={false}
|
||||||
|
rightContent={
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/scenarios/new')}
|
onClick={() => 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"
|
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() {
|
|||||||
<Plus className="h-4 w-4 mr-1" />
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
新建计划
|
新建计划
|
||||||
</button>
|
</button>
|
||||||
</div>
|
}
|
||||||
</header>
|
/>
|
||||||
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import PageHeader from '@/components/PageHeader';
|
||||||
import {
|
import {
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
Smartphone,
|
Smartphone,
|
||||||
Users,
|
Users,
|
||||||
Star,
|
Star,
|
||||||
@@ -407,17 +408,10 @@ export default function WechatAccountDetail() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex-1 bg-gradient-to-b from-blue-50 to-white min-h-screen overflow-x-hidden">
|
<div className="flex-1 bg-gradient-to-b from-blue-50 to-white min-h-screen overflow-x-hidden">
|
||||||
{/* 固定header */}
|
{/* 固定header */}
|
||||||
<header className="fixed top-0 left-0 right-0 z-50 bg-white/95 backdrop-blur-md border-b border-gray-200 shadow-sm">
|
<PageHeader
|
||||||
<div className="flex items-center p-4">
|
title="账号详情"
|
||||||
<button
|
defaultBackPath="/wechat-accounts"
|
||||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
/>
|
||||||
onClick={handleBack}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
<h1 className="ml-2 text-lg font-medium">账号详情</h1>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* 内容区域 - 添加顶部内边距避免被固定header遮挡 */}
|
{/* 内容区域 - 添加顶部内边距避免被固定header遮挡 */}
|
||||||
<div className="pt-20 p-4 space-y-4">
|
<div className="pt-20 p-4 space-y-4">
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 { fetchWechatAccountList, transformWechatAccount } from '@/api/wechat-accounts';
|
||||||
import { useToast } from '@/components/ui/toast';
|
import { useToast } from '@/components/ui/toast';
|
||||||
import { useWechatAccount } from '@/contexts/WechatAccountContext';
|
import { useWechatAccount } from '@/contexts/WechatAccountContext';
|
||||||
|
import PageHeader from '@/components/PageHeader';
|
||||||
|
|
||||||
interface WechatAccount {
|
interface WechatAccount {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -150,24 +151,16 @@ export default function WechatAccounts() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
{/* 固定header */}
|
<PageHeader
|
||||||
<header className="fixed top-0 left-0 right-0 z-20 bg-white border-b border-gray-200">
|
title="微信号"
|
||||||
<div className="flex items-center px-4 py-3">
|
defaultBackPath="/"
|
||||||
<button
|
/>
|
||||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
<h1 className="ml-2 text-lg font-semibold">微信号</h1>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* 内容区域 */}
|
{/* 内容区域 */}
|
||||||
<div className="pt-16 pb-20">
|
<div className="pt-14 pb-20">
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{/* 搜索和操作栏 */}
|
{/* 搜索和操作栏 */}
|
||||||
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100 mb-4">
|
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100 mb-4 ">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Search className="w-4 h-4 absolute left-3 top-3 text-gray-400" />
|
<Search className="w-4 h-4 absolute left-3 top-3 text-gray-400" />
|
||||||
|
|||||||
Reference in New Issue
Block a user