feat(security): 优化客户端专属授权码按钮布局并支持60秒自动隐藏
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-13 23:23:36 +08:00
parent 84e8e5c0ac
commit dc4090b20b
3 changed files with 126 additions and 80 deletions

View File

@ -1,4 +1,4 @@
.third-party-authorization {
.third-party-authorization {
padding: 0;
display: flex;
flex-direction: column;
@ -11,7 +11,7 @@
gap: 12px;
}
/* 顶部轻量工具条 */
/* 顶部轻量工具条:纯净标题栏 */
.compact-header {
display: flex;
align-items: center;
@ -45,40 +45,34 @@
text-overflow: ellipsis;
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
/* 客户端选择与管理主卡片 */
.client-main-card {
padding: 12px 14px;
padding: 14px 16px;
background: var(--pm-bg-secondary);
border: 1px solid var(--pm-border-color);
border-radius: var(--pm-radius-md);
display: flex;
flex-direction: column;
gap: 10px;
gap: 12px;
}
.client-picker-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
gap: 14px;
flex-wrap: wrap;
}
.picker-group {
display: flex;
align-items: center;
gap: 8px;
gap: 10px;
flex-wrap: wrap;
}
.picker-label {
font-size: 13px;
font-size: 14px;
font-weight: 600;
color: var(--pm-text-primary);
white-space: nowrap;
@ -96,15 +90,19 @@
width: 100%;
}
.btn-create-client {
font-weight: 600;
}
.picker-actions {
display: flex;
align-items: center;
gap: 8px;
gap: 10px;
}
/* 授权码详情框 */
.client-detail-box {
padding: 10px 12px;
padding: 12px 14px;
background: var(--pm-bg-primary);
border: 1px dashed var(--pm-border-color);
border-radius: var(--pm-radius-sm);
@ -115,7 +113,7 @@
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
gap: 10px;
}
.code-label {
@ -128,7 +126,8 @@
.code-value-area {
display: flex;
align-items: center;
gap: 8px;
gap: 10px;
flex-wrap: wrap;
}
.masked-code {
@ -136,22 +135,30 @@
font-family: monospace;
font-size: 14px;
color: var(--pm-text-secondary);
font-weight: 600;
}
.revealed-code {
padding: 2px 8px;
padding: 4px 10px;
border-radius: 4px;
background: color-mix(in srgb, var(--pm-primary-color) 12%, var(--pm-bg-secondary));
color: var(--pm-primary-color);
font-family: monospace;
font-size: 14px;
font-size: 15px;
font-weight: 700;
letter-spacing: 1px;
}
.action-btn {
font-size: 13px;
padding: 0 4px;
}
.countdown-tag {
font-weight: 600;
}
.btn-verify-reveal {
font-weight: 600;
}
.text-tip {
@ -169,11 +176,15 @@
}
.empty-client-box {
padding: 12px;
padding: 18px 12px;
text-align: center;
background: var(--pm-bg-primary);
border: 1px dashed var(--pm-border-color);
border-radius: var(--pm-radius-sm);
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.empty-tip {
@ -181,6 +192,10 @@
color: var(--pm-text-placeholder, #909399);
}
.empty-action {
margin-top: 4px;
}
/* 紧凑指引网格 */
.compact-guide-section {
display: flex;
@ -280,4 +295,4 @@
flex-direction: column;
align-items: stretch;
}
}
}

View File

@ -1,4 +1,4 @@
import { computed, onMounted, reactive, ref } from 'vue'
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import { ElMessageBox, ElNotification } from 'element-plus'
@ -13,6 +13,8 @@ export function usePage() {
// 解密后的各客户端授权码明文 { [clientId]: 'xxxx-yyyy-...' }
const revealedCodes = reactive({})
const codesRevealed = ref(false)
const revealCountdown = ref(0)
let countdownTimer = null
// 新增客户端弹窗
const createDialogVisible = ref(false)
@ -37,6 +39,36 @@ export function usePage() {
})
}
const clearCountdownTimer = () => {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
}
const hideCodesAutomatically = (silent = false) => {
clearCountdownTimer()
revealCountdown.value = 0
Object.keys(revealedCodes).forEach(k => delete revealedCodes[k])
codesRevealed.value = false
if (!silent) {
notify('授权码明文已达到 60 秒安全保护期,已自动隐藏', 'info')
}
}
const startRevealCountdown = (seconds = 60) => {
clearCountdownTimer()
revealCountdown.value = seconds
codesRevealed.value = true
countdownTimer = setInterval(() => {
revealCountdown.value -= 1
if (revealCountdown.value <= 0) {
hideCodesAutomatically(false)
}
}, 1000)
}
const formatTime = (timeStr) => {
if (!timeStr) return '--'
try {
@ -124,9 +156,9 @@ export function usePage() {
if (item && item.id) {
selectedClientId.value = item.id
revealedCodes[item.id] = code
codesRevealed.value = true
startRevealCountdown(60)
}
notify(`客户端「${name}」专属授权码已创建并就绪`, 'success')
notify(`客户端「${name}」专属授权码已创建并就绪(60秒内有效显示)`, 'success')
} catch (error) {
notify(error?.message || '创建客户端授权码失败')
} finally {
@ -160,8 +192,8 @@ export function usePage() {
}
const newCode = response.data?.code || ''
revealedCodes[client.id] = newCode
codesRevealed.value = true
notify(`客户端「${client.client_name}」专属授权码已重新生成`, 'success')
startRevealCountdown(60)
notify(`客户端「${client.client_name}」专属授权码已重新生成(60秒内有效显示)`, 'success')
await loadClientCredentials()
} catch (error) {
notify(error?.message || '重新生成失败')
@ -253,13 +285,13 @@ export function usePage() {
const map = response.data || {}
Object.keys(revealedCodes).forEach(k => delete revealedCodes[k])
Object.assign(revealedCodes, map)
codesRevealed.value = true
closeVerifyDialog()
const count = Object.keys(map).length
if (count > 0) {
notify(`安全验证通过,已解密显示客户端专属授权码`, 'success')
startRevealCountdown(60)
notify('安全验证通过,授权码将在 60 秒内明文显示', 'success')
} else {
notify('安全验证通过,当前未发现加密密文,可重新生成', 'warning')
notify('安全验证通过,当前未发现加密密文,可点击重新生成', 'warning')
}
} catch (error) {
notify(error?.message || '验证失败,请检查密码是否正确')
@ -268,17 +300,15 @@ export function usePage() {
}
}
const hideRevealedCodes = () => {
Object.keys(revealedCodes).forEach(k => delete revealedCodes[k])
codesRevealed.value = false
notify('已恢复安全隐藏状态', 'info')
}
onMounted(() => {
loadBaseInfo()
loadClientCredentials()
})
onUnmounted(() => {
clearCountdownTimer()
})
return {
loading,
clientList,
@ -287,6 +317,7 @@ export function usePage() {
currentDomain,
revealedCodes,
codesRevealed,
revealCountdown,
createDialogVisible,
newClientName,
createLoading,
@ -304,6 +335,5 @@ export function usePage() {
openVerifyDialog,
closeVerifyDialog,
submitVerifyAndReveal,
hideRevealedCodes,
}
}
}

View File

@ -1,39 +1,12 @@
<template>
<template>
<div class="third-party-authorization" v-loading="loading">
<section class="authorization-section">
<!-- 顶部轻量工具条:紧凑一行 -->
<!-- 顶部轻量工具条:纯净标题与说明 -->
<div class="compact-header">
<div class="header-left">
<span class="header-title">客户端专属授权码</span>
<span class="header-subtitle">为手机、Foxmail 等设备创建独立授权码,随时查看或撤销</span>
</div>
<div class="header-right">
<el-button
v-if="hasClients && !codesRevealed"
type="primary"
plain
size="small"
@click="openVerifyDialog"
>
安全验证查看明文
</el-button>
<el-button
v-else-if="codesRevealed"
type="info"
plain
size="small"
@click="hideRevealedCodes"
>
安全隐藏明文
</el-button>
<el-button
type="primary"
size="small"
@click="openCreateDialog"
>
+ 新增客户端
</el-button>
</div>
</div>
<!-- 客户端选择与管理主卡片 -->
@ -60,13 +33,22 @@
</div>
</el-option>
</el-select>
<el-button
type="primary"
size="default"
class="btn-create-client"
@click="openCreateDialog"
>
+ 新增客户端
</el-button>
</div>
<div class="picker-actions" v-if="currentClient">
<el-button
type="warning"
plain
size="small"
size="default"
@click="regenerateClientCode(currentClient)"
>
重新生成
@ -74,7 +56,7 @@
<el-button
type="danger"
plain
size="small"
size="default"
@click="revokeClientCode(currentClient)"
>
撤销删除
@ -91,13 +73,20 @@
<code class="revealed-code">{{ revealedCodes[currentClient.id] }}</code>
<el-button
type="primary"
link
size="small"
size="default"
class="action-btn"
@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-tip">未保留加密密文,可点击重新生成</span>
@ -106,12 +95,12 @@
<span class="masked-code">••••••••••••••••••••</span>
<el-button
type="primary"
link
size="small"
class="action-btn"
plain
size="default"
class="btn-verify-reveal"
@click="openVerifyDialog"
>
验证查看
安全验证查看明文
</el-button>
</template>
</div>
@ -126,6 +115,15 @@
<div class="empty-client-box" v-else>
<span class="empty-tip">暂无可用客户端,请点击上方「+ 新增客户端」为您的设备配置专属授权码</span>
<div class="empty-action">
<el-button
type="primary"
size="default"
@click="openCreateDialog"
>
+ 立即新增客户端
</el-button>
</div>
</div>
</div>
@ -135,6 +133,7 @@
<span>通用客户端配置参数</span>
<span class="guide-sub">支持用户名直接登录或完整邮箱登录,密码统一使用上方专属授权码</span>
</div>
<div class="guide-grid">
<div class="guide-card">
<div class="card-head">
@ -212,9 +211,10 @@
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeCreateDialog">取消</el-button>
<el-button size="default" @click="closeCreateDialog">取消</el-button>
<el-button
type="primary"
size="default"
:loading="createLoading"
:disabled="!newClientName.trim()"
@click="submitCreateClient"
@ -253,9 +253,10 @@
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeVerifyDialog">取消</el-button>
<el-button size="default" @click="closeVerifyDialog">取消</el-button>
<el-button
type="primary"
size="default"
:loading="verifyLoading"
:disabled="!verifyPassword.trim()"
@click="submitVerifyAndReveal"
@ -280,6 +281,7 @@ const {
currentDomain,
revealedCodes,
codesRevealed,
revealCountdown,
createDialogVisible,
newClientName,
createLoading,
@ -297,7 +299,6 @@ const {
openVerifyDialog,
closeVerifyDialog,
submitVerifyAndReveal,
hideRevealedCodes,
} = usePage()
const hasClients = computed(() =>
@ -305,4 +306,4 @@ const hasClients = computed(() =>
)
</script>
<style scoped src="./ThirdPartyAuthorization.css"></style>
<style scoped src="./ThirdPartyAuthorization.css"></style>