use indexedDB manager proxySwitch

This commit is contained in:
go0p
2026-08-06 15:11:35 +08:00
committed by go0p
parent a8ab3a2899
commit 841f663ed5
23 changed files with 1542 additions and 753 deletions
+78 -2
View File
@@ -1,17 +1,24 @@
import React, {useEffect, useState} from "react";
import {Select, Button, Tooltip, Modal, Form, Input, Radio, Space, message} from "antd";
import {Select, Button, Tooltip, Modal, Form, Input, Radio, Space, message, Switch} from "antd";
import {PlusOutlined, SettingOutlined} from "@ant-design/icons";
import {ProxyConfig} from "@/types/proxy";
import {StorageChanges} from "@/types/chrome";
import {ProxyActionType} from '@/types/action';
import "./index.css";
export const ProxySwitch: React.FC = () => {
interface ProxySwitchProps {
onChange?: (checked: boolean) => void;
}
export const ProxySwitch: React.FC<ProxySwitchProps> = ({ onChange }) => {
const [currentMode, setCurrentMode] = useState<string>("direct");
const [proxyConfigs, setProxyConfigs] = useState<ProxyConfig[]>([]);
const [isModalVisible, setIsModalVisible] = useState(false);
const [form] = Form.useForm();
const [proxyHost, setProxyHost] = useState('');
const [proxyPort, setProxyPort] = useState('');
const [enabled, setEnabled] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadConfigs().catch(error => {
@@ -44,6 +51,10 @@ export const ProxySwitch: React.FC = () => {
return () => chrome.storage.onChanged.removeListener(handleStorageChange);
}, [currentMode]);
useEffect(() => {
loadProxyStatus();
}, []);
const loadConfigs = async () => {
try {
const result = await chrome.storage.local.get('proxyConfigs');
@@ -188,6 +199,65 @@ export const ProxySwitch: React.FC = () => {
}
};
const proxyType: ProxyConfig['proxyType'] = proxyConfigs.find((c: ProxyConfig) => c.id === currentMode)?.proxyType || 'direct';
const loadProxyStatus = async () => {
try {
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.GET_PROXY_STATUS
});
if (response.success) {
setEnabled(response.data.enabled);
}
setLoading(false);
} catch (error) {
console.error('Error loading proxy status:', error);
setLoading(false);
}
};
const handleChange = async (checked: boolean) => {
try {
if (checked) {
// 获取配置列表
const configResponse = await chrome.runtime.sendMessage({
action: ProxyActionType.GET_PROXY_CONFIGS
});
if (!configResponse.success) {
throw new Error(configResponse.error || '获取代理配置失败');
}
const configs = configResponse.data || [];
const defaultConfig = configs.find((c: ProxyConfig) => c.id === 'direct');
if (defaultConfig) {
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.SET_PROXY_CONFIG,
config: defaultConfig
});
if (response.success) {
setEnabled(true);
onChange?.(true);
}
}
} else {
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.CLEAR_PROXY_CONFIG
});
if (response.success) {
setEnabled(false);
onChange?.(false);
}
}
} catch (error) {
console.error('Error toggling proxy:', error);
}
};
return (
<div className="proxy-switch">
<div className="proxy-switch-header">
@@ -283,6 +353,12 @@ export const ProxySwitch: React.FC = () => {
</Form.Item>
</Form>
</Modal>
<Switch
checked={enabled}
onChange={handleChange}
loading={loading}
/>
</div>
);
};
@@ -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>
);
};
@@ -0,0 +1,177 @@
import { useState, useEffect } from 'react';
import { ProxyConfig } from '@/types/proxy';
import { ProxyActionType } from '@/types/action';
import { message } from 'antd';
export const useProxyConfigs = () => {
const [proxyConfigs, setProxyConfigs] = useState<ProxyConfig[]>([]);
useEffect(() => {
loadConfigs();
// 监听配置更新
const handleConfigUpdate = () => {
loadConfigs();
};
chrome.runtime.onMessage.addListener((message) => {
if (message.action === 'PROXY_CONFIGS_UPDATED') {
handleConfigUpdate();
}
});
return () => {
chrome.runtime.onMessage.removeListener(handleConfigUpdate);
};
}, []);
const loadConfigs = async () => {
try {
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.GET_PROXY_CONFIGS
});
if (response.success) {
setProxyConfigs(response.data || []);
}
} catch (error) {
console.error('Error loading configs:', error);
message.error('加载配置时发生错误');
}
};
const handleAddProxy = async (config: ProxyConfig) => {
try {
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.ADD_PROXY_CONFIG,
config: config
});
if (response.success) {
message.success('添加代理成功');
loadConfigs();
} else {
message.error(response.error || '添加代理失败');
}
} catch (error) {
console.error('Error adding proxy:', error);
message.error('添加代理时发生错误');
}
};
const handleConfigChange = async (configId: string, field: keyof ProxyConfig, value: any) => {
try {
const updatedConfigs = proxyConfigs.map(config =>
config.id === configId ? { ...config, [field]: value } : config
);
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.UPDATE_PROXY_CONFIG,
configs: updatedConfigs
});
if (response?.success) {
setProxyConfigs(response.data || updatedConfigs);
message.success('更新配置成功');
} else {
message.error(response?.error || '更新配置失败');
await loadConfigs();
}
} catch (error) {
console.error('Error updating config:', error);
message.error('更新配置时发生错误');
await loadConfigs();
}
};
const handleDeleteProxy = async (configId: string) => {
try {
console.log('Deleting proxy:', configId);
const updatedConfigs = proxyConfigs.filter(config => config.id !== configId);
console.log('Updated configs after delete:', updatedConfigs);
// 使用 Promise 包装消息发送
const response = await new Promise<any>((resolve) => {
chrome.runtime.sendMessage({
action: ProxyActionType.UPDATE_PROXY_CONFIG,
configs: updatedConfigs
}, (result) => {
console.log('Delete response received:', result);
resolve(result);
});
});
console.log('Delete response:', response);
if (response?.success) {
setProxyConfigs(response.data || updatedConfigs);
message.success('删除代理成功');
} else {
console.error('Delete failed:', response?.error);
message.error(response?.error || '删除代理失败');
await loadConfigs();
}
} catch (error) {
console.error('Error deleting proxy:', error);
message.error('删除代理时发生错误');
await loadConfigs();
}
};
const handleApplyConfig = async (configId: string) => {
try {
const config = proxyConfigs.find(c => c.id === configId);
if (!config) return;
// 先取消当前启用的代理
const currentEnabled = proxyConfigs.find(c => c.enabled);
if (currentEnabled && currentEnabled.id !== configId) {
// 如果当前启用的不是直接连接,需要先清除代理设置
if (currentEnabled.id !== 'direct') {
await chrome.runtime.sendMessage({
action: ProxyActionType.CLEAR_PROXY_CONFIG
});
}
}
// 应用新的代理设置
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.SET_PROXY_CONFIG,
config: config
});
if (response.success) {
message.success('代理设置已应用');
} else {
message.error(response.error || '应用代理设置失败');
}
} catch (error) {
console.error('Error applying proxy:', error);
message.error('应用代理设置时发生错误');
}
};
const handleClearProxy = async () => {
try {
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.CLEAR_PROXY_CONFIG
});
if (response.success) {
message.success('已切换至直接连接');
} else {
message.error(response.error || '清除代理设置失败');
}
} catch (error) {
console.error('Error clearing proxy:', error);
message.error('清除代理设置时发生错误');
}
};
return {
proxyConfigs,
handleAddProxy,
handleConfigChange,
handleDeleteProxy,
handleApplyConfig,
handleClearProxy
};
};
@@ -0,0 +1,59 @@
import { useState, useEffect } from 'react';
import { ProxyLog } from '@/types/proxy';
import { ProxyActionType } from '@/types/action';
export const useProxyLogs = () => {
const [proxyLogs, setProxyLogs] = useState<ProxyLog[]>([]);
useEffect(() => {
loadLogs();
// 监听日志更新
const handleLogsUpdate = () => {
loadLogs();
};
chrome.runtime.onMessage.addListener((message) => {
if (message.action === 'PROXY_LOGS_UPDATED') {
handleLogsUpdate();
}
});
return () => {
chrome.runtime.onMessage.removeListener(handleLogsUpdate);
};
}, []);
const loadLogs = async () => {
try {
console.log('Fetching proxy logs...');
const response = await chrome.runtime.sendMessage({
action: 'GET_PROXY_LOGS'
});
console.log('Received response:', response);
if (response.success) {
setProxyLogs(response.data || []);
} else {
console.error('Failed to load logs:', response.error);
}
} catch (error) {
console.error('Error loading logs:', error);
}
};
const handleClearLogs = async () => {
try {
await chrome.runtime.sendMessage({
action: ProxyActionType.CLEAR_PROXY_LOGS
});
setProxyLogs([]);
} catch (error) {
console.error('Error clearing logs:', error);
}
};
return {
proxyLogs,
handleClearLogs
};
};
+12 -1
View File
@@ -1,5 +1,6 @@
.options-page {
min-height: 100vh;
height: 100vh;
overflow: auto;
background: #f0f2f5;
}
@@ -97,4 +98,14 @@
.options-page .ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
background-color: var(--yakit-primary-5);
}
/* 防止 Modal 出现时页面跳动 */
.ant-modal-wrap {
overflow: hidden;
}
.ant-modal-content {
max-height: 90vh;
overflow: auto;
}
+36 -393
View File
@@ -1,59 +1,25 @@
import React, { useEffect, useState } from "react";
import { Layout, Button, Card, Input, Select, InputNumber, Space, Typography, Modal, Switch, Tabs, Table, Form, App } from "antd";
import { PlusOutlined, DeleteOutlined, ImportOutlined, ExportOutlined } from "@ant-design/icons";
import { ProxyConfig } from "@/types/proxy";
import { StorageChanges } from "@/types/chrome";
import './index.css';
import { ProxyActionType } from '@/types/action';
import React from 'react';
import { Layout, Tabs } from 'antd';
import { ProxySettings } from './components/ProxySettings';
import { ProxyLogs } from './components/ProxyLogs';
import { useProxyConfigs } from './hooks/useProxyConfigs';
import { useProxyLogs } from './hooks/useProxyLogs';
import { ProxyConfig } from '@/types/proxy';
const { Header, Content } = Layout;
const { Title } = Typography;
const { TextArea } = Input;
const headerStyle = {
background: '#fff',
padding: '0 24px',
borderBottom: '1px solid #f0f0f0'
};
const contentStyle = {
padding: '24px',
background: '#f0f2f5',
minHeight: '100vh'
};
const titleStyle = {
margin: '16px 0',
color: '#31343F'
};
interface ProxyLog {
id: string;
timestamp: number;
url: string;
proxyId: string;
proxyName: string;
status: 'success' | 'error';
errorMessage?: string;
}
const { Content } = Layout;
export const OptionsPage: React.FC = () => {
const { message } = App.useApp();
const [proxyConfigs, setProxyConfigs] = useState<ProxyConfig[]>([]);
const [proxyLogs, setProxyLogs] = useState<ProxyLog[]>([]);
const [activeTab, setActiveTab] = useState('settings');
const [currentConfigId, setCurrentConfigId] = useState<string>('');
const {
proxyConfigs,
handleAddProxy,
handleConfigChange,
handleDeleteProxy,
handleApplyConfig,
handleClearProxy
} = useProxyConfigs();
const { proxyLogs, handleClearLogs } = useProxyLogs();
useEffect(() => {
loadConfigs();
}, []);
const loadConfigs = async () => {
const result = await chrome.storage.local.get('proxyConfigs');
setProxyConfigs(result.proxyConfigs || []);
};
const handleAddProxy = () => {
const handleAdd = () => {
const newConfig: ProxyConfig = {
id: Date.now().toString(),
name: '新建代理',
@@ -63,360 +29,37 @@ export const OptionsPage: React.FC = () => {
port: 8080,
enabled: false
};
const updatedConfigs = [...proxyConfigs, newConfig];
chrome.storage.local.set({ proxyConfigs: updatedConfigs });
setProxyConfigs(updatedConfigs);
};
const handleConfigChange = (configId: string, field: keyof ProxyConfig, value: any) => {
const updatedConfigs = proxyConfigs.map(config => {
if (config.id === configId) {
return { ...config, [field]: value };
}
return config;
});
chrome.storage.local.set({ proxyConfigs: updatedConfigs });
setProxyConfigs(updatedConfigs);
};
const handleDeleteProxy = (configId: string) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除这个代理配置吗?',
onOk: () => {
const updatedConfigs = proxyConfigs.filter(config => config.id !== configId);
chrome.storage.local.set({ proxyConfigs: updatedConfigs });
setProxyConfigs(updatedConfigs);
}
});
};
useEffect(() => {
const handleStorageChange = (changes: StorageChanges) => {
if (changes.proxyConfigs) {
setProxyConfigs(changes.proxyConfigs.newValue || []);
}
};
chrome.storage.onChanged.addListener(handleStorageChange);
return () => chrome.storage.onChanged.removeListener(handleStorageChange);
}, []);
useEffect(() => {
// 加载日志
const loadLogs = async () => {
const result = await chrome.storage.local.get('proxyLogs');
setProxyLogs(result.proxyLogs || []);
};
loadLogs();
// 监听存储变化
const handleStorageChange = (changes: StorageChanges) => {
if (changes.proxyLogs) {
setProxyLogs(changes.proxyLogs.newValue || []);
}
};
chrome.storage.onChanged.addListener(handleStorageChange);
return () => chrome.storage.onChanged.removeListener(handleStorageChange);
}, []);
const columns = [
{
title: '时间',
dataIndex: 'timestamp',
key: 'timestamp',
render: (timestamp: number) => new Date(timestamp).toLocaleString()
},
{
title: 'URL',
dataIndex: 'url',
key: 'url',
ellipsis: true,
render: (url: string) => (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
style={{
color: '#1890ff',
textDecoration: 'none',
maxWidth: '400px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'block'
}}
onClick={(e) => {
e.preventDefault();
chrome.tabs.create({ url });
}}
>
{url}
</a>
)
},
{
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 handleApplyConfig = async (configId: string) => {
const config = proxyConfigs.find(c => c.id === configId);
if (config) {
try {
const response = await new Promise<any>((resolve) => {
chrome.runtime.sendMessage({
action: ProxyActionType.SET_PROXY_CONFIG,
config: {
...config,
scheme: config.scheme || 'http',
host: config.host || '127.0.0.1',
port: Number(config.port) || 8080,
}
}, resolve);
});
if (response && response.success) {
const updatedConfigs = proxyConfigs.map(c => ({
...c,
enabled: c.id === configId
}));
await chrome.storage.local.set({ proxyConfigs: updatedConfigs });
setProxyConfigs(updatedConfigs);
message.success('代理设置已应用');
} else {
message.error((response && response.error) || '代理设置失败');
}
} catch (error) {
console.error('Failed to apply proxy config:', error);
message.error('操作失败');
}
}
};
const handleClearProxy = async (configId: string) => {
try {
const response = await new Promise<any>((resolve) => {
chrome.runtime.sendMessage({
action: ProxyActionType.CLEAR_PROXY_CONFIG
}, resolve);
});
if (response && response.success) {
const updatedConfigs = proxyConfigs.map(c => ({
...c,
enabled: false
}));
await chrome.storage.local.set({ proxyConfigs: updatedConfigs });
setProxyConfigs(updatedConfigs);
message.success('代理已取消');
} else {
message.error((response && response.error) || '取消代理失败');
}
} catch (error) {
console.error('Error clearing proxy:', error);
message.error('操作失败');
}
};
const handleClearLogs = () => {
chrome.runtime.sendMessage({
action: ProxyActionType.CLEAR_PROXY_LOGS
}, (response) => {
if (chrome.runtime.lastError) {
message.error(chrome.runtime.lastError.message || '清除日志失败');
return;
}
if (response?.success) {
message.success('日志已清除');
} else {
message.error(response?.error || '清除日志失败');
}
});
handleAddProxy(newConfig);
};
return (
<Layout className="options-page">
<Content style={contentStyle}>
<Layout style={{ height: '100vh' }}>
<Content style={{ padding: '24px' }}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
className="proxy-tabs"
tabBarExtraContent={{
right: (
<Space>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAddProxy}
>
添加代理
</Button>
<Button icon={<ImportOutlined />}>导入</Button>
<Button icon={<ExportOutlined />}>导出</Button>
</Space>
)
}}
defaultActiveKey="1"
items={[
{
key: 'settings',
key: '1',
label: '代理设置',
children: (
<Space direction="vertical" style={{ width: '100%' }}>
{proxyConfigs.map(config => (
<Card
key={config.id}
size="small"
title={
<Input
placeholder="代理名称"
value={config.name}
onChange={e => handleConfigChange(config.id, 'name', e.target.value)}
disabled={config.id === 'direct'}
variant="borderless"
style={{ fontSize: '16px', padding: 0 }}
/>
}
extra={
<Space>
<Button
className="proxy-action-btn"
type={config.enabled ? "primary" : "default"}
danger={config.enabled}
onClick={() => config.enabled ?
handleClearProxy(config.id) :
handleApplyConfig(config.id)
}
>
{config.enabled ? '取消应用' : '应用选项'}
</Button>
{config.id !== 'direct' && (
<Button
danger
icon={<DeleteOutlined />}
onClick={() => handleDeleteProxy(config.id)}
/>
)}
</Space>
}
style={{ borderRadius: '4px' }}
>
<Space direction="vertical" style={{ width: '100%' }}>
<Select
style={{ width: '100%' }}
value={config.proxyType}
onChange={value => handleConfigChange(config.id, 'proxyType', value)}
disabled={config.id === 'direct'}
>
<Select.Option value="direct">直接连接</Select.Option>
<Select.Option value="fixed_server">代理服务器</Select.Option>
<Select.Option value="pac_script">PAC 脚本</Select.Option>
<Select.Option value="bypass_list">代理规则列表</Select.Option>
</Select>
{config.proxyType === 'bypass_list' && (
<TextArea
rows={4}
value={config.bypassList?.join('\n')}
onChange={e => handleConfigChange(config.id, 'bypassList', e.target.value.split('\n'))}
placeholder="每行一个规则,例如:
*.example.com
[::1]
127.0.0.1"
/>
)}
{config.proxyType === 'fixed_server' && (
<Space style={{ width: '100%' }}>
<Select
style={{ width: 120 }}
value={config.scheme}
onChange={value => handleConfigChange(config.id, 'scheme', value)}
>
<Select.Option value="http">HTTP</Select.Option>
<Select.Option value="https">HTTPS</Select.Option>
<Select.Option value="socks4">SOCKS4</Select.Option>
<Select.Option value="socks5">SOCKS5</Select.Option>
</Select>
<Input
placeholder="代理服务器"
value={config.host}
onChange={e => handleConfigChange(config.id, 'host', e.target.value)}
/>
<InputNumber
placeholder="端口"
value={config.port}
onChange={value => handleConfigChange(config.id, 'port', value)}
style={{ width: 100 }}
/>
</Space>
)}
{config.proxyType === 'pac_script' && (
<TextArea
rows={4}
value={config.pacScript}
onChange={e => handleConfigChange(config.id, 'pacScript', e.target.value)}
placeholder="输入 PAC 脚本"
/>
)}
</Space>
</Card>
))}
</Space>
<ProxySettings
proxyConfigs={proxyConfigs}
onAdd={handleAdd}
onChange={handleConfigChange}
onDelete={handleDeleteProxy}
onApply={handleApplyConfig}
onClear={handleClearProxy}
/>
)
},
{
key: 'logs',
key: '2',
label: '代理日志',
children: (
<>
<div style={{
marginBottom: 16,
display: 'flex',
justifyContent: 'flex-end'
}}>
<Button
danger
onClick={handleClearLogs}
icon={<DeleteOutlined />}
>
清除日志
</Button>
</div>
<Table
dataSource={proxyLogs}
columns={columns}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `共 ${total} 条`,
pageSizeOptions: ['10', '20', '50', '100']
}}
rowKey="id"
/>
</>
<ProxyLogs
logs={proxyLogs}
onClearLogs={handleClearLogs}
/>
)
}
]}
+5 -1
View File
@@ -14,7 +14,11 @@ export const ProxyActionType = {
SET_PROXY_CONFIG: "SET_PROXY_CONFIG",
CLEAR_PROXY_CONFIG: "CLEAR_PROXY_CONFIG",
GET_PROXY_STATUS: "GET_PROXY_STATUS",
CLEAR_PROXY_LOGS: "CLEAR_PROXY_LOGS"
CLEAR_PROXY_LOGS: "CLEAR_PROXY_LOGS",
GET_PROXY_LOGS: "GET_PROXY_LOGS",
GET_PROXY_CONFIGS: "GET_PROXY_CONFIGS",
ADD_PROXY_CONFIG: "ADD_PROXY_CONFIG",
UPDATE_PROXY_CONFIG: "UPDATE_PROXY_CONFIG"
} as const;
export type ProxyActionType = typeof ProxyActionType[keyof typeof ProxyActionType];
+10
View File
@@ -0,0 +1,10 @@
declare namespace chrome.storage {
interface StorageChange {
oldValue?: any;
newValue?: any;
}
type StorageChanges = {
[key: string]: StorageChange;
};
}
+26 -1
View File
@@ -7,5 +7,30 @@ export interface ProxyConfig {
port?: number;
scheme?: "http" | "https" | "socks4" | "socks5";
pacScript?: string;
bypassList?: string[];
}
export interface ProxyLog {
id: string;
timestamp: number;
url: string;
proxyId: string;
proxyName: string;
status: 'success' | 'error';
errorMessage?: string;
method?: string;
requestHeaders?: Record<string, string>;
requestBody?: string;
responseHeaders?: Record<string, string>;
responseBody?: string;
timing?: {
startTime: number;
endTime: number;
duration: number;
};
protocol?: string;
ip?: string;
fromCache?: boolean;
host?: string;
port?: number;
resourceType?: 'xhr' | 'fetch' | 'script' | 'stylesheet' | 'image' | 'other';
}