feat(amazon): 实现商标筛查功能并优化用户体验
- 添加商标筛查面板和相关API接口- 实现Excel文件解析和数据过滤功能 - 添加文件上传进度跟踪和错误处理-优化空状态显示和操作引导- 实现tab状态持久化存储 - 添加订阅会员弹窗和付费入口 -优化文件选择和删除功能 - 改进UI样式和响应式布局
This commit is contained in:
@@ -211,7 +211,7 @@ function startSpringBoot() {
|
||||
}
|
||||
}
|
||||
|
||||
// startSpringBoot();
|
||||
startSpringBoot();
|
||||
function stopSpringBoot() {
|
||||
if (!springProcess) return;
|
||||
try {
|
||||
@@ -347,10 +347,10 @@ app.whenReady().then(() => {
|
||||
splashWindow.loadFile(splashPath);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
openAppIfNotOpened();
|
||||
}, 100);
|
||||
//666
|
||||
// setTimeout(() => {
|
||||
// openAppIfNotOpened();
|
||||
// }, 100);
|
||||
|
||||
app.on('activate', () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export type HttpMethod = 'GET' | 'POST' | 'DELETE';
|
||||
export const CONFIG = {
|
||||
CLIENT_BASE: 'http://localhost:8081',
|
||||
// RUOYI_BASE: 'http://8.138.23.49:8085',
|
||||
RUOYI_BASE: 'http://192.168.1.89:8085',
|
||||
SSE_URL: 'http://192.168.1.89:8085/monitor/account/events'
|
||||
RUOYI_BASE: 'http://8.138.23.49:8085',
|
||||
//RUOYI_BASE: 'http://192.168.1.89:8085',
|
||||
SSE_URL: 'http://8.138.23.49:8085/monitor/account/events'
|
||||
} as const;
|
||||
|
||||
function resolveBase(path: string): string {
|
||||
@@ -31,8 +31,18 @@ async function getToken(): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
async function getUsername(): Promise<string> {
|
||||
try {
|
||||
const tokenModule = await import('../utils/token');
|
||||
return tokenModule.getUsernameFromToken() || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options: RequestInit & { signal?: AbortSignal }): Promise<T> {
|
||||
const token = await getToken();
|
||||
const username = await getUsername();
|
||||
let res: Response;
|
||||
|
||||
try {
|
||||
@@ -43,6 +53,7 @@ async function request<T>(path: string, options: RequestInit & { signal?: AbortS
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
...(username ? { 'username': username } : {}),
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
@@ -90,6 +101,7 @@ export const http = {
|
||||
|
||||
async upload<T>(path: string, form: FormData, signal?: AbortSignal) {
|
||||
const token = await getToken();
|
||||
const username = await getUsername();
|
||||
let res: Response;
|
||||
|
||||
try {
|
||||
@@ -98,7 +110,10 @@ export const http = {
|
||||
body: form,
|
||||
credentials: 'omit',
|
||||
cache: 'no-store',
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
||||
headers: {
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
...(username ? { 'username': username } : {})
|
||||
},
|
||||
signal
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -8,21 +8,108 @@ export const markApi = {
|
||||
return http.upload<{ code: number, data: any, msg: string }>('/tool/mark/newTask', formData)
|
||||
},
|
||||
|
||||
// 获取任务列表及筛选数据
|
||||
// 获取任务列表及筛选数据(返回完整行数据和表头)
|
||||
getTask() {
|
||||
return http.get<{ code: number, data: { original: any, filtered: any[] }, msg: string }>('/tool/mark/task')
|
||||
return http.get<{
|
||||
code: number,
|
||||
data: {
|
||||
original: any,
|
||||
filtered: Record<string, any>[], // 完整的行数据(Map格式)
|
||||
headers: string[] // 表头
|
||||
},
|
||||
msg: string
|
||||
}>('/tool/mark/task')
|
||||
},
|
||||
|
||||
// 品牌商标筛查
|
||||
brandCheck(brands: string[]) {
|
||||
return http.post<{ code: number, data: { total: number, filtered: number, passed: number, data: any[] }, msg: string }>('/tool/mark/brandCheck', brands)
|
||||
brandCheck(brands: string[], taskId?: string) {
|
||||
return http.post<{ code: number, data: { total: number, checked: number, registered: number, unregistered: number, failed: number, data: any[], duration: string }, msg: string }>('/api/trademark/brandCheck', { brands, taskId })
|
||||
},
|
||||
|
||||
// 从Excel提取品牌列表(客户端本地接口)
|
||||
// 查询品牌筛查进度
|
||||
getBrandCheckProgress(taskId: string) {
|
||||
return http.get<{ code: number, data: { current: number }, msg: string }>('/api/trademark/brandCheckProgress', { taskId })
|
||||
},
|
||||
|
||||
// 取消品牌筛查任务
|
||||
cancelBrandCheck(taskId: string) {
|
||||
return http.post<{ code: number, data: string, msg: string }>('/api/trademark/cancelBrandCheck', { taskId })
|
||||
},
|
||||
|
||||
// 验证Excel表头
|
||||
validateHeaders(file: File, requiredHeaders?: string[]) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
if (requiredHeaders && requiredHeaders.length > 0) {
|
||||
formData.append('requiredHeaders', JSON.stringify(requiredHeaders))
|
||||
}
|
||||
return http.upload<{
|
||||
code: number,
|
||||
data: {
|
||||
headers: string[],
|
||||
valid?: boolean,
|
||||
missing?: string[]
|
||||
},
|
||||
msg: string
|
||||
}>('/api/trademark/validateHeaders', formData)
|
||||
},
|
||||
|
||||
// 从Excel提取品牌列表(客户端本地接口,返回完整Excel数据)
|
||||
extractBrands(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return http.upload<{ code: number, data: { total: number, brands: string[] }, msg: string }>('/api/trademark/extractBrands', formData)
|
||||
return http.upload<{
|
||||
code: number,
|
||||
data: {
|
||||
total: number,
|
||||
brands: string[],
|
||||
headers: string[],
|
||||
allRows: Record<string, any>[]
|
||||
},
|
||||
msg: string
|
||||
}>('/api/trademark/extractBrands', formData)
|
||||
},
|
||||
|
||||
// 根据ASIN列表从Excel中过滤完整行数据(客户端本地接口)
|
||||
filterByAsins(file: File, asins: string[]) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('asins', JSON.stringify(asins))
|
||||
return http.upload<{
|
||||
code: number,
|
||||
data: {
|
||||
headers: string[],
|
||||
filteredRows: Record<string, any>[],
|
||||
total: number
|
||||
},
|
||||
msg: string
|
||||
}>('/api/trademark/filterByAsins', formData)
|
||||
},
|
||||
|
||||
// 根据品牌列表从Excel中过滤完整行数据(客户端本地接口)
|
||||
filterByBrands(file: File, brands: string[]) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('brands', JSON.stringify(brands))
|
||||
return http.upload<{
|
||||
code: number,
|
||||
data: {
|
||||
headers: string[],
|
||||
filteredRows: Record<string, any>[],
|
||||
total: number
|
||||
},
|
||||
msg: string
|
||||
}>('/api/trademark/filterByBrands', formData)
|
||||
},
|
||||
|
||||
// 保存查询会话
|
||||
saveSession(sessionData: any) {
|
||||
return http.post<{ code: number, data: { sessionId: string }, msg: string }>('/api/trademark/saveSession', sessionData)
|
||||
},
|
||||
|
||||
// 恢复查询会话
|
||||
getSession(sessionId: string) {
|
||||
return http.get<{ code: number, data: any, msg: string }>('/api/trademark/getSession', { sessionId })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch, defineAsyncComponent } from 'vue'
|
||||
import { getUsernameFromToken } from '../../utils/token'
|
||||
import AsinQueryPanel from './AsinQueryPanel.vue'
|
||||
import GenmaiSpiritPanel from './GenmaiSpiritPanel.vue'
|
||||
import TrademarkCheckPanel from './TrademarkCheckPanel.vue'
|
||||
|
||||
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
|
||||
|
||||
const props = defineProps<{
|
||||
isVip: boolean
|
||||
}>()
|
||||
const currentTab = ref<'asin' | 'genmai' | 'trademark'>('asin')
|
||||
|
||||
// 从localStorage恢复tab状态
|
||||
function getInitialTab(): 'asin' | 'genmai' | 'trademark' {
|
||||
try {
|
||||
const username = getUsernameFromToken()
|
||||
const saved = localStorage.getItem(`amazon_tab_${username}`)
|
||||
if (saved && ['asin', 'genmai', 'trademark'].includes(saved)) {
|
||||
return saved as 'asin' | 'genmai' | 'trademark'
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略错误
|
||||
}
|
||||
return 'asin'
|
||||
}
|
||||
|
||||
const currentTab = ref<'asin' | 'genmai' | 'trademark'>(getInitialTab())
|
||||
|
||||
// 监听tab切换,保存到localStorage
|
||||
watch(currentTab, (newTab) => {
|
||||
try {
|
||||
const username = getUsernameFromToken()
|
||||
localStorage.setItem(`amazon_tab_${username}`, newTab)
|
||||
} catch (e) {
|
||||
// 忽略错误
|
||||
}
|
||||
})
|
||||
const asinPanelRef = ref<any>(null)
|
||||
const trademarkPanelRef = ref<any>(null)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const localProductData = ref<any[]>([])
|
||||
const trademarkData = ref<any[]>([])
|
||||
const showSubscribeDialog = ref(false)
|
||||
const paginatedData = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
@@ -82,6 +112,16 @@ function handleRetryTask() {
|
||||
trademarkPanelRef.value.startTrademarkQuery()
|
||||
}
|
||||
}
|
||||
|
||||
function openSubscribeDialog() {
|
||||
showSubscribeDialog.value = true
|
||||
}
|
||||
|
||||
function handleExportData() {
|
||||
if (trademarkPanelRef.value && typeof trademarkPanelRef.value.exportTrademarkData === 'function') {
|
||||
trademarkPanelRef.value.exportTrademarkData()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -170,7 +210,7 @@ function handleRetryTask() {
|
||||
|
||||
<div class="info-box">
|
||||
<div class="info-title">收费标准</div>
|
||||
<div class="info-text">目前免费试用3天,付费会员无使用限制。<a href="#" class="info-link">点击付费会员</a></div>
|
||||
<div class="info-text">目前免费试用3天,付费会员无使用限制。<a href="#" class="info-link" @click.prevent="openSubscribeDialog">点击付费会员</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,7 +282,7 @@ function handleRetryTask() {
|
||||
</div>
|
||||
<div class="banner-actions">
|
||||
<el-button size="default" @click="handleNewTask">新建任务</el-button>
|
||||
<el-button v-if="trademarkPanelRef.queryStatus === 'done'" type="primary" size="default">导出数据</el-button>
|
||||
<el-button v-if="trademarkPanelRef.queryStatus === 'done'" type="primary" size="default" @click="handleExportData">导出数据</el-button>
|
||||
<el-button v-else type="primary" size="default" @click="handleRetryTask">重新筛查</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -378,15 +418,27 @@ function handleRetryTask() {
|
||||
</div>
|
||||
<div v-if="paginatedData.length === 0 && !(currentTab === 'trademark' && (trademarkPanelRef?.queryStatus === 'inProgress' || trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError' || trademarkPanelRef?.queryStatus === 'cancel'))" class="empty-abs">
|
||||
<!-- 商标筛查状态显示 -->
|
||||
<div v-if="currentTab === 'trademark' && trademarkPanelRef?.queryStatus && trademarkPanelRef.queryStatus !== 'inProgress' && trademarkPanelRef.queryStatus !== 'done' && trademarkPanelRef.queryStatus !== 'error' && trademarkPanelRef.queryStatus !== 'networkError' && trademarkPanelRef.queryStatus !== 'cancel'" class="empty-container">
|
||||
<img
|
||||
:src="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].icon"
|
||||
:alt="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].title"
|
||||
class="status-image"
|
||||
/>
|
||||
<div class="empty-title">{{ trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].title }}</div>
|
||||
<div v-if="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].desc" class="empty-desc">
|
||||
{{ trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].desc }}
|
||||
<div v-if="currentTab === 'trademark' && trademarkPanelRef?.queryStatus && trademarkPanelRef.queryStatus !== 'inProgress' && trademarkPanelRef.queryStatus !== 'done' && trademarkPanelRef.queryStatus !== 'error' && trademarkPanelRef.queryStatus !== 'networkError' && trademarkPanelRef.queryStatus !== 'cancel'" class="trademark-empty-wrapper">
|
||||
<div class="trademark-empty-content">
|
||||
<img src="/image/img.png" alt="暂无数据" class="empty-image" />
|
||||
<div class="empty-text">暂无数据,请按左侧流程操作</div>
|
||||
</div>
|
||||
|
||||
<div class="trademark-info-boxes">
|
||||
<div class="info-box">
|
||||
<div class="info-title">功能说明</div>
|
||||
<div class="info-text">从卖家精灵平台导出的产品表格,通过此功能,可以快速筛查文档中所有产品、品牌的商标注册状态以及是否允许跟卖。</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<div class="info-title">需要注意</div>
|
||||
<div class="info-text">请确保卖家精灵选品表、亚马逊账号与选择的专利地区一致,如不一致可能导致查询结果不准确或失败。</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<div class="info-title">收费标准</div>
|
||||
<div class="info-text">目前免费用户可以体验3天,付费会员无使用限制,点击<a href="#" class="info-link" @click.prevent="openSubscribeDialog">加入付费会员</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 其他tab的加载状态 -->
|
||||
@@ -394,8 +446,8 @@ function handleRetryTask() {
|
||||
<div class="spinner">⟳</div>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
<!-- 默认空状态 -->
|
||||
<div v-else class="empty-container">
|
||||
<!-- 默认空状态(跟卖精灵tab不显示) -->
|
||||
<div v-else-if="currentTab !== 'genmai'" class="empty-container">
|
||||
<img src="/image/img.png" alt="暂无数据" class="empty-image" />
|
||||
<div class="empty-text">暂无数据,请按左侧流程操作</div>
|
||||
</div>
|
||||
@@ -416,6 +468,9 @@ function handleRetryTask() {
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订阅会员弹窗 -->
|
||||
<TrialExpiredDialog v-model="showSubscribeDialog" expired-type="subscribe" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -713,6 +768,36 @@ function handleRetryTask() {
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 商标筛查空状态样式 */
|
||||
.trademark-empty-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.trademark-empty-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.trademark-info-boxes {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: 8px 0;
|
||||
gap: 10px;
|
||||
width: 90%;
|
||||
max-width: 872px;
|
||||
margin: 0 auto;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.genmai-image-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
@@ -44,6 +44,14 @@ function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'i
|
||||
ElMessage({ message, type })
|
||||
}
|
||||
|
||||
function removeSelectedFile() {
|
||||
selectedFileName.value = ''
|
||||
pendingAsins.value = []
|
||||
if (amazonUpload.value) {
|
||||
amazonUpload.value.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function processExcelFile(file: File) {
|
||||
try {
|
||||
loading.value = true
|
||||
@@ -275,6 +283,7 @@ defineExpose({
|
||||
<div v-if="selectedFileName" class="file-chip">
|
||||
<span class="dot"></span>
|
||||
<span class="name">{{ selectedFileName }}</span>
|
||||
<span class="delete-btn" @click="removeSelectedFile" title="删除文件">🗑️</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -352,7 +361,7 @@ defineExpose({
|
||||
.steps-flow:before { content: ''; position: absolute; left: 13px; top: 26px; bottom: 0; width: 2px; background: rgba(229, 231, 235, 0.6); }
|
||||
.flow-item { position: relative; display: grid; grid-template-columns: 28px 1fr; gap: 12px; padding: 10px 0; }
|
||||
.flow-item .step-index { position: static; width: 28px; height: 28px; line-height: 28px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 14px; font-weight: 600; margin-top: 2px; }
|
||||
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; }
|
||||
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; min-width: 0; }
|
||||
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||
.title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
|
||||
.desc { font-size: 12px; color: #909399; margin-bottom: 10px; text-align: left; line-height: 1.5; }
|
||||
@@ -364,9 +373,11 @@ defineExpose({
|
||||
.dz-el-icon { font-size: 18px; margin-bottom: 4px; color: #909399; }
|
||||
.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; }
|
||||
.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; width: 100%; box-sizing: border-box; }
|
||||
.file-chip .dot { width: 6px; height: 6px; background: #409EFF; border-radius: 50%; flex-shrink: 0; }
|
||||
.file-chip .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.file-chip .delete-btn { cursor: pointer; opacity: 0.6; flex-shrink: 0; }
|
||||
.file-chip .delete-btn:hover { opacity: 1; }
|
||||
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
|
||||
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
|
||||
.btn-blue:disabled { background: #a6c8ff; border-color: #a6c8ff; color: #fff; }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, defineAsyncComponent } from 'vue'
|
||||
import { ref, inject, defineAsyncComponent, onMounted, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { handlePlatformFileExport } from '../../utils/settings'
|
||||
import { getUsernameFromToken } from '../../utils/token'
|
||||
@@ -16,16 +16,20 @@ const emit = defineEmits<{
|
||||
updateData: [data: any[]]
|
||||
}>()
|
||||
|
||||
const trademarkData = ref<any[]>([])
|
||||
const trademarkData = ref<any[]>([]) // 用于显示的数据(可能是简化版)
|
||||
const trademarkFullData = ref<Record<string, any>[]>([]) // 完整的行数据(用于导出)
|
||||
const trademarkHeaders = ref<string[]>([]) // 表头
|
||||
const trademarkFileName = ref('')
|
||||
const trademarkFile = ref<File | null>(null)
|
||||
const trademarkUpload = ref<HTMLInputElement | null>(null)
|
||||
const trademarkLoading = ref(false)
|
||||
const uploadLoading = ref(false)
|
||||
const trademarkProgress = ref(0)
|
||||
const exportLoading = ref(false)
|
||||
const currentStep = ref(0)
|
||||
const totalSteps = ref(0)
|
||||
let brandProgressTimer: any = null
|
||||
const brandTaskId = ref('')
|
||||
|
||||
// 三个任务的进度数据
|
||||
const taskProgress = ref({
|
||||
@@ -82,8 +86,18 @@ const regionOptions = [
|
||||
{ label: '美国', value: '美国', flag: '🇺🇸' }
|
||||
]
|
||||
|
||||
// 查询类型多选
|
||||
const queryTypes = ref<string[]>(['product'])
|
||||
// 查询类型多选(默认全部勾选)
|
||||
const queryTypes = ref<string[]>(['product', 'brand'])
|
||||
|
||||
// 计算已完成的配置步骤数
|
||||
const completedSteps = computed(() => {
|
||||
let count = 0
|
||||
if (trademarkFileName.value) count++
|
||||
if (selectedAccount.value) count++
|
||||
if (region.value) count++
|
||||
if (queryTypes.value.length > 0) count++
|
||||
return count
|
||||
})
|
||||
|
||||
const showTrialExpiredDialog = ref(false)
|
||||
const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account')
|
||||
@@ -93,6 +107,80 @@ function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'i
|
||||
ElMessage({ message, type })
|
||||
}
|
||||
|
||||
function removeTrademarkFile() {
|
||||
trademarkFileName.value = ''
|
||||
trademarkFile.value = null
|
||||
uploadLoading.value = false
|
||||
if (trademarkUpload.value) {
|
||||
trademarkUpload.value.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 保存会话到后端
|
||||
async function saveSession() {
|
||||
if (!trademarkData.value.length) return
|
||||
|
||||
try {
|
||||
const sessionData = {
|
||||
fileName: trademarkFileName.value,
|
||||
resultData: trademarkData.value,
|
||||
fullData: trademarkFullData.value,
|
||||
headers: trademarkHeaders.value,
|
||||
taskProgress: taskProgress.value,
|
||||
queryStatus: queryStatus.value
|
||||
}
|
||||
|
||||
const result = await markApi.saveSession(sessionData)
|
||||
if (result.code === 200 || result.code === 0) {
|
||||
const username = getUsernameFromToken()
|
||||
localStorage.setItem(`trademark_session_${username}`, JSON.stringify({
|
||||
sessionId: result.data.sessionId,
|
||||
timestamp: Date.now()
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存会话失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 从后端恢复会话
|
||||
async function restoreSession() {
|
||||
try {
|
||||
const username = getUsernameFromToken()
|
||||
const saved = localStorage.getItem(`trademark_session_${username}`)
|
||||
if (!saved) return
|
||||
|
||||
const { sessionId, timestamp } = JSON.parse(saved)
|
||||
|
||||
// 检查是否在7天内
|
||||
if (Date.now() - timestamp > 7 * 24 * 60 * 60 * 1000) {
|
||||
localStorage.removeItem(`trademark_session_${username}`)
|
||||
return
|
||||
}
|
||||
|
||||
const result = await markApi.getSession(sessionId)
|
||||
if (result.code === 200 || result.code === 0) {
|
||||
trademarkFileName.value = result.data.fileName || ''
|
||||
trademarkData.value = result.data.resultData || []
|
||||
trademarkFullData.value = result.data.fullData || []
|
||||
trademarkHeaders.value = result.data.headers || []
|
||||
queryStatus.value = result.data.queryStatus || 'idle'
|
||||
|
||||
if (result.data.taskProgress) {
|
||||
Object.assign(taskProgress.value, result.data.taskProgress)
|
||||
}
|
||||
|
||||
emit('updateData', trademarkData.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('恢复会话失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
restoreSession()
|
||||
})
|
||||
|
||||
function openTrademarkUpload() {
|
||||
trademarkUpload.value?.click()
|
||||
}
|
||||
@@ -108,13 +196,41 @@ async function handleTrademarkUpload(e: Event) {
|
||||
return
|
||||
}
|
||||
|
||||
trademarkFileName.value = file.name
|
||||
trademarkFile.value = file
|
||||
queryStatus.value = 'idle' // 重置状态
|
||||
trademarkData.value = []
|
||||
emit('updateData', [])
|
||||
showMessage(`文件已准备:${file.name}`, 'success')
|
||||
input.value = ''
|
||||
uploadLoading.value = true
|
||||
|
||||
try {
|
||||
// 根据选中的查询类型确定需要的表头
|
||||
const requiredHeaders: string[] = []
|
||||
if (queryTypes.value.includes('product')) {
|
||||
requiredHeaders.push('商品主图')
|
||||
}
|
||||
if (queryTypes.value.includes('brand')) {
|
||||
requiredHeaders.push('品牌')
|
||||
}
|
||||
|
||||
// 验证表头
|
||||
if (requiredHeaders.length > 0) {
|
||||
const validateResult = await markApi.validateHeaders(file, requiredHeaders)
|
||||
if (validateResult.code !== 200 && validateResult.code !== 0) {
|
||||
showMessage(validateResult.msg || '表头验证失败', 'error')
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
trademarkFileName.value = file.name
|
||||
trademarkFile.value = file
|
||||
queryStatus.value = 'idle'
|
||||
trademarkData.value = []
|
||||
trademarkFullData.value = []
|
||||
trademarkHeaders.value = []
|
||||
emit('updateData', [])
|
||||
} catch (error: any) {
|
||||
showMessage('表头验证失败: ' + error.message, 'error')
|
||||
} finally {
|
||||
uploadLoading.value = false
|
||||
input.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function startTrademarkQuery() {
|
||||
@@ -137,6 +253,8 @@ async function startTrademarkQuery() {
|
||||
trademarkLoading.value = true
|
||||
trademarkProgress.value = 0
|
||||
trademarkData.value = []
|
||||
trademarkFullData.value = []
|
||||
trademarkHeaders.value = []
|
||||
queryStatus.value = 'inProgress'
|
||||
|
||||
// 重置任务进度
|
||||
@@ -159,15 +277,12 @@ async function startTrademarkQuery() {
|
||||
|
||||
if (needProductCheck) {
|
||||
// 步骤1: 产品商标筛查 - 调用新建任务接口
|
||||
showMessage('正在上传文件...', 'info')
|
||||
const createResult = await markApi.newTask(trademarkFile.value)
|
||||
|
||||
if (createResult.code !== 200 && createResult.code !== 0) {
|
||||
throw new Error(createResult.msg || '创建任务失败')
|
||||
}
|
||||
|
||||
showMessage('文件上传成功,正在处理...', 'success')
|
||||
|
||||
const taskData = taskProgress.value.product
|
||||
taskData.total = 100 // 设置临时总数以显示进度动画
|
||||
taskData.current = 5 // 立即显示初始进度
|
||||
@@ -235,35 +350,56 @@ async function startTrademarkQuery() {
|
||||
clearInterval(progressTimer)
|
||||
}
|
||||
|
||||
// 映射后端数据到前端格式
|
||||
trademarkData.value = productResult.data.filtered.map((item: any) => ({
|
||||
name: item['品牌'] || '',
|
||||
status: item['商标类型'] || '',
|
||||
// 第三方API返回的筛查结果(TM和未注册的商品)
|
||||
const thirdPartyFiltered = productResult.data.filtered
|
||||
|
||||
// 提取ASIN列表
|
||||
const filteredAsins = thirdPartyFiltered
|
||||
.map((item: any) => item['ASIN'])
|
||||
.filter((asin: string) => asin && asin.trim())
|
||||
|
||||
if (filteredAsins.length === 0) {
|
||||
showMessage('第三方筛查结果为空', 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
// 从原始Excel中过滤出这些ASIN的完整行(保持原始表头和列)
|
||||
const originalFilterResult = await markApi.filterByAsins(trademarkFile.value, filteredAsins)
|
||||
|
||||
if (originalFilterResult.code !== 200 && originalFilterResult.code !== 0) {
|
||||
throw new Error(originalFilterResult.msg || '从原始文件过滤失败')
|
||||
}
|
||||
|
||||
// 保存完整数据(用于导出,使用原始表头)
|
||||
trademarkFullData.value = originalFilterResult.data.filteredRows
|
||||
trademarkHeaders.value = originalFilterResult.data.headers || []
|
||||
|
||||
// 映射后端数据到前端格式(用于显示)
|
||||
trademarkData.value = originalFilterResult.data.filteredRows.map((row: any) => ({
|
||||
name: row['品牌'] || '',
|
||||
status: row['商标类型'] || '',
|
||||
class: '',
|
||||
owner: '',
|
||||
expireDate: item['注册时间'] || '',
|
||||
expireDate: row['注册时间'] || '',
|
||||
similarity: 0,
|
||||
asin: item['ASIN'],
|
||||
productImage: item['商品主图']
|
||||
asin: row['ASIN'],
|
||||
productImage: row['商品主图']
|
||||
}))
|
||||
|
||||
// 如果需要品牌筛查,从产品结果中提取品牌列表
|
||||
// 如果需要品牌筛查,从产品结果中提取品牌列表(不去重,保持和产品数量一致)
|
||||
if (needBrandCheck) {
|
||||
brandList = productResult.data.filtered
|
||||
brandList = thirdPartyFiltered
|
||||
.map((item: any) => item['品牌'])
|
||||
.filter((brand: string) => brand && brand.trim())
|
||||
}
|
||||
|
||||
showMessage(`产品筛查完成,共 ${taskData.total} 条,筛查出 ${taskData.completed} 条`, 'success')
|
||||
}
|
||||
|
||||
// 品牌商标筛查
|
||||
if (needBrandCheck) {
|
||||
if (!trademarkLoading.value) return
|
||||
|
||||
// 如果没有执行产品筛查,需要先从Excel提取品牌列表
|
||||
// 如果没有执行产品筛查,需要先从Excel提取品牌列表和完整数据
|
||||
if (!needProductCheck) {
|
||||
showMessage('正在从Excel提取品牌列表...', 'info')
|
||||
const extractResult = await markApi.extractBrands(trademarkFile.value)
|
||||
|
||||
if (extractResult.code !== 200 && extractResult.code !== 0) {
|
||||
@@ -275,7 +411,8 @@ async function startTrademarkQuery() {
|
||||
}
|
||||
|
||||
brandList = extractResult.data.brands
|
||||
showMessage(`品牌列表提取成功,共 ${brandList.length} 个品牌`, 'success')
|
||||
// 保存表头和完整数据(如果没有产品筛查)
|
||||
trademarkHeaders.value = extractResult.data.headers || []
|
||||
}
|
||||
|
||||
if (brandList.length === 0) {
|
||||
@@ -283,42 +420,69 @@ async function startTrademarkQuery() {
|
||||
} else {
|
||||
const brandData = taskProgress.value.brand
|
||||
brandData.total = brandList.length
|
||||
brandData.current = 0
|
||||
brandData.current = 1 // 立即显示初始进度
|
||||
brandData.completed = 0
|
||||
|
||||
showMessage(`开始品牌商标筛查,共 ${brandList.length} 个品牌...`, 'info')
|
||||
|
||||
// 模拟进度动画
|
||||
brandProgressTimer = setInterval(() => {
|
||||
if (brandData.current < brandList.length * 0.95) {
|
||||
brandData.current = Math.min(brandData.current + 20, brandList.length * 0.95)
|
||||
// 生成任务ID并轮询真实进度
|
||||
brandTaskId.value = `task_${Date.now()}`
|
||||
brandProgressTimer = setInterval(async () => {
|
||||
try {
|
||||
const res = await markApi.getBrandCheckProgress(brandTaskId.value)
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
brandData.current = res.data.current || 0
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略进度查询错误
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
const brandResult = await markApi.brandCheck(brandList)
|
||||
const brandResult = await markApi.brandCheck(brandList, brandTaskId.value)
|
||||
if (brandProgressTimer) clearInterval(brandProgressTimer)
|
||||
|
||||
if (!trademarkLoading.value) return
|
||||
|
||||
if (brandResult.code === 200 || brandResult.code === 0) {
|
||||
// 完成,显示100%
|
||||
brandData.total = brandResult.data.checked || brandResult.data.total || brandData.total
|
||||
brandData.current = brandData.total
|
||||
brandData.completed = brandResult.data.filtered
|
||||
brandData.completed = brandResult.data.unregistered || 0
|
||||
|
||||
// 将品牌筛查结果追加到展示数据中
|
||||
const brandItems = brandResult.data.data.map((item: any) => ({
|
||||
name: item.brand || '',
|
||||
status: item.status || '未注册',
|
||||
class: '',
|
||||
owner: '',
|
||||
expireDate: '',
|
||||
similarity: 0,
|
||||
isBrand: true // 标记为品牌数据
|
||||
}))
|
||||
// 提取未注册品牌列表
|
||||
const unregisteredBrands = brandResult.data.data.map((item: any) => item.brand).filter(Boolean)
|
||||
|
||||
trademarkData.value = [...trademarkData.value, ...brandItems]
|
||||
|
||||
showMessage(`品牌筛查完成,共查询 ${brandData.total} 个品牌,筛查出 ${brandData.completed} 个未注册品牌`, 'success')
|
||||
if (unregisteredBrands.length > 0) {
|
||||
// 从原始Excel中过滤出包含这些品牌的完整行
|
||||
const filterResult = await markApi.filterByBrands(trademarkFile.value, unregisteredBrands)
|
||||
|
||||
if (filterResult.code === 200 || filterResult.code === 0) {
|
||||
// 保存完整数据(用于导出,使用原始表头)
|
||||
trademarkFullData.value = filterResult.data.filteredRows
|
||||
trademarkHeaders.value = filterResult.data.headers || []
|
||||
|
||||
// 更新统计:显示过滤出的实际行数(而不是品牌数)
|
||||
brandData.completed = filterResult.data.filteredRows.length
|
||||
|
||||
// 将品牌筛查结果作为展示数据
|
||||
const brandItems = filterResult.data.filteredRows.map((row: any) => ({
|
||||
name: row['品牌'] || '',
|
||||
status: '未注册',
|
||||
class: '',
|
||||
owner: '',
|
||||
expireDate: row['注册时间'] || '',
|
||||
similarity: 0,
|
||||
asin: row['ASIN'] || '',
|
||||
productImage: row['商品主图'] || '',
|
||||
isBrand: true // 标记为品牌数据
|
||||
}))
|
||||
|
||||
// 如果有产品筛查,也替换展示数据(只显示品牌筛查结果)
|
||||
if (needProductCheck) {
|
||||
trademarkData.value = brandItems
|
||||
} else {
|
||||
trademarkData.value = [...trademarkData.value, ...brandItems]
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(brandResult.msg || '品牌筛查失败')
|
||||
}
|
||||
@@ -333,6 +497,9 @@ async function startTrademarkQuery() {
|
||||
if (needProductCheck) summaryMsg += `,产品:${taskProgress.value.product.completed}/${taskProgress.value.product.total}`
|
||||
if (needBrandCheck && brandList.length > 0) summaryMsg += `,品牌:${taskProgress.value.brand.completed}/${taskProgress.value.brand.total}`
|
||||
showMessage(summaryMsg, 'success')
|
||||
|
||||
// 保存会话
|
||||
await saveSession()
|
||||
}
|
||||
} catch (error: any) {
|
||||
const hasProductData = taskProgress.value.product.total > 0
|
||||
@@ -356,6 +523,8 @@ async function startTrademarkQuery() {
|
||||
// 仅在第1步失败时清空数据
|
||||
if (!hasProductData) {
|
||||
trademarkData.value = []
|
||||
trademarkFullData.value = []
|
||||
trademarkHeaders.value = []
|
||||
emit('updateData', [])
|
||||
} else {
|
||||
emit('updateData', trademarkData.value)
|
||||
@@ -374,51 +543,64 @@ async function startTrademarkQuery() {
|
||||
}
|
||||
|
||||
function stopTrademarkQuery() {
|
||||
// 清除进度动画定时器
|
||||
// 通知后端取消任务
|
||||
if (brandTaskId.value) {
|
||||
markApi.cancelBrandCheck(brandTaskId.value).catch(() => {
|
||||
// 忽略取消失败
|
||||
})
|
||||
}
|
||||
|
||||
// 清除进度定时器
|
||||
if (brandProgressTimer) {
|
||||
clearInterval(brandProgressTimer)
|
||||
brandProgressTimer = null
|
||||
}
|
||||
|
||||
trademarkLoading.value = false
|
||||
queryStatus.value = 'cancel'
|
||||
currentStep.value = 0
|
||||
totalSteps.value = 0
|
||||
showMessage('已停止筛查', 'info')
|
||||
}
|
||||
|
||||
async function exportTrademarkData() {
|
||||
if (!trademarkData.value.length) {
|
||||
if (!trademarkFullData.value.length || !trademarkHeaders.value.length) {
|
||||
showMessage('没有数据可供导出', 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
exportLoading.value = true
|
||||
|
||||
let html = `<table>
|
||||
<tr><th>商标名称</th><th>状态</th><th>类别</th><th>权利人</th><th>到期日期</th><th>相似度</th></tr>`
|
||||
|
||||
trademarkData.value.forEach(item => {
|
||||
html += `<tr>
|
||||
<td>${item.name || ''}</td>
|
||||
<td>${item.status || ''}</td>
|
||||
<td>${item.class || ''}</td>
|
||||
<td>${item.owner || ''}</td>
|
||||
<td>${item.expireDate || ''}</td>
|
||||
<td>${item.similarity ? item.similarity + '%' : '-'}</td>
|
||||
</tr>`
|
||||
})
|
||||
html += '</table>'
|
||||
|
||||
const blob = new Blob([html], { type: 'application/vnd.ms-excel' })
|
||||
const fileName = `商标筛查结果_${new Date().toISOString().slice(0, 10)}.xls`
|
||||
|
||||
const username = getUsernameFromToken()
|
||||
const success = await handlePlatformFileExport('amazon', blob, fileName, username)
|
||||
|
||||
if (success) {
|
||||
showMessage('Excel文件导出成功!', 'success')
|
||||
try {
|
||||
let html = '<table border="1" style="border-collapse:collapse;"><tr>'
|
||||
trademarkHeaders.value.forEach(header => {
|
||||
html += `<th>${header || ''}</th>`
|
||||
})
|
||||
html += '</tr>'
|
||||
|
||||
trademarkFullData.value.forEach(row => {
|
||||
html += '<tr>'
|
||||
trademarkHeaders.value.forEach(header => {
|
||||
const cellValue = row[header]
|
||||
html += `<td>${cellValue !== null && cellValue !== undefined ? cellValue : ''}</td>`
|
||||
})
|
||||
html += '</tr>'
|
||||
})
|
||||
html += '</table>'
|
||||
|
||||
const blob = new Blob([html], { type: 'application/vnd.ms-excel' })
|
||||
const fileName = `商标筛查结果_${new Date().toISOString().slice(0, 10)}.xls`
|
||||
|
||||
const username = getUsernameFromToken()
|
||||
const success = await handlePlatformFileExport('amazon', blob, fileName, username)
|
||||
|
||||
if (success) {
|
||||
showMessage('Excel文件导出成功!', 'success')
|
||||
}
|
||||
} catch (error: any) {
|
||||
showMessage('导出失败: ' + error.message, 'error')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
exportLoading.value = false
|
||||
}
|
||||
|
||||
function toggleQueryType(type: string) {
|
||||
@@ -431,7 +613,7 @@ function toggleQueryType(type: string) {
|
||||
}
|
||||
|
||||
function viewTrademarkExample() {
|
||||
showMessage('商标列表应包含一列商标名称', 'info')
|
||||
// 查看示例
|
||||
}
|
||||
|
||||
function downloadTrademarkTemplate() {
|
||||
@@ -450,6 +632,8 @@ function downloadTrademarkTemplate() {
|
||||
function resetToIdle() {
|
||||
queryStatus.value = 'idle'
|
||||
trademarkData.value = []
|
||||
trademarkFullData.value = []
|
||||
trademarkHeaders.value = []
|
||||
trademarkFileName.value = ''
|
||||
trademarkFile.value = null
|
||||
taskProgress.value.product.total = 0
|
||||
@@ -461,6 +645,14 @@ function resetToIdle() {
|
||||
taskProgress.value.platform.total = 0
|
||||
taskProgress.value.platform.current = 0
|
||||
taskProgress.value.platform.completed = 0
|
||||
|
||||
// 清空localStorage中的会话数据
|
||||
try {
|
||||
const username = getUsernameFromToken()
|
||||
localStorage.removeItem(`trademark_session_${username}`)
|
||||
} catch (e) {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@@ -473,7 +665,8 @@ defineExpose({
|
||||
errorMessage,
|
||||
resetToIdle,
|
||||
stopTrademarkQuery,
|
||||
startTrademarkQuery
|
||||
startTrademarkQuery,
|
||||
exportTrademarkData
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -486,16 +679,18 @@ defineExpose({
|
||||
<div class="step-card">
|
||||
<div class="step-header"><div class="title">导入Excel表格</div></div>
|
||||
<div class="desc">产品筛查:需导入卖家精灵选品表格,并勾选"导出主图";品牌筛查:Excel需包含"品牌"列</div>
|
||||
<div class="dropzone" @click="openTrademarkUpload">
|
||||
<div class="dz-icon">📤</div>
|
||||
<div class="dz-text">点击或将文件拖拽到这里上传</div>
|
||||
<div class="dz-sub">支持 .xls .xlsx</div>
|
||||
<div class="dropzone" :class="{ uploading: uploadLoading }" @click="!uploadLoading && openTrademarkUpload()">
|
||||
<div v-if="!uploadLoading" class="dz-icon">📤</div>
|
||||
<div v-else class="dz-icon spinner">⟳</div>
|
||||
<div class="dz-text">{{ uploadLoading ? '正在验证表头...' : '点击或将文件拖拽到这里上传' }}</div>
|
||||
<div v-if="!uploadLoading" class="dz-sub">支持 .xls .xlsx</div>
|
||||
</div>
|
||||
<input ref="trademarkUpload" style="display:none" type="file" accept=".xls,.xlsx" @change="handleTrademarkUpload" :disabled="trademarkLoading" />
|
||||
<input ref="trademarkUpload" style="display:none" type="file" accept=".xls,.xlsx" @change="handleTrademarkUpload" :disabled="trademarkLoading || uploadLoading" />
|
||||
|
||||
<div v-if="trademarkFileName" class="file-chip">
|
||||
<span class="dot"></span>
|
||||
<span class="name">{{ trademarkFileName }}</span>
|
||||
<span class="delete-btn" @click="removeTrademarkFile" title="删除文件">🗑️</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -590,7 +785,7 @@ defineExpose({
|
||||
<!-- 底部开始查询按钮 -->
|
||||
<div class="bottom-action">
|
||||
<div class="action-header">
|
||||
<span class="step-indicator">{{ trademarkLoading ? currentStep : 1 }}/4</span>
|
||||
<span class="step-indicator">{{ completedSteps }}/4</span>
|
||||
<el-button v-if="!trademarkLoading" class="start-btn" type="primary" :disabled="!trademarkFileName || queryTypes.length === 0" @click="startTrademarkQuery">
|
||||
开始筛查
|
||||
</el-button>
|
||||
@@ -625,39 +820,18 @@ defineExpose({
|
||||
.steps-flow:before { content: ''; position: absolute; left: 13px; top: 26px; bottom: 0; width: 2px; background: rgba(229, 231, 235, 0.6); }
|
||||
.flow-item { position: relative; display: grid; grid-template-columns: 28px 1fr; gap: 12px; padding: 10px 0; }
|
||||
.flow-item .step-index { position: static; width: 28px; height: 28px; line-height: 28px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 14px; font-weight: 600; margin-top: 2px; }
|
||||
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; }
|
||||
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; min-width: 0; }
|
||||
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||
.title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
|
||||
.desc { font-size: 12px; color: #909399; margin-bottom: 10px; text-align: left; line-height: 1.5; }
|
||||
.links { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
|
||||
.link { color: #909399; cursor: pointer; 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;
|
||||
min-width: 0;
|
||||
}
|
||||
.file-chip .dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: #409EFF;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.file-chip .name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.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; width: 100%; box-sizing: border-box; }
|
||||
.file-chip .dot { width: 6px; height: 6px; background: #409EFF; border-radius: 50%; flex-shrink: 0; }
|
||||
.file-chip .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.file-chip .delete-btn { cursor: pointer; opacity: 0.6; flex-shrink: 0; }
|
||||
.file-chip .delete-btn:hover { opacity: 1; }
|
||||
|
||||
.dropzone {
|
||||
border: 1px dashed #c0c4cc;
|
||||
@@ -669,9 +843,13 @@ defineExpose({
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.dropzone:hover { background: #f6fbff; border-color: #409EFF; }
|
||||
.dropzone.uploading { cursor: not-allowed; opacity: 0.7; }
|
||||
.dropzone.uploading:hover { background: #fafafa; border-color: #c0c4cc; }
|
||||
.dz-icon { font-size: 20px; margin-bottom: 6px; color: #909399; }
|
||||
.dz-text { color: #303133; font-size: 13px; margin-bottom: 2px; }
|
||||
.dz-sub { color: #909399; font-size: 12px; }
|
||||
.spinner { animation: spin 1s linear infinite; }
|
||||
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||
|
||||
.query-options {
|
||||
display: flex;
|
||||
|
||||
@@ -363,6 +363,14 @@ function showMessage(message: string, type: 'info' | 'success' | 'warning' | 'er
|
||||
ElMessage({ message, type })
|
||||
}
|
||||
|
||||
function removeSelectedFile() {
|
||||
selectedFileName.value = ''
|
||||
pendingFile.value = null
|
||||
if (uploadInputRef.value) {
|
||||
uploadInputRef.value.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function exportToExcel() {
|
||||
if (!allProducts.value.length) {
|
||||
showMessage('没有数据可供导出', 'warning')
|
||||
@@ -481,6 +489,7 @@ onMounted(loadLatest)
|
||||
<div v-if="selectedFileName" class="file-chip">
|
||||
<span class="dot"></span>
|
||||
<span class="name">{{ selectedFileName }}</span>
|
||||
<span class="delete-btn" @click="removeSelectedFile" title="删除文件">🗑️</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -665,7 +674,7 @@ onMounted(loadLatest)
|
||||
.flow-item { position: relative; display: grid; grid-template-columns: 22px 1fr; gap: 10px; padding: 8px 0; }
|
||||
.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; }
|
||||
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; min-width: 0; }
|
||||
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
||||
.title { font-size: 13px; font-weight: 600; color: #303133; text-align: left; }
|
||||
.desc { font-size: 12px; color: #909399; margin-bottom: 8px; text-align: left; }
|
||||
@@ -686,9 +695,11 @@ onMounted(loadLatest)
|
||||
.single-input.left { display: flex; gap: 8px; }
|
||||
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
|
||||
|
||||
.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; }
|
||||
.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; width: 100%; box-sizing: border-box; }
|
||||
.file-chip .dot { width: 6px; height: 6px; background: #409EFF; border-radius: 50%; flex-shrink: 0; }
|
||||
.file-chip .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.file-chip .delete-btn { cursor: pointer; opacity: 0.6; flex-shrink: 0; }
|
||||
.file-chip .delete-btn:hover { opacity: 1; }
|
||||
|
||||
.progress-section.left { margin-top: 10px; }
|
||||
.full { width: 100%; }
|
||||
|
||||
Reference in New Issue
Block a user