Files
mihomo-patchbay/web/views/SubscriptionDetail.vue
T
im0o 0eef680fba 配置页:订阅的增删改和立刻更新
写配置的纯逻辑在 shared/providers.ts。删除前 subscriptionReferences 同时查
直接 use 和经订阅副本(共用缓存文件 path)的间接引用,有被引用的就拦下并列出
策略组。订阅不允许改名:名字被 use: 和副本引用,同步所有引用不如删了重建。

页面还没挂进 App.vue 的导航,由集成者统一加。
2026-08-11 18:50:13 +08:00

251 lines
5.7 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">
/**
* 选中那个订阅的详情:地址、更新间隔、健康检查,和它带来的线路。
*
* 线路列表只读。这些线路不在内核的全局名字表里,不能被入站直接引用,
* 只能经策略组的 `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" @click="emit('edit')"><IconEdit /> 编辑</button>
<button type="button" class="danger" :disabled="saving" @click="emit('remove')">
<IconDelete /> 删除
</button>
</div>
</div>
<dl class="facts">
<dt>地址</dt>
<dd class="mono url">{{ subscription.url }}</dd>
<dt>更新间隔</dt>
<dd class="mono">
{{ subscription.interval !== null ? `${subscription.interval} 秒` : '未设置' }}
</dd>
<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-3);
border: 1px solid var(--c-border-strong);
border-radius: var(--r-sm);
background: var(--c-surface);
font-size: var(--t-sm);
white-space: nowrap;
cursor: pointer;
}
.acts button:hover:not(:disabled) {
border-color: var(--c-accent);
color: var(--c-accent-text);
}
.acts button.danger:hover:not(:disabled) {
border-color: var(--c-danger);
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>