配置每次落盘都打顶层 patchbay: 标记(盐加版本的哈希)。内核忽略未知 顶层键(yaml.Unmarshal 没开严格模式,config/config.go:612)。刻意不对 内容做哈希:本项目只接管部分段落,手改其余段落是正常用法,内容哈希 会把合法手改判成无效。上传配置时靠标记分流:有标记的确认后替换, 没有的用更重的措辞确认导入;两者都走 config-store 完整保护链路 (新端点 POST /api/config/import)。自动备份列表从界面撤下,改成 下载当前配置 + 拖放上传。 内核参数一组编辑五个老式顶层端口和 keep-alive-interval(config.go:444, 整数秒),走带基准指纹的保存链路。资源一组接 POST /configs/geo (同步接口,透传给了 300 秒超时)。测速地址成为本地偏好,延迟测试 读它;prefs 模块顶层的浏览器全局加了存在性守卫——api.ts 开始引用 prefs,而 bun test 环境没有 localStorage。 退出登录挪到侧栏底部;密度设置从设置页撤下(侧栏按钮还在); 免责声明和关于做成子页(左上角返回),登录页输入框下加同一份 免责声明的弹层入口。
194 lines
4.0 KiB
Vue
194 lines
4.0 KiB
Vue
<script setup lang="ts">
|
||
import { ref, watch } from 'vue'
|
||
import { DISCLAIMER, DISCLAIMER_TITLE } from '../legal'
|
||
import IconClose from '~icons/ri/close-line'
|
||
|
||
const emit = defineEmits<{ submit: [token: string] }>()
|
||
const value = ref('')
|
||
|
||
const legalOpen = ref(false)
|
||
const legalDialog = ref<HTMLDialogElement | null>(null)
|
||
|
||
watch(legalOpen, (open) => {
|
||
if (open) legalDialog.value?.showModal()
|
||
else legalDialog.value?.close()
|
||
})
|
||
|
||
/** 点在遮罩上就关闭。内容区的点击 target 是里面的元素,到不了 dialog 本身。 */
|
||
function onBackdrop(event: MouseEvent) {
|
||
if (event.target === legalDialog.value) legalOpen.value = false
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="gate">
|
||
<form class="box" @submit.prevent="emit('submit', value)">
|
||
<h1>Patchbay</h1>
|
||
<p class="lead">需要访问令牌。</p>
|
||
<input
|
||
v-model="value"
|
||
class="field mono"
|
||
type="password"
|
||
autocomplete="off"
|
||
spellcheck="false"
|
||
placeholder="令牌"
|
||
autofocus
|
||
/>
|
||
<button class="go" type="submit" :disabled="!value.trim()">进入</button>
|
||
<p class="where">
|
||
令牌位于服务端配置文件的同级目录 <code>.patchbay-token</code>,启动时也会输出到控制台。
|
||
</p>
|
||
<button type="button" class="legal" @click="legalOpen = true">
|
||
使用 Patchbay 即视为同意《免责声明》
|
||
</button>
|
||
</form>
|
||
|
||
<dialog
|
||
ref="legalDialog"
|
||
class="dlg"
|
||
@click="onBackdrop"
|
||
@close="legalOpen = false"
|
||
@cancel.prevent="legalOpen = false"
|
||
>
|
||
<div class="dlg-body">
|
||
<header class="dlg-head">
|
||
<h2>{{ DISCLAIMER_TITLE }}</h2>
|
||
<button type="button" class="x" @click="legalOpen = false"><IconClose /></button>
|
||
</header>
|
||
<p v-for="(para, i) in DISCLAIMER" :key="i" class="para">{{ para }}</p>
|
||
</div>
|
||
</dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.gate {
|
||
display: grid;
|
||
place-items: center;
|
||
min-height: 100%;
|
||
padding: var(--s-6);
|
||
}
|
||
|
||
.box {
|
||
width: 100%;
|
||
max-width: 360px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--s-3);
|
||
}
|
||
|
||
h1 {
|
||
font-size: var(--t-2xl);
|
||
font-weight: 600;
|
||
letter-spacing: -0.01em;
|
||
}
|
||
|
||
.lead {
|
||
color: var(--c-text-soft);
|
||
margin-bottom: var(--s-2);
|
||
}
|
||
|
||
.field {
|
||
width: 100%;
|
||
padding: var(--s-3) var(--s-4);
|
||
border: 1px solid var(--c-border-strong);
|
||
border-radius: var(--r-sm);
|
||
background: var(--c-surface);
|
||
}
|
||
|
||
.go {
|
||
padding: var(--s-3) var(--s-4);
|
||
border: 1px solid transparent;
|
||
border-radius: var(--r-sm);
|
||
background: var(--c-accent);
|
||
color: var(--c-bg);
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.go:disabled {
|
||
background: var(--c-surface-sunken);
|
||
color: var(--c-text-faint);
|
||
border-color: var(--c-border);
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.where {
|
||
color: var(--c-text-faint);
|
||
font-size: var(--t-sm);
|
||
line-height: 1.6;
|
||
margin-top: var(--s-2);
|
||
}
|
||
|
||
.legal {
|
||
align-self: flex-start;
|
||
padding: 0;
|
||
border: none;
|
||
background: none;
|
||
color: var(--c-text-faint);
|
||
font-size: var(--t-xs);
|
||
cursor: pointer;
|
||
text-decoration: underline;
|
||
text-underline-offset: 3px;
|
||
}
|
||
.legal:hover {
|
||
color: var(--c-text-soft);
|
||
}
|
||
|
||
.dlg {
|
||
width: min(480px, 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);
|
||
}
|
||
|
||
.dlg-body {
|
||
padding: var(--s-5);
|
||
}
|
||
|
||
.dlg-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: var(--s-3);
|
||
}
|
||
|
||
.dlg-head h2 {
|
||
font-size: var(--t-lg);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.x {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 28px;
|
||
height: 28px;
|
||
border: 1px solid transparent;
|
||
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);
|
||
}
|
||
|
||
.para {
|
||
color: var(--c-text-soft);
|
||
font-size: var(--t-sm);
|
||
line-height: var(--line);
|
||
}
|
||
|
||
.para + .para {
|
||
margin-top: var(--s-3);
|
||
}
|
||
</style>
|