This commit is contained in:
cnwhy
2024-06-13 18:27:46 +08:00
parent d652eebdf8
commit 2f843862b4
40 changed files with 612 additions and 274 deletions
+2 -1
View File
@@ -23,4 +23,5 @@ dist-ssr
*.sln
*.sw?
public/monaco-editor
public/monaco-editor
src/i18n/*.json
+77
View File
@@ -13,6 +13,8 @@
"axios": "^1.6.0",
"classnames": "^2.5.1",
"events": "^3.3.0",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^8.0.0",
"idb": "^7.1.1",
"idb-open-plus": "^1.0.0",
"jsonc-parser": "^3.2.0",
@@ -21,6 +23,7 @@
"qs": "^6.11.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.2",
"uuid": "^9.0.1"
},
"devDependencies": {
@@ -33,6 +36,7 @@
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
"cpx2": "^7.0.1",
"csv-parse": "^5.5.6",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
@@ -1947,6 +1951,12 @@
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/csv-parse": {
"version": "5.5.6",
"resolved": "https://registry.npmmirror.com/csv-parse/-/csv-parse-5.5.6.tgz",
"integrity": "sha512-uNpm30m/AGSkLxxy7d9yRXpJQFrZzVWLFBkS+6ngPcZkw/5k3L/jjFuj7tVnEpRn+QgmiXr21nDlhCiUK4ij2A==",
"dev": true
},
"node_modules/dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.10.tgz",
@@ -2812,6 +2822,44 @@
"node": ">= 0.4"
}
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/i18next": {
"version": "23.11.5",
"resolved": "https://registry.npmmirror.com/i18next/-/i18next-23.11.5.tgz",
"integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz",
"integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/idb": {
"version": "7.1.1",
"resolved": "https://registry.npmmirror.com/idb/-/idb-7.1.1.tgz",
@@ -6664,6 +6712,27 @@
"react": "^18.2.0"
}
},
"node_modules/react-i18next": {
"version": "14.1.2",
"resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-14.1.2.tgz",
"integrity": "sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 23.2.3",
"react": ">= 16.8.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.2.0.tgz",
@@ -7353,6 +7422,14 @@
"source-map": "^0.7.4"
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
+9 -1
View File
@@ -9,8 +9,12 @@
"analyzer": "tsc && vite build --mode=analyzer",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"init": "npm run init:monaco && npm run init:i18n",
"init:i18n": "node ./script/language.js",
"init:monaco": "cpx ./node_modules/monaco-editor/min/** ./public/monaco-editor/min",
"predev": "npm run init:monaco"
"predev": "npm run init",
"prebuild": "npm run init:i18n",
"preanalyzer": "npm run init:i18n"
},
"dependencies": {
"@ant-design/icons": "^5.2.6",
@@ -18,6 +22,8 @@
"axios": "^1.6.0",
"classnames": "^2.5.1",
"events": "^3.3.0",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^8.0.0",
"idb": "^7.1.1",
"idb-open-plus": "^1.0.0",
"jsonc-parser": "^3.2.0",
@@ -26,6 +32,7 @@
"qs": "^6.11.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.2",
"uuid": "^9.0.1"
},
"devDependencies": {
@@ -38,6 +45,7 @@
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
"cpx2": "^7.0.1",
"csv-parse": "^5.5.6",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
+78
View File
@@ -0,0 +1,78 @@
import fs from "fs";
import { writeFile } from "fs/promises";
import path from "path";
import url from "url";
import { parse } from "csv-parse";
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
const importFile = path.join(__dirname, "../src/i18n/languages.csv");
const exportDir = path.join(__dirname, "../src/i18n");
let languages;
const langMap = new Map();
const init = (arr) => {
const [_key, ...langs] = arr;
languages = langs.map((langName) => {
const lang = {};
langMap.set(langName, lang);
return lang;
});
};
const output = () => {
// 保存为json文件
const langList = [];
const arr = [];
langMap.forEach((json, lang) => {
langList.push({ lang, label: json.language });
arr.push(
writeFile(
path.join(exportDir, `${lang}.json`),
JSON.stringify(json, undefined, 4)
)
);
});
arr.push(
writeFile(
path.join(exportDir, `languages.json`),
JSON.stringify(langList, undefined, 4)
)
);
Promise.all(arr).then(() => console.log("done"));
};
const setValue = (obj, key, value) => {
if (!obj || !key) return;
const keys = key.split(".");
let _obj = obj;
while (keys.length > 1) {
const _key = keys.shift();
if (_obj[_key] === undefined) {
_obj = _obj[_key] = {};
} else if (typeof _obj[_key] === "object") {
_obj = _obj[_key];
} else {
console.error(`Key [${key}] error!`);
}
}
const _key = keys[0];
if (_obj[_key] !== undefined) {
console.error(`Key [${key}] repeat!`);
}
_obj[_key] = value || "";
};
function main(){
const csvp = parse();
csvp.on("end", output);
csvp.on("data", function (chunk) {
// 解析language.csv
if (!languages) return init(chunk); // 第一行为表头
const [key, ...values] = chunk;
values.forEach((value, index) => {
setValue(languages[index], key, value);
});
});
fs.createReadStream(importFile).pipe(csvp);
}
main();
+13 -1
View File
@@ -20,9 +20,12 @@ import "./App.css";
import { configEvent } from "./uitls/events";
import zhCN from "antd/locale/zh_CN";
import enGB from "antd/locale/en_GB";
import { ServerComm } from "./api/local";
import Ctx from "./uitls/ctx";
import { useIsDark } from "./uitls/useTheme";
import './uitls/i18n.ts';
import { useTranslation } from "react-i18next";
const Manage = React.lazy(() => import("./Pages/Manage"));
@@ -45,6 +48,7 @@ const AntdGlobal: React.FC<{ children: React.ReactNode }> = (props) => {
function App() {
const info = useInfo();
const { t, i18n } = useTranslation();
// const [gostConfig, setGostConfig] = useState<any>(null);
// const [localConfig, setLocalConfig] = useState<any>(null);
const gostConfig = useServerConfig();
@@ -53,10 +57,12 @@ function App() {
// const [userTheme, setUserTheme] = useState<any>(null); // 用户主题
const [serverLoading, setServerLoading] = useState<any>(false);
const [localLoading, setLocalLoading] = useState<any>(false);
const [locale, setLocale] = useState<any>(zhCN);
const isLoading = useMemo(
() => serverLoading || localLoading,
[serverLoading, localLoading]
);
const slef = useRef({
update: async () => {
try {
@@ -137,6 +143,12 @@ function App() {
(window as any)?.monaco?.editor.setTheme("vs");
}
}, [isDark]);
useEffect(() => {
const lang = i18n.resolvedLanguage;
setLocale(lang === 'zh' ? zhCN : enGB);
},[i18n.resolvedLanguage])
return (
<Ctx.Provider
value={{
@@ -147,7 +159,7 @@ function App() {
>
<ConfigProvider
theme={{ algorithm: isDark ? theme.darkAlgorithm : undefined }}
locale={zhCN}
locale={locale}
>
<AntdGlobal>
<React.Suspense fallback="loading...">
+17 -15
View File
@@ -1,12 +1,14 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React from "react";
import { GlobalOutlined, LockOutlined, UserOutlined } from "@ant-design/icons";
import { Button, Checkbox, Form, Input } from "antd";
import { Button, Checkbox, Form, Input, Space } from "antd";
import { login } from "../uitls/server";
import LocalServers from "../components/LocalServers";
import { ThemeButton } from "../components/Theme";
import { useTranslation, Trans } from 'react-i18next';
import { LanguageButton } from "../components/Language";
const Home: React.FC = () => {
const { t, i18n } = useTranslation();
return (
<>
<Form
@@ -36,30 +38,27 @@ const Home: React.FC = () => {
);
}}
>
<h1>GOST API Manage</h1>
<h2>API服务</h2>
<h1>{t('base.title')}</h1>
<h2>{t('base.description')}</h2>
<Form.Item
name="baseURL"
rules={[
{
required: true,
message: "请输入API地址",
message: t('msg.baseURL.required'),
},
{
validator(rule, value, callback) {
if (value === "http://") {
callback("请输入API地址");
callback(t('msg.baseURL.required'));
}
callback();
},
},
// {
// type:'url'
// }
]}
>
<Input
placeholder="API baseURL"
placeholder={t('placeholder.baseURL')}
prefix={<GlobalOutlined className={"prefixIcon"} />}
></Input>
</Form.Item>
@@ -76,18 +75,21 @@ const Home: React.FC = () => {
></Input.Password>
</Form.Item>
<Form.Item name="save" valuePropName="checked">
<Checkbox></Checkbox>
<Checkbox>{t('base.form.local')}</Checkbox>
</Form.Item>
<Form.Item noStyle style={{ marginBottom: "1em" }}>
<Button block type="primary" htmlType="submit">
{t('base.cmd.connect')}
</Button>
</Form.Item>
<LocalServers></LocalServers>
</Form>
<ThemeButton
style={{ position: "absolute", top: "1em", right: "1em" }}
></ThemeButton>
<div style={{ position: "absolute", top: "1em", right: "1em" }}>
<Space>
<ThemeButton />
<LanguageButton />
</Space>
</div>
</>
);
};
+30 -17
View File
@@ -36,6 +36,8 @@ import {
SettingOutlined,
} from "@ant-design/icons";
import { ThemeButton } from "../components/Theme";
import { LanguageButton } from "../components/Language";
import { useTranslation } from "react-i18next";
const colSpan = {
xs: 24,
@@ -52,6 +54,7 @@ const colSpan1 = {
const Manage = () => {
const info = useInfo()!;
const { t } = useTranslation();
const gostConfig = useServerConfig();
const [show, setShow] = useState(false);
const [loading, setLoading] = useState(false);
@@ -128,7 +131,7 @@ const Manage = () => {
if (locals.length) {
items.push({
key: "2",
label: " 切换 ",
label: t("manage.cmd.switch"),
children: locals,
});
items.push({
@@ -137,13 +140,13 @@ const Manage = () => {
}
items.push({
key: "new",
label: "打开新链接",
label: t("manage.cmd.new"),
onClick: () => {
window.open(location.href, undefined, "noopener");
},
});
return items;
}, [locals]);
}, [locals, t]);
return (
<Layout style={{ height: "100vh", overflow: "hidden" }}>
@@ -162,7 +165,7 @@ const Manage = () => {
useServerConfig.set((await API.getConfig()) as any);
}}
>
{t("manage.cmd.reload")}
</Button>
</Col>
<Col>{info.addr}</Col>
@@ -174,7 +177,7 @@ const Manage = () => {
loading={loading}
onClick={() => ref.current?.onSave?.()}
>
{t("manage.cmd.save")}
</Button>
<Button
icon={<SettingOutlined />}
@@ -188,15 +191,13 @@ const Manage = () => {
download(jsonFormat(gostConfig!), "gost.json");
}}
>
{t("manage.cmd.download")}
</Button>
<Dropdown.Button menu={{ items }} onClick={logout}>
退
{/* <Button type="link" onClick={logout}>
切换连接
</Button> */}
{t("manage.cmd.logout")}
</Dropdown.Button>
<ThemeButton size="small"></ThemeButton>
<ThemeButton />
<LanguageButton />
</Space>
</Col>
</Row>
@@ -218,17 +219,29 @@ const Manage = () => {
}
}}
>
<Form.Item name="autoSave" label="自动保存" valuePropName="checked">
<Form.Item
name="autoSave"
label={t("manage.label.autoSave")}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item name="saveFormat" label="格式" initialValue={"json"}>
<Form.Item
name="saveFormat"
label={t("manage.label.format")}
initialValue={"json"}
>
<Radio.Group optionType="button" buttonStyle="solid">
<Radio value="json">json</Radio>
<Radio value="yaml">yaml</Radio>
</Radio.Group>
</Form.Item>
<Form.Item name="savePath" label="保存路径" trigger="onChange">
<Input placeholder="指定保存,默认保存到上下文目录" />
<Form.Item
name="savePath"
label={t("manage.label.path")}
trigger="onChange"
>
<Input placeholder={t("placeholder.savePath")} />
</Form.Item>
</Form>
</Modal>
@@ -276,7 +289,7 @@ const Manage = () => {
<ListCard module="observer" />
</Col>
<Col span={24}>
<ProCard boxShadow title="限速限流">
<ProCard boxShadow title={t("manage.label.limit")}>
<Row gutter={[16, 16]}>
<Col {...colSpan1}>
<ListCard module="limiter" bordered boxShadow={false} />
@@ -293,7 +306,7 @@ const Manage = () => {
<Col span={24}>
<ProCard
boxShadow
title="All Config JSON"
title={t("manage.label.allConfig")}
styles={{ body: { padding: 20 } }}
>
<CodeEditor
+5 -3
View File
@@ -7,6 +7,7 @@ import { CardCtx } from "../../uitls/ctx";
import { jsonParse } from "../../uitls";
import { GostCommit } from "../../api/local";
import { UseTemplates } from "../ListCard/hooks";
import { useTranslation } from "react-i18next";
type Props = {
keyName: string;
@@ -14,20 +15,21 @@ type Props = {
};
const AddButton: React.FC<Props> = (props) => {
const { t } = useTranslation();
const { keyName, title } = props;
const { comm } = useContext(CardCtx);
const templates = UseTemplates({ name: keyName! });
return (
<JsonForm
title={`添加 ${title || ""}`}
title={t("title.add", { name: title })}
templates={templates}
trigger={<Button icon={<PlusOutlined />} size="small" />}
onFinish={async (values: any) => {
const { value } = values;
const json = jsonParse(value);
await comm!.addValue(json);
return true
return true;
}}
></JsonForm>
);
+33 -26
View File
@@ -1,6 +1,13 @@
import React, { useRef } from "react";
import ReactDOM from "react-dom/client";
import { Button, Dropdown, Form, FormInstance, Space, ConfigProvider } from "antd";
import {
Button,
Dropdown,
Form,
FormInstance,
Space,
ConfigProvider,
} from "antd";
import { DownOutlined } from "@ant-design/icons";
import { jsonFormat, jsonStringFormat, jsonParse, jsonEdit } from "../../uitls";
import { Template } from "../../templates";
@@ -10,6 +17,8 @@ import ModalForm, { ModalFormProps } from "../ModalForm";
import { CodeEditor } from "../../uitls/useMonacoEdit";
import { globalConfig } from "antd/es/config-provider";
import { HookAPI } from "antd/es/modal/useModal";
import { useTranslation } from "react-i18next";
import { getLabel } from "../../uitls/i18n";
const template2menu: any = (template: Template) => {
const { children, ...other } = template;
@@ -49,12 +58,15 @@ type JsonFromProps = ModalFormProps & {
const JsonForm: React.FC<JsonFromProps> = (props) => {
const { templates, ...other } = props;
const { t } = useTranslation();
const formRef = useRef<FormInstance<any>>();
const templateHandle = (json: string) => {
let value = json;
if (props.initialValues?.value) {
const _old = jsonParse(props.initialValues.value);
value = _old.name ? jsonEdit(json, [{path:'name', value: _old.name}]) : json;
value = _old.name
? jsonEdit(json, [{ path: "name", value: _old.name }])
: json;
}
value = jsonStringFormat(value);
formRef.current?.setFieldValue("value", value);
@@ -70,8 +82,10 @@ const JsonForm: React.FC<JsonFromProps> = (props) => {
children: children.map(template2menu),
};
} else if (json) {
const { label } = other;
return {
...other,
label: getLabel(label),
title: cli,
onClick: () => templateHandle(json),
};
@@ -91,8 +105,11 @@ const JsonForm: React.FC<JsonFromProps> = (props) => {
}}
>
{hasTemplate ? (
<Space size={"small"} style={{ marginBottom: 5 }}>
<span>:</span>
<Space
size={"small"}
style={{ marginBottom: 5, width: "100%", overflow: "auto" }}
>
<span>{t("terms.template")}:</span>
{templates.map((template, index) => {
if (template.children?.length) {
const menu = {
@@ -115,7 +132,7 @@ const JsonForm: React.FC<JsonFromProps> = (props) => {
onClick={() => templateHandle(template.json)}
menu={menu}
>
{template.label}
{getLabel(template.label)}
</Dropdown.Button>
);
} else {
@@ -123,7 +140,7 @@ const JsonForm: React.FC<JsonFromProps> = (props) => {
<Dropdown key={index} menu={menu}>
<Button size="small">
<Space>
{template.label}
{getLabel(template.label)}
<DownOutlined />
</Space>
</Button>
@@ -138,7 +155,7 @@ const JsonForm: React.FC<JsonFromProps> = (props) => {
title={template.cli}
onClick={() => templateHandle(template.json)}
>
<Space>{template.label}</Space>
<Space>{getLabel(template.label)}</Space>
</Button>
);
} else {
@@ -147,25 +164,10 @@ const JsonForm: React.FC<JsonFromProps> = (props) => {
})}
</Space>
) : null}
{/* <ProFormTextArea
fieldProps={{ rows: 8 }}
name="value"
rules={[
{ required: true, message: "不能为空" },
{
validator: (rule, value) => {
return new Promise((resolve, reject) => {
if (value) jsonParse(value);
resolve(null);
});
},
},
]}
></ProFormTextArea> */}
<Form.Item
name="value"
rules={[
{ required: true, message: "不能为空" },
{ required: true, message: t("msg.require") },
{
validator: (rule, value) => {
return new Promise((resolve, reject) => {
@@ -175,7 +177,7 @@ const JsonForm: React.FC<JsonFromProps> = (props) => {
resolve(null);
}).catch((err) => {
console.error(err);
throw new Error("json 格式错误");
throw new Error(t("msg.formatError", { name: "JSON" }));
});
},
},
@@ -187,7 +189,8 @@ const JsonForm: React.FC<JsonFromProps> = (props) => {
language="json"
options={{
minimap: { enabled: false },
}} />
}}
/>
{/* <MonacoEditor
className={"g-boder"}
height={300}
@@ -211,7 +214,11 @@ export const showJsonForm = (props: JsonFromProps) => {
const config = globalConfig();
timeoutId = setTimeout(() => {
document.body.append(container);
root.render(<ConfigProvider theme={config.getTheme()}><JsonForm {...props} /></ConfigProvider>);
root.render(
<ConfigProvider theme={config.getTheme()}>
<JsonForm {...props} />
</ConfigProvider>
);
}, 100);
}
+17
View File
@@ -0,0 +1,17 @@
import { Button, ButtonProps } from "antd";
import { useTranslation } from "react-i18next";
export const LanguageButton = (props: ButtonProps) => {
const { i18n } = useTranslation();
return (
<Button
shape="circle"
{...props}
onClick={() => {
i18n.changeLanguage(i18n.resolvedLanguage === "en" ? "zh" : "en");
}}
>
{i18n.resolvedLanguage === "en" ? "中" : "En"}
</Button>
);
};
+20 -13
View File
@@ -21,6 +21,7 @@ import { GostCommit } from "../../api/local";
import Ctx, { CardCtx } from "../../uitls/ctx";
import { useListData1, UseTemplates } from "../ListCard/hooks";
import { configEvent } from "../../uitls/events";
import { useTranslation } from "react-i18next";
export type PublicListProps = {
name: string;
@@ -56,6 +57,7 @@ const PublicList: React.FC<PublicListProps> = (props) => {
renderConfig = defaultRenderConfig,
filter = defFilter,
} = props;
const { t } = useTranslation();
const { localList, comm } = useContext(CardCtx);
const { gostConfig, localConfig } = useContext(Ctx);
const { dataList, dataSource } = useListData1({
@@ -116,7 +118,7 @@ const PublicList: React.FC<PublicListProps> = (props) => {
});
};
showJsonForm({
title: "修改",
title: t("base.cmd.edit"),
initialValues: { value: jsonFormatValue(v) },
onFinish: async (values: any) => {
onup(jsonParse(values.value));
@@ -142,9 +144,14 @@ const PublicList: React.FC<PublicListProps> = (props) => {
size="small"
dataSource={showList}
columns={[
{ title: rowKey, dataIndex: rowKey, ellipsis: true, width: 100 },
{
title: "详情",
title: t("base.form.name"),
dataIndex: rowKey,
ellipsis: true,
width: 100,
},
{
title: t("base.form.details"),
ellipsis: true,
render: (value, record, index) => {
const isEnable = dataList.includes(record);
@@ -165,7 +172,7 @@ const PublicList: React.FC<PublicListProps> = (props) => {
},
},
{
title: "操作",
title: t("base.cmd.controls"),
width: localApi ? 120 : 90,
align: "right",
dataIndex: rowKey,
@@ -179,7 +186,7 @@ const PublicList: React.FC<PublicListProps> = (props) => {
{localApi ? (
isEnable ? (
<Button
title="点击禁用"
title={t("base.cmd.disable")}
icon={
<CheckCircleOutlined
style={{ color: green.primary }}
@@ -195,7 +202,7 @@ const PublicList: React.FC<PublicListProps> = (props) => {
</Button>
) : (
<Button
title="点击启用"
title={t("base.cmd.enabled")}
type="link"
icon={<StopOutlined style={{ color: red.primary }} />}
size={"small"}
@@ -208,13 +215,13 @@ const PublicList: React.FC<PublicListProps> = (props) => {
)
) : null}
<Button
title="修改"
title={t("base.cmd.edit")}
icon={<EditOutlined />}
type="link"
size={"small"}
onClick={() => {
showJsonForm({
title: `修改 ${value || ""}`,
title: t("title.edit", {name: value || ''}),
templates: templates,
initialValues: { value: jsonFormatValue(content) },
onFinish: async (values: any) => {
@@ -234,13 +241,13 @@ const PublicList: React.FC<PublicListProps> = (props) => {
}}
/>
<Button
title="复制"
title={t("base.cmd.copy")}
icon={<CopyOutlined />}
type="link"
size={"small"}
onClick={() => {
showJsonForm({
title: `复制自 ${value || ""}`,
title: t("title.copied", {name: value || ''}),
templates: templates,
initialValues: { value: jsonFormatValue(content) },
onFinish: async (values: any) => {
@@ -253,8 +260,8 @@ const PublicList: React.FC<PublicListProps> = (props) => {
}}
/>
<Popconfirm
title="警告"
description="确定要删除吗?"
title={t("text.warn")}
description={t("text.deleteing")}
onConfirm={() => {
if (isEnable) {
deleteValue(record);
@@ -264,7 +271,7 @@ const PublicList: React.FC<PublicListProps> = (props) => {
}}
>
<Button
title="删除"
title={t("base.cmd.del")}
icon={<DeleteOutlined />}
type="link"
size={"small"}
+14 -5
View File
@@ -11,6 +11,7 @@ import { getModule } from "../../api/modules";
import { configEvent } from "../../uitls/events";
import { CloseOutlined } from "@ant-design/icons";
import classnames from "classnames";
import { useTranslation } from "react-i18next";
export type ListCardProps = {
module?: string;
@@ -36,6 +37,7 @@ export const ProCard = (props: CardProps & { boxShadow?: boolean }) => {
};
const ListCard: React.FC<ListCardProps> = (props) => {
const { t } = useTranslation();
const {
title,
subTitle,
@@ -49,7 +51,14 @@ const ListCard: React.FC<ListCardProps> = (props) => {
localApi,
filter,
} = useMemo(() => {
return { ...getModule(props.module || "")!, ...props };
return {
...getModule(props.module || "")!,
...{
title: t(`modules.${props.module}.title`),
subTitle: t(`modules.${props.module}.subTitle`),
},
...props,
};
}, [props]);
const [keyword, setKeyword] = useState("");
const _prop = {
@@ -95,8 +104,8 @@ const ListCard: React.FC<ListCardProps> = (props) => {
if (hasName()) {
const confirmed = await new Promise((resolve, reject) => {
Modal.confirm({
title: "name无效",
content: "是否自动分配name",
title: t("msg.invalidName"),
content: t("msg.autofixName"),
zIndex: 2000,
onOk: () => resolve(true),
onCancel: () => resolve(false),
@@ -112,8 +121,8 @@ const ListCard: React.FC<ListCardProps> = (props) => {
await addValue({ ...json, name: addName });
json.name !== addName &&
notification.info({
description: `新分配 name 为 "${addName}"`,
message: "自动修正提醒",
description: t("msg.fixName", { name: addName }),
message: t("msg.autofix"),
});
update && configEvent.emit("apiUpdate", {});
},
+4 -1
View File
@@ -2,7 +2,10 @@ import React, { useCallback, useEffect, useState } from "react";
import { Col, Flex, Row, Space } from "antd";
import { DeleteOutlined } from "@ant-design/icons";
import { GostApiConfig, getLocalServers, deleteLocal } from "../uitls/server";
import { useTranslation } from "react-i18next";
const LocalServers = () => {
const { t } = useTranslation();
const [list, setList] = useState<GostApiConfig[]>();
// const [local, setLocal] = useState<Record<string, GostApiConfig>>();
@@ -26,7 +29,7 @@ const LocalServers = () => {
<>
{list && list?.length > 0 ? (
<Space direction="vertical" style={{ display: "flex" }}>
<div></div>
<div>{t('home.quickConnect')}</div>
<Row gutter={10}>
{list.map((item) => {
return (
+1 -1
View File
@@ -10,9 +10,9 @@ export const ThemeButton = (props: ButtonProps) => {
const Icon = isDark ? <MoonOutlined /> : <SunOutlined />;
return (
<Button
{...props}
shape="circle"
icon={Icon}
{...props}
onClick={() => {
const _settings = { ...settings };
_settings.theme = isDark ? "light" : "dark";
+4 -2
View File
@@ -6,6 +6,7 @@ import { showJsonForm } from "../Forms/Json";
import { jsonFormatValue, jsonParse } from "../../uitls";
import { useContext } from "react";
import { UpdateCtx } from "../List/Public";
import { useTranslation } from "react-i18next";
export default function viewChain(this: Partial<Config>, chain: ChainConfig) {
const { hops } = chain;
@@ -14,6 +15,7 @@ export default function viewChain(this: Partial<Config>, chain: ChainConfig) {
export function ViewHops(props: { hops: HopConfig[]; root: any }) {
const { update } = useContext(UpdateCtx);
const { t } = useTranslation();
return (
<Space size={5}>
{props.hops
@@ -30,10 +32,10 @@ export function ViewHops(props: { hops: HopConfig[]; root: any }) {
bordered={false}
color="blue"
className="editor-json"
title="双击修改"
title={t("text.doubleClickEdit")}
onDoubleClick={() => {
showJsonForm({
title: "修改",
title: t('base.cmd.edit'),
initialValues: { value: jsonFormatValue(hop) },
onFinish: async (values: any) => {
props.hops[i] = jsonParse(values.value);
+4 -2
View File
@@ -6,6 +6,7 @@ import { Space, Tag, Tooltip } from "antd";
import { showJsonForm } from "../Forms/Json";
import { jsonFormatValue, jsonParse } from "../../uitls";
import { UpdateCtx } from "../List/Public";
import { useTranslation } from "react-i18next";
export function viewNode(this: Partial<Config>, data: NodeConfig) {
const {
@@ -55,6 +56,7 @@ export const ViewNode = ({
isLink?: boolean;
upjson?: (newNode: NodeConfig) => void;
}) => {
const { t } = useTranslation();
const { name } = node;
const { update } = useContext(UpdateCtx);
return (
@@ -63,11 +65,11 @@ export const ViewNode = ({
bordered={false}
color="green"
className="editor-json"
title="双击修改"
title={t("text.doubleClickEdit")}
onDoubleClick={() => {
if (!upjson) return;
showJsonForm({
title: "修改",
title: t('base.cmd.edit'),
initialValues: { value: jsonFormatValue(node) },
onFinish: async (values: any) => {
upjson(jsonParse(values.value));
+6 -3
View File
@@ -12,6 +12,7 @@ import { UpdateCtx } from "../List/Public";
import { Space, Tag, Tooltip } from "antd";
import { ViewNode } from "./node";
import { ViewChain } from "./chain";
import { useTranslation } from "react-i18next";
export default function viewService(
this: Partial<Config>,
@@ -116,6 +117,8 @@ export const ViewByChain = (props: { chainName: string }) => {
export const ViewService: React.FC<ServiceConfig> = (service) => {
const { name, addr, handler, listener, forwarder } = service;
const { t } = useTranslation();
const type = useMemo(() => {
const hType = handler?.type;
const lType = listener?.type;
@@ -132,7 +135,7 @@ export const ViewService: React.FC<ServiceConfig> = (service) => {
color="#c7e7ff"
arrow={false}
>
<Tag></Tag>
<Tag>{t("modules.chain.subTitle")}</Tag>
</Tooltip>
)}
{listener.chain && (
@@ -141,7 +144,7 @@ export const ViewService: React.FC<ServiceConfig> = (service) => {
color="#fff0d7"
arrow={false}
>
<Tag color="orange"></Tag>
<Tag color="orange">{t("terms.key0")}</Tag>
</Tooltip>
)}
{forwarder && (
@@ -150,7 +153,7 @@ export const ViewService: React.FC<ServiceConfig> = (service) => {
color="#c7e7ff"
arrow={false}
>
<Tag>/ </Tag>
<Tag>{t("terms.forwarder")}</Tag>
</Tooltip>
)}
</Space>
+80
View File
@@ -0,0 +1,80 @@
key,zh,en
language,简体中文,English
base.title,GOST API Manage,GOST API Manage
base.description,连接API服务,Connecting to API Services
base.form.username,用户名,Username
base.form.password,密码,Password
base.form.local,保存到本地,Save to local
base.form.name,名称,Name
base.form.details,详情,Details
base.cmd.connect,连接,Connect
base.cmd.controls,操作,Operation
base.cmd.enabled,启用,Enabled
base.cmd.disable,禁用,Disable
base.cmd.copy,复制,Copy
base.cmd.edit,修改,Edit
base.cmd.del,删除,Delete
text.warn,警告,Warning
text.deleteing,确定要删除吗?,Are you sure you want to delete?
text.doubleClickEdit,双击修改,Double click to modify
modules.admission.title,准入控制器(Admission),Admission
modules.admission.subTitle,准入控制器,admission
modules.auther.title,认证器(Auther),Auther
modules.auther.subTitle,认证器,auther
modules.bypass.title,分流器(Bypass),Bypass
modules.bypass.subTitle,分流器,bypass
modules.chain.title,转发链(Chain),Chain
modules.chain.subTitle,转发链,chain
modules.climiter.title,并发连接数限制,climiter
modules.climiter.subTitle,,climiter
modules.limiter.title,流量速率限制,limiter
modules.limiter.subTitle,,limiter
modules.rlimiter.title,请求速率限制,rlimiter
modules.rlimiter.subTitle,,rlimiter
modules.hop.title,跳跃点(Hop),Hop
modules.hop.subTitle,跳跃点,hop
modules.host.title,主机映射器(Hosts),Hosts
modules.host.subTitle,主机映射器,hosts
modules.ingress.title,Ingress,Ingress
modules.ingress.subTitle,Ingress,Ingress
modules.resolver.title,域名解析器(Resolver),Resolver
modules.resolver.subTitle,域名解析器,resolver
modules.service.title,服务(Service),Service
modules.service.subTitle,服务,service
modules.sd.title,服务发现(SD),SD
modules.sd.subTitle,服务发现,SD
modules.observer.title,观测器(Observer),Observer
modules.observer.subTitle,观测器,observer
msg.baseURL.required,请输入API地址,BaseURL cannot be empty
msg.require,不能为空!,Can not be empty!
msg.success,操作成功!,Success!
msg.unknown,出现未知错误!,Unknown error!
msg.connectionFailed,连接失败!,Connection failed!
msg.wordWrap,自动换行,Word Wrap
msg.invalidName,name无效,Invalid name
msg.autofixName,是否自动分配name,Whether to automatically assign a name?
msg.autofix,自动修正提醒,Autofix Reminder
msg.fixName,新分配 name 为 {{name}},Reassign name to {{name}}
msg.formatError,{{name}}格式错误!,{{name}}Format error!
placeholder.baseURL,API baseURL,API baseURL
placeholder.savePath,默认保存到上下文目录,"By default, it is saved to the context directory."
home.quickConnect,快速连接,Quick Connect
manage.cmd.reload,刷新配置,Reload Config
manage.cmd.save,保存到服务器,Save to server
manage.cmd.download,下载当前配置,Download Config
manage.cmd.logout,退出,Logout
manage.cmd.switch,切换服务,Switch Services
manage.cmd.new,连接新服务,Connect to new Services
manage.label.autoSave,自动保存,Autosave
manage.label.format,格式,Format
manage.label.path,保存路径,Save path
manage.label.limit,限速限流,Speed and current limit
manage.label.allConfig,完整配置,All Config JSON
title.edit,修改 {{name}},Edit {{name}}
title.add,添加 {{name}},Add {{name}}
title.copied,复制自 {{name}},Copied from {{name}}
template.default,默认,Default
template.inline,内联,Inline
terms.template,模板,Template
terms.forwarder,转发/反代,Forwarder
terms.key0,远程端口转发,Remote Port Forwarding
1 key zh en
2 language 简体中文 English
3 base.title GOST API Manage GOST API Manage
4 base.description 连接API服务 Connecting to API Services
5 base.form.username 用户名 Username
6 base.form.password 密码 Password
7 base.form.local 保存到本地 Save to local
8 base.form.name 名称 Name
9 base.form.details 详情 Details
10 base.cmd.connect 连接 Connect
11 base.cmd.controls 操作 Operation
12 base.cmd.enabled 启用 Enabled
13 base.cmd.disable 禁用 Disable
14 base.cmd.copy 复制 Copy
15 base.cmd.edit 修改 Edit
16 base.cmd.del 删除 Delete
17 text.warn 警告 Warning
18 text.deleteing 确定要删除吗? Are you sure you want to delete?
19 text.doubleClickEdit 双击修改 Double click to modify
20 modules.admission.title 准入控制器(Admission) Admission
21 modules.admission.subTitle 准入控制器 admission
22 modules.auther.title 认证器(Auther) Auther
23 modules.auther.subTitle 认证器 auther
24 modules.bypass.title 分流器(Bypass) Bypass
25 modules.bypass.subTitle 分流器 bypass
26 modules.chain.title 转发链(Chain) Chain
27 modules.chain.subTitle 转发链 chain
28 modules.climiter.title 并发连接数限制 climiter
29 modules.climiter.subTitle climiter
30 modules.limiter.title 流量速率限制 limiter
31 modules.limiter.subTitle limiter
32 modules.rlimiter.title 请求速率限制 rlimiter
33 modules.rlimiter.subTitle rlimiter
34 modules.hop.title 跳跃点(Hop) Hop
35 modules.hop.subTitle 跳跃点 hop
36 modules.host.title 主机映射器(Hosts) Hosts
37 modules.host.subTitle 主机映射器 hosts
38 modules.ingress.title Ingress Ingress
39 modules.ingress.subTitle Ingress Ingress
40 modules.resolver.title 域名解析器(Resolver) Resolver
41 modules.resolver.subTitle 域名解析器 resolver
42 modules.service.title 服务(Service) Service
43 modules.service.subTitle 服务 service
44 modules.sd.title 服务发现(SD) SD
45 modules.sd.subTitle 服务发现 SD
46 modules.observer.title 观测器(Observer) Observer
47 modules.observer.subTitle 观测器 observer
48 msg.baseURL.required 请输入API地址 BaseURL cannot be empty
49 msg.require 不能为空! Can not be empty!
50 msg.success 操作成功! Success!
51 msg.unknown 出现未知错误! Unknown error!
52 msg.connectionFailed 连接失败! Connection failed!
53 msg.wordWrap 自动换行 Word Wrap
54 msg.invalidName name无效 Invalid name
55 msg.autofixName 是否自动分配name? Whether to automatically assign a name?
56 msg.autofix 自动修正提醒 Autofix Reminder
57 msg.fixName 新分配 name 为 {{name}} Reassign name to {{name}}
58 msg.formatError {{name}}格式错误! {{name}}Format error!
59 placeholder.baseURL API baseURL API baseURL
60 placeholder.savePath 默认保存到上下文目录 By default, it is saved to the context directory.
61 home.quickConnect 快速连接 Quick Connect
62 manage.cmd.reload 刷新配置 Reload Config
63 manage.cmd.save 保存到服务器 Save to server
64 manage.cmd.download 下载当前配置 Download Config
65 manage.cmd.logout 退出 Logout
66 manage.cmd.switch 切换服务 Switch Services
67 manage.cmd.new 连接新服务 Connect to new Services
68 manage.label.autoSave 自动保存 Autosave
69 manage.label.format 格式 Format
70 manage.label.path 保存路径 Save path
71 manage.label.limit 限速限流 Speed and current limit
72 manage.label.allConfig 完整配置 All Config JSON
73 title.edit 修改 {{name}} Edit {{name}}
74 title.add 添加 {{name}} Add {{name}}
75 title.copied 复制自 {{name}} Copied from {{name}}
76 template.default 默认 Default
77 template.inline 内联 Inline
78 terms.template 模板 Template
79 terms.forwarder 转发/反代 Forwarder
80 terms.key0 远程端口转发 Remote Port Forwarding
+4 -5
View File
@@ -1,15 +1,14 @@
import { getByName } from "./default";
import { getOtherAll } from "./otherOrigin";
import { Template } from "./type";
const {def, docUrl, _docUrl }= getByName("admissions");
const def = getByName("admissions");
export default [
def
? def
: {
label: "内联",
json: `
// https://gost.run/concepts/admission
label: { zh: "内联", en: "Inline" },
json: _docUrl + `
{
"name": "admission-0",
"matchers": [
@@ -19,5 +18,5 @@ export default [
]
}`,
},
...getOtherAll("admission", "https://gost.run/concepts/admission"),
...getOtherAll("admission", docUrl),
] as Template[];
+4 -22
View File
@@ -2,27 +2,9 @@ import { getByName } from "./default";
import { getOtherAll } from "./otherOrigin";
import { Template } from "./type";
const def = getByName("authers");
const {def, docUrl }= getByName("authers");
export default [
def
? def
: {
label: "内联",
json: `
// https://gost.run/concepts/auth
{
"name": "auther-0",
"auths": [
{
"username": "user1",
"password": "pass1"
},
{
"username": "user2",
"password": "pass2"
}
]
}`,
},
...getOtherAll("auther", "https://gost.run/concepts/auth"),
def,
...getOtherAll("auther", docUrl),
] as Template[];
+5 -5
View File
@@ -2,14 +2,14 @@ import { getByName } from "./default";
import { getOtherAll } from "./otherOrigin";
import { Template } from "./type";
const def = getByName("bypasses");
const {def, docUrl, _docUrl }= getByName("bypasses");
export default [
def
? def
: {
label: "内联",
json: `
// https://gost.run/concepts/bypass/
label: { zh: "内联", en: "Inline" },
json: _docUrl + `
{
"name": "bypass-0",
"matchers": [
@@ -21,5 +21,5 @@ export default [
]
}`,
},
...getOtherAll("bypass", "https://gost.run/concepts/bypass/"),
...getOtherAll("bypass", docUrl),
] as Template[];
+6 -3
View File
@@ -1,13 +1,16 @@
import { getByName } from "./default";
import { Template } from "./type";
const def = getByName("chains");
const {def, docUrl, _docUrl }= getByName("chains");
export default [
def,
{
label: "典型转发链",
label: { zh: "典型转发链", en: "Typical" },
cli: "",
json: `
json:
_docUrl +
`
{
"name": "chain-0",
"hops": [
+5 -5
View File
@@ -2,14 +2,14 @@ import { getByName } from "./default";
import { getOtherAll } from "./otherOrigin";
import { Template } from "./type";
const def = getByName("climiter");
const {def, docUrl, _docUrl }= getByName("climiter");
export default [
def
? def
: {
label: "内联",
json: `
// https://gost.run/concepts/limiter/
label: { zh: "内联", en: "Inline" },
json: _docUrl + `
{
"name": "climiter-0",
"limits": [
@@ -19,7 +19,7 @@ export default [
]
}`,
},
...getOtherAll("climiter", "https://gost.run/concepts/limiter/", {
...getOtherAll("climiter", docUrl, {
redisType: "set",
}),
] as Template[];
+35 -4
View File
@@ -1,5 +1,6 @@
import { findNodeAtLocation, parseTree } from "jsonc-parser";
import { Template } from "./type";
// 源自 https://gost.run/reference/configuration/file/
const defaultTemplates = `{
"services": [
@@ -241,6 +242,29 @@ const defaultTemplates = `{
}
}`;
const defaultLabel = {
en: "Default",
zh: "默认",
};
const docs: { [key: string]: string } = {
admissions: "https://gost.run/concepts/admission/",
authers: "https://gost.run/concepts/auth/",
bypasses: "https://gost.run/concepts/bypass/",
chains: "https://gost.run/concepts/chain/",
hops: "https://gost.run/concepts/hop/",
hosts: "https://gost.run/concepts/hosts/",
ingresses: "https://gost.run/concepts/ingress/",
observers: "https://gost.run/concepts/observer/",
resolvers: "https://gost.run/concepts/resolver/",
limiters: "https://gost.run/concepts/limiter/",
rlimiter: "https://gost.run/concepts/limiter/",
climiter: "https://gost.run/concepts/limiter/",
routers: "https://gost.run/concepts/router/",
sds: "https://gost.run/concepts/sd/",
services: "https://gost.run/concepts/service/",
};
const error: any[] = [];
const tree = parseTree(defaultTemplates, error, { allowTrailingComma: true });
@@ -254,15 +278,22 @@ export const getDefaultTempplates = (name: string) => {
return;
};
export const getByName = (name: string, label = "默认") => {
export const getByName = (name: string, label = defaultLabel) => {
const defT = getDefaultTempplates(name);
// console.log("defT", name, defT);
const docUrl = docs[name];
const _docUrl = "// " + docUrl + "\n";
let def;
if (defT) {
return {
def = {
label,
json: defT,
json: _docUrl + defT,
} as Template;
}
return {
def,
docUrl,
_docUrl,
};
};
export default defaultTemplates;
+5 -5
View File
@@ -2,14 +2,14 @@ import { getByName } from "./default";
import { getOtherAll } from "./otherOrigin";
import { Template } from "./type";
const def = getByName("hops");
const {def, docUrl, _docUrl }= getByName("hops");
export default [
def
? def
: {
label: "内联",
json: `
// https://gost.run/concepts/hop
label: { zh: "内联", en: "Inline" },
json: _docUrl + `
{
"name": "hop-0",
"nodes": [
@@ -36,5 +36,5 @@ export default [
]
}`,
},
...getOtherAll("hop", "https://gost.run/concepts/hop"),
...getOtherAll("hop", docUrl),
] as Template[];
+21 -19
View File
@@ -2,27 +2,29 @@ import { getByName } from "./default";
import { getOtherAll } from "./otherOrigin";
import { Template } from "./type";
const def = getByName("hosts");
const {def, docUrl, _docUrl }= getByName("hosts");
export default [
def
? def
: {
label: "内联",
json: `
// https://gost.run/concepts/hosts/
{
"name": "hosts-0",
"mappings": [
{
"ip": "127.0.0.1",
"hostname": "example.com"
},
{
"ip": "2001:db8::1",
"hostname": "example.com"
}
]
}`,
},
...getOtherAll("hosts", "https://gost.run/concepts/hosts/"),
label: { zh: "内联", en: "Inline" },
json:
_docUrl +
`
{
"name": "hosts-0",
"mappings": [
{
"ip": "127.0.0.1",
"hostname": "example.com"
},
{
"ip": "2001:db8::1",
"hostname": "example.com"
}
]
}`,
},
...getOtherAll("hosts", docUrl),
] as Template[];
+5 -4
View File
@@ -2,13 +2,14 @@ import { getByName } from "./default";
import { getOtherAll } from "./otherOrigin";
import { Template } from "./type";
const def = getByName("ingresses");
const {def, docUrl, _docUrl }= getByName("ingresses");
export default [
def
? def
: {
label: "内联",
json: `{
label: { zh: "内联", en: "Inline" },
json: _docUrl + `{
"name": "ingress-0",
"rules": [
{
@@ -22,7 +23,7 @@ export default [
]
}`,
},
...getOtherAll("ingress", "https://gost.run/concepts/ingress/", {
...getOtherAll("ingress", docUrl, {
redisType: "hash",
}),
] as Template[];
+7 -5
View File
@@ -2,14 +2,16 @@ import { getByName } from "./default";
import { getOtherAll } from "./otherOrigin";
import { Template } from "./type";
const def = getByName("limiters");
const {def, docUrl, _docUrl }= getByName("limiters");
export default [
def
? def
: {
label: "内联",
json: `
// https://gost.run/concepts/limiter/
label: { zh: "内联", en: "Inline" },
json:
_docUrl +
`
{
"name": "limiter-0",
"limits": [
@@ -20,7 +22,7 @@ export default [
]
}`,
},
...getOtherAll("limiters", "https://gost.run/concepts/limiter/", {
...getOtherAll("limiters", docUrl, {
redisType: "set",
}),
] as Template[];
+5 -5
View File
@@ -1,15 +1,15 @@
import { getByName } from "./default";
import { getOtherAll } from "./otherOrigin";
// import { getOtherAll } from "./otherOrigin";
import { Template } from "./type";
const def = getByName("observers");
const {def, docUrl, _docUrl }= getByName("observers");
export default [
def
? def
: {
label: "插件",
json: `
// https://gost.run/concepts/observer/
label: "Plugin",
json: _docUrl + `
{
"name": "observer-0",
"plugin": {
+2 -2
View File
@@ -53,7 +53,7 @@ export const getOtherAll = (
const doc = docUrl ? `// ${docUrl} \n` : "";
return [
{
label: "文件",
label: "File",
json: doc + getFileTemplate(name),
},
{
@@ -65,7 +65,7 @@ export const getOtherAll = (
json: doc + getHttpTemplate(name),
},
{
label: "插件",
label: "Plugin",
json: doc + getPluginTemplate(name),
},
] as Template[];
+6 -52
View File
@@ -2,58 +2,12 @@ import { getByName } from "./default";
import { getPluginTemplate } from "./otherOrigin";
import { Template } from "./type";
const def = getByName("resolvers");
const {def, docUrl, _docUrl }= getByName("resolvers");
export default [
def,
// {
// label: "默认",
// json: `
// // https://gost.run/concepts/resolver/
// {
// "name": "resolver-0",
// "nameservers": [
// {
// "addr": "1.1.1.1"
// },
// {
// "addr": "tcp://8.8.8.8"
// },
// {
// "addr": "tls://8.8.8.8:853"
// },
// {
// "addr": "https://1.0.0.1/dns-query"
// }
// ]
// }`,
// },
// {
// label: "使用转发链",
// json: `
// // https://gost.run/concepts/resolver/
// {
// "name": "resolver-0",
// "nameservers": [
// {
// "addr": "1.1.1.1"
// },
// {
// "addr": "tcp://8.8.8.8:53",
// "chain": "chain-0"
// },
// {
// "addr": "tls://8.8.8.8:853",
// "chain": "chain-1"
// },
// {
// "addr": "https://1.0.0.1/dns-query",
// "chain": "chain-2"
// }
// ]
// }`,
// },
{
label: "插件",
json: getPluginTemplate("resolver"),
},
{
label: "Plugin",
json: _docUrl + getPluginTemplate("resolver"),
},
] as Template[];
+7 -5
View File
@@ -2,14 +2,16 @@ import { getByName } from "./default";
import { getOtherAll } from "./otherOrigin";
import { Template } from "./type";
const def = getByName("rlimiter");
const {def, docUrl, _docUrl }= getByName("rlimiter");
export default [
def
? def
: {
label: "内联",
json: `
// https://gost.run/concepts/limiter/
label: { zh: "内联", en: "Inline" },
json:
_docUrl +
`
{
"name": "rlimiter-0",
"limits": [
@@ -20,7 +22,7 @@ export default [
]
}`,
},
...getOtherAll("rlimiter", "https://gost.run/concepts/limiter/", {
...getOtherAll("rlimiter", docUrl, {
redisType: "set",
}),
] as Template[];
+5 -5
View File
@@ -2,14 +2,14 @@ import { getByName } from "./default";
import { getOtherAll } from "./otherOrigin";
import { Template } from "./type";
const def = getByName("routers");
const {def, docUrl, _docUrl }= getByName("routers");
export default [
def
? def
: {
label: "内联",
json: `
// https://gost.run/concepts/router/
label: { zh: "内联", en: "Inline" },
json: _docUrl + `
{
"name": "router-0",
"routes": [
@@ -24,5 +24,5 @@ export default [
]
}`,
},
...getOtherAll("router", "https://gost.run/concepts/router/"),
...getOtherAll("router", docUrl),
] as Template[];
+4 -3
View File
@@ -2,13 +2,14 @@ import { getByName } from "./default";
import { getOtherAll } from "./otherOrigin";
import { Template } from "./type";
const def = getByName("sds");
const {def, docUrl, _docUrl }= getByName("sds");
export default [
def
? def
: {
label: "HTTP插件",
json: `
label: "HTTP Plugin",
json: _docUrl + `
// https://gost.run/concepts/sd/
{
"name": "sd-0",
+20 -24
View File
@@ -2,12 +2,16 @@ import * as JSONC from "jsonc-parser";
import { getByName } from "./default";
import { Template } from "./type";
const {def, docUrl, _docUrl }= getByName("services");
const getProxyJson = (
handlerType: string,
listenerType: string,
metadata?: object
) => {
let jsonc = `
let jsonc =
_docUrl +
`
{
"name": "service-0",
"addr": ":1080",
@@ -28,33 +32,21 @@ const getProxyJson = (
edits.push(...JSONC.modify(jsonc, ["listener", "metadata"], metadata, {}));
// edits.push(...JSONC.modify(jsonc, ["name"], undefined, {})) // undefined 可删除
jsonc = JSONC.applyEdits(jsonc, edits);
// jsonc = JSONC.applyEdits(
// jsonc,
// JSONC.modify(jsonc, ["metadata"], metadata, {})
// );
// jsonc = JSONC.applyEdits(
// jsonc,
// JSONC.modify(jsonc, ["handler","metadata"], metadata, {})
// );
// jsonc = JSONC.applyEdits(
// jsonc,
// JSONC.modify(jsonc, ["listener","metadata"], metadata, {})
// );
}
return jsonc;
};
const def = getByName("services");
export default [
def,
{
label: "端口转发",
label: { zh: "端口转发", en: "Port Forwarding" },
cli: "-L tcp://:80/:8080",
json: `
json:
_docUrl +
`
{
"name": "service-0", // 服务名称
"name": "service-0",
"addr": ":80",
"handler": {
"type": "tcp"
@@ -73,9 +65,11 @@ export default [
}`,
},
{
label: "反向代理",
label: { zh: "反向代理", en: "Reverse Proxy" },
cli: "",
json: `
json:
_docUrl +
`
{
"name": "service-0",
"addr": ":80",
@@ -104,7 +98,7 @@ export default [
}`,
},
{
label: "代理服务",
label: { zh: "代理服务", en: "Proxy Service" },
children: [
{
label: "http",
@@ -156,7 +150,7 @@ export default [
json: getProxyJson("socks5", "tcp"),
},
{
label: "socks5(支持udp)",
label: "socks5(udp)",
json: getProxyJson("socks5", "tcp", { udp: "true" }),
},
{
@@ -168,9 +162,11 @@ export default [
],
},
{
label: "代理转发",
label: { zh: "代理转发", en: "Porxy Forwarding" },
cli: "-L socks5://:1080 -F socks5://xxx.com:1080",
json: `
json:
_docUrl +
`
{
"name": "service-0",
"addr": ":1080",
+5 -5
View File
@@ -1,6 +1,6 @@
export type Template = {
label: string;
json: string;
cli?: string;
children?: Template[];
}
label: string | { [key: string]: string };
json: string;
cli?: string;
children?: Template[];
};
+34
View File
@@ -0,0 +1,34 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { isValidElement } from "react"
const en = await import("../i18n/en.json");
const zh = await import("../i18n/zh.json");
const fallbackLng = 'en';
i18n
// 检测用户当前使用的语言
// 文档: https://github.com/i18next/i18next-browser-languageDetector
.use(LanguageDetector)
// 注入 react-i18next 实例
.use(initReactI18next)
// 初始化 i18next
// 配置参数的文档: https://www.i18next.com/overview/configuration-options
.init({
debug: true,
fallbackLng: fallbackLng,
interpolation: {
escapeValue: false,
},
resources: {
en: { translation: en },
zh: { translation: zh },
},
});
export function getLabel(label: string | { [key: string]: string }) {
if(typeof label === "string") return label;
if(isValidElement(label)) return label;
const lang = i18n.resolvedLanguage!;
return label[lang] || label[fallbackLng] || '<unknown>';
}
export default i18n;
+3 -2
View File
@@ -2,6 +2,7 @@ import axios from "axios";
import { getInfo } from "./server";
import { message } from "antd";
import { configEvent } from "./events";
import { t } from "i18next";
const require = axios.create();
require.interceptors.request.use((config) => {
@@ -14,7 +15,7 @@ require.interceptors.response.use(
(res) => {
if (res.config.method !== "get" && !(res.config as any)?.noMsg) {
// configEvent.emit("apiUpdate", res.config);
message.success('操作成功!')
message.success(t("msg.success"));
}
if (res.data) {
return res.data;
@@ -23,7 +24,7 @@ require.interceptors.response.use(
},
(error) => {
const { response } = error || {};
let msg = error.message || "出现未知错误!";
let msg = error.message || t("msg.unknown");
if (response?.data?.msg) {
msg = response?.data?.msg;
}
+2 -1
View File
@@ -2,6 +2,7 @@ import getUseValue from "./getUseValue";
import axios from "axios";
import qs from "qs";
import { message } from "antd";
import { t } from "i18next";
import { ServerComm } from "../api/local";
import { Config } from "../api/types";
const gostServerKey = "__GOST_SERVER__";
@@ -111,7 +112,7 @@ export const login = async (arg: GostApiConfig, save?: false) => {
} catch (e: any) {
if (e === "verify error") {
logout();
message.error(e?.message || "连接失败");
message.error(e?.message || t("msg.connectionFailed"));
}
throw e;
}
+8 -2
View File
@@ -4,6 +4,7 @@ import type * as Monaco from "monaco-editor";
import classnames from "classnames";
import { useIsDark } from "./useTheme";
import { Input } from "antd";
import { getI18n, useTranslation } from 'react-i18next';
(function () {
const require = (window as any).require;
@@ -28,10 +29,14 @@ import { Input } from "antd";
// const monacoURL = `//unpkg.com/monaco-editor@0.47.0/min/vs`
// require.config({ paths: { vs: `${monacoURL}` } });
if (require) {
const i18n = getI18n();
i18n.on('languageChanged',(event)=>{
console.log('languageChanged', event)
})
require.config({
"vs/nls": {
availableLanguages: {
"*": "zh-cn",
"*": i18n.resolvedLanguage === "zh" ? "zh-cn" : "en",
},
},
});
@@ -76,6 +81,7 @@ const CodeEditor_: React.ForwardRefRenderFunction<
CodeEditorProps
> = (props, ref) => {
const { onChange, options, height, className, theme, style } = props;
const { t } = useTranslation();
const [language, setLanguage] = useBindValue(props.language, "javascript");
const [value, setValue] = useBindValue(props.value, props.defaultValue, "");
const [isReady, setReady] = useState(false);
@@ -131,7 +137,7 @@ const CodeEditor_: React.ForwardRefRenderFunction<
_editor.addAction({
id: "my-autoWrap-toggle",
label: "切换自动换行",
label: t("msg.wordWrap"),
keybindings: [
monaco.KeyMod.Alt | monaco.KeyCode.KeyZ,
// monaco.KeyMod.chord(