更新外观模块

This commit is contained in:
super
2026-05-06 22:41:26 +08:00
parent a00ff1804c
commit 8cd1a4e2b1
24 changed files with 1378 additions and 175 deletions

View File

@@ -0,0 +1,337 @@
<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="560px" class="secret-settings-dialog" :append-to-body="true">
<template #header>
<div class="dialog-header">
<div class="dialog-title">设置</div>
<div class="dialog-subtitle">按当前登录用户保存在本机可分别管理外观专利和货源查询密钥</div>
</div>
</template>
<div class="secret-settings-body">
<section v-for="item in moduleStates" :key="item.moduleKey" class="secret-card">
<div class="secret-card-head">
<div>
<div class="secret-card-title">{{ item.title }}</div>
<div class="secret-card-desc">{{ item.description }}</div>
</div>
<button
v-if="item.exists"
type="button"
class="link-danger"
@click="clearSecret(item.moduleKey)"
>
清空
</button>
</div>
<input
v-model="item.value"
class="secret-input"
type="password"
:placeholder="`请输入${item.title}密钥`"
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="item.retention" type="radio" :value="option.value" />
<span>{{ option.label }}</span>
</label>
</div>
</div>
<div class="secret-meta">
<span v-if="item.exists">{{ formatRetentionText(item.retention, item.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 ModuleState = {
moduleKey: ApiSecretModuleKey
title: string
description: string
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 moduleStates = ref<ModuleState[]>([])
function buildModuleState(
moduleKey: ApiSecretModuleKey,
title: string,
description: string,
): ModuleState {
const snapshot = getStoredApiSecretSnapshot(moduleKey)
return {
moduleKey,
title,
description,
value: snapshot.value,
retention: snapshot.retention,
expiresAt: snapshot.expiresAt,
exists: snapshot.exists,
}
}
function loadStates() {
moduleStates.value = [
buildModuleState('appearance-patent', '外观专利', '用于外观专利检测的 Coze 接口密钥。'),
buildModuleState('similar-asin', '货源查询', '用于货源查询的 Coze 接口密钥。'),
]
}
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 item of moduleStates.value) {
saveStoredApiSecret(item.moduleKey, item.value, item.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;
}
.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;
}
.secret-card {
padding: 16px;
border: 1px solid #2f363f;
border-radius: 12px;
background: #22272d;
}
.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: #5aa2ea;
color: #fff;
}
</style>