feat(electron):优化商标筛查面板与资源加载逻辑

- 将多个 v-if 条件渲染改为 v-show,提升组件切换性能
- 优化商标任务完成状态判断逻辑,确保准确显示采集完成图标- 调整任务统计数据显示条件,支持零数据展示- 更新 API 配置地址,切换至本地开发环境地址
- 降低 Spring Boot 线程池与数据库连接池配置,适应小规模并发- 禁用 devtools 热部署与 Swagger 接口文档,优化生产环境性能
- 配置 RestTemplate 使用 HttpClient 连接池,增强 HTTP 请求稳定性
- 改进静态资源拷贝脚本,确保 icon 与 image 文件夹正确复制
- 更新 electron-builder 配置,优化资源打包路径与应用图标
- 修改 HTTP 路由规则,明确区分客户端与管理端接口路径- 注册文件协议拦截器,解决生产环境下 icon/image 资源加载问题
- 调整商标 API 接口路径,指向 erp_client_sb服务
-重构 MarkController 控制器,专注 Token 管理功能
- 优化线程池参数,适配低并发业务场景- 强化商标筛查流程控制,完善任务取消与异常处理机制
- 新增方舟精选任务管理接口,实现 Excel 下载与数据解析功能
This commit is contained in:
2025-11-06 14:39:58 +08:00
parent 2f00fde3be
commit 7c7009ffed
13 changed files with 255 additions and 114 deletions

View File

@@ -4,10 +4,9 @@ import {join, dirname, basename, extname} from 'path';
import {spawn, ChildProcess} from 'child_process';
import * as https from 'https';
import * as http from 'http';
import { fileURLToPath } from 'url';
import { createTray, destroyTray } from './tray';
const isDev = process.env.NODE_ENV === 'development';
let springProcess: ChildProcess | null = null;
let mainWindow: BrowserWindow | null = null;
let splashWindow: BrowserWindow | null = null;
@@ -74,7 +73,7 @@ function getJarFilePath(): string {
}
const getSplashPath = () => getResourcePath('../../public/splash.html', 'public/splash.html');
const getIconPath = () => getResourcePath('../../public/icon/icon1.png', 'public/icon/icon1.png', '../renderer/icon/icon1.png');
const getIconPath = () => getResourcePath('../../public/icon/icon1.png', 'public/icon/icon1.png');
const getLogbackConfigPath = () => getResourcePath('../../public/config/logback.xml', 'public/config/logback.xml');
function getDataDirectoryPath(): string {
@@ -211,7 +210,7 @@ function startSpringBoot() {
}
}
// startSpringBoot();
startSpringBoot();
function stopSpringBoot() {
if (!springProcess) return;
try {
@@ -305,14 +304,21 @@ if (!gotTheLock) {
}
app.whenReady().then(() => {
// 注册文件协议拦截器,将 /icon/ 和 /image/ 请求重定向到 public 目录
if (!isDev) {
protocol.interceptFileProtocol('file', (request, callback) => {
let url = request.url.substring(8); // 移除 'file:///'
// 使用 fileURLToPath 正确解码 URL处理空格和特殊字符
let filePath: string;
try {
filePath = fileURLToPath(request.url);
} catch (e) {
// 如果解码失败,回退到原来的方法
filePath = decodeURIComponent(request.url.substring(8));
}
// 检查是否是 icon 或 image 资源请求
if (url.includes('/icon/') || url.includes('/image/')) {
const match = url.match(/\/(icon|image)\/([^?#]+)/);
if (filePath.includes('/icon/') || filePath.includes('\\icon\\') ||
filePath.includes('/image/') || filePath.includes('\\image\\')) {
const match = filePath.match(/[/\\](icon|image)[/\\]([^?#]+)/);
if (match) {
const [, type, filename] = match;
const publicPath = join(process.resourcesPath, 'app.asar.unpacked', 'public', type, filename);
@@ -323,7 +329,7 @@ app.whenReady().then(() => {
}
}
callback({ path: url });
callback({ path: filePath });
});
}
@@ -370,9 +376,9 @@ app.whenReady().then(() => {
}
}
//666
setTimeout(() => {
openAppIfNotOpened();
}, 100);
// setTimeout(() => {
// openAppIfNotOpened();
// }, 100);
app.on('activate', () => {
if (mainWindow && !mainWindow.isDestroyed()) {

View File

@@ -9,9 +9,7 @@ function getIconPath(): string {
if (isDev) {
return join(__dirname, '../../public/icon/icon1.png')
}
const bundledPath = join(process.resourcesPath, 'app.asar.unpacked', 'public/icon/icon1.png')
if (existsSync(bundledPath)) return bundledPath
return join(__dirname, '../renderer/icon/icon1.png')
return join(process.resourcesPath, 'app.asar.unpacked', 'public/icon/icon1.png')
}
export function createTray(mainWindow: BrowserWindow | null) {

View File

@@ -1,11 +1,11 @@
export type HttpMethod = 'GET' | 'POST' | 'DELETE';
const RUOYI_BASE = 'http://8.138.23.49:8085';
// const RUOYI_BASE = 'http://192.168.1.89:8085';
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,
SSE_URL: `${RUOYI_BASE}/monitor/account/events`
} as const;
function resolveBase(path: string): string {
// 路由到 ruoyi-admin (8085):仅系统管理和监控相关
if (path.startsWith('/monitor/') || path.startsWith('/system/') || path.startsWith('/tool/banma') || path.startsWith('/tool/genmai')) {

View File

@@ -0,0 +1,9 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
}

View File

@@ -0,0 +1,31 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
}
}

View File

@@ -249,7 +249,7 @@ function handleExportData() {
</div>
<div class="banner-content">
<div class="banner-title">数据筛查中...</div>
<div class="banner-desc">确保结果准确耐心等待保持后台运行...</div>
<div class="banner-desc">避免进程中断勿退出应用保持后台运行...</div>
</div>
<div class="banner-actions">
<el-button size="default" @click="handleCancelTask">取消</el-button>
@@ -278,7 +278,7 @@ function handleExportData() {
</div>
<div class="banner-content">
<div class="banner-title">{{ trademarkPanelRef.queryStatus === 'done' ? '筛查已完成' : '数据筛查失败' }}</div>
<div class="banner-desc">{{ trademarkPanelRef.queryStatus === 'done' ? '点击"导出数据"按钮,可导出为 Excel 表格文。' : (trademarkPanelRef.errorMessage || '请稍后重试') }}</div>
<div class="banner-desc">{{ trademarkPanelRef.queryStatus === 'done' ? '点击右侧“导出数据按钮,可导出为 Excel 表格文档。如过滤结果为 0请排查选品表格内容是否正确产品所属地区与商标查询地区是否不一致如不一致可能导致筛查结果偏差或错误。' : (trademarkPanelRef.errorMessage || '请稍后重试') }}</div>
</div>
<div class="banner-actions">
<el-button size="default" @click="handleNewTask">新建任务</el-button>
@@ -328,7 +328,7 @@ function handleExportData() {
<div class="task-title-row">
<div class="task-info">
<div class="task-name">{{ trademarkPanelRef?.taskProgress?.product?.label || '未注册/TM商标筛查' }}</div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.product?.desc || '筛查未注册商标或TM标的产品' }}<span v-if="(trademarkPanelRef?.taskProgress?.product?.total || 0) > 0"> (已完成)</span></div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.product?.desc || '筛查未注册商标或TM标的产品' }}<span v-if="trademarkPanelRef?.isProductTaskRealData && (trademarkPanelRef?.taskProgress?.product?.current || 0) >= trademarkPanelRef.taskProgress.product.total"> (已完成)</span></div>
</div>
</div>
<div class="task-progress-wrapper">
@@ -340,15 +340,15 @@ function handleExportData() {
<div class="task-stats">
<div class="task-stat">
<span class="stat-label">查询数量</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 ? trademarkPanelRef.taskProgress.product.total : '-' }}</span>
<span class="stat-value">{{ trademarkPanelRef?.isProductTaskRealData ? trademarkPanelRef.taskProgress.product.total : '-' }}</span>
</div>
<div class="task-stat highlight">
<span class="stat-label">未注册/TM标</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 && (trademarkPanelRef?.taskProgress?.product?.current || 0) >= trademarkPanelRef.taskProgress.product.total ? (trademarkPanelRef?.taskProgress?.product?.completed || 0) : '-' }}</span>
<span class="stat-value">{{ trademarkPanelRef?.isProductTaskRealData ? trademarkPanelRef.taskProgress.product.completed : '-' }}</span>
</div>
<div class="task-stat">
<span class="stat-label">已过滤</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 && (trademarkPanelRef?.taskProgress?.product?.current || 0) >= trademarkPanelRef.taskProgress.product.total ? ((trademarkPanelRef?.taskProgress?.product?.total || 0) - (trademarkPanelRef?.taskProgress?.product?.completed || 0)) : '-' }}</span>
<span class="stat-value">{{ trademarkPanelRef?.isProductTaskRealData ? (trademarkPanelRef.taskProgress.product.total - trademarkPanelRef.taskProgress.product.completed) : '-' }}</span>
</div>
</div>
</div>
@@ -357,7 +357,7 @@ function handleExportData() {
<div class="task-title-row">
<div class="task-info">
<div class="task-name">{{ trademarkPanelRef?.taskProgress?.brand?.label || '品牌商标筛查' }}</div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.brand?.desc || '筛查未注册商标的品牌' }}<span v-if="(trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 && (trademarkPanelRef?.taskProgress?.brand?.current || 0) >= trademarkPanelRef.taskProgress.brand.total"> (已完成)</span></div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.brand?.desc || '筛查未注册商标的品牌' }}<span v-if="trademarkPanelRef?.isBrandTaskRealData && (trademarkPanelRef?.taskProgress?.brand?.current || 0) >= trademarkPanelRef.taskProgress.brand.total"> (已完成)</span></div>
</div>
</div>
<div class="task-progress-wrapper">
@@ -369,15 +369,15 @@ function handleExportData() {
<div class="task-stats">
<div class="task-stat">
<span class="stat-label">查询数量</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.brand?.total || 0) === 0 ? '-' : trademarkPanelRef.taskProgress.brand.total }}</span>
<span class="stat-value">{{ trademarkPanelRef?.isBrandTaskRealData ? trademarkPanelRef.taskProgress.brand.total : '-' }}</span>
</div>
<div class="task-stat highlight">
<span class="stat-label">未注册</span>
<span class="stat-value">{{ ((trademarkPanelRef?.taskProgress?.brand?.current || 0) >= (trademarkPanelRef?.taskProgress?.brand?.total || 1)) ? (trademarkPanelRef?.taskProgress?.brand?.completed || 0) : '-' }}</span>
<span class="stat-value">{{ trademarkPanelRef?.isBrandTaskRealData ? trademarkPanelRef.taskProgress.brand.completed : '-' }}</span>
</div>
<div class="task-stat">
<span class="stat-label">已注册</span>
<span class="stat-value">{{ ((trademarkPanelRef?.taskProgress?.brand?.current || 0) >= (trademarkPanelRef?.taskProgress?.brand?.total || 1)) ? ((trademarkPanelRef?.taskProgress?.brand?.total || 0) - (trademarkPanelRef?.taskProgress?.brand?.completed || 0)) : '-' }}</span>
<span class="stat-value">{{ trademarkPanelRef?.isBrandTaskRealData ? (trademarkPanelRef.taskProgress.brand.total - trademarkPanelRef.taskProgress.brand.completed) : '-' }}</span>
</div>
</div>
</div>

View File

@@ -4,6 +4,7 @@ import { ElMessage } from 'element-plus'
import { handlePlatformFileExport } from '../../utils/settings'
import { getUsernameFromToken } from '../../utils/token'
import { markApi } from '../../api/mark'
import { useFileDrop } from '../../composables/useFileDrop'
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
@@ -31,6 +32,10 @@ const totalSteps = ref(0)
let brandProgressTimer: any = null
const brandTaskId = ref('')
// 真实数据标记(区分临时进度值和真实统计数据)
const isProductTaskRealData = ref(false)
const isBrandTaskRealData = ref(false)
// 三个任务的进度数据
const taskProgress = ref({
product: { total: 0, current: 0, completed: 0, label: '产品商标筛查', desc: '筛查未注册商标或TM标的商品' },
@@ -107,6 +112,49 @@ function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'i
ElMessage({ message, type })
}
// 拖拽上传
async function processTrademarkFile(file: File) {
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')
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
}
}
const { dragActive, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop({
accept: /\.xlsx?$/i,
onFile: processTrademarkFile,
onError: (msg) => showMessage(msg, 'warning')
})
function removeTrademarkFile() {
trademarkFileName.value = ''
trademarkFile.value = null
@@ -196,41 +244,8 @@ async function handleTrademarkUpload(e: Event) {
return
}
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 = ''
}
await processTrademarkFile(file)
input.value = ''
}
async function startTrademarkQuery() {
@@ -268,6 +283,10 @@ async function startTrademarkQuery() {
taskProgress.value.platform.current = 0
taskProgress.value.platform.completed = 0
// 重置真实数据标记
isProductTaskRealData.value = false
isBrandTaskRealData.value = false
// 通知父组件更新数据
emit('updateData', [])
@@ -297,7 +316,7 @@ async function startTrademarkQuery() {
// 轮询检查任务状态
const pollTask = async () => {
const maxWaitTime = 60000
const pollInterval = 3000
const pollInterval = 2000
const startTime = Date.now()
let taskResult: any = null
@@ -307,7 +326,16 @@ async function startTrademarkQuery() {
return null
}
await new Promise(resolve => setTimeout(resolve, pollInterval))
// 分段等待每500ms检查一次取消状态
const waitSegments = pollInterval / 500
for (let i = 0; i < waitSegments; i++) {
if (!trademarkLoading.value) {
clearInterval(progressTimer)
return null
}
await new Promise(resolve => setTimeout(resolve, 500))
}
if (!trademarkLoading.value) {
clearInterval(progressTimer)
return null
@@ -321,8 +349,14 @@ async function startTrademarkQuery() {
return taskResult
}
}
} catch (err) {
// 继续等待
} catch (err: any) {
const errorMsg = err.message || ''
if (errorMsg.includes('表格没有数据') || errorMsg.includes('没有数据') ||
errorMsg.includes('任务处理失败') || errorMsg.includes('任务失败')) {
clearInterval(progressTimer)
throw err
}
// 其他错误(网络错误等)继续等待
}
}
@@ -333,7 +367,12 @@ async function startTrademarkQuery() {
try {
productResult = await pollTask()
if (!productResult || (productResult.code !== 200 && productResult.code !== 0)) {
// 如果返回null说明用户取消了直接返回
if (!productResult) {
return
}
if (productResult.code !== 200 && productResult.code !== 0) {
throw new Error('获取任务超时或失败,请重试')
}
@@ -345,6 +384,7 @@ async function startTrademarkQuery() {
taskData.total = productResult.data.original?.total || 0
taskData.current = taskData.total
taskData.completed = productResult.data.filtered.length
isProductTaskRealData.value = true
} finally {
clearInterval(progressTimer)
}
@@ -440,6 +480,7 @@ async function startTrademarkQuery() {
brandData.total = brandResult.data.checked || brandResult.data.total || brandData.total
brandData.current = brandData.total
brandData.completed = brandResult.data.unregistered || 0
isBrandTaskRealData.value = true
// 提取未注册品牌列表
const unregisteredBrands = brandResult.data.data.map((item: any) => item.brand).filter(Boolean)
@@ -455,6 +496,7 @@ async function startTrademarkQuery() {
// 更新统计:显示过滤出的实际行数(而不是品牌数)
brandData.completed = filterResult.data.filteredRows.length
isBrandTaskRealData.value = true
// 将品牌筛查结果作为展示数据
const brandItems = filterResult.data.filteredRows.map((row: any) => ({
@@ -484,44 +526,61 @@ async function startTrademarkQuery() {
}
// 只要流程正常完成就设置为done状态不再依赖trademarkLoading
queryStatus.value = 'done'
emit('updateData', trademarkData.value)
let summaryMsg = '筛查完成'
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()
queryStatus.value = 'done'
emit('updateData', trademarkData.value)
let summaryMsg = '筛查完成'
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
const hasProductData = isProductTaskRealData.value && taskProgress.value.product.total > 0
const hasBrandData = isBrandTaskRealData.value && taskProgress.value.brand.total > 0
// 优化错误信息 - 只显示友好提示
let msg = error.message || ''
if (msg.includes('网络') || msg.includes('network')) {
let friendlyMsg = ''
if (msg.includes('网络') || msg.includes('network') || msg.includes('Network')) {
queryStatus.value = 'networkError'
errorMessage.value = '网络不可用,请检查你的网络或代理设置'
} else if (msg.includes('超时') || msg.includes('timeout')) {
friendlyMsg = '网络连接失败,请检查网络或代理设置'
} else if (msg.includes('超时') || msg.includes('timeout') || msg.includes('Timeout')) {
queryStatus.value = 'error'
errorMessage.value = '数据库维护中,请稍后重试'
friendlyMsg = '查询超时,请稍后重试'
} else if (msg.includes('403') || msg.includes('风控')) {
queryStatus.value = 'error'
errorMessage.value = '网站风控限制,请稍后重试'
friendlyMsg = '访问受限,请稍后重试'
} else if (msg.includes('表格没有数据') || msg.includes('没有数据')) {
queryStatus.value = 'error'
friendlyMsg = '表格没有有效数据,请检查文件内容'
} else if (msg.includes('创建任务失败') || msg.includes('提取品牌')) {
queryStatus.value = 'error'
friendlyMsg = '数据处理失败,请检查文件格式是否正确'
} else {
queryStatus.value = 'error'
errorMessage.value = '数据库维护中,请稍后重试'
friendlyMsg = '查询失败,请稍后重试'
}
// 仅在第1步失败时清空数据
if (!hasProductData) {
// 根据失败阶段确定错误提示
if (hasProductData && !hasBrandData && needBrandCheck) {
// 产品筛查完成,品牌筛查失败
errorMessage.value = `品牌筛查失败:${friendlyMsg}。产品筛查结果已保留`
// 保留产品数据
emit('updateData', trademarkData.value)
} else if (!hasProductData) {
errorMessage.value = `产品筛查失败:${friendlyMsg}`
trademarkData.value = []
trademarkFullData.value = []
trademarkHeaders.value = []
emit('updateData', [])
} else {
errorMessage.value = friendlyMsg
emit('updateData', trademarkData.value)
}
showMessage(errorMessage.value, 'error')
} finally {
// 清除定时器
@@ -646,6 +705,10 @@ function resetToIdle() {
taskProgress.value.platform.current = 0
taskProgress.value.platform.completed = 0
// 重置真实数据标记
isProductTaskRealData.value = false
isBrandTaskRealData.value = false
// 清空localStorage中的会话数据
try {
const username = getUsernameFromToken()
@@ -666,7 +729,9 @@ defineExpose({
resetToIdle,
stopTrademarkQuery,
startTrademarkQuery,
exportTrademarkData
exportTrademarkData,
isProductTaskRealData,
isBrandTaskRealData
})
</script>
@@ -679,7 +744,15 @@ defineExpose({
<div class="step-card">
<div class="step-header"><div class="title">导入Excel表格</div></div>
<div class="desc">产品筛查需导入卖家精灵选品表格并勾选"导出主图"品牌筛查Excel需包含"品牌"</div>
<div class="dropzone" :class="{ uploading: uploadLoading }" @click="!uploadLoading && openTrademarkUpload()">
<div
class="dropzone"
:class="{ uploading: uploadLoading, 'drag-active': dragActive }"
@click="!uploadLoading && openTrademarkUpload()"
@dragenter="onDragEnter"
@dragover="onDragOver"
@dragleave="onDragLeave"
@drop="onDrop"
>
<div v-if="!uploadLoading" class="dz-icon">📤</div>
<div v-else class="dz-icon spinner"></div>
<div class="dz-text">{{ uploadLoading ? '正在验证表头...' : '点击或将文件拖拽到这里上传' }}</div>
@@ -845,6 +918,7 @@ defineExpose({
.dropzone:hover { background: #f6fbff; border-color: #409EFF; }
.dropzone.uploading { cursor: not-allowed; opacity: 0.7; }
.dropzone.uploading:hover { background: #fafafa; border-color: #c0c4cc; }
.dropzone.drag-active { background: #e6f4ff; border-color: #1677FF; }
.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; }