mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-25 20:51:52 +08:00
Remove ord directory from tracking
This commit is contained in:
-30
@@ -1,30 +0,0 @@
|
||||
import React from "react";
|
||||
// import "./App.css";
|
||||
import {ConfigProvider} from "antd";
|
||||
import {Contro} from "@components/Contro";
|
||||
import {Proxifier} from "@components/Proxifier";
|
||||
import {EvalInTab} from "@components/EvalInTab";
|
||||
import {ProxySwitch} from "@components/ProxySwitch";
|
||||
import './styles/global.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: "#F28B44",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="App">
|
||||
{/*<Contro/>*/}
|
||||
{/* <Proxifier/> */}
|
||||
<ProxySwitch/>
|
||||
|
||||
{/* <EvalInTab/> */}
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,168 +0,0 @@
|
||||
import Icon from "@ant-design/icons";
|
||||
import { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon";
|
||||
import React from "react";
|
||||
|
||||
interface IconProps extends CustomIconComponentProps {
|
||||
onClick: (e: React.MouseEvent) => void;
|
||||
ref?: any;
|
||||
}
|
||||
|
||||
const X = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M4 12L12 4M4 4L12 12"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
/**
|
||||
* @description Icon/Outline/x
|
||||
*/
|
||||
export const XIcon = (props: Partial<IconProps>) => {
|
||||
return <Icon component={X} {...props} />;
|
||||
};
|
||||
|
||||
const Check = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M3.33337 8.66669L6.00004 11.3334L12.6667 4.66669"
|
||||
stroke="#56C991"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
/**
|
||||
* @description Icon/Outline/check
|
||||
*/
|
||||
export const CheckIcon = (props: Partial<IconProps>) => {
|
||||
return <Icon component={Check} {...props} />;
|
||||
};
|
||||
|
||||
const PencilAlt = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M7.33329 3.33334H3.99996C3.26358 3.33334 2.66663 3.93029 2.66663 4.66667V12C2.66663 12.7364 3.26358 13.3333 3.99996 13.3333H11.3333C12.0697 13.3333 12.6666 12.7364 12.6666 12V8.66667M11.7238 2.39052C12.2445 1.86983 13.0887 1.86983 13.6094 2.39052C14.1301 2.91122 14.1301 3.75544 13.6094 4.27614L7.88557 10H5.99996L5.99996 8.11438L11.7238 2.39052Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
/**
|
||||
* @description Icon/Outline/pencil-alt
|
||||
*/
|
||||
export const PencilAltIcon = (props: Partial<IconProps>) => {
|
||||
return <Icon component={PencilAlt} {...props} />;
|
||||
};
|
||||
|
||||
const Refresh = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M2.66663 2.66669V6.00002H3.0543M13.292 7.33335C12.964 4.70248 10.7197 2.66669 7.99996 2.66669C5.76171 2.66669 3.84549 4.04547 3.0543 6.00002M3.0543 6.00002H5.99996M13.3333 13.3334V10H12.9456M12.9456 10C12.1544 11.9546 10.2382 13.3334 7.99996 13.3334C5.28021 13.3334 3.03595 11.2976 2.70789 8.66669M12.9456 10H9.99996"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
/**
|
||||
* @description Icon/Outline/refresh
|
||||
*/
|
||||
export const RefreshIcon = (props: Partial<IconProps>) => {
|
||||
return <Icon component={Refresh} {...props} />;
|
||||
};
|
||||
|
||||
const Exit = () => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.33333 6.66668C9.10226 6.78107 8.87965 6.90988 8.66667 7.0519C7.05869 8.12418 6 9.95027 6 12.0227C6 15.3239 8.68629 18 12 18C15.3137 18 18 15.3239 18 12.0227C18 9.95027 16.9413 8.12418 15.3333 7.0519C15.1204 6.90988 14.8977 6.78107 14.6667 6.66668M12 5.33334V10.6667"
|
||||
stroke="#F7544A"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
/**
|
||||
* @description 退出
|
||||
*/
|
||||
export const ExitIcon = (props: Partial<IconProps>) => {
|
||||
return <Icon component={Exit} {...props} />;
|
||||
};
|
||||
|
||||
const PlusSm = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M8 4V8M8 8V12M8 8H12M8 8L4 8"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
/**
|
||||
* @description Icon/Outline/plus-sm
|
||||
*/
|
||||
export const PlusSmIcon = (props: Partial<IconProps>) => {
|
||||
return <Icon component={PlusSm} {...props} />;
|
||||
};
|
||||
|
||||
const Trash = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M12.6666 4.66667L12.0884 12.7617C12.0386 13.4594 11.458 14 10.7585 14H5.24145C4.54193 14 3.96135 13.4594 3.91151 12.7617L3.33329 4.66667M6.66663 7.33333V11.3333M9.33329 7.33333V11.3333M9.99996 4.66667V2.66667C9.99996 2.29848 9.70148 2 9.33329 2H6.66663C6.29844 2 5.99996 2.29848 5.99996 2.66667V4.66667M2.66663 4.66667H13.3333"
|
||||
stroke="#F7544A"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
/**
|
||||
* @description Icon/Outline/trash
|
||||
*/
|
||||
export const TrashIcon = (props: Partial<IconProps>) => {
|
||||
return <Icon component={Trash} {...props} />;
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
.add-proxy-form {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.form-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
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<EditFormData>();
|
||||
|
||||
// 初始化表单
|
||||
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 (
|
||||
<div className="add-proxy-form">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSave}
|
||||
>
|
||||
{/* 表单项与之前相同 */}
|
||||
<Form.Item className="form-buttons">
|
||||
<Button type="primary" htmlType="submit">
|
||||
确定
|
||||
</Button>
|
||||
<Button onClick={() => window.close()}>
|
||||
取消
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
.Contro {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.Contro-error-bg {
|
||||
background-color: rgba(244, 115, 107, .10);
|
||||
}
|
||||
|
||||
.Contro-success-bg {
|
||||
background-color: rgba(86, 201, 145, .10);
|
||||
}
|
||||
|
||||
.Contro-cont-input {
|
||||
height: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.Contro-lable {
|
||||
color: #85899E;
|
||||
}
|
||||
|
||||
.Contro-cont {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.Contro-cont-text-error {
|
||||
color: #F6544A;
|
||||
}
|
||||
|
||||
.Contro-cont-text-success {
|
||||
color: #56C991;
|
||||
}
|
||||
|
||||
.Contro-handle-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Contro-handle-icon svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.contro-handle-icon-check {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.icon-p {
|
||||
display: inline-block;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.icon-p svg {
|
||||
color: #85899E;
|
||||
}
|
||||
|
||||
.Contro-cont span.anticon:hover {
|
||||
background: #F0F1F3;
|
||||
}
|
||||
|
||||
.grey-icon svg {
|
||||
color: #85899E;
|
||||
}
|
||||
|
||||
.icon-active:active svg {
|
||||
color: #F28B44;
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import React, {useEffect, useMemo, useRef, useState} from "react";
|
||||
import {Divider, Tooltip, Input} from "antd";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
CheckIcon,
|
||||
ExitIcon,
|
||||
PencilAltIcon,
|
||||
RefreshIcon,
|
||||
XIcon,
|
||||
} from "@assets/icon/icon";
|
||||
import {wsc} from "@network/chrome";
|
||||
import "./Contro.css";
|
||||
|
||||
interface ControProps {
|
||||
}
|
||||
|
||||
export const Contro: React.FC<ControProps> = () => {
|
||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [autoFindFailedReason, setAutoFindFailedReason] = useState<string>("");
|
||||
const [enginePort, setEnginePort] = useState<string>("");
|
||||
const [enginePortTemp, setEnginePortTemp] = useState<string>(enginePort);
|
||||
const enginePortRef = useRef<string>(enginePort);
|
||||
|
||||
useEffect(() => {
|
||||
enginePortRef.current = enginePort;
|
||||
}, [enginePort]);
|
||||
|
||||
useEffect(() => {
|
||||
const yakitConnectInfo = localStorage.getItem("yakit-connect");
|
||||
if (!yakitConnectInfo) {
|
||||
findPort(11212, 11222);
|
||||
} else {
|
||||
const {port, connected} = JSON.parse(yakitConnectInfo);
|
||||
setConnected(connected);
|
||||
setEnginePort(port + "");
|
||||
setAutoFindFailedReason("");
|
||||
}
|
||||
|
||||
wsc.onWSCMessage((message) => {
|
||||
if (message.action === wsc.ActionType.STATUS) {
|
||||
if (message.connected === false) {
|
||||
handleConnectFail();
|
||||
} else {
|
||||
localStorage.setItem(
|
||||
"yakit-connect",
|
||||
JSON.stringify({connected: true, port: message.port})
|
||||
);
|
||||
setEnginePort(message.port + "");
|
||||
setConnected(true);
|
||||
setAutoFindFailedReason("");
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const findPort = (port: number, max: number) => {
|
||||
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
|
||||
ws.onclose = (e: CloseEvent) => {
|
||||
if (enginePortRef.current) {
|
||||
handleConnectFail();
|
||||
return;
|
||||
}
|
||||
if (e.reason !== `FoundYakitWebSocketController` && port + 1 <= max) {
|
||||
setTimeout(() => findPort(port + 1, max), 200);
|
||||
}
|
||||
|
||||
if (port + 1 > max) {
|
||||
setConnected(false);
|
||||
setEnginePort("");
|
||||
setAutoFindFailedReason("Cannot found Yakit");
|
||||
localStorage.setItem("yakit-connect", "");
|
||||
}
|
||||
};
|
||||
ws.onopen = () => {
|
||||
setConnected(true);
|
||||
setEnginePort(port + "");
|
||||
ws.close(1000, "FoundYakitWebSocketController");
|
||||
connectPort(port);
|
||||
};
|
||||
};
|
||||
|
||||
const handleConnectFail = () => {
|
||||
setConnected(false);
|
||||
setAutoFindFailedReason("Yakit WebSocket Controller Connect Fail");
|
||||
localStorage.setItem("yakit-connect", "");
|
||||
};
|
||||
|
||||
const connectPort = (port: number) => {
|
||||
setAutoFindFailedReason("");
|
||||
wsc.connect(port);
|
||||
};
|
||||
|
||||
const safeConnected = useMemo(() => {
|
||||
return connected && enginePort;
|
||||
}, [connected, enginePort]);
|
||||
|
||||
const failConnected = useMemo(() => {
|
||||
return !connected && autoFindFailedReason;
|
||||
}, [connected, autoFindFailedReason]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{safeConnected || failConnected ? (
|
||||
<div
|
||||
className={classNames("Contro", {
|
||||
["Contro-success-bg"]: safeConnected,
|
||||
["Contro-error-bg"]: failConnected,
|
||||
})}
|
||||
>
|
||||
<div className="Contro-lable">Yakit 引擎连接状态:</div>
|
||||
<div className="Contro-cont">
|
||||
{isEdit ? (
|
||||
<>
|
||||
<Input
|
||||
rootClassName="Contro-cont-input"
|
||||
value={enginePortTemp}
|
||||
placeholder="输入范围 11212 - 11222"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setEnginePortTemp(value);
|
||||
}}
|
||||
/>
|
||||
<div className="Contro-handle-icon">
|
||||
<XIcon
|
||||
className="grey-icon icon-p icon-active"
|
||||
onClick={() => {
|
||||
setIsEdit(false);
|
||||
}}
|
||||
/>
|
||||
<CheckIcon
|
||||
className="contro-handle-icon-check icon-p"
|
||||
onClick={() => {
|
||||
if (enginePortTemp) {
|
||||
setIsEdit(false);
|
||||
setEnginePort(enginePortTemp);
|
||||
connectPort(Number(enginePortTemp));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={classNames("Contro-cont-text", {
|
||||
["Contro-cont-text-success"]: safeConnected,
|
||||
["Contro-cont-text-error"]: failConnected,
|
||||
})}
|
||||
>
|
||||
{safeConnected && "已连接(" + enginePort + ")"}
|
||||
{failConnected &&
|
||||
(enginePort
|
||||
? autoFindFailedReason + "(" + enginePort + ")"
|
||||
: autoFindFailedReason)}
|
||||
</div>
|
||||
<div className="Contro-handle-icon">
|
||||
<Tooltip title="修改监听端口">
|
||||
<PencilAltIcon
|
||||
className="grey-icon icon-p icon-active"
|
||||
onClick={() => {
|
||||
setIsEdit(true);
|
||||
setEnginePortTemp(enginePort);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Divider type="vertical" style={{height: 16}}/>
|
||||
{safeConnected && (
|
||||
<ExitIcon onClick={() => wsc.disconnect()}/>
|
||||
)}
|
||||
{failConnected && (
|
||||
<RefreshIcon
|
||||
className="grey-icon icon-active"
|
||||
onClick={() => {
|
||||
if (enginePort) {
|
||||
connectPort(Number(enginePort));
|
||||
} else {
|
||||
findPort(11212, 11222);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,81 +0,0 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Button, Input} from "antd";
|
||||
import TextArea from "antd/lib/input/TextArea";
|
||||
import {wsc} from "@network/chrome";
|
||||
|
||||
interface EvalInTabProps {
|
||||
}
|
||||
|
||||
export const EvalInTab: React.FC<EvalInTabProps> = () => {
|
||||
const [funcName, setFuncName] = useState("");
|
||||
const [inputArgsData, setInputArgsData] = useState("");
|
||||
const [code, setCode] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
wsc.onWSCMessage((message) => {
|
||||
console.log("message from content script:", message)
|
||||
if (message.action === wsc.ActionType.TO_EXTENSION_PAGE) {
|
||||
console.log("eval in tab:", message.result);
|
||||
// alert("from content script: " + JSON.stringify(message.result));
|
||||
// 发送结果
|
||||
wsc.sendMessage({"type": "chrome-extension", res: message.result})
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
const [tab] = await wsc.getTab();
|
||||
await chrome.runtime.sendMessage({
|
||||
action: wsc.ActionType.INJECT_SCRIPT,
|
||||
tabId: tab.id,
|
||||
value: {mode: "CONTENT_CALL_FUNCTION", fn_name: funcName, args: inputArgsData},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("error:", error)
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setInputArgsData(event.target.value);
|
||||
};
|
||||
|
||||
|
||||
const handleEvalCodeClick = async () => {
|
||||
try {
|
||||
const [tab] = await wsc.getTab();
|
||||
await chrome.runtime.sendMessage({
|
||||
action: wsc.ActionType.INJECT_SCRIPT,
|
||||
tabId: tab.id,
|
||||
value: {
|
||||
mode: "CONTENT_EVAL_CODE",
|
||||
code: code,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={funcName}
|
||||
onChange={(e) => setFuncName(e.target.value)}
|
||||
placeholder="Enter function name"
|
||||
></Input>
|
||||
<TextArea
|
||||
value={inputArgsData}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter function args"
|
||||
/>
|
||||
<Button onClick={handleClick}>eval func in tab</Button>
|
||||
|
||||
<TextArea
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder="Enter your expression (e.g., 22 * 33)"
|
||||
/>
|
||||
<Button onClick={handleEvalCodeClick}>eval code in tab</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,113 +0,0 @@
|
||||
.Prox-title-wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 24px 8px;
|
||||
}
|
||||
|
||||
.Prox-title-wrap-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Prox-title-wrap-left .prox-title {
|
||||
margin-right: 4px;
|
||||
color: #31343F;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.Prox-title-wrap-left .prox-number {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 16px;
|
||||
text-align: center;
|
||||
color: #85899E;
|
||||
border-radius: 8px;
|
||||
background: #F0F1F3;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.Prox-title-wrap-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Prox-title-wrap-right .Prox-add-text {
|
||||
margin-right: 4px;
|
||||
color: var(--yakit-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.Prox-title-wrap-right .Prox-add-icon svg {
|
||||
color: var(--yakit-primary);
|
||||
}
|
||||
|
||||
.Prox-list-wrap {
|
||||
overflow-y: auto;
|
||||
max-height: 305px;
|
||||
padding: 16px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.add-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #EAECF3;
|
||||
border-radius: 4px;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.add-list:hover {
|
||||
border: 1px solid var(--yakit-primary);
|
||||
}
|
||||
|
||||
.add-list .add-list-icon svg {
|
||||
color: #85899E;
|
||||
}
|
||||
|
||||
.Prox-list-wrap .add-list .add-list-text {
|
||||
margin-left: 4px;
|
||||
color: #31343F;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.Prox-list-wrap .Prox-list-item-wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.Prox-list-wrap .Prox-list-item-wrap:hover {
|
||||
background-color: #F8F8F8;
|
||||
}
|
||||
|
||||
.Prox-list-item-space .ant-space-item .ant-space-compact {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.Prox-list-item-space .ant-space-item .ant-space-compact .ant-select-single {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.Prox-list-wrap .Prox-list-item-wrap:hover .proxy-list-del-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.proxy-list-del-icon {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.proxy-list-del-icon:hover {
|
||||
background: #F0F1F3;
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Space, Select, Input, Switch} from "antd";
|
||||
import {PlusSmIcon, TrashIcon} from "@assets/icon/icon";
|
||||
import {wsc} from "@network/chrome";
|
||||
import "./Proxifier.css";
|
||||
|
||||
type Scheme = "http" | "socks5";
|
||||
interface ProxyConfig {
|
||||
id: string;
|
||||
scheme: Scheme;
|
||||
host: string;
|
||||
port: string;
|
||||
hostStatus: "error" | "";
|
||||
portStatus: "error" | "";
|
||||
open: boolean;
|
||||
proxy: string;
|
||||
}
|
||||
|
||||
export interface ProxifierProps {}
|
||||
export const Proxifier: React.FC<ProxifierProps> = () => {
|
||||
const [proxyList, setProxyList] = useState<ProxyConfig[]>(() => {
|
||||
const storageProxyList = localStorage.getItem("yakit-proxy-list") || "[]";
|
||||
return JSON.parse(storageProxyList);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("yakit-proxy-list", JSON.stringify(proxyList));
|
||||
}, [proxyList]);
|
||||
|
||||
const addNewProxyListItem = (
|
||||
scheme: Scheme,
|
||||
host: string,
|
||||
port: string,
|
||||
open: boolean,
|
||||
proxy: string
|
||||
) => {
|
||||
const proxyItem: ProxyConfig = {
|
||||
id: Math.random() + "",
|
||||
scheme: scheme as Scheme,
|
||||
host: host,
|
||||
port: port,
|
||||
hostStatus: "",
|
||||
portStatus: "",
|
||||
open: open,
|
||||
proxy: proxy,
|
||||
};
|
||||
return proxyItem;
|
||||
};
|
||||
|
||||
const parseUrl = (url: string) => {
|
||||
const regex = /^(.*?):\/\/(.*?):(\d+)/;
|
||||
const match = url.match(regex);
|
||||
if (match) {
|
||||
const scheme = match[1];
|
||||
const host = match[2];
|
||||
const port = match[3];
|
||||
return {
|
||||
scheme,
|
||||
host,
|
||||
port,
|
||||
};
|
||||
} else {
|
||||
return null; // 不匹配格式
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
wsc.updateProxyStatus();
|
||||
|
||||
wsc.onProxyStatusMessage((msg) => {
|
||||
console.log("msg", msg)
|
||||
if (msg.proxy === undefined || msg.enable === undefined) {
|
||||
return;
|
||||
}
|
||||
if (msg.proxy === "" || msg.enable === false) {
|
||||
if (proxyList.some((i) => i.open)) {
|
||||
const copyProxyList = [...proxyList];
|
||||
copyProxyList.forEach((i) => {
|
||||
i.open = false;
|
||||
});
|
||||
setProxyList(copyProxyList);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.proxy && msg.enable) {
|
||||
const copyProxyList = [...proxyList];
|
||||
let newProxyItem: ProxyConfig = undefined;
|
||||
if (!copyProxyList.length) {
|
||||
const urlObj = parseUrl(msg.proxy);
|
||||
if (urlObj) {
|
||||
newProxyItem = addNewProxyListItem(
|
||||
urlObj.scheme as Scheme,
|
||||
urlObj.host,
|
||||
urlObj.port,
|
||||
true,
|
||||
msg.proxy
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const proxyExist = copyProxyList.some((i) => i.proxy === msg.proxy);
|
||||
if (proxyExist) {
|
||||
const proxyOpen = copyProxyList.some(
|
||||
(i) => i.open && i.proxy === msg.proxy
|
||||
);
|
||||
if (!proxyOpen) {
|
||||
copyProxyList.forEach((i) => {
|
||||
i.open = false;
|
||||
});
|
||||
for (let i = 0; i < copyProxyList.length; i++) {
|
||||
if (copyProxyList[i].proxy === msg.proxy) {
|
||||
copyProxyList[i].open = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
copyProxyList.forEach((i) => {
|
||||
i.open = false;
|
||||
});
|
||||
const urlObj = parseUrl(msg.proxy);
|
||||
if (urlObj) {
|
||||
newProxyItem = addNewProxyListItem(
|
||||
urlObj.scheme as Scheme,
|
||||
urlObj.host,
|
||||
urlObj.port,
|
||||
true,
|
||||
msg.proxy
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newProxyItem) {
|
||||
copyProxyList.unshift(newProxyItem);
|
||||
}
|
||||
setProxyList(copyProxyList);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const hostOnchange = (value: string, id: string) => {
|
||||
const ipPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
|
||||
const domainPattern = /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
const copyProxyList = structuredClone(proxyList);
|
||||
if (value === "" || ipPattern.test(value) || domainPattern.test(value)) {
|
||||
copyProxyList.forEach((i) => {
|
||||
if (i.id === id) {
|
||||
i.hostStatus = "";
|
||||
i.host = value;
|
||||
i.proxy = i.scheme + "://" + value + ":" + i.port;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
copyProxyList.forEach((i) => {
|
||||
if (i.id === id) {
|
||||
i.hostStatus = "error";
|
||||
i.host = value;
|
||||
i.proxy = i.scheme + "://" + value + ":" + i.port;
|
||||
}
|
||||
});
|
||||
}
|
||||
setProxyList(copyProxyList);
|
||||
};
|
||||
|
||||
const portOnchange = (value: string, id: string) => {
|
||||
const portNumber = parseInt(value, 10);
|
||||
const copyProxyList = structuredClone(proxyList);
|
||||
if (
|
||||
value === "" ||
|
||||
(/^\d+$/.test(value) && portNumber >= 0 && portNumber <= 65535)
|
||||
) {
|
||||
copyProxyList.forEach((i) => {
|
||||
if (i.id === id) {
|
||||
i.portStatus = "";
|
||||
const port = value === "" ? "" : portNumber + "";
|
||||
i.port = port;
|
||||
i.proxy = i.scheme + "://" + i.host + ":" + port;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
copyProxyList.forEach((i) => {
|
||||
if (i.id === id) {
|
||||
i.portStatus = "error";
|
||||
i.port = value;
|
||||
i.proxy = i.scheme + "://" + i.host + ":" + value;
|
||||
}
|
||||
});
|
||||
}
|
||||
setProxyList(copyProxyList);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="Prox">
|
||||
<div className="Prox-title-wrap">
|
||||
<div className="Prox-title-wrap-left">
|
||||
<span className="prox-title">设置代理</span>
|
||||
<span className="prox-number">{proxyList.length}</span>
|
||||
</div>
|
||||
<div
|
||||
className="Prox-title-wrap-right"
|
||||
onClick={() => {
|
||||
setProxyList([
|
||||
...proxyList,
|
||||
addNewProxyListItem("http", "", "", false, "http://"),
|
||||
]);
|
||||
}}
|
||||
>
|
||||
<span className="Prox-add-text">添加</span>
|
||||
<PlusSmIcon className="Prox-add-icon"/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="Prox-list-wrap">
|
||||
{proxyList.length ? (
|
||||
proxyList.map((item) => (
|
||||
<div className="Prox-list-item-wrap" key={item.id}>
|
||||
<Space className="Prox-list-item-space">
|
||||
<Space.Compact>
|
||||
<Select
|
||||
value={item.scheme}
|
||||
style={{width: 88}}
|
||||
disabled={item.open}
|
||||
onChange={(value, option) => {
|
||||
const copyProxyList = structuredClone(proxyList);
|
||||
copyProxyList.forEach((i) => {
|
||||
if (i.id === item.id) {
|
||||
i.scheme = value;
|
||||
i.proxy = value + "://" + i.host + ":" + i.port;
|
||||
}
|
||||
});
|
||||
setProxyList(copyProxyList);
|
||||
}}
|
||||
>
|
||||
<Select.Option value="http">HTTP</Select.Option>
|
||||
<Select.Option value="socks5">Socks5</Select.Option>
|
||||
</Select>
|
||||
<Input
|
||||
value={item.host}
|
||||
style={{width: 136}}
|
||||
disabled={item.open}
|
||||
status={item.hostStatus}
|
||||
onChange={(e) => hostOnchange(e.target.value, item.id)}
|
||||
/>
|
||||
<Input
|
||||
value={item.port}
|
||||
style={{width: 64}}
|
||||
disabled={item.open}
|
||||
status={item.portStatus}
|
||||
onChange={(e) => portOnchange(e.target.value, item.id)}
|
||||
/>
|
||||
</Space.Compact>
|
||||
</Space>
|
||||
{!item.open && (
|
||||
<TrashIcon
|
||||
className="proxy-list-del-icon"
|
||||
onClick={() => {
|
||||
setProxyList(proxyList.filter((i) => i.id !== item.id));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Switch
|
||||
checkedChildren="启"
|
||||
unCheckedChildren="停"
|
||||
value={item.open}
|
||||
disabled={
|
||||
item.hostStatus === "error" ||
|
||||
item.portStatus === "error" ||
|
||||
item.host === "" ||
|
||||
item.port === ""
|
||||
}
|
||||
onChange={(checked: boolean) => {
|
||||
wsc.clearProxy();
|
||||
const copyProxyList = structuredClone(proxyList);
|
||||
copyProxyList.forEach((i) => {
|
||||
if (i.id === item.id) {
|
||||
i.open = checked;
|
||||
if (checked) {
|
||||
wsc.setProxy(item.scheme, item.host, Number(item.port));
|
||||
}
|
||||
} else {
|
||||
i.open = false;
|
||||
}
|
||||
});
|
||||
setProxyList(copyProxyList);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div
|
||||
className="add-list"
|
||||
onClick={() => {
|
||||
setProxyList([
|
||||
addNewProxyListItem(
|
||||
"http",
|
||||
"127.0.0.1",
|
||||
"8083",
|
||||
false,
|
||||
"http://127.0.0.1:8083"
|
||||
),
|
||||
]);
|
||||
}}
|
||||
>
|
||||
<PlusSmIcon className="add-list-icon"/>
|
||||
<span className="add-list-text">添加</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,235 +0,0 @@
|
||||
.proxy-container {
|
||||
min-width: 200px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.proxy-menu {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
height: 40px !important;
|
||||
line-height: 40px !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 16px !important;
|
||||
}
|
||||
|
||||
.menu-item .anticon {
|
||||
font-size: 16px;
|
||||
color: var(--yakit-primary);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.menu-item-label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background-color: var(--yakit-primary-5) !important;
|
||||
}
|
||||
|
||||
.menu-item:hover .anticon,
|
||||
.menu-item:hover .menu-item-label {
|
||||
color: var(--yakit-primary) !important;
|
||||
}
|
||||
|
||||
/* 选中状态 */
|
||||
.menu-item.ant-menu-item-selected {
|
||||
background-color: var(--yakit-primary) !important;
|
||||
}
|
||||
|
||||
.menu-item.ant-menu-item-selected .anticon,
|
||||
.menu-item.ant-menu-item-selected .menu-item-label {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.menu-item.ant-menu-item-selected:hover {
|
||||
background-color: var(--yakit-primary-hover) !important;
|
||||
}
|
||||
|
||||
/* 分隔线 */
|
||||
.ant-menu-item-divider {
|
||||
margin: 4px 0 !important;
|
||||
border-color: #EAECF3 !important;
|
||||
}
|
||||
|
||||
/* 设置选项 */
|
||||
.menu-item-setting {
|
||||
border-top: 1px solid #EAECF3;
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
|
||||
.menu-item-setting .anticon {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.menu-item-setting:hover {
|
||||
background-color: var(--yakit-primary-5) !important;
|
||||
}
|
||||
|
||||
.menu-item-setting:hover .anticon,
|
||||
.menu-item-setting:hover .menu-item-label {
|
||||
color: var(--yakit-primary) !important;
|
||||
}
|
||||
|
||||
/* 调整图标大小和对齐 */
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 添加以下样式来确保下拉菜单显示在正确的位置 */
|
||||
.ant-dropdown {
|
||||
position: absolute !important;
|
||||
top: 100% !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
min-width: 200px !important;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 3px 6px -4px rgba(0,0,0,0.12),
|
||||
0 6px 16px 0 rgba(0,0,0,0.08),
|
||||
0 9px 28px 8px rgba(0,0,0,0.05);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* 确保容器不会限制弹出层 */
|
||||
.proxy-container {
|
||||
min-width: 200px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
position: static;
|
||||
}
|
||||
|
||||
/* 添加这个样式来确保下拉菜单显示在正确的位置 */
|
||||
body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ant-menu {
|
||||
border: none !important;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15) !important;
|
||||
padding: 4px 0 !important;
|
||||
width: 180px !important;
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
.ant-menu-item {
|
||||
height: 28px !important;
|
||||
line-height: 28px !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 16px !important;
|
||||
}
|
||||
|
||||
.ant-menu-item:hover {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
margin-right: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-item-selected {
|
||||
background-color: #e6f7ff !important;
|
||||
}
|
||||
|
||||
.ant-menu-item-divider {
|
||||
margin: 4px 0 !important;
|
||||
height: 1px !important;
|
||||
background-color: #f0f0f0 !important;
|
||||
}
|
||||
|
||||
.ant-menu-item:last-child {
|
||||
margin-top: 4px !important;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
/* 移除多余的样式 */
|
||||
.ant-menu-root {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 调整整体容器大小 */
|
||||
.ant-menu-root {
|
||||
width: 180px !important;
|
||||
min-height: auto !important;
|
||||
}
|
||||
|
||||
/* 添加代理按钮样式 */
|
||||
.menu-item-add {
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
.menu-item-add:hover {
|
||||
background-color: #f5f5f5 !important;
|
||||
color: var(--yakit-primary) !important;
|
||||
}
|
||||
|
||||
.menu-item-add .anticon {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.menu-item-add:hover .anticon {
|
||||
color: var(--yakit-primary);
|
||||
}
|
||||
|
||||
.menu-loading {
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.menu-item-loading {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.menu-item-selected {
|
||||
transition: all 0.3s ease;
|
||||
background-color: var(--yakit-primary-5) !important;
|
||||
}
|
||||
|
||||
/* 添加过渡效果 */
|
||||
.ant-menu-item {
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
.ant-menu-item .menu-icon {
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.proxy-switch-container {
|
||||
position: relative;
|
||||
width: 180px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-watermark {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.03;
|
||||
pointer-events: none;
|
||||
object-fit: contain;
|
||||
object-position: right bottom;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* 确保菜单项在水印上层 */
|
||||
.ant-menu-item {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 确保分割线在水印上层 */
|
||||
.ant-menu-item-divider {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Menu} from "antd";
|
||||
import {GlobalOutlined, DisconnectOutlined, SettingOutlined, EditOutlined, PlusOutlined} from "@ant-design/icons";
|
||||
import {ProxyActionType} from '@/types/action';
|
||||
import "./index.css";
|
||||
import type { MenuProps } from 'antd';
|
||||
import type { ProxyConfig } from '@/types/proxy';
|
||||
|
||||
// 添加 YAK 图标 URL 常量
|
||||
const YAK_ICON_URL = chrome.runtime.getURL('/images/yak.svg');
|
||||
|
||||
// 固定的代理模式
|
||||
const FIXED_MODES = [
|
||||
{
|
||||
key: 'direct',
|
||||
name: '[直接连接]',
|
||||
icon: <DisconnectOutlined />,
|
||||
color: '#666',
|
||||
config: {
|
||||
id: 'direct',
|
||||
name: '[直接连接]',
|
||||
proxyType: 'direct',
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'system',
|
||||
name: '[系统代理]',
|
||||
icon: <SettingOutlined />,
|
||||
color: '#666',
|
||||
config: {
|
||||
id: 'system',
|
||||
name: '[系统代理]',
|
||||
proxyType: 'system',
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
interface CustomProxy {
|
||||
key: string;
|
||||
name: string;
|
||||
color: string;
|
||||
config: ProxyConfig;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
interface ProxySwitchProps {
|
||||
proxyConfigs: ProxyConfig[];
|
||||
currentProxy: ProxyConfig | null;
|
||||
onProxyChange: (config: ProxyConfig) => void;
|
||||
}
|
||||
|
||||
export const ProxySwitch: React.FC<ProxySwitchProps> = () => {
|
||||
const [initialized, setInitialized] = useState<boolean>(false);
|
||||
const [currentMode, setCurrentMode] = useState<string>('');
|
||||
const [customProxies, setCustomProxies] = useState<CustomProxy[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
// 修改存储变化监听
|
||||
useEffect(() => {
|
||||
const handleMessage = (message: any) => {
|
||||
if (message.action === 'PROXY_CONFIGS_UPDATED' && message.source !== 'proxy_switch') {
|
||||
loadCustomProxies();
|
||||
}
|
||||
};
|
||||
|
||||
chrome.runtime.onMessage.addListener(handleMessage);
|
||||
return () => {
|
||||
chrome.runtime.onMessage.removeListener(handleMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
// 修改初始化逻辑,避免并行请求
|
||||
await loadProxyStatus();
|
||||
await loadCustomProxies();
|
||||
setInitialized(true);
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const loadProxyStatus = async () => {
|
||||
try {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
action: ProxyActionType.GET_PROXY_STATUS
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
console.log('No response from background script');
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
const activeMode = response.data.mode;
|
||||
if (FIXED_MODES.some(mode => mode.key === activeMode)) {
|
||||
setCurrentMode(activeMode);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading proxy status:', error);
|
||||
setCurrentMode('direct');
|
||||
}
|
||||
};
|
||||
|
||||
const loadCustomProxies = async () => {
|
||||
try {
|
||||
const DB_NAME = 'yaklang_extension';
|
||||
const STORE_NAME = 'proxy_configs';
|
||||
|
||||
// 打开数据库
|
||||
const db = await new Promise<IDBDatabase>((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, 1);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
|
||||
// 从数据库读取代理配置
|
||||
const configs = await new Promise<ProxyConfig[]>((resolve, reject) => {
|
||||
try {
|
||||
const transaction = db.transaction([STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result || []);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理代理配置
|
||||
const proxies = configs
|
||||
.filter((proxy: ProxyConfig) => !FIXED_MODES.some(mode => mode.key === proxy.id))
|
||||
.map((proxy: ProxyConfig): CustomProxy => ({
|
||||
key: proxy.id,
|
||||
name: proxy.name,
|
||||
color: '#1890ff',
|
||||
config: proxy,
|
||||
enabled: proxy.enabled
|
||||
}));
|
||||
setCustomProxies(proxies);
|
||||
|
||||
const enabledProxy = configs.find((proxy: ProxyConfig) => proxy.enabled);
|
||||
if (enabledProxy) {
|
||||
setCurrentMode(enabledProxy.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading custom proxies:', error);
|
||||
setCustomProxies([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModeChange = async (mode: string) => {
|
||||
if (mode === 'setting' || mode === 'add') {
|
||||
if (mode === 'setting') {
|
||||
await chrome.runtime.openOptionsPage?.();
|
||||
}
|
||||
if (mode === 'add') {
|
||||
try {
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true
|
||||
});
|
||||
const optionsUrl = chrome.runtime.getURL('/proxy/options.html');
|
||||
|
||||
if (activeTab?.url === optionsUrl) {
|
||||
chrome.tabs.sendMessage(activeTab.id!, {
|
||||
action: 'TRIGGER_ADD_PROXY'
|
||||
});
|
||||
} else {
|
||||
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);
|
||||
chrome.tabs.sendMessage(tab.id!, {
|
||||
action: 'TRIGGER_ADD_PROXY'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
chrome.tabs.onUpdated.addListener(listener);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get current tab:', error);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const fixedMode = FIXED_MODES.find(fixed => fixed.key === mode);
|
||||
const customProxy = customProxies.find(proxy => proxy.key === mode);
|
||||
|
||||
const config = fixedMode?.config || customProxy?.config;
|
||||
if (!config) {
|
||||
console.error('No config found for mode:', mode);
|
||||
return;
|
||||
}
|
||||
|
||||
// 立即更新UI状态
|
||||
setCurrentMode(mode);
|
||||
if (customProxy) {
|
||||
setCustomProxies(prev => prev.map(p => ({
|
||||
...p,
|
||||
enabled: p.key === mode
|
||||
})));
|
||||
}
|
||||
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
action: ProxyActionType.SET_PROXY_CONFIG,
|
||||
config,
|
||||
// 添加一个标志,表示这是从 ProxySwitch 发起的更改
|
||||
source: 'proxy_switch'
|
||||
});
|
||||
|
||||
if (response?.success === false) {
|
||||
throw new Error(response.error || '设置代理失败');
|
||||
}
|
||||
|
||||
// 不需要重新加载,因为我们已经更新了本地状态
|
||||
} catch (error) {
|
||||
console.error('Error applying proxy config:', error);
|
||||
// 发生错误时才重新加载以确保状态正确
|
||||
await loadCustomProxies();
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems: MenuProps['items'] = [
|
||||
...FIXED_MODES.map(mode => ({
|
||||
key: mode.key,
|
||||
icon: <span className="menu-icon" style={{
|
||||
color: currentMode === mode.key ? 'var(--yakit-primary)' : mode.color
|
||||
}}>{mode.icon}</span>,
|
||||
label: `${mode.name}${currentMode === mode.key ? ' ✅' : ''}`,
|
||||
className: `${currentMode === mode.key ? 'menu-item-selected' : ''} ${isLoading ? 'menu-item-loading' : ''}`,
|
||||
title: mode.name.replace(/[\[\]]/g, '')
|
||||
})),
|
||||
{ type: 'divider' },
|
||||
...customProxies.map(proxy => ({
|
||||
key: proxy.key,
|
||||
icon: <span className="menu-icon" style={{
|
||||
color: currentMode === proxy.key ? 'var(--yakit-primary)' : proxy.color
|
||||
}}>
|
||||
{proxy.config.proxyType === 'pac_script' ? '📜' : <GlobalOutlined />}
|
||||
</span>,
|
||||
label: <span style={{
|
||||
color: currentMode === proxy.key ? 'var(--yakit-primary)' : 'inherit',
|
||||
opacity: isLoading ? 0.7 : 1
|
||||
}}>{proxy.name}{currentMode === proxy.key ? ' ✅' : ''}</span>,
|
||||
className: `${currentMode === proxy.key ? 'menu-item-selected' : ''} ${isLoading ? 'menu-item-loading' : ''}`,
|
||||
title: proxy.config.scheme
|
||||
? `${proxy.config.scheme.toUpperCase()} ${proxy.config.host}:${proxy.config.port}`
|
||||
: `${proxy.config.host}:${proxy.config.port}`
|
||||
})),
|
||||
{
|
||||
key: 'add',
|
||||
icon: <PlusOutlined />,
|
||||
label: '添加代理...',
|
||||
className: 'menu-item-add'
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: 'setting',
|
||||
icon: <EditOutlined />,
|
||||
label: '选项'
|
||||
}
|
||||
];
|
||||
|
||||
return initialized ? (
|
||||
<div className="proxy-switch-container" style={{ position: 'relative' }}>
|
||||
<img
|
||||
src={YAK_ICON_URL}
|
||||
className="panel-watermark"
|
||||
alt=""
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
opacity: 0.1,
|
||||
backgroundColor: '#fff7e6',
|
||||
pointerEvents: 'none',
|
||||
objectFit: 'contain',
|
||||
objectPosition: 'right bottom',
|
||||
zIndex: 0
|
||||
}}
|
||||
/>
|
||||
<Menu
|
||||
items={menuItems}
|
||||
selectedKeys={[currentMode]}
|
||||
onClick={({ key }) => !isLoading && handleModeChange(key)}
|
||||
style={{ width: 180, position: 'relative', zIndex: 1, background: 'transparent' }}
|
||||
className={isLoading ? 'menu-loading' : ''}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ width: 180, height: 100, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
html:root {
|
||||
--yakit-primary: #F28B44
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 180px;
|
||||
height: fit-content;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
// 获取根元素
|
||||
const container = document.getElementById('root');
|
||||
if (!container) throw new Error('Failed to find the root element');
|
||||
|
||||
// 创建根
|
||||
const root = createRoot(container);
|
||||
|
||||
// 渲染应用,移除 StrictMode
|
||||
root.render(<App />);
|
||||
@@ -1,69 +0,0 @@
|
||||
export namespace wsc {
|
||||
export enum ActionType {
|
||||
CONNECT = 'connect',
|
||||
SEND_MESSAGE = 'send_message',
|
||||
|
||||
DISCONNECT = 'disconnect',
|
||||
STATUS = 'status',
|
||||
PROXY_STATUS = 'proxy_status',
|
||||
SET_PROXY = 'set_proxy',
|
||||
CLEAR_PROXY = 'clear_proxy',
|
||||
INJECT_SCRIPT = 'yakit_inject_script',
|
||||
// 用于接收来自content script的消息
|
||||
TO_EXTENSION_PAGE = "yakit_to_extension_page",
|
||||
}
|
||||
|
||||
export function connect(port: number, host?: string) {
|
||||
chrome.runtime.sendMessage({
|
||||
action: ActionType.CONNECT,
|
||||
host: host || '127.0.0.1',
|
||||
port: port,
|
||||
});
|
||||
}
|
||||
|
||||
export function sendMessage(message: any) {
|
||||
chrome.runtime.sendMessage({
|
||||
action: ActionType.SEND_MESSAGE,
|
||||
message: message,
|
||||
});
|
||||
}
|
||||
|
||||
export function disconnect() {
|
||||
chrome.runtime.sendMessage({
|
||||
action: ActionType.DISCONNECT,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateWSCStatus() {
|
||||
chrome.runtime.sendMessage({
|
||||
action: ActionType.STATUS,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateProxyStatus() {
|
||||
chrome.runtime.sendMessage({
|
||||
action: ActionType.PROXY_STATUS,
|
||||
});
|
||||
}
|
||||
|
||||
export function onWSCMessage(onMessage: (message: any) => void) {
|
||||
chrome.runtime.onMessage.addListener(onMessage);
|
||||
}
|
||||
|
||||
export function onProxyStatusMessage(onMessage: (message: { enable: boolean, proxy: string }) => any) {
|
||||
chrome.runtime.onMessage.addListener(onMessage)
|
||||
}
|
||||
|
||||
export function setProxy(scheme: string, host: string, port: number) {
|
||||
chrome.runtime.sendMessage({action: ActionType.SET_PROXY, scheme, host, port})
|
||||
}
|
||||
|
||||
export function clearProxy() {
|
||||
chrome.runtime.sendMessage({action: ActionType.CLEAR_PROXY})
|
||||
}
|
||||
|
||||
// 获取当前tab
|
||||
export async function getTab() {
|
||||
return chrome.tabs.query({active: true, currentWindow: true})
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,131 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
/* 表格样式 */
|
||||
.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;
|
||||
}
|
||||
@@ -1,461 +0,0 @@
|
||||
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, EditOutlined, CheckOutlined } from '@ant-design/icons';
|
||||
import { ProxyConfig } from '@/types/proxy';
|
||||
import './index.css';
|
||||
import punycode from 'punycode';
|
||||
|
||||
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<void>;
|
||||
onClear: (configId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
// 修改 EditFormData 接口
|
||||
interface EditFormData {
|
||||
name: string;
|
||||
proxyType: "direct" | "system" | "fixed_servers" | "pac_script" | "auto_detect";
|
||||
scheme?: "http" | "https" | "socks4" | "socks5";
|
||||
host?: string;
|
||||
port?: number;
|
||||
pacScript?: string;
|
||||
bypassList?: string;
|
||||
matchList?: string; // 仅用于 UI 编辑
|
||||
proxyServer?: string; // 添加 proxyServer 字段,用于 PAC 脚本模式选择代理服务器
|
||||
}
|
||||
|
||||
// 或者更好的方式是创建一个专门的类型
|
||||
type ProxyConfigField = keyof ProxyConfig | 'config';
|
||||
|
||||
export const ProxySettings: React.FC<ProxySettingsProps> = ({
|
||||
proxyConfigs,
|
||||
onAdd,
|
||||
onChange,
|
||||
onDelete,
|
||||
onApply,
|
||||
onClear
|
||||
}) => {
|
||||
const [editingConfig, setEditingConfig] = useState<ProxyConfig | null>(null);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [form] = Form.useForm<EditFormData>();
|
||||
|
||||
// 添加 useEffect 来监听表单值变化
|
||||
useEffect(() => {
|
||||
if (editModalVisible && editingConfig) {
|
||||
form.setFieldsValue({
|
||||
name: editingConfig.name,
|
||||
proxyType: editingConfig.proxyType,
|
||||
scheme: editingConfig.scheme,
|
||||
host: editingConfig.host,
|
||||
port: editingConfig.port,
|
||||
bypassList: editingConfig.bypassList?.join('\n') || '',
|
||||
matchList: editingConfig.matchList?.join('\n') || '',
|
||||
proxyServer: editingConfig.host && editingConfig.port
|
||||
? `${editingConfig.host}:${editingConfig.port}`
|
||||
: undefined
|
||||
});
|
||||
}
|
||||
}, [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 getAvailableProxies = (configs: ProxyConfig[]) => {
|
||||
return configs
|
||||
.filter(config => config.proxyType === 'fixed_servers')
|
||||
.map(config => ({
|
||||
label: `${config.name} (${config.scheme}://${config.host}:${config.port})`,
|
||||
value: `${config.host}:${config.port}`,
|
||||
config
|
||||
}));
|
||||
};
|
||||
|
||||
// 处理编辑保存
|
||||
const handleEditSave = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
if (editingConfig) {
|
||||
if (editingConfig.enabled) {
|
||||
await onClear(editingConfig.id);
|
||||
}
|
||||
|
||||
let updatedConfig: ProxyConfig;
|
||||
|
||||
if (values.proxyType === 'fixed_servers') {
|
||||
// 处理固定代理服务器模式
|
||||
const bypassList = values.bypassList
|
||||
? values.bypassList.split('\n').map(line => line.trim()).filter(line => line.length > 0)
|
||||
: [""];
|
||||
|
||||
updatedConfig = {
|
||||
id: editingConfig.id,
|
||||
name: values.name,
|
||||
enabled: editingConfig.enabled,
|
||||
proxyType: 'fixed_servers',
|
||||
scheme: values.scheme,
|
||||
host: values.host,
|
||||
port: values.port,
|
||||
bypassList,
|
||||
};
|
||||
} else if (values.proxyType === 'pac_script') {
|
||||
const domains = values.matchList
|
||||
? values.matchList.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0)
|
||||
.map(domain => {
|
||||
try {
|
||||
// 如果域名包含非 ASCII 字符,转换为 Punycode
|
||||
if (/[^\x00-\x7F]/.test(domain)) {
|
||||
if (domain.startsWith('*.')) {
|
||||
const suffix = domain.substring(2);
|
||||
return '*.' + suffix.split('.').map(part => {
|
||||
return /[^\x00-\x7F]/.test(part) ? 'xn--' + punycode.encode(part) : part;
|
||||
}).join('.');
|
||||
} else {
|
||||
return domain.split('.').map(part => {
|
||||
return /[^\x00-\x7F]/.test(part) ? 'xn--' + punycode.encode(part) : part;
|
||||
}).join('.');
|
||||
}
|
||||
}
|
||||
return domain;
|
||||
} catch (error) {
|
||||
console.error('Error encoding domain:', domain, error);
|
||||
return domain;
|
||||
}
|
||||
})
|
||||
: [];
|
||||
|
||||
// 从选择的代理服务器中获取配置
|
||||
const [host, port] = values.proxyServer.split(':');
|
||||
|
||||
// 生成 PAC 脚本
|
||||
const pacScriptContent = `
|
||||
function FindProxyForURL(url, host) {
|
||||
// Convert host to lowercase for case-insensitive matching
|
||||
host = host.toLowerCase();
|
||||
|
||||
// Define domain patterns
|
||||
var domains = ${JSON.stringify(domains)};
|
||||
|
||||
// Check each domain pattern
|
||||
for (var i = 0; i < domains.length; i++) {
|
||||
var pattern = domains[i].toLowerCase();
|
||||
|
||||
if (pattern.startsWith('*.')) {
|
||||
var suffix = pattern.substring(2);
|
||||
if (host === suffix || host.endsWith('.' + suffix)) {
|
||||
return 'PROXY ${host}:${port}';
|
||||
}
|
||||
} else if (host === pattern) {
|
||||
return 'PROXY ${host}:${port}';
|
||||
}
|
||||
}
|
||||
|
||||
return 'DIRECT';
|
||||
}`;
|
||||
|
||||
updatedConfig = {
|
||||
id: editingConfig.id,
|
||||
name: values.name,
|
||||
enabled: editingConfig.enabled,
|
||||
proxyType: 'pac_script',
|
||||
mode: 'pac_script',
|
||||
// 保存代理服务器信息
|
||||
host,
|
||||
port: parseInt(port),
|
||||
// 保存匹配域名列表
|
||||
matchList: domains,
|
||||
pacScript: {
|
||||
data: pacScriptContent,
|
||||
mandatory: true
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// 处理其他模式
|
||||
updatedConfig = {
|
||||
id: editingConfig.id,
|
||||
name: values.name,
|
||||
enabled: editingConfig.enabled,
|
||||
proxyType: values.proxyType,
|
||||
bypassList: [], // 其他模式下设置为空数组
|
||||
};
|
||||
}
|
||||
|
||||
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<ProxyConfig> = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (text: string) => text
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'proxyType',
|
||||
key: 'proxyType',
|
||||
render: (text: string) => {
|
||||
const typeMap = {
|
||||
direct: '直接连接',
|
||||
fixed_servers: '代理服务器',
|
||||
pac_script: 'PAC 脚本'
|
||||
};
|
||||
return typeMap[text as keyof typeof typeMap] || text;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '协议',
|
||||
dataIndex: 'scheme',
|
||||
key: 'scheme'
|
||||
},
|
||||
{
|
||||
title: '主机',
|
||||
dataIndex: 'host',
|
||||
key: 'host'
|
||||
},
|
||||
{
|
||||
title: '端口',
|
||||
dataIndex: 'port',
|
||||
key: 'port'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 120,
|
||||
render: (_, record: ProxyConfig) => (
|
||||
<Space size="middle">
|
||||
<CheckOutlined
|
||||
className={`action-icon ${record.enabled ? 'enabled' : ''}`}
|
||||
onClick={async () => {
|
||||
if (record.enabled) {
|
||||
await onClear(record.id);
|
||||
} else {
|
||||
await onApply(record.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<EditOutlined
|
||||
className="action-icon"
|
||||
onClick={() => handleEdit(record)}
|
||||
/>
|
||||
{record.id !== 'direct' && (
|
||||
<DeleteOutlined
|
||||
className="action-icon delete"
|
||||
onClick={() => onDelete(record.id)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const buttonRef = useRef(null);
|
||||
|
||||
// 过滤掉固定模式的代理
|
||||
const filteredProxyConfigs = proxyConfigs.filter(
|
||||
config => !['direct', 'system'].includes(config.id)
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space style={{ marginBottom: 16, justifyContent: 'flex-end', width: '100%' }}>
|
||||
<Button
|
||||
className="add-proxy-btn"
|
||||
onClick={handleAdd}
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
添加代理
|
||||
</Button>
|
||||
</Space>
|
||||
<Table
|
||||
className="proxy-table"
|
||||
dataSource={filteredProxyConfigs}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
bordered={false}
|
||||
size="middle"
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="编辑代理配置"
|
||||
open={editModalVisible}
|
||||
onOk={handleEditSave}
|
||||
onCancel={handleModalClose}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
preserve={false}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="名称"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="proxyType"
|
||||
label="类型"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
// { label: '直接连接', value: 'direct' },
|
||||
{ label: '代理服务器', value: 'fixed_servers' },
|
||||
{ label: 'PAC 脚本', value: 'pac_script' }
|
||||
]}
|
||||
onChange={(value) => {
|
||||
// 当类型改变时,清除相关字段
|
||||
if (value !== 'fixed_servers') {
|
||||
form.setFieldsValue({
|
||||
scheme: undefined,
|
||||
host: undefined,
|
||||
port: undefined
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => prevValues.proxyType !== currentValues.proxyType}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const proxyType = getFieldValue('proxyType');
|
||||
if (proxyType === 'fixed_servers') {
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="scheme"
|
||||
label="协议"
|
||||
rules={[{ required: true, message: '请选择协议' }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'HTTP', value: 'http' },
|
||||
{ label: 'HTTPS', value: 'https' },
|
||||
{ label: 'SOCKS4', value: 'socks4' },
|
||||
{ label: 'SOCKS5', value: 'socks5' }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="host"
|
||||
label="主机"
|
||||
rules={[{ required: true, message: '请输入主机地址' }]}
|
||||
>
|
||||
<Input placeholder="127.0.0.1" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="port"
|
||||
label="端口"
|
||||
rules={[{ required: true, message: '请输入端口号' }]}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={65535}
|
||||
placeholder="8080"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="bypassList"
|
||||
label="不经过代理的地址"
|
||||
help="每行一个地址,支持通配符 *"
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
placeholder={`例如:
|
||||
localhost
|
||||
127.0.0.1
|
||||
*.example.com`}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
} else if (proxyType === 'pac_script') {
|
||||
const availableProxies = getAvailableProxies(proxyConfigs);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="matchList"
|
||||
label="匹配域名"
|
||||
help="每行一个域名,支持通配符 *"
|
||||
rules={[{ required: true, message: '请输入至少一个匹配域名' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
placeholder={`例如:
|
||||
*.example.com
|
||||
google.com
|
||||
github.com`}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="proxyServer"
|
||||
label="选择代理服务器"
|
||||
rules={[{ required: true, message: '请选择代理服务器' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="选择一个代理服务器"
|
||||
options={availableProxies}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,167 +0,0 @@
|
||||
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 messageListener = (message: any) => {
|
||||
if (message.action === 'PROXY_CONFIGS_UPDATED') {
|
||||
loadConfigs();
|
||||
}
|
||||
};
|
||||
|
||||
chrome.runtime.onMessage.addListener(messageListener);
|
||||
|
||||
return () => {
|
||||
chrome.runtime.onMessage.removeListener(messageListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
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);
|
||||
await chrome.runtime.sendMessage({
|
||||
action: 'PROXY_CONFIGS_UPDATED'
|
||||
});
|
||||
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);
|
||||
|
||||
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 response = await chrome.runtime.sendMessage({
|
||||
action: ProxyActionType.SET_PROXY_CONFIG,
|
||||
config: config
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
await chrome.runtime.sendMessage({
|
||||
action: 'PROXY_STATUS_CHANGED'
|
||||
});
|
||||
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
|
||||
};
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
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
|
||||
};
|
||||
};
|
||||
@@ -1,111 +0,0 @@
|
||||
.options-page {
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.options-page .ant-layout-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.proxy-tabs {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.options-page .ant-tabs-nav::before {
|
||||
border-bottom-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.options-page .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||
color: var(--yakit-primary);
|
||||
}
|
||||
|
||||
.options-page .ant-tabs-ink-bar {
|
||||
background: var(--yakit-primary);
|
||||
}
|
||||
|
||||
.options-page .ant-card {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.options-page .ant-card-head {
|
||||
border-bottom: none;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.options-page .ant-card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.options-page .ant-btn-primary {
|
||||
background: var(--yakit-primary);
|
||||
border-color: var(--yakit-primary);
|
||||
}
|
||||
|
||||
.options-page .ant-btn-primary:hover {
|
||||
background: var(--yakit-primary-hover);
|
||||
border-color: var(--yakit-primary-hover);
|
||||
}
|
||||
|
||||
.proxy-action-btn.ant-btn-default {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
border-color: #d9d9d9;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.proxy-action-btn.ant-btn-default:hover {
|
||||
color: var(--yakit-primary);
|
||||
border-color: var(--yakit-primary);
|
||||
}
|
||||
|
||||
.proxy-action-btn.ant-btn-primary {
|
||||
color: #fff;
|
||||
background: var(--yakit-primary);
|
||||
border-color: var(--yakit-primary);
|
||||
}
|
||||
|
||||
.proxy-action-btn.ant-btn-primary.ant-btn-dangerous {
|
||||
background: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.proxy-action-btn.ant-btn-primary.ant-btn-dangerous:hover {
|
||||
background: #ff7875;
|
||||
border-color: #ff7875;
|
||||
}
|
||||
|
||||
.options-page .ant-table-wrapper {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.options-page .ant-input:focus,
|
||||
.options-page .ant-input-focused {
|
||||
border-color: var(--yakit-primary);
|
||||
box-shadow: 0 0 0 2px var(--yakit-primary-5);
|
||||
}
|
||||
|
||||
.options-page .ant-select-focused .ant-select-selector {
|
||||
border-color: var(--yakit-primary) !important;
|
||||
box-shadow: 0 0 0 2px var(--yakit-primary-5) !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
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<void>;
|
||||
onClear: (configId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const OptionsPage: React.FC = () => {
|
||||
const {
|
||||
proxyConfigs,
|
||||
handleAddProxy,
|
||||
handleConfigChange: handleConfigChangeHook,
|
||||
handleDeleteProxy,
|
||||
handleApplyConfig,
|
||||
handleClearProxy
|
||||
} = useProxyConfigs();
|
||||
const { proxyLogs, handleClearLogs } = useProxyLogs();
|
||||
const [proxyConfigsState, setProxyConfigs] = useState<ProxyConfig[]>([]);
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
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 (
|
||||
<Layout style={{ height: '100vh' }}>
|
||||
<Content style={{ padding: '24px' }}>
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: '代理设置',
|
||||
children: (
|
||||
<ProxySettings
|
||||
proxyConfigs={proxyConfigsState}
|
||||
onAdd={handleAdd}
|
||||
onChange={handleConfigChange}
|
||||
onDelete={handleDeleteProxy}
|
||||
onApply={handleApplyConfig}
|
||||
onClear={handleClearProxy}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: '代理日志',
|
||||
children: (
|
||||
<ProxyLogs
|
||||
logs={proxyLogs}
|
||||
onClearLogs={handleClearLogs}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Content>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from 'antd';
|
||||
import { OptionsPage } from './OptionsPage';
|
||||
import '@/styles/global.css';
|
||||
|
||||
const container = document.getElementById('root');
|
||||
if (!container) throw new Error('Failed to find the root element');
|
||||
|
||||
const root = createRoot(container);
|
||||
|
||||
root.render(
|
||||
<App>
|
||||
<OptionsPage />
|
||||
</App>
|
||||
);
|
||||
@@ -1,58 +0,0 @@
|
||||
:root {
|
||||
--yakit-primary: #F28B44;
|
||||
--yakit-primary-hover: #f4a061;
|
||||
--yakit-primary-active: #e87633;
|
||||
--yakit-primary-5: #fff5eb;
|
||||
--yakit-primary-10: rgba(242, 139, 68, 0.1);
|
||||
|
||||
/* 添加其他全局变量 */
|
||||
--border-color: #f0f0f0;
|
||||
--text-color: #333;
|
||||
--icon-color: #666;
|
||||
|
||||
/* 菜单相关变量 */
|
||||
--menu-item-height: 28px;
|
||||
--menu-padding: 4px;
|
||||
--menu-width: 180px;
|
||||
}
|
||||
|
||||
.ant-btn-primary {
|
||||
background-color: var(--yakit-primary) !important;
|
||||
}
|
||||
|
||||
.ant-btn-primary:hover {
|
||||
background-color: var(--yakit-primary-hover) !important;
|
||||
}
|
||||
|
||||
.ant-btn-primary:active {
|
||||
background-color: var(--yakit-primary-active) !important;
|
||||
}
|
||||
|
||||
.ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
|
||||
background-color: var(--yakit-primary-5) !important;
|
||||
}
|
||||
|
||||
.ant-select-focused .ant-select-selector,
|
||||
.ant-input-focused,
|
||||
.ant-input:focus,
|
||||
.ant-input-number-focused,
|
||||
.ant-input-number:focus {
|
||||
border-color: var(--yakit-primary) !important;
|
||||
box-shadow: 0 0 0 2px var(--yakit-primary-10) !important;
|
||||
}
|
||||
|
||||
.ant-btn:not(.ant-btn-primary):hover {
|
||||
color: var(--yakit-primary) !important;
|
||||
border-color: var(--yakit-primary) !important;
|
||||
}
|
||||
|
||||
/* 移除所有滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 确保所有内容都在视口内 */
|
||||
html, body {
|
||||
overflow: hidden;
|
||||
height: fit-content;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// export const ActionType = {
|
||||
// CONNECT: "CONNECT",
|
||||
// SEND_MESSAGE: "SEND_MESSAGE",
|
||||
// DISCONNECT: "DISCONNECT",
|
||||
// SET_PROXY: "SET_PROXY",
|
||||
// CLEAR_PROXY: "CLEAR_PROXY",
|
||||
// PROXY_STATUS: "PROXY_STATUS",
|
||||
// INJECT_SCRIPT: "INJECT_SCRIPT"
|
||||
// } as const;
|
||||
|
||||
// export type ActionType = typeof ActionType[keyof typeof ActionType];
|
||||
|
||||
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",
|
||||
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];
|
||||
Vendored
-10
@@ -1,10 +0,0 @@
|
||||
declare namespace chrome.storage {
|
||||
interface StorageChange {
|
||||
oldValue?: any;
|
||||
newValue?: any;
|
||||
}
|
||||
|
||||
type StorageChanges = {
|
||||
[key: string]: StorageChange;
|
||||
};
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
export interface StorageChange<T = any> {
|
||||
oldValue?: T;
|
||||
newValue?: T;
|
||||
}
|
||||
|
||||
export interface StorageChanges {
|
||||
[key: string]: StorageChange;
|
||||
}
|
||||
|
||||
export interface ProxyConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
scheme: 'http' | 'https' | 'socks5';
|
||||
proxyType: 'fixed_servers';
|
||||
enabled?: boolean;
|
||||
id?: string;
|
||||
name?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
export interface PacScript {
|
||||
data?: string;
|
||||
url?: string;
|
||||
mandatory?: boolean;
|
||||
}
|
||||
|
||||
export interface ProxyConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
proxyType: "direct" | "system" | "fixed_servers" | "pac_script" | "auto_detect";
|
||||
mode?: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
scheme?: "http" | "https" | "socks4" | "socks5";
|
||||
pacScript?: PacScript;
|
||||
bypassList?: string[];
|
||||
matchList?: 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';
|
||||
}
|
||||
Reference in New Issue
Block a user