feat(amazon): 更新商标筛查界面图标与状态展示- 将商标筛查状态图标从图片替换为 SVG 图标
- 添加了进行中、取消、完成/失败状态的 SVG 图标 -优化任务进度指示器,使用 SVG 并支持旋转动画 - 禁用跟卖许可筛查功能并更新提示文案 - 调整标签页样式和间距,适配不同屏幕尺寸 -修复状态横幅图标显示问题,并统一图标尺寸 - 更新全局样式以提升视觉一致性和用户体验
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -32,10 +32,8 @@ export function createTray(mainWindow: BrowserWindow | null) {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 右键菜单
|
||||
updateTrayMenu(mainWindow)
|
||||
|
||||
return tray
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"> 支持 JPG、PNG 格式,最佳显示尺寸/比例 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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
177
electron-vue-template/src/renderer/utils/imageCompressor.ts
Normal file
177
electron-vue-template/src/renderer/utils/imageCompressor.ts
Normal 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
|
||||
Reference in New Issue
Block a user