clash-verge-rev/src/components/setting/mods/external-controller-cors.tsx
Sline c8aa72186e
chore: i18n (#5276)
* chore: notice i18n

* feat: add script to clean up unused i18n keys

* chore: cleanup i18n keys

* refactor(i18n/proxies): migrate proxies UI to structured locale keys

* chore: i18n for rule module

* chore: i18n for profile module

* chore: i18n for connections module

* chore: i18n for settings module

* chore: i18n for verge settings

* chore: i18n for theme settings

* chore: i18n for theme

* chore(i18n): components.home.*

* chore(i18n): remove unused i18n keys

* chore(i18n): components.profile.*

* chore(i18n): components.connection

* chore(i18n): pages.logs.*

* chore(i18n): pages.*.provider

* chore(i18n): components.settings.externalCors.*

* chore(i18n): components.settings.clash.*

* chore(i18n): components.settings.liteMode.*

* chore(i18n): components.settings.backup.*

* chore(i18n): components.settings.clash.port.*

* chore(i18n): components.settings.misc.*

* chore(i18n): components.settings.update.*

* chore(i18n): components.settings.sysproxy.*

* chore(i18n): components.settings.sysproxy.*

* chore(i18n): pages.profiles.notices/components.providers.notices

* refactor(notice): unify showNotice usage

* refactor(notice): add typed showNotice shortcuts, centralize defaults, and simplify subscriptions

* refactor: unify showNotice usage

* refactor(notice): unify showNotice API

* refactor(notice): unify showNotice usage

* chore(i18n): components.test.*

* chore(i18n): components.settings.dns.*

* chore(i18n): components.home.clashInfo.*

* chore(i18n): components.home.systemInfo.*

* chore(i18n): components.home.ipInfo/traffic.*

* chore(i18n): navigation.*

* refactor(i18n): remove pages.* namespace and migrate route texts under module-level page keys

* chore(i18n): common.*

* chore(i18n): common.*

* fix: change error handling in patch_profiles_config to return false when a switch is in progress

* fix: improve error handling in patch_profiles_config to prevent requests during profile switching

* fix: change error handling in patch_profiles_config to return false when a switch is in progress

fix: ensure CURRENT_SWITCHING_PROFILE is reset after config updates in perform_config_update and patch_profiles_config

* chore(i18n): restructure root-level locale keys into namespaces

* chore(i18n): add missing i18n keys

* docs: i18n guide

* chore: adjust i18n

* refactor(i18n): align UI actions and status labels with common keys

* refactor(i18n): unify two-name locale namespaces

* refactor(i18n/components): unify locale keys and update component references

* chore(i18n): add shared and entities namespaces to all locale files

* refactor(i18n): consolidate shared and entity namespaces across features

* chore(deps): update npm dependencies to ^7.3.5 (#5310)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* refactor(i18n): migrate shared editor modes and consolidate entities namespaces

* tmp

* refactor(i18n): flatten locales and move theme/validation strings

* docs: CONTRIBUTING_i18n.md

* refactor(i18n): restructure feedback and profile namespaces for better organization

* refactor(i18n): unify settings locale structure and update references

* refactor(i18n): reorganize locale keys for home, proxies, rules, connections, logs, unlock, and tests

* refactor(i18n/feedback/layout): unify shared toasts & normalize layout namespace

* refactor(i18n): centralize common UI strings in shared

* refactor(i18n): flatten headers and unify locale schema

* refactor(i18n): consolidate duplicate per-feature translations into shared namespace

* refactor(i18n): split locales into per-namespace files

* style: lint

* refactor(i18n): unify unlock UI translations under tests namespace

* feat(i18n): add type-checked translation keys

* style: eslint import order

* feat(i18n): replace ad-hoc loader with rust-i18n backend bundles

* chore(prebuild): remove locale-copy step

* fix(i18n, notice): propagate runtime params and update cleanup script path

* fix(i18n,notice): make locale formatting idempotent and guard early notice translations

* fix(i18n): resolve locale aliases and match OS codes correctly

* fix(unlock): use i18next-compatible double-brace interpolation in failure notice

* fix(i18n): route unlock error notices through translation keys

* fix(i18n): i18n types

* feat(i18n): localize upgrade notice for Clash core viewer

* fix(notice): ensure runtime overrides apply to prefix translations

* chore(i18n): replace literal notices with translation keys

* chore(i18n): types

* chore(i18n): regen typings before formatting to keep keys in sync

* chore(i18n): simply labels

* chore(i18n): adjust translation

* chore: remove eslint-plugin-i18next

* chore(i18n): add/refine Korean translations across frontend scopes and Rust backend (#5341)

* chore(i18n): translate settings.json (missed in previous pass) (#5343)

* chore(i18n): add/refine Korean translations across frontend scopes and Rust backend

* chore(i18n): add/refine Korean translations across frontend scopes and Rust backend

* fix(i18n-tauri): quote placeholder-leading value in ko.yml to prevent rust_i18n parse panic

* chore(i18n): translate settings.json (forgot to include previously)

---------

Co-authored-by: rozan <34974262+thelojan@users.noreply.github.com>
2025-11-08 19:40:38 +08:00

289 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Delete as DeleteIcon } from "@mui/icons-material";
import { Box, Button, Divider, List, ListItem, TextField } from "@mui/material";
import { useLockFn, useRequest } from "ahooks";
import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { BaseDialog, Switch } from "@/components/base";
import { useClash } from "@/hooks/use-clash";
import { showNotice } from "@/services/noticeService";
// 定义开发环境的URL列表
// 这些URL在开发模式下会被自动包含在允许的来源中
// 在生产环境中这些URL会被过滤掉
// 这样可以确保在生产环境中不会意外暴露开发环境的URL
const DEV_URLS = [
"tauri://localhost",
"http://tauri.localhost",
"http://localhost:3000",
];
// 获取完整的源列表包括开发URL
const getFullOrigins = (origins: string[]) => {
// 合并现有源和开发URL并去重
const allOrigins = [...origins, ...DEV_URLS];
const uniqueOrigins = [...new Set(allOrigins)];
return uniqueOrigins;
};
// 过滤基础URL(确保后续添加)
const filterBaseOriginsForUI = (origins: string[]) => {
return origins.filter((origin: string) => !DEV_URLS.includes(origin.trim()));
};
// 统一使用的按钮样式
const buttonStyle = {
borderRadius: "8px",
textTransform: "none",
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
transition: "all 0.3s ease",
"&:hover": {
boxShadow: "0 4px 8px rgba(0,0,0,0.15)",
transform: "translateY(-1px)",
},
"&:active": {
transform: "translateY(0)",
},
};
// 添加按钮样式
const addButtonStyle = {
...buttonStyle,
backgroundColor: "#4CAF50",
color: "white",
"&:hover": {
backgroundColor: "#388E3C",
},
};
// 删除按钮样式
const deleteButtonStyle = {
...buttonStyle,
backgroundColor: "#FF5252",
color: "white",
"&:hover": {
backgroundColor: "#D32F2F",
},
};
interface ClashHeaderConfigingRef {
open: () => void;
close: () => void;
}
export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
(props, ref) => {
const { t } = useTranslation();
const { clash, mutateClash, patchClash } = useClash();
const [open, setOpen] = useState(false);
// CORS配置状态管理
const [corsConfig, setCorsConfig] = useState<{
allowPrivateNetwork: boolean;
allowOrigins: string[];
}>(() => {
const cors = clash?.["external-controller-cors"];
const origins = cors?.["allow-origins"] ?? [];
return {
allowPrivateNetwork: cors?.["allow-private-network"] ?? true,
allowOrigins: filterBaseOriginsForUI(origins),
};
});
// 处理CORS配置变更
const handleCorsConfigChange = (
key: "allowPrivateNetwork" | "allowOrigins",
value: boolean | string[],
) => {
setCorsConfig((prev) => ({
...prev,
[key]: value,
}));
};
// 添加新的允许来源
const handleAddOrigin = () => {
handleCorsConfigChange("allowOrigins", [...corsConfig.allowOrigins, ""]);
};
// 更新允许来源列表中的某一项
const handleUpdateOrigin = (index: number, value: string) => {
const newOrigins = [...corsConfig.allowOrigins];
newOrigins[index] = value;
handleCorsConfigChange("allowOrigins", newOrigins);
};
// 删除允许来源列表中的某一项
const handleDeleteOrigin = (index: number) => {
const newOrigins = [...corsConfig.allowOrigins];
newOrigins.splice(index, 1);
handleCorsConfigChange("allowOrigins", newOrigins);
};
// 保存配置请求
const { loading, run: saveConfig } = useRequest(
async () => {
// 保存时使用完整的源列表包括开发URL
const fullOrigins = getFullOrigins(corsConfig.allowOrigins);
await patchClash({
"external-controller-cors": {
"allow-private-network": corsConfig.allowPrivateNetwork,
"allow-origins": fullOrigins.filter(
(origin: string) => origin.trim() !== "",
),
},
});
await mutateClash();
},
{
manual: true,
onSuccess: () => {
setOpen(false);
showNotice.success(
"shared.feedback.notifications.common.saveSuccess",
);
},
onError: () => {
showNotice.error("shared.feedback.notifications.common.saveFailed");
},
},
);
useImperativeHandle(ref, () => ({
open: () => {
const cors = clash?.["external-controller-cors"];
const origins = cors?.["allow-origins"] ?? [];
setCorsConfig({
allowPrivateNetwork: cors?.["allow-private-network"] ?? true,
allowOrigins: filterBaseOriginsForUI(origins),
});
setOpen(true);
},
close: () => setOpen(false),
}));
const handleSave = useLockFn(async () => {
await saveConfig();
});
const originEntries = useMemo(() => {
const counts: Record<string, number> = {};
return corsConfig.allowOrigins.map((origin, index) => {
const occurrence = (counts[origin] = (counts[origin] ?? 0) + 1);
const keyBase = origin || "origin";
return {
origin,
index,
key: `${keyBase}-${occurrence}`,
};
});
}, [corsConfig.allowOrigins]);
return (
<BaseDialog
open={open}
title={t("settings.sections.externalCors.title")}
contentSx={{ width: 500 }}
okBtn={loading ? t("shared.statuses.saving") : t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={handleSave}
>
<List sx={{ width: "90%", padding: 2 }}>
<ListItem sx={{ padding: "8px 0" }}>
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
width="100%"
>
<span style={{ fontWeight: "normal" }}>
{t("settings.sections.externalCors.fields.allowPrivateNetwork")}
</span>
<Switch
edge="end"
checked={corsConfig.allowPrivateNetwork}
onChange={(e) =>
handleCorsConfigChange(
"allowPrivateNetwork",
e.target.checked,
)
}
/>
</Box>
</ListItem>
<Divider sx={{ my: 2 }} />
<ListItem sx={{ padding: "8px 0" }}>
<div style={{ width: "100%" }}>
<div style={{ marginBottom: 8, fontWeight: "bold" }}>
{t("settings.sections.externalCors.fields.allowedOrigins")}
</div>
{originEntries.map(({ origin, index, key }) => (
<div
key={key}
style={{
display: "flex",
alignItems: "center",
marginBottom: 8,
}}
>
<TextField
fullWidth
size="small"
sx={{ fontSize: 14, marginRight: 2 }}
value={origin}
onChange={(e) => handleUpdateOrigin(index, e.target.value)}
placeholder={t(
"settings.sections.externalCors.placeholders.origin",
)}
inputProps={{ style: { fontSize: 14 } }}
/>
<Button
variant="contained"
color="error"
size="small"
onClick={() => handleDeleteOrigin(index)}
disabled={corsConfig.allowOrigins.length <= 0}
sx={deleteButtonStyle}
>
<DeleteIcon fontSize="small" />
</Button>
</div>
))}
<Button
variant="contained"
size="small"
onClick={handleAddOrigin}
sx={addButtonStyle}
>
{t("settings.sections.externalCors.actions.add")}
</Button>
<div
style={{
marginTop: 12,
padding: 8,
backgroundColor: "#f5f5f5",
borderRadius: 4,
}}
>
<div
style={{ color: "#666", fontSize: 12, fontStyle: "italic" }}
>
{t("settings.sections.externalCors.messages.alwaysIncluded", {
urls: DEV_URLS.join(", "),
})}
</div>
</div>
</div>
</ListItem>
</List>
</BaseDialog>
);
},
);