Files
mihomo-patchbay/web/views/InboundList.vue
T
im0o 7a764f5fe1 搭出前端骨架和入站列表
设计基调定为:层次靠底色和一像素描边而不是投影,一屏最多两层容器,间距给足。
tokens.css 顶部写了这三条规则,后面的组件都是它们的推论。

入站列表把两套写法合并成一张表:老式顶层端口字段(mixed-port 这些)和 listeners 数组。
只画 listeners 的话,一台只用 mixed-port 的机器打开会是空板,而那个端口正在跑——
这等于对用户撒谎。合并逻辑在 shared/inbounds.ts,带 10 个测试。

后端顺带能提供打包后的静态文件了,一个进程就能跑起来。
2026-08-10 04:10:17 +08:00

208 lines
4.9 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 { collectInbounds } from '@shared/inbounds.ts'
import type { Inbound, OutboundMode } from '@shared/inbounds.ts'
import type { ConfigObject } from '../../vendor/config-editor/index.ts'
const props = defineProps<{ data: ConfigObject; kernelOnline: boolean }>()
const inbounds = computed(() => collectInbounds(props.data))
/** 出站去向在表格里怎么显示。「跟着规则走」是弱化的,因为它是默认状态而非用户的选择。 */
function outboundLabel(mode: OutboundMode): { text: string; muted: boolean } {
if (mode.kind === 'rules') return { text: '跟着规则走', muted: true }
if (mode.kind === 'direct') return { text: '直连', muted: false }
if (mode.kind === 'global') return { text: '全局', muted: false }
return { text: mode.name, muted: false }
}
function typeLabel(inbound: Inbound): string {
if (inbound.type === 'mixed') return 'HTTP + SOCKS5'
if (inbound.type === 'http') return 'HTTP'
if (inbound.type === 'socks') return 'SOCKS5'
if (inbound.type === 'redir') return '透明代理'
if (inbound.type === 'tproxy') return '透明代理'
return inbound.type
}
</script>
<template>
<section>
<header class="head">
<div>
<h2>入站</h2>
<p class="sub">每一路进来的流量,以及它从哪里出去。</p>
</div>
<button class="add" type="button" disabled title="下一步做">新增</button>
</header>
<ol class="rows">
<li v-for="inbound in inbounds" :key="inbound.id" class="row">
<div class="port">
<span class="num">{{ inbound.port }}</span>
<span class="type">{{ typeLabel(inbound) }}</span>
</div>
<div class="mid">
<div class="name">
{{ inbound.name }}
<span v-if="inbound.isDefault" class="tag">默认</span>
</div>
<div v-if="inbound.listen" class="listen mono">{{ inbound.listen }}</div>
</div>
<div class="wire" aria-hidden="true"></div>
<div class="out">
<span :class="['out-text', outboundLabel(inbound.outbound).muted && 'is-muted']">
{{ outboundLabel(inbound.outbound).text }}
</span>
<span v-if="inbound.ruleSet" class="ruleset">附加规则 {{ inbound.ruleSet }}</span>
</div>
</li>
</ol>
<p v-if="inbounds.length === 0" class="empty">
这份配置里没有任何入站。mihomo 不会监听任何端口。
</p>
<p v-if="inbounds.some((i) => i.isDefault)" class="footnote">
标着「默认」的入站由配置文件顶层的端口字段声明,这种写法没法单独指定出站,流量一律走分流规则。
</p>
</section>
</template>
<style scoped>
.head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--s-4);
margin-bottom: var(--s-5);
}
h2 {
font-size: var(--t-xl);
font-weight: 600;
letter-spacing: -0.01em;
}
.sub {
color: var(--c-text-soft);
font-size: var(--t-sm);
margin-top: var(--s-1);
}
.add {
padding: var(--s-2) var(--s-4);
border: 1px solid var(--c-border-strong);
border-radius: var(--r-sm);
background: var(--c-surface);
cursor: pointer;
}
.add:disabled {
color: var(--c-text-faint);
cursor: not-allowed;
}
/* 一层容器,内部用分割线分行——不做卡片套卡片 */
.rows {
list-style: none;
margin: 0;
padding: 0;
border: 1px solid var(--c-border);
border-radius: var(--r-md);
background: var(--c-surface);
overflow: hidden;
}
.row {
display: grid;
grid-template-columns: 7.5rem 1fr 3rem minmax(8rem, auto);
align-items: center;
gap: var(--s-4);
padding: var(--s-4) var(--s-5);
}
.row + .row {
border-top: 1px solid var(--c-border);
}
.port {
display: flex;
flex-direction: column;
gap: 2px;
}
.num {
font-family: var(--f-mono);
font-size: var(--t-lg);
font-variant-numeric: tabular-nums;
}
.type {
font-size: var(--t-xs);
color: var(--c-text-faint);
}
.name {
display: flex;
align-items: center;
gap: var(--s-2);
}
.tag {
padding: 1px var(--s-2);
border: 1px solid var(--c-border-strong);
border-radius: 999px;
font-size: var(--t-xs);
color: var(--c-text-soft);
line-height: 1.5;
}
.listen {
font-size: var(--t-xs);
color: var(--c-text-faint);
}
/* 插线板的那根线。它承载的信息是「左边这个口通向右边那个出口」 */
.wire {
height: 1px;
background: linear-gradient(
to right,
var(--c-border-strong),
color-mix(in srgb, var(--c-accent) 55%, transparent)
);
}
.out {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
text-align: right;
}
.out-text {
font-weight: 500;
}
.out-text.is-muted {
font-weight: 400;
color: var(--c-text-faint);
}
.ruleset {
font-size: var(--t-xs);
color: var(--c-text-faint);
}
.empty,
.footnote {
color: var(--c-text-faint);
font-size: var(--t-sm);
margin-top: var(--s-4);
line-height: 1.7;
}
</style>