feat: 优化已配置客户端列表与发信中继配置
This commit is contained in:
parent
3015081e19
commit
bbf26f91c5
@ -3,138 +3,142 @@
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.relay-list-heading,
|
||||
.relay-item-heading,
|
||||
.relay-editor-heading,
|
||||
.relay-item-actions,
|
||||
.relay-editor-actions {
|
||||
/* 顶部卡片 */
|
||||
.relay-header-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.relay-item-actions,
|
||||
.relay-editor-actions {
|
||||
justify-content: flex-end;
|
||||
flex-wrap: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.relay-item-actions :deep(.el-button),
|
||||
.relay-editor-actions :deep(.el-button) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.relay-list-heading h5,
|
||||
.relay-editor-heading h5 {
|
||||
margin: 0;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.relay-list-heading p,
|
||||
.relay-editor-heading span,
|
||||
.relay-subtitle {
|
||||
margin: 5px 0 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.relay-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.relay-item {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
gap: 20px;
|
||||
padding: 16px 20px;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.relay-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px 20px;
|
||||
margin: 18px 0;
|
||||
}
|
||||
|
||||
.relay-summary div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.relay-summary dt {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.relay-summary dd {
|
||||
margin: 4px 0 0;
|
||||
overflow: hidden;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.relay-editor {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.relay-fields {
|
||||
display: grid;
|
||||
align-items: start;
|
||||
margin-top: 16px;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px 16px;
|
||||
.relay-title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.relay-fields :deep(.config-field) {
|
||||
min-width: 0;
|
||||
.relay-main-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.relay-main-title h5 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.relay-status-tag {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.relay-desc {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.relay-switch-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
background: var(--el-fill-color-light);
|
||||
padding: 8px 14px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.relay-alert {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@container config-main (max-width: 760px) {
|
||||
.relay-fields {
|
||||
/* 编辑表单平铺卡片 */
|
||||
.relay-editor-box {
|
||||
padding: 18px 20px;
|
||||
background: var(--el-fill-color-blank);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.editor-title-wrap h6 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.editor-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.editor-btns {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 紧凑响应式栅格:一行两列/三列 */
|
||||
.relay-grid-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px 18px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.relay-grid-form :deep(.config-field) {
|
||||
min-width: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.relay-grid-form {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@container config-main (max-width: 500px) {
|
||||
.relay-fields {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.relay-summary {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.relay-list-heading,
|
||||
.relay-item-heading,
|
||||
.relay-editor-heading {
|
||||
align-items: flex-start;
|
||||
@media (max-width: 650px) {
|
||||
.relay-header-card {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.relay-summary {
|
||||
.relay-grid-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.editor-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
@ -1,61 +1,67 @@
|
||||
<template>
|
||||
<div class="relay-config">
|
||||
<div class="relay-list-heading">
|
||||
<div>
|
||||
<h5>已配置的 SMTP 中继</h5>
|
||||
<p>中继配置独立于本机邮件服务;关闭开关只停止使用,不会删除已填写的信息。</p>
|
||||
<!-- 顶部标题与控制开关 -->
|
||||
<div class="relay-header-card">
|
||||
<div class="relay-title-group">
|
||||
<div class="relay-main-title">
|
||||
<h5>SMTP 发信中继</h5>
|
||||
<el-tag :type="relay.enabled ? 'success' : 'info'" effect="light" class="relay-status-tag">
|
||||
{{ relay.enabled ? '中继服务:已启用' : '中继服务:已停用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<p class="relay-desc">
|
||||
配置独立于本机邮件服务;关闭中继开关只停止使用,绝不删除已填写的中继信息。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="relay-switch-control">
|
||||
<span class="control-label">中继控制开关</span>
|
||||
<el-switch
|
||||
:model-value="!!relay.enabled"
|
||||
:disabled="disabled"
|
||||
active-text="启用"
|
||||
inactive-text="关闭"
|
||||
inline-prompt
|
||||
@change="val => emit('change', ['relay', 'enabled'], val)"
|
||||
/>
|
||||
</div>
|
||||
<el-button v-if="hasConfigured && !editing" type="primary" plain :disabled="disabled" @click="editing = true">
|
||||
编辑中继
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 清除标记提醒 -->
|
||||
<el-alert
|
||||
v-if="relay.clear"
|
||||
title="已标记清除,保存后将删除服务器、端口、账号、密码、发件邮箱和发件域名。"
|
||||
title="中继配置已标记清除,保存生效后将删除服务器、端口、账号、密码及发信配置。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="relay-alert"
|
||||
/>
|
||||
|
||||
<div v-if="hasConfigured && !editing" class="relay-list" role="list" aria-label="SMTP 中继列表">
|
||||
<article class="relay-item" role="listitem">
|
||||
<div class="relay-item-heading">
|
||||
<div>
|
||||
<strong>默认 SMTP 中继</strong>
|
||||
<span class="relay-subtitle">单个默认发信中继配置</span>
|
||||
</div>
|
||||
<el-tag :type="relay.enabled ? 'success' : 'info'" effect="plain">
|
||||
{{ relay.enabled ? '已启用' : '已关闭' }}
|
||||
</el-tag>
|
||||
<!-- 中继编辑表单区域:平铺展示,不再折叠收起 -->
|
||||
<div class="relay-editor-box">
|
||||
<div class="editor-header">
|
||||
<div class="editor-title-wrap">
|
||||
<h6>编辑 SMTP 中继配置</h6>
|
||||
<span class="editor-tip">表单字段已按响应式一行两列/三列进行紧凑排列;密码留空保持原值。</span>
|
||||
</div>
|
||||
<dl class="relay-summary">
|
||||
<div><dt>SMTP 服务器</dt><dd>{{ relay.relayHost || '未填写' }}</dd></div>
|
||||
<div><dt>端口</dt><dd>{{ relay.relayPort || '未填写' }}</dd></div>
|
||||
<div><dt>中继加密方式</dt><dd>{{ relay.security === 'starttls' ? 'STARTTLS' : relay.security === 'implicit_tls' ? '隐式 TLS' : '未选择' }}</dd></div>
|
||||
<div><dt>认证账号</dt><dd>{{ relay.username || '未填写' }}</dd></div>
|
||||
<div><dt>发件邮箱</dt><dd>{{ relay.from || '未填写' }}</dd></div>
|
||||
<div><dt>密码</dt><dd>{{ passwordConfigured ? '已配置(不显示明文)' : '未配置' }}</dd></div>
|
||||
<div><dt>发件域名</dt><dd>{{ relay.fromDomain || '未填写' }}</dd></div>
|
||||
</dl>
|
||||
<div class="relay-item-actions">
|
||||
<el-button type="primary" plain :disabled="disabled" @click="editing = true">编辑</el-button>
|
||||
<el-button type="danger" plain :disabled="disabled" @click="clearRelay">清除中继</el-button>
|
||||
<div class="editor-btns">
|
||||
<el-button
|
||||
v-if="hasConfigured"
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
:disabled="disabled"
|
||||
@click="clearRelay"
|
||||
>
|
||||
清除中继
|
||||
</el-button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<el-empty v-else-if="!editing" description="暂无已配置的 SMTP 中继" />
|
||||
|
||||
<div v-if="editing" class="relay-editor">
|
||||
<div class="relay-editor-heading">
|
||||
<h5>{{ hasConfigured ? '编辑 SMTP 中继' : '添加 SMTP 中继' }}</h5>
|
||||
<span>密码留空表示保持原值,清除操作需要单独点击“清除中继”。</span>
|
||||
</div>
|
||||
<div class="form-grid relay-fields">
|
||||
|
||||
<!-- 紧凑表单排版:一行两列/三列 -->
|
||||
<div class="relay-grid-form">
|
||||
<ConfigField
|
||||
v-for="field in fields"
|
||||
v-for="field in formFields"
|
||||
:key="fieldKey(field)"
|
||||
:field="field"
|
||||
:value="getValue(config, field.path)"
|
||||
@ -64,16 +70,12 @@
|
||||
@change="(path, value) => emit('change', path, value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="relay-editor-actions">
|
||||
<el-button v-if="hasConfigured" :disabled="disabled" @click="editing = false">取消编辑</el-button>
|
||||
<el-button v-if="hasConfigured" type="danger" plain :disabled="disabled" @click="clearRelay">清除中继</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import ConfigField from '../ConfigField/ConfigField.vue'
|
||||
import { fieldKey, getValue, relayConfigured } from '../ConfigForm/ConfigForm.js'
|
||||
@ -85,10 +87,14 @@ const props = defineProps({
|
||||
disabled: Boolean
|
||||
})
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const relay = computed(() => props.config.relay || {})
|
||||
const hasConfigured = computed(() => relayConfigured(props.config))
|
||||
const passwordConfigured = computed(() => !!props.config.relayPasswordStatus?.configured)
|
||||
const editing = ref(!hasConfigured.value)
|
||||
|
||||
// 过滤掉已在顶部独立展示的中继开关,其余表单字段在下方平铺展示
|
||||
const formFields = computed(() => {
|
||||
return (props.fields || []).filter(f => fieldKey(f) !== 'relay.enabled')
|
||||
})
|
||||
|
||||
const clearRelay = async () => {
|
||||
try {
|
||||
@ -98,11 +104,10 @@ const clearRelay = async () => {
|
||||
{ type: 'warning', confirmButtonText: '确认清除', cancelButtonText: '取消' }
|
||||
)
|
||||
emit('change', ['relay', 'clear'], true)
|
||||
editing.value = false
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') throw error
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="./RelayConfig.css"></style>
|
||||
<style scoped src="./RelayConfig.css"></style>
|
||||
@ -5,311 +5,233 @@
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.authorization-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.settings-card {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 通用板块卡片容器 */
|
||||
.panel-card {
|
||||
background: var(--pm-bg-secondary);
|
||||
border: 1px solid var(--pm-border-color);
|
||||
border-radius: var(--pm-radius-md);
|
||||
padding: 16px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
/* 顶部标题栏 */
|
||||
.card-header-bar {
|
||||
.settings-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
.settings-header h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--pm-text-primary);
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--pm-text-primary);
|
||||
.group-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.group-toolbar :deep(.el-button) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
min-height: 32px;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 13px;
|
||||
color: var(--pm-text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 核心主要操作按钮:高对比度实心显色,杜绝半透明或虚化 */
|
||||
.btn-primary-action,
|
||||
.btn-primary-action.el-button--primary {
|
||||
background-color: var(--pm-primary-color) !important;
|
||||
border-color: var(--pm-primary-color) !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600 !important;
|
||||
opacity: 1 !important;
|
||||
box-shadow: 0 2px 6px rgba(0, 113, 227, 0.25);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary-action.el-button--primary:hover {
|
||||
background-color: var(--pm-primary-hover, #0077ed) !important;
|
||||
border-color: var(--pm-primary-hover, #0077ed) !important;
|
||||
color: #ffffff !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 113, 227, 0.35);
|
||||
}
|
||||
|
||||
.btn-primary-action.el-button--primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 卡片内容区域 */
|
||||
.card-content-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.client-op-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.op-left-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--pm-text-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.client-select-input {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.client-option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.op-right-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 授权码展示横条 */
|
||||
.code-banner {
|
||||
padding: 14px 16px;
|
||||
/* 面板分组容器 */
|
||||
.account-group-panel {
|
||||
margin-top: 16px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: var(--pm-bg-primary);
|
||||
border: 1px dashed var(--pm-border-color);
|
||||
border-radius: var(--pm-radius-sm);
|
||||
}
|
||||
|
||||
.account-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.banner-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.banner-label {
|
||||
.account-group-header h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--pm-text-primary);
|
||||
}
|
||||
|
||||
.client-count-tag {
|
||||
font-size: 12px;
|
||||
color: var(--pm-text-secondary);
|
||||
}
|
||||
|
||||
.proto-desc-hint {
|
||||
font-size: 12px;
|
||||
color: var(--pm-text-secondary);
|
||||
}
|
||||
|
||||
/* 表格容器与样式 */
|
||||
.table-container {
|
||||
border-top: 1px solid var(--pm-border-color);
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.modern-table {
|
||||
--el-table-header-bg-color: var(--pm-bg-secondary);
|
||||
--el-table-row-hover-bg-color: color-mix(in srgb, var(--el-color-primary) 6%, transparent);
|
||||
}
|
||||
|
||||
.modern-table :deep(th.el-table__cell) {
|
||||
background-color: var(--pm-bg-secondary);
|
||||
color: var(--pm-text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modern-table :deep(.el-table__cell) {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.modern-table :deep(.cell) {
|
||||
padding: 0 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.banner-code-box {
|
||||
.modern-table :deep(th.el-table__cell .cell) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.client-name-text {
|
||||
font-weight: 600;
|
||||
color: var(--pm-text-primary);
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
border-radius: var(--pm-radius-sm);
|
||||
min-width: 48px;
|
||||
justify-content: center;
|
||||
padding: 0 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.time-col {
|
||||
font-size: 12px;
|
||||
color: var(--pm-text-secondary);
|
||||
}
|
||||
|
||||
.table-op-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.masked-code {
|
||||
letter-spacing: 2px;
|
||||
font-family: monospace;
|
||||
font-size: 15px;
|
||||
color: var(--pm-text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.revealed-code {
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
.mini-revealed-code {
|
||||
padding: 2px 6px;
|
||||
background: color-mix(in srgb, var(--pm-primary-color) 12%, var(--pm-bg-secondary));
|
||||
color: var(--pm-primary-color);
|
||||
font-family: monospace;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.btn-copy-code {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.countdown-tag {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-verify-reveal {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-hint {
|
||||
font-size: 13px;
|
||||
color: var(--pm-text-placeholder, #909399);
|
||||
}
|
||||
|
||||
.banner-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--pm-text-secondary);
|
||||
margin-left: auto;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.meta-tag {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 空状态占位 */
|
||||
.empty-client-placeholder {
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
background: var(--pm-bg-primary);
|
||||
border: 1px dashed var(--pm-border-color);
|
||||
.action-btn {
|
||||
border-radius: var(--pm-radius-sm);
|
||||
padding: 4px 8px;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* 授权码倒计时提醒 */
|
||||
.reveal-alert-bar {
|
||||
padding: 8px 14px;
|
||||
background: var(--pm-bg-secondary);
|
||||
border-top: 1px dashed var(--pm-border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 14px;
|
||||
color: var(--pm-text-placeholder, #909399);
|
||||
.reveal-countdown-badge {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 通用协议指引网格 */
|
||||
.proto-grid {
|
||||
/* 分页控件 */
|
||||
.pagination-wrapper {
|
||||
margin: 12px 0 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 通用配置协议卡片 */
|
||||
.proto-guide-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.proto-grid-body {
|
||||
padding: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 12px;
|
||||
background: var(--pm-bg-primary);
|
||||
}
|
||||
|
||||
.proto-card {
|
||||
min-width: 0;
|
||||
padding: 12px 14px;
|
||||
background: var(--pm-bg-primary);
|
||||
padding: 12px;
|
||||
background: var(--pm-bg-secondary);
|
||||
border: 1px solid var(--pm-border-color);
|
||||
border-radius: var(--pm-radius-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.proto-card:hover {
|
||||
border-color: var(--pm-primary-color);
|
||||
}
|
||||
|
||||
.proto-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
gap: 6px 8px;
|
||||
padding-bottom: 9px;
|
||||
border-bottom: 1px solid var(--pm-border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.proto-name-badge {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.proto-name-badge > .el-tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding-inline: 4px;
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.proto-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
line-height: 1.35;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.proto-name {
|
||||
min-width: 0;
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--pm-text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.proto-service {
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--pm-text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
color: var(--pm-text-secondary);
|
||||
}
|
||||
|
||||
.btn-card-copy {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
justify-self: end;
|
||||
flex-shrink: 0;
|
||||
padding-inline: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@ -317,31 +239,24 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding-top: 1px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.field-item {
|
||||
display: grid;
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
align-items: baseline;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
gap: 6px;
|
||||
line-height: 1.35;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.field-k {
|
||||
color: var(--pm-text-secondary);
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.field-v {
|
||||
min-width: 0;
|
||||
color: var(--pm-text-primary);
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
@ -353,7 +268,7 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 弹窗通用紧凑样式 */
|
||||
/* 对话框与通用样式 */
|
||||
.compact-dialog .dialog-tip {
|
||||
margin: 0 0 14px;
|
||||
color: var(--pm-text-secondary);
|
||||
@ -367,21 +282,21 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.card-header-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.client-op-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.code-banner {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.banner-right {
|
||||
margin-left: 0;
|
||||
.btn-primary-action,
|
||||
.btn-primary-action.el-button--primary {
|
||||
background-color: var(--pm-primary-color) !important;
|
||||
border-color: var(--pm-primary-color) !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
@container settings-content (max-width: 760px) {
|
||||
.settings-header {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
}
|
||||
.group-toolbar {
|
||||
flex: 1 1 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
|
||||
import { ElMessageBox, ElNotification } from 'element-plus'
|
||||
|
||||
import { Plus, Refresh } from '@element-plus/icons-vue'
|
||||
import { http } from '@/utils/axios'
|
||||
|
||||
export function usePage() {
|
||||
@ -10,6 +9,16 @@ export function usePage() {
|
||||
const selectedClientId = ref(null)
|
||||
const currentDomain = ref('')
|
||||
|
||||
// 分页控制
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
const pagedClients = computed(() => {
|
||||
const list = clientList.value || []
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return list.slice(start, start + pageSize.value)
|
||||
})
|
||||
|
||||
// 解密后的各客户端授权码明文 { [clientId]: 'xxxx-yyyy-...' }
|
||||
const revealedCodes = reactive({})
|
||||
const codesRevealed = ref(false)
|
||||
@ -31,17 +40,12 @@ export function usePage() {
|
||||
const verifyActionLabel = computed(() => ({
|
||||
reveal: '查看明文',
|
||||
regenerate: '重新生成',
|
||||
revoke: '撤销授权码',
|
||||
revoke: '撤销删除',
|
||||
}[verifyAction.value]))
|
||||
const verifyDescription = computed(() => verificationClient.value
|
||||
? `请验证本人登录密码,以${verifyActionLabel.value}:${verificationClient.value.client_name}`
|
||||
? `请验证本人登录密码,以${verifyActionLabel.value}客户端:${verificationClient.value.client_name}`
|
||||
: '查看客户端授权码前,请验证本人登录密码。')
|
||||
|
||||
const currentClient = computed(() => {
|
||||
if (!selectedClientId.value || !Array.isArray(clientList.value)) return null
|
||||
return clientList.value.find(c => c.id === selectedClientId.value) || null
|
||||
})
|
||||
|
||||
const notify = (message, type = 'error') => {
|
||||
ElNotification({
|
||||
title: type === 'success' ? '操作成功' : (type === 'info' ? '提示' : '操作失败'),
|
||||
@ -135,8 +139,8 @@ export function usePage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleClientSelect = (val) => {
|
||||
selectedClientId.value = val
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
const openCreateDialog = () => {
|
||||
@ -266,7 +270,7 @@ export function usePage() {
|
||||
verifyDialogVisible.value = true
|
||||
}
|
||||
|
||||
const openVerifyDialog = () => openActionVerification('reveal')
|
||||
const openVerifyDialog = (client) => openActionVerification('reveal', client)
|
||||
|
||||
const closeVerifyDialog = () => {
|
||||
verifyDialogVisible.value = false
|
||||
@ -344,8 +348,11 @@ export function usePage() {
|
||||
return {
|
||||
loading,
|
||||
clientList,
|
||||
pagedClients,
|
||||
currentPage,
|
||||
pageSize,
|
||||
handlePageChange,
|
||||
selectedClientId,
|
||||
currentClient,
|
||||
currentDomain,
|
||||
revealedCodes,
|
||||
codesRevealed,
|
||||
@ -359,7 +366,7 @@ export function usePage() {
|
||||
verifyLoading,
|
||||
verifyActionLabel,
|
||||
verifyDescription,
|
||||
handleClientSelect,
|
||||
loadClientCredentials,
|
||||
openCreateDialog,
|
||||
closeCreateDialog,
|
||||
submitCreateClient,
|
||||
@ -372,4 +379,4 @@ export function usePage() {
|
||||
closeVerifyDialog,
|
||||
submitVerifyAndReveal,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,262 +1,249 @@
|
||||
<template>
|
||||
<div class="third-party-authorization" v-loading="loading">
|
||||
<section class="authorization-section">
|
||||
<!-- 客户端与专属授权码管理卡片 -->
|
||||
<div class="panel-card client-control-card">
|
||||
<div class="card-header-bar">
|
||||
<div class="header-left">
|
||||
<span class="main-title">客户端专属授权码</span>
|
||||
<span class="sub-title">为手机、Foxmail、Outlook 等设备创建独立授权码,随时查看或撤销</span>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="default"
|
||||
class="btn-primary-action"
|
||||
@click="openCreateDialog"
|
||||
>
|
||||
+ 新增客户端
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="settings-card">
|
||||
<div class="settings-header">
|
||||
<h3>已配置客户端</h3>
|
||||
<div class="group-toolbar">
|
||||
<el-button type="primary" :disabled="loading || createLoading" @click="openCreateDialog">
|
||||
<el-icon><Plus /></el-icon> 新增客户端
|
||||
</el-button>
|
||||
<el-button :loading="loading" @click="loadClientCredentials">
|
||||
<el-icon><Refresh /></el-icon> 刷新列表
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content-area">
|
||||
<div class="client-op-row">
|
||||
<div class="op-left-group">
|
||||
<span class="item-label">选择客户端:</span>
|
||||
<el-select
|
||||
v-model="selectedClientId"
|
||||
placeholder="请选择客户端设备"
|
||||
size="default"
|
||||
class="client-select-input"
|
||||
@change="handleClientSelect"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in clientList"
|
||||
:key="item.id"
|
||||
:label="item.client_name"
|
||||
:value="item.id"
|
||||
>
|
||||
<div class="client-option-item">
|
||||
<span class="opt-name">{{ item.client_name }}</span>
|
||||
<el-tag size="small" type="success" effect="light">已就绪</el-tag>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<!-- 已配置客户端列表表格 -->
|
||||
<section class="account-group-panel client-list-panel">
|
||||
<header class="account-group-header">
|
||||
<h4>客户端授权管理</h4>
|
||||
<span class="client-count-tag" v-if="clientList && clientList.length > 0">
|
||||
共 {{ clientList.length }} 个设备
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="op-right-group" v-if="currentClient">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
size="default"
|
||||
@click="regenerateClientCode(currentClient)"
|
||||
>
|
||||
重新生成
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
size="default"
|
||||
@click="revokeClientCode(currentClient)"
|
||||
>
|
||||
撤销删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 授权码展示横条 -->
|
||||
<div class="code-banner" v-if="currentClient">
|
||||
<div class="banner-left">
|
||||
<span class="banner-label">当前专属授权码:</span>
|
||||
<div class="banner-code-box">
|
||||
<template v-if="codesRevealed && revealedCodes[currentClient.id]">
|
||||
<code class="revealed-code">{{ revealedCodes[currentClient.id] }}</code>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="default"
|
||||
class="btn-copy-code"
|
||||
@click="copyCode(revealedCodes[currentClient.id])"
|
||||
>
|
||||
一键复制
|
||||
</el-button>
|
||||
<el-tag
|
||||
size="default"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="countdown-tag"
|
||||
>
|
||||
{{ revealCountdown }}s 后自动隐藏
|
||||
</el-tag>
|
||||
</template>
|
||||
<template v-else-if="codesRevealed && !revealedCodes[currentClient.id]">
|
||||
<span class="text-hint">未保留加密密文,可点击右上方重新生成</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="masked-code">••••••••••••••••••••</span>
|
||||
<div class="table-container">
|
||||
<el-table
|
||||
:data="pagedClients"
|
||||
class="modern-table"
|
||||
style="width: 100%"
|
||||
empty-text="暂无已配置的客户端设备,点击上方「新增客户端」创建"
|
||||
>
|
||||
<el-table-column label="ID" prop="id" min-width="40" align="center" />
|
||||
<el-table-column label="客户端名称" prop="client_name" min-width="90" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<span class="client-name-text">{{ scope.row.client_name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="60" align="center">
|
||||
<template #default>
|
||||
<el-tag size="small" type="success" effect="light" class="status-tag">已就绪</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" min-width="90" align="center">
|
||||
<template #default="scope">
|
||||
<span class="time-col">{{ formatTime(scope.row.created_at) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近活跃" min-width="90" align="center">
|
||||
<template #default="scope">
|
||||
<span class="time-col">{{ formatTime(scope.row.last_used_at) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="160" align="right">
|
||||
<template #default="scope">
|
||||
<div class="table-op-cell">
|
||||
<template v-if="codesRevealed && revealedCodes[scope.row.id]">
|
||||
<code class="mini-revealed-code">{{ revealedCodes[scope.row.id] }}</code>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
class="action-btn"
|
||||
@click="copyCode(revealedCodes[scope.row.id])"
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
</template>
|
||||
<el-button
|
||||
v-else
|
||||
type="primary"
|
||||
plain
|
||||
size="default"
|
||||
class="btn-verify-reveal"
|
||||
@click="openVerifyDialog"
|
||||
size="small"
|
||||
class="action-btn"
|
||||
@click="openVerifyDialog(scope.row)"
|
||||
>
|
||||
安全验证查看明文
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banner-right">
|
||||
<span class="meta-tag">创建时间:{{ formatTime(currentClient.created_at) }}</span>
|
||||
<span class="meta-tag" v-if="currentClient.last_used_at">
|
||||
最近活跃:{{ formatTime(currentClient.last_used_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态引导 -->
|
||||
<div class="empty-client-placeholder" v-else>
|
||||
<div class="placeholder-text">暂无可用客户端,请点击右上方「+ 新增客户端」为您的设备配置专属授权码</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="default"
|
||||
class="btn-primary-action"
|
||||
@click="openCreateDialog"
|
||||
>
|
||||
+ 立即新增客户端
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
size="small"
|
||||
class="action-btn"
|
||||
@click="regenerateClientCode(scope.row)"
|
||||
>
|
||||
重置
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
class="action-btn"
|
||||
@click="revokeClientCode(scope.row)"
|
||||
>
|
||||
撤销删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 倒计时提示栏(当有授权码明文在展示时呈现) -->
|
||||
<div class="reveal-alert-bar" v-if="codesRevealed">
|
||||
<el-tag type="warning" effect="plain" class="reveal-countdown-badge">
|
||||
授权码正在安全展示,{{ revealCountdown }} 秒后自动隐藏
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 分页控件 -->
|
||||
<div class="pagination-wrapper" v-if="clientList && clientList.length > pageSize">
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:total="clientList.length"
|
||||
:page-size="pageSize"
|
||||
:current-page="currentPage"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 通用客户端配置参数卡片 -->
|
||||
<div class="panel-card proto-guide-card">
|
||||
<div class="card-header-bar">
|
||||
<div class="header-left">
|
||||
<span class="main-title">通用客户端配置参数</span>
|
||||
<span class="sub-title">请使用完整邮箱地址登录,密码统一使用上方专属授权码</span>
|
||||
</div>
|
||||
</div>
|
||||
<section class="proto-guide-section">
|
||||
<div class="account-group-panel proto-panel">
|
||||
<header class="account-group-header">
|
||||
<h4>通用客户端配置参数</h4>
|
||||
<span class="proto-desc-hint">请使用完整邮箱地址登录,密码统一使用上方专属授权码</span>
|
||||
</header>
|
||||
|
||||
<div class="proto-grid">
|
||||
<!-- IMAP 收信 -->
|
||||
<div class="proto-card">
|
||||
<div class="proto-header">
|
||||
<div class="proto-name-badge">
|
||||
<div class="proto-title">
|
||||
<span class="proto-name">IMAP</span>
|
||||
<span class="proto-service">收信服务</span>
|
||||
<div class="proto-grid-body">
|
||||
<!-- IMAP 收信 -->
|
||||
<div class="proto-card">
|
||||
<div class="proto-header">
|
||||
<div class="proto-name-badge">
|
||||
<div class="proto-title">
|
||||
<span class="proto-name">IMAP</span>
|
||||
<span class="proto-service">收信服务</span>
|
||||
</div>
|
||||
<el-tag size="small" type="primary" effect="light">SSL / TLS</el-tag>
|
||||
</div>
|
||||
<el-tag size="small" type="primary" effect="light">SSL / TLS</el-tag>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
class="btn-card-copy"
|
||||
:disabled="!currentDomain"
|
||||
@click="copyText(`imap.${currentDomain}:993`, 'IMAP 参数')"
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
class="btn-card-copy"
|
||||
:disabled="!currentDomain"
|
||||
@click="copyText(`imap.${currentDomain}:993`, 'IMAP 参数')"
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="proto-fields">
|
||||
<div class="field-item">
|
||||
<span class="field-k">服务器</span>
|
||||
<span class="field-v font-mono">{{ currentDomain ? `imap.${currentDomain}` : '未配置邮件域名' }}</span>
|
||||
</div>
|
||||
<div class="field-item">
|
||||
<span class="field-k">端口</span>
|
||||
<span class="field-v font-mono highlight">993</span>
|
||||
</div>
|
||||
<div class="field-item">
|
||||
<span class="field-k">加密</span>
|
||||
<span class="field-v">SSL / TLS</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- POP3 收信 -->
|
||||
<div class="proto-card">
|
||||
<div class="proto-header">
|
||||
<div class="proto-name-badge">
|
||||
<div class="proto-title">
|
||||
<span class="proto-name">POP3</span>
|
||||
<span class="proto-service">收信服务</span>
|
||||
<div class="proto-fields">
|
||||
<div class="field-item">
|
||||
<span class="field-k">服务器</span>
|
||||
<span class="field-v font-mono">{{ currentDomain ? `imap.${currentDomain}` : '未配置邮件域名' }}</span>
|
||||
</div>
|
||||
<el-tag size="small" type="primary" effect="light">SSL / TLS</el-tag>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
class="btn-card-copy"
|
||||
:disabled="!currentDomain"
|
||||
@click="copyText(`pop.${currentDomain}:995`, 'POP3 参数')"
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="proto-fields">
|
||||
<div class="field-item">
|
||||
<span class="field-k">服务器</span>
|
||||
<span class="field-v font-mono">{{ currentDomain ? `pop.${currentDomain}` : '未配置邮件域名' }}</span>
|
||||
</div>
|
||||
<div class="field-item">
|
||||
<span class="field-k">端口</span>
|
||||
<span class="field-v font-mono highlight">995</span>
|
||||
</div>
|
||||
<div class="field-item">
|
||||
<span class="field-k">加密</span>
|
||||
<span class="field-v">SSL / TLS</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SMTP 发信 -->
|
||||
<div class="proto-card">
|
||||
<div class="proto-header">
|
||||
<div class="proto-name-badge">
|
||||
<div class="proto-title">
|
||||
<span class="proto-name">SMTP</span>
|
||||
<span class="proto-service">发信服务</span>
|
||||
<div class="field-item">
|
||||
<span class="field-k">端口</span>
|
||||
<span class="field-v font-mono highlight">993</span>
|
||||
</div>
|
||||
<div class="field-item">
|
||||
<span class="field-k">加密</span>
|
||||
<span class="field-v">SSL / TLS</span>
|
||||
</div>
|
||||
<el-tag size="small" type="primary" effect="light">SSL / STARTTLS</el-tag>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
class="btn-card-copy"
|
||||
:disabled="!currentDomain"
|
||||
@click="copyText(`smtp.${currentDomain}:465`, 'SMTP 参数')"
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="proto-fields">
|
||||
<div class="field-item">
|
||||
<span class="field-k">服务器</span>
|
||||
<span class="field-v font-mono">{{ currentDomain ? `smtp.${currentDomain}` : '未配置邮件域名' }}</span>
|
||||
<!-- POP3 收信 -->
|
||||
<div class="proto-card">
|
||||
<div class="proto-header">
|
||||
<div class="proto-name-badge">
|
||||
<div class="proto-title">
|
||||
<span class="proto-name">POP3</span>
|
||||
<span class="proto-service">收信服务</span>
|
||||
</div>
|
||||
<el-tag size="small" type="primary" effect="light">SSL / TLS</el-tag>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
class="btn-card-copy"
|
||||
:disabled="!currentDomain"
|
||||
@click="copyText(`pop.${currentDomain}:995`, 'POP3 参数')"
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="field-item">
|
||||
<span class="field-k">端口</span>
|
||||
<span class="field-v font-mono highlight">465 (SSL) / 587</span>
|
||||
|
||||
<div class="proto-fields">
|
||||
<div class="field-item">
|
||||
<span class="field-k">服务器</span>
|
||||
<span class="field-v font-mono">{{ currentDomain ? `pop.${currentDomain}` : '未配置邮件域名' }}</span>
|
||||
</div>
|
||||
<div class="field-item">
|
||||
<span class="field-k">端口</span>
|
||||
<span class="field-v font-mono highlight">995</span>
|
||||
</div>
|
||||
<div class="field-item">
|
||||
<span class="field-k">加密</span>
|
||||
<span class="field-v">SSL / TLS</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-item">
|
||||
<span class="field-k">加密</span>
|
||||
<span class="field-v">SSL / STARTTLS</span>
|
||||
</div>
|
||||
|
||||
<!-- SMTP 发信 -->
|
||||
<div class="proto-card">
|
||||
<div class="proto-header">
|
||||
<div class="proto-name-badge">
|
||||
<div class="proto-title">
|
||||
<span class="proto-name">SMTP</span>
|
||||
<span class="proto-service">发信服务</span>
|
||||
</div>
|
||||
<el-tag size="small" type="primary" effect="light">SSL / STARTTLS</el-tag>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
class="btn-card-copy"
|
||||
:disabled="!currentDomain"
|
||||
@click="copyText(`smtp.${currentDomain}:465`, 'SMTP 参数')"
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="proto-fields">
|
||||
<div class="field-item">
|
||||
<span class="field-k">服务器</span>
|
||||
<span class="field-v font-mono">{{ currentDomain ? `smtp.${currentDomain}` : '未配置邮件域名' }}</span>
|
||||
</div>
|
||||
<div class="field-item">
|
||||
<span class="field-k">端口</span>
|
||||
<span class="field-v font-mono highlight">465 (SSL) / 587</span>
|
||||
</div>
|
||||
<div class="field-item">
|
||||
<span class="field-k">加密</span>
|
||||
<span class="field-v">SSL / STARTTLS</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 新增客户端弹窗 -->
|
||||
<el-dialog
|
||||
@ -365,12 +352,15 @@
|
||||
|
||||
<script setup>
|
||||
import { usePage } from './ThirdPartyAuthorization.js'
|
||||
import { Plus, Refresh } from '@element-plus/icons-vue'
|
||||
|
||||
const {
|
||||
loading,
|
||||
clientList,
|
||||
selectedClientId,
|
||||
currentClient,
|
||||
pagedClients,
|
||||
currentPage,
|
||||
pageSize,
|
||||
handlePageChange,
|
||||
currentDomain,
|
||||
revealedCodes,
|
||||
codesRevealed,
|
||||
@ -384,7 +374,7 @@ const {
|
||||
verifyLoading,
|
||||
verifyActionLabel,
|
||||
verifyDescription,
|
||||
handleClientSelect,
|
||||
loadClientCredentials,
|
||||
openCreateDialog,
|
||||
closeCreateDialog,
|
||||
submitCreateClient,
|
||||
@ -400,4 +390,4 @@ const {
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped src="./ThirdPartyAuthorization.css"></style>
|
||||
<style scoped src="./ThirdPartyAuthorization.css"></style>
|
||||
@ -22,7 +22,7 @@
|
||||
.table-container {
|
||||
border: 1px solid var(--pm-border-color);
|
||||
border-radius: var(--pm-radius-sm);
|
||||
overflow-x: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@ -33,10 +33,10 @@
|
||||
<el-alert v-if="group.error" :title="group.error" type="error" :closable="false" />
|
||||
<div class="table-container">
|
||||
<el-table :data="group.rows" class="modern-table" style="width: 100%" empty-text="本组暂无成员">
|
||||
<el-table-column label="ID" prop="ID" width="44" />
|
||||
<el-table-column :label="lang.account" prop="Account" width="88" show-overflow-tooltip />
|
||||
<el-table-column :label="lang.user_name" prop="Name" width="76" show-overflow-tooltip />
|
||||
<el-table-column label="权限级别" width="100">
|
||||
<el-table-column label="ID" prop="ID" min-width="40" align="center" />
|
||||
<el-table-column :label="lang.account" prop="Account" min-width="80" show-overflow-tooltip />
|
||||
<el-table-column :label="lang.user_name" prop="Name" min-width="70" show-overflow-tooltip />
|
||||
<el-table-column label="权限级别" min-width="84" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="roleMeta(scope.row.Role).type"
|
||||
@ -48,7 +48,7 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="安全码" width="72">
|
||||
<el-table-column label="安全码" min-width="64" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="recoveryCodeMeta(scope.row.RecoveryCodeConfigured).type"
|
||||
@ -60,7 +60,7 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="外部邮箱" width="76">
|
||||
<el-table-column label="外部邮箱" min-width="70" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="externalEmailMeta(scope.row.ExternalEmailConfigured).type"
|
||||
@ -72,7 +72,7 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="账号状态" width="72">
|
||||
<el-table-column label="账号状态" min-width="68" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="accountStatusMeta(scope.row.Disabled).type"
|
||||
@ -84,7 +84,7 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="right" :width="canManageMembership ? 186 : 126" fixed="right">
|
||||
<el-table-column label="操作" align="center" :min-width="canManageMembership ? 130 : 96">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
size="small"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user