From 206e14c7e70f523a1580b0d684d97d799057b53e Mon Sep 17 00:00:00 2001 From: im0o <0ojixueseno0@gmail.com> Date: Mon, 10 Aug 2026 11:55:53 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=AF=E7=94=B1=E9=A1=B5=E6=94=B9=E6=88=90?= =?UTF-8?q?=E4=B8=BB=E4=BB=8E=E5=B8=83=E5=B1=80=EF=BC=8C=E5=87=BA=E7=AB=99?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E7=9C=9F=E7=9A=84=E6=94=B9=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 右侧详情面板取代原来的手风琴展开:空间用起来了,选中状态更清楚,行也不会被推来推去。 出站选择做成分组的按钮网格而不是下拉框——一眼看得到全部可选项和当前状态。 点一下就走完整的落盘链路(校验、备份、写入、重载),失败时按停在哪一步显示不同的话。 订阅线路那一档暂时禁用:选中它需要先自动建一个筛选组,那部分还没做。 顺带两处从截图里看出来的问题:连接线原本是固定 40px 的短横,读起来像装饰不像连接, 改成占据弹性列真的把两头连起来;默认入站的名字原本是「HTTP 和 SOCKS5 共用」, 和类型列重复,改成它来自的配置键 mixed-port。 --- shared/inbounds.test.ts | 2 +- shared/inbounds.ts | 18 +- web/App.vue | 27 ++- web/views/InboundDetail.vue | 259 +++++++++++++++++++++++++++++ web/views/InboundList.vue | 310 ---------------------------------- web/views/RouterPage.vue | 323 ++++++++++++++++++++++++++++++++++++ 6 files changed, 618 insertions(+), 321 deletions(-) create mode 100644 web/views/InboundDetail.vue delete mode 100644 web/views/InboundList.vue create mode 100644 web/views/RouterPage.vue diff --git a/shared/inbounds.test.ts b/shared/inbounds.test.ts index 2aa5e20..15e6165 100644 --- a/shared/inbounds.test.ts +++ b/shared/inbounds.test.ts @@ -28,7 +28,7 @@ describe('合并两套写法的入站', () => { 'mixed-port': 7777, listeners: [{ name: '容器A', type: 'mixed', port: 7778, proxy: 'AUTO' }], }) - expect(list.map((i) => i.name)).toEqual(['HTTP 和 SOCKS5 共用', '容器A']) + expect(list.map((i) => i.name)).toEqual(['mixed-port', '容器A']) expect(list[0]!.isDefault).toBe(true) expect(list[1]!.isDefault).toBe(false) expect(list[1]!.listenerIndex).toBe(0) diff --git a/shared/inbounds.ts b/shared/inbounds.ts index 0821907..03f09ee 100644 --- a/shared/inbounds.ts +++ b/shared/inbounds.ts @@ -23,12 +23,12 @@ export const BUILTIN_OUTBOUNDS = [ ] as const /** 旧写法的顶层端口字段,以及它们各自等价于哪种入站类型。 */ -const LEGACY_PORTS: Array<{ key: string; type: string; label: string }> = [ - { key: 'mixed-port', type: 'mixed', label: 'HTTP 和 SOCKS5 共用' }, - { key: 'port', type: 'http', label: 'HTTP' }, - { key: 'socks-port', type: 'socks', label: 'SOCKS5' }, - { key: 'redir-port', type: 'redir', label: '透明代理(REDIRECT)' }, - { key: 'tproxy-port', type: 'tproxy', label: '透明代理(TPROXY)' }, +const LEGACY_PORTS: Array<{ key: string; type: string }> = [ + { key: 'mixed-port', type: 'mixed' }, + { key: 'port', type: 'http' }, + { key: 'socks-port', type: 'socks' }, + { key: 'redir-port', type: 'redir' }, + { key: 'tproxy-port', type: 'tproxy' }, ] /** 一路入站的出站去向。四选一,对应界面上那个下拉框。 */ @@ -77,14 +77,16 @@ export function outboundToProxyField(mode: OutboundMode): string | undefined { export function collectInbounds(data: ConfigObject): Inbound[] { const result: Inbound[] = [] - for (const { key, type, label } of LEGACY_PORTS) { + for (const { key, type } of LEGACY_PORTS) { const raw = data[key] const port = typeof raw === 'number' ? raw : Number(raw) // 0 和缺省都表示没启用这个端口 if (!Number.isFinite(port) || port <= 0) continue result.push({ id: `legacy:${key}`, - name: label, + // 名字直接用它来自的那个配置键。比「HTTP 和 SOCKS5 共用」这种描述好: + // 类型那一列已经写着协议了,名字再说一遍是重复,而配置键能直接对上文件里的行。 + name: key, type, port: String(port), listen: null, diff --git a/web/App.vue b/web/App.vue index 20d95e8..53aebff 100644 --- a/web/App.vue +++ b/web/App.vue @@ -4,13 +4,14 @@ import { ApiError, api, getToken, setToken } from './api' import type { ConfigSnapshot } from './api' import { NAV } from './nav' import { density, toggleDensity } from './prefs' -import InboundList from './views/InboundList.vue' +import RouterPage from './views/RouterPage.vue' import TokenGate from './views/TokenGate.vue' import IconFold from '~icons/ri/sidebar-fold-line' import IconUnfold from '~icons/ri/sidebar-unfold-line' const snapshot = ref(null) const kernel = ref<{ online: boolean; version?: string }>({ online: false }) +const providerLines = ref([]) const needsToken = ref(!getToken()) const loadError = ref('') const loading = ref(false) @@ -24,6 +25,9 @@ async function load() { snapshot.value = config kernel.value = status needsToken.value = false + // 订阅里有哪些线路只有运行中的内核知道,配置文件里只有一个订阅地址。 + // 拿不到不算错误——界面照常能编辑,只是订阅线路那一档不可选。 + providerLines.value = status.online ? await readProviderLines() : [] } catch (error) { if (error instanceof ApiError && error.unauthorized) { needsToken.value = true @@ -35,6 +39,18 @@ async function load() { } } +/** 只取真正的订阅。内核会给每个用了 proxies 的策略组造一个内部 provider,那些不是订阅。 */ +async function readProviderLines(): Promise { + try { + const { providers } = await api.providers() + return Object.values(providers) + .filter((p) => p.vehicleType !== 'Compatible') + .flatMap((p) => p.proxies.map((line) => line.name)) + } catch { + return [] + } +} + function submitToken(value: string) { setToken(value) void load() @@ -109,7 +125,14 @@ const current = computed(() => NAV.find((n) => n.key === active.value)) - +
diff --git a/web/views/InboundDetail.vue b/web/views/InboundDetail.vue new file mode 100644 index 0000000..ad48ea8 --- /dev/null +++ b/web/views/InboundDetail.vue @@ -0,0 +1,259 @@ + + + + + diff --git a/web/views/InboundList.vue b/web/views/InboundList.vue deleted file mode 100644 index 33de9c5..0000000 --- a/web/views/InboundList.vue +++ /dev/null @@ -1,310 +0,0 @@ - - - - - diff --git a/web/views/RouterPage.vue b/web/views/RouterPage.vue new file mode 100644 index 0000000..98dcc6c --- /dev/null +++ b/web/views/RouterPage.vue @@ -0,0 +1,323 @@ + + + + +