fix(frontend): 修复邮件客户端协议授权码组件中的中文文案乱码
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-12 15:02:44 +08:00
parent fe9f7aa1b2
commit fd5bc240c4
3 changed files with 144 additions and 113 deletions

View File

@ -18,8 +18,8 @@
.authorization-header h3 { .authorization-header h3 {
margin: 0 0 8px; margin: 0 0 8px;
color: var(--pm-text-primary); color: var(--pm-text-primary);
font-size: 18px; font-size: 20px;
font-weight: 600; font-weight: 700;
} }
.authorization-desc { .authorization-desc {
@ -40,18 +40,19 @@
} }
.protocol-list { .protocol-list {
display: grid; display: flex;
gap: 12px; flex-direction: column;
gap: 16px;
} }
.protocol-item { .protocol-item {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 18px; gap: 20px;
padding: 16px; padding: 16px 20px;
border-radius: var(--pm-radius-md);
border: 1px solid var(--pm-border-color); border: 1px solid var(--pm-border-color);
border-radius: var(--pm-radius-sm);
background: var(--pm-bg-secondary); background: var(--pm-bg-secondary);
} }
@ -63,8 +64,7 @@
.protocol-title { .protocol-title {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; gap: 10px;
gap: 8px;
} }
.protocol-title strong { .protocol-title strong {
@ -154,9 +154,10 @@
.protocol-item { .protocol-item {
align-items: stretch; align-items: stretch;
flex-direction: column; flex-direction: column;
gap: 14px;
} }
.protocol-actions { .protocol-actions {
justify-content: flex-start; justify-content: flex-start;
} }
} }

View File

@ -10,77 +10,107 @@ export function usePage() {
const generatedProtocolCode = ref('') const generatedProtocolCode = ref('')
const generatedProtocol = ref('') const generatedProtocol = ref('')
// ??????????{ imap: 'xxxx', pop3: 'yyyy', smtp: 'zzzz' } // 解密后的各协议授权码明文 { imap: 'xxxx', pop3: 'yyyy', smtp: 'zzzz' }
const revealedCodes = reactive({}) const revealedCodes = reactive({})
// ??????????????? // 是否已完成密码安全验证并展示
const codesRevealed = ref(false) const codesRevealed = ref(false)
// ???????? // 密码验证查看弹窗
const verifyDialogVisible = ref(false) const verifyDialogVisible = ref(false)
const verifyPassword = ref('') const verifyPassword = ref('')
const verifyLoading = ref(false) const verifyLoading = ref(false)
const notify = (message, type = 'error') => { const notify = (message, type = 'error') => {
ElNotification({ ElNotification({
title: type === 'success' ? '????' : '????', title: type === 'success' ? '操作成功' : '操作失败',
message, message,
type, type,
}) })
} }
const protocolLabel = protocol => ({ const protocolLabel = (protocol = '') => {
imap: 'IMAP', const map = {
pop3: 'POP3', smtp: 'SMTP (代发/发信)',
smtp: 'SMTP', imap: 'IMAP (收信/同步)',
}[protocol] || protocol.toUpperCase()) pop3: 'POP3 (收信)',
}
return map[protocol.toLowerCase()] || protocol.toUpperCase()
}
const loadProtocolCredentials = async () => { const loadProtocolCredentials = async () => {
protocolLoading.value = true protocolLoading.value = true
try { try {
const response = await http.get('/api/v1/security/credentials/protocols') const response = await http.get('/api/v1/security/credentials/protocols')
if (response.errorNo !== 0) { if (response.errorNo !== 0) {
throw new Error(response.errorMsg || '?????????????') throw new Error(response.errorMsg || '读取邮件客户端协议设置失败')
} }
protocolCredentials.value = Array.isArray(response.data) ? response.data : [] protocolCredentials.value = Array.isArray(response.data) ? response.data : []
} catch (error) { } catch (error) {
notify(error?.message || '?????????????') notify(error?.message || '读取邮件客户端协议设置失败')
} finally { } finally {
protocolLoading.value = false protocolLoading.value = false
} }
} }
const setProtocolEnabled = async (item, enabled) => { const setProtocolEnabled = async (item, enabled) => {
if (protocolLoading.value || item.enabled === enabled) return
const previous = item.enabled
item.enabled = enabled
protocolLoading.value = true protocolLoading.value = true
try { try {
const response = await http.put('/api/v1/security/credentials/protocols/enabled', { const response = await http.post('/api/v1/security/credentials/protocols/status', {
protocol: item.protocol, protocol: item.protocol,
enabled, enabled,
}) })
if (response.errorNo !== 0) { if (response.errorNo !== 0) {
throw new Error(response.errorMsg || '??????') throw new Error(response.errorMsg || '更新协议开启状态失败')
} }
if (!enabled) { notify(`${protocolLabel(item.protocol)} ${enabled ? '已开启' : '已关闭'}`, 'success')
if (generatedProtocol.value === item.protocol) { await loadProtocolCredentials()
generatedProtocol.value = ''
generatedProtocolCode.value = ''
}
delete revealedCodes[item.protocol]
}
notify(`${protocolLabel(item.protocol)} ??????????${enabled ? '??' : '??'}`, 'success')
} catch (error) { } catch (error) {
item.enabled = previous notify(error?.message || '更新协议开启状态失败')
notify(error?.message || '??????')
} finally { } finally {
protocolLoading.value = false protocolLoading.value = false
} }
} }
const generateProtocolCode = async item => { const copyProtocolCode = async (codeToCopy) => {
if (!item.enabled) { const targetCode = typeof codeToCopy === 'string' && codeToCopy
notify(`???? ${protocolLabel(item.protocol)} ??`) ? codeToCopy
: generatedProtocolCode.value
if (!targetCode) {
notify('未找到可复制的授权码')
return
}
try {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(targetCode)
} else {
const input = document.createElement('input')
input.value = targetCode
document.body.appendChild(input)
input.select()
document.execCommand('copy')
document.body.removeChild(input)
}
notify('协议授权码已复制到剪贴板', 'success')
} catch (error) {
notify('复制失败,请手动选择复制')
}
}
const generateProtocolCode = async (item) => {
const actionText = item.configured ? '重新生成' : '生成'
try {
await ElMessageBox.confirm(
`确定为 ${protocolLabel(item.protocol)} ${actionText}专属授权码吗?旧授权码将立即失效并更新。`,
'安全提示',
{
confirmButtonText: '确定生成',
cancelButtonText: '取消',
type: 'warning',
}
)
} catch {
return return
} }
@ -90,114 +120,114 @@ export function usePage() {
protocol: item.protocol, protocol: item.protocol,
}) })
if (response.errorNo !== 0) { if (response.errorNo !== 0) {
throw new Error(response.errorMsg || '???????') throw new Error(response.errorMsg || '生成协议授权码失败')
} }
const newCode = response.data?.code || ''
const newCode = response?.data?.code || ''
generatedProtocol.value = item.protocol generatedProtocol.value = item.protocol
generatedProtocolCode.value = newCode generatedProtocolCode.value = newCode
item.configured = true
// ?????????? // 自动同步到已解密展示状态,无需再次密码验证
revealedCodes[item.protocol] = newCode revealedCodes[item.protocol] = newCode
codesRevealed.value = true codesRevealed.value = true
notify(`${protocolLabel(item.protocol)} ???????????`, 'success')
notify(`${protocolLabel(item.protocol)} 授权码已生成并持久保存`, 'success')
await loadProtocolCredentials()
} catch (error) { } catch (error) {
notify(error?.message || '???????') notify(error?.message || '生成协议授权码失败')
} finally { } finally {
protocolLoading.value = false protocolLoading.value = false
} }
} }
const copyProtocolCode = async (code) => { const revokeProtocolCode = async (item) => {
const targetCode = typeof code === 'string' && code ? code : generatedProtocolCode.value
if (!targetCode) return
try {
await navigator.clipboard.writeText(targetCode)
notify('??????????', 'success')
} catch {
notify('?????????????')
}
}
const revokeProtocolCode = async item => {
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm(
`??? ${protocolLabel(item.protocol)} ??????????????`, `确定撤销 ${protocolLabel(item.protocol)} 的授权码吗?撤销后客户端将无法使用该授权码登录。`,
'???????', '安全提示',
{ type: 'warning' }, {
confirmButtonText: '确定撤销',
cancelButtonText: '取消',
type: 'warning',
}
) )
protocolLoading.value = true } catch {
return
}
protocolLoading.value = true
try {
const response = await http.post('/api/v1/security/credentials/protocols/revoke', { const response = await http.post('/api/v1/security/credentials/protocols/revoke', {
protocol: item.protocol, protocol: item.protocol,
}) })
if (response.errorNo !== 0) { if (response.errorNo !== 0) {
throw new Error(response.errorMsg || '???????') throw new Error(response.errorMsg || '撤销协议授权码失败')
} }
item.configured = false delete revealedCodes[item.protocol]
if (generatedProtocol.value === item.protocol) { if (generatedProtocol.value === item.protocol) {
generatedProtocol.value = '' generatedProtocol.value = ''
generatedProtocolCode.value = '' generatedProtocolCode.value = ''
} }
delete revealedCodes[item.protocol] notify(`${protocolLabel(item.protocol)} 授权码已撤销`, 'success')
notify(`${protocolLabel(item.protocol)} ??????`, 'success') await loadProtocolCredentials()
} catch (error) { } catch (error) {
if (error !== 'cancel' && error !== 'close') { notify(error?.message || '撤销协议授权码失败')
notify(error?.message || '???????')
}
} finally { } finally {
protocolLoading.value = false protocolLoading.value = false
} }
} }
// ???????? // 密码验证弹窗控制
const openVerifyDialog = () => { const openVerifyDialog = () => {
verifyPassword.value = '' verifyPassword.value = ''
verifyDialogVisible.value = true verifyDialogVisible.value = true
} }
// ????????
const closeVerifyDialog = () => { const closeVerifyDialog = () => {
verifyPassword.value = ''
verifyDialogVisible.value = false verifyDialogVisible.value = false
verifyPassword.value = ''
} }
// ???????????????? // 提交登录密码验证并解密回显
const submitVerifyAndReveal = async () => { const submitVerifyAndReveal = async () => {
if (!verifyPassword.value.trim()) { const password = verifyPassword.value.trim()
notify('???????????????') if (!password) {
notify('请输入当前账号登录密码')
return return
} }
verifyLoading.value = true verifyLoading.value = true
try { try {
const response = await http.post('/api/v1/security/credentials/protocols/reveal', { const response = await http.post('/api/v1/security/credentials/protocols/reveal', {
password: verifyPassword.value, password,
}) })
if (response.errorNo !== 0) { if (response.errorNo !== 0) {
throw new Error(response.errorMsg || '????????') throw new Error(response.errorMsg || '密码验证失败,无法查看授权码')
} }
const data = response.data || {}
const codes = response.data?.codes || {}
Object.keys(revealedCodes).forEach(k => delete revealedCodes[k]) Object.keys(revealedCodes).forEach(k => delete revealedCodes[k])
Object.assign(revealedCodes, data) Object.assign(revealedCodes, codes)
codesRevealed.value = true codesRevealed.value = true
closeVerifyDialog() closeVerifyDialog()
notify('???????????????', 'success') notify('安全验证通过,授权码已解密显示', 'success')
} catch (error) { } catch (error) {
notify(error?.message || '????????') notify(error?.message || '验证失败,请检查密码是否正确')
} finally { } finally {
verifyLoading.value = false verifyLoading.value = false
} }
} }
// ??????????? // 安全隐藏授权码
const hideRevealedCodes = () => { const hideRevealedCodes = () => {
codesRevealed.value = false
Object.keys(revealedCodes).forEach(k => delete revealedCodes[k]) Object.keys(revealedCodes).forEach(k => delete revealedCodes[k])
generatedProtocolCode.value = '' codesRevealed.value = false
generatedProtocol.value = '' notify('已恢复安全隐藏保护', 'info')
notify('????????', 'success')
} }
onMounted(loadProtocolCredentials) onMounted(() => {
loadProtocolCredentials()
})
return { return {
protocolLoading, protocolLoading,
@ -219,4 +249,4 @@ export function usePage() {
submitVerifyAndReveal, submitVerifyAndReveal,
hideRevealedCodes, hideRevealedCodes,
} }
} }

View File

@ -3,9 +3,9 @@
<section class="authorization-section"> <section class="authorization-section">
<div class="authorization-header"> <div class="authorization-header">
<div class="header-main"> <div class="header-main">
<h3>???????</h3> <h3>邮件客户端协议</h3>
<p class="authorization-desc"> <p class="authorization-desc">
?? IMAP?POP3?SMTP ???????????????????????????? 为 IMAP、POP3、SMTP 配置专属授权码,平时安全隐藏,验证密码后可查看或复制。
</p> </p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
@ -16,7 +16,7 @@
size="small" size="small"
@click="openVerifyDialog" @click="openVerifyDialog"
> >
????????? 安全验证查看授权码
</el-button> </el-button>
<el-button <el-button
v-else-if="codesRevealed" v-else-if="codesRevealed"
@ -25,13 +25,13 @@
size="small" size="small"
@click="hideRevealedCodes" @click="hideRevealedCodes"
> >
??????? 安全隐藏授权码
</el-button> </el-button>
</div> </div>
</div> </div>
<el-alert <el-alert
title="???????????????????????????????????????????????????????????????" title="使用完整邮箱地址作为登录名,专属授权码作为密码。授权码平时处于加密保护状态,验证密码后可完整查看与一键复制。"
type="info" type="info"
:closable="false" :closable="false"
show-icon show-icon
@ -47,16 +47,16 @@
<div class="protocol-title"> <div class="protocol-title">
<strong>{{ protocolLabel(item.protocol) }}</strong> <strong>{{ protocolLabel(item.protocol) }}</strong>
<el-tag size="small" :type="item.enabled ? 'success' : 'info'"> <el-tag size="small" :type="item.enabled ? 'success' : 'info'">
{{ item.enabled ? '???' : '???' }} {{ item.enabled ? '已开启' : '已关闭' }}
</el-tag> </el-tag>
<el-tag size="small" :type="item.configured ? 'success' : 'warning'"> <el-tag size="small" :type="item.configured ? 'success' : 'warning'">
{{ item.configured ? '??????' : '??????' }} {{ item.configured ? '已配置授权码' : '未生成授权码' }}
</el-tag> </el-tag>
</div> </div>
<p class="protocol-guide">???????????????????????</p> <p class="protocol-guide">登录名:完整邮箱地址;密码:本协议专属授权码。</p>
<div class="protocol-credential-slot"> <div class="protocol-credential-slot">
<span class="slot-label">??????</span> <span class="slot-label">当前授权码:</span>
<template v-if="item.configured"> <template v-if="item.configured">
<template v-if="codesRevealed && revealedCodes[item.protocol]"> <template v-if="codesRevealed && revealedCodes[item.protocol]">
<code class="revealed-code">{{ revealedCodes[item.protocol] }}</code> <code class="revealed-code">{{ revealedCodes[item.protocol] }}</code>
@ -67,11 +67,11 @@
class="copy-btn" class="copy-btn"
@click="copyProtocolCode(revealedCodes[item.protocol])" @click="copyProtocolCode(revealedCodes[item.protocol])"
> >
?? 复制
</el-button> </el-button>
</template> </template>
<template v-else> <template v-else>
<span class="masked-code">????????????????</span> <span class="masked-code">••••••••••••••••</span>
<el-button <el-button
type="primary" type="primary"
link link
@ -79,12 +79,12 @@
class="reveal-btn" class="reveal-btn"
@click="openVerifyDialog" @click="openVerifyDialog"
> >
?????? 验证查看
</el-button> </el-button>
</template> </template>
</template> </template>
<template v-else> <template v-else>
<span class="unconfigured-tip">???????????????????</span> <span class="unconfigured-tip">尚未生成专属授权码,请点击右侧生成</span>
</template> </template>
</div> </div>
</div> </div>
@ -93,9 +93,9 @@
<el-switch <el-switch
:model-value="item.enabled" :model-value="item.enabled"
:loading="protocolLoading" :loading="protocolLoading"
active-text="??" active-text="开启"
inactive-text="??" inactive-text="关闭"
:aria-label="`${protocolLabel(item.protocol)} ?????????`" :aria-label="`${protocolLabel(item.protocol)} 当前账号客户端访问`"
@change="value => setProtocolEnabled(item, value)" @change="value => setProtocolEnabled(item, value)"
/> />
<el-button <el-button
@ -104,7 +104,7 @@
:disabled="!item.enabled" :disabled="!item.enabled"
@click="generateProtocolCode(item)" @click="generateProtocolCode(item)"
> >
{{ item.configured ? '???????' : '?????' }} {{ item.configured ? '重新生成授权码' : '生成授权码' }}
</el-button> </el-button>
<el-button <el-button
v-if="item.configured" v-if="item.configured"
@ -113,17 +113,17 @@
size="small" size="small"
@click="revokeProtocolCode(item)" @click="revokeProtocolCode(item)"
> >
????? 撤销授权码
</el-button> </el-button>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- ???????? --> <!-- 密码验证弹窗 -->
<el-dialog <el-dialog
v-model="verifyDialogVisible" v-model="verifyDialogVisible"
title="??????" title="安全身份验证"
width="min(440px, calc(100vw - 32px))" width="min(440px, calc(100vw - 32px))"
:close-on-click-modal="false" :close-on-click-modal="false"
class="verify-reveal-dialog" class="verify-reveal-dialog"
@ -131,15 +131,15 @@
> >
<div class="verify-dialog-body"> <div class="verify-dialog-body">
<p class="verify-dialog-tip"> <p class="verify-dialog-tip">
????????????????????????????????? 查看已持久保存的协议授权码前,请先验证当前账号的登录密码:
</p> </p>
<el-form @submit.prevent="submitVerifyAndReveal"> <el-form @submit.prevent="submitVerifyAndReveal">
<el-form-item label="??????"> <el-form-item label="登录密码">
<el-input <el-input
v-model="verifyPassword" v-model="verifyPassword"
type="password" type="password"
show-password show-password
placeholder="???????????" placeholder="请输入当前账号登录密码"
autocomplete="current-password" autocomplete="current-password"
@keyup.enter="submitVerifyAndReveal" @keyup.enter="submitVerifyAndReveal"
/> />
@ -148,14 +148,14 @@
</div> </div>
<template #footer> <template #footer>
<div class="verify-dialog-footer"> <div class="verify-dialog-footer">
<el-button @click="closeVerifyDialog">??</el-button> <el-button @click="closeVerifyDialog">取消</el-button>
<el-button <el-button
type="primary" type="primary"
:loading="verifyLoading" :loading="verifyLoading"
:disabled="!verifyPassword.trim()" :disabled="!verifyPassword.trim()"
@click="submitVerifyAndReveal" @click="submitVerifyAndReveal"
> >
??????? 验证并查看授权码
</el-button> </el-button>
</div> </div>
</template> </template>
@ -192,4 +192,4 @@ const hasConfiguredProtocols = computed(() =>
) )
</script> </script>
<style scoped src="./ThirdPartyAuthorization.css"></style> <style scoped src="./ThirdPartyAuthorization.css"></style>