feat(密钥): 用户 API 密钥服务端化——V115 按账号绑定存储 + 后台密钥管理页 + 桌面端全站拦截与配置引导
- 新增 usersecret 模块:外观专利/货源查询密钥从本地 localStorage 迁移至 biz_user_api_secret(AES 加密、按 uid 绑定) - 后台「密钥管理」页:脱敏展示、立即检测、清空;每日 04:30 分布式锁定时巡检 - 桌面端:密钥设置面板走服务端、未配齐引导 /setup-secrets、代理余量展示 - 删除专利汇令牌全链路与密钥保留时长选择器
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<div class="setup-root">
|
||||
<header class="setup-header">
|
||||
<span class="setup-title">数富AI</span>
|
||||
<div class="setup-header-right">
|
||||
<span class="username-text">{{ username || '未登录' }}</span>
|
||||
<router-link to="/login?logout=1" class="logout-link">退出</router-link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="setup-main">
|
||||
<div class="setup-card">
|
||||
<div class="setup-card-title">完成密钥配置后即可使用</div>
|
||||
<div class="setup-card-desc">
|
||||
密钥保存在服务端并绑定当前账号,换设备登录后自动同步。
|
||||
请填写以下密钥并保存,检测通过后即可进入工具台。
|
||||
</div>
|
||||
|
||||
<ApiSecretSettingsPanel ref="panelRef" />
|
||||
|
||||
<div class="setup-actions">
|
||||
<button type="button" class="setup-submit" :disabled="submitting" @click="submit">
|
||||
{{ submitting ? '校验中...' : '保存并进入' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="setup-hint">{{ hint }}</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import ApiSecretSettingsPanel from '@/shared/components/ApiSecretSettingsPanel.vue'
|
||||
import {
|
||||
checkApiSecret,
|
||||
getStoredApiSecretSnapshot,
|
||||
listApiSecretModules,
|
||||
loadApiSecrets,
|
||||
type ApiSecretModuleKey,
|
||||
} from '@/shared/utils/api-secret-store'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const panelRef = ref<InstanceType<typeof ApiSecretSettingsPanel> | null>(null)
|
||||
const submitting = ref(false)
|
||||
const username = ref('')
|
||||
const hint = ref('保存后系统会自动检测密钥连通性;检测未通过的密钥需要修正后重试。')
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
username.value = window.localStorage.getItem('username') || ''
|
||||
} catch {
|
||||
/* 忽略存储异常 */
|
||||
}
|
||||
})
|
||||
|
||||
/** 对已保存但尚未检测出结果的必填模块补一次检测,避免"已配置却因未检测被拦"。 */
|
||||
async function ensureAllChecked() {
|
||||
for (const module of listApiSecretModules()) {
|
||||
const moduleKey = module.moduleKey as ApiSecretModuleKey
|
||||
const snapshot = getStoredApiSecretSnapshot(moduleKey)
|
||||
if (!snapshot.exists) continue
|
||||
if (snapshot.checkStatus === 'passed' || snapshot.checkStatus === 'error') continue
|
||||
try {
|
||||
await checkApiSecret(moduleKey)
|
||||
} catch (error) {
|
||||
console.warn('[api-secret] 补齐检测失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (submitting.value) return
|
||||
const panel = panelRef.value
|
||||
if (!panel) return
|
||||
submitting.value = true
|
||||
try {
|
||||
const saved = await panel.saveAll({ requireAll: true })
|
||||
if (!saved) return
|
||||
|
||||
await ensureAllChecked()
|
||||
const state = await loadApiSecrets({ force: true })
|
||||
if (state === 'incomplete') {
|
||||
const failed = listApiSecretModules()
|
||||
.map((module) => getStoredApiSecretSnapshot(module.moduleKey as ApiSecretModuleKey))
|
||||
.filter((snapshot) => !snapshot.exists || (snapshot.checkStatus !== 'passed' && snapshot.checkStatus !== 'error'))
|
||||
hint.value = `以下密钥尚未通过检测:${failed.map((item) => item.masked || '未配置').join('、')},请点击「检测」确认。`
|
||||
ElMessage.warning('密钥尚未全部检测通过,请逐项检测确认')
|
||||
return
|
||||
}
|
||||
const redirect = typeof route.query.redirect === 'string' && route.query.redirect.startsWith('/')
|
||||
? route.query.redirect
|
||||
: '/home'
|
||||
console.log('[api-secret] 密钥配置完成,进入', redirect)
|
||||
await router.replace(redirect)
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.setup-root {
|
||||
min-height: 100vh;
|
||||
background: #12161a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.setup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 56px;
|
||||
padding: 0 24px;
|
||||
background: #171b20;
|
||||
border-bottom: 1px solid #262d35;
|
||||
}
|
||||
|
||||
.setup-title {
|
||||
color: #f2f6fa;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.setup-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.username-text {
|
||||
color: #9aa6b3;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.logout-link {
|
||||
color: #8dc4ff;
|
||||
font-size: 13px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logout-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.setup-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 40px 16px 64px;
|
||||
}
|
||||
|
||||
.setup-card {
|
||||
width: 640px;
|
||||
max-width: 100%;
|
||||
padding: 26px;
|
||||
border: 1px solid #2c3540;
|
||||
border-radius: 16px;
|
||||
background: #171b20;
|
||||
box-shadow: 0 24px 70px rgba(0, 0, 0, .45);
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.setup-card-title {
|
||||
color: #f5f7fa;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.setup-card-desc {
|
||||
margin: 8px 0 18px;
|
||||
color: #8f9aa7;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.setup-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.setup-submit {
|
||||
min-width: 132px;
|
||||
height: 40px;
|
||||
padding: 0 20px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: #4f8fda;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.setup-submit:hover:not(:disabled) {
|
||||
background: #67a6ed;
|
||||
}
|
||||
|
||||
.setup-submit:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: .58;
|
||||
}
|
||||
|
||||
.setup-hint {
|
||||
margin-top: 12px;
|
||||
color: #7f8a96;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user