增加公共下载进度、增加接收SKU、密钥分别存放
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
type LegacyApiSecretModuleKey = 'appearance-patent' | 'similar-asin'
|
||||
export type ApiSecretModuleKey = 'appearance-patent' | 'similar-asin'
|
||||
|
||||
export type ApiSecretRetention = 'session' | '1d' | '7d' | '30d' | 'forever'
|
||||
|
||||
@@ -19,7 +19,7 @@ export type ApiSecretSnapshot = {
|
||||
|
||||
const STORAGE_PREFIX = 'brand:api-secret'
|
||||
const COMMON_SECRET_KEY = 'common'
|
||||
const LEGACY_SECRET_KEYS: LegacyApiSecretModuleKey[] = ['appearance-patent', 'similar-asin']
|
||||
const MODULE_SECRET_KEYS: ApiSecretModuleKey[] = ['appearance-patent', 'similar-asin']
|
||||
|
||||
function currentUserStorageId() {
|
||||
if (typeof window === 'undefined') return '0'
|
||||
@@ -106,40 +106,40 @@ function getLiveRecordFromKey(moduleKey: string): ApiSecretRecord | null {
|
||||
|
||||
function clearLegacyStoredApiSecrets() {
|
||||
if (typeof window === 'undefined') return
|
||||
for (const moduleKey of LEGACY_SECRET_KEYS) {
|
||||
for (const moduleKey of MODULE_SECRET_KEYS) {
|
||||
clearStorageRecord(window.sessionStorage, moduleKey)
|
||||
clearStorageRecord(window.localStorage, moduleKey)
|
||||
}
|
||||
}
|
||||
|
||||
function migrateLegacyRecord(record: ApiSecretRecord) {
|
||||
function migrateCommonRecord(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
|
||||
for (const moduleKey of MODULE_SECRET_KEYS) {
|
||||
if (!getLiveRecordFromKey(moduleKey)) {
|
||||
storage.setItem(buildStorageKey(moduleKey), JSON.stringify(record))
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
clearStorageRecord(window.sessionStorage, COMMON_SECRET_KEY)
|
||||
clearStorageRecord(window.localStorage, COMMON_SECRET_KEY)
|
||||
}
|
||||
|
||||
export function getStoredApiSecret() {
|
||||
return getLiveRecord()?.value || ''
|
||||
function getLiveRecord(moduleKey: ApiSecretModuleKey): ApiSecretRecord | null {
|
||||
const moduleRecord = getLiveRecordFromKey(moduleKey)
|
||||
if (moduleRecord) return moduleRecord
|
||||
|
||||
const commonRecord = getLiveRecordFromKey(COMMON_SECRET_KEY)
|
||||
if (!commonRecord) return null
|
||||
migrateCommonRecord(commonRecord)
|
||||
return getLiveRecordFromKey(moduleKey)
|
||||
}
|
||||
|
||||
export function getStoredApiSecretSnapshot(): ApiSecretSnapshot {
|
||||
const record = getLiveRecord()
|
||||
export function getStoredApiSecret(moduleKey: ApiSecretModuleKey) {
|
||||
return getLiveRecord(moduleKey)?.value || ''
|
||||
}
|
||||
|
||||
export function getStoredApiSecretSnapshot(moduleKey: ApiSecretModuleKey): ApiSecretSnapshot {
|
||||
const record = getLiveRecord(moduleKey)
|
||||
if (!record) {
|
||||
return {
|
||||
value: '',
|
||||
@@ -159,13 +159,14 @@ export function getStoredApiSecretSnapshot(): ApiSecretSnapshot {
|
||||
}
|
||||
|
||||
export function saveStoredApiSecret(
|
||||
moduleKey: ApiSecretModuleKey,
|
||||
value: string,
|
||||
retention: ApiSecretRetention,
|
||||
) {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
const trimmedValue = value.trim()
|
||||
clearStoredApiSecret()
|
||||
clearStoredApiSecret(moduleKey)
|
||||
if (!trimmedValue) return
|
||||
|
||||
const now = Date.now()
|
||||
@@ -177,10 +178,16 @@ export function saveStoredApiSecret(
|
||||
}
|
||||
|
||||
const storage = retention === 'session' ? window.sessionStorage : window.localStorage
|
||||
storage.setItem(buildStorageKey(COMMON_SECRET_KEY), JSON.stringify(record))
|
||||
storage.setItem(buildStorageKey(moduleKey), JSON.stringify(record))
|
||||
}
|
||||
|
||||
export function clearStoredApiSecret() {
|
||||
export function clearStoredApiSecret(moduleKey: ApiSecretModuleKey) {
|
||||
if (typeof window === 'undefined') return
|
||||
clearStorageRecord(window.sessionStorage, moduleKey)
|
||||
clearStorageRecord(window.localStorage, moduleKey)
|
||||
}
|
||||
|
||||
export function clearAllStoredApiSecrets() {
|
||||
if (typeof window === 'undefined') return
|
||||
clearStorageRecord(window.sessionStorage, COMMON_SECRET_KEY)
|
||||
clearStorageRecord(window.localStorage, COMMON_SECRET_KEY)
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { computed, reactive } from 'vue'
|
||||
|
||||
import { getPywebviewApi } from '@/shared/bridges/pywebview'
|
||||
|
||||
export type DownloadProgressStatus = 'selecting' | 'running' | 'success' | 'failed' | 'cancelled'
|
||||
|
||||
export type DownloadProgressItem = {
|
||||
id: string
|
||||
filename: string
|
||||
status: DownloadProgressStatus
|
||||
path?: string
|
||||
downloaded: number
|
||||
total: number
|
||||
percent: number
|
||||
error?: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
type PywebviewDownloadProgressEvent = {
|
||||
id: string
|
||||
status: 'running' | 'success' | 'failed'
|
||||
path?: string
|
||||
downloaded: number
|
||||
total: number
|
||||
percent: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
const progressItems = reactive<Record<string, DownloadProgressItem>>({})
|
||||
let progressListenerBound = false
|
||||
|
||||
function normalizePercent(value: number) {
|
||||
if (!Number.isFinite(value)) return 0
|
||||
return Math.max(0, Math.min(100, Math.round(value)))
|
||||
}
|
||||
|
||||
function now() {
|
||||
return Date.now()
|
||||
}
|
||||
|
||||
function upsertProgress(partial: Omit<Partial<DownloadProgressItem>, 'id'> & { id: string }) {
|
||||
const existing = progressItems[partial.id]
|
||||
const timestamp = now()
|
||||
progressItems[partial.id] = {
|
||||
id: partial.id,
|
||||
filename: partial.filename || existing?.filename || '下载文件',
|
||||
status: partial.status || existing?.status || 'running',
|
||||
path: partial.path ?? existing?.path,
|
||||
downloaded: Number(partial.downloaded ?? existing?.downloaded ?? 0),
|
||||
total: Number(partial.total ?? existing?.total ?? 0),
|
||||
percent: normalizePercent(Number(partial.percent ?? existing?.percent ?? 0)),
|
||||
error: partial.error ?? existing?.error,
|
||||
createdAt: existing?.createdAt || timestamp,
|
||||
updatedAt: timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
function handlePywebviewProgress(event: Event) {
|
||||
const detail = (event as CustomEvent<PywebviewDownloadProgressEvent>).detail
|
||||
if (!detail?.id) return
|
||||
upsertProgress({
|
||||
id: detail.id,
|
||||
status: detail.status,
|
||||
path: detail.path,
|
||||
downloaded: detail.downloaded,
|
||||
total: detail.total,
|
||||
percent: detail.percent,
|
||||
error: detail.error,
|
||||
})
|
||||
}
|
||||
|
||||
export function ensureDownloadProgressListener() {
|
||||
if (progressListenerBound || typeof window === 'undefined') return
|
||||
progressListenerBound = true
|
||||
window.addEventListener('pywebview-download-progress', handlePywebviewProgress)
|
||||
}
|
||||
|
||||
export function useDownloadProgress() {
|
||||
ensureDownloadProgressListener()
|
||||
const items = computed(() =>
|
||||
Object.values(progressItems)
|
||||
.filter((item) => item.status !== 'cancelled')
|
||||
.sort((a, b) => b.createdAt - a.createdAt),
|
||||
)
|
||||
return {
|
||||
items,
|
||||
clearDownloadProgress,
|
||||
}
|
||||
}
|
||||
|
||||
export function clearDownloadProgress(id: string) {
|
||||
delete progressItems[id]
|
||||
}
|
||||
|
||||
function buildDownloadId(filename: string) {
|
||||
const safeName = filename.replace(/[^a-zA-Z0-9._-]+/g, '-').slice(0, 60) || 'download'
|
||||
return `download:${Date.now()}:${Math.random().toString(16).slice(2)}:${safeName}`
|
||||
}
|
||||
|
||||
export async function saveUrlWithProgress(url: string, filename: string, id = buildDownloadId(filename)) {
|
||||
ensureDownloadProgressListener()
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.save_file_from_url_new) {
|
||||
upsertProgress({
|
||||
id,
|
||||
filename,
|
||||
status: 'failed',
|
||||
downloaded: 0,
|
||||
total: 0,
|
||||
percent: 0,
|
||||
error: '当前客户端未提供下载能力',
|
||||
})
|
||||
return { success: false, error: '当前客户端未提供下载能力' }
|
||||
}
|
||||
|
||||
upsertProgress({
|
||||
id,
|
||||
filename,
|
||||
status: 'selecting',
|
||||
downloaded: 0,
|
||||
total: 0,
|
||||
percent: 0,
|
||||
})
|
||||
|
||||
const result = api.save_file_from_url_with_progress
|
||||
? await api.save_file_from_url_with_progress(url, filename, id)
|
||||
: await api.save_file_from_url_new(url, filename)
|
||||
|
||||
if (result.success) {
|
||||
const existing = progressItems[id]
|
||||
upsertProgress({
|
||||
id,
|
||||
filename,
|
||||
status: 'success',
|
||||
path: result.path,
|
||||
downloaded: existing?.downloaded || existing?.total || 0,
|
||||
total: existing?.total || existing?.downloaded || 0,
|
||||
percent: 100,
|
||||
})
|
||||
} else if (result.error === '用户取消') {
|
||||
upsertProgress({
|
||||
id,
|
||||
filename,
|
||||
status: 'cancelled',
|
||||
downloaded: 0,
|
||||
total: 0,
|
||||
percent: 0,
|
||||
})
|
||||
clearDownloadProgress(id)
|
||||
} else {
|
||||
upsertProgress({
|
||||
id,
|
||||
filename,
|
||||
status: 'failed',
|
||||
downloaded: 0,
|
||||
total: 0,
|
||||
percent: 0,
|
||||
error: result.error || '下载失败',
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function formatDownloadBytes(value: number) {
|
||||
const size = Number(value || 0)
|
||||
if (!Number.isFinite(size) || size <= 0) return '未知大小'
|
||||
if (size < 1024) return `${size} B`
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
||||
if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(1)} MB`
|
||||
return `${(size / 1024 / 1024 / 1024).toFixed(2)} GB`
|
||||
}
|
||||
|
||||
export function downloadProgressText(item: DownloadProgressItem) {
|
||||
if (item.status === 'selecting') return '等待选择保存位置'
|
||||
if (item.status === 'success') return '下载完成'
|
||||
if (item.status === 'failed') return item.error || '下载失败'
|
||||
return item.total > 0 ? '正在下载,请等待完成后再打开' : '正在下载,正在获取文件大小'
|
||||
}
|
||||
Reference in New Issue
Block a user