use indexedDB manager proxySwitch

This commit is contained in:
go0p
2025-03-04 16:07:14 +08:00
parent 1e25561fae
commit 6f82842c92
23 changed files with 1542 additions and 753 deletions
@@ -0,0 +1,166 @@
import React from 'react';
import { Modal, Button, Space, Descriptions, Tabs, Card, Typography, message } from 'antd';
import { ProxyLog } from '@/types/proxy';
const { Text, Paragraph } = Typography;
interface LogDetailProps {
log: ProxyLog | null;
onClose: () => void;
}
export const LogDetail: React.FC<LogDetailProps> = ({ log, onClose }) => {
const renderHttpRequest = (log: ProxyLog) => {
if (!log) return '';
// 构建请求头
const headers = Object.entries(log.requestHeaders || {})
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
// 构建完整的 HTTP 请求
return `${log.method || 'GET'} ${log.url} ${log.protocol || 'HTTP/1.1'}
${headers}
${log.requestBody || ''}`;
};
const renderHttpResponse = (log: ProxyLog) => {
if (!log || !log.responseHeaders) return '';
// 构建响应头
const headers = Object.entries(log.responseHeaders)
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
// 构建完整的 HTTP 响应
return `HTTP/1.1 ${log.status === 'success' ? '200 OK' : '500 Error'}
${headers}
${log.responseBody || ''}`;
};
const handleCopyRaw = () => {
if (!log) return;
navigator.clipboard.writeText(renderHttpRequest(log))
.then(() => message.success('已复制到剪贴板'))
.catch(() => message.error('复制失败'));
};
const formatProxyInfo = (log: ProxyLog) => {
if (!log) return '';
return `${log.proxyName}${log.host ? ` - ${log.host}:${log.port}` : ''}`;
};
return (
<Modal
title="请求详情"
open={!!log}
onCancel={onClose}
width={800}
footer={[
<Button key="copy" onClick={handleCopyRaw}>
复制原始数据
</Button>,
<Button key="close" onClick={onClose}>
关闭
</Button>
]}
>
{log && (
<Space direction="vertical" style={{ width: '100%' }}>
<Descriptions bordered column={2}>
<Descriptions.Item label="请求时间">
{new Date(log.timestamp).toLocaleString()}
</Descriptions.Item>
<Descriptions.Item label="代理服务器">
{formatProxyInfo(log)}
</Descriptions.Item>
<Descriptions.Item label="状态">
<Text type={log.status === 'success' ? 'success' : 'danger'}>
{log.status === 'success' ? '成功' : '失败'}
</Text>
</Descriptions.Item>
<Descriptions.Item label="响应时间">
{log.timing?.duration}ms
</Descriptions.Item>
{log.errorMessage && (
<Descriptions.Item label="错误信息" span={2}>
<Text type="danger">{log.errorMessage}</Text>
</Descriptions.Item>
)}
</Descriptions>
<Tabs
items={[
{
key: 'request',
label: '请求数据',
children: (
<Card size="small">
<pre style={{
background: '#f5f5f5',
padding: 16,
borderRadius: 4,
maxHeight: 400,
overflow: 'auto',
margin: 0
}}>
{renderHttpRequest(log)}
</pre>
</Card>
)
},
{
key: 'response',
label: '响应数据',
children: (
<Card size="small">
<pre style={{
background: '#f5f5f5',
padding: 16,
borderRadius: 4,
maxHeight: 400,
overflow: 'auto',
margin: 0
}}>
{renderHttpResponse(log)}
</pre>
</Card>
)
},
{
key: 'timing',
label: '性能数据',
children: (
<Card size="small">
<Descriptions bordered>
<Descriptions.Item label="开始时间">
{new Date(log.timing?.startTime || 0).toLocaleString()}
</Descriptions.Item>
<Descriptions.Item label="结束时间">
{new Date(log.timing?.endTime || 0).toLocaleString()}
</Descriptions.Item>
<Descriptions.Item label="总耗时">
{log.timing?.duration}ms
</Descriptions.Item>
<Descriptions.Item label="IP地址">
{log.ip || '-'}
</Descriptions.Item>
<Descriptions.Item label="协议">
{log.protocol || '-'}
</Descriptions.Item>
<Descriptions.Item label="缓存">
{log.fromCache ? '是' : '否'}
</Descriptions.Item>
</Descriptions>
</Card>
)
}
]}
/>
</Space>
)}
</Modal>
);
};
@@ -0,0 +1,131 @@
import React, { useState } from 'react';
import { Table, Button, Space } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { DeleteOutlined, FilterFilled } from '@ant-design/icons';
import { ProxyLog } from '@/types/proxy';
import { LogDetail } from './LogDetail';
interface ProxyLogsProps {
logs: ProxyLog[];
onClearLogs: () => void;
}
export const ProxyLogs: React.FC<ProxyLogsProps> = ({
logs,
onClearLogs
}) => {
const [selectedLog, setSelectedLog] = useState<ProxyLog | null>(null);
const [resourceFilter, setResourceFilter] = useState<string[]>([]);
const columns: ColumnsType<ProxyLog> = [
{
title: '时间',
dataIndex: 'timestamp',
key: 'timestamp',
render: (timestamp: number) => new Date(timestamp).toLocaleString()
},
{
title: 'URL',
dataIndex: 'url',
key: 'url',
ellipsis: true
},
{
title: (
<Space>
类型
{resourceFilter.length > 0 && <FilterFilled style={{ color: '#f50' }} />}
</Space>
),
dataIndex: 'resourceType',
key: 'resourceType',
render: (type: string) => {
const typeMap: Record<string, string> = {
xhr: 'XHR',
fetch: 'Fetch',
script: 'JS',
stylesheet: 'CSS',
image: 'Image',
other: 'Other'
};
return typeMap[type] || 'Other';
},
filters: [
{ text: 'XHR', value: 'xhr' },
{ text: 'Fetch', value: 'fetch' },
{ text: 'JS', value: 'script' },
{ text: 'CSS', value: 'stylesheet' },
{ text: 'Image', value: 'image' },
{ text: 'Other', value: 'other' }
],
filterMode: 'menu' as const,
filtered: resourceFilter.length > 0,
onFilter: (value: string, record: ProxyLog) => record.resourceType === value
},
{
title: '使用代理',
dataIndex: 'proxyName',
key: 'proxyName'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<span style={{ color: status === 'success' ? '#52c41a' : '#ff4d4f' }}>
{status === 'success' ? '成功' : '失败'}
</span>
)
},
{
title: '错误信息',
dataIndex: 'errorMessage',
key: 'errorMessage',
ellipsis: true
}
];
const filteredLogs = logs.filter(log => {
if (resourceFilter.length === 0) return true;
return resourceFilter.includes(log.resourceType || 'other');
});
return (
<Space direction="vertical" style={{ width: '100%' }}>
<div style={{
marginBottom: 16,
display: 'flex',
justifyContent: 'flex-end'
}}>
<Button
danger
onClick={onClearLogs}
icon={<DeleteOutlined />}
>
清除日志
</Button>
</div>
<Table
dataSource={filteredLogs}
columns={columns}
onRow={(record) => ({
onClick: () => setSelectedLog(record),
style: { cursor: 'pointer' }
})}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `共 ${total} 条`,
pageSizeOptions: ['10', '20', '50', '100']
}}
rowKey="id"
/>
<LogDetail
log={selectedLog}
onClose={() => setSelectedLog(null)}
/>
</Space>
);
};
@@ -0,0 +1,165 @@
import React from 'react';
import { Card, Input, Space, Button, Select, InputNumber, Form, Table } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import { ProxyConfig } from '@/types/proxy';
interface ProxySettingsProps {
proxyConfigs: ProxyConfig[];
onAdd: () => void;
onChange: (configId: string, field: keyof ProxyConfig, value: any) => void;
onDelete: (configId: string) => void;
onApply: (configId: string) => Promise<void>;
onClear: (configId: string) => Promise<void>;
}
export const ProxySettings: React.FC<ProxySettingsProps> = ({
proxyConfigs,
onAdd,
onChange,
onDelete,
onApply,
onClear
}) => {
const columns: ColumnsType<ProxyConfig> = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
render: (text: string, record: ProxyConfig) => (
<Input
value={text}
onChange={e => onChange(record.id, 'name', e.target.value)}
disabled={record.id === 'direct'}
/>
)
},
{
title: '类型',
dataIndex: 'proxyType',
key: 'proxyType',
render: (text: string, record: ProxyConfig) => (
<Select
value={text}
onChange={value => onChange(record.id, 'proxyType', value)}
style={{ width: 120 }}
disabled={record.id === 'direct'}
options={[
{ label: '直接连接', value: 'direct' },
{ label: '代理服务器', value: 'fixed_server' },
{ label: 'PAC 脚本', value: 'pac_script' }
]}
/>
)
},
{
title: '协议',
dataIndex: 'scheme',
key: 'scheme',
render: (text: string, record: ProxyConfig) => (
record.proxyType === 'fixed_server' && (
<Select
value={text}
onChange={value => onChange(record.id, 'scheme', value)}
style={{ width: 100 }}
options={[
{ label: 'HTTP', value: 'http' },
{ label: 'HTTPS', value: 'https' },
{ label: 'SOCKS4', value: 'socks4' },
{ label: 'SOCKS5', value: 'socks5' }
]}
/>
)
)
},
{
title: '主机',
dataIndex: 'host',
key: 'host',
render: (text: string, record: ProxyConfig) => (
record.proxyType === 'fixed_server' && (
<Input
value={text}
onChange={e => onChange(record.id, 'host', e.target.value)}
placeholder="127.0.0.1"
/>
)
)
},
{
title: '端口',
dataIndex: 'port',
key: 'port',
render: (text: number, record: ProxyConfig) => (
record.proxyType === 'fixed_server' && (
<InputNumber
value={text}
onChange={value => onChange(record.id, 'port', value)}
min={1}
max={65535}
/>
)
)
},
{
title: 'PAC 脚本',
dataIndex: 'pacScript',
key: 'pacScript',
render: (text: string, record: ProxyConfig) => (
record.proxyType === 'pac_script' && (
<Input.TextArea
value={text}
onChange={e => onChange(record.id, 'pacScript', e.target.value)}
rows={4}
placeholder="输入 PAC 脚本"
/>
)
)
},
{
title: '操作',
key: 'action',
render: (_, record: ProxyConfig) => (
<Space>
<Button
type={record.enabled ? "primary" : "default"}
danger={record.enabled}
onClick={() => record.enabled ?
onClear(record.id) :
onApply(record.id)
}
>
{record.enabled ? '取消应用' : '应用选项'}
</Button>
{record.id !== 'direct' && (
<Button
danger
icon={<DeleteOutlined />}
onClick={() => onDelete(record.id)}
/>
)}
</Space>
)
}
];
return (
<div>
<Space style={{ marginBottom: 16, justifyContent: 'flex-end', width: '100%' }}>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={onAdd}
>
添加代理
</Button>
</Space>
<Table
dataSource={proxyConfigs}
columns={columns}
rowKey="id"
pagination={false}
/>
</div>
);
};