diff --git a/package.json b/package.json index 4a19aa0..ae74949 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "jest": "^27.4.3", "jest-resolve": "^27.4.2", "jest-watch-typeahead": "^1.0.0", + "lodash": "^4.17.21", "mini-css-extract-plugin": "^2.4.5", "postcss": "^8.4.4", "postcss-flexbugs-fixes": "^5.0.2", @@ -139,6 +140,7 @@ "@babel/preset-env": "^7.24.1", "@babel/preset-react": "^7.24.1", "@types/chrome": "^0.0.268", + "@types/lodash": "^4.17.15", "babel-loader": "^9.1.3", "copy-webpack-plugin": "^12.0.2", "cross-env": "^7.0.3", diff --git a/public/manifest.json b/public/manifest.json index 26299b3..4e1bb1e 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -27,7 +27,8 @@ "storage", "sidePanel", "webRequest", - "declarativeNetRequest" + "declarativeNetRequest", + "tabs" ], "host_permissions": [ "" diff --git a/src/components/AddProxyForm/index.css b/src/components/AddProxyForm/index.css new file mode 100644 index 0000000..9bb13ab --- /dev/null +++ b/src/components/AddProxyForm/index.css @@ -0,0 +1,10 @@ +.add-proxy-form { + padding: 24px; +} + +.form-buttons { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 24px; +} \ No newline at end of file diff --git a/src/components/AddProxyForm/index.tsx b/src/components/AddProxyForm/index.tsx new file mode 100644 index 0000000..a08e01a --- /dev/null +++ b/src/components/AddProxyForm/index.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { Form, Input, Select, InputNumber, Button, message } from 'antd'; +import { ProxyActionType } from '@/types/action'; +import './index.css'; + +interface EditFormData { + name: string; + proxyType: string; + scheme?: string; + host?: string; + port?: number; + pacScript?: string; +} + +export const AddProxyForm: React.FC = () => { + const [form] = Form.useForm(); + + // 初始化表单 + React.useEffect(() => { + form.setFieldsValue({ + name: '', + proxyType: 'fixed_servers', + scheme: 'http', + host: '127.0.0.1', + port: 8080 + }); + }, []); + + const handleSave = async () => { + try { + const values = await form.validateFields(); + const newConfig = { + id: Date.now().toString(), + ...values, + enabled: false + }; + + const response = await chrome.runtime.sendMessage({ + action: ProxyActionType.ADD_PROXY_CONFIG, + config: newConfig + }); + + if (response.success) { + message.success('添加成功'); + window.close(); // 关闭窗口 + } + } catch (error) { + console.error('Failed to add proxy:', error); + message.error('添加失败'); + } + }; + + return ( +
+
+ {/* 表单项与之前相同 */} + + + + +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/ProxySwitch/index.tsx b/src/components/ProxySwitch/index.tsx index d3f8600..e0bf4fd 100644 --- a/src/components/ProxySwitch/index.tsx +++ b/src/components/ProxySwitch/index.tsx @@ -42,7 +42,17 @@ interface CustomProxy { enabled?: boolean; } -export const ProxySwitch: React.FC = () => { +interface ProxySwitchProps { + proxyConfigs: ProxyConfig[]; + currentProxy: ProxyConfig | null; + onProxyChange: (config: ProxyConfig) => void; +} + +export const ProxySwitch: React.FC = ({ + proxyConfigs, + currentProxy, + onProxyChange, +}) => { const [currentMode, setCurrentMode] = useState('direct'); const [customProxies, setCustomProxies] = useState([]); @@ -126,12 +136,48 @@ export const ProxySwitch: React.FC = () => { const handleModeChange = async (mode: string) => { if (mode === 'setting') { - chrome.runtime.openOptionsPage?.(); + await chrome.runtime.openOptionsPage?.(); return; } if (mode === 'add') { - chrome.runtime.openOptionsPage?.(); + try { + // 获取当前活动标签页 + const [activeTab] = await chrome.tabs.query({ + active: true, + currentWindow: true + }); + const optionsUrl = chrome.runtime.getURL('/proxy/options.html'); + + if (activeTab?.url === optionsUrl) { + // 如果当前就在 options 页面,直接发消息触发添加代理 + chrome.tabs.sendMessage(activeTab.id!, { + action: 'TRIGGER_ADD_PROXY' + }); + } else { + // 如果不在 options 页面,创建新的 + const tab = await chrome.tabs.create({ + url: optionsUrl + }); + + // 等待页面加载完成 + const listener = (tabId: number, changeInfo: chrome.tabs.TabChangeInfo) => { + if (tabId === tab.id && changeInfo.status === 'complete') { + chrome.tabs.onUpdated.removeListener(listener); + // 给页面一点时间完全初始化 + // setTimeout(() => { + chrome.tabs.sendMessage(tab.id!, { + action: 'TRIGGER_ADD_PROXY' + }); + // }, 500); // 减少延迟时间 + } + }; + + chrome.tabs.onUpdated.addListener(listener); + } + } catch (error) { + console.error('Failed to get current tab:', error); + } return; } diff --git a/src/pages/OptionsPage/components/ProxySettings/index.css b/src/pages/OptionsPage/components/ProxySettings/index.css new file mode 100644 index 0000000..9ffed30 --- /dev/null +++ b/src/pages/OptionsPage/components/ProxySettings/index.css @@ -0,0 +1,65 @@ +/* 表格样式 */ +.proxy-table { + background: white; + border-radius: 8px; +} + +/* 表头样式 */ +.proxy-table .ant-table-thead > tr > th { + background: white !important; + color: #333; + font-weight: 500; + border-bottom: 1px solid var(--border-color); + padding: 12px 16px; +} + +/* 斑马纹样式 */ +.proxy-table .ant-table-tbody > tr:nth-child(even) { + background-color: #fafafa; +} + +/* 单元格样式 */ +.proxy-table .ant-table-tbody > tr > td { + border-bottom: 1px solid var(--border-color); + padding: 12px 16px; +} + +/* 操作列图标样式 */ +.action-icon { + font-size: 16px; + cursor: pointer; + color: #666; + transition: all 0.3s; +} + +.action-icon:hover { + color: var(--yakit-primary); + transform: scale(1.1); +} + +.action-icon.enabled { + color: var(--yakit-primary); +} + +.action-icon.delete:hover { + color: #ff4d4f; +} + +/* 间距调整 */ +.ant-space-middle { + gap: 16px !important; +} + +/* 添加按钮样式 */ +.add-proxy-btn { + background-color: var(--yakit-primary); + color: white; + border: none; + border-radius: 4px; + margin-bottom: 16px; +} + +.add-proxy-btn:hover { + background-color: var(--yakit-primary-hover) !important; + color: white !important; +} \ No newline at end of file diff --git a/src/pages/OptionsPage/components/ProxySettings/index.tsx b/src/pages/OptionsPage/components/ProxySettings/index.tsx index ae6e597..5c9f75c 100644 --- a/src/pages/OptionsPage/components/ProxySettings/index.tsx +++ b/src/pages/OptionsPage/components/ProxySettings/index.tsx @@ -1,18 +1,32 @@ -import React, { useRef } from 'react'; -import { Card, Input, Space, Button, Select, InputNumber, Form, Table, Tooltip, Popover } from 'antd'; +import React, { useRef, useState, useEffect, useCallback } from 'react'; +import { Card, Input, Space, Button, Select, InputNumber, Form, Table, Tooltip, Popover, Modal, message } from 'antd'; import type { ColumnsType } from 'antd/es/table'; -import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'; +import { DeleteOutlined, PlusOutlined, EditOutlined, CheckOutlined } from '@ant-design/icons'; import { ProxyConfig } from '@/types/proxy'; +import './index.css'; interface ProxySettingsProps { proxyConfigs: ProxyConfig[]; - onAdd: () => void; - onChange: (configId: string, field: keyof ProxyConfig, value: any) => void; + onAdd: (config: ProxyConfig) => void; + onChange: (configId: string, field: keyof ProxyConfig | 'config', value: any) => void; onDelete: (configId: string) => void; onApply: (configId: string) => Promise; onClear: (configId: string) => Promise; } +// 添加编辑表单的接口 +interface EditFormData { + name: string; + proxyType: "direct" | "system" | "fixed_servers" | "pac_script" | "auto_detect"; + scheme?: "http" | "https" | "socks4" | "socks5"; + host?: string; + port?: number; + pacScript?: string; +} + +// 或者更好的方式是创建一个专门的类型 +type ProxyConfigField = keyof ProxyConfig | 'config'; + export const ProxySettings: React.FC = ({ proxyConfigs, onAdd, @@ -21,120 +35,141 @@ export const ProxySettings: React.FC = ({ onApply, onClear }) => { + const [editingConfig, setEditingConfig] = useState(null); + const [editModalVisible, setEditModalVisible] = useState(false); + const [form] = Form.useForm(); + + // 添加 useEffect 来监听表单值变化 + useEffect(() => { + if (editModalVisible && editingConfig) { + form.setFieldsValue({ + name: editingConfig.name, + proxyType: editingConfig.proxyType, + scheme: editingConfig.scheme, + host: editingConfig.host, + port: editingConfig.port, + pacScript: editingConfig.pacScript + }); + } + }, [editModalVisible, editingConfig, form]); + + // 处理添加按钮点击 + const handleAdd = useCallback(() => { + setEditingConfig({ + id: Date.now().toString(), + name: '', + proxyType: 'fixed_servers', + scheme: 'http' as "http" | "https" | "socks4" | "socks5", + host: '127.0.0.1', + port: 8080, + enabled: false + }); + setEditModalVisible(true); + }, []); + + // 处理编辑按钮点击 + const handleEdit = (record: ProxyConfig) => { + setEditingConfig(record); + setEditModalVisible(true); + }; + + // 处理编辑保存 + const handleEditSave = async () => { + try { + const values = await form.validateFields(); + if (editingConfig) { + if (editingConfig.enabled) { + await onClear(editingConfig.id); + } + + const updatedConfig: ProxyConfig = { + ...editingConfig, + ...values, + proxyType: values.proxyType as "direct" | "system" | "fixed_servers" | "pac_script" | "auto_detect", + scheme: values.scheme as "http" | "https" | "socks4" | "socks5" + }; + + if (!proxyConfigs.find(config => config.id === editingConfig.id)) { + await onAdd(updatedConfig); + } else { + await onChange(editingConfig.id, 'config', updatedConfig); + } + + setEditModalVisible(false); + setEditingConfig(null); + form.resetFields(); + } + } catch (error) { + console.error('Validate Failed:', error); + message.error('保存失败,请检查表单'); + } + }; + + // 处理模态框关闭 + const handleModalClose = () => { + form.resetFields(); + setEditModalVisible(false); + setEditingConfig(null); + }; + const columns: ColumnsType = [ { title: '名称', dataIndex: 'name', key: 'name', - render: (text: string, record: ProxyConfig) => ( - onChange(record.id, 'name', e.target.value)} - disabled={record.id === 'direct'} - /> - ) + render: (text: string) => text }, { title: '类型', dataIndex: 'proxyType', key: 'proxyType', - render: (text: string, record: ProxyConfig) => ( - 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' } - ]} - /> - ) - ) + key: 'scheme' }, { title: '主机', dataIndex: 'host', - key: 'host', - render: (text: string, record: ProxyConfig) => ( - record.proxyType === 'fixed_servers' && ( - onChange(record.id, 'host', e.target.value)} - placeholder="127.0.0.1" - /> - ) - ) + key: 'host' }, { title: '端口', dataIndex: 'port', - key: 'port', - render: (text: number, record: ProxyConfig) => ( - record.proxyType === 'fixed_servers' && ( - onChange(record.id, 'port', value)} - min={1} - max={65535} - /> - ) - ) - }, - { - title: 'PAC 脚本', - dataIndex: 'pacScript', - key: 'pacScript', - render: (text: string, record: ProxyConfig) => ( - record.proxyType === 'pac_script' && ( - onChange(record.id, 'pacScript', e.target.value)} - rows={4} - placeholder="输入 PAC 脚本" - /> - ) - ) + key: 'port' }, { title: '操作', key: 'action', + width: 120, render: (_, record: ProxyConfig) => ( - - + + { + if (record.enabled) { + await onClear(record.id); + } else { + await onApply(record.id); + } + }} + /> + handleEdit(record)} + /> {record.id !== 'direct' && ( - - + + + +
+ + + + + + + + + + + + + + ) : proxyType === 'pac_script' ? ( + + + + ) : null; + }} + + +
- ); + ); }; \ No newline at end of file diff --git a/src/pages/OptionsPage/index.tsx b/src/pages/OptionsPage/index.tsx index 4688878..f136d6f 100644 --- a/src/pages/OptionsPage/index.tsx +++ b/src/pages/OptionsPage/index.tsx @@ -1,35 +1,104 @@ -import React from 'react'; -import { Layout, Tabs } from 'antd'; +import React, { useState, useEffect, useRef } from 'react'; +import { Layout, Tabs, message } 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'; +import { ProxyActionType } from '@/types/action'; const { Content } = Layout; +interface ProxySettingsProps { + proxyConfigs: ProxyConfig[]; + onAdd: (config: ProxyConfig) => void; + onChange: (configId: string, field: keyof ProxyConfig | 'config', value: any) => void; + onDelete: (configId: string) => void; + onApply: (configId: string) => Promise; + onClear: (configId: string) => Promise; +} + export const OptionsPage: React.FC = () => { const { proxyConfigs, handleAddProxy, - handleConfigChange, + handleConfigChange: handleConfigChangeHook, handleDeleteProxy, handleApplyConfig, handleClearProxy } = useProxyConfigs(); const { proxyLogs, handleClearLogs } = useProxyLogs(); + const [proxyConfigsState, setProxyConfigs] = useState([]); - const handleAdd = () => { - const newConfig: ProxyConfig = { - id: Date.now().toString(), - name: '新建代理', - proxyType: 'fixed_servers', - scheme: 'http', - host: '127.0.0.1', - port: 8080, - enabled: false + useEffect(() => { + setProxyConfigs(proxyConfigs); + }, [proxyConfigs]); + + // 通知 background 页面已准备就绪 + useEffect(() => { + chrome.runtime.sendMessage({ action: 'OPTIONS_PAGE_READY' }); + + const messageListener = ( + message: any, + sender: chrome.runtime.MessageSender, + sendResponse: (response?: any) => void + ) => { + if (message.action === 'TRIGGER_ADD_PROXY') { + const proxySettingsElement = document.querySelector('.add-proxy-btn'); + if (proxySettingsElement) { + (proxySettingsElement as HTMLElement).click(); + } + } + sendResponse(); }; - handleAddProxy(newConfig); + + chrome.runtime.onMessage.addListener(messageListener); + return () => { + chrome.runtime.onMessage.removeListener(messageListener); + }; + }, []); + + const handleAdd = async (config: ProxyConfig) => { + try { + await handleAddProxy(config); + } catch (error) { + console.error('Failed to add proxy:', error); + message.error('添加代理失败'); + } + }; + + const handleConfigChange = async (configId: string, field: keyof ProxyConfig | 'config', value: any) => { + try { + const updatedConfigs = proxyConfigsState.map(config => { + if (config.id === configId) { + if (field === 'config') { + // 如果是整个配置更新 + return value; + } else { + // 如果是单个字段更新 + return { + ...config, + [field]: value + }; + } + } + return config; + }); + + // 更新 IndexedDB + await chrome.runtime.sendMessage({ + action: ProxyActionType.UPDATE_PROXY_CONFIG, + configs: updatedConfigs + }); + + // 更新本地状态 + setProxyConfigs(updatedConfigs); + + message.success('更新配置成功'); + } catch (error) { + console.error('Failed to update config:', error); + message.error('更新配置失败'); + } }; return ( @@ -43,7 +112,7 @@ export const OptionsPage: React.FC = () => { label: '代理设置', children: (