设置页第二轮:行内编辑、GEO 资源展开、恢复与导入分家
单值设置从常驻输入框改成 InlineEdit(纯文本加铅笔,Enter 或失焦提交, 失焦无效直接放弃——失焦是「我走了」,不该把人拽回来)。测速地址加恢复 默认按钮;内核参数逐项提交,不再攒一个保存按钮。 资源一节按文件展开:四类 GEO 文件的大小和更新时间由服务端 stat 内核 工作目录得来(GET /api/geo;文件名和大小写不敏感匹配照 constant/ path.go:18-20,193-224),下载地址可改(geox-url,config.go:363-368, 默认值 578-583)。行内的更新按钮仍是全量更新——内核只有这一个口子, 没变化的文件靠内容哈希跳过。 上传拆成两个入口:恢复只认带 Patchbay 标记的文件,导入收任意配置; 新增恢复策略(兼容只搬接管段落 listeners/proxy-groups/rules/ proxy-providers/rule-providers,覆盖整份替换)。平时是按钮,拖着文件 进窗口时整行变成投放区。
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 「纯文本 + 编辑铅笔」的行内编辑:平时只显示值,点铅笔换成输入框,
|
||||
* Enter 或失焦提交,Esc 放弃。设置页里一行一个值的场合都用它,
|
||||
* 免得整页铺满常驻的输入框。
|
||||
*
|
||||
* 校验交给 validate:Enter 提交没过就地标红不退出;失焦没过直接放弃改动
|
||||
* ——失焦是「我走了」,不该把人拽回来。
|
||||
*/
|
||||
import { nextTick, ref } from 'vue'
|
||||
import IconEdit from '~icons/ri/pencil-line'
|
||||
import IconReset from '~icons/ri/arrow-go-back-line'
|
||||
|
||||
const props = defineProps<{
|
||||
/** 当前值的原始文本,空串表示未设置 */
|
||||
value: string
|
||||
/** 空值时显示的说明(「未启用」「内核默认」) */
|
||||
placeholder?: string
|
||||
/** 值后面的单位 */
|
||||
unit?: string
|
||||
mono?: boolean
|
||||
/** 显示「恢复默认」按钮 */
|
||||
canReset?: boolean
|
||||
/** 提交进行中,禁用编辑入口 */
|
||||
busy?: boolean
|
||||
validate?: (value: string) => boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ submit: [value: string]; reset: [] }>()
|
||||
|
||||
const editing = ref(false)
|
||||
const draft = ref('')
|
||||
const bad = ref(false)
|
||||
const inputEl = ref<HTMLInputElement | null>(null)
|
||||
|
||||
async function open() {
|
||||
draft.value = props.value
|
||||
bad.value = false
|
||||
editing.value = true
|
||||
await nextTick()
|
||||
inputEl.value?.focus()
|
||||
inputEl.value?.select()
|
||||
}
|
||||
|
||||
/** lenient 是失焦路径:无效就放弃,不纠缠。 */
|
||||
function finish(lenient: boolean) {
|
||||
if (!editing.value) return
|
||||
const next = draft.value.trim()
|
||||
if (props.validate && !props.validate(next)) {
|
||||
if (lenient) editing.value = false
|
||||
else bad.value = true
|
||||
return
|
||||
}
|
||||
editing.value = false
|
||||
if (next !== props.value) emit('submit', next)
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
editing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="ie">
|
||||
<template v-if="!editing">
|
||||
<span :class="['val', mono && 'mono', !value && 'faint']" :title="value || undefined">
|
||||
{{ value || placeholder }}
|
||||
</span>
|
||||
<span v-if="unit && value" class="unit">{{ unit }}</span>
|
||||
<button
|
||||
v-if="canReset"
|
||||
type="button"
|
||||
class="ico"
|
||||
title="恢复默认"
|
||||
:disabled="busy"
|
||||
@click="emit('reset')"
|
||||
>
|
||||
<IconReset />
|
||||
</button>
|
||||
<button type="button" class="ico" title="编辑" :disabled="busy" @click="open">
|
||||
<IconEdit />
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<input
|
||||
ref="inputEl"
|
||||
v-model="draft"
|
||||
:class="['input', mono && 'mono', bad && 'is-bad']"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
@keydown.enter.prevent="finish(false)"
|
||||
@keydown.esc.prevent="cancel"
|
||||
@blur="finish(true)"
|
||||
/>
|
||||
<span v-if="unit" class="unit">{{ unit }}</span>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ie {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.val {
|
||||
font-size: var(--t-sm);
|
||||
max-width: var(--ie-max, 320px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.faint {
|
||||
color: var(--c-text-faint);
|
||||
}
|
||||
|
||||
.unit {
|
||||
color: var(--c-text-soft);
|
||||
font-size: var(--t-sm);
|
||||
}
|
||||
|
||||
.ico {
|
||||
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;
|
||||
}
|
||||
.ico:hover:not(:disabled) {
|
||||
background: var(--c-surface-sunken);
|
||||
color: var(--c-text);
|
||||
}
|
||||
.ico:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ico svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: var(--ie-width, 220px);
|
||||
max-width: 100%;
|
||||
padding: var(--s-1) var(--s-2);
|
||||
border: 1px solid var(--c-border-strong);
|
||||
border-radius: var(--r-sm);
|
||||
background: var(--c-surface);
|
||||
font-size: var(--t-sm);
|
||||
}
|
||||
|
||||
.input.is-bad {
|
||||
border-color: var(--c-danger);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user