feat(amazon): 更新商标筛查界面图标与状态展示- 将商标筛查状态图标从图片替换为 SVG 图标

- 添加了进行中、取消、完成/失败状态的 SVG 图标
-优化任务进度指示器,使用 SVG 并支持旋转动画
- 禁用跟卖许可筛查功能并更新提示文案
- 调整标签页样式和间距,适配不同屏幕尺寸
-修复状态横幅图标显示问题,并统一图标尺寸
- 更新全局样式以提升视觉一致性和用户体验
This commit is contained in:
2025-11-12 15:55:06 +08:00
parent cce281497b
commit cfb9096788
24 changed files with 994 additions and 783 deletions

View File

@@ -23,13 +23,18 @@ function openAppIfNotOpened() {
!appOpened && setTimeout(openAppIfNotOpened, 50);
return;
}
appOpened = true;
isDev
isDev
? mainWindow.loadURL(`http://localhost:${process.argv[2] || 8083}`)
: mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
mainWindow.webContents.once('did-finish-load', () => {
if (splashWindow && !splashWindow.isDestroyed()) {
splashWindow.webContents.send('splash-complete');
}
// 先显示主窗口再关闭splash避免白屏
setTimeout(() => {
if (mainWindow && !mainWindow.isDestroyed()) {
const shouldMinimize = loadConfig().launchMinimized || false;
@@ -39,14 +44,19 @@ function openAppIfNotOpened() {
}
if (isDev) mainWindow.webContents.openDevTools();
}
if (splashWindow && !splashWindow.isDestroyed()) {
splashWindow.close();
splashWindow = null;
}
}, 100);
// 延迟关闭splash确保主窗口已显示
setTimeout(() => {
if (splashWindow && !splashWindow.isDestroyed()) {
splashWindow.close();
splashWindow = null;
}
}, 100);
}, 200);
});
}
// 通用资源路径获取函数
function getResourcePath(devPath: string, prodPath: string, fallbackPath?: string): string {
if (isDev) return join(__dirname, devPath);
@@ -76,6 +86,74 @@ const getSplashPath = () => getResourcePath('../../public/splash.html', 'public/
const getIconPath = () => getResourcePath('../../public/icon/icon1.png', 'public/icon/icon1.png');
const getLogbackConfigPath = () => getResourcePath('../../public/config/logback.xml', 'public/config/logback.xml');
// 图片缓存目录
const getImageCacheDir = () => {
const cacheDir = join(app.getPath('userData'), 'image-cache');
if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true });
return cacheDir;
};
// 下载图片到本地
async function downloadImageToLocal(imageUrl: string, username: string, type: 'splash' | 'logo'): Promise<void> {
return new Promise((resolve) => {
const protocol = imageUrl.startsWith('https') ? https : http;
protocol.get(imageUrl, (res) => {
if (res.statusCode !== 200) return resolve();
const chunks: Buffer[] = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const buffer = Buffer.concat(chunks);
const ext = imageUrl.match(/\.(jpg|jpeg|png|gif|webp)$/i)?.[1] || 'png';
const filepath = join(getImageCacheDir(), `${username}_${type}.${ext}`);
writeFileSync(filepath, buffer);
console.log(`[图片缓存] 已保存: ${username}_${type}.${ext}`);
resolve();
});
res.on('error', () => resolve());
}).on('error', () => resolve());
});
}
// 加载本地缓存图片
function loadCachedImage(username: string, type: 'splash' | 'logo'): string | null {
try {
const files = readdirSync(getImageCacheDir());
const file = files.find(f => f.startsWith(`${username}_${type}.`));
if (file) {
const buffer = readFileSync(join(getImageCacheDir(), file));
const ext = extname(file).slice(1);
const mime = { jpg: 'jpeg', jpeg: 'jpeg', png: 'png', gif: 'gif', webp: 'webp' }[ext] || 'png';
return `url('data:image/${mime};base64,${buffer.toString('base64')}')`;
}
} catch (err) {}
return null;
}
// 删除本地缓存图片
function deleteCachedImage(username: string, type: 'splash' | 'logo'): void {
try {
const files = readdirSync(getImageCacheDir());
const file = files.find(f => f.startsWith(`${username}_${type}.`));
if (file) {
const filepath = join(getImageCacheDir(), file);
if (existsSync(filepath)) {
require('fs').unlinkSync(filepath);
console.log(`[图片缓存] 已删除: ${file}`);
}
}
} catch (err) {}
}
// 获取默认开屏图片
function getDefaultSplashImage(): string {
const path = getResourcePath('../../public/image/splash_screen.png', 'public/image/splash_screen.png');
if (existsSync(path)) {
const base64 = readFileSync(path).toString('base64');
return `url('data:image/png;base64,${base64}')`;
}
return 'none';
}
function getDataDirectoryPath(): string {
const dataDir = join(app.getPath('userData'), 'data');
if (!existsSync(dataDir)) mkdirSync(dataDir, {recursive: true});
@@ -101,6 +179,7 @@ interface AppConfig {
launchMinimized?: boolean;
lastUsername?: string;
splashImageUrl?: string;
brandLogoUrl?: string;
}
function getConfigPath(): string {
@@ -143,12 +222,15 @@ function migrateDataFromPublic(): void {
function startSpringBoot() {
console.log('[Spring Boot] 开始启动...');
migrateDataFromPublic();
const jarPath = getJarFilePath();
const javaPath = getJavaExecutablePath();
const dataDir = getDataDirectoryPath();
const logDir = getLogDirectoryPath();
const logbackConfigPath = getLogbackConfigPath();
console.log('[Spring Boot] JAR路径:', jarPath);
console.log('[Spring Boot] Java路径:', javaPath);
if (!existsSync(jarPath)) {
dialog.showErrorBox('启动失败', `JAR 文件不存在:\n${jarPath}`);
app.quit();
@@ -174,14 +256,16 @@ function startSpringBoot() {
springProcess.on('close', () => mainWindow ? mainWindow.close() : app.quit());
springProcess.on('error', (error) => {
dialog.showErrorBox('启动失败', error.message.includes('ENOENT')
? '找不到 Java 运行环境'
dialog.showErrorBox('启动失败', error.message.includes('ENOENT')
? '找不到 Java 运行环境'
: '启动 Java 应用失败');
app.quit();
});
let checkCount = 0;
const checkHealth = () => {
if (startupCompleted) return;
http.get('http://127.0.0.1:8081/api/system/version', (res) => {
if (res.statusCode !== 200) {
setTimeout(checkHealth, 100);
@@ -213,7 +297,6 @@ function startSpringBoot() {
}
}
// startSpringBoot();
function stopSpringBoot() {
if (!springProcess) return;
try {
@@ -236,8 +319,10 @@ function stopSpringBoot() {
function createWindow() {
mainWindow = new BrowserWindow({
width: 1280,
width: 1200,
height: 800,
minWidth: 1200,
minHeight: 800,
show: false, //
frame: false,
autoHideMenuBar: true,
@@ -317,9 +402,9 @@ app.whenReady().then(() => {
// 如果解码失败,回退到原来的方法
filePath = decodeURIComponent(request.url.substring(8));
}
// 检查是否是 icon 或 image 资源请求
if (filePath.includes('/icon/') || filePath.includes('\\icon\\') ||
if (filePath.includes('/icon/') || filePath.includes('\\icon\\') ||
filePath.includes('/image/') || filePath.includes('\\image\\')) {
const match = filePath.match(/[/\\](icon|image)[/\\]([^?#]+)/);
if (match) {
@@ -331,15 +416,15 @@ app.whenReady().then(() => {
}
}
}
callback({ path: filePath });
});
}
// 应用开机自启动配置
const config = loadConfig();
const shouldMinimize = config.launchMinimized || false;
if (config.autoLaunch !== undefined) {
app.setLoginItemSettings({
openAtLogin: config.autoLaunch,
@@ -352,49 +437,48 @@ app.whenReady().then(() => {
// 只有在不需要最小化启动时才显示 splash 窗口
if (!shouldMinimize) {
const config = loadConfig();
const username = config.lastUsername || '';
const imageUrl = config.splashImageUrl || '';
// 图片加载:本地缓存 > 默认图片
let splashImage = (imageUrl && username && loadCachedImage(username, 'splash')) || getDefaultSplashImage();
// 如果有URL但缓存不存在后台下载
if (imageUrl && username && !loadCachedImage(username, 'splash')) {
downloadImageToLocal(imageUrl, username, 'splash');
}
const splashHtml = readFileSync(getSplashPath(), 'utf-8').replace('__SPLASH_IMAGE__', splashImage);
splashWindow = new BrowserWindow({
width: 1200,
height: 675,
height: 800,
frame: false,
transparent: false,
resizable: false,
alwaysOnTop: false,
show: true,
show: false,
center: true,
icon: getIconPath(),
backgroundColor: '#ffffff',
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
nodeIntegration: true,
contextIsolation: false,
}
});
// 监听启动窗口关闭事件
splashWindow.on('closed', () => {
splashWindow = null;
});
splashWindow.on('closed', () => splashWindow = null);
const splashPath = getSplashPath();
if (existsSync(splashPath)) {
const config = loadConfig();
const imageUrl = config.splashImageUrl || '';
console.log('[开屏图片] 启动配置:', { username: config.lastUsername, imageUrl, configPath: getConfigPath() });
splashWindow.loadFile(splashPath);
if (imageUrl) {
splashWindow.webContents.once('did-finish-load', () => {
splashWindow?.webContents.executeJavaScript(`
document.body.style.setProperty('--splash-image', "url('${imageUrl}')");
`).then(() => console.log('[开屏图片] 注入成功:', imageUrl))
.catch(err => console.error('[开屏图片] 注入失败:', err));
});
}
}
// 加载预注入的 HTML图片已base64内联无跨域问题
splashWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(splashHtml)}`);
splashWindow.once('ready-to-show', () => splashWindow?.show());
}
//666
console.log('[启动流程] 准备启动 Spring Boot...');
setTimeout(() => {
openAppIfNotOpened();
}, 100);
startSpringBoot();
}, 200);
app.on('activate', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
@@ -424,6 +508,11 @@ ipcMain.on('message', (event, message) => {
console.log(message);
});
ipcMain.on('quit-app', () => {
isQuitting = true;
app.quit();
});
ipcMain.handle('get-jar-version', () => {
const jarPath = getJarFilePath();
const match = jarPath ? basename(jarPath).match(/erp_client_sb-(\d+\.\d+\.\d+)\.jar/) : null;
@@ -785,13 +874,21 @@ ipcMain.handle('window-is-maximized', () => {
return mainWindow && !mainWindow.isDestroyed() ? mainWindow.isMaximized() : false;
});
// 保存开屏图片配置(用户名 + URL
ipcMain.handle('save-splash-config', (event, username: string, imageUrl: string) => {
// 保存开屏图片配置(用户名 + URL并下载到本地
ipcMain.handle('save-splash-config', async (event, username: string, imageUrl: string) => {
const config = loadConfig();
config.lastUsername = username;
config.splashImageUrl = imageUrl;
saveConfig(config);
console.log('[开屏图片] 已保存配置:', { username, imageUrl, path: getConfigPath() });
// 如果有图片URL立即下载到本地缓存
if (imageUrl && username) {
await downloadImageToLocal(imageUrl, username, 'splash');
} else if (username) {
// 如果图片URL为空删除本地缓存
deleteCachedImage(username, 'splash');
}
return { success: true };
});
@@ -801,6 +898,48 @@ ipcMain.handle('get-splash-config', () => {
return { username: config.lastUsername || '', imageUrl: config.splashImageUrl || '' };
});
// 保存品牌logo配置
ipcMain.handle('save-brand-logo-config', async (event, username: string, logoUrl: string) => {
const config = loadConfig();
config.brandLogoUrl = logoUrl;
saveConfig(config);
// 如果有logo URL立即下载到本地缓存
if (logoUrl && username) {
await downloadImageToLocal(logoUrl, username, 'logo');
} else if (username) {
// 如果logo URL为空删除本地缓存
deleteCachedImage(username, 'logo');
}
return { success: true };
});
// 清除用户配置(退出登录时调用)
ipcMain.handle('clear-user-config', async () => {
const config = loadConfig();
const username = config.lastUsername;
// 清除配置
config.lastUsername = '';
config.splashImageUrl = '';
config.brandLogoUrl = '';
saveConfig(config);
// 删除本地缓存的图片
if (username) {
deleteCachedImage(username, 'splash');
deleteCachedImage(username, 'logo');
}
return { success: true };
});
// 加载完整配置
ipcMain.handle('load-config', () => {
return loadConfig();
});
async function getFileSize(url: string): Promise<number> {
return new Promise((resolve) => {

View File

@@ -47,6 +47,11 @@ const electronAPI = {
saveSplashConfig: (username: string, imageUrl: string) => ipcRenderer.invoke('save-splash-config', username, imageUrl),
getSplashConfig: () => ipcRenderer.invoke('get-splash-config'),
// 品牌logo相关 API
saveBrandLogoConfig: (username: string, logoUrl: string) => ipcRenderer.invoke('save-brand-logo-config', username, logoUrl),
loadConfig: () => ipcRenderer.invoke('load-config'),
clearUserConfig: () => ipcRenderer.invoke('clear-user-config'),
onDownloadProgress: (callback: (progress: any) => void) => {
ipcRenderer.removeAllListeners('download-progress')
ipcRenderer.on('download-progress', (event, progress) => callback(progress))

View File

@@ -32,10 +32,8 @@ export function createTray(mainWindow: BrowserWindow | null) {
}
}
})
// 右键菜单
updateTrayMenu(mainWindow)
return tray
}

View File

@@ -245,7 +245,7 @@ async function handleLoginSuccess(data: {
}
}
function clearLocalAuth() {
async function clearLocalAuth() {
removeToken()
isAuthenticated.value = false
currentUsername.value = ''
@@ -253,9 +253,17 @@ function clearLocalAuth() {
vipExpireTime.value = null
deviceTrialExpired.value = false
accountType.value = 'trial'
brandLogoUrl.value = '' // 清除品牌logo
showAuthDialog.value = true
showDeviceDialog.value = false
SSEManager.disconnect()
// 清除主进程中的用户配置和缓存
try {
await (window as any).electronAPI.clearUserConfig()
} catch (error) {
console.warn('清除用户配置失败:', error)
}
}
async function logout() {
@@ -265,7 +273,7 @@ async function logout() {
} catch (error) {
console.warn('离线通知失败:', error)
}
clearLocalAuth()
await clearLocalAuth()
}
async function handleUserClick() {
@@ -374,11 +382,22 @@ async function syncSettingsToElectron() {
// 加载品牌logo
async function loadBrandLogo() {
try {
const username = getUsernameFromToken()
if (!username) return
// 1. 优先从本地缓存读取(秒开)
const config = await (window as any).electronAPI.loadConfig()
if (config.brandLogoUrl) {
brandLogoUrl.value = config.brandLogoUrl
}
const res = await splashApi.getBrandLogo(username)
brandLogoUrl.value = res.data.url
// 2. 后台异步更新(不阻塞UI)
const username = getUsernameFromToken()
if (username) {
const res = await splashApi.getBrandLogo(username)
const newUrl = res.data.url
if (newUrl !== brandLogoUrl.value) {
brandLogoUrl.value = newUrl
await (window as any).electronAPI.saveBrandLogoConfig(username, newUrl)
}
}
} catch (error) {
brandLogoUrl.value = ''
}
@@ -411,7 +430,7 @@ const SSEManager = {
}
},
handleMessage(e: MessageEvent) {
async handleMessage(e: MessageEvent) {
try {
if (e.type === 'ping') return
@@ -422,11 +441,11 @@ const SSEManager = {
console.log('SSE连接已就绪')
break
case 'DEVICE_REMOVED':
clearLocalAuth()
await clearLocalAuth()
ElMessage.warning('会话已失效,请重新登录')
break
case 'FORCE_LOGOUT':
logout()
await logout()
ElMessage.warning('会话已失效,请重新登录')
break
case 'PERMISSIONS_UPDATED':
@@ -529,7 +548,7 @@ async function confirmRemoveDevice(row: DeviceItem) {
deviceQuota.value.used = Math.max(0, deviceQuota.value.used - 1)
if (row.deviceId === getClientIdFromToken()) {
clearLocalAuth()
await clearLocalAuth()
}
ElMessage.success('已移除设备')
@@ -613,13 +632,37 @@ onUnmounted(() => {
</div>
<div class="erp-container">
<div class="sidebar">
<div class="user-avatar">
<div class="main-logo">
<img src="/icon/icon.png" alt="logo"/>
</div>
<!-- 品牌logo区域有logo时显示 -->
<!-- 用户头像区域 -->
<div class="user-avatar-section" @click="handleUserClick">
<div class="avatar-wrapper">
<img
v-if="isAuthenticated"
src="/image/img_v3_02qd_052605f0-4be3-44db-9691-35ee5ff6201g.jpg"
alt="用户头像"
class="user-avatar-img"
/>
<img
v-else
src="/image/user.png"
alt="默认头像"
class="user-avatar-img"
/>
</div>
<div class="user-info">
<div class="user-name-wrapper">
<span class="user-name">{{ isAuthenticated ? currentUsername : '登录/注册' }}</span>
<span v-if="isAuthenticated && vipStatus.isVip" class="vip-badge">VIP {{ vipStatus.daysLeft }}</span>
</div>
<div class="user-action">{{ isAuthenticated ? '18659156151' : '登录账号体验完整功能' }}</div>
</div>
</div>
<div v-if="brandLogoUrl" class="brand-logo-section">
<img :src="brandLogoUrl" alt="品牌logo" class="brand-logo"/>
<img :src="brandLogoUrl" alt="品牌 Banner" class="brand-logo"/>
</div>
<div class="menu-group-title">电商平台</div>
@@ -683,8 +726,7 @@ onUnmounted(() => {
@open-device="openDeviceManager"
@open-settings="openSettings"
@open-account-manager="openAccountManager"
@check-update="handleCheckUpdate"
@show-login="showAuthDialog = true"/>
@check-update="handleCheckUpdate"/>
<div class="content-body">
<div
class="dashboard-home"
@@ -835,8 +877,8 @@ onUnmounted(() => {
}
.sidebar {
width: 180px;
min-width: 180px;
width: 220px;
min-width: 220px;
flex-shrink: 0;
background: #F0F0F0;
border-right: 1px solid #e8eaec;
@@ -859,20 +901,104 @@ onUnmounted(() => {
object-fit: contain;
}
.user-avatar {
/* 主Logo */
.main-logo {
display: flex;
align-items: center;
justify-content: center;
padding: 12px 0;
margin: 0 0 12px 0;
padding: 8px 0;
margin: 0 0 16px 0;
}
.user-avatar img {
width: 90px;
.main-logo img {
width: 120px;
object-fit: contain;
}
/* 用户头像区域 */
.user-avatar-section {
display: flex;
align-items: center;
gap: 7px;
padding: 8px 10px;
margin: 0 0 16px 0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.avatar-wrapper {
flex-shrink: 0;
width: 44px;
height: 44px;
border-radius: 50%;
overflow: hidden;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.user-avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.user-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
text-align: left;
}
.user-name-wrapper {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.user-name {
font-size: 14px;
font-weight: 600;
color: #303133;
white-space: nowrap;
line-height: 1.2;
flex-shrink: 0;
}
.vip-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0px 5px;
height: 16px;
background: #BAE0FF;
border: 1px solid rgba(22, 119, 255, 0.05);
border-radius: 8px;
font-size: 10px;
font-weight: 600;
color: rgba(0, 29, 102, 1);
white-space: nowrap;
flex-shrink: 0;
line-height: 100%;
text-align: center;
letter-spacing: 0%;
}
.user-action {
font-size: 11px;
color: #909399;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.menu-group-title {
font-size: 12px;
color: #909399;
@@ -886,8 +1012,8 @@ onUnmounted(() => {
justify-content: center;
align-items: center;
width: 100%;
height: 48px;
margin-bottom: 12px;
height: 60px;
margin-bottom: 16px;
padding: 0 4px;
box-sizing: border-box;
}

View File

@@ -1,6 +1,6 @@
export type HttpMethod = 'GET' | 'POST' | 'DELETE';
//const RUOYI_BASE = 'http://8.138.23.49:8085';
const RUOYI_BASE = 'http://192.168.1.89:8085';
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,

View File

@@ -131,7 +131,7 @@ function handleExportData() {
<div class="top-tabs">
<div :class="['tab-item', { active: currentTab === 'asin' }]" @click="currentTab = 'asin'">
<img src="/icon/asin.png" alt="ASIN查询" class="tab-icon" />
<span class="tab-text">ASIN查询</span>
<span class="tab-text">ASIN监控</span>
</div>
<div :class="['tab-item', { active: currentTab === 'genmai' }]" @click="currentTab = 'genmai'">
<img src="/icon/anjldk.png" alt="跟卖精灵" class="tab-icon" />
@@ -245,7 +245,17 @@ function handleExportData() {
<!-- 进行中状态横幅 -->
<div v-if="trademarkPanelRef?.queryStatus === 'inProgress'" class="status-banner progress-banner">
<div class="banner-icon">
<img src="/icon/wait.png" alt="筛查中" class="icon-image" />
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 4H40" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 44H40" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 4V16L21 26" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M36 44V29.5L27 21" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 44V30L18.5 23.5" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M36 4V16L29.5 23.5" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 33H19" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M29.1484 32.6465L29.8555 33.3536" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M24 38H25" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="banner-content">
<div class="banner-title">数据筛查中...</div>
@@ -259,7 +269,11 @@ function handleExportData() {
<!-- 取消状态横幅 -->
<div v-if="trademarkPanelRef?.queryStatus === 'cancel'" class="status-banner cancel-banner">
<div class="banner-icon">
<img src="/icon/cancel.png" alt="已取消" class="icon-image" />
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z" stroke="#FAAD14" stroke-width="2" stroke-linejoin="round"/>
<path d="M29.6574 18.3428L18.3438 29.6565" stroke="#FAAD14" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.3438 18.3428L29.6574 29.6565" stroke="#FAAD14" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="banner-content">
<div class="banner-title">已取消查询</div>
@@ -274,7 +288,15 @@ function handleExportData() {
<!-- 完成/失败状态横幅 -->
<div v-if="trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError'" class="status-banner done-banner">
<div class="banner-icon">
<img :src="trademarkPanelRef.queryStatus === 'done' ? '/icon/done1.png' : '/icon/error.png'" alt="完成" class="icon-image" />
<svg v-if="trademarkPanelRef.queryStatus === 'done'" width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 44C29.5228 44 34.5228 41.7614 38.1421 38.1421C41.7614 34.5228 44 29.5228 44 24C44 18.4772 41.7614 13.4772 38.1421 9.85786C34.5228 6.23858 29.5228 4 24 4C18.4772 4 13.4772 6.23858 9.85786 9.85786C6.23858 13.4772 4 18.4772 4 24C4 29.5228 6.23858 34.5228 9.85786 38.1421C13.4772 41.7614 18.4772 44 24 44Z" stroke="#52C41A" stroke-width="2" stroke-linejoin="round"/>
<path d="M16 24L22 30L34 18" stroke="#52C41A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z" stroke="#FF4D4F" stroke-width="2" stroke-linejoin="round"/>
<path d="M29.6574 18.3428L18.3438 29.6565" stroke="#FF4D4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.3438 18.3428L29.6574 29.6565" stroke="#FF4D4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="banner-content">
<div class="banner-title">{{ trademarkPanelRef.queryStatus === 'done' ? '筛查已完成' : '数据筛查失败' }}</div>
@@ -296,29 +318,101 @@ function handleExportData() {
<div class="status-column">
<!-- 任务1产品商标筛查 -->
<div class="status-item">
<img v-if="(trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 && (trademarkPanelRef?.taskProgress?.product?.current || 0) >= trademarkPanelRef.taskProgress.product.total" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-else-if="((trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.product?.current || 0) > 0) && (trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError')" src="/icon/error.png" alt="筛查失败" class="status-indicator-icon" />
<img v-else-if="((trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.product?.current || 0) > 0) && trademarkPanelRef?.queryStatus === 'cancel'" src="/icon/cancel.png" alt="已取消" class="status-indicator-icon" />
<img v-else-if="(trademarkPanelRef?.taskProgress?.product?.current || 0) > 0" src="/icon/inProgress.png" alt="采集中" class="status-indicator-icon" />
<img v-else src="/icon/acquisition.png" alt="待采集" class="status-indicator-icon" />
<svg v-if="(trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 && (trademarkPanelRef?.taskProgress?.product?.current || 0) >= trademarkPanelRef.taskProgress.product.total" width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="status-indicator-icon">
<path d="M24 44C29.5228 44 34.5228 41.7614 38.1421 38.1421C41.7614 34.5228 44 29.5228 44 24C44 18.4772 41.7614 13.4772 38.1421 9.85786C34.5228 6.23858 29.5228 4 24 4C18.4772 4 13.4772 6.23858 9.85786 9.85786C6.23858 13.4772 4 18.4772 4 24C4 29.5228 6.23858 34.5228 9.85786 38.1421C13.4772 41.7614 18.4772 44 24 44Z" stroke="#52C41A" stroke-width="2" stroke-linejoin="round"/>
<path d="M16 24L22 30L34 18" stroke="#52C41A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else-if="((trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.product?.current || 0) > 0) && (trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError')" width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="status-indicator-icon">
<path d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z" stroke="#FF4D4F" stroke-width="2" stroke-linejoin="round"/>
<path d="M29.6574 18.3428L18.3438 29.6565" stroke="#FF4D4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.3438 18.3428L29.6574 29.6565" stroke="#FF4D4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else-if="((trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.product?.current || 0) > 0) && trademarkPanelRef?.queryStatus === 'cancel'" width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="status-indicator-icon">
<path d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z" stroke="#FAAD14" stroke-width="2" stroke-linejoin="round"/>
<path d="M29.6574 18.3428L18.3438 29.6565" stroke="#FAAD14" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.3438 18.3428L29.6574 29.6565" stroke="#FAAD14" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else-if="(trademarkPanelRef?.taskProgress?.product?.current || 0) > 0" width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="status-indicator-icon spinning">
<path d="M8 4H40" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 44H40" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 4V16L21 26" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M36 44V29.5L27 21" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 44V30L18.5 23.5" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M36 4V16L29.5 23.5" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 33H19" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M29.1484 32.6465L29.8555 33.3536" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M24 38H25" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="status-indicator-icon">
<circle cx="24" cy="24" r="20" stroke="#D9D9D9" stroke-width="2" stroke-linejoin="round"/>
<circle cx="24" cy="24" r="4" fill="#D9D9D9"/>
</svg>
</div>
<div class="status-connector"></div>
<!-- 任务2品牌商标筛查 -->
<div class="status-item">
<img v-if="(trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 && (trademarkPanelRef?.taskProgress?.brand?.current || 0) >= trademarkPanelRef.taskProgress.brand.total" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-else-if="((trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.brand?.current || 0) > 0) && (trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError')" src="/icon/error.png" alt="筛查失败" class="status-indicator-icon" />
<img v-else-if="((trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.brand?.current || 0) > 0) && trademarkPanelRef?.queryStatus === 'cancel'" src="/icon/cancel.png" alt="已取消" class="status-indicator-icon" />
<img v-else-if="(trademarkPanelRef?.taskProgress?.brand?.current || 0) > 0" src="/icon/inProgress.png" alt="采集中" class="status-indicator-icon" />
<img v-else src="/icon/acquisition.png" alt="待采集" class="status-indicator-icon" />
<svg v-if="(trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 && (trademarkPanelRef?.taskProgress?.brand?.current || 0) >= trademarkPanelRef.taskProgress.brand.total" width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="status-indicator-icon">
<path d="M24 44C29.5228 44 34.5228 41.7614 38.1421 38.1421C41.7614 34.5228 44 29.5228 44 24C44 18.4772 41.7614 13.4772 38.1421 9.85786C34.5228 6.23858 29.5228 4 24 4C18.4772 4 13.4772 6.23858 9.85786 9.85786C6.23858 13.4772 4 18.4772 4 24C4 29.5228 6.23858 34.5228 9.85786 38.1421C13.4772 41.7614 18.4772 44 24 44Z" stroke="#52C41A" stroke-width="2" stroke-linejoin="round"/>
<path d="M16 24L22 30L34 18" stroke="#52C41A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else-if="((trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.brand?.current || 0) > 0) && (trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError')" width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="status-indicator-icon">
<path d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z" stroke="#FF4D4F" stroke-width="2" stroke-linejoin="round"/>
<path d="M29.6574 18.3428L18.3438 29.6565" stroke="#FF4D4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.3438 18.3428L29.6574 29.6565" stroke="#FF4D4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else-if="((trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.brand?.current || 0) > 0) && trademarkPanelRef?.queryStatus === 'cancel'" width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="status-indicator-icon">
<path d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z" stroke="#FAAD14" stroke-width="2" stroke-linejoin="round"/>
<path d="M29.6574 18.3428L18.3438 29.6565" stroke="#FAAD14" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.3438 18.3428L29.6574 29.6565" stroke="#FAAD14" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else-if="(trademarkPanelRef?.taskProgress?.brand?.current || 0) > 0" width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="status-indicator-icon spinning">
<path d="M8 4H40" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 44H40" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 4V16L21 26" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M36 44V29.5L27 21" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 44V30L18.5 23.5" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M36 4V16L29.5 23.5" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 33H19" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M29.1484 32.6465L29.8555 33.3536" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M24 38H25" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="status-indicator-icon">
<circle cx="24" cy="24" r="20" stroke="#D9D9D9" stroke-width="2" stroke-linejoin="round"/>
<circle cx="24" cy="24" r="4" fill="#D9D9D9"/>
</svg>
</div>
<div class="status-connector"></div>
<!-- 任务3跟卖许可筛查 -->
<div class="status-item">
<img v-if="(trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 && (trademarkPanelRef?.taskProgress?.platform?.current || 0) >= trademarkPanelRef.taskProgress.platform.total" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-else-if="((trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.platform?.current || 0) > 0) && (trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError')" src="/icon/error.png" alt="筛查失败" class="status-indicator-icon" />
<img v-else-if="((trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.platform?.current || 0) > 0) && trademarkPanelRef?.queryStatus === 'cancel'" src="/icon/cancel.png" alt="已取消" class="status-indicator-icon" />
<img v-else-if="(trademarkPanelRef?.taskProgress?.platform?.current || 0) > 0" src="/icon/inProgress.png" alt="采集中" class="status-indicator-icon" />
<img v-else src="/icon/acquisition.png" alt="待采集" class="status-indicator-icon" />
<svg v-if="(trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 && (trademarkPanelRef?.taskProgress?.platform?.current || 0) >= trademarkPanelRef.taskProgress.platform.total" width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="status-indicator-icon">
<path d="M24 44C29.5228 44 34.5228 41.7614 38.1421 38.1421C41.7614 34.5228 44 29.5228 44 24C44 18.4772 41.7614 13.4772 38.1421 9.85786C34.5228 6.23858 29.5228 4 24 4C18.4772 4 13.4772 6.23858 9.85786 9.85786C6.23858 13.4772 4 18.4772 4 24C4 29.5228 6.23858 34.5228 9.85786 38.1421C13.4772 41.7614 18.4772 44 24 44Z" stroke="#52C41A" stroke-width="2" stroke-linejoin="round"/>
<path d="M16 24L22 30L34 18" stroke="#52C41A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else-if="((trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.platform?.current || 0) > 0) && (trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError')" width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="status-indicator-icon">
<path d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z" stroke="#FF4D4F" stroke-width="2" stroke-linejoin="round"/>
<path d="M29.6574 18.3428L18.3438 29.6565" stroke="#FF4D4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.3438 18.3428L29.6574 29.6565" stroke="#FF4D4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else-if="((trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.platform?.current || 0) > 0) && trademarkPanelRef?.queryStatus === 'cancel'" width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="status-indicator-icon">
<path d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z" stroke="#FAAD14" stroke-width="2" stroke-linejoin="round"/>
<path d="M29.6574 18.3428L18.3438 29.6565" stroke="#FAAD14" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.3438 18.3428L29.6574 29.6565" stroke="#FAAD14" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else-if="(trademarkPanelRef?.taskProgress?.platform?.current || 0) > 0" width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="status-indicator-icon spinning">
<path d="M8 4H40" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 44H40" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 4V16L21 26" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M36 44V29.5L27 21" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 44V30L18.5 23.5" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M36 4V16L29.5 23.5" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 33H19" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M29.1484 32.6465L29.8555 33.3536" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M24 38H25" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="status-indicator-icon">
<circle cx="24" cy="24" r="20" stroke="#D9D9D9" stroke-width="2" stroke-linejoin="round"/>
<circle cx="24" cy="24" r="4" fill="#D9D9D9"/>
</svg>
</div>
</div>
@@ -386,7 +480,7 @@ function handleExportData() {
<div class="task-item">
<div class="task-title-row">
<div class="task-info">
<div class="task-name">{{ trademarkPanelRef?.taskProgress?.platform?.label || '亚马逊跟卖许可筛查' }}</div>
<div class="task-name task-name-disabled">亚马逊跟卖许可筛查即将上线敬请期待</div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.platform?.desc || '筛查亚马逊许可跟卖的商品' }}<span v-if="trademarkPanelRef?.queryStatus !== 'inProgress'"> (未开始)</span></div>
</div>
</div>
@@ -507,7 +601,6 @@ function handleExportData() {
align-items: center;
padding: 0px 12px;
gap: 8px;
width: 140px;
height: 40px;
min-height: 40px;
max-height: 40px;
@@ -518,19 +611,19 @@ function handleExportData() {
flex-shrink: 0;
cursor: pointer;
transition: all 0.2s ease;
color: #606266;
color: #303133;
font-size: 14px;
font-weight: 400;
}
.tab-item:hover {
background: #f0f9ff;
border-color: #1677FF;
color: #1677FF;
color: #303133;
}
.tab-item.active {
background: #FFFFFF;
border: 1px solid #1677FF;
color: #1677FF;
color: #303133;
cursor: default;
}
.tab-icon { width: 20px; height: 20px; flex-shrink: 0; object-fit: contain; }
@@ -576,7 +669,6 @@ function handleExportData() {
display: flex;
flex-direction: column;
box-sizing: border-box;
transition: width 0.3s ease;
}
.steps-sidebar.narrow {
width: 240px;
@@ -793,7 +885,7 @@ function handleExportData() {
gap: 10px;
width: 90%;
max-width: 872px;
margin: 0 auto;
margin: 16px auto;
background: rgba(0, 0, 0, 0.02);
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 8px;
@@ -816,6 +908,7 @@ function handleExportData() {
pointer-events: none;
}
.genmai-info-boxes {
margin-bottom: 16px;
box-sizing: border-box;
display: flex;
text-align: left;
@@ -840,7 +933,7 @@ function handleExportData() {
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 16px;
padding: 0px 15px;
}
.info-box:not(:last-child) {
border-right: 1px solid rgba(0, 0, 0, 0.06);
@@ -859,6 +952,8 @@ function handleExportData() {
.info-link {
color: #1677FF;
text-decoration: none;
position: relative;
z-index: 10;
}
.info-link:hover {
text-decoration: underline;
@@ -909,13 +1004,12 @@ function handleExportData() {
background: transparent;
border-radius: 50%;
}
.done-banner .banner-icon {
background: transparent;
}
.icon-image {
.banner-icon svg {
width: 48px;
height: 48px;
filter: none;
}
.done-banner .banner-icon {
background: transparent;
}
.banner-content {
flex: 1;
@@ -946,10 +1040,6 @@ function handleExportData() {
.unified-task-card {
width: 70%;
max-width: 900px;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 12px;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
@@ -982,7 +1072,7 @@ function handleExportData() {
height: 24px;
object-fit: contain;
}
.status-indicator-icon[src*="inProgress"] {
.status-indicator-icon.spinning {
animation: spin 1.5s linear infinite;
}
.status-connector {
@@ -1038,6 +1128,9 @@ function handleExportData() {
margin-bottom: 2px;
line-height: 1.4;
}
.task-name-disabled {
color: #909399;
}
.task-desc {
font-size: 11px;
color: #909399;
@@ -1115,8 +1208,7 @@ function handleExportData() {
}
/* wide保持280px不变 */
.tab-item {
width: 120px;
padding: 0px 8px;
padding: 0px 15px;
}
}
@@ -1146,8 +1238,13 @@ function handleExportData() {
max-height: 300px;
}
.tab-item {
width: 100px;
font-size: 13px;
font-size: 12px;
padding: 0px 6px;
gap: 6px;
}
.tab-icon {
width: 16px;
height: 16px;
}
.status-banner,
.unified-task-card {
@@ -1161,9 +1258,9 @@ function handleExportData() {
width: 40px;
height: 40px;
}
.icon-image {
width: 24px;
height: 24px;
.banner-icon svg {
width: 32px;
height: 32px;
}
.banner-title {
font-size: 14px;

View File

@@ -106,6 +106,7 @@ const completedSteps = computed(() => {
const showTrialExpiredDialog = ref(false)
const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account')
const showExcelExample = ref(false)
const vipStatus = inject<any>('vipStatus')
function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'info' = 'info') {
@@ -167,7 +168,7 @@ function removeTrademarkFile() {
// 保存会话到后端
async function saveSession() {
if (!trademarkData.value.length) return
try {
const sessionData = {
fileName: trademarkFileName.value,
@@ -177,7 +178,7 @@ async function saveSession() {
taskProgress: taskProgress.value,
queryStatus: queryStatus.value
}
const result = await markApi.saveSession(sessionData)
if (result.code === 200 || result.code === 0) {
const username = getUsernameFromToken()
@@ -197,15 +198,13 @@ async function restoreSession() {
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 || ''
@@ -213,11 +212,33 @@ async function restoreSession() {
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)
const saved = result.data.taskProgress
if (saved.product) {
taskProgress.value.product.total = saved.product.total || 0
taskProgress.value.product.current = saved.product.current || 0
taskProgress.value.product.completed = saved.product.completed || 0
}
if (saved.brand) {
taskProgress.value.brand.total = saved.brand.total || 0
taskProgress.value.brand.current = saved.brand.current || 0
taskProgress.value.brand.completed = saved.brand.completed || 0
}
if (saved.platform) {
taskProgress.value.platform.total = saved.platform.total || 0
taskProgress.value.platform.current = saved.platform.current || 0
taskProgress.value.platform.completed = saved.platform.completed || 0
}
}
// 恢复真实数据标记
if (taskProgress.value.product.total > 0) isProductTaskRealData.value = true
if (taskProgress.value.brand.total > 0) isBrandTaskRealData.value = true
emit('updateData', trademarkData.value)
}
} catch (error) {
@@ -742,7 +763,8 @@ defineExpose({
<div class="step-index">1</div>
<div class="step-card">
<div class="step-header"><div class="title">导入Excel表格</div></div>
<div class="desc">产品筛查需导入卖家精灵选品表格并勾选"导出主图"品牌筛查Excel需包含"品牌"</div>
<div class="desc">在卖家精灵导出文档时必须要勾选导出主图导入的表格必须具备品牌商品主图两个表头商品主图必须为超链接
<span class="example-link" @click="showExcelExample = true">点击查看示例</span></div>
<div
class="dropzone"
:class="{ uploading: uploadLoading, 'drag-active': dragActive }"
@@ -767,8 +789,8 @@ defineExpose({
</div>
</div>
<!-- 2. 选择亚马逊商标地区 -->
<div class="flow-item">
<!-- 2. 选择亚马逊商标地区 - 已隐藏 -->
<div v-if="false" class="flow-item">
<div class="step-index">2</div>
<div class="step-card">
<div class="step-header"><div class="title">选择亚马逊商家账号</div></div>
@@ -798,9 +820,9 @@ defineExpose({
</div>
</div>
<!-- 3. 选择产品地理区域 -->
<!-- 2. 选择产品地理区域 -->
<div class="flow-item">
<div class="step-index">3</div>
<div class="step-index">2</div>
<div class="step-card">
<div class="step-header"><div class="title">选择产品品牌地区</div></div>
<div class="desc">暂时仅支持美国品牌商标筛查后续将开放更多地区敬请期待</div>
@@ -812,15 +834,15 @@ defineExpose({
</div>
</div>
<!-- 4. 选择需查询的可多选 -->
<!-- 3. 选择需查询的可多选 -->
<div class="flow-item">
<div class="step-index">4</div>
<div class="step-index">3</div>
<div class="step-card">
<div class="step-header"><div class="title">选择需查询的可多选</div></div>
<div class="desc">默认查询违规产品可多选</div>
<div class="query-options">
<div :class="['query-card', { active: queryTypes.includes('product') }]" @click="toggleQueryType('product')">
<div :class="['query-card', 'query-disabled', { active: queryTypes.includes('product') }]">
<div class="query-check">
<div class="check-icon" v-if="queryTypes.includes('product')"></div>
</div>
@@ -830,7 +852,7 @@ defineExpose({
</div>
</div>
<div :class="['query-card', { active: queryTypes.includes('brand') }]" @click="toggleQueryType('brand')">
<div :class="['query-card', 'query-disabled', { active: queryTypes.includes('brand') }]">
<div class="query-check">
<div class="check-icon" v-if="queryTypes.includes('brand')"></div>
</div>
@@ -846,7 +868,7 @@ defineExpose({
</div>
<div class="query-content">
<div class="query-title">跟卖许可筛查</div>
<div class="query-desc">筛查亚马逊许可跟卖的商品</div>
<div class="query-desc">筛查亚马逊许可跟卖的商品(即将上线,敬请期待)</div>
</div>
</div>
</div>
@@ -857,7 +879,7 @@ defineExpose({
<!-- 底部开始查询按钮 -->
<div class="bottom-action">
<div class="action-header">
<span class="step-indicator">{{ completedSteps }}/4</span>
<span class="step-indicator">{{ completedSteps }}/3</span>
<el-button v-if="!trademarkLoading" class="start-btn" type="primary" :disabled="!trademarkFileName || queryTypes.length === 0" @click="startTrademarkQuery">
开始筛查
</el-button>
@@ -868,6 +890,11 @@ defineExpose({
</div>
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
<!-- Excel示例弹框 -->
<el-dialog v-model="showExcelExample" title="表格要求" width="600px" class="excel-dialog">
<img src="/image/excel-format-example.png" alt="Excel格式示例" style="width: 100%; height: auto; border-radius: 4px;" />
</el-dialog>
</div>
</template>
@@ -896,6 +923,24 @@ defineExpose({
.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; }
.example-link {
color: #1677FF;
cursor: pointer;
text-decoration: underline;
margin-left: 4px;
}
.example-link:hover {
color: #0958d9;
}
/* 弹框标题左对齐 */
:deep(.excel-dialog .el-dialog__header) {
text-align: left;
font-weight: bold;
margin-bottom: 0;
padding-bottom: 0;
}
.links { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
.link { color: #909399; cursor: pointer; font-size: 12px; }
@@ -954,7 +999,6 @@ defineExpose({
}
.query-card.query-disabled {
cursor: not-allowed;
opacity: 0.5;
pointer-events: none;
}
.query-check {
@@ -998,14 +1042,14 @@ defineExpose({
text-overflow: ellipsis;
width: 100%;
}
.query-title-disabled {
color: #909399;
}
.query-desc {
font-size: 11px;
color: #909399;
line-height: 1.3;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}

View File

@@ -97,6 +97,8 @@ async function saveBrandLogoInBackground(username: string) {
try {
const res = await splashApi.getBrandLogo(username)
const url = res?.data?.url || ''
// 保存到本地配置
await (window as any).electronAPI.saveBrandLogoConfig(username, url)
// 触发App.vue加载品牌logo
window.dispatchEvent(new CustomEvent('brandLogoChanged', { detail: url }))
} catch (error) {

View File

@@ -13,6 +13,7 @@ import { getToken, getUsernameFromToken } from '../../utils/token'
import { getOrCreateDeviceId } from '../../utils/deviceId'
import { updateApi } from '../../api/update'
import { splashApi } from '../../api/splash'
import { compressImage, formatFileSize, COMPRESS_PRESETS } from '../../utils/imageCompressor'
const TrialExpiredDialog = defineAsyncComponent(() => import('./TrialExpiredDialog.vue'))
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
@@ -394,10 +395,13 @@ function triggerFileSelect() {
async function loadSplashImage() {
try {
const username = getUsernameFromToken()
if (!username) return
if (!username) {
splashImageUrl.value = ''
return
}
const res = await splashApi.getSplashImage(username)
splashImageUrl.value = res.data.url
const url = res?.data?.url || res?.data?.data?.url || ''
splashImageUrl.value = url ? `${url}?t=${Date.now()}` : ''
} catch (error) {
splashImageUrl.value = ''
}
@@ -406,31 +410,36 @@ async function loadSplashImage() {
// 上传开屏图片
async function handleSplashImageUpload(event: Event) {
const input = event.target as HTMLInputElement
if (!input.files || input.files.length === 0) return
if (!input.files?.length) return
const username = getUsernameFromToken()
if (!username) {
ElMessage.warning('请先登录')
input.value = ''
return
}
// VIP验证
if (refreshVipStatus) await refreshVipStatus()
if (!props.isVip) {
if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType
trialExpiredType.value = vipStatus?.value?.expiredType || 'account'
showTrialExpiredDialog.value = true
input.value = ''
return
}
const file = input.files[0]
const username = getUsernameFromToken()
if (!username) return ElMessage.warning('请先登录')
if (!file.type.startsWith('image/')) return ElMessage.error('只支持图片文件')
if (file.size > 5 * 1024 * 1024) return ElMessage.error('图片大小不能超过5MB')
if (file.size > 10 * 1024 * 1024) return ElMessage.error('图片大小不能超过10MB')
try {
uploadingSplashImage.value = true
const res = await splashApi.uploadSplashImage(file, username)
const compressed = await compressImage(file, COMPRESS_PRESETS.HIGH)
const res = await splashApi.uploadSplashImage(compressed.file, username)
splashImageUrl.value = res.url
await (window as any).electronAPI.saveSplashConfig(username, res.url)
ElMessage.success('开屏图片设置成功,重启应用后生效')
} catch (error: any) {
ElMessage.error(error?.message || '上传失败')
} catch (error) {
splashImageUrl.value = ''
} finally {
uploadingSplashImage.value = false
input.value = ''
@@ -450,9 +459,11 @@ async function handleDeleteSplashImage() {
const username = getUsernameFromToken()
if (!username) return ElMessage.warning('请先登录')
// 先清空UI显示立即生效
splashImageUrl.value = ''
await splashApi.deleteSplashImage(username)
await (window as any).electronAPI.saveSplashConfig(username, '')
splashImageUrl.value = ''
ElMessage.success('开屏图片已删除,重启应用后恢复默认')
} catch (error: any) {
if (error !== 'cancel') ElMessage.error(error?.message || '删除失败')
@@ -465,10 +476,13 @@ async function handleDeleteSplashImage() {
async function loadBrandLogo() {
try {
const username = getUsernameFromToken()
if (!username) return
if (!username) {
brandLogoUrl.value = ''
return
}
const res = await splashApi.getBrandLogo(username)
brandLogoUrl.value = res.data.url
const url = res?.data?.url || ''
brandLogoUrl.value = url ? `${url}?t=${Date.now()}` : ''
} catch (error) {
brandLogoUrl.value = ''
}
@@ -477,31 +491,36 @@ async function loadBrandLogo() {
// 上传品牌logo
async function handleBrandLogoUpload(event: Event) {
const input = event.target as HTMLInputElement
if (!input.files || input.files.length === 0) return
if (!input.files?.length) return
const username = getUsernameFromToken()
if (!username) {
ElMessage.warning('请先登录')
input.value = ''
return
}
// VIP验证
if (refreshVipStatus) await refreshVipStatus()
if (!props.isVip) {
if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType
trialExpiredType.value = vipStatus?.value?.expiredType || 'account'
showTrialExpiredDialog.value = true
input.value = ''
return
}
const file = input.files[0]
const username = getUsernameFromToken()
if (!username) return ElMessage.warning('请先登录')
if (!file.type.startsWith('image/')) return ElMessage.error('只支持图片文件')
if (file.size > 5 * 1024 * 1024) return ElMessage.error('图片大小不能超过5MB')
if (file.size > 10 * 1024 * 1024) return ElMessage.error('图片大小不能超过10MB')
try {
uploadingBrandLogo.value = true
const res = await splashApi.uploadBrandLogo(file, username)
const compressed = await compressImage(file, COMPRESS_PRESETS.HIGH)
const res = await splashApi.uploadBrandLogo(compressed.file, username)
brandLogoUrl.value = res.url
ElMessage.success('品牌logo设置成功')
window.dispatchEvent(new CustomEvent('brandLogoChanged', { detail: res.url }))
} catch (error: any) {
ElMessage.error(error?.message || '上传失败')
} catch (error) {
brandLogoUrl.value = ''
} finally {
uploadingBrandLogo.value = false
input.value = ''
@@ -512,7 +531,7 @@ async function handleBrandLogoUpload(event: Event) {
async function handleDeleteBrandLogo() {
try {
await ElMessageBox.confirm(
'确定要删除品牌logo吗?删除后将隐藏logo区域。',
'确定要删除品牌 Banner吗?删除后将隐藏区域。',
'确认删除',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
)
@@ -521,11 +540,10 @@ async function handleDeleteBrandLogo() {
const username = getUsernameFromToken()
if (!username) return ElMessage.warning('请先登录')
await splashApi.deleteBrandLogo(username)
// 立即清空本地状态
// 先清空UI显示立即生效
brandLogoUrl.value = ''
ElMessage.success('品牌logo已删除')
await splashApi.deleteBrandLogo(username)
// 立即触发App.vue清空logo
window.dispatchEvent(new CustomEvent('brandLogoChanged', { detail: '' }))
@@ -542,6 +560,16 @@ onMounted(() => {
loadCurrentVersion()
loadSplashImage()
loadBrandLogo()
// 监听品牌logo删除事件
const handleBrandLogoChange = (e: any) => {
const url = e.detail || ''
brandLogoUrl.value = url
}
window.addEventListener('brandLogoChanged', handleBrandLogoChange)
// 组件卸载时移除监听
return () => window.removeEventListener('brandLogoChanged', handleBrandLogoChange)
})
</script>
@@ -736,7 +764,7 @@ onMounted(() => {
<div class="setting-item">
<div class="image-preview-wrapper" v-if="splashImageUrl">
<img :src="splashImageUrl" alt="开屏图片预览" class="preview-image" />
<img :src="splashImageUrl" alt="开屏图片" class="preview-image" />
<div class="image-buttons">
<input ref="fileInputRef" type="file" accept="image/*" @change="handleSplashImageUpload" style="display: none;" />
<button class="img-btn" :disabled="uploadingSplashImage" @click="triggerFileSelect">
@@ -747,29 +775,27 @@ onMounted(() => {
</button>
</div>
</div>
<div class="splash-placeholder" v-else>
<span class="placeholder-icon">🖼</span>
<span class="placeholder-text">未设置自定义开屏图片</span>
<div class="image-preview-wrapper splash-placeholder" v-else>
<input ref="fileInputRef" type="file" accept="image/*" @change="handleSplashImageUpload" style="display: none;" />
<el-button type="primary" size="small" style="margin-top: 8px;" :loading="uploadingSplashImage" @click="triggerFileSelect">
{{ uploadingSplashImage ? '上传中' : '选择图片' }}
</el-button>
<div class="placeholder-content" @click="triggerFileSelect">
<span class="placeholder-icon">🖼</span>
<span class="placeholder-text">{{ uploadingSplashImage ? '上传中...' : '点击选择开屏图片' }}</span>
</div>
</div>
</div>
</div>
<!-- 品牌logo页面 -->
<div id="section-brand" class="setting-section" @mouseenter="activeTab = 'brand'">
<div class="section-title-row">
<div class="section-title">品牌logo</div>
<div class="section-title">品牌 Banner</div>
<img src="/icon/vipExclusive.png" alt="VIP专享" class="vip-exclusive-logo" />
</div>
<div class="section-subtitle-text"> 支持 JPGPNG 格式最佳显示尺寸/比例 1200*736</div>
<div class="setting-item">
<div class="image-preview-wrapper" v-if="brandLogoUrl">
<img :src="brandLogoUrl" alt="品牌logo" class="preview-image" />
<img :src="brandLogoUrl" alt="品牌 Banner" class="preview-image" />
<div class="image-buttons">
<input ref="brandLogoInputRef" type="file" accept="image/*" @change="handleBrandLogoUpload" style="display: none;" />
<button class="img-btn" :disabled="uploadingBrandLogo" @click="brandLogoInputRef?.click()">
@@ -780,15 +806,13 @@ onMounted(() => {
</button>
</div>
</div>
<div class="splash-placeholder" v-else>
<span class="placeholder-icon">🏷</span>
<span class="placeholder-text">未设置品牌logo</span>
<div class="image-preview-wrapper splash-placeholder" v-else>
<input ref="brandLogoInputRef" type="file" accept="image/*" @change="handleBrandLogoUpload" style="display: none;" />
<el-button type="primary" size="small" style="margin-top: 8px;" :loading="uploadingBrandLogo" @click="brandLogoInputRef?.click()">
{{ uploadingBrandLogo ? '上传中' : '选择图片' }}
</el-button>
<div class="placeholder-content" @click="brandLogoInputRef?.click()">
<span class="placeholder-icon">🏷</span>
<span class="placeholder-text">{{ uploadingBrandLogo ? '上传中...' : '点击选择品牌 Banner' }}</span>
</div>
</div>
</div>
</div>
@@ -1246,8 +1270,43 @@ onMounted(() => {
.preview-image {
width: 100%;
height: 100%;
display: block;
object-fit: contain;
border-radius: 8px;
}
/* 占位符 */
.splash-placeholder {
width: 75% !important;
height: auto !important;
border: 2px dashed #d9d9d9 !important;
border-radius: 8px !important;
background: #fafafa !important;
cursor: pointer;
transition: all 0.3s;
}
.splash-placeholder:hover {
border-color: #1677ff !important;
background: #f0f7ff !important;
}
.placeholder-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 30px 20px;
}
.placeholder-icon {
font-size: 28px;
}
.placeholder-text {
font-size: 13px;
color: rgba(0, 0, 0, 0.45);
}
.image-buttons {

View File

@@ -20,7 +20,6 @@ interface Emits {
(e: 'open-settings'): void
(e: 'open-account-manager'): void
(e: 'check-update'): void
(e: 'show-login'): void
}
const props = defineProps<Props>()
@@ -113,10 +112,6 @@ onMounted(async () => {
</template>
</el-dropdown>
<!-- 登录/注册按钮 -->
<button v-if="!isAuthenticated" class="login-btn" @click="$emit('show-login')">
登录/注册
</button>
</div>
<div class="navbar-center">
<div class="breadcrumbs">
@@ -324,29 +319,6 @@ onMounted(async () => {
}
/* 登录/注册按钮 */
.login-btn {
height: 28px;
padding: 0 12px;
border: none;
border-radius: 4px;
background: #1677FF;
color: #fff;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
white-space: nowrap;
margin-left: 8px;
}
.login-btn:hover {
background: #0d5ed6;
}
.login-btn:active {
background: #0c54c2;
}
</style>
<style>

View File

@@ -0,0 +1,177 @@
/**
* 图片压缩工具 - 在上传前压缩图片,减少传输和存储大小
* 保持视觉效果的同时显著减小文件体积
*/
export interface CompressOptions {
/** 目标质量 0-1默认0.8 */
quality?: number
/** 最大宽度超过则等比缩放默认1920 */
maxWidth?: number
/** 最大高度超过则等比缩放默认1080 */
maxHeight?: number
/** 输出格式,默认'image/jpeg' */
mimeType?: string
/** 是否转换为WebP格式更小体积默认false */
useWebP?: boolean
}
/**
* 压缩图片文件
* @param file 原始图片文件
* @param options 压缩选项
* @returns 压缩后的Blob和压缩信息
*/
export async function compressImage(
file: File,
options: CompressOptions = {}
): Promise<{
blob: Blob
file: File
originalSize: number
compressedSize: number
compressionRatio: number
}> {
const {
quality = 0.85,
maxWidth = 1920,
maxHeight = 1080,
mimeType = 'image/jpeg',
useWebP = false,
} = options
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e) => {
const img = new Image()
img.onload = () => {
try {
// 计算压缩后的尺寸(保持宽高比)
let { width, height } = img
const aspectRatio = width / height
if (width > maxWidth) {
width = maxWidth
height = width / aspectRatio
}
if (height > maxHeight) {
height = maxHeight
width = height * aspectRatio
}
// 创建canvas进行压缩
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
if (!ctx) {
reject(new Error('无法获取canvas上下文'))
return
}
// 绘制图片
ctx.drawImage(img, 0, 0, width, height)
// 转换为Blob - 如果原图是PNG,保持PNG格式以保留透明度
const isPNG = file.type === 'image/png'
const outputMimeType = useWebP ? 'image/webp' : (isPNG ? 'image/png' : mimeType)
canvas.toBlob(
(blob) => {
if (!blob) {
reject(new Error('图片压缩失败'))
return
}
const originalSize = file.size
const compressedSize = blob.size
const compressionRatio = ((1 - compressedSize / originalSize) * 100).toFixed(2)
// 转换为File对象
const isPNG = file.type === 'image/png'
let newFileName = file.name
if (useWebP) {
newFileName = file.name.replace(/\.[^.]+$/, '.webp')
} else if (!isPNG) {
newFileName = file.name.replace(/\.[^.]+$/, '.jpg')
}
// 如果是PNG,保持原文件名
const compressedFile = new File(
[blob],
newFileName,
{ type: outputMimeType }
)
resolve({
blob,
file: compressedFile,
originalSize,
compressedSize,
compressionRatio: parseFloat(compressionRatio),
})
},
outputMimeType,
isPNG ? 1.0 : quality // PNG使用无损压缩(quality=1.0)
)
} catch (error) {
reject(error)
}
}
img.onerror = () => {
reject(new Error('图片加载失败'))
}
img.src = e.target?.result as string
}
reader.onerror = () => {
reject(new Error('文件读取失败'))
}
reader.readAsDataURL(file)
})
}
/**
* 格式化文件大小
*/
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
}
/**
* 预设压缩配置
*/
export const COMPRESS_PRESETS = {
/** 高质量适合开屏图片、品牌Logo- 1920x1080, 85%质量 */
HIGH: {
quality: 0.85,
maxWidth: 1920,
maxHeight: 1080,
mimeType: 'image/jpeg',
},
/** 中等质量(适合一般图片)- 1280x720, 80%质量 */
MEDIUM: {
quality: 0.8,
maxWidth: 1280,
maxHeight: 720,
mimeType: 'image/jpeg',
},
/** 缩略图(适合列表展示)- 400x400, 75%质量 */
THUMBNAIL: {
quality: 0.75,
maxWidth: 400,
maxHeight: 400,
mimeType: 'image/jpeg',
},
} as const