feat(client): 实现跟卖精灵异步启动和Chrome驱动预加载- 在GenmaiServiceImpl中添加@Async注解实现异步启动跟卖精灵- 增加ChromeDriverPreloader组件预加载Chrome驱动- 添加AsyncConfig配置类启用异步支持

- 优化跟卖精灵启动提示信息和加载状态显示
- 移除Java代码中关于刷新令牌的相关逻辑和依赖- 更新版本号从2.5.5到2.5.6
This commit is contained in:
2025-10-27 16:49:37 +08:00
parent 7e065c1a0b
commit 84087ddf80
20 changed files with 138 additions and 302 deletions

View File

@@ -40,7 +40,7 @@ const navigationHistory = ref<string[]>(['rakuten'])
const currentHistoryIndex = ref(0)
// 应用状态
const activeMenu = ref('rakuten')
const activeMenu = ref(localStorage.getItem('active-menu') || 'rakuten')
const isAuthenticated = ref(false)
const showAuthDialog = ref(false)
const showRegDialog = ref(false)
@@ -190,6 +190,7 @@ function handleMenuSelect(key: string) {
}
activeMenu.value = key
localStorage.setItem('active-menu', key)
addToHistory(key)
}
@@ -778,7 +779,7 @@ onUnmounted(() => {
width: 180px;
min-width: 180px;
flex-shrink: 0;
background: #ffffff;
background: #F0F0F0;
border-right: 1px solid #e8eaec;
padding: 16px 12px;
box-sizing: border-box;

View File

@@ -33,20 +33,29 @@ async function getToken(): Promise<string> {
async function request<T>(path: string, options: RequestInit & { signal?: AbortSignal }): Promise<T> {
const token = await getToken();
const res = await fetch(`${resolveBase(path)}${path}`, {
credentials: 'omit',
cache: 'no-store',
...options,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
...options.headers
}
});
let res: Response;
try {
res = await fetch(`${resolveBase(path)}${path}`, {
credentials: 'omit',
cache: 'no-store',
...options,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
...options.headers
}
});
} catch (e) {
throw new Error('无法连接服务器,请检查网络后重试');
}
if (!res.ok) {
if (res.status >= 500) {
throw new Error('无法连接服务器,请检查网络后重试');
}
const text = await res.text().catch(() => '');
throw new Error(text || `HTTP ${res.status}`);
throw new Error(text || '无法连接服务器,请检查网络后重试');
}
const contentType = res.headers.get('content-type') || '';
@@ -81,18 +90,27 @@ export const http = {
async upload<T>(path: string, form: FormData, signal?: AbortSignal) {
const token = await getToken();
const res = await fetch(`${resolveBase(path)}${path}`, {
method: 'POST',
body: form,
credentials: 'omit',
cache: 'no-store',
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
signal
});
let res: Response;
try {
res = await fetch(`${resolveBase(path)}${path}`, {
method: 'POST',
body: form,
credentials: 'omit',
cache: 'no-store',
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
signal
});
} catch (e) {
throw new Error('无法连接服务器,请检查网络后重试');
}
if (!res.ok) {
if (res.status >= 500) {
throw new Error('无法连接服务器,请检查网络后重试');
}
const text = await res.text().catch(() => '');
throw new Error(text || `HTTP ${res.status}`);
throw new Error(text || '无法连接服务器,请检查网络后重试');
}
const contentType = res.headers.get('content-type') || '';

View File

@@ -170,7 +170,6 @@ async function batchGetProductInfo(asinList: string[]) {
// 处理完成状态更新
progressPercentage.value = 100
currentAsin.value = '处理完成'
selectedFileName.value = ''
@@ -178,7 +177,6 @@ async function batchGetProductInfo(asinList: string[]) {
if (error.name !== 'AbortError') {
showMessage(error.message || '批量获取产品信息失败', 'error')
currentAsin.value = '处理失败'
selectedFileName.value = ''
}
} finally {
tableLoading.value = false
@@ -261,7 +259,6 @@ function stopFetch() {
abortController = null
loading.value = false
currentAsin.value = '已停止'
selectedFileName.value = ''
showMessage('已停止获取产品数据', 'info')
}
@@ -273,8 +270,10 @@ async function openGenmaiSpirit() {
genmaiLoading.value = true
try {
await systemApi.openGenmaiSpirit(selectedGenmaiAccountId.value)
showMessage('跟卖精灵已打开', 'success')
} finally {
showMessage('跟卖精灵正在启动,请稍候...', 'success')
setTimeout(() => { genmaiLoading.value = false }, 3000)
} catch (error: any) {
showMessage(error.message || '启动失败', 'error')
genmaiLoading.value = false
}
}
@@ -366,7 +365,7 @@ onMounted(async () => {
>
<span class="acct-row">
<span :class="['status-dot', acc.status === 1 ? 'on' : 'off']"></span>
<img class="avatar" src="/image/img_v3_02qd_052605f0-4be3-44db-9691-35ee5ff6201g.jpg" alt="avatar" />
<img class="avatar" src="/image/user.png" alt="avatar" />
<span class="acct-text">{{ acc.name || acc.username }}</span>
<span v-if="selectedGenmaiAccountId === acc.id" class="acct-check"></span>
</span>
@@ -391,7 +390,7 @@ onMounted(async () => {
<div class="step-index">2</div>
<div class="step-card">
<div class="step-header"><div class="title">启动服务</div></div>
<div class="desc">请确保设备已安装Chrome浏览器否则服务将无法启动打开跟卖精灵将关闭Chrome浏览器进程</div>
<div class="desc">请确保设备已安装Chrome浏览器否则服务将无法启动打开跟卖精灵将关闭Chrome浏览器进程首次启动需初始化浏览器驱动可能需要1-3分钟</div>
<div class="action-buttons column">
<el-button
size="small"
@@ -400,7 +399,7 @@ onMounted(async () => {
@click="openGenmaiSpirit"
>
<span v-if="!genmaiLoading">启动服务</span>
<span v-else> 启动中...</span>
<span v-else><span class="spinner"></span> 启动中...</span>
</el-button>
</div>
</div>
@@ -680,6 +679,7 @@ onMounted(async () => {
.price { color: #e6a23c; font-weight: 600; }
.table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; }
.spinner { font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px; }
.action-buttons .spinner { font-size: 14px; margin-bottom: 0; display: inline-block; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.pagination-fixed { flex-shrink: 0; padding: 8px 12px 0 12px; background: #fff; display: flex; justify-content: flex-end; }
.pagination-fixed :deep(.el-pager li.is-active) { border: 1px solid #1677FF; border-radius: 4px; color: #1677FF; background: #fff; }

View File

@@ -136,7 +136,7 @@ export default defineComponent({ name: 'AccountManager' })
<div v-for="a in accounts" :key="a.id" class="row">
<span :class="['dot', a.status === 1 ? 'on' : 'off']"></span>
<div class="user-info">
<img class="avatar" src="/image/img_v3_02qd_052605f0-4be3-44db-9691-35ee5ff6201g.jpg" />
<img class="avatar" src="/image/user.png" />
<span class="name">{{ a.name || a.username }}</span>
</div>
<span class="date">{{ formatDate(a) }}</span>

View File

@@ -485,7 +485,7 @@ onMounted(() => {
</el-checkbox>
</div>
<div class="setting-desc" style="margin-top: 4px;">
自动安装将仅会安装到新版本后但需手动点击"重启升级"后才可升级
关闭自动安装,你仍会收到新版本提示,但需手动点击"重启升级"后才可升级
</div>
</div>

View File

@@ -6,7 +6,7 @@
<div v-if="stage === 'check'" class="update-content">
<div class="update-layout">
<div class="left-pane">
<img src="/icon/icon.png" class="app-icon app-icon-large" alt="App Icon"/>
<img src="/icon/icon1.png" class="app-icon app-icon-large" alt="App Icon"/>
</div>
<div class="right-pane">
<p class="announce">新版本的"{{ appName }}"已经发布</p>
@@ -41,7 +41,7 @@
<div v-else-if="stage === 'downloading'" class="update-content">
<div class="download-main">
<div class="download-icon">
<img src="/icon/icon.png" class="app-icon" alt="App Icon"/>
<img src="/icon/icon1.png" class="app-icon" alt="App Icon"/>
</div>
<div class="download-content">
<div class="download-info">
@@ -64,7 +64,7 @@
<div v-else-if="stage === 'completed'" class="update-content">
<div class="download-main">
<div class="download-icon">
<img src="/icon/icon.png" class="app-icon" alt="App Icon"/>
<img src="/icon/icon1.png" class="app-icon" alt="App Icon"/>
</div>
<div class="download-content">
<div class="download-info">

View File

@@ -32,6 +32,14 @@ const displayUsername = computed(() => {
return props.isAuthenticated ? props.currentUsername : '未登录'
})
const menuItems = [
{ command: 'check-update', label: computed(() => `检查更新 v${props.currentVersion}`), class: 'menu-item' },
{ command: 'account-manager', label: '我的电商账号', class: 'menu-item' },
{ command: 'device', label: '我的设备', class: 'menu-item' },
{ command: 'settings', label: '设置', class: 'menu-item' },
{ command: 'logout', label: '退出', class: 'menu-item logout-item', showIf: () => props.isAuthenticated }
]
async function handleMinimize() {
await (window as any).electronAPI.windowMinimize()
}
@@ -45,24 +53,17 @@ async function handleClose() {
await (window as any).electronAPI.windowClose()
}
const commandMap: Record<string, keyof Emits> = {
logout: 'logout',
device: 'open-device',
settings: 'open-settings',
'account-manager': 'open-account-manager',
'check-update': 'check-update'
}
function handleCommand(command: string) {
switch (command) {
case 'logout':
emit('logout')
break
case 'device':
emit('open-device')
break
case 'settings':
emit('open-settings')
break
case 'account-manager':
emit('open-account-manager')
break
case 'check-update':
emit('check-update')
break
}
const emitName = commandMap[command]
if (emitName) emit(emitName as any)
}
onMounted(async () => {
@@ -100,20 +101,13 @@ onMounted(async () => {
<el-dropdown-item disabled class="username-item">
{{ displayUsername }}
</el-dropdown-item>
<el-dropdown-item command="check-update" class="menu-item">
检查更新 v{{ currentVersion }}
</el-dropdown-item>
<el-dropdown-item command="account-manager" class="menu-item">
我的电商账号
</el-dropdown-item>
<el-dropdown-item command="device" class="menu-item">
我的设备
</el-dropdown-item>
<el-dropdown-item command="settings" class="menu-item">
设置
</el-dropdown-item>
<el-dropdown-item v-if="isAuthenticated" command="logout" class="menu-item logout-item">
退出
<el-dropdown-item
v-for="item in menuItems"
:key="item.command"
v-show="!item.showIf || item.showIf()"
:command="item.command"
:class="item.class">
{{ typeof item.label === 'string' ? item.label : item.label.value }}
</el-dropdown-item>
</el-dropdown-menu>
</template>

View File

@@ -265,15 +265,10 @@ async function handleStartSearch() {
}
allProducts.value = products
pendingFile.value = null
selectedFileName.value = ''
} catch (e: any) {
if (e.name !== 'AbortError') {
statusType.value = 'error'
statusMessage.value = '解析失败,请重试'
// 失败后清空文件信息,让用户重新上传
pendingFile.value = null
selectedFileName.value = ''
}
} finally {
loading.value = false
@@ -309,9 +304,6 @@ function stopTask() {
statusMessage.value = '任务已停止'
// 保留进度条和当前进度
allProducts.value = allProducts.value.map(p => ({...p, searching1688: false}))
// 清空文件信息,让用户重新上传
pendingFile.value = null
selectedFileName.value = ''
}
async function startBatch1688Search(products: any[]) {
@@ -499,7 +491,7 @@ onMounted(loadLatest)
<div class="step-header">
<div class="title">网站地区</div>
</div>
<div class="desc">请选择目标网站地区日本区</div>
<div class="desc">仅支持乐天市场日本区商品查询后续将开放更多乐天网站地区敬请期待</div>
<el-select v-model="region" placeholder="选择地区" size="small" style="width: 100%" disabled>
<el-option v-for="opt in regionOptions" :key="opt.value" :label="opt.label" :value="opt.value">
<span style="margin-right:6px">{{ opt.flag }}</span>{{ opt.label }}

View File

@@ -384,7 +384,7 @@ async function removeCurrentAccount() {
>
<span class="acct-row">
<span :class="['status-dot', a.status === 1 ? 'on' : 'off']"></span>
<img class="avatar" src="/image/img_v3_02qd_052605f0-4be3-44db-9691-35ee5ff6201g.jpg" alt="avatar" />
<img class="avatar" src="/image/user.png" alt="avatar" />
<span class="acct-text">{{ a.name || a.username }}</span>
<span v-if="accountId === a.id" class="acct-check"></span>
</span>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB