修掉审查查出的五个问题
保存失败会丢掉整张表单:对话框在 await commit 之前就关了,重开是空的。改成只有成功才关。
错误响应原本有两种形状({error} 和 {message,hint}),前端只读 message,于是服务端
写好的中文提示一律退化成「请求失败(HTTP 4xx)」。统一成 { message, hint? }。
路由处理器没有兜底:请求体不是合法 JSON、配置文件运行中被删,都会返回一个没有响应体
的 500。这条链路的立意是出事时把用户救回来,而出事时给的偏偏是最没信息量的一句话。
延迟测试失败被写进 done,而 done 的样式是成功绿。拆出中性的 notice——测不通是关于
线路的结论,既不是操作失败也不是成功。
刷新原本只有一个动词,测一条线路的延迟会导致整份配置被重读、重新解析、跑一遍引用校验,
外加四个往返。拆成 saved 和 runtime-changed 两个事件,运行时动作只重读运行时状态。
This commit is contained in:
+36
-15
@@ -25,7 +25,13 @@ const props = defineProps<{
|
||||
runtime: Record<string, ProxyInfo>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ changed: []; optimisticSelect: [group: string, member: string] }>()
|
||||
const emit = defineEmits<{
|
||||
/** 配置文件变了,需要重读整份配置 */
|
||||
saved: []
|
||||
/** 只有内核的运行时状态变了,不用重读配置 */
|
||||
runtimeChanged: []
|
||||
optimisticSelect: [group: string, member: string]
|
||||
}>()
|
||||
|
||||
const inbounds = computed(() => collectInbounds(props.data))
|
||||
const outbounds = computed(() => collectOutboundNames(props.data))
|
||||
@@ -33,7 +39,10 @@ const outbounds = computed(() => collectOutboundNames(props.data))
|
||||
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)
|
||||
@@ -88,17 +97,16 @@ async function commit(next: ConfigObject, success: string) {
|
||||
saving.value = true
|
||||
failure.value = null
|
||||
done.value = ''
|
||||
notice.value = ''
|
||||
try {
|
||||
await api.applyConfig(next, props.revision)
|
||||
done.value = success
|
||||
emit('changed')
|
||||
emit('saved')
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
const payload = error.payload as { message?: string; hint?: string }
|
||||
failure.value = { message: payload.message ?? error.message, hint: payload.hint ?? '' }
|
||||
} else {
|
||||
failure.value = { message: String(error), hint: '' }
|
||||
}
|
||||
failure.value =
|
||||
error instanceof ApiError
|
||||
? { message: error.message, hint: error.hint }
|
||||
: { message: String(error), hint: '' }
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -144,8 +152,10 @@ async function submitForm(entry: ConfigObject) {
|
||||
else listeners[editingIndex] = entry
|
||||
next.listeners = listeners
|
||||
|
||||
formOpen.value = false
|
||||
await commit(next, editingIndex === null ? `已新增 ${String(entry.name)}` : '已保存')
|
||||
// 失败时不能关对话框——关了再打开就是空表单,用户填的名称、端口、类型全没了
|
||||
if (failure.value) return
|
||||
formOpen.value = false
|
||||
if (editingIndex === null) selectedId.value = `listener:${String(listeners.length - 1)}`
|
||||
}
|
||||
|
||||
@@ -156,12 +166,15 @@ async function selectMember(group: string, member: string) {
|
||||
emit('optimisticSelect', group, member)
|
||||
failure.value = null
|
||||
done.value = ''
|
||||
notice.value = ''
|
||||
try {
|
||||
await api.selectProxy(group, member)
|
||||
emit('changed')
|
||||
emit('runtimeChanged')
|
||||
} catch (error) {
|
||||
const message = error instanceof ApiError ? String(error.payload.error ?? error.message) : String(error)
|
||||
failure.value = { message, hint: '该操作仅修改内核运行状态,配置文件未被修改。' }
|
||||
failure.value = {
|
||||
message: error instanceof ApiError ? error.message : String(error),
|
||||
hint: '该操作仅修改内核运行状态,配置文件未被修改。',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,10 +214,11 @@ async function testLine(name: string) {
|
||||
testing.value = [...testing.value, name]
|
||||
try {
|
||||
await api.testLine(name, props.lineProvider[name], TEST_TIMEOUT_MS)
|
||||
emit('changed')
|
||||
emit('runtimeChanged')
|
||||
} catch (error) {
|
||||
// 测不通是关于线路的结论,不是 Patchbay 出了故障,所以不用错误框
|
||||
done.value = describeTestFailure(name, error)
|
||||
// 测不通是关于线路的结论,不是 Patchbay 出了故障。既不该用错误框,
|
||||
// 也不该用成功的绿色——它需要第三种中性的样子。
|
||||
notice.value = describeTestFailure(name, error)
|
||||
} finally {
|
||||
testing.value = testing.value.filter((n) => n !== name)
|
||||
}
|
||||
@@ -255,6 +269,7 @@ async function removeSelected() {
|
||||
<span v-if="failure.hint">{{ failure.hint }}</span>
|
||||
</div>
|
||||
<p v-else-if="done" class="done">{{ done }}</p>
|
||||
<p v-else-if="notice" class="notice">{{ notice }}</p>
|
||||
|
||||
<div class="split">
|
||||
<ol class="rows">
|
||||
@@ -379,6 +394,12 @@ async function removeSelected() {
|
||||
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