408 lines
9.3 KiB
Vue
408 lines
9.3 KiB
Vue
<template>
|
||
<div class="secret-settings-entry">
|
||
<button type="button" class="secret-settings-trigger" @click="dialogVisible = true">
|
||
<span class="secret-settings-icon" aria-hidden="true">◎</span>
|
||
<span>密钥设置</span>
|
||
</button>
|
||
|
||
<el-dialog v-model="dialogVisible" width="620px" class="secret-settings-dialog" :append-to-body="true">
|
||
<template #header>
|
||
<div class="dialog-header">
|
||
<div class="dialog-title">密钥设置</div>
|
||
<div class="dialog-subtitle">按当前登录用户保存在本机,外观专利和货源查询分别使用独立的 Coze 接口密钥。</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="secret-settings-body">
|
||
<section v-for="config in secretConfigs" :key="config.key" class="secret-card">
|
||
<div class="secret-card-head">
|
||
<div>
|
||
<div class="secret-card-title">{{ config.title }}</div>
|
||
<div class="secret-card-desc">{{ config.description }}</div>
|
||
</div>
|
||
<button
|
||
v-if="secretStates[config.key].exists"
|
||
type="button"
|
||
class="link-danger"
|
||
@click="clearSecret(config.key)"
|
||
>
|
||
清空
|
||
</button>
|
||
</div>
|
||
|
||
<input
|
||
v-model="secretStates[config.key].value"
|
||
class="secret-input"
|
||
type="password"
|
||
:placeholder="config.placeholder"
|
||
autocomplete="off"
|
||
spellcheck="false"
|
||
/>
|
||
|
||
<div class="retention-block">
|
||
<div class="retention-label">保留时长</div>
|
||
<div class="retention-options">
|
||
<label v-for="option in retentionOptions" :key="option.value" class="retention-option">
|
||
<input v-model="secretStates[config.key].retention" type="radio" :value="option.value" />
|
||
<span>{{ option.label }}</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="secret-meta">
|
||
<span v-if="secretStates[config.key].exists">
|
||
{{ formatRetentionText(secretStates[config.key].retention, secretStates[config.key].expiresAt) }}
|
||
</span>
|
||
<span v-else>当前未保存</span>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<button type="button" class="footer-btn footer-btn-ghost" @click="dialogVisible = false">取消</button>
|
||
<button type="button" class="footer-btn footer-btn-primary" @click="saveAll">保存</button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, watch } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import {
|
||
clearStoredApiSecret,
|
||
getStoredApiSecretSnapshot,
|
||
saveStoredApiSecret,
|
||
type ApiSecretModuleKey,
|
||
type ApiSecretRetention,
|
||
} from '@/shared/utils/api-secret-store'
|
||
|
||
type SecretState = {
|
||
value: string
|
||
retention: ApiSecretRetention
|
||
expiresAt: number | null
|
||
exists: boolean
|
||
}
|
||
|
||
const dialogVisible = ref(false)
|
||
|
||
const retentionOptions: Array<{ value: ApiSecretRetention; label: string }> = [
|
||
{ value: 'session', label: '本次打开有效' },
|
||
{ value: '1d', label: '1 天' },
|
||
{ value: '7d', label: '7 天' },
|
||
{ value: '30d', label: '30 天' },
|
||
{ value: 'forever', label: '长期保留' },
|
||
]
|
||
|
||
const secretConfigs: Array<{ key: ApiSecretModuleKey; title: string; description: string; placeholder: string }> = [
|
||
{
|
||
key: 'appearance-patent',
|
||
title: '外观专利密钥',
|
||
description: '仅用于外观专利检测。',
|
||
placeholder: '请输入 Coze 接口密钥',
|
||
},
|
||
{
|
||
key: 'appearance-patent-token',
|
||
title: '专利汇令牌',
|
||
description: '仅用于外观专利检测,非必填。',
|
||
placeholder: '请输入专利汇令牌,可留空',
|
||
},
|
||
{
|
||
key: 'similar-asin',
|
||
title: '货源查询密钥',
|
||
description: '仅用于货源查询。',
|
||
placeholder: '请输入 Coze 接口密钥',
|
||
},
|
||
]
|
||
|
||
const secretStates = ref<Record<ApiSecretModuleKey, SecretState>>({
|
||
'appearance-patent': emptySecretState(),
|
||
'appearance-patent-token': emptySecretState(),
|
||
'similar-asin': emptySecretState(),
|
||
})
|
||
|
||
function loadStates() {
|
||
const nextStates = { ...secretStates.value }
|
||
for (const config of secretConfigs) {
|
||
const snapshot = getStoredApiSecretSnapshot(config.key)
|
||
nextStates[config.key] = {
|
||
value: snapshot.value,
|
||
retention: snapshot.retention,
|
||
expiresAt: snapshot.expiresAt,
|
||
exists: snapshot.exists,
|
||
}
|
||
}
|
||
secretStates.value = nextStates
|
||
}
|
||
|
||
function emptySecretState(): SecretState {
|
||
return {
|
||
value: '',
|
||
retention: 'session',
|
||
expiresAt: null,
|
||
exists: false,
|
||
}
|
||
}
|
||
|
||
function formatRetentionText(retention: ApiSecretRetention, expiresAt: number | null) {
|
||
if (retention === 'session') return '关闭软件后自动清空'
|
||
if (retention === 'forever') return '长期保留,直到手动清空'
|
||
if (!expiresAt) return '已保存'
|
||
const date = new Date(expiresAt)
|
||
const year = date.getFullYear()
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
const hour = String(date.getHours()).padStart(2, '0')
|
||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||
return `有效期至 ${year}-${month}-${day} ${hour}:${minute}`
|
||
}
|
||
|
||
function clearSecret(moduleKey: ApiSecretModuleKey) {
|
||
clearStoredApiSecret(moduleKey)
|
||
loadStates()
|
||
ElMessage.success('已清空密钥')
|
||
}
|
||
|
||
function saveAll() {
|
||
for (const config of secretConfigs) {
|
||
const state = secretStates.value[config.key]
|
||
saveStoredApiSecret(config.key, state.value, state.retention)
|
||
}
|
||
loadStates()
|
||
dialogVisible.value = false
|
||
ElMessage.success('密钥设置已保存')
|
||
}
|
||
|
||
watch(dialogVisible, (visible) => {
|
||
if (visible) {
|
||
loadStates()
|
||
}
|
||
})
|
||
|
||
loadStates()
|
||
</script>
|
||
|
||
<style scoped>
|
||
.secret-settings-entry {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.secret-settings-trigger {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
height: 38px;
|
||
padding: 0 14px;
|
||
border: 1px solid #3c4a58;
|
||
border-radius: 10px;
|
||
background: linear-gradient(180deg, #2b3239 0%, #242a31 100%);
|
||
color: #e7edf4;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.secret-settings-trigger:hover {
|
||
border-color: #4c647d;
|
||
}
|
||
|
||
.secret-settings-icon {
|
||
color: #8dc4ff;
|
||
font-size: 14px;
|
||
line-height: 1;
|
||
}
|
||
|
||
:global(.secret-settings-dialog) {
|
||
--el-dialog-bg-color: #171b20;
|
||
--el-dialog-padding-primary: 20px;
|
||
overflow: hidden;
|
||
border: 1px solid #2c3540;
|
||
border-radius: 14px;
|
||
background: #171b20;
|
||
box-shadow: 0 24px 70px rgba(0, 0, 0, .52);
|
||
}
|
||
|
||
:global(.secret-settings-dialog .el-dialog__header) {
|
||
padding: 22px 22px 12px;
|
||
margin: 0;
|
||
background: #171b20;
|
||
}
|
||
|
||
:global(.secret-settings-dialog .el-dialog__body) {
|
||
padding: 12px 22px 16px;
|
||
background: #171b20;
|
||
}
|
||
|
||
:global(.secret-settings-dialog .el-dialog__footer) {
|
||
padding: 0 22px 22px;
|
||
background: #171b20;
|
||
}
|
||
|
||
:global(.secret-settings-dialog .el-dialog__headerbtn) {
|
||
top: 18px;
|
||
right: 18px;
|
||
}
|
||
|
||
:global(.secret-settings-dialog .el-dialog__close) {
|
||
color: #8793a0;
|
||
}
|
||
|
||
:global(.secret-settings-dialog .el-dialog__close:hover) {
|
||
color: #d8e3ee;
|
||
}
|
||
|
||
.dialog-header {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.dialog-title {
|
||
color: #f5f7fa;
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.dialog-subtitle {
|
||
color: #8f9aa7;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.secret-settings-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
max-height: 62vh;
|
||
overflow-y: auto;
|
||
padding-right: 4px;
|
||
}
|
||
|
||
.secret-card {
|
||
padding: 18px;
|
||
border: 1px solid #313b46;
|
||
border-radius: 10px;
|
||
background: #20252b;
|
||
}
|
||
|
||
.secret-card-head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.secret-card-title {
|
||
color: #eef4fb;
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.secret-card-desc {
|
||
margin-top: 4px;
|
||
color: #909ba8;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.secret-input {
|
||
width: 100%;
|
||
height: 42px;
|
||
padding: 0 12px;
|
||
box-sizing: border-box;
|
||
border: 1px solid #3b4652;
|
||
border-radius: 10px;
|
||
background: #1b2026;
|
||
color: #dce6f0;
|
||
font-size: 13px;
|
||
outline: none;
|
||
}
|
||
|
||
.secret-input:focus {
|
||
border-color: #5b96d6;
|
||
}
|
||
|
||
.retention-block {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.retention-label {
|
||
margin-bottom: 8px;
|
||
color: #a3afbb;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.retention-options {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.retention-option {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 10px;
|
||
border-radius: 999px;
|
||
background: #1b2026;
|
||
color: #dce4ec;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.retention-option input {
|
||
margin: 0;
|
||
}
|
||
|
||
.secret-meta {
|
||
margin-top: 10px;
|
||
color: #7f8a96;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.link-danger {
|
||
border: none;
|
||
background: transparent;
|
||
color: #ff9b9b;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
}
|
||
|
||
.dialog-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
}
|
||
|
||
.footer-btn {
|
||
height: 38px;
|
||
padding: 0 18px;
|
||
border-radius: 10px;
|
||
border: 1px solid transparent;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.footer-btn-ghost {
|
||
border-color: #3a4653;
|
||
background: #232a31;
|
||
color: #d9e2eb;
|
||
}
|
||
|
||
.footer-btn-primary {
|
||
background: #4f8fda;
|
||
color: #fff;
|
||
}
|
||
|
||
.footer-btn-primary:hover {
|
||
background: #67a6ed;
|
||
}
|
||
</style>
|