From 9103da3ff3ed4e2f18774c953fb3276095ce2bc8 Mon Sep 17 00:00:00 2001 From: smail Date: Sat, 12 Sep 2026 14:18:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=82=AE=E4=BB=B6=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E5=8D=8F=E8=AE=AE=E6=8E=88=E6=9D=83=E7=A0=81?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=EF=BC=9A=E5=B9=B3=E6=97=B6=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E9=9A=90=E8=97=8F=E3=80=81=E5=AF=86=E7=A0=81=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E5=90=8E=E8=A7=A3=E5=AF=86=E5=9B=9E=E6=98=BE?= =?UTF-8?q?=E4=B8=8E=E4=B8=80=E9=94=AE=E5=A4=8D=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ThirdPartyAuthorization.css | 102 +++++++++--- .../ThirdPartyAuthorization.js | 127 +++++++++++--- .../ThirdPartyAuthorization.vue | 157 +++++++++++++++--- 3 files changed, 308 insertions(+), 78 deletions(-) diff --git a/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.css b/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.css index 24f7376..1626516 100644 --- a/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.css +++ b/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.css @@ -3,9 +3,18 @@ } .authorization-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; margin-bottom: 24px; } +.authorization-header .header-main { + flex: 1; + min-width: 0; +} + .authorization-header h3 { margin: 0 0 8px; color: var(--pm-text-primary); @@ -20,6 +29,12 @@ line-height: 1.7; } +.authorization-header .header-actions { + flex: none; + display: flex; + align-items: center; +} + .authorization-section > .el-alert { margin-bottom: 18px; } @@ -42,6 +57,7 @@ .protocol-info { min-width: 0; + flex: 1; } .protocol-title { @@ -56,12 +72,57 @@ font-size: 16px; } -.protocol-info p { - margin: 8px 0 0; +.protocol-info .protocol-guide { + margin: 6px 0 0; color: var(--pm-text-secondary); font-size: 13px; } +.protocol-credential-slot { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + margin-top: 10px; + padding: 8px 12px; + border-radius: var(--pm-radius-sm); + background: var(--pm-bg-primary); + border: 1px dashed var(--pm-border-color); + font-size: 13px; +} + +.slot-label { + color: var(--pm-text-secondary); + font-weight: 500; +} + +.masked-code { + letter-spacing: 2px; + color: var(--pm-text-secondary); + font-family: monospace; +} + +.revealed-code { + padding: 2px 6px; + 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-weight: 600; + letter-spacing: 1px; +} + +.reveal-btn, .copy-btn { + padding: 0 4px; + font-size: 13px; +} + +.unconfigured-tip { + color: var(--pm-text-placeholder, #909399); + font-style: italic; +} + .protocol-actions { display: flex; flex: none; @@ -71,36 +132,25 @@ gap: 8px; } -.protocol-code-panel { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 12px; - margin-top: 16px; - padding: 16px; - border: 1px solid var(--pm-primary-color); - border-radius: var(--pm-radius-sm); - background: color-mix(in srgb, var(--pm-primary-color) 7%, var(--pm-bg-primary)); -} - -.protocol-code-panel .one-time-tip { - width: 100%; +.verify-dialog-tip { + margin: 0 0 16px; color: var(--pm-text-secondary); - font-size: 13px; + font-size: 14px; + line-height: 1.6; } -.protocol-code-panel code { - padding: 8px 12px; - border-radius: var(--pm-radius-sm); - background: var(--pm-bg-secondary); - color: var(--pm-text-primary); - font-size: 16px; - font-weight: 700; - letter-spacing: 1px; - word-break: break-all; +.verify-dialog-footer { + display: flex; + justify-content: flex-end; + gap: 12px; } @media (max-width: 720px) { + .authorization-header { + flex-direction: column; + align-items: stretch; + } + .protocol-item { align-items: stretch; flex-direction: column; diff --git a/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.js b/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.js index 212ef63..2efee64 100644 --- a/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.js +++ b/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.js @@ -1,4 +1,4 @@ -import { onMounted, ref } from 'vue' +import { onMounted, reactive, ref } from 'vue' import { ElMessageBox, ElNotification } from 'element-plus' @@ -10,9 +10,19 @@ export function usePage() { const generatedProtocolCode = ref('') const generatedProtocol = ref('') + // ??????????{ imap: 'xxxx', pop3: 'yyyy', smtp: 'zzzz' } + const revealedCodes = reactive({}) + // ??????????????? + const codesRevealed = ref(false) + + // ???????? + const verifyDialogVisible = ref(false) + const verifyPassword = ref('') + const verifyLoading = ref(false) + const notify = (message, type = 'error') => { ElNotification({ - title: type === 'success' ? '操作成功' : '操作失败', + title: type === 'success' ? '????' : '????', message, type, }) @@ -29,11 +39,11 @@ export function usePage() { try { const response = await http.get('/api/v1/security/credentials/protocols') if (response.errorNo !== 0) { - throw new Error(response.errorMsg || '读取邮件客户端协议设置失败') + throw new Error(response.errorMsg || '?????????????') } protocolCredentials.value = Array.isArray(response.data) ? response.data : [] } catch (error) { - notify(error?.message || '读取邮件客户端协议设置失败') + notify(error?.message || '?????????????') } finally { protocolLoading.value = false } @@ -50,16 +60,19 @@ export function usePage() { enabled, }) if (response.errorNo !== 0) { - throw new Error(response.errorMsg || '协议设置失败') + throw new Error(response.errorMsg || '??????') } - if (!enabled && generatedProtocol.value === item.protocol) { - generatedProtocol.value = '' - generatedProtocolCode.value = '' + if (!enabled) { + if (generatedProtocol.value === item.protocol) { + generatedProtocol.value = '' + generatedProtocolCode.value = '' + } + delete revealedCodes[item.protocol] } - notify(`${protocolLabel(item.protocol)} 当前账号客户端访问已${enabled ? '开启' : '关闭'}`, 'success') + notify(`${protocolLabel(item.protocol)} ??????????${enabled ? '??' : '??'}`, 'success') } catch (error) { item.enabled = previous - notify(error?.message || '协议设置失败') + notify(error?.message || '??????') } finally { protocolLoading.value = false } @@ -67,7 +80,7 @@ export function usePage() { const generateProtocolCode = async item => { if (!item.enabled) { - notify(`请先开启 ${protocolLabel(item.protocol)} 协议`) + notify(`???? ${protocolLabel(item.protocol)} ??`) return } @@ -77,34 +90,39 @@ export function usePage() { protocol: item.protocol, }) if (response.errorNo !== 0) { - throw new Error(response.errorMsg || '生成授权码失败') + throw new Error(response.errorMsg || '???????') } + const newCode = response.data?.code || '' generatedProtocol.value = item.protocol - generatedProtocolCode.value = response.data?.code || '' + generatedProtocolCode.value = newCode item.configured = true - notify(`${protocolLabel(item.protocol)} 授权码已生成,请立即保存`, 'success') + // ?????????? + revealedCodes[item.protocol] = newCode + codesRevealed.value = true + notify(`${protocolLabel(item.protocol)} ???????????`, 'success') } catch (error) { - notify(error?.message || '生成授权码失败') + notify(error?.message || '???????') } finally { protocolLoading.value = false } } - const copyProtocolCode = async () => { - if (!generatedProtocolCode.value) return + const copyProtocolCode = async (code) => { + const targetCode = typeof code === 'string' && code ? code : generatedProtocolCode.value + if (!targetCode) return try { - await navigator.clipboard.writeText(generatedProtocolCode.value) - notify('授权码已复制', 'success') + await navigator.clipboard.writeText(targetCode) + notify('??????????', 'success') } catch { - notify('复制失败,请手动复制授权码') + notify('?????????????') } } const revokeProtocolCode = async item => { try { await ElMessageBox.confirm( - `撤销后 ${protocolLabel(item.protocol)} 授权码将立即失效,是否继续?`, - '撤销协议授权码', + `??? ${protocolLabel(item.protocol)} ??????????????`, + '???????', { type: 'warning' }, ) protocolLoading.value = true @@ -112,23 +130,73 @@ export function usePage() { protocol: item.protocol, }) if (response.errorNo !== 0) { - throw new Error(response.errorMsg || '撤销授权码失败') + throw new Error(response.errorMsg || '???????') } item.configured = false if (generatedProtocol.value === item.protocol) { generatedProtocol.value = '' generatedProtocolCode.value = '' } - notify(`${protocolLabel(item.protocol)} 授权码已撤销`, 'success') + delete revealedCodes[item.protocol] + notify(`${protocolLabel(item.protocol)} ??????`, 'success') } catch (error) { if (error !== 'cancel' && error !== 'close') { - notify(error?.message || '撤销授权码失败') + notify(error?.message || '???????') } } finally { protocolLoading.value = false } } + // ???????? + const openVerifyDialog = () => { + verifyPassword.value = '' + verifyDialogVisible.value = true + } + + // ???????? + const closeVerifyDialog = () => { + verifyPassword.value = '' + verifyDialogVisible.value = false + } + + // ???????????????? + const submitVerifyAndReveal = async () => { + if (!verifyPassword.value.trim()) { + notify('???????????????') + return + } + + verifyLoading.value = true + try { + const response = await http.post('/api/v1/security/credentials/protocols/reveal', { + password: verifyPassword.value, + }) + if (response.errorNo !== 0) { + throw new Error(response.errorMsg || '????????') + } + const data = response.data || {} + Object.keys(revealedCodes).forEach(k => delete revealedCodes[k]) + Object.assign(revealedCodes, data) + codesRevealed.value = true + closeVerifyDialog() + notify('???????????????', 'success') + } catch (error) { + notify(error?.message || '????????') + } finally { + verifyLoading.value = false + } + } + + // ??????????? + const hideRevealedCodes = () => { + codesRevealed.value = false + Object.keys(revealedCodes).forEach(k => delete revealedCodes[k]) + generatedProtocolCode.value = '' + generatedProtocol.value = '' + notify('????????', 'success') + } + onMounted(loadProtocolCredentials) return { @@ -136,10 +204,19 @@ export function usePage() { protocolCredentials, generatedProtocolCode, generatedProtocol, + revealedCodes, + codesRevealed, + verifyDialogVisible, + verifyPassword, + verifyLoading, protocolLabel, setProtocolEnabled, generateProtocolCode, copyProtocolCode, revokeProtocolCode, + openVerifyDialog, + closeVerifyDialog, + submitVerifyAndReveal, + hideRevealedCodes, } } diff --git a/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.vue b/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.vue index 7f1b855..60e0e3c 100644 --- a/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.vue +++ b/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.vue @@ -2,41 +2,100 @@
-

邮件客户端协议

-

- 本地服务不依赖 SMTP 中继;开关只控制当前账号。 -

+
+

???????

+

+ ?? IMAP?POP3?SMTP ???????????????????????????? +

+
+
+ + ????????? + + + ??????? + +
-
+
{{ protocolLabel(item.protocol) }} - {{ item.enabled ? '已开启' : '已关闭' }} + {{ item.enabled ? '???' : '???' }} - {{ item.configured ? '已配置授权码' : '未生成授权码' }} + {{ item.configured ? '??????' : '??????' }}
-

登录名:完整邮箱地址;密码:本协议授权码。

+

???????????????????????

+ +
+ ?????? + + +
- {{ item.configured ? '重新生成授权码' : '生成授权码' }} + {{ item.configured ? '???????' : '?????' }} - 撤销授权码 + ?????
- -
-
- {{ protocolLabel(generatedProtocol) }} 授权码仅本次显示,请复制保存: -
- {{ generatedProtocolCode }} - - 复制授权码 - -
+ + + +
+

+ ????????????????????????????????? +

+ + + + + +
+ +