修改完善这个专利部分
This commit is contained in:
@@ -8,33 +8,33 @@
|
||||
<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 class="dialog-title">密钥设置</div>
|
||||
<div class="dialog-subtitle">按当前登录用户保存在本机,外观专利和货源查询共用同一个 Coze 接口密钥。</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="secret-settings-body">
|
||||
<section v-for="item in moduleStates" :key="item.moduleKey" class="secret-card">
|
||||
<section 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 class="secret-card-title">通用 Coze 密钥</div>
|
||||
<div class="secret-card-desc">用于外观专利检测和货源查询。</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="item.exists"
|
||||
v-if="secretState.exists"
|
||||
type="button"
|
||||
class="link-danger"
|
||||
@click="clearSecret(item.moduleKey)"
|
||||
@click="clearSecret"
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
v-model="item.value"
|
||||
v-model="secretState.value"
|
||||
class="secret-input"
|
||||
type="password"
|
||||
:placeholder="`请输入${item.title}密钥`"
|
||||
placeholder="请输入 Coze 接口密钥"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
@@ -43,14 +43,14 @@
|
||||
<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" />
|
||||
<input v-model="secretState.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-if="secretState.exists">{{ formatRetentionText(secretState.retention, secretState.expiresAt) }}</span>
|
||||
<span v-else>当前未保存</span>
|
||||
</div>
|
||||
</section>
|
||||
@@ -73,14 +73,10 @@ import {
|
||||
clearStoredApiSecret,
|
||||
getStoredApiSecretSnapshot,
|
||||
saveStoredApiSecret,
|
||||
type ApiSecretModuleKey,
|
||||
type ApiSecretRetention,
|
||||
} from '@/shared/utils/api-secret-store'
|
||||
|
||||
type ModuleState = {
|
||||
moduleKey: ApiSecretModuleKey
|
||||
title: string
|
||||
description: string
|
||||
type SecretState = {
|
||||
value: string
|
||||
retention: ApiSecretRetention
|
||||
expiresAt: number | null
|
||||
@@ -90,25 +86,18 @@ type ModuleState = {
|
||||
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: 'session', label: '本次打开有效' },
|
||||
{ value: '1d', label: '1 天' },
|
||||
{ value: '7d', label: '7 天' },
|
||||
{ value: '30d', label: '30 天' },
|
||||
{ value: 'forever', label: '长期保留' },
|
||||
]
|
||||
|
||||
const moduleStates = ref<ModuleState[]>([])
|
||||
const secretState = ref<SecretState>(emptySecretState())
|
||||
|
||||
function buildModuleState(
|
||||
moduleKey: ApiSecretModuleKey,
|
||||
title: string,
|
||||
description: string,
|
||||
): ModuleState {
|
||||
const snapshot = getStoredApiSecretSnapshot(moduleKey)
|
||||
return {
|
||||
moduleKey,
|
||||
title,
|
||||
description,
|
||||
function loadStates() {
|
||||
const snapshot = getStoredApiSecretSnapshot()
|
||||
secretState.value = {
|
||||
value: snapshot.value,
|
||||
retention: snapshot.retention,
|
||||
expiresAt: snapshot.expiresAt,
|
||||
@@ -116,11 +105,13 @@ function buildModuleState(
|
||||
}
|
||||
}
|
||||
|
||||
function loadStates() {
|
||||
moduleStates.value = [
|
||||
buildModuleState('appearance-patent', '外观专利', '用于外观专利检测的 Coze 接口密钥。'),
|
||||
buildModuleState('similar-asin', '货源查询', '用于货源查询的 Coze 接口密钥。'),
|
||||
]
|
||||
function emptySecretState(): SecretState {
|
||||
return {
|
||||
value: '',
|
||||
retention: 'session',
|
||||
expiresAt: null,
|
||||
exists: false,
|
||||
}
|
||||
}
|
||||
|
||||
function formatRetentionText(retention: ApiSecretRetention, expiresAt: number | null) {
|
||||
@@ -136,16 +127,14 @@ function formatRetentionText(retention: ApiSecretRetention, expiresAt: number |
|
||||
return `有效期至 ${year}-${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
function clearSecret(moduleKey: ApiSecretModuleKey) {
|
||||
clearStoredApiSecret(moduleKey)
|
||||
function clearSecret() {
|
||||
clearStoredApiSecret()
|
||||
loadStates()
|
||||
ElMessage.success('已清空密钥')
|
||||
}
|
||||
|
||||
function saveAll() {
|
||||
for (const item of moduleStates.value) {
|
||||
saveStoredApiSecret(item.moduleKey, item.value, item.retention)
|
||||
}
|
||||
saveStoredApiSecret(secretState.value.value, secretState.value.retention)
|
||||
loadStates()
|
||||
dialogVisible.value = false
|
||||
ElMessage.success('密钥设置已保存')
|
||||
@@ -192,6 +181,45 @@ loadStates()
|
||||
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;
|
||||
@@ -217,10 +245,10 @@ loadStates()
|
||||
}
|
||||
|
||||
.secret-card {
|
||||
padding: 16px;
|
||||
border: 1px solid #2f363f;
|
||||
border-radius: 12px;
|
||||
background: #22272d;
|
||||
padding: 18px;
|
||||
border: 1px solid #313b46;
|
||||
border-radius: 10px;
|
||||
background: #20252b;
|
||||
}
|
||||
|
||||
.secret-card-head {
|
||||
@@ -331,7 +359,11 @@ loadStates()
|
||||
}
|
||||
|
||||
.footer-btn-primary {
|
||||
background: #5aa2ea;
|
||||
background: #4f8fda;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.footer-btn-primary:hover {
|
||||
background: #67a6ed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,10 +18,8 @@
|
||||
</div>
|
||||
|
||||
<div class="prompt-card">
|
||||
<div class="section-title">AI 提示词</div>
|
||||
<textarea v-model="aiPrompt" class="prompt-input" rows="8" placeholder="可选,留空时使用下方默认提示词" />
|
||||
<div class="prompt-default-label">留空时默认使用以下提示词</div>
|
||||
<div class="prompt-preview">{{ defaultAiPrompt }}</div>
|
||||
<div class="section-title">新增条件要求</div>
|
||||
<textarea v-model="aiPrompt" class="prompt-input" rows="8" placeholder="可选,填写后会追加到默认检测提示词中" />
|
||||
</div>
|
||||
|
||||
<div class="run-row">
|
||||
@@ -251,11 +249,13 @@ function mergeCurrentTaskItem(item: AppearancePatentHistoryItem) {
|
||||
}
|
||||
|
||||
function effectiveAiPrompt() {
|
||||
return aiPrompt.value.trim() || defaultAiPrompt
|
||||
const extraPrompt = aiPrompt.value.trim()
|
||||
if (!extraPrompt) return defaultAiPrompt
|
||||
return `${defaultAiPrompt}\n\n新增条件要求:\n${extraPrompt}`
|
||||
}
|
||||
|
||||
function effectiveCozeApiKey() {
|
||||
return getStoredApiSecret('appearance-patent').trim()
|
||||
return getStoredApiSecret().trim()
|
||||
}
|
||||
|
||||
function maskSecret(secret: string) {
|
||||
@@ -937,8 +937,6 @@ onUnmounted(() => {
|
||||
.prompt-input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 180px; padding: 10px 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; line-height: 1.6; outline: none; }
|
||||
.secret-input { width: 100%; box-sizing: border-box; height: 38px; padding: 0 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; outline: none; }
|
||||
.prompt-input:focus, .secret-input:focus { border-color: #3498db; }
|
||||
.prompt-default-label { margin-top: 10px; color: #8d8d8d; font-size: 12px; }
|
||||
.prompt-preview { margin-top: 10px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #9ea7b3; font-size: 12px; line-height: 1.6; white-space: pre-wrap; }
|
||||
.parse-card, .queue-payload { margin-top: 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #b8c1cc; font-size: 12px; }
|
||||
.queue-payload { max-height: 220px; overflow: auto; color: #8fd3ff; white-space: pre-wrap; }
|
||||
.panel-header { padding: 16px 20px; border-bottom: 1px solid #2a2a2a; font-size: 15px; font-weight: 600; color: #ddd; }
|
||||
|
||||
@@ -44,29 +44,6 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="category-card">
|
||||
<div class="category-card-head">
|
||||
<div class="section-title">商品类目</div>
|
||||
<div class="category-actions">
|
||||
<button type="button" class="opt-btn" @click="openCategoryDialog">选择类目</button>
|
||||
<button
|
||||
v-if="selectedProductCategoryIds.length"
|
||||
type="button"
|
||||
class="link-danger"
|
||||
@click="clearSelectedProductCategories"
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!selectedProductCategories.length" class="empty-conditions">暂未选择商品类目</div>
|
||||
<div v-else class="selected-category-list">
|
||||
<span v-for="category in selectedProductCategories" :key="category.id" class="category-chip">
|
||||
{{ category.path || category.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="run-row">
|
||||
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
|
||||
{{ parsing ? '解析中...' : '解析并创建任务' }}
|
||||
@@ -167,40 +144,6 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="categoryDialogVisible"
|
||||
title="选择商品类目"
|
||||
width="560px"
|
||||
class="category-dialog"
|
||||
>
|
||||
<div class="category-dialog-toolbar">
|
||||
<span class="muted">可多选,确认后会追加到筛选条件 prompt 后面。</span>
|
||||
<button type="button" class="link-danger" @click="clearCategoryTreeSelection">清空选择</button>
|
||||
</div>
|
||||
<div v-if="loadingProductCategories" class="empty-tasks">正在加载商品类目...</div>
|
||||
<div v-else-if="!productCategoryTree.length" class="empty-tasks">暂无商品类目</div>
|
||||
<el-tree
|
||||
v-else
|
||||
ref="categoryTreeRef"
|
||||
class="category-tree"
|
||||
:data="productCategoryTree"
|
||||
node-key="id"
|
||||
show-checkbox
|
||||
default-expand-all
|
||||
:props="{ label: 'name', children: 'children' }"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<span class="category-tree-node">
|
||||
<span>{{ data.name }}</span>
|
||||
<span v-if="data.description" class="category-description">{{ data.description }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
<template #footer>
|
||||
<button type="button" class="opt-btn" @click="categoryDialogVisible = false">取消</button>
|
||||
<button type="button" class="btn-run category-confirm" @click="confirmCategorySelection">确定</button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -218,10 +161,8 @@ import {
|
||||
getSimilarAsinHistory,
|
||||
getSimilarAsinResultDownloadUrl,
|
||||
getSimilarAsinTaskProgressBatch,
|
||||
listProductCategories,
|
||||
listSimilarAsinFilterConditions,
|
||||
parseSimilarAsin,
|
||||
type ProductCategoryItemVo,
|
||||
type SimilarAsinFilterConditionVo,
|
||||
type SimilarAsinDashboardVo,
|
||||
type SimilarAsinHistoryItem,
|
||||
@@ -244,12 +185,6 @@ const queuedTaskSummary = ref<TaskSummary | null>(null)
|
||||
const filterConditionInput = ref('')
|
||||
const filterConditions = ref<SimilarAsinFilterConditionVo[]>([])
|
||||
const selectedFilterConditionId = ref<number | null>(null)
|
||||
const productCategoryTree = ref<ProductCategoryItemVo[]>([])
|
||||
const productCategoryItems = ref<ProductCategoryItemVo[]>([])
|
||||
const selectedProductCategoryIds = ref<number[]>([])
|
||||
const categoryDialogVisible = ref(false)
|
||||
const loadingProductCategories = ref(false)
|
||||
const categoryTreeRef = ref<any>(null)
|
||||
const parsing = ref(false)
|
||||
const pushing = ref(false)
|
||||
const savingCondition = ref(false)
|
||||
@@ -310,78 +245,11 @@ function selectedFilterConditionText() {
|
||||
}
|
||||
|
||||
function effectiveFilterCondition() {
|
||||
return appendProductCategoriesToPrompt(filterConditionInput.value.trim() || selectedFilterConditionText())
|
||||
return filterConditionInput.value.trim() || selectedFilterConditionText()
|
||||
}
|
||||
|
||||
function effectiveCozeApiKey() {
|
||||
return getStoredApiSecret('similar-asin').trim()
|
||||
}
|
||||
|
||||
const selectedProductCategories = computed(() => {
|
||||
const selected = new Set(selectedProductCategoryIds.value)
|
||||
return productCategoryItems.value.filter((item) => selected.has(item.id))
|
||||
})
|
||||
|
||||
function appendProductCategoriesToPrompt(prompt: string) {
|
||||
const lines = selectedProductCategories.value
|
||||
.map((item) => {
|
||||
const path = item.path || item.name
|
||||
const description = item.description?.trim()
|
||||
return description ? `${path}(${description})` : path
|
||||
})
|
||||
.filter(Boolean)
|
||||
if (!lines.length) return prompt
|
||||
const categoryBlock = [
|
||||
'商品类目要求:',
|
||||
...lines.map((line) => `- ${line}`),
|
||||
].join('\n')
|
||||
return prompt ? `${prompt}\n\n${categoryBlock}` : categoryBlock
|
||||
}
|
||||
|
||||
async function loadProductCategories() {
|
||||
if (loadingProductCategories.value) return
|
||||
loadingProductCategories.value = true
|
||||
try {
|
||||
const res = await listProductCategories()
|
||||
productCategoryTree.value = res.tree || []
|
||||
productCategoryItems.value = res.items || []
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '加载商品类目失败')
|
||||
} finally {
|
||||
loadingProductCategories.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openCategoryDialog() {
|
||||
categoryDialogVisible.value = true
|
||||
if (!productCategoryTree.value.length) {
|
||||
await loadProductCategories()
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
categoryTreeRef.value?.setCheckedKeys?.(selectedProductCategoryIds.value)
|
||||
})
|
||||
}
|
||||
|
||||
function hasCheckedDescendant(item: ProductCategoryItemVo, checkedIds: Set<number>): boolean {
|
||||
return Boolean(item.children?.some((child) => checkedIds.has(child.id) || hasCheckedDescendant(child, checkedIds)))
|
||||
}
|
||||
|
||||
function confirmCategorySelection() {
|
||||
const checkedNodes = (categoryTreeRef.value?.getCheckedNodes?.(false, false) || []) as ProductCategoryItemVo[]
|
||||
const checkedIds = new Set(checkedNodes.map((item) => item.id))
|
||||
selectedProductCategoryIds.value = checkedNodes
|
||||
.filter((item) => !hasCheckedDescendant(item, checkedIds))
|
||||
.map((item) => item.id)
|
||||
categoryDialogVisible.value = false
|
||||
}
|
||||
|
||||
function clearCategoryTreeSelection() {
|
||||
categoryTreeRef.value?.setCheckedKeys?.([])
|
||||
}
|
||||
|
||||
function clearSelectedProductCategories() {
|
||||
selectedProductCategoryIds.value = []
|
||||
categoryTreeRef.value?.setCheckedKeys?.([])
|
||||
return getStoredApiSecret().trim()
|
||||
}
|
||||
|
||||
function maskSecret(secret: string) {
|
||||
@@ -911,7 +779,6 @@ onMounted(async () => {
|
||||
loadPollingIds()
|
||||
await Promise.all([
|
||||
loadFilterConditions().catch(() => undefined),
|
||||
loadProductCategories().catch(() => undefined),
|
||||
loadDashboard().catch(() => undefined),
|
||||
loadHistory().catch(() => undefined),
|
||||
])
|
||||
@@ -958,18 +825,6 @@ onUnmounted(() => {
|
||||
.condition-check input { width: 16px; height: 16px; margin: 0; flex: 0 0 auto; }
|
||||
.condition-check span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.link-danger { border: none; background: transparent; color: #ff8f8f; cursor: pointer; font-size: 12px; padding: 2px 0; white-space: nowrap; }
|
||||
.category-card { margin: 0 0 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; }
|
||||
.category-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.category-card-head .section-title { margin-bottom: 0; }
|
||||
.category-actions { display: flex; align-items: center; gap: 10px; }
|
||||
.category-actions .opt-btn { padding: 6px 12px; }
|
||||
.selected-category-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; max-height: 96px; overflow: auto; }
|
||||
.category-chip { max-width: 100%; padding: 4px 8px; border-radius: 6px; background: rgba(52, 152, 219, .16); color: #cfe7ff; font-size: 12px; line-height: 1.4; }
|
||||
.category-dialog-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
|
||||
.category-tree { max-height: 420px; overflow: auto; padding: 8px; border: 1px solid #2f2f2f; border-radius: 8px; background: #202020; --el-tree-bg-color: #202020; --el-tree-text-color: #d8d8d8; --el-tree-node-hover-bg-color: #2a2a2a; }
|
||||
.category-tree-node { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
.category-description { color: #888; font-size: 12px; }
|
||||
.category-confirm { padding: 8px 16px; }
|
||||
.parse-card, .queue-payload { margin-top: 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #b8c1cc; font-size: 12px; }
|
||||
.queue-payload { max-height: 220px; overflow: auto; color: #8fd3ff; white-space: pre-wrap; }
|
||||
.panel-header { padding: 16px 20px; border-bottom: 1px solid #2a2a2a; font-size: 15px; font-weight: 600; color: #ddd; }
|
||||
|
||||
@@ -1680,31 +1680,6 @@ export function getAppearancePatentResultDownloadUrl(resultId: number) {
|
||||
|
||||
// ========== 货源查询 ==========
|
||||
|
||||
export interface ProductCategoryItemVo {
|
||||
id: number;
|
||||
parentId?: number | null;
|
||||
name: string;
|
||||
categoryKey?: string;
|
||||
sortOrder?: number;
|
||||
description?: string;
|
||||
isBuiltin?: boolean;
|
||||
childCount?: number;
|
||||
level?: number;
|
||||
path?: string;
|
||||
children?: ProductCategoryItemVo[];
|
||||
}
|
||||
|
||||
export interface ProductCategoryListVo {
|
||||
tree: ProductCategoryItemVo[];
|
||||
items: ProductCategoryItemVo[];
|
||||
}
|
||||
|
||||
export function listProductCategories() {
|
||||
return unwrapJavaResponse(
|
||||
get<JavaApiResponse<ProductCategoryListVo>>(`${JAVA_API_PREFIX}/admin/product-categories`),
|
||||
);
|
||||
}
|
||||
|
||||
export interface SimilarAsinFilterConditionVo {
|
||||
id: number;
|
||||
conditionText: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type ApiSecretModuleKey = 'appearance-patent' | 'similar-asin'
|
||||
type LegacyApiSecretModuleKey = 'appearance-patent' | 'similar-asin'
|
||||
|
||||
export type ApiSecretRetention = 'session' | '1d' | '7d' | '30d' | 'forever'
|
||||
|
||||
@@ -18,17 +18,19 @@ export type ApiSecretSnapshot = {
|
||||
}
|
||||
|
||||
const STORAGE_PREFIX = 'brand:api-secret'
|
||||
const COMMON_SECRET_KEY = 'common'
|
||||
const LEGACY_SECRET_KEYS: LegacyApiSecretModuleKey[] = ['appearance-patent', 'similar-asin']
|
||||
|
||||
function currentUserStorageId() {
|
||||
if (typeof window === 'undefined') return '0'
|
||||
return window.localStorage.getItem('uid') || '0'
|
||||
}
|
||||
|
||||
function buildStorageKey(moduleKey: ApiSecretModuleKey) {
|
||||
function buildStorageKey(moduleKey: string) {
|
||||
return `${STORAGE_PREFIX}:${currentUserStorageId()}:${moduleKey}`
|
||||
}
|
||||
|
||||
function readStorageRecord(storage: Storage, moduleKey: ApiSecretModuleKey): ApiSecretRecord | null {
|
||||
function readStorageRecord(storage: Storage, moduleKey: string): ApiSecretRecord | null {
|
||||
const raw = storage.getItem(buildStorageKey(moduleKey))
|
||||
if (!raw) return null
|
||||
try {
|
||||
@@ -81,11 +83,11 @@ function isExpired(record: ApiSecretRecord) {
|
||||
return record.expiresAt != null && record.expiresAt <= Date.now()
|
||||
}
|
||||
|
||||
function clearStorageRecord(storage: Storage, moduleKey: ApiSecretModuleKey) {
|
||||
function clearStorageRecord(storage: Storage, moduleKey: string) {
|
||||
storage.removeItem(buildStorageKey(moduleKey))
|
||||
}
|
||||
|
||||
function getLiveRecord(moduleKey: ApiSecretModuleKey): ApiSecretRecord | null {
|
||||
function getLiveRecordFromKey(moduleKey: string): ApiSecretRecord | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
const sessionRecord = readStorageRecord(window.sessionStorage, moduleKey)
|
||||
@@ -102,12 +104,42 @@ function getLiveRecord(moduleKey: ApiSecretModuleKey): ApiSecretRecord | null {
|
||||
return localRecord
|
||||
}
|
||||
|
||||
export function getStoredApiSecret(moduleKey: ApiSecretModuleKey) {
|
||||
return getLiveRecord(moduleKey)?.value || ''
|
||||
function clearLegacyStoredApiSecrets() {
|
||||
if (typeof window === 'undefined') return
|
||||
for (const moduleKey of LEGACY_SECRET_KEYS) {
|
||||
clearStorageRecord(window.sessionStorage, moduleKey)
|
||||
clearStorageRecord(window.localStorage, moduleKey)
|
||||
}
|
||||
}
|
||||
|
||||
export function getStoredApiSecretSnapshot(moduleKey: ApiSecretModuleKey): ApiSecretSnapshot {
|
||||
const record = getLiveRecord(moduleKey)
|
||||
function migrateLegacyRecord(record: ApiSecretRecord) {
|
||||
if (typeof window === 'undefined') return
|
||||
const storage = record.retention === 'session' ? window.sessionStorage : window.localStorage
|
||||
storage.setItem(buildStorageKey(COMMON_SECRET_KEY), JSON.stringify(record))
|
||||
clearLegacyStoredApiSecrets()
|
||||
}
|
||||
|
||||
function getLiveRecord(): ApiSecretRecord | null {
|
||||
const commonRecord = getLiveRecordFromKey(COMMON_SECRET_KEY)
|
||||
if (commonRecord) return commonRecord
|
||||
|
||||
for (const moduleKey of LEGACY_SECRET_KEYS) {
|
||||
const legacyRecord = getLiveRecordFromKey(moduleKey)
|
||||
if (legacyRecord) {
|
||||
migrateLegacyRecord(legacyRecord)
|
||||
return legacyRecord
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function getStoredApiSecret() {
|
||||
return getLiveRecord()?.value || ''
|
||||
}
|
||||
|
||||
export function getStoredApiSecretSnapshot(): ApiSecretSnapshot {
|
||||
const record = getLiveRecord()
|
||||
if (!record) {
|
||||
return {
|
||||
value: '',
|
||||
@@ -127,14 +159,13 @@ export function getStoredApiSecretSnapshot(moduleKey: ApiSecretModuleKey): ApiSe
|
||||
}
|
||||
|
||||
export function saveStoredApiSecret(
|
||||
moduleKey: ApiSecretModuleKey,
|
||||
value: string,
|
||||
retention: ApiSecretRetention,
|
||||
) {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
const trimmedValue = value.trim()
|
||||
clearStoredApiSecret(moduleKey)
|
||||
clearStoredApiSecret()
|
||||
if (!trimmedValue) return
|
||||
|
||||
const now = Date.now()
|
||||
@@ -146,11 +177,12 @@ export function saveStoredApiSecret(
|
||||
}
|
||||
|
||||
const storage = retention === 'session' ? window.sessionStorage : window.localStorage
|
||||
storage.setItem(buildStorageKey(moduleKey), JSON.stringify(record))
|
||||
storage.setItem(buildStorageKey(COMMON_SECRET_KEY), JSON.stringify(record))
|
||||
}
|
||||
|
||||
export function clearStoredApiSecret(moduleKey: ApiSecretModuleKey) {
|
||||
export function clearStoredApiSecret() {
|
||||
if (typeof window === 'undefined') return
|
||||
clearStorageRecord(window.sessionStorage, moduleKey)
|
||||
clearStorageRecord(window.localStorage, moduleKey)
|
||||
clearStorageRecord(window.sessionStorage, COMMON_SECRET_KEY)
|
||||
clearStorageRecord(window.localStorage, COMMON_SECRET_KEY)
|
||||
clearLegacyStoredApiSecrets()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user