保存单屏布局与三级菜单优化进度,继续完善验收
This commit is contained in:
parent
64de3a94f8
commit
09c7f6e6df
@ -1,6 +1,6 @@
|
||||
.config-overview {
|
||||
min-width: 0;
|
||||
padding-bottom: 18px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--pm-border-color);
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.overview-copy {
|
||||
@ -31,7 +31,7 @@
|
||||
}
|
||||
|
||||
.overview-copy .settings-desc {
|
||||
margin: 6px 0 0;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.overview-actions {
|
||||
@ -47,7 +47,7 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 0;
|
||||
margin: 16px 0 0;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
|
||||
.overview-meta-item {
|
||||
@ -56,7 +56,7 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 8px;
|
||||
min-width: 0;
|
||||
padding: 0 16px;
|
||||
padding: 0 12px;
|
||||
border-left: 1px solid var(--pm-border-color);
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@ -31,6 +31,40 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.field-help {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--pm-border-color);
|
||||
border-radius: 50%;
|
||||
background: var(--pm-bg-primary);
|
||||
color: var(--pm-text-secondary);
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.field-help:hover,
|
||||
.field-help:focus-visible {
|
||||
color: var(--pm-primary-color);
|
||||
border-color: currentColor;
|
||||
outline: 2px solid color-mix(in srgb, var(--pm-primary-color) 15%, transparent);
|
||||
}
|
||||
|
||||
/* 说明由可聚焦的帮助按钮展开,同时保留表单控件的无障碍描述。 */
|
||||
.field-description {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.config-field > .el-input-number,
|
||||
.config-field > .el-select {
|
||||
width: 100%;
|
||||
@ -115,7 +149,7 @@
|
||||
color: var(--pm-text-secondary);
|
||||
}
|
||||
|
||||
@container config-main (max-width: 620px) {
|
||||
@container config-main (max-width: 440px) {
|
||||
.network-row {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
padding: 10px;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { computed } from 'vue'
|
||||
import { computed, nextTick } from 'vue'
|
||||
import { fieldKey } from '../ConfigForm/ConfigForm.js'
|
||||
import { usePagination } from '../ConfigPagination/ConfigPagination.js'
|
||||
|
||||
export { fieldKey }
|
||||
|
||||
@ -10,6 +11,7 @@ export function useField(props, emit) {
|
||||
props.error && `${id.value}-error`
|
||||
].filter(Boolean).join(' ') || undefined)
|
||||
const entries = computed(() => Array.isArray(props.value) ? props.value : [])
|
||||
const pagination = usePagination(entries, 2)
|
||||
const selectOptions = computed(() => {
|
||||
const values = props.field.options || []
|
||||
const current = props.value ?? props.field.fallback ?? ''
|
||||
@ -23,6 +25,11 @@ export function useField(props, emit) {
|
||||
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))
|
||||
const addEntry = async () => {
|
||||
update([...entries.value, props.field.type === 'network' ? { scheme: 'https', host: '', port: 443 } : ''])
|
||||
await nextTick()
|
||||
pagination.currentPage.value = pagination.pageCount.value
|
||||
}
|
||||
|
||||
return { id, descriptionId, entries, selectOptions, update, updateEntry, updateTarget, removeEntry }
|
||||
return { id, descriptionId, entries, selectOptions, update, updateEntry, updateTarget, removeEntry, addEntry, ...pagination }
|
||||
}
|
||||
|
||||
@ -4,6 +4,11 @@
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="field-required" aria-hidden="true">*</span>
|
||||
<span v-if="field.locked" class="field-locked">系统维护</span>
|
||||
<el-popover v-if="field.hint" trigger="click" :width="300" :content="field.hint" placement="top">
|
||||
<template #reference>
|
||||
<button type="button" class="field-help" :aria-label="`${field.label}填写说明`" @click.prevent>?</button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</label>
|
||||
<el-select
|
||||
v-if="field.type === 'select'"
|
||||
@ -46,10 +51,10 @@
|
||||
@update:model-value="update"
|
||||
/>
|
||||
<div v-else-if="field.type === 'list'" class="field-list" role="group" :aria-labelledby="`${id}-label`">
|
||||
<div v-for="(entry, index) in entries" :key="index" class="field-list-row">
|
||||
<div v-for="{ entry, index } in pageItems" :key="index" class="field-list-row">
|
||||
<el-input
|
||||
class="field-text-input"
|
||||
:id="index === 0 ? id : `${id}-${index}`"
|
||||
:id="index === pageItems[0]?.index ? id : `${id}-${index}`"
|
||||
:model-value="entry"
|
||||
:disabled="disabled || field.locked"
|
||||
:placeholder="field.placeholder || '填写内容'"
|
||||
@ -60,12 +65,12 @@
|
||||
/>
|
||||
<el-button :disabled="disabled || field.locked" :aria-label="`移除${field.label}第 ${index + 1} 项`" @click="removeEntry(index)">移除</el-button>
|
||||
</div>
|
||||
<el-button class="field-add" :disabled="disabled || field.locked" plain @click="update([...entries, ''])">添加一项</el-button>
|
||||
<el-button class="field-add" :disabled="disabled || field.locked" plain @click="addEntry">添加一项</el-button>
|
||||
</div>
|
||||
<div v-else-if="field.type === 'network'" class="field-list" role="group" :aria-labelledby="`${id}-label`">
|
||||
<div v-for="(entry, index) in entries" :key="index" class="network-row">
|
||||
<div v-for="{ entry, index } in pageItems" :key="index" class="network-row">
|
||||
<el-select
|
||||
:id="index === 0 ? id : `${id}-${index}`"
|
||||
:id="index === pageItems[0]?.index ? id : `${id}-${index}`"
|
||||
:model-value="entry.scheme"
|
||||
:disabled="disabled || field.locked"
|
||||
:aria-label="`网络目标 ${index + 1} 协议`"
|
||||
@ -101,7 +106,7 @@
|
||||
/>
|
||||
<el-button :disabled="disabled || field.locked" :aria-label="`移除网络目标 ${index + 1}`" @click="removeEntry(index)">移除</el-button>
|
||||
</div>
|
||||
<el-button class="field-add" :disabled="disabled || field.locked" plain @click="update([...entries, { scheme: 'https', host: '', port: 443 }])">添加网络目标</el-button>
|
||||
<el-button class="field-add" :disabled="disabled || field.locked" plain @click="addEntry">添加网络目标</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
v-else
|
||||
@ -115,6 +120,13 @@
|
||||
autocomplete="off"
|
||||
@update:model-value="update"
|
||||
/>
|
||||
<ConfigPagination
|
||||
v-if="['list', 'network'].includes(field.type)"
|
||||
v-model:current-page="currentPage"
|
||||
:page-count="pageCount"
|
||||
:total="total"
|
||||
:label="`${field.label}分页`"
|
||||
/>
|
||||
<div v-if="field.recommendedPort !== undefined" class="field-port-guide">
|
||||
<span :class="{ 'port-custom': value !== field.recommendedPort }">
|
||||
{{ value === field.recommendedPort ? '已使用推荐端口' : '推荐端口' }}:{{ field.recommendedPort }}
|
||||
@ -130,13 +142,14 @@
|
||||
使用推荐端口
|
||||
</el-button>
|
||||
</div>
|
||||
<p v-if="field.hint" :id="`${id}-hint`" class="field-hint">{{ field.hint }}</p>
|
||||
<p v-if="field.hint" :id="`${id}-hint`" class="field-hint field-description">{{ field.hint }}</p>
|
||||
<p v-if="error" :id="`${id}-error`" class="field-error" role="alert">{{ error }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { fieldKey, useField } from './ConfigField.js'
|
||||
import ConfigPagination from '../ConfigPagination/ConfigPagination.vue'
|
||||
|
||||
const props = defineProps({
|
||||
field: { type: Object, required: true },
|
||||
@ -145,7 +158,10 @@ const props = defineProps({
|
||||
error: { type: String, default: '' }
|
||||
})
|
||||
const emit = defineEmits(['change'])
|
||||
const { id, descriptionId, entries, selectOptions, update, updateEntry, updateTarget, removeEntry } = useField(props, emit)
|
||||
const {
|
||||
id, descriptionId, selectOptions, update, updateEntry, updateTarget, removeEntry,
|
||||
addEntry, currentPage, pageCount, pageItems, total
|
||||
} = useField(props, emit)
|
||||
</script>
|
||||
|
||||
<style scoped src="./ConfigField.css"></style>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
.config-form {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-form:not(:first-child) {
|
||||
margin-top: 16px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
@ -13,14 +13,14 @@
|
||||
}
|
||||
|
||||
.form-group + .form-group {
|
||||
padding-top: 20px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--pm-border-color);
|
||||
}
|
||||
|
||||
.credential-status,
|
||||
.form-group-note,
|
||||
.form-empty {
|
||||
margin: 0 0 12px;
|
||||
margin: 0 0 8px;
|
||||
color: var(--pm-text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
@ -31,12 +31,37 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
align-items: start;
|
||||
gap: 20px;
|
||||
gap: 12px 16px;
|
||||
}
|
||||
|
||||
@container config-main (max-width: 620px) {
|
||||
@container config-main (min-width: 640px) {
|
||||
.form-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.config-form[data-form-section="base"] {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.config-form[data-form-section="base"] .form-group {
|
||||
align-self: start;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--pm-border-color);
|
||||
border-radius: 10px;
|
||||
background: var(--pm-bg-primary);
|
||||
}
|
||||
|
||||
.config-form[data-form-section="base"] .form-grid,
|
||||
.config-form[data-form-section="email"] .form-grid,
|
||||
.form-group[data-group="verification"] .form-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@container config-main (max-width: 440px) {
|
||||
.form-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="config-form" :data-form-section="section">
|
||||
<p v-if="!groups.length" class="form-empty">当前插件配置不存在,请刷新配置或检查初始化状态。</p>
|
||||
<section v-for="group in groups" :key="group.id" class="form-group" :aria-label="group.title">
|
||||
<section v-for="group in groups" :key="group.id" class="form-group" :data-group="group.id" :aria-label="group.title">
|
||||
<p v-if="group.secretConfigured !== undefined" class="credential-status">{{ group.secretConfigured ? '凭据已配置' : '未配置凭据' }}</p>
|
||||
<p v-if="group.note" class="form-group-note" role="note">{{ group.note }}</p>
|
||||
<div class="form-grid">
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { usePage } from '../ConfigSettings.js'
|
||||
import { usePagination } from '../ConfigPagination/ConfigPagination.js'
|
||||
|
||||
export function useMaintenance(pageKey) {
|
||||
const page = usePage(pageKey)
|
||||
@ -8,5 +9,7 @@ export function useMaintenance(pageKey) {
|
||||
page.rollbackPreview.value = null
|
||||
page.rollbackConfirmed.value = false
|
||||
}
|
||||
return { ...page, selectBackup }
|
||||
const auditPagination = usePagination(page.auditItems, 6)
|
||||
const backupPagination = usePagination(page.backups, 6)
|
||||
return { ...page, selectBackup, auditPagination, backupPagination }
|
||||
}
|
||||
|
||||
@ -120,7 +120,7 @@
|
||||
<el-empty v-else-if="!backups.length && !backupsLoading" description="暂无可用配置备份" :image-size="72" />
|
||||
<div v-else class="backup-list">
|
||||
<button
|
||||
v-for="backup in backups"
|
||||
v-for="{ entry: backup } in backupRows"
|
||||
:key="backup.generation"
|
||||
type="button"
|
||||
class="backup-item"
|
||||
@ -133,6 +133,7 @@
|
||||
<small>{{ formatTime(backup.createdAt) }}</small>
|
||||
</button>
|
||||
</div>
|
||||
<ConfigPagination v-model:current-page="backupPage" :page-count="backupPageCount" :total="backups.length" label="备份列表" />
|
||||
<el-checkbox v-model="rollbackConfirmed" :disabled="!!working || !rollbackPreview" class="confirmation-check">
|
||||
我已阅读回滚预览,并确认执行一次性回滚
|
||||
</el-checkbox>
|
||||
@ -161,7 +162,7 @@
|
||||
</div>
|
||||
<el-alert v-if="auditError" :title="auditError" type="warning" :closable="false" show-icon />
|
||||
<div v-else-if="auditItems.length" class="audit-list">
|
||||
<div v-for="item in auditItems" :key="item.id || `${item.createdAt}-${item.operation}`" class="audit-item">
|
||||
<div v-for="{ entry: item } in auditRows" :key="item.id || `${item.createdAt}-${item.operation}`" class="audit-item">
|
||||
<div>
|
||||
<strong>{{ operationLabel(item.operation) }}</strong>
|
||||
<span>{{ sectionLabel(item.section) }} · {{ item.actorId || '系统' }}</span>
|
||||
@ -170,6 +171,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else-if="!auditLoading" description="暂无配置审计记录" :image-size="72" />
|
||||
<ConfigPagination v-model:current-page="auditPage" :page-count="auditPageCount" :total="auditItems.length" label="审计记录" />
|
||||
</div>
|
||||
</section>
|
||||
<el-empty v-else description="当前账号无权访问此页面" />
|
||||
@ -179,6 +181,7 @@
|
||||
|
||||
<script setup>
|
||||
import { useMaintenance } from './ConfigMaintenance.js'
|
||||
import ConfigPagination from '../ConfigPagination/ConfigPagination.vue'
|
||||
|
||||
const props = defineProps({
|
||||
pageKey: { type: String, required: true },
|
||||
@ -194,8 +197,10 @@ const {
|
||||
canRollback, backupsLoading, backupsError, backups, rollbackGeneration, rollbackConfirmed,
|
||||
rollbackPreview, loadBackups, selectBackup, previewRollback, executeRollback,
|
||||
canAudit, canExportAudit, auditLoading, auditError, auditItems, loadAudit, exportAudit,
|
||||
formatTime, sectionLabel, operationLabel
|
||||
formatTime, sectionLabel, operationLabel, auditPagination, backupPagination
|
||||
} = useMaintenance(props.pageKey)
|
||||
const { currentPage: auditPage, pageCount: auditPageCount, pageItems: auditRows } = auditPagination
|
||||
const { currentPage: backupPage, pageCount: backupPageCount, pageItems: backupRows } = backupPagination
|
||||
|
||||
defineExpose({ beforeLeave })
|
||||
</script>
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
.config-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
color: var(--pm-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.config-pagination > span {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.config-pagination .el-button {
|
||||
margin: 0;
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
// 只分页展示,不切割源配置;绝对索引确保编辑和移除始终命中原条目。
|
||||
export function usePagination(items, pageSize = 6) {
|
||||
const requestedPage = ref(1)
|
||||
const total = computed(() => items.value.length)
|
||||
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / pageSize)))
|
||||
const currentPage = computed({
|
||||
get: () => Math.min(requestedPage.value, pageCount.value),
|
||||
set: value => { requestedPage.value = Math.max(1, Math.min(pageCount.value, Number(value) || 1)) }
|
||||
})
|
||||
const pageItems = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize
|
||||
return items.value.slice(start, start + pageSize).map((entry, offset) => ({ entry, index: start + offset }))
|
||||
})
|
||||
return { currentPage, pageCount, pageItems, total }
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<nav v-if="pageCount > 1" class="config-pagination" :aria-label="label">
|
||||
<span aria-live="polite">共 {{ total }} 项 · {{ currentPage }} / {{ pageCount }} 页</span>
|
||||
<el-button size="small" :disabled="disabled || currentPage <= 1" :aria-label="`${label}上一页`" @click="emit('update:currentPage', currentPage - 1)">上一页</el-button>
|
||||
<el-button size="small" :disabled="disabled || currentPage >= pageCount" :aria-label="`${label}下一页`" @click="emit('update:currentPage', currentPage + 1)">下一页</el-button>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
currentPage: { type: Number, required: true },
|
||||
pageCount: { type: Number, required: true },
|
||||
total: { type: Number, required: true },
|
||||
label: { type: String, default: '列表分页' },
|
||||
disabled: Boolean
|
||||
})
|
||||
const emit = defineEmits(['update:currentPage'])
|
||||
</script>
|
||||
|
||||
<style scoped src="./ConfigPagination.css"></style>
|
||||
@ -1,14 +1,13 @@
|
||||
.config-settings {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
padding-bottom: 16px;
|
||||
gap: 12px;
|
||||
--config-accent: var(--pm-primary-color);
|
||||
}
|
||||
.config-page,
|
||||
.config-page-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
.config-navigation {
|
||||
@ -16,7 +15,7 @@
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
gap: 12px;
|
||||
}
|
||||
.config-navigation > .settings-submenu {
|
||||
grid-area: auto;
|
||||
@ -83,11 +82,11 @@
|
||||
.editor-panel,
|
||||
.maintenance-panel {
|
||||
border: 1px solid var(--pm-border-color);
|
||||
border-radius: 16px;
|
||||
border-radius: 12px;
|
||||
background: var(--pm-bg-secondary);
|
||||
}
|
||||
.editor-panel {
|
||||
padding: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
.tag-list {
|
||||
display: flex;
|
||||
@ -100,16 +99,16 @@
|
||||
}
|
||||
.config-workspace {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 152px;
|
||||
grid-template-columns: minmax(0, 1fr) 196px;
|
||||
grid-template-areas: "main submenu";
|
||||
align-items: start;
|
||||
gap: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
.config-main {
|
||||
grid-area: main;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
container: config-main / inline-size;
|
||||
}
|
||||
@ -138,9 +137,9 @@
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(180px, 1fr);
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
padding: 14px;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--pm-border-color);
|
||||
border-radius: 10px;
|
||||
background: var(--pm-bg-primary);
|
||||
@ -163,8 +162,8 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
.editor-footer {
|
||||
margin-top: 18px;
|
||||
padding-top: 16px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--pm-border-color);
|
||||
}
|
||||
.editor-footer .confirmation-check {
|
||||
@ -183,7 +182,7 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.action-row > .el-button,
|
||||
.audit-actions > .el-button {
|
||||
@ -263,7 +262,7 @@
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
.maintenance-body {
|
||||
padding: 18px 20px 20px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.backup-list {
|
||||
display: grid;
|
||||
@ -320,16 +319,13 @@
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.audit-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 12px 14px;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--pm-radius-sm);
|
||||
background: var(--pm-bg-primary);
|
||||
}
|
||||
@ -404,12 +400,12 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.migration-source > div {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 12px 14px;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--pm-radius-sm);
|
||||
background: var(--pm-bg-primary);
|
||||
}
|
||||
@ -423,12 +419,12 @@
|
||||
}
|
||||
.migration-form {
|
||||
max-width: 880px;
|
||||
margin-top: 16px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.migration-connection-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
|
||||
gap: 0 20px;
|
||||
gap: 6px 16px;
|
||||
}
|
||||
.migration-connection-grid :deep(.el-input-number) {
|
||||
width: 100%;
|
||||
@ -441,6 +437,40 @@
|
||||
.migration-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.migration-form :deep(.el-form-item__label) {
|
||||
margin-bottom: 4px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.migration-form > * + * {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.maintenance-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
@container config-main (min-width: 700px) {
|
||||
.editor-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
}
|
||||
.editor-footer .confirmation-check {
|
||||
flex: 1 1 280px;
|
||||
}
|
||||
.editor-footer .action-row {
|
||||
margin-top: 0;
|
||||
flex: 0 1 auto;
|
||||
gap: 8px;
|
||||
}
|
||||
.editor-footer > .el-alert,
|
||||
.editor-footer .action-hint {
|
||||
flex-basis: 100%;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.migration-result-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
.mail-port-summary {
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.summary-note {
|
||||
margin: 0 0 14px;
|
||||
margin: 0 0 8px;
|
||||
color: var(--pm-text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
@ -16,14 +16,14 @@
|
||||
}
|
||||
|
||||
.protocol-card {
|
||||
padding: 16px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--pm-border-color);
|
||||
border-radius: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.protocol-card dl {
|
||||
margin: 12px 0 0;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
|
||||
.port-row {
|
||||
@ -31,7 +31,7 @@
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 6px 12px;
|
||||
margin-top: 8px;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@container config-main (max-width: 680px) {
|
||||
@container config-main (max-width: 480px) {
|
||||
.summary-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@
|
||||
/* 抽屉由 Teleport 挂载,使用专属类限定,不能依赖祖先的作用域属性。 */
|
||||
:global(.settings-drawer > .el-drawer__header) {
|
||||
margin-bottom: 0;
|
||||
padding: 24px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--pm-border-color);
|
||||
}
|
||||
|
||||
@ -108,14 +108,14 @@
|
||||
:global(.settings-drawer > .el-drawer__body) {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
padding: 24px;
|
||||
padding: 16px 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-tabs > :deep(.el-tabs__header) {
|
||||
flex: 0 0 184px;
|
||||
margin: 0 0 0 20px !important;
|
||||
padding: 12px 8px;
|
||||
flex: 0 0 168px;
|
||||
margin: 0 0 0 16px !important;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--pm-border-color);
|
||||
border-radius: var(--pm-radius-lg);
|
||||
background: var(--pm-bg-primary);
|
||||
@ -136,10 +136,10 @@
|
||||
|
||||
.settings-tabs > :deep(.el-tabs__header .el-tabs__item) {
|
||||
height: auto;
|
||||
min-height: 64px;
|
||||
min-height: 56px;
|
||||
justify-content: flex-start;
|
||||
margin: 3px 0;
|
||||
padding: 12px !important;
|
||||
padding: 10px !important;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 12px;
|
||||
color: var(--pm-text-secondary);
|
||||
@ -200,7 +200,6 @@
|
||||
min-width: 0;
|
||||
padding-right: 4px;
|
||||
overflow: auto;
|
||||
scrollbar-gutter: stable;
|
||||
container: settings-content / inline-size;
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
<!-- 设置抽屉保留右侧导航,一级分类不影响内部标签页。 -->
|
||||
<el-drawer
|
||||
v-model="globalStatus.settingsDrawerVisible"
|
||||
:size="isMobile ? '100%' : 'min(1120px, calc(100vw - 32px))'"
|
||||
:size="isMobile ? '100%' : 'min(1600px, calc(100vw - 24px))'"
|
||||
:title="lang.settings"
|
||||
class="settings-drawer"
|
||||
:with-header="true"
|
||||
|
||||
@ -3,15 +3,18 @@
|
||||
position: sticky;
|
||||
top: 0;
|
||||
min-width: 0;
|
||||
padding: 4px 0 4px 14px;
|
||||
padding: 2px 0 2px 12px;
|
||||
border-left: 1px solid var(--pm-border-color);
|
||||
--submenu-accent: var(--pm-primary-color);
|
||||
}
|
||||
|
||||
.settings-submenu-heading {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
padding: 4px 10px 16px;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
padding: 2px 8px 10px;
|
||||
}
|
||||
|
||||
.settings-submenu-caption {
|
||||
@ -28,14 +31,14 @@
|
||||
|
||||
.settings-submenu-list {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.settings-submenu-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
padding: 10px 12px;
|
||||
min-height: 34px;
|
||||
padding: 7px 10px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
@ -62,9 +65,9 @@
|
||||
.settings-submenu-item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
bottom: 12px;
|
||||
left: -15px;
|
||||
top: 10px;
|
||||
bottom: 10px;
|
||||
left: -13px;
|
||||
width: 3px;
|
||||
border-radius: 3px;
|
||||
background: var(--submenu-accent);
|
||||
@ -75,29 +78,49 @@
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.settings-submenu-count {
|
||||
.settings-submenu-chevron {
|
||||
float: right;
|
||||
min-width: 18px;
|
||||
border-radius: 5px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin: 5px 3px 0 8px;
|
||||
border-right: 1.5px solid currentColor;
|
||||
border-bottom: 1.5px solid currentColor;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.has-children {
|
||||
margin-top: 6px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--pm-border-color);
|
||||
border-radius: 12px;
|
||||
background: var(--pm-bg-primary);
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.has-children > .settings-submenu-item {
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.has-children > .settings-submenu-item::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.settings-tertiary-list {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin: 6px 0 8px 12px;
|
||||
padding-left: 8px;
|
||||
border-left: 1px solid var(--pm-border-color);
|
||||
gap: 3px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.settings-tertiary-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 36px;
|
||||
padding: 8px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
min-height: 37px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--pm-text-secondary);
|
||||
font: inherit;
|
||||
@ -110,9 +133,38 @@
|
||||
.settings-tertiary-item.active {
|
||||
color: var(--submenu-accent);
|
||||
background: color-mix(in srgb, var(--submenu-accent) 8%, var(--pm-bg-secondary));
|
||||
border-color: color-mix(in srgb, var(--submenu-accent) 22%, var(--pm-border-color));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-tertiary-icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
color: var(--submenu-accent);
|
||||
background: color-mix(in srgb, var(--submenu-accent) 8%, transparent);
|
||||
}
|
||||
|
||||
.settings-tertiary-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.6;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.settings-tertiary-current {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin-left: auto;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.settings-tertiary-item:hover:not(.active) {
|
||||
color: var(--pm-text-primary);
|
||||
background: var(--pm-bg-hover);
|
||||
@ -147,7 +199,7 @@
|
||||
|
||||
.settings-submenu-list {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
flex-wrap: wrap;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
@ -161,8 +213,12 @@
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.has-children {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.settings-tertiary-list {
|
||||
margin-left: 4px;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.settings-submenu-item.active::before {
|
||||
|
||||
@ -5,20 +5,20 @@
|
||||
<strong>{{ title }}</strong>
|
||||
</div>
|
||||
<div class="settings-submenu-list">
|
||||
<div v-for="item in items" :key="item.value" class="settings-submenu-entry">
|
||||
<div v-for="item in items" :key="item.value" class="settings-submenu-entry" :class="{ 'has-children': item.children?.length }">
|
||||
<button
|
||||
type="button"
|
||||
class="settings-submenu-item"
|
||||
:class="{ active: modelValue === item.value || item.children?.some(child => child.value === modelValue) }"
|
||||
:aria-current="modelValue === item.value ? 'page' : undefined"
|
||||
:aria-expanded="item.children ? item.children.some(child => child.value === modelValue) : undefined"
|
||||
:aria-expanded="item.children?.length ? true : undefined"
|
||||
@click="select(item.value)"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<span v-if="item.children" class="settings-submenu-count">{{ item.children.length }}</span>
|
||||
<span v-if="item.children?.length" class="settings-submenu-chevron" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div
|
||||
v-if="item.children?.some(child => child.value === modelValue)"
|
||||
v-if="item.children?.length"
|
||||
class="settings-tertiary-list"
|
||||
role="group"
|
||||
:aria-label="`${item.label}三级菜单`"
|
||||
@ -32,7 +32,22 @@
|
||||
:aria-current="modelValue === child.value ? 'page' : undefined"
|
||||
@click="select(child.value)"
|
||||
>
|
||||
{{ child.label }}
|
||||
<span class="settings-tertiary-icon" :data-plugin="child.value" aria-hidden="true">
|
||||
<svg v-if="child.value === 'spam_block'" viewBox="0 0 24 24" fill="none">
|
||||
<path d="m12 3 8 3v6c0 5-8 9-8 9s-8-4-8-9V6l8-3Z" />
|
||||
<path d="m8 12 3 3 5-6" />
|
||||
</svg>
|
||||
<svg v-else-if="child.value === 'telegram_push'" viewBox="0 0 24 24" fill="none">
|
||||
<path d="m3 10 18-7-5 18-5-7-8-4Z" />
|
||||
<path d="m11 14 5-6" />
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20 11a8 8 0 0 1-8 8H5l-3 2 1-6a8 8 0 1 1 17-4Z" />
|
||||
<path d="M7 10h.01M12 10h.01M17 10h.01" stroke-width="3" />
|
||||
</svg>
|
||||
</span>
|
||||
<span>{{ child.label }}</span>
|
||||
<span v-if="modelValue === child.value" class="settings-tertiary-current" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user