路由页一轮交互细化:草稿预览、预选、若干修正

上半部分收敛成一条规则:草稿和已保存一致时显示内核现状;不一致时显示
按草稿保存后的样子,将移除的画删除线、新增的虚线描边,即将被移除的当前
选中用危险色警示。点击语义只看卡片:现在就存在的成员点了立刻生效,保存
后才加入的点了是预选,保存成功后再作为运行时动作选上。预览的集合运算在
web/member-preview.ts,组件里只留接线。

其余:订阅选择器重写成可搜索多选(原平铺标签订阅一多就是一面墙);删除
入站换成页面内确认对话框;列表右列解析到最终出口并标注链式;「高级配置」
更名「配置」;表单重置改按内容比较(按引用的话任何一次运行时刷新都会冲掉
未保存的草稿,watch 传返回数组的 getter 同样触发这个问题);保存后先刷
运行时再换配置数据,避免卡片区闪空。
This commit is contained in:
2026-08-11 14:39:47 +08:00
parent 6bedce215c
commit a4d08892f7
10 changed files with 658 additions and 132 deletions
+49 -8
View File
@@ -14,7 +14,9 @@ import {
chainGroupName,
groupUses,
panelGroups,
proxyGroupName,
pruneGenerated,
sharedPeers,
syncGenerated,
readSetup,
userProviders,
@@ -27,6 +29,7 @@ import type { GroupView } from '../runtime'
import { optimisticSelect, providerLines, reloadRuntime, runtime } from '../runtime'
import { useDelayTest } from '../delay-test'
import { density } from '../prefs'
import ConfirmDialog from '../components/ConfirmDialog.vue'
import InboundList from '../components/InboundList.vue'
import InboundDetail from './InboundDetail.vue'
import InboundForm from './InboundForm.vue'
@@ -91,7 +94,12 @@ const panel = computed(() =>
: { groups: [], shared: false },
)
/** 共用的组是别人的,改不了它的来源,所以高级配置也不给。 */
/** 选中这一路落在共用组上时,还有哪些入站也走同一个组。决定「共用」标记显不显示。 */
const sharedWith = computed(() =>
selected.value ? sharedPeers(props.data, runtime.value, inbounds.value, selected.value) : [],
)
/** 共用的组是别人的,改不了它的来源,所以配置表单也不给。 */
const setup = computed<OutboundSetup | null>(() =>
selected.value && !panel.value.shared ? readSetup(props.data, selected.value.name) : null,
)
@@ -115,8 +123,9 @@ async function commit(raw: ConfigObject, success: string) {
done.value = ''
notice.value = ''
try {
await api.applyConfig(next, props.revision)
done.value = success
const result = await api.applyConfig(next, props.revision)
// 没重载有两种情况(内容没有变化、内核不在线只写了文件),都要如实说,不能只报成功
done.value = result.reloaded ? success : `${success}(${result.reloadMessage})`
emit('saved')
} catch (error) {
failure.value =
@@ -128,11 +137,25 @@ async function commit(raw: ConfigObject, success: string) {
}
}
/** 保存高级配置:改的是这一路的组从哪些订阅取成员、筛掉哪些。 */
async function saveSetup(next: OutboundSetup) {
/** 保存这一路的配置:组从哪些订阅取成员、筛掉哪些、开不开链式代理。 */
async function saveSetup(next: OutboundSetup, picks: { proxy: string | null; chain: string | null }) {
const target = selected.value
if (!target || saving.value) return
await commit(applySetup(clone(), target.name, next), `${target.name} 的代理配置已保存`)
if (failure.value) return
// 草稿态里预选的线路在保存成功、组按新配置就位之后逐个选上。这是运行时动作,
// 失败不影响已保存的配置:组停在内核的默认选中上,刷新后显示的就是真实状态。
const picked: Array<[string, string | null]> = [
[proxyGroupName(target.name), picks.proxy],
[chainGroupName(target.name), picks.chain],
]
let selectedAny = false
for (const [group, member] of picked) {
if (!member) continue
await api.selectProxy(group, member).catch(() => {})
selectedAny = true
}
if (selectedAny) await reloadRuntime()
}
/** 把一路还没接管的入站改成专属代理组模式。 */
@@ -192,10 +215,19 @@ async function selectMember(group: string, member: string) {
}
}
async function removeSelected() {
/** 待确认删除的那一路。非 null 时确认对话框打开着。 */
const removing = ref<Inbound | null>(null)
function askRemove() {
const target = selected.value
if (!target || target.listenerIndex === null || saving.value) return
if (!confirm(`删除入站「${target.name}」?端口 ${target.port} 将停止监听。`)) return
removing.value = target
}
async function removeConfirmed() {
const target = removing.value
removing.value = null
if (!target || target.listenerIndex === null || saving.value) return
const next = clone()
const listeners = (next.listeners as ConfigObject[]).filter((_, i) => i !== target.listenerIndex)
@@ -246,13 +278,14 @@ async function removeSelected() {
v-if="selected"
:inbound="selected"
:panel="panel"
:shared-with="sharedWith"
:chain="chainView"
:subscriptions="subscriptions"
:setup="setup"
:testing="testing"
:saving="saving"
@edit="openEdit"
@remove="removeSelected"
@remove="askRemove"
@select="selectMember"
@test="test"
@save="saveSetup"
@@ -271,6 +304,14 @@ async function removeSelected() {
@close="formOpen = false"
@submit="submitForm"
/>
<ConfirmDialog
:open="removing !== null"
:message="removing ? `删除入站「${removing.name}」?端口 ${removing.port} 将停止监听。` : ''"
action="删除"
@close="removing = null"
@confirm="removeConfirmed"
/>
</div>
</template>