feat(security): 优化客户端专属授权码按钮布局并支持60秒自动隐藏
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
84e8e5c0ac
commit
dc4090b20b
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user