feat: add clear ingress/egress and data flow indicators for proxy chain

This commit is contained in:
wonfen 2026-02-11 04:41:46 +08:00
parent 4d72d2d0df
commit a4617d1fed
No known key found for this signature in database
GPG Key ID: CEAFD6C73AB2001F
16 changed files with 114 additions and 28 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ scripts/_env.sh
.eslintcache
.changelog_backups
target
CLAUDE.md

View File

@ -27,5 +27,6 @@
- 避免脏订阅地址无法 Scheme 导入订阅
- macOS TUN 覆盖 DNS 时使用 114.114.114.114
- 连通性测试替换为更快的 https://1.1.1.1
- 链式代理增加明显的入口出口与数据流向标识
</details>

View File

@ -16,6 +16,7 @@ import {
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import {
ArrowDownward,
Delete as DeleteIcon,
DragIndicator,
Link,
@ -69,10 +70,18 @@ interface ProxyChainProps {
interface SortableItemProps {
proxy: ProxyChainItem;
index: number;
isFirst: boolean;
isLast: boolean;
onRemove: (id: string) => void;
}
const SortableItem = ({ proxy, index, onRemove }: SortableItemProps) => {
const SortableItem = ({
proxy,
index,
isFirst,
isLast,
onRemove,
}: SortableItemProps) => {
const theme = useTheme();
const { t } = useTranslation();
const {
@ -90,12 +99,24 @@ const SortableItem = ({ proxy, index, onRemove }: SortableItemProps) => {
opacity: isDragging ? 0.5 : 1,
};
const roleLabel = isFirst
? t("proxies.page.chain.entryNode")
: isLast
? t("proxies.page.chain.exitNode")
: undefined;
const roleColor = isFirst
? theme.palette.success.main
: isLast
? theme.palette.warning.main
: undefined;
return (
<Box
ref={setNodeRef}
style={style}
sx={{
mb: 1,
mb: 0,
display: "flex",
alignItems: "center",
p: 1,
@ -103,7 +124,9 @@ const SortableItem = ({ proxy, index, onRemove }: SortableItemProps) => {
? theme.palette.action.selected
: theme.palette.background.default,
borderRadius: 1,
border: `1px solid ${theme.palette.divider}`,
border: roleColor
? `1.5px solid ${roleColor}`
: `1px solid ${theme.palette.divider}`,
boxShadow: isDragging ? theme.shadows[4] : theme.shadows[1],
transition: "box-shadow 0.2s, background-color 0.2s",
}}
@ -125,12 +148,25 @@ const SortableItem = ({ proxy, index, onRemove }: SortableItemProps) => {
<DragIndicator />
</Box>
<Chip
label={`${index + 1}`}
size="small"
color="primary"
sx={{ mr: 1, minWidth: 32 }}
/>
{roleLabel ? (
<Chip
label={roleLabel}
size="small"
sx={{
mr: 1,
fontWeight: 700,
color: "#fff",
backgroundColor: roleColor,
}}
/>
) : (
<Chip
label={`${index + 1}`}
size="small"
color="primary"
sx={{ mr: 1, minWidth: 32 }}
/>
)}
<Typography
variant="body2"
@ -579,12 +615,34 @@ export const ProxyChain = ({
}}
>
{proxyChain.map((proxy, index) => (
<SortableItem
key={proxy.id}
proxy={proxy}
index={index}
onRemove={handleRemoveProxy}
/>
<Box key={proxy.id}>
<SortableItem
proxy={proxy}
index={index}
isFirst={index === 0}
isLast={
index === proxyChain.length - 1 && proxyChain.length > 1
}
onRemove={handleRemoveProxy}
/>
{index < proxyChain.length - 1 && (
<Box
sx={{
display: "flex",
justifyContent: "center",
py: 0.25,
}}
>
<ArrowDownward
sx={{
fontSize: 20,
color: theme.palette.primary.main,
opacity: 0.7,
}}
/>
</Box>
)}
</Box>
))}
</Box>
</SortableContext>

View File

@ -49,7 +49,9 @@
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
"connectFailed": "Failed to connect to proxy chain",
"disconnectFailed": "Failed to disconnect from proxy chain",
"duplicateNode": "Proxy node already exists in chain"
"duplicateNode": "Proxy node already exists in chain",
"entryNode": "مدخل",
"exitNode": "مخرج"
},
"messages": {
"directMode": "الوضع المباشر"

View File

@ -49,7 +49,9 @@
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
"connectFailed": "Failed to connect to proxy chain",
"disconnectFailed": "Failed to disconnect from proxy chain",
"duplicateNode": "Proxy node already exists in chain"
"duplicateNode": "Proxy node already exists in chain",
"entryNode": "Eingang",
"exitNode": "Ausgang"
},
"messages": {
"directMode": "Direktverbindungs-Modus"

View File

@ -49,7 +49,9 @@
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
"connectFailed": "Failed to connect to proxy chain",
"disconnectFailed": "Failed to disconnect from proxy chain",
"duplicateNode": "Proxy node already exists in chain"
"duplicateNode": "Proxy node already exists in chain",
"entryNode": "Entry",
"exitNode": "Exit"
},
"messages": {
"directMode": "Direct Mode"

View File

@ -49,7 +49,9 @@
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
"connectFailed": "Failed to connect to proxy chain",
"disconnectFailed": "Failed to disconnect from proxy chain",
"duplicateNode": "Proxy node already exists in chain"
"duplicateNode": "Proxy node already exists in chain",
"entryNode": "Entrada",
"exitNode": "Salida"
},
"messages": {
"directMode": "Modo de conexión directa"

View File

@ -49,7 +49,9 @@
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
"connectFailed": "Failed to connect to proxy chain",
"disconnectFailed": "Failed to disconnect from proxy chain",
"duplicateNode": "Proxy node already exists in chain"
"duplicateNode": "Proxy node already exists in chain",
"entryNode": "ورودی",
"exitNode": "خروجی"
},
"messages": {
"directMode": "حالت مستقیم"

View File

@ -49,7 +49,9 @@
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
"connectFailed": "Failed to connect to proxy chain",
"disconnectFailed": "Failed to disconnect from proxy chain",
"duplicateNode": "Proxy node already exists in chain"
"duplicateNode": "Proxy node already exists in chain",
"entryNode": "Masuk",
"exitNode": "Keluar"
},
"messages": {
"directMode": "Mode Langsung"

View File

@ -49,7 +49,9 @@
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
"connectFailed": "Failed to connect to proxy chain",
"disconnectFailed": "Failed to disconnect from proxy chain",
"duplicateNode": "Proxy node already exists in chain"
"duplicateNode": "Proxy node already exists in chain",
"entryNode": "入口",
"exitNode": "出口"
},
"messages": {
"directMode": "直接接続モード"

View File

@ -49,7 +49,9 @@
"minimumNodesHint": "체인 프록시는 최소 2개의 노드가 필요합니다. 하나 더 추가하세요.",
"connectFailed": "프록시 체인 연결 실패",
"disconnectFailed": "프록시 체인 연결 해제 실패",
"duplicateNode": "프록시 노드가 체인에 이미 존재합니다"
"duplicateNode": "프록시 노드가 체인에 이미 존재합니다",
"entryNode": "입구",
"exitNode": "출구"
},
"messages": {
"directMode": "직접 모드"

View File

@ -49,7 +49,9 @@
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
"connectFailed": "Failed to connect to proxy chain",
"disconnectFailed": "Failed to disconnect from proxy chain",
"duplicateNode": "Proxy node already exists in chain"
"duplicateNode": "Proxy node already exists in chain",
"entryNode": "Вход",
"exitNode": "Выход"
},
"messages": {
"directMode": "Прямой режим"

View File

@ -49,7 +49,9 @@
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
"connectFailed": "Failed to connect to proxy chain",
"disconnectFailed": "Failed to disconnect from proxy chain",
"duplicateNode": "Proxy node already exists in chain"
"duplicateNode": "Proxy node already exists in chain",
"entryNode": "Giriş",
"exitNode": ıkış"
},
"messages": {
"directMode": "Doğrudan Mod"

View File

@ -49,7 +49,9 @@
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
"connectFailed": "Failed to connect to proxy chain",
"disconnectFailed": "Failed to disconnect from proxy chain",
"duplicateNode": "Proxy node already exists in chain"
"duplicateNode": "Proxy node already exists in chain",
"entryNode": "Кереш",
"exitNode": "Чыгыш"
},
"messages": {
"directMode": "Туры режим"

View File

@ -49,7 +49,9 @@
"minimumNodesHint": "链式代理至少需要 2 个节点,请再添加一个节点。",
"connectFailed": "连接链式代理失败",
"disconnectFailed": "断开链式代理失败",
"duplicateNode": "该节点已在链式代理表中"
"duplicateNode": "该节点已在链式代理表中",
"entryNode": "入口",
"exitNode": "出口"
},
"messages": {
"directMode": "直连模式"

View File

@ -49,7 +49,9 @@
"minimumNodesHint": "鏈式代理至少需要 2 個節點,請再新增一個節點。",
"connectFailed": "連線鏈式代理失敗",
"disconnectFailed": "中斷鏈式代理失敗",
"duplicateNode": "該節點已在鏈式代理表中"
"duplicateNode": "該節點已在鏈式代理表中",
"entryNode": "入口",
"exitNode": "出口"
},
"messages": {
"directMode": "直連模式"