diff --git a/web/App.vue b/web/App.vue
index 4683305..b103444 100644
--- a/web/App.vue
+++ b/web/App.vue
@@ -28,9 +28,11 @@ async function load() {
refreshing.value = true
loadError.value = ''
try {
+ // 先刷运行时再换配置数据。反过来的话,保存后界面已经按新配置渲染,
+ // 而新建的组在运行时里还查不到,成员卡片会先闪空一下再出现。
+ await reloadRuntime()
snapshot.value = await api.readConfig()
needsToken.value = false
- await reloadRuntime()
} catch (error) {
if (error instanceof ApiError && error.unauthorized) {
clearToken()
diff --git a/web/components/ConfirmDialog.vue b/web/components/ConfirmDialog.vue
new file mode 100644
index 0000000..98aece5
--- /dev/null
+++ b/web/components/ConfirmDialog.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
diff --git a/web/components/InboundList.vue b/web/components/InboundList.vue
index 17cbf71..bbaa40a 100644
--- a/web/components/InboundList.vue
+++ b/web/components/InboundList.vue
@@ -6,8 +6,10 @@
*/
import { inboundTypeLabel, isUnsetOutbound, outboundLabel } from '@shared/inbounds.ts'
import { displayName, isGenerated } from '@shared/inbound-groups.ts'
+import { resolveChain } from '@shared/outbound-chain.ts'
import type { Inbound } from '@shared/inbounds.ts'
-import { runtime } from '../runtime'
+import { isChainedLine, runtime } from '../runtime'
+import IconLink from '~icons/ri/links-line'
defineProps<{ inbounds: Inbound[] }>()
const selected = defineModel('selected', { required: true })
@@ -15,13 +17,19 @@ const selected = defineModel('selected', { required: true })
/**
* 这一行右侧显示什么。
*
- * 指向 Patchbay 自动创建的代理组时显示那个组当前选中的线路,而不是组名——组名是
- * 「patchbay:」加入站名,和左边的名字列完全重复,等于一列没有信息。
+ * 指向 Patchbay 自动创建的代理组时,顺着「当前选中」解析到最终出口——组名和左边的
+ * 名字列完全重复,而组选中 AUTO 时「AUTO」也不是答案,用户要的是流量走哪条线。
+ * 出口经过链式代理入口时带一个标记,和详情里的流量去向对得上。
*/
-function outboundOf(inbound: Inbound): string {
+function outboundOf(inbound: Inbound): { text: string; chained: boolean } {
const mode = inbound.outbound
- if (mode.kind !== 'named' || !isGenerated(mode.name)) return outboundLabel(mode)
- return displayName(runtime.value[mode.name]?.now ?? '') || '未选择'
+ if (mode.kind !== 'named' || !isGenerated(mode.name))
+ return { text: outboundLabel(mode), chained: false }
+ const exit = resolveChain(runtime.value, runtime.value[mode.name]?.now ?? null).at(-1)
+ return {
+ text: displayName(exit ?? '') || '未选择',
+ chained: exit !== undefined && isChainedLine(exit),
+ }
}
@@ -40,7 +48,10 @@ function outboundOf(inbound: Inbound): string {
默认
- {{ outboundOf(inbound) }}
+
+
+
+ {{ outboundOf(inbound).text }}
@@ -114,8 +125,16 @@ function outboundOf(inbound: Inbound): string {
}
.out {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: var(--s-1);
font-size: var(--t-sm);
- text-align: right;
+}
+
+.chain {
+ display: flex;
+ color: var(--c-chain-text);
}
.out.is-muted {
diff --git a/web/components/OutboundCard.vue b/web/components/OutboundCard.vue
index 7cd865e..16b2c3b 100644
--- a/web/components/OutboundCard.vue
+++ b/web/components/OutboundCard.vue
@@ -19,6 +19,10 @@ defineProps<{
disabledReason?: string
/** 这张卡的改动正在提交 */
pending?: boolean
+ /** 草稿预览里保存后才加入的成员 */
+ draft?: boolean
+ /** 草稿预览里保存后将移除的成员 */
+ removed?: boolean
/** 这条线路正在测延迟 */
testing?: boolean
/** 能不能测延迟。内置出站没得测 */
@@ -48,6 +52,9 @@ function delayClass(ms: number | null): string {
selected && 'is-on',
disabledReason && 'is-off',
pending && 'is-pending',
+ draft && 'is-draft',
+ removed && 'is-removed',
+ !testable && !pending && 'no-foot',
]"
>
-