优化插件配置页面布局

This commit is contained in:
smail 2026-09-06 19:46:06 +08:00
parent e78f382ee4
commit 756c8d5abe
4 changed files with 185 additions and 73 deletions

View File

@ -1,19 +1,6 @@
<template>
<div class="config-form" :data-form-section="section">
<p v-if="!groups.length" class="form-empty">当前插件配置不存在,请刷新配置或检查初始化状态。</p>
<nav v-if="section === 'base' && groups.length > 1" class="form-sections" aria-label="基础配置分区">
<button
v-for="group in groups"
:key="group.id"
type="button"
:class="{ active: activeGroup === group.id }"
:aria-pressed="activeGroup === group.id"
@click="activeGroup = group.id"
>
{{ group.title }}
<span v-if="group.fields.some(field => errors[fieldKey(field)])" class="section-error">待修正</span>
</button>
</nav>
<section
v-for="group in groups"
:key="group.id"

View File

@ -5,14 +5,14 @@
}
.settings-header {
margin-bottom: 24px;
margin-bottom: 18px;
}
.settings-header h3 {
font-size: 18px;
font-weight: 600;
color: var(--pm-text-primary);
margin: 0 0 8px 0;
margin: 0 0 6px;
}
.settings-desc {
@ -22,61 +22,112 @@
}
.plugin-container {
flex-grow: 1;
border: 1px solid var(--pm-border-color);
border-radius: var(--pm-radius-sm);
background: var(--pm-bg-secondary);
flex: 1;
min-width: 0;
}
.plugin-list {
display: grid;
gap: 12px;
}
.plugin-section {
min-width: 0;
overflow: hidden;
border: 1px solid var(--pm-border-color);
border-radius: var(--pm-radius-md);
background: var(--pm-bg-secondary);
box-shadow: 0 2px 8px rgb(15 23 42 / 4%);
}
.custom-tabs > :deep(.el-tabs__header) {
margin-bottom: 0;
background: var(--pm-bg-primary);
padding: 8px 12px;
.plugin-section:hover:not(.is-disabled) {
border-color: color-mix(in srgb, var(--pm-primary-color) 28%, var(--pm-border-color));
}
.plugin-section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 58px;
padding: 10px 16px;
border-bottom: 1px solid var(--pm-border-color);
background: var(--pm-bg-primary);
}
.custom-tabs > :deep(.el-tabs__header .el-tabs__nav-wrap::after),
.custom-tabs > :deep(.el-tabs__header .el-tabs__active-bar) {
display: none;
.plugin-section-title {
display: flex;
align-items: center;
min-width: 0;
gap: 12px;
}
.custom-tabs > :deep(.el-tabs__header .el-tabs__item) {
height: 38px;
margin-right: 6px;
padding: 0 14px !important;
border-radius: 8px;
color: var(--pm-text-secondary);
font-size: 13px;
.plugin-section-index {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
flex: 0 0 30px;
border-radius: 9px;
color: var(--pm-primary-color);
background: color-mix(in srgb, var(--pm-primary-color) 12%, var(--pm-bg-secondary));
font-size: 12px;
font-weight: 700;
letter-spacing: .04em;
}
.custom-tabs > :deep(.el-tabs__header .el-tabs__item:hover:not(.is-active)) {
.plugin-section-header h4 {
overflow: hidden;
margin: 0 0 2px;
color: var(--pm-text-primary);
background: var(--pm-bg-hover);
font-size: 15px;
font-weight: 650;
text-overflow: ellipsis;
white-space: nowrap;
}
.custom-tabs > :deep(.el-tabs__header .el-tabs__item.is-active) {
color: var(--settings-accent, var(--pm-primary-color));
background: color-mix(in srgb, var(--pm-primary-color) 10%, var(--pm-bg-secondary));
.plugin-section-header p {
margin: 0;
color: var(--pm-text-secondary);
font-size: 12px;
}
.plugin-section-status {
flex: 0 0 auto;
padding: 4px 9px;
border: 1px solid color-mix(in srgb, #16a34a 22%, var(--pm-border-color));
border-radius: 999px;
color: #15803d;
background: #f0fdf4;
font-size: 12px;
line-height: 1;
}
.plugin-section-status.is-active {
font-weight: 600;
box-shadow: inset 0 -2px var(--settings-accent, var(--pm-primary-color));
}
.custom-tabs > :deep(.el-tabs__header .el-tabs__item:focus-visible) {
outline: 2px solid var(--settings-accent, var(--pm-primary-color));
outline-offset: -2px;
.plugin-section :deep(button:focus-visible),
.plugin-section :deep(input:focus-visible),
.plugin-section :deep(textarea:focus-visible),
.plugin-section :deep(select:focus-visible) {
outline: 2px solid var(--pm-primary-color);
outline-offset: 2px;
}
.custom-tabs > :deep(.el-tabs__content) {
height: 100%;
.plugin-section-content {
min-width: 0;
}
.iframe-wrapper {
height: calc(100vh - 250px);
min-height: 400px;
height: 320px;
min-height: 240px;
background: var(--pm-bg-secondary);
}
iframe {
display: block;
width: 100%;
height: 100%;
border: 0;
@ -84,7 +135,32 @@ iframe {
.empty-state {
display: flex;
min-height: 260px;
justify-content: center;
align-items: center;
height: 300px;
border: 1px dashed var(--pm-border-color);
border-radius: var(--pm-radius-md);
background: var(--pm-bg-secondary);
}
@media (max-width: 768px) {
.settings-header {
margin-bottom: 14px;
}
.plugin-list {
gap: 10px;
}
.plugin-section-header {
padding: 10px 12px;
}
.plugin-section-status {
display: none;
}
.iframe-wrapper {
height: 280px;
}
}

View File

@ -17,11 +17,45 @@ export function usePage() {
return aliases[name] || name
}
const pluginEntries = computed(() => Object.entries(pluginList).map(([name, src]) => ({
name,
src,
logicalID: pluginLogicalID(name)
})))
const pluginEntries = computed(() => {
const order = {
spam_block: 1,
telegram_push: 2,
wechat_push: 3
}
const labels = {
spam_block: {
label: '垃圾邮件拦截',
description: '模型识别与过滤策略'
},
telegram_push: {
label: 'Telegram 推送',
description: '邮件通知与消息推送'
},
wechat_push: {
label: '微信推送',
description: '插件页面与推送参数'
}
}
return Object.entries(pluginList)
.map(([name, src]) => {
const logicalID = pluginLogicalID(name)
const metadata = labels[logicalID] || {
label: name,
description: '插件配置页面'
}
return {
name,
src,
logicalID,
label: metadata.label,
description: metadata.description
}
})
.sort((left, right) => (order[left.logicalID] || 99) - (order[right.logicalID] || 99))
})
http.get('/api/v1/plugin/list').then(response => {
if (Array.isArray(response.data) && response.data.length > 0) {
@ -43,4 +77,4 @@ export function usePage() {
pluginEntries,
pluginFrames
}
}
}

View File

@ -6,27 +6,42 @@
</div>
<div class="plugin-container">
<el-tabs class="custom-tabs">
<el-tab-pane v-for="entry in pluginEntries" :key="entry.src" :label="entry.name">
<SpamBlockSettings v-if="entry.logicalID === 'spam_block'" />
<TelegramSettings v-else-if="entry.logicalID === 'telegram_push'" />
<div v-else class="iframe-wrapper">
<iframe
:src="entry.src"
sandbox="allow-scripts"
referrerpolicy="no-referrer"
ref="pluginFrames"
:data-plugin="entry.name"
:title="entry.name"
/>
<div v-if="pluginEntries.length" class="plugin-list">
<section
v-for="(entry, index) in pluginEntries"
:key="entry.src"
class="plugin-section"
>
<header class="plugin-section-header">
<div class="plugin-section-title">
<span class="plugin-section-index">{{ String(index + 1).padStart(2, '0') }}</span>
<div>
<h4>{{ entry.label }}</h4>
<p>{{ entry.description }}</p>
</div>
</div>
<span class="plugin-section-status is-active">已启用</span>
</header>
<div class="plugin-section-content">
<SpamBlockSettings v-if="entry.logicalID === 'spam_block'" />
<TelegramSettings v-else-if="entry.logicalID === 'telegram_push'" />
<div v-else class="iframe-wrapper">
<iframe
:src="entry.src"
sandbox="allow-scripts"
referrerpolicy="no-referrer"
ref="pluginFrames"
:data-plugin="entry.name"
:title="entry.name"
/>
</div>
</div>
</el-tab-pane>
<el-tab-pane v-if="pluginEntries.length === 0" :label="lang.no_plugins">
<div class="empty-state">
<el-empty :description="lang.no_plugins_installed" :image-size="120" />
</div>
</el-tab-pane>
</el-tabs>
</section>
</div>
<div v-else class="empty-state">
<el-empty :description="lang.no_plugins_installed" :image-size="120" />
</div>
</div>
</div>
</template>