保存证书路径输入布局修复进度
This commit is contained in:
parent
52d6e90735
commit
c4f53f28e9
@ -1,4 +1,4 @@
|
||||
import { computed, nextTick } from 'vue'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { fieldKey } from '../ConfigForm/ConfigForm.js'
|
||||
import { usePagination } from '../ConfigPagination/ConfigPagination.js'
|
||||
|
||||
@ -11,6 +11,8 @@ export function useField(props, emit) {
|
||||
props.error && `${id.value}-error`
|
||||
].filter(Boolean).join(' ') || undefined)
|
||||
const entries = computed(() => Array.isArray(props.value) ? props.value : [])
|
||||
const textValue = ref(typeof props.value === 'string' ? props.value : '')
|
||||
const textEditing = ref(false)
|
||||
const pagination = usePagination(entries, 2)
|
||||
const selectOptions = computed(() => {
|
||||
const values = props.field.options || []
|
||||
@ -22,6 +24,17 @@ export function useField(props, emit) {
|
||||
const update = value => {
|
||||
if (!props.disabled && !props.field.locked) emit('change', props.field.path, value)
|
||||
}
|
||||
const updateText = value => {
|
||||
textValue.value = String(value ?? '')
|
||||
update(textValue.value)
|
||||
}
|
||||
const beginTextEditing = () => {
|
||||
textEditing.value = true
|
||||
}
|
||||
const endTextEditing = () => {
|
||||
textEditing.value = false
|
||||
textValue.value = typeof props.value === 'string' ? props.value : ''
|
||||
}
|
||||
const updateEntry = (index, value) => update(entries.value.map((entry, current) => current === index ? value : entry))
|
||||
const updateTarget = (index, key, value) => updateEntry(index, { ...entries.value[index], [key]: value ?? null })
|
||||
const removeEntry = index => update(entries.value.filter((_, current) => current !== index))
|
||||
@ -31,5 +44,27 @@ export function useField(props, emit) {
|
||||
pagination.currentPage.value = pagination.pageCount.value
|
||||
}
|
||||
|
||||
return { id, descriptionId, entries, selectOptions, update, updateEntry, updateTarget, removeEntry, addEntry, ...pagination }
|
||||
watch(() => props.value, value => {
|
||||
const nextValue = typeof value === 'string' ? value : ''
|
||||
if (!textEditing.value || nextValue !== textValue.value) {
|
||||
textValue.value = nextValue
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
id,
|
||||
descriptionId,
|
||||
entries,
|
||||
selectOptions,
|
||||
textValue,
|
||||
update,
|
||||
updateText,
|
||||
beginTextEditing,
|
||||
endTextEditing,
|
||||
updateEntry,
|
||||
updateTarget,
|
||||
removeEntry,
|
||||
addEntry,
|
||||
...pagination
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,7 +122,7 @@
|
||||
v-else
|
||||
class="field-text-input"
|
||||
:id="id"
|
||||
:model-value="value ?? ''"
|
||||
:model-value="textValue"
|
||||
:disabled="disabled || field.locked"
|
||||
:placeholder="field.placeholder || '填写内容'"
|
||||
:aria-describedby="descriptionId"
|
||||
@ -130,7 +130,9 @@
|
||||
:type="field.type === 'password' ? 'password' : 'text'"
|
||||
:show-password="field.type === 'password'"
|
||||
:autocomplete="field.type === 'password' ? 'new-password' : 'off'"
|
||||
@update:model-value="update"
|
||||
@focus="beginTextEditing"
|
||||
@blur="endTextEditing"
|
||||
@update:model-value="updateText"
|
||||
/>
|
||||
<ConfigPagination
|
||||
v-if="['list', 'network'].includes(field.type)"
|
||||
@ -171,8 +173,9 @@ const props = defineProps({
|
||||
})
|
||||
const emit = defineEmits(['change'])
|
||||
const {
|
||||
id, descriptionId, selectOptions, update, updateEntry, updateTarget, removeEntry,
|
||||
addEntry, currentPage, pageCount, pageItems, total
|
||||
id, descriptionId, selectOptions, textValue, update, updateText, beginTextEditing,
|
||||
endTextEditing, updateEntry, updateTarget, removeEntry, addEntry, currentPage,
|
||||
pageCount, pageItems, total
|
||||
} = useField(props, emit)
|
||||
</script>
|
||||
|
||||
|
||||
@ -56,6 +56,11 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* SSL 路径必须保持整行宽度,避免长路径被多列布局截断。 */
|
||||
.config-form[data-form-section="ssl"] .form-group[data-group="certificate"] .form-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.section-error {
|
||||
margin-left: 6px;
|
||||
color: var(--el-color-danger);
|
||||
|
||||
@ -160,6 +160,7 @@ const securityPolicyGroups = [
|
||||
const sslGroups = [
|
||||
group('certificate', '证书与存储', '', [
|
||||
field('tls.management', '证书管理方式', 'select', {
|
||||
wide: true,
|
||||
fallback: 'custom',
|
||||
options: options([
|
||||
['web_server', 'Web 服务器管理'],
|
||||
@ -168,15 +169,18 @@ const sslGroups = [
|
||||
])
|
||||
}),
|
||||
field('tls.challenge', '证书验证方式', 'select', {
|
||||
wide: true,
|
||||
show: projectManagedCertificate,
|
||||
fallback: 'http',
|
||||
options: options([['http', 'HTTP 验证'], ['dns', 'DNS 验证']])
|
||||
}),
|
||||
field('tls.certificateRef', '证书文件', 'text', {
|
||||
wide: true,
|
||||
show: externalManagedCertificate,
|
||||
placeholder: 'config/ssl/example.com/fullchain.pem'
|
||||
}),
|
||||
field('tls.privateKeyRef', '私钥文件', 'text', {
|
||||
wide: true,
|
||||
show: externalManagedCertificate,
|
||||
placeholder: 'config/ssl/example.com/privkey.pem'
|
||||
})
|
||||
|
||||
@ -317,6 +317,42 @@ test('证书管理方式只展示三种业务模式并同步底层托管配置',
|
||||
assert.ok(getFieldGroups('ssl', updated).flatMap(group => group.fields).some(field => field.path.join('.') === 'tls.certificateRef'))
|
||||
})
|
||||
|
||||
test('自定义证书配置使用单列三行并完整展示路径输入框', () => {
|
||||
const tls = {
|
||||
management: 'custom',
|
||||
mode: 'managed_upload',
|
||||
defaultDirectory: 'config/ssl',
|
||||
customDirectory: 'config/ssl/imported',
|
||||
certificateRef: 'config/ssl/imported/fullchain.pem',
|
||||
privateKeyRef: 'config/ssl/imported/privkey.pem'
|
||||
}
|
||||
const fields = getFieldGroups('ssl', { tls }).flatMap(group => group.fields)
|
||||
assert.deepEqual(fields.map(field => field.path.join('.')), [
|
||||
'tls.management',
|
||||
'tls.certificateRef',
|
||||
'tls.privateKeyRef'
|
||||
])
|
||||
assert.ok(fields.every(field => field.wide === true))
|
||||
|
||||
const css = readFileSync(new URL('ConfigForm/ConfigForm.css', root), 'utf8')
|
||||
assert.match(
|
||||
css,
|
||||
/\.config-form\[data-form-section="ssl"\] \.form-group\[data-group="certificate"\] \.form-grid\s*\{\s*grid-template-columns: minmax\(0, 1fr\);/
|
||||
)
|
||||
})
|
||||
|
||||
test('普通文本输入使用本地编辑值,父级配置刷新不会抢占光标', () => {
|
||||
const template = readFileSync(new URL('ConfigField/ConfigField.vue', root), 'utf8')
|
||||
const script = readFileSync(new URL('ConfigField/ConfigField.js', root), 'utf8')
|
||||
assert.match(template, /:model-value="textValue"/)
|
||||
assert.match(template, /@focus="beginTextEditing"/)
|
||||
assert.match(template, /@blur="endTextEditing"/)
|
||||
assert.match(template, /@update:model-value="updateText"/)
|
||||
assert.match(script, /const textValue = ref\(/)
|
||||
assert.match(script, /const textEditing = ref\(false\)/)
|
||||
assert.match(script, /watch\(\(\) => props\.value/)
|
||||
})
|
||||
|
||||
test('统一加密方式同步切换 SMTP、IMAP、POP3 与已配置中继', () => {
|
||||
for (const [security, expected] of [
|
||||
['both', { relay: 587, relaySecurity: 'starttls', smtpStartTLS: 587, smtpTLS: 465, imap: 993, pop3: 110, pop3TLS: 995 }],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user