feat(security): 优化客户端专属授权码页面空间利用与三板块饱满布局
Some checks are pending
Security Checks / security (push) Waiting to run
Some checks are pending
Security Checks / security (push) Waiting to run
This commit is contained in:
parent
cc34b64648
commit
6087f028a2
@ -2,61 +2,28 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.authorization-section {
|
.authorization-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 顶部轻量工具条:纯净标题栏 */
|
/* 通用板块卡片容器 */
|
||||||
.compact-header {
|
.panel-card {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 10px 14px;
|
|
||||||
background: var(--pm-bg-secondary);
|
|
||||||
border: 1px solid var(--pm-border-color);
|
|
||||||
border-radius: var(--pm-radius-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 8px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--pm-text-primary);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-subtitle {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--pm-text-secondary);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 客户端选择与管理主卡片 */
|
|
||||||
.client-main-card {
|
|
||||||
padding: 14px 16px;
|
|
||||||
background: var(--pm-bg-secondary);
|
background: var(--pm-bg-secondary);
|
||||||
border: 1px solid var(--pm-border-color);
|
border: 1px solid var(--pm-border-color);
|
||||||
border-radius: var(--pm-radius-md);
|
border-radius: var(--pm-radius-md);
|
||||||
|
padding: 16px 18px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-picker-row {
|
/* 顶部标题栏 */
|
||||||
|
.card-header-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -64,22 +31,68 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.picker-group {
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--pm-text-primary);
|
||||||
|
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 {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片内容区域 */
|
||||||
|
.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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.picker-label {
|
.item-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--pm-text-primary);
|
color: var(--pm-text-primary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-select {
|
.client-select-input {
|
||||||
width: 220px;
|
width: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-option-item {
|
.client-option-item {
|
||||||
@ -90,40 +103,40 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-create-client {
|
.op-right-group {
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 授权码详情框 */
|
/* 授权码展示横条 */
|
||||||
.client-detail-box {
|
.code-banner {
|
||||||
padding: 12px 14px;
|
padding: 14px 16px;
|
||||||
background: var(--pm-bg-primary);
|
background: var(--pm-bg-primary);
|
||||||
border: 1px dashed var(--pm-border-color);
|
border: 1px dashed var(--pm-border-color);
|
||||||
border-radius: var(--pm-radius-sm);
|
border-radius: var(--pm-radius-sm);
|
||||||
}
|
|
||||||
|
|
||||||
.code-display-row {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-label {
|
.banner-left {
|
||||||
font-size: 13px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-label {
|
||||||
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--pm-text-secondary);
|
color: var(--pm-text-secondary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-value-area {
|
.banner-code-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@ -133,7 +146,7 @@
|
|||||||
.masked-code {
|
.masked-code {
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
color: var(--pm-text-secondary);
|
color: var(--pm-text-secondary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@ -149,8 +162,8 @@
|
|||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.btn-copy-code {
|
||||||
font-size: 13px;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.countdown-tag {
|
.countdown-tag {
|
||||||
@ -161,22 +174,27 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-tip {
|
.text-hint {
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
color: var(--pm-text-placeholder, #909399);
|
color: var(--pm-text-placeholder, #909399);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-meta {
|
.banner-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 14px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--pm-text-secondary);
|
color: var(--pm-text-secondary);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-client-box {
|
.meta-tag {
|
||||||
padding: 18px 12px;
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态占位 */
|
||||||
|
.empty-client-placeholder {
|
||||||
|
padding: 24px 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: var(--pm-bg-primary);
|
background: var(--pm-bg-primary);
|
||||||
border: 1px dashed var(--pm-border-color);
|
border: 1px dashed var(--pm-border-color);
|
||||||
@ -184,89 +202,150 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-tip {
|
.placeholder-text {
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: var(--pm-text-placeholder, #909399);
|
color: var(--pm-text-placeholder, #909399);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-action {
|
/* 通用协议指引网格 */
|
||||||
margin-top: 4px;
|
.proto-grid {
|
||||||
}
|
|
||||||
|
|
||||||
/* 紧凑指引网格 */
|
|
||||||
.compact-guide-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guide-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--pm-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.guide-sub {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: normal;
|
|
||||||
color: var(--pm-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.guide-grid {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 10px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-card {
|
.proto-card {
|
||||||
padding: 10px 12px;
|
padding: 14px 16px;
|
||||||
background: var(--pm-bg-secondary);
|
background: var(--pm-bg-primary);
|
||||||
border: 1px solid var(--pm-border-color);
|
border: 1px solid var(--pm-border-color);
|
||||||
border-radius: var(--pm-radius-md);
|
border-radius: var(--pm-radius-sm);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 10px;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-head {
|
.proto-card:hover {
|
||||||
|
border-color: var(--pm-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 2px;
|
gap: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--pm-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-proto {
|
.proto-name-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--pm-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 13px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-k {
|
||||||
|
color: var(--pm-text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-v {
|
||||||
|
color: var(--pm-text-primary);
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-mono {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-v.highlight {
|
||||||
|
color: var(--pm-primary-color);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 模块 3:配置提示与常见问题卡片 */
|
||||||
|
.instructions-card {
|
||||||
|
background: var(--pm-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruct-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: var(--pm-bg-primary);
|
||||||
|
border: 1px solid var(--pm-border-color);
|
||||||
|
border-radius: var(--pm-radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruct-num {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: color-mix(in srgb, var(--pm-primary-color) 16%, var(--pm-bg-secondary));
|
||||||
|
color: var(--pm-primary-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruct-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruct-title {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--pm-text-primary);
|
color: var(--pm-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-line {
|
.instruct-desc {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
line-height: 1.5;
|
||||||
|
|
||||||
.cl-label {
|
|
||||||
color: var(--pm-text-secondary);
|
color: var(--pm-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cl-val {
|
.instruct-desc code {
|
||||||
font-family: monospace;
|
padding: 1px 4px;
|
||||||
color: var(--pm-text-primary);
|
border-radius: 3px;
|
||||||
font-weight: 500;
|
background: color-mix(in srgb, var(--pm-primary-color) 10%, var(--pm-bg-secondary));
|
||||||
}
|
|
||||||
|
|
||||||
.cl-val.highlight {
|
|
||||||
color: var(--pm-primary-color);
|
color: var(--pm-primary-color);
|
||||||
font-weight: 700;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 弹窗通用紧凑样式 */
|
/* 弹窗通用紧凑样式 */
|
||||||
@ -283,16 +362,24 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 960px) {
|
||||||
.guide-grid {
|
.proto-grid,
|
||||||
|
.instructions-content {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
.compact-header {
|
.card-header-bar {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
.client-picker-row {
|
.client-op-row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
.code-banner {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.banner-right {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -106,7 +106,6 @@ export function usePage() {
|
|||||||
const list = Array.isArray(response.data) ? response.data : []
|
const list = Array.isArray(response.data) ? response.data : []
|
||||||
clientList.value = list
|
clientList.value = list
|
||||||
if (list.length > 0) {
|
if (list.length > 0) {
|
||||||
// 如果当前选中的不在列表中,默认选中第一个
|
|
||||||
if (!selectedClientId.value || !list.some(c => c.id === selectedClientId.value)) {
|
if (!selectedClientId.value || !list.some(c => c.id === selectedClientId.value)) {
|
||||||
selectedClientId.value = list[0].id
|
selectedClientId.value = list[0].id
|
||||||
}
|
}
|
||||||
@ -236,28 +235,33 @@ export function usePage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyCode = async (codeText) => {
|
const copyText = async (text, label = '内容') => {
|
||||||
if (!codeText) {
|
if (!text) return
|
||||||
notify('未找到可复制的授权码')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
if (navigator?.clipboard?.writeText) {
|
if (navigator?.clipboard?.writeText) {
|
||||||
await navigator.clipboard.writeText(codeText)
|
await navigator.clipboard.writeText(text)
|
||||||
} else {
|
} else {
|
||||||
const input = document.createElement('input')
|
const input = document.createElement('input')
|
||||||
input.value = codeText
|
input.value = text
|
||||||
document.body.appendChild(input)
|
document.body.appendChild(input)
|
||||||
input.select()
|
input.select()
|
||||||
document.execCommand('copy')
|
document.execCommand('copy')
|
||||||
document.body.removeChild(input)
|
document.body.removeChild(input)
|
||||||
}
|
}
|
||||||
notify('专属授权码已复制到剪贴板', 'success')
|
notify(`${label}已复制到剪贴板`, 'success')
|
||||||
} catch {
|
} catch {
|
||||||
notify('复制失败,请手动选择复制')
|
notify('复制失败,请手动选择复制')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copyCode = async (codeText) => {
|
||||||
|
if (!codeText) {
|
||||||
|
notify('未找到可复制的授权码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await copyText(codeText, '专属授权码')
|
||||||
|
}
|
||||||
|
|
||||||
const openVerifyDialog = () => {
|
const openVerifyDialog = () => {
|
||||||
verifyPassword.value = ''
|
verifyPassword.value = ''
|
||||||
verifyDialogVisible.value = true
|
verifyDialogVisible.value = true
|
||||||
@ -330,6 +334,7 @@ export function usePage() {
|
|||||||
submitCreateClient,
|
submitCreateClient,
|
||||||
regenerateClientCode,
|
regenerateClientCode,
|
||||||
revokeClientCode,
|
revokeClientCode,
|
||||||
|
copyText,
|
||||||
copyCode,
|
copyCode,
|
||||||
formatTime,
|
formatTime,
|
||||||
openVerifyDialog,
|
openVerifyDialog,
|
||||||
|
|||||||
@ -1,182 +1,276 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="third-party-authorization" v-loading="loading">
|
<div class="third-party-authorization" v-loading="loading">
|
||||||
<section class="authorization-section">
|
<section class="authorization-section">
|
||||||
<!-- 顶部轻量工具条:纯净标题与说明 -->
|
<!-- 模块 1:客户端与专属授权码管理卡片 -->
|
||||||
<div class="compact-header">
|
<div class="panel-card client-control-card">
|
||||||
<div class="header-left">
|
<div class="card-header-bar">
|
||||||
<span class="header-title">客户端专属授权码</span>
|
<div class="header-left">
|
||||||
<span class="header-subtitle">为手机、Foxmail 等设备创建独立授权码,随时查看或撤销</span>
|
<span class="main-title">客户端专属授权码</span>
|
||||||
</div>
|
<span class="sub-title">为手机、Foxmail、Outlook 等设备创建独立授权码,随时查看或撤销</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 客户端选择与管理主卡片 -->
|
|
||||||
<div class="client-main-card">
|
|
||||||
<div class="client-picker-row">
|
|
||||||
<div class="picker-group">
|
|
||||||
<span class="picker-label">选择客户端:</span>
|
|
||||||
<el-select
|
|
||||||
v-model="selectedClientId"
|
|
||||||
placeholder="请选择或添加客户端"
|
|
||||||
size="default"
|
|
||||||
class="client-select"
|
|
||||||
@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>{{ item.client_name }}</span>
|
|
||||||
<el-tag size="small" type="success">已就绪</el-tag>
|
|
||||||
</div>
|
|
||||||
</el-option>
|
|
||||||
</el-select>
|
|
||||||
|
|
||||||
|
<div class="header-actions">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="default"
|
size="default"
|
||||||
class="btn-create-client"
|
class="btn-primary-action"
|
||||||
@click="openCreateDialog"
|
@click="openCreateDialog"
|
||||||
>
|
>
|
||||||
+ 新增客户端
|
+ 新增客户端
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="picker-actions" 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>
|
||||||
|
|
||||||
<!-- 当前选中客户端的授权码展示条 -->
|
<div class="card-content-area">
|
||||||
<div class="client-detail-box" v-if="currentClient">
|
<div class="client-op-row">
|
||||||
<div class="code-display-row">
|
<div class="op-left-group">
|
||||||
<span class="code-label">当前专属授权码:</span>
|
<span class="item-label">选择客户端:</span>
|
||||||
<div class="code-value-area">
|
<el-select
|
||||||
<template v-if="codesRevealed && revealedCodes[currentClient.id]">
|
v-model="selectedClientId"
|
||||||
<code class="revealed-code">{{ revealedCodes[currentClient.id] }}</code>
|
placeholder="请选择客户端设备"
|
||||||
<el-button
|
size="default"
|
||||||
type="primary"
|
class="client-select-input"
|
||||||
size="default"
|
@change="handleClientSelect"
|
||||||
class="action-btn"
|
>
|
||||||
@click="copyCode(revealedCodes[currentClient.id])"
|
<el-option
|
||||||
|
v-for="item in clientList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.client_name"
|
||||||
|
:value="item.id"
|
||||||
>
|
>
|
||||||
一键复制
|
<div class="client-option-item">
|
||||||
</el-button>
|
<span class="opt-name">{{ item.client_name }}</span>
|
||||||
<el-tag
|
<el-tag size="small" type="success" effect="light">已就绪</el-tag>
|
||||||
size="default"
|
</div>
|
||||||
type="warning"
|
</el-option>
|
||||||
effect="plain"
|
</el-select>
|
||||||
class="countdown-tag"
|
|
||||||
>
|
|
||||||
{{ revealCountdown }}s 后自动隐藏
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="codesRevealed && !revealedCodes[currentClient.id]">
|
|
||||||
<span class="text-tip">未保留加密密文,可点击重新生成</span>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<span class="masked-code">••••••••••••••••••••</span>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
plain
|
|
||||||
size="default"
|
|
||||||
class="btn-verify-reveal"
|
|
||||||
@click="openVerifyDialog"
|
|
||||||
>
|
|
||||||
安全验证查看明文
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="client-meta">
|
|
||||||
<span class="meta-item">创建时间:{{ formatTime(currentClient.created_at) }}</span>
|
<div class="op-right-group" v-if="currentClient">
|
||||||
<span class="meta-item" v-if="currentClient.last_used_at">
|
<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>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
size="default"
|
||||||
|
class="btn-verify-reveal"
|
||||||
|
@click="openVerifyDialog"
|
||||||
|
>
|
||||||
|
安全验证查看明文
|
||||||
|
</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) }}
|
最近活跃:{{ formatTime(currentClient.last_used_at) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="empty-client-box" v-else>
|
<!-- 空状态引导 -->
|
||||||
<span class="empty-tip">暂无可用客户端,请点击上方「+ 新增客户端」为您的设备配置专属授权码</span>
|
<div class="empty-client-placeholder" v-else>
|
||||||
<div class="empty-action">
|
<div class="placeholder-text">暂无可用客户端,请点击右上方「+ 新增客户端」为您的设备配置专属授权码</div>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="default"
|
size="default"
|
||||||
@click="openCreateDialog"
|
@click="openCreateDialog"
|
||||||
>
|
>
|
||||||
+ 立即新增客户端
|
立即新增客户端
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 客户端配置指引紧凑网格(一屏显示完整,无滚动条) -->
|
<!-- 模块 2:通用客户端配置参数卡片 -->
|
||||||
<div class="compact-guide-section">
|
<div class="panel-card proto-guide-card">
|
||||||
<div class="guide-title">
|
<div class="card-header-bar">
|
||||||
<span>通用客户端配置参数</span>
|
<div class="header-left">
|
||||||
<span class="guide-sub">支持用户名直接登录或完整邮箱登录,密码统一使用上方专属授权码</span>
|
<span class="main-title">通用客户端配置参数</span>
|
||||||
|
<span class="sub-title">支持用户名(如 xuans)或完整邮箱登录,密码统一使用上方专属授权码</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="guide-grid">
|
<div class="proto-grid">
|
||||||
<div class="guide-card">
|
<!-- IMAP 收信 -->
|
||||||
<div class="card-head">
|
<div class="proto-card">
|
||||||
<span class="card-proto">IMAP 收信服务</span>
|
<div class="proto-header">
|
||||||
<el-tag size="small" type="primary">SSL/TLS</el-tag>
|
<div class="proto-name-badge">
|
||||||
|
<span class="proto-name">IMAP 收信服务</span>
|
||||||
|
<el-tag size="small" type="primary" effect="light">SSL / TLS</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
@click="copyText(`imap.${currentDomain || 'solit.cn'}:993`, 'IMAP 参数')"
|
||||||
|
>
|
||||||
|
复制
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-line">
|
|
||||||
<span class="cl-label">服务器地址:</span>
|
<div class="proto-fields">
|
||||||
<span class="cl-val">imap.{{ currentDomain || 'solit.cn' }}</span>
|
<div class="field-item">
|
||||||
</div>
|
<span class="field-k">服务器地址:</span>
|
||||||
<div class="card-line">
|
<span class="field-v font-mono">imap.{{ currentDomain || 'solit.cn' }}</span>
|
||||||
<span class="cl-label">服务端口:</span>
|
</div>
|
||||||
<span class="cl-val highlight">993</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="guide-card">
|
<!-- POP3 收信 -->
|
||||||
<div class="card-head">
|
<div class="proto-card">
|
||||||
<span class="card-proto">POP3 收信服务</span>
|
<div class="proto-header">
|
||||||
<el-tag size="small" type="primary">SSL/TLS</el-tag>
|
<div class="proto-name-badge">
|
||||||
|
<span class="proto-name">POP3 收信服务</span>
|
||||||
|
<el-tag size="small" type="primary" effect="light">SSL / TLS</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
@click="copyText(`pop.${currentDomain || 'solit.cn'}:995`, 'POP3 参数')"
|
||||||
|
>
|
||||||
|
复制
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-line">
|
|
||||||
<span class="cl-label">服务器地址:</span>
|
<div class="proto-fields">
|
||||||
<span class="cl-val">pop.{{ currentDomain || 'solit.cn' }}</span>
|
<div class="field-item">
|
||||||
</div>
|
<span class="field-k">服务器地址:</span>
|
||||||
<div class="card-line">
|
<span class="field-v font-mono">pop.{{ currentDomain || 'solit.cn' }}</span>
|
||||||
<span class="cl-label">服务端口:</span>
|
</div>
|
||||||
<span class="cl-val highlight">995</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="guide-card">
|
<!-- SMTP 发信 -->
|
||||||
<div class="card-head">
|
<div class="proto-card">
|
||||||
<span class="card-proto">SMTP 发信服务</span>
|
<div class="proto-header">
|
||||||
<el-tag size="small" type="primary">SSL / STARTTLS</el-tag>
|
<div class="proto-name-badge">
|
||||||
|
<span class="proto-name">SMTP 发信服务</span>
|
||||||
|
<el-tag size="small" type="primary" effect="light">SSL / STARTTLS</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
@click="copyText(`smtp.${currentDomain || 'solit.cn'}:465`, 'SMTP 参数')"
|
||||||
|
>
|
||||||
|
复制
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-line">
|
|
||||||
<span class="cl-label">服务器地址:</span>
|
<div class="proto-fields">
|
||||||
<span class="cl-val">smtp.{{ currentDomain || 'solit.cn' }}</span>
|
<div class="field-item">
|
||||||
|
<span class="field-k">服务器地址:</span>
|
||||||
|
<span class="field-v font-mono">smtp.{{ currentDomain || 'solit.cn' }}</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 class="card-line">
|
</div>
|
||||||
<span class="cl-label">服务端口:</span>
|
</div>
|
||||||
<span class="cl-val highlight">465 (SSL) / 587</span>
|
</div>
|
||||||
|
|
||||||
|
<!-- 模块 3:客户端使用与排错说明卡片(充实页面、解决大面积留白) -->
|
||||||
|
<div class="panel-card instructions-card">
|
||||||
|
<div class="card-header-bar">
|
||||||
|
<div class="header-left">
|
||||||
|
<span class="main-title">配置提示与常见问题</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="instructions-content">
|
||||||
|
<div class="instruct-item">
|
||||||
|
<span class="instruct-num">1</span>
|
||||||
|
<div class="instruct-body">
|
||||||
|
<div class="instruct-title">一机一码隔离保护</div>
|
||||||
|
<div class="instruct-desc">建议手机、办公电脑与备用客户端分别创建专属授权码。若设备遗失或更换,仅需在上方撤销对应客户端,其他设备与主密码完全不受影响。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="instruct-item">
|
||||||
|
<span class="instruct-num">2</span>
|
||||||
|
<div class="instruct-body">
|
||||||
|
<div class="instruct-title">登录用户名规范</div>
|
||||||
|
<div class="instruct-desc">第三方客户端的“账户名称/用户名”一栏可直接填写账号名(如 <code>xuans</code>)或完整邮箱(如 <code>xuans@{{ currentDomain || 'solit.cn' }}</code>),密码填写生成的专属授权码。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="instruct-item">
|
||||||
|
<span class="instruct-num">3</span>
|
||||||
|
<div class="instruct-body">
|
||||||
|
<div class="instruct-title">证书信任与安全握手</div>
|
||||||
|
<div class="instruct-desc">当前邮件系统所有协议均强制启用安全加密连接。如使用第三方邮件软件出现证书提示,请确认服务器域名解析是否正确指向当前邮件服务器。</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -294,6 +388,7 @@ const {
|
|||||||
submitCreateClient,
|
submitCreateClient,
|
||||||
regenerateClientCode,
|
regenerateClientCode,
|
||||||
revokeClientCode,
|
revokeClientCode,
|
||||||
|
copyText,
|
||||||
copyCode,
|
copyCode,
|
||||||
formatTime,
|
formatTime,
|
||||||
openVerifyDialog,
|
openVerifyDialog,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user