Files
mihomo-patchbay/web/views/SubscriptionDetail.vue
T
im0o 129a76696b 仪表盘和订阅详情的操作按钮改 text-button,公网 IP 带旗帜
检测公网 IP、重启内核、订阅详情右上三个按钮都去掉边框,hover 出浅底。
检测结果由国家代码换成旗帜 emoji 加 IP;Windows 的系统字体没有旗帜
字形,浏览器会退回显示两个字母,和原来的效果一致。
2026-08-12 13:17:21 +08:00

264 lines
6.1 KiB
Vue
Raw Permalink 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">
/**
* 选中那个订阅的详情:地址、更新间隔、健康检查,和它带来的线路。
*
* 线路列表只读。这些线路不在内核的全局名字表里,不能被入站直接引用,
* 只能经策略组的 `use:` 整批引入,所以这里没有「换线」可点。
*/
import { computed } from 'vue'
import type { Subscription } from '@shared/providers.ts'
import { latestDelay } from '../api'
import type { ProviderInfo } from '../api'
import IconEdit from '~icons/ri/edit-line'
import IconDelete from '~icons/ri/delete-bin-line'
import IconRefresh from '~icons/ri/refresh-line'
import IconOffline from '~icons/ri/cloud-off-line'
const props = defineProps<{
subscription: Subscription
/** 运行时数据:当前有哪些线路、上次更新时间。内核没连接时是 undefined */
info: ProviderInfo | undefined
online: boolean
/** 正在等内核重新拉取 */
refreshing: boolean
/** 配置正在保存 */
saving: boolean
}>()
const emit = defineEmits<{
edit: []
remove: []
/** 让内核立刻重新拉取。运行时动作,不写配置文件 */
refresh: []
}>()
const lines = computed(() => props.info?.proxies ?? [])
function delayClass(ms: number | null): string {
if (ms === null) return ''
if (ms < 200) return 'd-ok'
if (ms < 500) return 'd-warn'
return ''
}
</script>
<template>
<aside class="detail">
<header class="top">
<div class="title">
<span class="name">{{ subscription.name }}</span>
<div class="acts">
<button type="button" :disabled="!online || refreshing" @click="emit('refresh')">
<IconRefresh /> {{ refreshing ? '更新中' : '更新订阅' }}
</button>
<button
type="button"
:disabled="saving || subscription.type === 'file'"
:title="subscription.type === 'file' ? '文件导入的订阅没有可编辑的字段' : undefined"
@click="emit('edit')"
>
<IconEdit /> 编辑
</button>
<button type="button" class="danger" :disabled="saving" @click="emit('remove')">
<IconDelete /> 删除
</button>
</div>
</div>
<dl class="facts">
<template v-if="subscription.type === 'file'">
<dt>文件</dt>
<dd class="mono url">{{ subscription.path ?? '—' }}</dd>
</template>
<template v-else>
<dt>地址</dt>
<dd class="mono url">{{ subscription.url }}</dd>
<dt>自动更新</dt>
<dd class="mono">
{{ subscription.interval !== null ? `每 ${subscription.interval} 秒` : '已关闭' }}
</dd>
</template>
<dt>健康检查</dt>
<dd class="mono">
<template v-if="subscription.healthCheck">
{{ subscription.healthCheck.url
}}{{ subscription.healthCheck.interval !== null ? `,每 ${subscription.healthCheck.interval} 秒` : '' }}
</template>
<template v-else>未开启</template>
</dd>
</dl>
</header>
<section class="block">
<h3>线路</h3>
<p v-if="!online" class="hint"><IconOffline /> 内核未连接</p>
<p v-else-if="lines.length === 0" class="hint">这个订阅现在没有线路</p>
<ol v-else class="lines">
<li v-for="line in lines" :key="line.name" class="row">
<span class="line-name">{{ line.name }}</span>
<span class="line-type mono">{{ line.type }}</span>
<span :class="['line-delay mono', delayClass(latestDelay(line))]">
{{ latestDelay(line) !== null ? `${latestDelay(line)} ms` : '—' }}
</span>
</li>
</ol>
</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);
}
.name {
font-size: var(--t-xl);
font-weight: 600;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.acts {
display: flex;
gap: var(--s-2);
margin-left: auto;
}
.acts button {
display: flex;
align-items: center;
gap: var(--s-1);
padding: var(--s-1) var(--s-2);
border: none;
border-radius: var(--r-sm);
background: none;
color: var(--c-text-soft);
font-size: var(--t-sm);
white-space: nowrap;
cursor: pointer;
}
.acts button:hover:not(:disabled) {
background: var(--c-surface-sunken);
color: var(--c-text);
}
.acts button.danger:hover:not(:disabled) {
color: var(--c-danger);
}
.acts button:disabled {
color: var(--c-text-faint);
cursor: not-allowed;
}
.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;
min-width: 0;
}
.url {
overflow-wrap: anywhere;
}
.block {
padding-top: var(--s-4);
}
h3 {
font-size: var(--t-sm);
font-weight: 600;
color: var(--c-text);
letter-spacing: 0.02em;
margin-bottom: var(--s-3);
}
.hint {
display: flex;
align-items: center;
gap: var(--s-2);
color: var(--c-text-faint);
font-size: var(--t-sm);
line-height: 1.6;
}
.lines {
list-style: none;
margin: 0;
padding: 0;
max-height: 420px;
overflow-y: auto;
}
.row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto 4.5rem;
align-items: baseline;
gap: var(--s-3);
padding: var(--s-1) 0;
font-size: var(--t-sm);
}
.row + .row {
border-top: 1px solid var(--c-border);
}
.line-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.line-type {
color: var(--c-text-faint);
font-size: var(--t-xs);
}
.line-delay {
text-align: right;
font-variant-numeric: tabular-nums;
color: var(--c-text-faint);
}
.d-ok {
color: var(--c-ok);
}
.d-warn {
color: var(--c-warn);
}
</style>