轻提示铺开到全部页面;内核状态悬停变重启;GEO 支持单文件下载
侧栏动作排撤掉重启图标,内核状态(圆点加版本号)本身变成按钮:悬停 换层显示「重启内核」,不在线时只当状态看。行高压成 1 修掉圆点和等宽 字体不居中的问题。 四个页面的一次性反馈(保存、删除、更新订阅、断开连接、测延迟结论) 全部改走 toast,commit() 改返回成败给「失败不关对话框」用;持续状态 (连接页轮询失败、请求页日志流失败)留在页内,会自动消失的提示装不下 需要一直可见的信息。toast 加 info 类别给中性结论用。 GEO 每行的更新改成单独下载那一个文件:内核只有全量更新的口子,单文件 只能服务端自己下(POST /api/geo/sync),代价是只有形状校验(MaxMind 末尾标记、geodata 首字节,shared/geo.ts 带测试)且内核重启后才换用新 文件;「更新全部」文字按钮保留内核的全量口子,有完整校验和热加载。 上传两行的按钮改「选择或拖放文件」,投放区拖入高亮,恢复策略挪到 恢复行下方。
This commit is contained in:
+16
-90
@@ -24,6 +24,7 @@ import {
|
||||
import type { OutboundSetup } from '@shared/inbound-groups.ts'
|
||||
import type { ConfigObject } from '../../vendor/config-editor/index.ts'
|
||||
import { ApiError, api } from '../api'
|
||||
import { toast, toastError } from '../toast'
|
||||
import type { ListenerFields } from '../api'
|
||||
import type { GroupView } from '../runtime'
|
||||
import { optimisticSelect, providerLines, reloadRuntime, runtime } from '../runtime'
|
||||
@@ -33,7 +34,6 @@ import InboundList from '../components/InboundList.vue'
|
||||
import InboundDetail from './InboundDetail.vue'
|
||||
import InboundForm from './InboundForm.vue'
|
||||
import IconAdd from '~icons/ri/add-line'
|
||||
import IconClose from '~icons/ri/close-line'
|
||||
|
||||
const props = defineProps<{
|
||||
data: ConfigObject
|
||||
@@ -56,20 +56,14 @@ const defaultSetup = (): OutboundSetup => ({
|
||||
|
||||
const selectedId = ref<string | null>(null)
|
||||
const saving = ref(false)
|
||||
const failure = ref<{ message: string; hint: string } | null>(null)
|
||||
/** 成功的结果,绿色 */
|
||||
const done = ref('')
|
||||
/** 中性的告知,比如某条线路测不通——那是关于线路的结论,不是操作失败 */
|
||||
const notice = ref('')
|
||||
const fields = ref<ListenerFields | null>(null)
|
||||
const formOpen = ref(false)
|
||||
const formEditing = ref<Inbound | null>(null)
|
||||
|
||||
const { testing, test } = useDelayTest(
|
||||
() => void reloadRuntime(),
|
||||
(message) => {
|
||||
notice.value = message
|
||||
},
|
||||
// 「某条线路测不通」是关于线路的结论,不是操作失败,走中性的轻提示
|
||||
(message) => toast(message, { kind: 'info' }),
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -130,25 +124,21 @@ const chainView = computed<GroupView | null>(() => {
|
||||
|
||||
const clone = () => JSON.parse(JSON.stringify(props.data)) as ConfigObject
|
||||
|
||||
/** 所有改配置文件的动作都从这里走。 */
|
||||
async function commit(raw: ConfigObject, success: string) {
|
||||
/** 所有改配置文件的动作都从这里走。反馈走右上角轻提示,返回值告知成败。 */
|
||||
async function commit(raw: ConfigObject, success: string): Promise<boolean> {
|
||||
// 先把生成的东西按当前规则重建一遍,再清掉没人再引用的。不清理的话,改几次文件里就
|
||||
// 攒下一堆用户从没主动建过的东西,而没人用的订阅副本还会按 interval 继续联网。
|
||||
const next = pruneGenerated(syncGenerated(raw, collectInbounds(raw)))
|
||||
saving.value = true
|
||||
failure.value = null
|
||||
done.value = ''
|
||||
notice.value = ''
|
||||
try {
|
||||
const result = await api.applyConfig(next, props.revision)
|
||||
// 没重载有两种情况(内容没有变化、内核不在线只写了文件),都要如实说,不能只报成功
|
||||
done.value = result.reloaded ? success : `${success}(${result.reloadMessage})`
|
||||
toast(result.reloaded ? success : `${success}(${result.reloadMessage})`)
|
||||
emit('saved')
|
||||
return true
|
||||
} catch (error) {
|
||||
failure.value =
|
||||
error instanceof ApiError
|
||||
? { message: error.message, hint: error.hint }
|
||||
: { message: String(error), hint: '' }
|
||||
toastError(error)
|
||||
return false
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -158,8 +148,8 @@ async function commit(raw: ConfigObject, success: string) {
|
||||
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 ok = await commit(applySetup(clone(), target.name, next), `${target.name} 的代理配置已保存`)
|
||||
if (!ok) return
|
||||
// 草稿态里预选的线路在保存成功、组按新配置就位之后逐个选上。这是运行时动作,
|
||||
// 失败不影响已保存的配置:组停在内核的默认选中上,刷新后显示的就是真实状态。
|
||||
const picked: Array<[string, string | null]> = [
|
||||
@@ -207,9 +197,9 @@ async function submitForm(entry: ConfigObject) {
|
||||
const name = String(entry.name)
|
||||
const withGroup = editingIndex === null ? applySetup(next, name, defaultSetup()) : next
|
||||
|
||||
await commit(withGroup, editingIndex === null ? `已新增 ${name}` : '已保存')
|
||||
const ok = await commit(withGroup, editingIndex === null ? `已新增 ${name}` : '已保存')
|
||||
// 失败时不能关对话框——关了再打开就是空表单,用户填的名称、端口、类型全没了
|
||||
if (failure.value) return
|
||||
if (!ok) return
|
||||
formOpen.value = false
|
||||
if (editingIndex === null) selectedId.value = `listener:${String(listeners.length - 1)}`
|
||||
}
|
||||
@@ -218,17 +208,14 @@ async function submitForm(entry: ConfigObject) {
|
||||
async function selectMember(group: string, member: string) {
|
||||
if (saving.value) return
|
||||
optimisticSelect(group, member)
|
||||
failure.value = null
|
||||
done.value = ''
|
||||
notice.value = ''
|
||||
try {
|
||||
await api.selectProxy(group, member)
|
||||
await reloadRuntime()
|
||||
} catch (error) {
|
||||
failure.value = {
|
||||
message: error instanceof ApiError ? error.message : String(error),
|
||||
toast(error instanceof ApiError ? error.message : String(error), {
|
||||
kind: 'error',
|
||||
hint: '该操作仅修改内核运行状态,配置文件未被修改。',
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,18 +269,6 @@ async function removeConfirmed() {
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div v-if="failure" class="failure">
|
||||
<div class="failure-text">
|
||||
<strong>{{ failure.message }}</strong>
|
||||
<span v-if="failure.hint">{{ failure.hint }}</span>
|
||||
</div>
|
||||
<button type="button" class="failure-x" title="关闭提示" @click="failure = null">
|
||||
<IconClose />
|
||||
</button>
|
||||
</div>
|
||||
<p v-else-if="done" class="done">{{ done }}</p>
|
||||
<p v-else-if="notice" class="notice">{{ notice }}</p>
|
||||
|
||||
<div class="split">
|
||||
<InboundList v-model:selected="selectedId" :inbounds="inbounds" />
|
||||
|
||||
@@ -389,55 +364,6 @@ async function removeConfirmed() {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.failure {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--s-3);
|
||||
border: 1px solid color-mix(in srgb, var(--c-danger) 35%, var(--c-border));
|
||||
border-radius: var(--r-md);
|
||||
background: var(--c-danger-soft);
|
||||
padding: var(--s-3) var(--s-4);
|
||||
margin-bottom: var(--s-4);
|
||||
font-size: var(--t-sm);
|
||||
}
|
||||
|
||||
.failure-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-1);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.failure-x {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-radius: var(--r-sm);
|
||||
background: none;
|
||||
color: var(--c-danger);
|
||||
cursor: pointer;
|
||||
}
|
||||
.failure-x:hover {
|
||||
background: color-mix(in srgb, var(--c-danger) 12%, transparent);
|
||||
}
|
||||
|
||||
.done {
|
||||
color: var(--c-ok);
|
||||
font-size: var(--t-sm);
|
||||
margin-bottom: var(--s-4);
|
||||
}
|
||||
|
||||
.notice {
|
||||
color: var(--c-text-soft);
|
||||
font-size: var(--t-sm);
|
||||
margin-bottom: var(--s-4);
|
||||
}
|
||||
|
||||
/* 左列表右详情。窄时改成上下,详情不再吸顶 */
|
||||
.split {
|
||||
display: grid;
|
||||
|
||||
Reference in New Issue
Block a user