Files
im0o 275f536429 追加 Hosts、路径撞名拦截、连接页表头随行滚动、GEO 借内核通路
设置页内核参数加 hosts 行,铅笔进弹窗编辑(顶层 hosts: 键,
config.go:454,值单个或数组 parseHosts config.go:1131)。弹窗里随便改、
保存才一次性提交——写配置有几秒的校验重载,逐行失焦提交会把等待塞进
每次输入。没动过的行原样写回,数组不被重整形。

订阅和规则组新增时查缓存路径撞名:路径由名字净化而来,「订阅 A」和
「订阅/A」是两个名字、同一个文件,名字查重挡不住,会互相覆盖缓存。
纯函数在 shared 带测试,页面提交前拦。

连接页表头挪进滚动容器加 sticky:.scroller 声明 overflow-y 后按规范
连带拿到横向滚动上下文,表头留在外面时窄屏横向滚动两者脱节。表头
40px 的窗口偏移由 overscan 8 行吸收,不配 scrollMargin。

GEO 单文件下载先借内核的混合端口出去再退直连——内核自己更新 GEO 走
的是它的分流,Patchbay 直连 GitHub 不一定通。ECharts 拆独立 chunk,
业务代码改动不再让这 500KB 跟着换 hash。
2026-08-12 02:47:17 +08:00

290 lines
6.6 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">
/**
* 追加 Hosts 的编辑弹窗:配置顶层的 `hosts:` 映射(config.go:454,值可以是
* 单个 IP/域名或数组,parseHosts config.go:1131)。
*
* 在弹窗里随便改,点保存才一次性提交——写配置要走校验和重载,每行失焦
* 就提交会把几秒的等待塞进每一次输入。
*
* 值的往返规则:没动过的行原样保留(数组仍是数组);动过的行按显示文本
* 重建,含逗号拆成数组,否则存字符串。
*/
import { ref, watch } from 'vue'
import type { ConfigObject } from '../../vendor/config-editor/index.ts'
import IconAdd from '~icons/ri/add-line'
import IconClose from '~icons/ri/close-line'
const props = defineProps<{ open: boolean; hosts: ConfigObject }>()
const emit = defineEmits<{ close: []; save: [hosts: ConfigObject] }>()
interface Row {
key: string
value: string
/** 值被编辑过。没动过的保存时用 original 原样写回 */
dirty: boolean
original: unknown
}
const dialog = ref<HTMLDialogElement | null>(null)
const rows = ref<Row[]>([])
/** 保存被拦下的行号(键空着或和别的行重复) */
const badRows = ref<Set<number>>(new Set())
function display(value: unknown): string {
if (typeof value === 'string') return value
if (Array.isArray(value)) return value.map(String).join(', ')
return JSON.stringify(value)
}
watch(
() => props.open,
(open) => {
if (open) {
rows.value = Object.entries(props.hosts).map(([key, value]) => ({
key,
value: display(value),
dirty: false,
original: value,
}))
badRows.value = new Set()
dialog.value?.showModal()
} else {
dialog.value?.close()
}
},
)
function addRow() {
rows.value = [...rows.value, { key: '', value: '', dirty: true, original: undefined }]
}
function removeRow(index: number) {
rows.value = rows.value.filter((_, i) => i !== index)
badRows.value = new Set()
}
function submit() {
const bad = new Set<number>()
const seen = new Set<string>()
const next: ConfigObject = {}
for (const [index, row] of rows.value.entries()) {
const key = row.key.trim()
const raw = row.value.trim()
if (key === '' && raw === '') continue
if (key === '' || seen.has(key)) {
bad.add(index)
continue
}
seen.add(key)
if (!row.dirty && row.original !== undefined) {
next[key] = row.original as ConfigObject[string]
} else {
next[key] = raw.includes(',')
? raw.split(',').map((part) => part.trim()).filter(Boolean)
: raw
}
}
if (bad.size > 0) {
badRows.value = bad
return
}
emit('save', next)
}
/** 点在遮罩上就关闭。内容区的点击 target 是里面的元素,到不了 dialog 本身。 */
function onBackdrop(event: MouseEvent) {
if (event.target === dialog.value) emit('close')
}
</script>
<template>
<dialog
ref="dialog"
class="dlg"
@click="onBackdrop"
@close="emit('close')"
@cancel.prevent="emit('close')"
>
<div class="body">
<header class="head">
<h2>追加 Hosts</h2>
<button type="button" class="add" @click="addRow"><IconAdd /> 添加</button>
</header>
<p v-if="rows.length === 0" class="faint">
没有条目。hosts 把域名固定解析到指定的 IP 或别名,值里用逗号分隔可以写多个。
</p>
<div v-else class="list">
<div v-for="(row, i) in rows" :key="i" :class="['hrow', badRows.has(i) && 'is-bad']">
<input
v-model="row.key"
class="input mono"
type="text"
placeholder="域名,如 router.lan"
autocomplete="off"
spellcheck="false"
/>
<input
v-model="row.value"
class="input mono grow"
type="text"
placeholder="IP 或别名,逗号分隔多个"
autocomplete="off"
spellcheck="false"
@input="row.dirty = true"
/>
<button type="button" class="x" title="删除这一行" @click="removeRow(i)">
<IconClose />
</button>
</div>
</div>
<footer class="foot">
<button type="button" class="btn" @click="emit('close')">取消</button>
<button type="button" class="btn go" @click="submit">保存</button>
</footer>
</div>
</dialog>
</template>
<style scoped>
.dlg {
width: min(560px, calc(100vw - 2 * var(--s-5)));
padding: 0;
border: 1px solid var(--c-border-strong);
border-radius: var(--r-lg);
background: var(--c-surface);
color: var(--c-text);
}
.dlg::backdrop {
background: rgb(0 0 0 / 0.45);
}
.body {
padding: var(--s-5);
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--s-4);
}
.head h2 {
font-size: var(--t-lg);
font-weight: 600;
}
.add {
display: inline-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);
cursor: pointer;
}
.add:hover {
background: var(--c-surface-sunken);
color: var(--c-text);
}
.add svg {
width: 15px;
height: 15px;
}
.faint {
color: var(--c-text-faint);
font-size: var(--t-sm);
line-height: var(--line);
}
.list {
display: flex;
flex-direction: column;
gap: var(--s-2);
max-height: 50vh;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--c-border-strong) transparent;
}
.hrow {
display: flex;
align-items: center;
gap: var(--s-2);
}
.input {
width: 180px;
padding: var(--s-2) var(--s-3);
border: 1px solid var(--c-border-strong);
border-radius: var(--r-sm);
background: var(--c-bg);
font-size: var(--t-sm);
}
.grow {
flex: 1;
min-width: 0;
}
.is-bad .input {
border-color: var(--c-danger);
}
.x {
flex: none;
display: flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border: none;
border-radius: var(--r-sm);
background: none;
color: var(--c-text-faint);
cursor: pointer;
}
.x:hover {
background: var(--c-surface-sunken);
color: var(--c-text);
}
.foot {
display: flex;
justify-content: flex-end;
gap: var(--s-3);
margin-top: var(--s-5);
}
.btn {
padding: var(--s-2) var(--s-4);
border: 1px solid var(--c-border-strong);
border-radius: var(--r-sm);
background: var(--c-surface);
cursor: pointer;
}
.btn:hover {
border-color: var(--c-accent);
color: var(--c-accent-text);
}
.go {
border-color: transparent;
background: var(--c-accent);
color: var(--c-bg);
}
.go:hover {
border-color: transparent;
color: var(--c-bg);
opacity: 0.9;
}
</style>