出站选项改成卡片,带协议和延迟
参照 fl-clash:一张卡三行——名字、协议(策略组显示「类型(当前选中)」)、最近一次延迟。 延迟按 200/500 毫秒分三档着色,没测过就留空而不是显示 0。 这些数据配置文件里都没有,全部来自内核的 /proxies。 另外主区最大宽度从 960 提到 1600,去掉选中行的左侧高亮条。
This commit is contained in:
+14
-2
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { ApiError, api, getToken, setToken } from './api'
|
import { ApiError, api, getToken, setToken } from './api'
|
||||||
import type { ConfigSnapshot } from './api'
|
import type { ConfigSnapshot, ProxyInfo } from './api'
|
||||||
import { NAV } from './nav'
|
import { NAV } from './nav'
|
||||||
import { density, toggleDensity } from './prefs'
|
import { density, toggleDensity } from './prefs'
|
||||||
import RouterPage from './views/RouterPage.vue'
|
import RouterPage from './views/RouterPage.vue'
|
||||||
@@ -12,6 +12,7 @@ import IconUnfold from '~icons/ri/sidebar-unfold-line'
|
|||||||
const snapshot = ref<ConfigSnapshot | null>(null)
|
const snapshot = ref<ConfigSnapshot | null>(null)
|
||||||
const kernel = ref<{ online: boolean; version?: string }>({ online: false })
|
const kernel = ref<{ online: boolean; version?: string }>({ online: false })
|
||||||
const providerLines = ref<string[]>([])
|
const providerLines = ref<string[]>([])
|
||||||
|
const runtime = ref<Record<string, ProxyInfo>>({})
|
||||||
const needsToken = ref(!getToken())
|
const needsToken = ref(!getToken())
|
||||||
const loadError = ref('')
|
const loadError = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -28,6 +29,7 @@ async function load() {
|
|||||||
// 订阅里有哪些线路只有运行中的内核知道,配置文件里只有一个订阅地址。
|
// 订阅里有哪些线路只有运行中的内核知道,配置文件里只有一个订阅地址。
|
||||||
// 拿不到不算错误——界面照常能编辑,只是订阅线路那一档不可选。
|
// 拿不到不算错误——界面照常能编辑,只是订阅线路那一档不可选。
|
||||||
providerLines.value = status.online ? await readProviderLines() : []
|
providerLines.value = status.online ? await readProviderLines() : []
|
||||||
|
runtime.value = status.online ? await readRuntime() : {}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ApiError && error.unauthorized) {
|
if (error instanceof ApiError && error.unauthorized) {
|
||||||
needsToken.value = true
|
needsToken.value = true
|
||||||
@@ -51,6 +53,15 @@ async function readProviderLines(): Promise<string[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 出站的运行时状态:协议类型、策略组当前选中了谁、最近一次延迟。 */
|
||||||
|
async function readRuntime(): Promise<Record<string, ProxyInfo>> {
|
||||||
|
try {
|
||||||
|
return (await api.proxies()).proxies
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function submitToken(value: string) {
|
function submitToken(value: string) {
|
||||||
setToken(value)
|
setToken(value)
|
||||||
void load()
|
void load()
|
||||||
@@ -131,6 +142,7 @@ const current = computed(() => NAV.find((n) => n.key === active.value))
|
|||||||
:revision="snapshot.revision"
|
:revision="snapshot.revision"
|
||||||
:kernel-online="kernel.online"
|
:kernel-online="kernel.online"
|
||||||
:provider-lines="providerLines"
|
:provider-lines="providerLines"
|
||||||
|
:runtime="runtime"
|
||||||
@changed="load"
|
@changed="load"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -316,7 +328,7 @@ const current = computed(() => NAV.find((n) => n.key === active.value))
|
|||||||
/* ── 主区 ───────────────────────────────────── */
|
/* ── 主区 ───────────────────────────────────── */
|
||||||
.main {
|
.main {
|
||||||
padding: var(--s-6) var(--s-6) var(--s-7);
|
padding: var(--s-6) var(--s-6) var(--s-7);
|
||||||
max-width: 960px;
|
max-width: 1600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note {
|
.note {
|
||||||
|
|||||||
+23
@@ -72,6 +72,23 @@ export interface ApplyFailure {
|
|||||||
restored: boolean
|
restored: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProxyInfo {
|
||||||
|
/** 协议类型(Vmess、Vless…)或策略组类型(Selector、URLTest…) */
|
||||||
|
type: string
|
||||||
|
/** 策略组当前选中的成员。不是策略组就没有这个字段 */
|
||||||
|
now?: string
|
||||||
|
/** 策略组的成员列表 */
|
||||||
|
all?: string[]
|
||||||
|
/** 延迟测试记录,最后一条是最近一次。delay 为 0 表示测失败 */
|
||||||
|
history?: { delay: number }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 最近一次延迟。没测过或测失败都返回 null。 */
|
||||||
|
export function latestDelay(info: ProxyInfo | undefined): number | null {
|
||||||
|
const last = info?.history?.at(-1)?.delay
|
||||||
|
return last === undefined || last === 0 ? null : last
|
||||||
|
}
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
readConfig: () => request<ConfigSnapshot>('GET', '/api/config'),
|
readConfig: () => request<ConfigSnapshot>('GET', '/api/config'),
|
||||||
|
|
||||||
@@ -85,6 +102,12 @@ export const api = {
|
|||||||
|
|
||||||
kernel: () => request<{ online: boolean; version?: string }>('GET', '/api/kernel'),
|
kernel: () => request<{ online: boolean; version?: string }>('GET', '/api/kernel'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有出站的运行时状态:协议类型、策略组当前选中了谁、最近一次延迟。
|
||||||
|
* 这些配置文件里都没有,只能问运行中的内核。
|
||||||
|
*/
|
||||||
|
proxies: () => request<{ proxies: Record<string, ProxyInfo> }>('GET', '/api/kernel/proxies'),
|
||||||
|
|
||||||
/** 订阅里当前有哪些线路。配置文件给不出这个答案,只能问运行中的内核。 */
|
/** 订阅里当前有哪些线路。配置文件给不出这个答案,只能问运行中的内核。 */
|
||||||
providers: () =>
|
providers: () =>
|
||||||
request<{ providers: Record<string, { vehicleType: string; proxies: { name: string }[] }> }>(
|
request<{ providers: Record<string, { vehicleType: string; proxies: { name: string }[] }> }>(
|
||||||
|
|||||||
+185
-76
@@ -1,39 +1,107 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { Inbound, OutboundMode } from '@shared/inbounds.ts'
|
import type { Inbound, OutboundMode } from '@shared/inbounds.ts'
|
||||||
|
import { latestDelay } from '../api'
|
||||||
|
import type { ProxyInfo } from '../api'
|
||||||
import IconLock from '~icons/ri/lock-line'
|
import IconLock from '~icons/ri/lock-line'
|
||||||
import IconOffline from '~icons/ri/cloud-off-line'
|
import IconOffline from '~icons/ri/cloud-off-line'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
inbound: Inbound
|
inbound: Inbound
|
||||||
/** 策略组名字 */
|
|
||||||
groups: string[]
|
groups: string[]
|
||||||
/** 顶层 proxies 里手写的线路 */
|
|
||||||
ownProxies: string[]
|
ownProxies: string[]
|
||||||
/** 订阅带来的线路。只有内核在线时才拿得到 */
|
|
||||||
providerLines: string[]
|
providerLines: string[]
|
||||||
|
/** 运行时状态,键是出站名字。内核没连接时是空的 */
|
||||||
|
runtime: Record<string, ProxyInfo>
|
||||||
kernelOnline: boolean
|
kernelOnline: boolean
|
||||||
saving: boolean
|
saving: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{ pick: [mode: OutboundMode] }>()
|
const emit = defineEmits<{ pick: [mode: OutboundMode] }>()
|
||||||
|
|
||||||
const BUILTINS: Array<{ mode: OutboundMode; label: string; hint: string }> = [
|
interface Option {
|
||||||
{ mode: { kind: 'rules' }, label: 'RULE', hint: '不指定出站,流量走分流规则' },
|
key: string
|
||||||
{ mode: { kind: 'direct' }, label: 'DIRECT', hint: '不经过任何代理' },
|
mode: OutboundMode
|
||||||
{ mode: { kind: 'global' }, label: 'GLOBAL', hint: '跟随 GLOBAL 组当前选中的出站' },
|
label: string
|
||||||
]
|
/** 卡片第二行。策略组显示「类型(当前选中)」,线路显示协议 */
|
||||||
|
sub: string
|
||||||
|
delay: number | null
|
||||||
|
disabled: boolean
|
||||||
|
/** 不可选时的原因,鼠标悬停显示 */
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
const current = computed(() => props.inbound.outbound)
|
/** 默认入站没有 proxy 字段可写,整块都不可选。 */
|
||||||
|
const locked = computed(() => props.inbound.isDefault)
|
||||||
|
|
||||||
|
function describe(name: string): { sub: string; delay: number | null } {
|
||||||
|
const info = props.runtime[name]
|
||||||
|
if (!info) return { sub: '', delay: null }
|
||||||
|
const sub = info.now ? `${info.type}(${info.now})` : info.type
|
||||||
|
return { sub, delay: latestDelay(info) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const builtins = computed<Option[]>(() =>
|
||||||
|
(
|
||||||
|
[
|
||||||
|
{ mode: { kind: 'rules' } as OutboundMode, label: 'RULE', sub: '走分流规则' },
|
||||||
|
{ mode: { kind: 'direct' } as OutboundMode, label: 'DIRECT', sub: '不经过代理' },
|
||||||
|
{ mode: { kind: 'global' } as OutboundMode, label: 'GLOBAL', sub: '跟随全局选择' },
|
||||||
|
] as const
|
||||||
|
).map((b) => ({
|
||||||
|
key: b.label,
|
||||||
|
mode: b.mode,
|
||||||
|
label: b.label,
|
||||||
|
sub: b.sub,
|
||||||
|
delay: null,
|
||||||
|
disabled: locked.value,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
function named(names: string[]): Option[] {
|
||||||
|
return names.map((name) => {
|
||||||
|
const { sub, delay } = describe(name)
|
||||||
|
return {
|
||||||
|
key: name,
|
||||||
|
mode: { kind: 'named', name } as OutboundMode,
|
||||||
|
label: name,
|
||||||
|
sub,
|
||||||
|
delay,
|
||||||
|
disabled: locked.value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupOptions = computed(() => named(props.groups))
|
||||||
|
const ownOptions = computed(() => named(props.ownProxies))
|
||||||
|
|
||||||
|
const lineOptions = computed<Option[]>(() =>
|
||||||
|
props.providerLines.map((name) => {
|
||||||
|
const { sub, delay } = describe(name)
|
||||||
|
return {
|
||||||
|
key: name,
|
||||||
|
mode: { kind: 'named', name } as OutboundMode,
|
||||||
|
label: name,
|
||||||
|
sub,
|
||||||
|
delay,
|
||||||
|
disabled: true,
|
||||||
|
reason: '订阅线路不能被直接引用,选中它需要自动建一个筛选组——这部分还没做',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
function isCurrent(mode: OutboundMode): boolean {
|
function isCurrent(mode: OutboundMode): boolean {
|
||||||
const now = current.value
|
const now = props.inbound.outbound
|
||||||
if (now.kind !== mode.kind) return false
|
if (now.kind !== mode.kind) return false
|
||||||
return now.kind !== 'named' || now.name === (mode as { name: string }).name
|
return now.kind !== 'named' || now.name === (mode as { name: string }).name
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 默认入站没有 proxy 字段可写,出站只能是 RULE。 */
|
function delayClass(ms: number | null): string {
|
||||||
const locked = computed(() => props.inbound.isDefault)
|
if (ms === null) return 'd-none'
|
||||||
|
if (ms < 200) return 'd-ok'
|
||||||
|
if (ms < 500) return 'd-warn'
|
||||||
|
return 'd-bad'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -56,73 +124,85 @@ const locked = computed(() => props.inbound.isDefault)
|
|||||||
<section class="pick">
|
<section class="pick">
|
||||||
<h3>出站</h3>
|
<h3>出站</h3>
|
||||||
|
|
||||||
<p v-if="locked" class="locked">
|
<p v-if="locked" class="aside-note">
|
||||||
<IconLock />
|
<IconLock />
|
||||||
由 <code>{{ inbound.legacyKey }}</code> 声明,这种写法没有 proxy 字段,只能走 RULE
|
由 <code>{{ inbound.legacyKey }}</code> 声明,没有 proxy 字段,只能走 RULE
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="chips">
|
<div class="grid">
|
||||||
<button
|
<button
|
||||||
v-for="b in BUILTINS"
|
v-for="opt in builtins"
|
||||||
:key="b.label"
|
:key="opt.key"
|
||||||
type="button"
|
type="button"
|
||||||
:class="['chip', isCurrent(b.mode) && 'is-on']"
|
:class="['card', isCurrent(opt.mode) && 'is-on']"
|
||||||
:disabled="locked || saving"
|
:disabled="opt.disabled || saving"
|
||||||
:title="b.hint"
|
@click="emit('pick', opt.mode)"
|
||||||
@click="emit('pick', b.mode)"
|
|
||||||
>
|
>
|
||||||
{{ b.label }}
|
<span class="card-name">{{ opt.label }}</span>
|
||||||
|
<span class="card-sub">{{ opt.sub }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="groups.length">
|
<template v-if="groupOptions.length">
|
||||||
<h4>策略组</h4>
|
<h4>策略组</h4>
|
||||||
<div class="chips">
|
<div class="grid">
|
||||||
<button
|
<button
|
||||||
v-for="g in groups"
|
v-for="opt in groupOptions"
|
||||||
:key="g"
|
:key="opt.key"
|
||||||
type="button"
|
type="button"
|
||||||
:class="['chip', isCurrent({ kind: 'named', name: g }) && 'is-on']"
|
:class="['card', isCurrent(opt.mode) && 'is-on']"
|
||||||
:disabled="locked || saving"
|
:disabled="opt.disabled || saving"
|
||||||
@click="emit('pick', { kind: 'named', name: g })"
|
@click="emit('pick', opt.mode)"
|
||||||
>
|
>
|
||||||
{{ g }}
|
<span class="card-name">{{ opt.label }}</span>
|
||||||
|
<span class="card-sub">{{ opt.sub || '—' }}</span>
|
||||||
|
<span :class="['card-delay', delayClass(opt.delay)]">
|
||||||
|
{{ opt.delay === null ? '' : `${opt.delay} ms` }}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="ownProxies.length">
|
<template v-if="ownOptions.length">
|
||||||
<h4>自有线路</h4>
|
<h4>自有线路</h4>
|
||||||
<div class="chips">
|
<div class="grid">
|
||||||
<button
|
<button
|
||||||
v-for="p in ownProxies"
|
v-for="opt in ownOptions"
|
||||||
:key="p"
|
:key="opt.key"
|
||||||
type="button"
|
type="button"
|
||||||
:class="['chip', isCurrent({ kind: 'named', name: p }) && 'is-on']"
|
:class="['card', isCurrent(opt.mode) && 'is-on']"
|
||||||
:disabled="locked || saving"
|
:disabled="opt.disabled || saving"
|
||||||
@click="emit('pick', { kind: 'named', name: p })"
|
@click="emit('pick', opt.mode)"
|
||||||
>
|
>
|
||||||
{{ p }}
|
<span class="card-name">{{ opt.label }}</span>
|
||||||
|
<span class="card-sub">{{ opt.sub || '—' }}</span>
|
||||||
|
<span :class="['card-delay', delayClass(opt.delay)]">
|
||||||
|
{{ opt.delay === null ? '' : `${opt.delay} ms` }}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<h4>订阅线路</h4>
|
<h4>订阅线路</h4>
|
||||||
<p v-if="!kernelOnline" class="unavailable">
|
<p v-if="!kernelOnline" class="aside-note">
|
||||||
<IconOffline />
|
<IconOffline />
|
||||||
内核未连接。订阅里有哪些线路只有运行中的内核知道,配置文件里只有订阅地址
|
内核未连接。订阅里有哪些线路只有运行中的内核知道
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="providerLines.length === 0" class="unavailable">订阅里没有线路</p>
|
<p v-else-if="lineOptions.length === 0" class="aside-note">订阅里没有线路</p>
|
||||||
<div v-else class="chips">
|
<div v-else class="grid">
|
||||||
<button
|
<button
|
||||||
v-for="line in providerLines"
|
v-for="opt in lineOptions"
|
||||||
:key="line"
|
:key="opt.key"
|
||||||
type="button"
|
type="button"
|
||||||
class="chip is-todo"
|
class="card"
|
||||||
disabled
|
disabled
|
||||||
title="选中会自动建一个只装这条线路的筛选组——这部分还没做"
|
:title="opt.reason"
|
||||||
>
|
>
|
||||||
{{ line }}
|
<span class="card-name">{{ opt.label }}</span>
|
||||||
|
<span class="card-sub">{{ opt.sub || '—' }}</span>
|
||||||
|
<span :class="['card-delay', delayClass(opt.delay)]">
|
||||||
|
{{ opt.delay === null ? '' : `${opt.delay} ms` }}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -196,54 +276,83 @@ h4 {
|
|||||||
font-size: var(--t-xs);
|
font-size: var(--t-xs);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--c-text-faint);
|
color: var(--c-text-faint);
|
||||||
margin: var(--s-4) 0 var(--s-2);
|
margin: var(--s-5) 0 var(--s-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chips {
|
.grid {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
|
||||||
gap: var(--s-2);
|
gap: var(--s-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip {
|
.card {
|
||||||
padding: var(--s-1) var(--s-3);
|
display: flex;
|
||||||
border: 1px solid var(--c-border-strong);
|
flex-direction: column;
|
||||||
|
gap: var(--s-1);
|
||||||
|
min-width: 0;
|
||||||
|
padding: var(--s-3);
|
||||||
|
border: 1px solid var(--c-border);
|
||||||
border-radius: var(--r-sm);
|
border-radius: var(--r-sm);
|
||||||
background: var(--c-surface);
|
background: var(--c-surface-sunken);
|
||||||
font-family: var(--f-mono);
|
text-align: left;
|
||||||
font-size: var(--t-sm);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
max-width: 100%;
|
}
|
||||||
|
|
||||||
|
.card:hover:not(:disabled) {
|
||||||
|
border-color: var(--c-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.is-on {
|
||||||
|
border-color: var(--c-accent);
|
||||||
|
background: var(--c-accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-name {
|
||||||
|
font-size: var(--t-sm);
|
||||||
|
font-weight: 500;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip:hover:not(:disabled) {
|
.card.is-on .card-name {
|
||||||
border-color: var(--c-accent);
|
|
||||||
color: var(--c-accent-text);
|
color: var(--c-accent-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip.is-on {
|
.card-sub {
|
||||||
background: var(--c-accent-soft);
|
font-size: var(--t-xs);
|
||||||
border-color: var(--c-accent);
|
|
||||||
color: var(--c-accent-text);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chip:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
color: var(--c-text-faint);
|
color: var(--c-text-faint);
|
||||||
border-color: var(--c-border);
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip.is-on:disabled {
|
.card-delay {
|
||||||
color: var(--c-accent-text);
|
font-family: var(--f-mono);
|
||||||
border-color: color-mix(in srgb, var(--c-accent) 45%, transparent);
|
font-size: var(--t-xs);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
min-height: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.locked,
|
.d-ok {
|
||||||
.unavailable {
|
color: var(--c-ok);
|
||||||
|
}
|
||||||
|
.d-warn {
|
||||||
|
color: var(--c-warn);
|
||||||
|
}
|
||||||
|
.d-bad {
|
||||||
|
color: var(--c-danger);
|
||||||
|
}
|
||||||
|
.d-none {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-note {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--s-2);
|
gap: var(--s-2);
|
||||||
@@ -253,7 +362,7 @@ h4 {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.locked code {
|
.aside-note code {
|
||||||
color: var(--c-text-soft);
|
color: var(--c-text-soft);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { collectInbounds, collectOutboundNames, outboundToProxyField } from '@sh
|
|||||||
import type { Inbound, OutboundMode } from '@shared/inbounds.ts'
|
import type { Inbound, OutboundMode } from '@shared/inbounds.ts'
|
||||||
import type { ConfigObject } from '../../vendor/config-editor/index.ts'
|
import type { ConfigObject } from '../../vendor/config-editor/index.ts'
|
||||||
import { ApiError, api } from '../api'
|
import { ApiError, api } from '../api'
|
||||||
|
import type { ProxyInfo } from '../api'
|
||||||
import { density } from '../prefs'
|
import { density } from '../prefs'
|
||||||
import InboundDetail from './InboundDetail.vue'
|
import InboundDetail from './InboundDetail.vue'
|
||||||
import IconAdd from '~icons/ri/add-line'
|
import IconAdd from '~icons/ri/add-line'
|
||||||
@@ -13,6 +14,7 @@ const props = defineProps<{
|
|||||||
revision: string
|
revision: string
|
||||||
kernelOnline: boolean
|
kernelOnline: boolean
|
||||||
providerLines: string[]
|
providerLines: string[]
|
||||||
|
runtime: Record<string, ProxyInfo>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{ changed: [] }>()
|
const emit = defineEmits<{ changed: [] }>()
|
||||||
@@ -145,6 +147,7 @@ async function pickOutbound(mode: OutboundMode) {
|
|||||||
:groups="outbounds.groups"
|
:groups="outbounds.groups"
|
||||||
:own-proxies="outbounds.ownProxies"
|
:own-proxies="outbounds.ownProxies"
|
||||||
:provider-lines="providerLines"
|
:provider-lines="providerLines"
|
||||||
|
:runtime="runtime"
|
||||||
:kernel-online="kernelOnline"
|
:kernel-online="kernelOnline"
|
||||||
:saving="saving"
|
:saving="saving"
|
||||||
@pick="pickOutbound"
|
@pick="pickOutbound"
|
||||||
@@ -251,7 +254,6 @@ async function pickOutbound(mode: OutboundMode) {
|
|||||||
gap: var(--s-3);
|
gap: var(--s-3);
|
||||||
padding: var(--s-3) var(--s-4);
|
padding: var(--s-3) var(--s-4);
|
||||||
border: none;
|
border: none;
|
||||||
border-left: 2px solid transparent;
|
|
||||||
background: none;
|
background: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -263,7 +265,6 @@ async function pickOutbound(mode: OutboundMode) {
|
|||||||
|
|
||||||
.line.is-on {
|
.line.is-on {
|
||||||
background: var(--c-accent-soft);
|
background: var(--c-accent-soft);
|
||||||
border-left-color: var(--c-accent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.port {
|
.port {
|
||||||
|
|||||||
Reference in New Issue
Block a user