Files
mihomo-patchbay/web/views/InboundDetail.vue
T
im0o 206e14c7e7 路由页改成主从布局,出站可以真的改了
右侧详情面板取代原来的手风琴展开:空间用起来了,选中状态更清楚,行也不会被推来推去。
出站选择做成分组的按钮网格而不是下拉框——一眼看得到全部可选项和当前状态。

点一下就走完整的落盘链路(校验、备份、写入、重载),失败时按停在哪一步显示不同的话。
订阅线路那一档暂时禁用:选中它需要先自动建一个筛选组,那部分还没做。

顺带两处从截图里看出来的问题:连接线原本是固定 40px 的短横,读起来像装饰不像连接,
改成占据弹性列真的把两头连起来;默认入站的名字原本是「HTTP 和 SOCKS5 共用」,
和类型列重复,改成它来自的配置键 mixed-port。
2026-08-10 11:55:53 +08:00

260 lines
6.1 KiB
Vue
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.
<script setup lang="ts">
import { computed } from 'vue'
import type { Inbound, OutboundMode } from '@shared/inbounds.ts'
import IconLock from '~icons/ri/lock-line'
import IconOffline from '~icons/ri/cloud-off-line'
const props = defineProps<{
inbound: Inbound
/** 策略组名字 */
groups: string[]
/** 顶层 proxies 里手写的线路 */
ownProxies: string[]
/** 订阅带来的线路。只有内核在线时才拿得到 */
providerLines: string[]
kernelOnline: boolean
saving: boolean
}>()
const emit = defineEmits<{ pick: [mode: OutboundMode] }>()
const BUILTINS: Array<{ mode: OutboundMode; label: string; hint: string }> = [
{ mode: { kind: 'rules' }, label: 'RULE', hint: '不指定出站,流量走分流规则' },
{ mode: { kind: 'direct' }, label: 'DIRECT', hint: '不经过任何代理' },
{ mode: { kind: 'global' }, label: 'GLOBAL', hint: '跟随 GLOBAL 组当前选中的出站' },
]
const current = computed(() => props.inbound.outbound)
function isCurrent(mode: OutboundMode): boolean {
const now = current.value
if (now.kind !== mode.kind) return false
return now.kind !== 'named' || now.name === (mode as { name: string }).name
}
/** 默认入站没有 proxy 字段可写,出站只能是 RULE。 */
const locked = computed(() => props.inbound.isDefault)
</script>
<template>
<aside class="detail">
<header class="top">
<div class="title">
<span class="port mono">{{ inbound.port }}</span>
<span class="name">{{ inbound.name }}</span>
</div>
<dl class="facts">
<dt>类型</dt>
<dd class="mono">{{ inbound.type }}</dd>
<dt>监听</dt>
<dd class="mono">{{ inbound.listen ?? '0.0.0.0' }}</dd>
<dt>附加规则</dt>
<dd class="mono">{{ inbound.ruleSet ?? '—' }}</dd>
</dl>
</header>
<section class="pick">
<h3>出站</h3>
<p v-if="locked" class="locked">
<IconLock />
由 <code>{{ inbound.legacyKey }}</code> 声明,这种写法没有 proxy 字段,只能走 RULE
</p>
<div class="chips">
<button
v-for="b in BUILTINS"
:key="b.label"
type="button"
:class="['chip', isCurrent(b.mode) && 'is-on']"
:disabled="locked || saving"
:title="b.hint"
@click="emit('pick', b.mode)"
>
{{ b.label }}
</button>
</div>
<template v-if="groups.length">
<h4>策略组</h4>
<div class="chips">
<button
v-for="g in groups"
:key="g"
type="button"
:class="['chip', isCurrent({ kind: 'named', name: g }) && 'is-on']"
:disabled="locked || saving"
@click="emit('pick', { kind: 'named', name: g })"
>
{{ g }}
</button>
</div>
</template>
<template v-if="ownProxies.length">
<h4>自有线路</h4>
<div class="chips">
<button
v-for="p in ownProxies"
:key="p"
type="button"
:class="['chip', isCurrent({ kind: 'named', name: p }) && 'is-on']"
:disabled="locked || saving"
@click="emit('pick', { kind: 'named', name: p })"
>
{{ p }}
</button>
</div>
</template>
<h4>订阅线路</h4>
<p v-if="!kernelOnline" class="unavailable">
<IconOffline />
内核未连接。订阅里有哪些线路只有运行中的内核知道,配置文件里只有订阅地址
</p>
<p v-else-if="providerLines.length === 0" class="unavailable">订阅里没有线路</p>
<div v-else class="chips">
<button
v-for="line in providerLines"
:key="line"
type="button"
class="chip is-todo"
disabled
title="选中会自动建一个只装这条线路的筛选组——这部分还没做"
>
{{ line }}
</button>
</div>
</section>
</aside>
</template>
<style scoped>
.detail {
border: 1px solid var(--c-border);
border-radius: var(--r-md);
background: var(--c-surface);
padding: var(--s-4) var(--s-5) var(--s-5);
align-self: start;
position: sticky;
top: var(--s-6);
}
.top {
padding-bottom: var(--s-4);
border-bottom: 1px solid var(--c-border);
}
.title {
display: flex;
align-items: baseline;
gap: var(--s-3);
margin-bottom: var(--s-3);
}
.port {
font-size: var(--t-xl);
font-variant-numeric: tabular-nums;
}
.name {
color: var(--c-text-soft);
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.facts {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--s-1) var(--s-4);
margin: 0;
font-size: var(--t-sm);
}
.facts dt {
color: var(--c-text-faint);
}
.facts dd {
margin: 0;
}
.pick {
padding-top: var(--s-4);
}
h3 {
font-size: var(--t-sm);
font-weight: 600;
color: var(--c-text-soft);
margin-bottom: var(--s-3);
}
h4 {
font-size: var(--t-xs);
font-weight: 500;
color: var(--c-text-faint);
margin: var(--s-4) 0 var(--s-2);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: var(--s-2);
}
.chip {
padding: var(--s-1) var(--s-3);
border: 1px solid var(--c-border-strong);
border-radius: var(--r-sm);
background: var(--c-surface);
font-family: var(--f-mono);
font-size: var(--t-sm);
cursor: pointer;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chip:hover:not(:disabled) {
border-color: var(--c-accent);
color: var(--c-accent-text);
}
.chip.is-on {
background: var(--c-accent-soft);
border-color: var(--c-accent);
color: var(--c-accent-text);
font-weight: 500;
}
.chip:disabled {
cursor: not-allowed;
color: var(--c-text-faint);
border-color: var(--c-border);
}
.chip.is-on:disabled {
color: var(--c-accent-text);
border-color: color-mix(in srgb, var(--c-accent) 45%, transparent);
}
.locked,
.unavailable {
display: flex;
align-items: center;
gap: var(--s-2);
color: var(--c-text-faint);
font-size: var(--t-sm);
margin-bottom: var(--s-3);
line-height: 1.6;
}
.locked code {
color: var(--c-text-soft);
}
</style>