feat(client): 实现用户数据隔离与设备绑定优化- 添加用户会话ID构建逻辑,确保数据按用户隔离- 优化设备绑定流程,支持设备状态更新和绑定时间同步- 实现用户缓存清理功能,仅清除当前用户的数据- 增强客户端账号删除逻辑,级联删除相关数据

- 调整设备在线查询逻辑,确保只返回活跃绑定的设备
- 优化试用期逻辑,精确计算过期时间和类型- 添加账号管理弹窗和相关状态注入
-修复跟卖精灵按钮加载状态显示问题
- 增强文件上传区域UI,显示选中文件名
- 调整分页组件样式,优化界面展示效果- 优化反馈日志存储路径逻辑,默认使用用户目录
- 移除冗余代码和无用导入,提升代码整洁度
This commit is contained in:
2025-10-24 13:43:46 +08:00
parent e2a438c84e
commit 3a76aaa3c0
50 changed files with 860 additions and 590 deletions

View File

@@ -4,6 +4,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { amazonApi } from '../../api/amazon'
import { systemApi } from '../../api/system'
import { handlePlatformFileExport } from '../../utils/settings'
import { getUsernameFromToken } from '../../utils/token'
import { useFileDrop } from '../../composables/useFileDrop'
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
@@ -34,7 +35,7 @@ const amazonUpload = ref<HTMLInputElement | null>(null)
const showTrialExpiredDialog = ref(false)
const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account')
const checkExpiredType = inject<() => 'device' | 'account' | 'both' | 'subscribe'>('checkExpiredType')
const vipStatus = inject<any>('vipStatus')
// 计算属性 - 当前页数据
const paginatedData = computed(() => {
@@ -51,6 +52,7 @@ const regionOptions = [
{ label: '美国 (USA)', value: 'US', flag: '🇺🇸' },
]
const pendingAsins = ref<string[]>([])
const selectedFileName = ref('')
// 通用消息提示Element Plus
function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'info' = 'info') {
@@ -73,6 +75,7 @@ async function processExcelFile(file: File) {
return
}
pendingAsins.value = asinList
selectedFileName.value = file.name
} catch (error: any) {
showMessage(error.message || '处理文件失败', 'error')
} finally {
@@ -102,7 +105,7 @@ async function batchGetProductInfo(asinList: string[]) {
// VIP检查
if (!props.isVip) {
if (checkExpiredType) trialExpiredType.value = checkExpiredType()
if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType
showTrialExpiredDialog.value = true
return
}
@@ -158,6 +161,7 @@ async function batchGetProductInfo(asinList: string[]) {
// 处理完成状态更新
progressPercentage.value = 100
currentAsin.value = '处理完成'
selectedFileName.value = ''
@@ -165,6 +169,7 @@ async function batchGetProductInfo(asinList: string[]) {
if (error.name !== 'AbortError') {
showMessage(error.message || '批量获取产品信息失败', 'error')
currentAsin.value = '处理失败'
selectedFileName.value = ''
}
} finally {
tableLoading.value = false
@@ -217,7 +222,8 @@ async function exportToExcel() {
const blob = new Blob([html], { type: 'application/vnd.ms-excel' })
const fileName = `Amazon产品数据_${new Date().toISOString().slice(0, 10)}.xls`
const success = await handlePlatformFileExport('amazon', blob, fileName)
const username = getUsernameFromToken()
const success = await handlePlatformFileExport('amazon', blob, fileName, username)
if (success) {
showMessage('Excel文件导出成功', 'success')
@@ -246,6 +252,7 @@ function stopFetch() {
abortController = null
loading.value = false
currentAsin.value = '已停止'
selectedFileName.value = ''
showMessage('已停止获取产品数据', 'info')
}
@@ -326,9 +333,10 @@ onMounted(async () => {
<span class="tab-icon">📦</span>
<span class="tab-text">ASIN查询</span>
</div>
<div class="tab-item" @click="openGenmaiSpirit">
<span class="tab-icon">🔍</span>
<span class="tab-text">跟卖精灵</span>
<div class="tab-item" :class="{ loading: genmaiLoading }" @click="!genmaiLoading && openGenmaiSpirit()">
<span class="tab-icon" v-if="!genmaiLoading">🔍</span>
<span class="tab-icon spinner-icon" v-else></span>
<span class="tab-text">{{ genmaiLoading ? '启动中...' : '跟卖精灵' }}</span>
</div>
</div>
<div class="steps-title">操作流程</div>
@@ -350,6 +358,10 @@ onMounted(async () => {
<div class="dz-sub">支持 .xls .xlsx</div>
</div>
<input ref="amazonUpload" style="display:none" type="file" accept=".xls,.xlsx" @change="handleExcelUpload" :disabled="loading" />
<div v-if="selectedFileName" class="file-chip">
<span class="dot"></span>
<span class="name">{{ selectedFileName }}</span>
</div>
</div>
</div>
<!-- 2 网站地区 -->
@@ -375,7 +387,6 @@ onMounted(async () => {
<el-button size="small" class="w100 btn-blue" :disabled="!pendingAsins.length || loading" @click="startQueuedFetch">{{ loading ? '处理中...' : '获取数据' }}</el-button>
<el-button size="small" class="w100" :disabled="!loading" @click="stopFetch">停止获取</el-button>
</div>
<div class="mini-hint" v-if="pendingAsins.length">已导入 {{ pendingAsins.length }} ASIN</div>
</div>
</div>
<!-- 4 -->
@@ -462,9 +473,8 @@ onMounted(async () => {
</div>
</div>
</div>
<div class="pagination-fixed" >
<div class="pagination-fixed">
<el-pagination
background
:current-page="currentPage"
:page-sizes="[15,30,50,100]"
:page-size="pageSize"
@@ -492,7 +502,9 @@ onMounted(async () => {
.tab-item:last-child { border-radius: 0 3px 3px 0; border-left: none; }
.tab-item:hover { background: #e8f4ff; color: #409EFF; }
.tab-item.active { background: #1677FF; color: #fff; border-color: #1677FF; cursor: default; }
.tab-item.loading { background: #e8f4ff; color: #409EFF; cursor: not-allowed; opacity: 0.8; }
.tab-icon { font-size: 12px; }
.spinner-icon { animation: spin 1s linear infinite; display: inline-block; }
.tab-text { line-height: 1; }
.body-layout { display: flex; gap: 12px; flex: 1; overflow: hidden; }
@@ -501,7 +513,6 @@ onMounted(async () => {
.steps-flow { position: relative; }
.steps-flow:before { content: ''; position: absolute; left: 11px; top: 20px; bottom: 0; width: 1px; background: rgba(229, 231, 235, 0.6); }
.flow-item { position: relative; display: grid; grid-template-columns: 22px 1fr; gap: 10px; padding: 8px 0; }
.flow-item + .flow-item { border-top: 1px dashed #ebeef5; }
.flow-item .step-index { position: static; width: 22px; height: 22px; line-height: 22px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 12px; font-weight: 600; margin-top: 2px; }
.flow-item:after { display: none; }
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; }
@@ -520,6 +531,9 @@ onMounted(async () => {
.dz-icon { font-size: 20px; margin-bottom: 6px; }
.dz-text { color: #303133; font-size: 13px; }
.dz-sub { color: #909399; font-size: 12px; }
.file-chip { display: flex; align-items: center; gap: 6px; padding: 6px 8px; background: #f5f7fa; border-radius: 4px; font-size: 12px; color: #606266; margin-top: 6px; }
.file-chip .dot { width: 6px; height: 6px; background: #409EFF; border-radius: 50%; display: inline-block; }
.file-chip .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.single-input.left { display: flex; gap: 8px; }
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
.form-row { margin-bottom: 10px; }
@@ -573,7 +587,8 @@ onMounted(async () => {
.table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; }
.spinner { font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.pagination-fixed { flex-shrink: 0; padding: 8px 12px; background: #f9f9f9; border-radius: 4px; display: flex; justify-content: center; border-top: 1px solid #ebeef5; margin-top: 8px; }
.pagination-fixed { flex-shrink: 0; padding: 8px 12px; background: #fff; display: flex; justify-content: flex-end; margin-top: 8px; }
.pagination-fixed :deep(.el-pager li.is-active) { border: 1px solid #1677FF; border-radius: 4px; color: #1677FF; background: #fff; }
.empty-tip { text-align: center; color: #909399; padding: 16px 0; }
.import-section[draggable], .import-section.drag-active { border: 1px dashed #409EFF; border-radius: 6px; }
.empty-container { text-align: center; }