mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-02 04:42:09 +08:00
perf(frontend): hoist notice table and skip no-op loading updates
The notice handler table was rebuilt on every notice event; build it once at module scope. withLoading returned a new state even when the loading set was unchanged, re-rendering store consumers for nothing.
This commit is contained in:
+207
-216
@@ -32,230 +32,221 @@ export const forgetShownStartupError = () => {
|
||||
* type makes a backend status without a frontend entry a compile error (and
|
||||
* contract regeneration keeps the union in sync).
|
||||
*/
|
||||
const handlers: Record<
|
||||
NoticeStatus,
|
||||
(msg: string, navigate: NavigateFunction) => void
|
||||
> = {
|
||||
info: (msg) => {
|
||||
if (msg) showNotice.info(msg)
|
||||
},
|
||||
'import_sub_url::ok': (_msg, navigate) => {
|
||||
// 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
|
||||
// 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符
|
||||
// navigate("/profile", { state: { current: msg } });
|
||||
navigate('/profile')
|
||||
showNotice.success(
|
||||
'shared.feedback.notifications.importSubscriptionSuccess',
|
||||
)
|
||||
},
|
||||
'import_sub_url::error': (msg, navigate) => {
|
||||
navigate('/profile')
|
||||
showNotice.error(msg)
|
||||
},
|
||||
'set_config::error': (msg) => showNotice.error(msg),
|
||||
'core_start::error': () => {
|
||||
const read = ++startupErrorReads
|
||||
void getCoreStartupError()
|
||||
.then(async (failure) => {
|
||||
if (read < settledStartupErrorRead) return
|
||||
settledStartupErrorRead = read
|
||||
if (!failure) {
|
||||
shownStartupError = null
|
||||
return
|
||||
}
|
||||
const window = getCurrentWindow()
|
||||
const [visible, minimized] = await Promise.all([
|
||||
window.isVisible(),
|
||||
window.isMinimized(),
|
||||
])
|
||||
const shown = `${failure.kind}:${failure.detail}`
|
||||
if (
|
||||
read === settledStartupErrorRead &&
|
||||
visible &&
|
||||
!minimized &&
|
||||
shownStartupError !== shown
|
||||
) {
|
||||
shownStartupError = shown
|
||||
showNotice.error(
|
||||
failure.kind === 'serviceCoreStopped'
|
||||
? 'settings.feedback.errors.clash.serviceCoreStopped'
|
||||
: 'settings.feedback.errors.clash.startFailed',
|
||||
failure.detail,
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending core startup error', error)
|
||||
})
|
||||
},
|
||||
'service_core::repair_required': () => {
|
||||
void takeServiceRepairNotice()
|
||||
.then((pending) => {
|
||||
if (pending) {
|
||||
requestService({ reason: 'serviceLocationRefused' })
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending service repair notice', error)
|
||||
})
|
||||
},
|
||||
'service_core::app_data_not_owned': () => {
|
||||
void takeServiceOwnerNotice()
|
||||
.then((command) => {
|
||||
if (command) {
|
||||
if (shownOwnerNotice !== null) hideNotice(shownOwnerNotice)
|
||||
shownOwnerNotice = showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.appDataNotOwned',
|
||||
{ command },
|
||||
0,
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending service owner notice', error)
|
||||
})
|
||||
},
|
||||
'service_core::sidecar_fallback': () => {
|
||||
void takeServiceFallbackNotice()
|
||||
.then((notice) => {
|
||||
if (notice?.kind === 'coreRejected') {
|
||||
showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.permissionFallback',
|
||||
{ reason: notice.reason },
|
||||
0,
|
||||
)
|
||||
} else if (notice?.kind === 'notAutoStarted') {
|
||||
requestService({ reason: 'serviceNotAutoStarted' })
|
||||
} else if (notice) {
|
||||
showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.sidecarFallback',
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending service fallback notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'dns_override::auto_disabled': () => {
|
||||
void takeDnsOverrideNotice()
|
||||
.then((pending) => {
|
||||
if (pending) {
|
||||
showNotice.info('settings.modals.dns.protection.autoDisabled')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending DNS override notice', error)
|
||||
})
|
||||
},
|
||||
'enhance::discarded_keys': () => {
|
||||
void takeDiscardedKeysNotice()
|
||||
.then((keys) => {
|
||||
if (keys) {
|
||||
showNotice.warning('profiles.page.feedback.notices.discardedKeys', {
|
||||
keys,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending discarded keys notice', error)
|
||||
})
|
||||
},
|
||||
'tun_mode::auto_disabled': () =>
|
||||
showNotice.info(
|
||||
'settings.sections.system.notifications.tunMode.autoDisabled',
|
||||
),
|
||||
'tun_mode::auto_disable_failed': () =>
|
||||
showNotice.error(
|
||||
'settings.sections.system.notifications.tunMode.autoDisableFailed',
|
||||
),
|
||||
'app_restart::core_stop_failed': (msg) =>
|
||||
showNotice.error(
|
||||
'layout.feedback.errors.restartCoreStopFailed',
|
||||
msg || undefined,
|
||||
),
|
||||
'app_quit::core_stop_failed': (msg) =>
|
||||
showNotice.error(
|
||||
'layout.feedback.errors.quitCoreStopFailed',
|
||||
msg || undefined,
|
||||
),
|
||||
update_with_clash_proxy: (msg) =>
|
||||
showNotice.success(
|
||||
'settings.feedback.notifications.updater.withClashProxySuccess',
|
||||
msg,
|
||||
),
|
||||
update_failed_even_with_clash: (msg) =>
|
||||
showNotice.error(
|
||||
'settings.feedback.notifications.updater.withClashProxyFailed',
|
||||
msg,
|
||||
),
|
||||
'reactivate_profiles::error': (msg) => showNotice.error(msg),
|
||||
update_failed: (msg) => showNotice.error(msg),
|
||||
'config_validate::boot_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.config.bootFailed', msg),
|
||||
'config_validate::error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.config.failed', msg),
|
||||
'config_validate::process_terminated': () =>
|
||||
showNotice.error('shared.feedback.validation.config.processTerminated'),
|
||||
'config_validate::script_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.script.fileError', msg),
|
||||
'config_validate::script_syntax_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.script.syntaxError', msg),
|
||||
'config_validate::script_missing_main': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.script.missingMain', msg),
|
||||
'config_validate::file_not_found': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.script.fileNotFound', msg),
|
||||
'config_validate::yaml_syntax_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.yaml.syntaxError', msg),
|
||||
'config_validate::yaml_read_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.yaml.readError', msg),
|
||||
'config_validate::yaml_mapping_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.yaml.mappingError', msg),
|
||||
'config_validate::merge_syntax_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.merge.syntaxError', msg),
|
||||
'config_validate::merge_mapping_error': (msg) =>
|
||||
showNotice.error('shared.feedback.validation.merge.mappingError', msg),
|
||||
'config_core::change_success': (msg) =>
|
||||
showNotice.success(
|
||||
'settings.feedback.notifications.clash.changeSuccess',
|
||||
msg,
|
||||
),
|
||||
'config_core::change_error': (msg) =>
|
||||
showNotice.error('settings.feedback.notifications.clash.changeFailed', msg),
|
||||
'mixed_port::fallback': (msg) => {
|
||||
const [oldPort, newPort] = msg.split(',')
|
||||
showNotice.info('settings.modals.clashPort.messages.automaticFallback', {
|
||||
oldPort,
|
||||
newPort,
|
||||
})
|
||||
},
|
||||
'mixed_port::fallback_error': (msg) =>
|
||||
showNotice.error(
|
||||
'settings.modals.clashPort.messages.automaticFallbackFailed',
|
||||
{ error: msg },
|
||||
),
|
||||
}
|
||||
|
||||
export const handleNoticeMessage = (
|
||||
status: string,
|
||||
msg: string,
|
||||
t: TranslateFunction,
|
||||
navigate: NavigateFunction,
|
||||
) => {
|
||||
const handlers: Record<NoticeStatus, () => void> = {
|
||||
info: () => {
|
||||
if (msg) showNotice.info(msg)
|
||||
},
|
||||
'import_sub_url::ok': () => {
|
||||
// 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
|
||||
// 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符
|
||||
// navigate("/profile", { state: { current: msg } });
|
||||
navigate('/profile')
|
||||
showNotice.success(
|
||||
'shared.feedback.notifications.importSubscriptionSuccess',
|
||||
)
|
||||
},
|
||||
'import_sub_url::error': () => {
|
||||
navigate('/profile')
|
||||
showNotice.error(msg)
|
||||
},
|
||||
'set_config::error': () => showNotice.error(msg),
|
||||
'core_start::error': () => {
|
||||
const read = ++startupErrorReads
|
||||
void getCoreStartupError()
|
||||
.then(async (failure) => {
|
||||
if (read < settledStartupErrorRead) return
|
||||
settledStartupErrorRead = read
|
||||
if (!failure) {
|
||||
shownStartupError = null
|
||||
return
|
||||
}
|
||||
const window = getCurrentWindow()
|
||||
const [visible, minimized] = await Promise.all([
|
||||
window.isVisible(),
|
||||
window.isMinimized(),
|
||||
])
|
||||
const shown = `${failure.kind}:${failure.detail}`
|
||||
if (
|
||||
read === settledStartupErrorRead &&
|
||||
visible &&
|
||||
!minimized &&
|
||||
shownStartupError !== shown
|
||||
) {
|
||||
shownStartupError = shown
|
||||
showNotice.error(
|
||||
failure.kind === 'serviceCoreStopped'
|
||||
? 'settings.feedback.errors.clash.serviceCoreStopped'
|
||||
: 'settings.feedback.errors.clash.startFailed',
|
||||
failure.detail,
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending core startup error', error)
|
||||
})
|
||||
},
|
||||
'service_core::repair_required': () => {
|
||||
void takeServiceRepairNotice()
|
||||
.then((pending) => {
|
||||
if (pending) {
|
||||
requestService({ reason: 'serviceLocationRefused' })
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending service repair notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'service_core::app_data_not_owned': () => {
|
||||
void takeServiceOwnerNotice()
|
||||
.then((command) => {
|
||||
if (command) {
|
||||
if (shownOwnerNotice !== null) hideNotice(shownOwnerNotice)
|
||||
shownOwnerNotice = showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.appDataNotOwned',
|
||||
{ command },
|
||||
0,
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending service owner notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'service_core::sidecar_fallback': () => {
|
||||
void takeServiceFallbackNotice()
|
||||
.then((notice) => {
|
||||
if (notice?.kind === 'coreRejected') {
|
||||
showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.permissionFallback',
|
||||
{ reason: notice.reason },
|
||||
0,
|
||||
)
|
||||
} else if (notice?.kind === 'notAutoStarted') {
|
||||
requestService({ reason: 'serviceNotAutoStarted' })
|
||||
} else if (notice) {
|
||||
showNotice.warning(
|
||||
'settings.feedback.notifications.clashService.sidecarFallback',
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending service fallback notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'dns_override::auto_disabled': () => {
|
||||
void takeDnsOverrideNotice()
|
||||
.then((pending) => {
|
||||
if (pending) {
|
||||
showNotice.info('settings.modals.dns.protection.autoDisabled')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to read the pending DNS override notice', error)
|
||||
})
|
||||
},
|
||||
'enhance::discarded_keys': () => {
|
||||
void takeDiscardedKeysNotice()
|
||||
.then((keys) => {
|
||||
if (keys) {
|
||||
showNotice.warning('profiles.page.feedback.notices.discardedKeys', {
|
||||
keys,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending discarded keys notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'tun_mode::auto_disabled': () =>
|
||||
showNotice.info(
|
||||
'settings.sections.system.notifications.tunMode.autoDisabled',
|
||||
),
|
||||
'tun_mode::auto_disable_failed': () =>
|
||||
showNotice.error(
|
||||
'settings.sections.system.notifications.tunMode.autoDisableFailed',
|
||||
),
|
||||
'app_restart::core_stop_failed': () =>
|
||||
showNotice.error(
|
||||
'layout.feedback.errors.restartCoreStopFailed',
|
||||
msg || undefined,
|
||||
),
|
||||
'app_quit::core_stop_failed': () =>
|
||||
showNotice.error(
|
||||
'layout.feedback.errors.quitCoreStopFailed',
|
||||
msg || undefined,
|
||||
),
|
||||
update_with_clash_proxy: () =>
|
||||
showNotice.success(
|
||||
'settings.feedback.notifications.updater.withClashProxySuccess',
|
||||
msg,
|
||||
),
|
||||
update_failed_even_with_clash: () =>
|
||||
showNotice.error(
|
||||
'settings.feedback.notifications.updater.withClashProxyFailed',
|
||||
msg,
|
||||
),
|
||||
'reactivate_profiles::error': () => showNotice.error(msg),
|
||||
update_failed: () => showNotice.error(msg),
|
||||
'config_validate::boot_error': () =>
|
||||
showNotice.error('shared.feedback.validation.config.bootFailed', msg),
|
||||
'config_validate::error': () =>
|
||||
showNotice.error('shared.feedback.validation.config.failed', msg),
|
||||
'config_validate::process_terminated': () =>
|
||||
showNotice.error('shared.feedback.validation.config.processTerminated'),
|
||||
'config_validate::script_error': () =>
|
||||
showNotice.error('shared.feedback.validation.script.fileError', msg),
|
||||
'config_validate::script_syntax_error': () =>
|
||||
showNotice.error('shared.feedback.validation.script.syntaxError', msg),
|
||||
'config_validate::script_missing_main': () =>
|
||||
showNotice.error('shared.feedback.validation.script.missingMain', msg),
|
||||
'config_validate::file_not_found': () =>
|
||||
showNotice.error('shared.feedback.validation.script.fileNotFound', msg),
|
||||
'config_validate::yaml_syntax_error': () =>
|
||||
showNotice.error('shared.feedback.validation.yaml.syntaxError', msg),
|
||||
'config_validate::yaml_read_error': () =>
|
||||
showNotice.error('shared.feedback.validation.yaml.readError', msg),
|
||||
'config_validate::yaml_mapping_error': () =>
|
||||
showNotice.error('shared.feedback.validation.yaml.mappingError', msg),
|
||||
'config_validate::merge_syntax_error': () =>
|
||||
showNotice.error('shared.feedback.validation.merge.syntaxError', msg),
|
||||
'config_validate::merge_mapping_error': () =>
|
||||
showNotice.error('shared.feedback.validation.merge.mappingError', msg),
|
||||
'config_core::change_success': () =>
|
||||
showNotice.success(
|
||||
'settings.feedback.notifications.clash.changeSuccess',
|
||||
msg,
|
||||
),
|
||||
'config_core::change_error': () =>
|
||||
showNotice.error(
|
||||
'settings.feedback.notifications.clash.changeFailed',
|
||||
msg,
|
||||
),
|
||||
'mixed_port::fallback': () => {
|
||||
const [oldPort, newPort] = msg.split(',')
|
||||
showNotice.info('settings.modals.clashPort.messages.automaticFallback', {
|
||||
oldPort,
|
||||
newPort,
|
||||
})
|
||||
},
|
||||
'mixed_port::fallback_error': () =>
|
||||
showNotice.error(
|
||||
'settings.modals.clashPort.messages.automaticFallbackFailed',
|
||||
{ error: msg },
|
||||
),
|
||||
}
|
||||
|
||||
const handler = handlers[status as NoticeStatus]
|
||||
if (handler) {
|
||||
handler()
|
||||
handler(msg, navigate)
|
||||
} else {
|
||||
console.warn(`未处理的通知状态: ${status}`)
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ const withLoading = (
|
||||
next.delete(uid)
|
||||
}
|
||||
}
|
||||
if (next.size === state.profileUpdates.loading.size) return state
|
||||
return {
|
||||
...state,
|
||||
profileUpdates: { ...state.profileUpdates, loading: next },
|
||||
|
||||
Reference in New Issue
Block a user