进一步精简配置保存流程
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-07 17:50:06 +08:00
parent 659d98b589
commit a7ad2bc32e
8 changed files with 26 additions and 21 deletions

View File

@ -25,6 +25,7 @@ func TestReauthenticateCurrentUser(t *testing.T) {
oldSession := session.Instance
oldUserAttempts := reauthUserAttempts
oldIPAttempts := reauthIPAttempts
testRoot := t.TempDir()
t.Cleanup(func() {
if db.Instance != nil && db.Instance != oldDB {
_ = db.Instance.Close()
@ -36,7 +37,7 @@ func TestReauthenticateCurrentUser(t *testing.T) {
reauthIPAttempts = oldIPAttempts
})
config.Instance = &config.Config{DbType: config.DBTypeSQLite, DbDSN: filepath.Join(t.TempDir(), "reauth.db")}
config.Instance = &config.Config{DbType: config.DBTypeSQLite, DbDSN: filepath.Join(testRoot, "reauth.db")}
if err := db.Init(""); err != nil {
t.Fatal(err)
}

View File

@ -64,6 +64,8 @@ function isAuthenticationRefreshRequest(config) {
}
function isAuthenticationFailureError(error) {
const errorCode = error?.response?.data?.data?.code
if (errorCode === 'RECENT_AUTH_REQUIRED') return false
return error?.response?.data?.errorNo === 403
|| (error?.response?.status === 403 && isAuthenticationRefreshRequest(error.config))
}
@ -71,6 +73,7 @@ function isAuthenticationFailureError(error) {
http.interceptors.response.use(async (response) => {
//响应成功
if (response.data?.errorNo === 403
&& response.data?.data?.code !== 'RECENT_AUTH_REQUIRED'
&& !isSetupRequest(response.config)
&& !isAuthenticationRefreshRequest(response.config)) {
await handleLoginRedirect();

View File

@ -18,12 +18,12 @@ export async function requestWithRecentAuthentication(request) {
let password = ''
try {
const result = await ElMessageBox.prompt(
'为保护敏感配置,请确认当前登录密码。确认后将自动继续应用。',
'为保护敏感配置,请确认当前登录密码。确认后将自动完成保存。',
'确认当前密码',
{
inputType: 'password',
inputPlaceholder: '请输入当前密码',
confirmButtonText: '确认并继续',
confirmButtonText: '确认并保存',
cancelButtonText: '取消',
closeOnClickModal: false,
inputValidator: value => String(value || '').length > 0 || '请输入当前密码'

View File

@ -78,7 +78,7 @@
<div v-if="canWrite" class="editor-footer">
<el-alert
v-if="pendingCandidate"
title="有一项配置尚未生效,可以继续应用或放弃更改。"
title="上次保存尚未完成,可以继续保存或放弃本次更改。"
type="warning"
:closable="false"
show-icon
@ -98,7 +98,7 @@
:disabled="sectionLoading || !!working || !!sectionError || !hasChanges"
@click="saveAndApply"
>
保存并应用
保存配置
</el-button>
<el-button
v-else
@ -107,7 +107,7 @@
:disabled="sectionLoading || !!working || !!sectionError"
@click="continuePending"
>
继续应用
继续保存
</el-button>
<el-button
v-if="pendingCandidate"

View File

@ -82,7 +82,7 @@ export function usePage(pageKey = 'base') {
const scope = pending.payload && loadedText.value
? (changed.length ? `本次候选涉及:${changed.join('、')}。` : '本次候选的插件运行配置未变化。')
: '候选内容尚未完整读取,请刷新后核对。'
return `${scope}三个插件共用一个配置分区,请在对应三级菜单核对;应用时整个候选一起生效,其他插件的原有配置会保留。`
return `${scope}三个插件共用一组运行配置,请在对应三级菜单核对;完成保存时会统一生效,其他插件的原有配置会保留。`
}
if (pending?.section !== 'mail' || apiSection() !== 'mail') return ''
const active = readFormConfig(loadedText.value)
@ -101,7 +101,7 @@ export function usePage(pageKey = 'base') {
const scope = pending.payload && loadedText.value
? (changed.length ? `本次候选涉及:${changed.join('、')}。` : '本次候选的邮件服务与安全策略未变化。')
: '候选内容尚未完整读取,请刷新后核对。'
return `${scope}邮件服务页和安全策略页共用一个候选配置;应用时全部变更一起生效,监听端口变更仍需重启后端。`
return `${scope}邮件服务页和安全策略页共用一组运行配置;完成保存时全部变更一起生效,监听端口变更仍需重启后端。`
})
let savedInput = null
const validationResult = ref(null)
@ -577,7 +577,7 @@ export function usePage(pageKey = 'base') {
validationResult.value = { valid: true, generation: savedGeneration.value, issues: [] }
await loadStatus()
await loadPending()
if (!quiet) ElMessage.success('配置已保存,等待应用')
if (!quiet) ElMessage.success('配置已暂存,正在完成保存')
return true
} catch (error) {
if (isRecentAuthenticationCancelled(error)) return false
@ -625,7 +625,7 @@ export function usePage(pageKey = 'base') {
return
}
const saved = await saveConfig({ quiet: true })
if (saved) await applyConfig({ successMessage: '配置已保存并应用' })
if (saved) await applyConfig({ successMessage: '配置已保存' })
}
const continuePending = async () => {
@ -716,7 +716,7 @@ export function usePage(pageKey = 'base') {
if (!hasChanges.value && !migrationToken.value && !rollbackPreview.value) return true
try {
await ElMessageBox.confirm(
'当前页有编辑内容、待应用候选配置或操作预览。离开会清除本页状态,但不会撤销已保存到服务器的候选配置。确认离开吗?',
'当前页有编辑内容、未完成的保存或操作预览。离开会清除本页状态,但不会撤销已暂存到服务器的更改。确认离开吗?',
'确认离开当前页',
{ type: 'warning', confirmButtonText: '离开页面', cancelButtonText: '继续编辑' }
)

View File

@ -131,7 +131,7 @@ export function useSpamBlockSettings() {
savedInput = JSON.stringify(settings)
validationResult.value = { valid: true, generation: savedGeneration.value, issues: [] }
await load()
if (!quiet) ElMessage.success('垃圾邮件插件配置已保存,等待应用')
if (!quiet) ElMessage.success('垃圾邮件插件配置已暂存,正在完成保存')
return true
} catch (error) {
if (isRecentAuthenticationCancelled(error)) return false
@ -201,7 +201,7 @@ export function useSpamBlockSettings() {
return
}
const saved = await save({ quiet: true })
if (saved) await apply({ successMessage: '垃圾邮件插件配置已保存并应用' })
if (saved) await apply({ successMessage: '垃圾邮件插件配置已保存' })
}
const continuePending = async () => {

View File

@ -25,7 +25,7 @@
show-icon
/>
<el-alert v-if="pendingCandidate" title="有一项配置尚未生效,可以继续应用或放弃更改。" type="warning" :closable="false" show-icon />
<el-alert v-if="pendingCandidate" title="上次保存尚未完成,可以继续保存或放弃本次更改。" type="warning" :closable="false" show-icon />
<el-form label-position="top" class="spam-form">
<el-form-item label="模型 API 接口地址">
<el-input
@ -68,7 +68,7 @@
:disabled="!canWrite() || !!working || loading || !hasChanges"
@click="saveAndApply"
>
保存并应用
保存配置
</el-button>
<el-button
v-else
@ -77,7 +77,7 @@
:disabled="!canWrite() || !!working || loading"
@click="continuePending"
>
继续应用
继续保存
</el-button>
<el-button v-if="canWrite() && pendingCandidate" type="danger" plain :disabled="!!working || pendingCandidate.committed" @click="managePending('cancel')">放弃更改</el-button>
</div>

View File

@ -9,6 +9,7 @@ const formSource = readFileSync(new URL('../../frontend/src/views/components/Con
const formHelpers = compileFunction(formSource.replaceAll('export function ', 'function ') + '\nreturn { readFormConfig, updateFormConfig, validateFormConfig }')()
const recentAuthenticationSource = readFileSync(new URL('../../frontend/src/utils/recentAuthentication.js', import.meta.url), 'utf8')
.replace(/^import .*$/gm, '')
.replaceAll('export async function ', 'async function ')
.replaceAll('export function ', 'function ')
const compileRecentAuthentication = compileFunction(
recentAuthenticationSource + '\nreturn { isRecentAuthenticationCancelled, requestWithRecentAuthentication }',
@ -429,7 +430,7 @@ test('独立页模板可编译,编辑器不再连接维护面板且权限绑
assert.doesNotMatch(template, /&#x[0-9a-f]+;/i)
})
test('配置编辑页将验证、候选保存和应用精简为单一操作', () => {
test('配置编辑页将验证、候选保存和应用精简为单一保存操作', () => {
const root = new URL('../../frontend/src/views/components/ConfigSettings/', import.meta.url)
const template = readFileSync(new URL('ConfigEditor/ConfigEditor.vue', root), 'utf8')
const sharedCss = readFileSync(new URL('ConfigSettings.css', root), 'utf8')
@ -437,9 +438,9 @@ test('配置编辑页将验证、候选保存和应用精简为单一操作', ()
assert.doesNotMatch(template, /change-workflow|step-number|配置变更流程/)
assert.doesNotMatch(sharedCss + editorCss, /change-workflow|step-number/)
assert.match(template, /@click="saveAndApply"/)
assert.match(template, /保存并应用/)
assert.match(template, /保存配置/)
assert.match(template, /@click="continuePending"/)
assert.match(template, /继续应用/)
assert.match(template, /继续保存/)
assert.match(template, /v-if="canWrite" class="editor-footer"/)
assert.match(template, /@click="managePending\('cancel'\)"/)
assert.match(template, /放弃更改/)
@ -857,7 +858,7 @@ test('邮件页与安全策略页共享候选并明确提示改动范围,忽
assert.match(page.pendingScopeHint.value, /邮件服务与安全策略未变化/)
page.pendingCandidate.value.payload = { ...active, pop3: { port: 1110, tlsPort: 1995 }, policy: { maxMessageBytes: 5678 } }
assert.match(page.pendingScopeHint.value, /本次候选涉及:POP3 监听、邮件与账号策略。/)
assert.match(page.pendingScopeHint.value, /邮件服务页和安全策略页共用一个候选配置/)
assert.match(page.pendingScopeHint.value, /邮件服务页和安全策略页共用一组运行配置/)
page.pendingCandidate.value = { section: 'smtp' }
assert.equal(page.pendingScopeHint.value, '')
page.pendingCandidate.value = { section: 'mail' }
@ -924,7 +925,7 @@ for (const key of ['spam_block', 'telegram_push', 'wechat_push']) {
const restored = fixture(() => {}, other, { http }).page
await restored.loadAll()
assert.equal(restored.savedGeneration.value, 2)
assert.match(restored.pendingScopeHint.value, /三个插件共用一个配置分区/)
assert.match(restored.pendingScopeHint.value, /三个插件共用一组运行配置/)
assert.ok(restored.pendingScopeHint.value.includes(page.currentSection.value.label))
await restored.applyConfig()
assert.ok(requests.includes('/api/v1/config/plugins/apply'))