This commit is contained in:
parent
7aab26221b
commit
f4c50226f8
@ -43,6 +43,19 @@
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-row :deep(.el-button) {
|
||||
min-width: 112px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.overview-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@ -128,6 +141,14 @@
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.action-row :deep(.el-button) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.overview-meta {
|
||||
gap: 8px 16px;
|
||||
}
|
||||
|
||||
@ -78,7 +78,7 @@
|
||||
<div v-if="canWrite" class="editor-footer">
|
||||
<el-alert
|
||||
v-if="pendingCandidate"
|
||||
:title="`服务器存在待应用候选,代次 ${pendingCandidate.generation}。已锁定编辑;如需修改,请先取消候选。刷新页面不会丢失候选。`"
|
||||
title="有一项配置尚未生效,可以继续应用或放弃更改。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
@ -90,30 +90,38 @@
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<el-checkbox v-model="confirmed" class="confirmation-check" :disabled="sectionLoading || !!working || !!sectionError">
|
||||
我已确认这是受控变更,并允许使用最近认证完成高风险配置操作
|
||||
</el-checkbox>
|
||||
<div class="action-row">
|
||||
<el-button :loading="working === 'validate'" :disabled="sectionLoading || !!working || !!sectionError || !!pendingCandidate" @click="validateConfig">
|
||||
只验证
|
||||
<el-button
|
||||
v-if="!pendingCandidate"
|
||||
type="primary"
|
||||
:loading="working === 'save' || working === 'apply'"
|
||||
:disabled="sectionLoading || !!working || !!sectionError || !hasChanges"
|
||||
@click="saveAndApply"
|
||||
>
|
||||
保存并应用
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="working === 'save'" :disabled="sectionLoading || !!working || !!sectionError || !!pendingCandidate" @click="saveConfig">
|
||||
保存候选配置
|
||||
<el-button
|
||||
v-else
|
||||
type="primary"
|
||||
:loading="working === 'resume' || working === 'apply'"
|
||||
:disabled="sectionLoading || !!working || !!sectionError"
|
||||
@click="continuePending"
|
||||
>
|
||||
继续应用
|
||||
</el-button>
|
||||
<el-button v-if="pendingCandidate && !pendingCandidate.sameSession" :disabled="!!working || !confirmed" @click="managePending('resume')">
|
||||
恢复候选到当前会话
|
||||
</el-button>
|
||||
<el-button v-if="pendingCandidate" type="warning" plain :disabled="!!working || pendingCandidate.committed" @click="managePending('cancel')">
|
||||
取消候选配置
|
||||
</el-button>
|
||||
<el-button type="success" plain :loading="working === 'apply'" :disabled="sectionLoading || !!working || !savedGeneration" @click="applyConfig">
|
||||
{{ savedGeneration ? `应用代次 ${savedGeneration}` : '应用配置' }}
|
||||
<el-button
|
||||
v-if="pendingCandidate"
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="!!working || pendingCandidate.committed"
|
||||
@click="managePending('cancel')"
|
||||
>
|
||||
放弃更改
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="validationResult" class="result-box" :class="validationResult.valid ? 'result-success' : 'result-error'" role="status">
|
||||
<strong>{{ validationResult.valid ? '验证通过' : '验证未通过' }}</strong>
|
||||
<span v-if="validationResult.generation">候选代次:{{ validationResult.generation }}</span>
|
||||
<div v-if="validationResult && !validationResult.valid" class="result-box result-error" role="status">
|
||||
<strong>配置检查未通过</strong>
|
||||
<ul v-if="validationResult.issues?.length">
|
||||
<li v-for="issue in validationResult.issues" :key="`${issue.code}-${issue.message}`">
|
||||
{{ issue.code }}:{{ issue.message }}
|
||||
@ -145,19 +153,15 @@ const {
|
||||
sectionError,
|
||||
secretField,
|
||||
secretValue,
|
||||
confirmed,
|
||||
savedGeneration,
|
||||
pendingCandidate,
|
||||
pendingScopeHint,
|
||||
managePending,
|
||||
validationResult,
|
||||
hasChanges,
|
||||
canWrite,
|
||||
refreshPage,
|
||||
validateConfig,
|
||||
saveConfig,
|
||||
applyConfig,
|
||||
formatTime,
|
||||
serviceLabel
|
||||
saveAndApply,
|
||||
continuePending
|
||||
} = inject('smail-config-page')
|
||||
</script>
|
||||
|
||||
|
||||
@ -243,27 +243,31 @@ export function usePage(pageKey = 'base') {
|
||||
}
|
||||
|
||||
const managePending = async action => {
|
||||
if (!canOperate() || !pendingCandidate.value) return
|
||||
if (!canOperate() || !pendingCandidate.value) return false
|
||||
working.value = action
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
action === 'cancel' ? '取消后保留当前生效配置,候选修改不会应用。确认取消吗?' : '恢复后将候选配置绑定到当前会话,请确认已核对候选内容。',
|
||||
action === 'cancel' ? '取消候选配置' : '恢复候选配置',
|
||||
{ type: 'warning', confirmButtonText: '确认', cancelButtonText: '返回' }
|
||||
)
|
||||
if (action === 'cancel') {
|
||||
await ElMessageBox.confirm(
|
||||
'放弃后会保留当前生效配置,本次修改不会应用。',
|
||||
'放弃本次更改',
|
||||
{ type: 'warning', confirmButtonText: '放弃更改', cancelButtonText: '返回' }
|
||||
)
|
||||
}
|
||||
const response = await http.post('/api/v1/config/pending', {
|
||||
generation: pendingCandidate.value.generation,
|
||||
candidateId: pendingCandidate.value.candidateId,
|
||||
action,
|
||||
confirmation: { confirmed: confirmed.value, method: 'recent_auth' }
|
||||
confirmation: { confirmed: true, method: 'recent_auth' }
|
||||
})
|
||||
notifyResponseError(response, '候选配置操作失败')
|
||||
await loadStatus()
|
||||
await loadSection()
|
||||
await loadPending()
|
||||
ElMessage.success(action === 'cancel' ? '候选已取消,可以继续编辑' : '候选已恢复,请核对后应用')
|
||||
if (action === 'cancel') ElMessage.success('本次更改已放弃')
|
||||
return true
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(error.message || '候选配置操作失败')
|
||||
return false
|
||||
} finally {
|
||||
working.value = ''
|
||||
}
|
||||
@ -528,7 +532,7 @@ export function usePage(pageKey = 'base') {
|
||||
expectedGeneration: Number(status.activeGeneration),
|
||||
payload: parsePayload(),
|
||||
secretChanges,
|
||||
confirmation: { confirmed: confirmed.value, method: 'recent_auth' }
|
||||
confirmation: { confirmed: true, method: 'recent_auth' }
|
||||
}
|
||||
}
|
||||
|
||||
@ -557,8 +561,8 @@ export function usePage(pageKey = 'base') {
|
||||
}
|
||||
}
|
||||
|
||||
const saveConfig = async () => {
|
||||
if (!canEdit() || !checkForm()) return
|
||||
const saveConfig = async ({ quiet = false } = {}) => {
|
||||
if (!canEdit() || !checkForm()) return false
|
||||
working.value = 'save'
|
||||
try {
|
||||
const request = buildRequest()
|
||||
@ -570,20 +574,22 @@ export function usePage(pageKey = 'base') {
|
||||
validationResult.value = { valid: true, generation: savedGeneration.value, issues: [] }
|
||||
await loadStatus()
|
||||
await loadPending()
|
||||
ElMessage.success(`候选配置已保存,代次 ${savedGeneration.value} 等待应用`)
|
||||
if (!quiet) ElMessage.success('配置已保存,等待应用')
|
||||
return true
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '配置保存失败')
|
||||
return false
|
||||
} finally {
|
||||
working.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const applyConfig = async () => {
|
||||
if (!canOperate() || !savedGeneration.value) return
|
||||
const applyConfig = async ({ successMessage = '配置已应用' } = {}) => {
|
||||
if (!canOperate() || !savedGeneration.value) return false
|
||||
if (!savedInput || savedInput.text !== editorText.value || savedInput.secret !== secretValue.value) {
|
||||
invalidateCandidate()
|
||||
ElMessage.warning('填写内容已变更,请重新保存候选配置后再应用')
|
||||
return
|
||||
return false
|
||||
}
|
||||
working.value = 'apply'
|
||||
try {
|
||||
@ -595,14 +601,34 @@ export function usePage(pageKey = 'base') {
|
||||
savedGeneration.value = 0
|
||||
await Promise.all([loadStatus(), loadSection()])
|
||||
await loadPending()
|
||||
ElMessage.success('配置已应用')
|
||||
ElMessage.success(successMessage)
|
||||
return true
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '配置应用失败')
|
||||
return false
|
||||
} finally {
|
||||
working.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const saveAndApply = async () => {
|
||||
if (pendingCandidate.value) {
|
||||
await continuePending()
|
||||
return
|
||||
}
|
||||
const saved = await saveConfig({ quiet: true })
|
||||
if (saved) await applyConfig({ successMessage: '配置已保存并应用' })
|
||||
}
|
||||
|
||||
const continuePending = async () => {
|
||||
if (!pendingCandidate.value) return
|
||||
if (!pendingCandidate.value.sameSession) {
|
||||
const resumed = await managePending('resume')
|
||||
if (!resumed) return
|
||||
}
|
||||
await applyConfig()
|
||||
}
|
||||
|
||||
const previewRollback = async () => {
|
||||
if (!canRollback.value || working.value || !rollbackGeneration.value) return
|
||||
working.value = 'preview'
|
||||
@ -715,7 +741,7 @@ export function usePage(pageKey = 'base') {
|
||||
sectionError, sensitiveHint, secretField, secretValue,
|
||||
confirmed, savedGeneration, pendingCandidate, managePending, validationResult, canWrite, canRollback, canAudit, canExportAudit,
|
||||
backupsLoading, backupsError, backups, rollbackGeneration, rollbackConfirmed, rollbackPreview,
|
||||
auditLoading, auditError, auditItems, loadAll, loadSection, validateConfig, saveConfig, applyConfig,
|
||||
auditLoading, auditError, auditItems, loadAll, loadSection, validateConfig, saveConfig, applyConfig, saveAndApply, continuePending,
|
||||
loadBackups, previewRollback, executeRollback, loadAudit, exportAudit, formatTime,
|
||||
serviceLabel, sectionLabel, operationLabel, databaseConfig, databaseTypeLabel, canMigrate,
|
||||
migrationTargetName, migrationPreview, migrationToken, migrationRecord, migrationStatusLabel, migrationStatusType,
|
||||
|
||||
@ -56,17 +56,19 @@
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.spam-confirmation {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.spam-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.spam-actions :deep(.el-button) {
|
||||
min-width: 112px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.spam-result {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
@ -101,4 +103,4 @@
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { http } from '@/utils/axios'
|
||||
import { useGlobalStatusStore } from '@/stores/useGlobalStatusStore'
|
||||
@ -16,7 +16,6 @@ export function useSpamBlockSettings() {
|
||||
const loading = ref(false)
|
||||
const working = ref('')
|
||||
const errorMessage = ref('')
|
||||
const confirmed = ref(false)
|
||||
const savedGeneration = ref(0)
|
||||
const pendingCandidate = ref(null)
|
||||
let savedInput = ''
|
||||
@ -24,6 +23,7 @@ export function useSpamBlockSettings() {
|
||||
const validationResult = ref(null)
|
||||
const pluginsPayload = ref(null)
|
||||
const settings = reactive({ ...defaultSettings })
|
||||
const hasChanges = computed(() => JSON.stringify(settings) !== savedInput)
|
||||
|
||||
const canWrite = () => globalStatus.hasCapability('config.write_global')
|
||||
|
||||
@ -95,7 +95,7 @@ export function useSpamBlockSettings() {
|
||||
expectedGeneration: activeGeneration.value,
|
||||
payload: buildPayload(),
|
||||
confirmation: {
|
||||
confirmed: confirmed.value,
|
||||
confirmed: true,
|
||||
method: 'recent_auth'
|
||||
}
|
||||
})
|
||||
@ -117,8 +117,8 @@ export function useSpamBlockSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
if (!canWrite() || loading.value || working.value || errorMessage.value || pendingCandidate.value) return
|
||||
const save = async ({ quiet = false } = {}) => {
|
||||
if (!canWrite() || loading.value || working.value || errorMessage.value || pendingCandidate.value) return false
|
||||
working.value = 'save'
|
||||
try {
|
||||
const response = await http.put('/api/v1/config/plugins', buildRequest())
|
||||
@ -128,19 +128,21 @@ export function useSpamBlockSettings() {
|
||||
savedInput = JSON.stringify(settings)
|
||||
validationResult.value = { valid: true, generation: savedGeneration.value, issues: [] }
|
||||
await load()
|
||||
ElMessage.success(`垃圾邮件插件配置已保存,代次 ${savedGeneration.value} 等待应用`)
|
||||
if (!quiet) ElMessage.success('垃圾邮件插件配置已保存,等待应用')
|
||||
return true
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '插件配置保存失败')
|
||||
return false
|
||||
} finally {
|
||||
working.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const apply = async () => {
|
||||
if (!canWrite() || loading.value || working.value || !savedGeneration.value) return
|
||||
const apply = async ({ successMessage = '垃圾邮件插件配置已应用' } = {}) => {
|
||||
if (!canWrite() || loading.value || working.value || !savedGeneration.value) return false
|
||||
if (savedInput !== JSON.stringify(settings)) {
|
||||
ElMessage.warning('填写内容与候选不一致,请刷新查看候选,或先取消候选再修改')
|
||||
return
|
||||
return false
|
||||
}
|
||||
working.value = 'apply'
|
||||
try {
|
||||
@ -151,49 +153,75 @@ export function useSpamBlockSettings() {
|
||||
notifyResponseError(response, '插件配置应用失败')
|
||||
savedGeneration.value = 0
|
||||
await load()
|
||||
ElMessage.success('垃圾邮件插件配置已应用')
|
||||
ElMessage.success(successMessage)
|
||||
return true
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '垃圾邮件插件配置应用失败')
|
||||
return false
|
||||
} finally {
|
||||
working.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const managePending = async action => {
|
||||
if (!canWrite() || loading.value || working.value || !pendingCandidate.value) return
|
||||
if (!canWrite() || loading.value || working.value || !pendingCandidate.value) return false
|
||||
working.value = action
|
||||
try {
|
||||
await ElMessageBox.confirm(action === 'cancel' ? '取消候选后不会影响当前生效配置,确认继续吗?' : '确认将已保存候选恢复到当前会话吗?', '管理候选配置', { type: 'warning', confirmButtonText: '确认', cancelButtonText: '返回' })
|
||||
if (action === 'cancel') {
|
||||
await ElMessageBox.confirm('放弃后会保留当前生效配置,本次修改不会应用。', '放弃本次更改', { type: 'warning', confirmButtonText: '放弃更改', cancelButtonText: '返回' })
|
||||
}
|
||||
const response = await http.post('/api/v1/config/pending', {
|
||||
generation: pendingCandidate.value.generation, action,
|
||||
candidateId: pendingCandidate.value.candidateId,
|
||||
confirmation: { confirmed: confirmed.value, method: 'recent_auth' }
|
||||
confirmation: { confirmed: true, method: 'recent_auth' }
|
||||
})
|
||||
notifyResponseError(response, '候选配置操作失败')
|
||||
await load()
|
||||
if (action === 'cancel') ElMessage.success('本次更改已放弃')
|
||||
return true
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(error.message || '候选配置操作失败')
|
||||
return false
|
||||
} finally {
|
||||
working.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const saveAndApply = async () => {
|
||||
if (pendingCandidate.value) {
|
||||
await continuePending()
|
||||
return
|
||||
}
|
||||
const saved = await save({ quiet: true })
|
||||
if (saved) await apply({ successMessage: '垃圾邮件插件配置已保存并应用' })
|
||||
}
|
||||
|
||||
const continuePending = async () => {
|
||||
if (!pendingCandidate.value) return
|
||||
if (!pendingCandidate.value.sameSession) {
|
||||
const resumed = await managePending('resume')
|
||||
if (!resumed) return
|
||||
}
|
||||
await apply()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
|
||||
return {
|
||||
loading,
|
||||
working,
|
||||
errorMessage,
|
||||
confirmed,
|
||||
savedGeneration,
|
||||
pendingCandidate,
|
||||
managePending,
|
||||
validationResult,
|
||||
settings,
|
||||
hasChanges,
|
||||
canWrite,
|
||||
load,
|
||||
validate,
|
||||
save,
|
||||
apply
|
||||
apply,
|
||||
saveAndApply,
|
||||
continuePending
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -60,21 +60,29 @@
|
||||
</div>
|
||||
<p class="spam-help">阈值越低,过滤越严格,也更容易产生误判;请根据模型返回结果逐步调整。</p>
|
||||
|
||||
<el-checkbox v-if="canWrite()" v-model="confirmed" class="spam-confirmation">
|
||||
我已确认这是受控变更,并允许使用最近认证完成高风险配置操作
|
||||
</el-checkbox>
|
||||
<div class="spam-actions">
|
||||
<el-button :loading="working === 'validate'" :disabled="!canWrite() || !!working || !!pendingCandidate || loading" @click="validate">只验证</el-button>
|
||||
<el-button type="primary" :loading="working === 'save'" :disabled="!canWrite() || !!working || !!pendingCandidate || loading" @click="save">保存候选配置</el-button>
|
||||
<el-button type="success" plain :loading="working === 'apply'" :disabled="!canWrite() || !savedGeneration || !!working || loading" @click="apply">
|
||||
应用{{ savedGeneration ? `代次 ${savedGeneration}` : '' }}
|
||||
<el-button
|
||||
v-if="!pendingCandidate"
|
||||
type="primary"
|
||||
:loading="working === 'save' || working === 'apply'"
|
||||
:disabled="!canWrite() || !!working || loading || !hasChanges"
|
||||
@click="saveAndApply"
|
||||
>
|
||||
保存并应用
|
||||
</el-button>
|
||||
<el-button v-if="canWrite() && pendingCandidate && !pendingCandidate.sameSession" :disabled="!!working || !confirmed" @click="managePending('resume')">恢复候选到当前会话</el-button>
|
||||
<el-button v-if="canWrite() && pendingCandidate" type="warning" plain :disabled="!!working || pendingCandidate.committed" @click="managePending('cancel')">取消候选配置</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="primary"
|
||||
:loading="working === 'resume' || working === 'apply'"
|
||||
: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>
|
||||
<div v-if="validationResult" class="spam-result" :class="validationResult.valid ? 'is-success' : 'is-error'">
|
||||
<strong>{{ validationResult.valid ? '验证通过' : '验证未通过' }}</strong>
|
||||
<span v-if="validationResult.generation">候选代次:{{ validationResult.generation }}</span>
|
||||
<div v-if="validationResult && !validationResult.valid" class="spam-result is-error">
|
||||
<strong>配置检查未通过</strong>
|
||||
<ul v-if="validationResult.issues?.length">
|
||||
<li v-for="issue in validationResult.issues" :key="`${issue.code}-${issue.message}`">{{ issue.code }}:{{ issue.message }}</li>
|
||||
</ul>
|
||||
@ -90,17 +98,15 @@ const {
|
||||
loading,
|
||||
working,
|
||||
errorMessage,
|
||||
confirmed,
|
||||
savedGeneration,
|
||||
pendingCandidate,
|
||||
managePending,
|
||||
validationResult,
|
||||
settings,
|
||||
hasChanges,
|
||||
canWrite,
|
||||
load,
|
||||
validate,
|
||||
save,
|
||||
apply
|
||||
saveAndApply,
|
||||
continuePending
|
||||
} = useSpamBlockSettings()
|
||||
</script>
|
||||
|
||||
|
||||
@ -390,7 +390,7 @@ test('独立页模板可编译,编辑器不再连接维护面板且权限绑
|
||||
assert.doesNotThrow(() => compileScript(descriptor, { id: 'config-layout', inlineTemplate: true }))
|
||||
assert.deepEqual(compileStyle({ source: css, id: 'data-v-config-layout', scoped: true }).errors, [])
|
||||
assert.match(template, /v-if="canWrite" class="editor-footer"/)
|
||||
assert.match(template, /v-model="confirmed"/)
|
||||
assert.doesNotMatch(template, /v-model="confirmed"/)
|
||||
assert.doesNotMatch(template, /maintenance-panel|migration-panel|rollback-panel|audit-panel/)
|
||||
for (const [key, capability] of [['migration', 'canMigrate'], ['restart', 'canRestart'], ['backups', 'canRollback'], ['audit', 'canAudit']]) {
|
||||
assert.ok(maintenance.includes(`pageKey === '${key}' && ${capability}`))
|
||||
@ -407,20 +407,62 @@ 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')
|
||||
const editorCss = readFileSync(new URL('ConfigEditor/ConfigEditor.css', root), 'utf8')
|
||||
assert.doesNotMatch(template, /change-workflow|step-number|配置变更流程/)
|
||||
assert.doesNotMatch(sharedCss + editorCss, /change-workflow|step-number/)
|
||||
for (const action of ['validateConfig', 'saveConfig', 'applyConfig']) {
|
||||
assert.match(template, new RegExp(`<el-button\\b[^>]*@click="${action}"[^>]*>`))
|
||||
}
|
||||
assert.match(template, /@click="saveAndApply"/)
|
||||
assert.match(template, /保存并应用/)
|
||||
assert.match(template, /@click="continuePending"/)
|
||||
assert.match(template, /继续应用/)
|
||||
assert.match(template, /v-if="canWrite" class="editor-footer"/)
|
||||
assert.match(template, /<el-checkbox\b[^>]*v-model="confirmed"/)
|
||||
assert.match(template, /@click="managePending\('resume'\)"/)
|
||||
assert.match(template, /@click="managePending\('cancel'\)"/)
|
||||
assert.match(template, /放弃更改/)
|
||||
assert.doesNotMatch(template, /只验证|保存候选配置|我已确认这是受控变更|应用代次/)
|
||||
})
|
||||
|
||||
test('保存并应用一次点击按候选协议连续完成保存和应用', async () => {
|
||||
const requests = []
|
||||
let pending = null
|
||||
const active = {
|
||||
listener: { port: 25, starttlsPort: 587, tlsPort: 465 },
|
||||
relay: { relayHost: '', relayPort: 0, security: '', from: '', fromDomain: '' },
|
||||
imap: { tlsPort: 993 },
|
||||
pop3: { port: 110, tlsPort: 995 },
|
||||
policy: { maxMessageBytes: 10 << 20, spamFilterLevel: 2 }
|
||||
}
|
||||
const { page } = fixture(async path => ({
|
||||
errorNo: 0,
|
||||
data: path.endsWith('/status') ? { activeGeneration: 4 } : active
|
||||
}), 'mail', {
|
||||
http: {
|
||||
get: async path => ({
|
||||
errorNo: 0,
|
||||
data: path.endsWith('/pending') ? pending : path.endsWith('/status') ? { activeGeneration: 4 } : active
|
||||
}),
|
||||
put: async (path, body) => {
|
||||
requests.push(path)
|
||||
pending = { generation: 5, candidateId: 'one-click-5', section: 'mail', sameSession: true, payload: body.payload }
|
||||
return { errorNo: 0, data: pending }
|
||||
},
|
||||
post: async (path, body) => {
|
||||
requests.push(path)
|
||||
assert.equal(body.generation, 5)
|
||||
assert.equal(body.candidateId, 'one-click-5')
|
||||
pending = null
|
||||
return { errorNo: 0, data: { generation: 5 } }
|
||||
}
|
||||
}
|
||||
})
|
||||
await page.loadAll()
|
||||
page.updateField(['policy', 'maxMessageBytes'], 12 << 20)
|
||||
await page.saveAndApply()
|
||||
assert.deepEqual(requests, ['/api/v1/config/mail', '/api/v1/config/mail/apply'])
|
||||
assert.equal(page.pendingCandidate.value, null)
|
||||
assert.equal(page.savedGeneration.value, 0)
|
||||
})
|
||||
|
||||
test('配置编辑区移除填写标题、说明与重复标签,保留主标题和必要提示', () => {
|
||||
@ -434,7 +476,7 @@ test('配置编辑区移除填写标题、说明与重复标签,保留主标
|
||||
assert.match(template, /v-if="sectionError"[\s\S]*?:title="sectionError"/)
|
||||
assert.doesNotMatch(template, /config-sensitive-hint|sensitiveHint/)
|
||||
assert.match(template, /v-if="secretField" class="secret-editor"/)
|
||||
assert.match(template, /v-if="validationResult"[^>]*role="status"/)
|
||||
assert.match(template, /v-if="validationResult && !validationResult\.valid"[^>]*role="status"/)
|
||||
})
|
||||
|
||||
test('离开、刷新和正在执行的变更受保护,取消时保留编辑内容', async () => {
|
||||
|
||||
@ -113,7 +113,21 @@ for (const action of ['edit', 'refresh']) {
|
||||
test(`保存候选配置后 ${action} 不应让页面失去继续保存或应用的入口`, async () => {
|
||||
const source = read('src/views/components/ConfigSettings/ConfigSettings.js')
|
||||
.replace(/^import .*$/gm, '').replace('export function usePage', 'function usePage')
|
||||
const initial = { generation: 1, policy: { smtpPort: 25, imapPort: 993, pop3Port: 995, maxMessageBytes: 0 } }
|
||||
const initial = {
|
||||
listener: { port: 25, starttlsPort: 587, tlsPort: 465 },
|
||||
relay: {
|
||||
relayHost: '',
|
||||
relayPort: 0,
|
||||
security: '',
|
||||
from: '',
|
||||
fromDomain: '',
|
||||
verificationEnabled: true,
|
||||
loginPolicy: 'new_device'
|
||||
},
|
||||
imap: { tlsPort: 993 },
|
||||
pop3: { port: 110, tlsPort: 995 },
|
||||
policy: { maxMessageBytes: 10 << 20, spamFilterLevel: 2, externalRecoveryEnabled: true }
|
||||
}
|
||||
let pending = null
|
||||
let applied = false
|
||||
const errors = []
|
||||
@ -121,7 +135,7 @@ for (const action of ['edit', 'refresh']) {
|
||||
get: async url => ({ errorNo: 0, data: url.endsWith('/pending') ? pending : url.endsWith('/status') ? { activeGeneration: 1, setupState: 'ready' } : initial }),
|
||||
put: async (url, body) => {
|
||||
if (pending) return { errorNo: 409, errorMsg: '配置代次冲突' }
|
||||
pending = { generation: 2, candidateId: 'audit-candidate', sameSession: true, section: 'email', payload: body.payload }
|
||||
pending = { generation: 2, candidateId: 'audit-candidate', sameSession: true, section: 'mail', payload: body.payload }
|
||||
return { errorNo: 0, data: pending }
|
||||
},
|
||||
post: async (url, body) => {
|
||||
@ -133,7 +147,7 @@ for (const action of ['edit', 'refresh']) {
|
||||
return { errorNo: 0, data: { generation: 2 } }
|
||||
}
|
||||
}
|
||||
const page = compileFunction(source + '\nreturn usePage("email")', [
|
||||
const page = compileFunction(source + '\nreturn usePage("mail")', [
|
||||
'computed', 'onMounted', 'onBeforeUnmount', 'reactive', 'ref', 'ElMessage', 'ElMessageBox',
|
||||
'http', 'useGlobalStatusStore', 'readFormConfig', 'updateFormConfig', 'validateFormConfig'
|
||||
])(
|
||||
@ -148,8 +162,7 @@ for (const action of ['edit', 'refresh']) {
|
||||
assert.deepEqual(errors, [])
|
||||
if (action === 'edit') page.updateField(['policy', 'maxMessageBytes'], 16 << 20)
|
||||
else await page.refreshPage()
|
||||
await page.saveConfig()
|
||||
await page.applyConfig()
|
||||
await page.continuePending()
|
||||
console.log(`action=${action} server_pending=${pending} saved_generation=${page.savedGeneration.value} apply_requested=${applied} errors=${JSON.stringify(errors)}`)
|
||||
assert.equal(applied, true, '页面必须保留恢复或取消候选配置的可用路径,而不是保存冲突且应用不发请求')
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user