feat(genmai): 集成跟卖精灵账号管理系统跟

- 新增卖精灵账号管理功能,支持多账号切换
- 实现账号增删改查接口与前端交互逻辑
-优化打开跟卖精灵流程,增加账号选择界面
- 添加账号权限限制与订阅升级提醒
- 完善后端账号实体类及数据库映射
- 更新系统控制器以支持指定账号启动功能- 调整HTTP请求路径适配新工具模块路由- 升级客户端版本至2.5.3并优化代码结构
This commit is contained in:
2025-10-27 09:13:00 +08:00
parent 35c9fc205a
commit 0be60bc103
18 changed files with 720 additions and 132 deletions

View File

@@ -166,7 +166,6 @@ function startSpringBoot() {
`--logging.config=file:${logbackConfigPath}`,
`--logging.file.path=${logDir}`
];
springProcess = spawn(javaPath, springArgs, {
cwd: dataDir,
detached: false,
@@ -224,7 +223,7 @@ function startSpringBoot() {
app.quit();
}
}
startSpringBoot();
startSpringBoot();
function stopSpringBoot() {
if (!springProcess) return;
try {

View File

@@ -0,0 +1,39 @@
import { http } from './http'
export interface GenmaiAccount {
id?: number
name?: string
username: string
password: string
clientUsername?: string
token?: string
tokenExpireAt?: string
status?: number
remark?: string
createTime?: string
updateTime?: string
}
export const genmaiApi = {
getAccounts(name?: string) {
return http.get('/tool/genmai/accounts', name ? { name } : undefined)
},
getAccountLimit(name?: string) {
return http.get('/tool/genmai/account-limit', name ? { name } : undefined)
},
saveAccount(body: GenmaiAccount, name?: string) {
const url = name ? `/tool/genmai/accounts?name=${encodeURIComponent(name)}` : '/tool/genmai/accounts'
return http.post(url, body)
},
removeAccount(id: number) {
return http.delete(`/tool/genmai/accounts/${id}`)
},
validateAndRefresh(id: number) {
return http.post(`/tool/genmai/accounts/${id}/validate`)
}
}

View File

@@ -7,7 +7,7 @@ export const CONFIG = {
} as const;
function resolveBase(path: string): string {
if (path.startsWith('/monitor/') || path.startsWith('/system/') || path.startsWith('/tool/banma')) {
if (path.startsWith('/monitor/') || path.startsWith('/system/') || path.startsWith('/tool/banma') || path.startsWith('/tool/genmai')) {
return CONFIG.RUOYI_BASE;
}
return CONFIG.CLIENT_BASE;

View File

@@ -1,8 +1,9 @@
import { http } from './http';
export const systemApi = {
openGenmaiSpirit() {
return http.post('/api/system/genmai/open');
openGenmaiSpirit(accountId?: number | null) {
const url = accountId ? `/api/system/genmai/open?accountId=${accountId}` : '/api/system/genmai/open';
return http.post(url);
},
clearCache() {

View File

@@ -3,11 +3,13 @@ import { ref, computed, onMounted, defineAsyncComponent, inject } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { amazonApi } from '../../api/amazon'
import { systemApi } from '../../api/system'
import { genmaiApi, type GenmaiAccount } from '../../api/genmai'
import { handlePlatformFileExport } from '../../utils/settings'
import { getUsernameFromToken } from '../../utils/token'
import { useFileDrop } from '../../composables/useFileDrop'
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
const AccountManager = defineAsyncComponent(() => import('../common/AccountManager.vue'))
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
@@ -24,6 +26,9 @@ const progressVisible = ref(false) // 进度条是否显示(完成后仍保留
const localProductData = ref<any[]>([]) // 本地产品数据
const currentAsin = ref('') // 当前处理的ASIN
const genmaiLoading = ref(false) // Genmai Spirit加载状态
const genmaiAccounts = ref<GenmaiAccount[]>([]) // 跟卖精灵账号列表
const selectedGenmaiAccountId = ref<number | null>(null) // 选中的跟卖精灵账号ID
const currentTab = ref<'asin' | 'genmai'>('asin') // 当前选中的tab
let abortController: AbortController | null = null // 请求取消控制器
// 分页配置
@@ -35,6 +40,10 @@ const amazonUpload = ref<HTMLInputElement | null>(null)
const showTrialExpiredDialog = ref(false)
const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account')
// 账号管理弹框
const showAccountManager = ref(false)
const accountManagerRef = ref<any>(null)
const vipStatus = inject<any>('vipStatus')
// 计算属性 - 当前页数据
@@ -257,25 +266,25 @@ function stopFetch() {
}
async function openGenmaiSpirit() {
try {
await ElMessageBox.confirm('打开跟卖精灵会关闭所有谷歌浏览器进程,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
genmaiLoading.value = true
try {
await systemApi.openGenmaiSpirit()
showMessage('跟卖精灵已打开', 'success')
} catch (error: any) {
const errorMsg = error?.msg || error?.message || '打开跟卖精灵失败'
showMessage(errorMsg, 'error')
} finally {
genmaiLoading.value = false
}
} catch {
// 用户取消
if (!genmaiAccounts.value.length) {
showAccountManager.value = true
return
}
genmaiLoading.value = true
try {
await systemApi.openGenmaiSpirit(selectedGenmaiAccountId.value)
showMessage('跟卖精灵已打开', 'success')
} finally {
genmaiLoading.value = false
}
}
async function loadGenmaiAccounts() {
try {
const res = await genmaiApi.getAccounts(getUsernameFromToken())
genmaiAccounts.value = (res as any)?.data ?? []
if (genmaiAccounts.value[0]) selectedGenmaiAccountId.value = genmaiAccounts.value[0].id
} catch {}
}
// 分页处理
@@ -310,14 +319,12 @@ function downloadAmazonTemplate() {
URL.revokeObjectURL(url)
}
// 组件挂载时获取最新数据
onMounted(async () => {
try {
const resp = await amazonApi.getLatestProducts()
localProductData.value = resp.data?.products || []
} catch {
// 静默处理初始化失败
}
} catch {}
await loadGenmaiAccounts()
})
</script>
@@ -329,18 +336,79 @@ onMounted(async () => {
<aside class="steps-sidebar">
<!-- 顶部标签栏 -->
<div class="top-tabs">
<div class="tab-item active">
<div :class="['tab-item', { active: currentTab === 'asin' }]" @click="currentTab = 'asin'">
<span class="tab-icon">📦</span>
<span class="tab-text">ASIN查询</span>
</div>
<div class="tab-item" :class="{ loading: genmaiLoading }" @click="!genmaiLoading && openGenmaiSpirit()">
<span class="tab-icon" v-if="!genmaiLoading">🔍</span>
<span class="tab-icon spinner-icon" v-else></span>
<span class="tab-text">{{ genmaiLoading ? '启动中...' : '跟卖精灵' }}</span>
<div :class="['tab-item', { active: currentTab === 'genmai' }]" @click="currentTab = 'genmai'">
<span class="tab-icon">🔍</span>
<span class="tab-text">跟卖精灵</span>
</div>
</div>
<div class="steps-title">操作流程</div>
<div class="steps-flow">
<!-- 跟卖精灵流程区 -->
<div v-if="currentTab === 'genmai'" class="steps-title">操作流程</div>
<div v-if="currentTab === 'genmai'" class="steps-flow">
<!-- 1. 选择账号 -->
<div class="flow-item">
<div class="step-index">1</div>
<div class="step-card">
<div class="step-header"><div class="title">需启动的跟卖精灵账号</div></div>
<div class="desc">请选择需启动的跟卖精灵账号</div>
<template v-if="genmaiAccounts.length">
<el-scrollbar :class="['account-list', { 'scroll-limit': genmaiAccounts.length > 3 }]">
<div>
<div
v-for="acc in genmaiAccounts"
:key="acc.id"
:class="['acct-item', { selected: selectedGenmaiAccountId === acc.id }]"
@click="selectedGenmaiAccountId = acc.id"
>
<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" />
<span class="acct-text">{{ acc.name || acc.username }}</span>
<span v-if="selectedGenmaiAccountId === acc.id" class="acct-check"></span>
</span>
</div>
</div>
</el-scrollbar>
</template>
<template v-else>
<div class="placeholder-box">
<img class="placeholder-img" src="/icon/image.png" alt="add-account" />
<div class="placeholder-tip">请添加跟卖精灵账号</div>
</div>
</template>
<div class="step-actions btn-row">
<el-button size="small" class="w50" @click="showAccountManager = true">添加账号</el-button>
<el-button size="small" class="w50 btn-blue" @click="showAccountManager = true">账号管理</el-button>
</div>
</div>
</div>
<!-- 2. 启动服务 -->
<div class="flow-item">
<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="action-buttons column">
<el-button
size="small"
class="w100 btn-blue"
:disabled="genmaiLoading || !genmaiAccounts.length"
@click="openGenmaiSpirit"
>
<span v-if="!genmaiLoading">启动服务</span>
<span v-else> 启动中...</span>
</el-button>
</div>
</div>
</div>
</div>
<div v-if="currentTab === 'asin'" class="steps-title">操作流程</div>
<div v-if="currentTab === 'asin'" class="steps-flow">
<!-- 1 -->
<div class="flow-item">
<div class="step-index">1</div>
@@ -423,6 +491,14 @@ onMounted(async () => {
<!-- 试用期过期弹框 -->
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
<!-- 账号管理弹框 -->
<AccountManager
ref="accountManagerRef"
v-model="showAccountManager"
platform="genmai"
@refresh="loadGenmaiAccounts"
/>
<!-- 表格上方进度条 -->
<div v-if="progressVisible" class="progress-head">
<div class="progress-section">
@@ -502,11 +578,26 @@ onMounted(async () => {
.tab-item:last-child { border-radius: 0 3px 3px 0; border-left: none; }
.tab-item:hover { background: #e8f4ff; color: #409EFF; }
.tab-item.active { background: #1677FF; color: #fff; border-color: #1677FF; cursor: default; }
.tab-item.loading { background: #e8f4ff; color: #409EFF; cursor: not-allowed; opacity: 0.8; }
.tab-icon { font-size: 12px; }
.spinner-icon { animation: spin 1s linear infinite; display: inline-block; }
.tab-text { line-height: 1; }
/* 账号列表样式(和斑马一致) */
.account-list { height: auto; }
.scroll-limit { max-height: 140px; }
.placeholder-box { display: flex; align-items: center; justify-content: center; flex-direction: column; height: 100px; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; margin-bottom: 8px; }
.placeholder-img { width: 80px; opacity: 0.9; }
.placeholder-tip { margin-top: 6px; font-size: 12px; color: #a8abb2; }
.avatar { width: 18px; height: 18px; border-radius: 50%; }
.acct-row { display: grid; grid-template-columns: 6px 18px 1fr auto; align-items: center; gap: 6px; width: 100%; }
.acct-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; font-size: 12px; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
.status-dot.on { background: #22c55e; }
.status-dot.off { background: #f87171; }
.acct-item { padding: 6px 8px; border-radius: 6px; cursor: pointer; margin-bottom: 4px; }
.acct-item.selected { background: #eef5ff; box-shadow: inset 0 0 0 1px #d6e4ff; }
.acct-check { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 50%; background: transparent; color: #111; font-size: 12px; }
.account-list::-webkit-scrollbar { width: 0; height: 0; }
.body-layout { display: flex; gap: 12px; flex: 1; overflow: hidden; }
.steps-sidebar { width: 220px; background: #fff; border: 1px solid #ebeef5; border-radius: 6px; padding: 10px; height: 100%; flex-shrink: 0; }
.steps-title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
@@ -536,6 +627,9 @@ onMounted(async () => {
.file-chip .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.single-input.left { display: flex; gap: 8px; }
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
.step-actions { margin-top: 8px; display: flex; gap: 8px; }
.btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.w50 { width: 100%; }
.form-row { margin-bottom: 10px; }
.label { display: block; font-size: 12px; color: #606266; margin-bottom: 6px; }

View File

@@ -1,32 +1,50 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, defineAsyncComponent, watch } from 'vue'
import { zebraApi, type BanmaAccount } from '../../api/zebra'
import { genmaiApi, type GenmaiAccount } from '../../api/genmai'
import { ElMessageBox, ElMessage } from 'element-plus'
import { getUsernameFromToken } from '../../utils/token'
type PlatformKey = 'zebra' | 'shopee' | 'rakuten' | 'amazon'
const TrialExpiredDialog = defineAsyncComponent(() => import('./TrialExpiredDialog.vue'))
type PlatformKey = 'zebra' | 'shopee' | 'rakuten' | 'amazon' | 'genmai'
const props = defineProps<{ modelValue: boolean; platform?: PlatformKey }>()
const emit = defineEmits(['update:modelValue', 'add', 'refresh'])
const emit = defineEmits(['update:modelValue', 'refresh'])
const visible = computed({ get: () => props.modelValue, set: v => emit('update:modelValue', v) })
const curPlatform = ref<PlatformKey>(props.platform || 'zebra')
// 监听弹框打开,同步平台并加载数据
watch(() => props.modelValue, (newVal) => {
if (newVal && props.platform) {
curPlatform.value = props.platform
load()
}
})
// 升级订阅弹框
const showUpgradeDialog = ref(false)
const PLATFORM_LABEL: Record<PlatformKey, string> = {
zebra: '斑马 ERP',
shopee: 'Shopee 虾皮购物',
rakuten: 'Rakuten 乐天购物',
amazon: 'Amazon 亚马逊'
amazon: 'Amazon 亚马逊',
genmai: '跟卖精灵'
}
const accounts = ref<BanmaAccount[]>([])
const accounts = ref<(BanmaAccount | GenmaiAccount)[]>([])
const accountLimit = ref({ limit: 1, count: 0 })
// 添加账号对话框
const accountDialogVisible = ref(false)
const formUsername = ref('')
const formPassword = ref('')
async function load() {
const api = curPlatform.value === 'genmai' ? genmaiApi : zebraApi
const username = getUsernameFromToken()
const [res, limitRes] = await Promise.all([
zebraApi.getAccounts(username),
zebraApi.getAccountLimit(username)
])
const list = (res as any)?.data ?? res
accounts.value = Array.isArray(list) ? list : []
const limitData = (limitRes as any)?.data ?? limitRes
accountLimit.value = { limit: limitData?.limit ?? 1, count: limitData?.count ?? 0 }
const [res, limitRes] = await Promise.all([api.getAccounts(username), api.getAccountLimit(username)])
accounts.value = (res as any)?.data ?? res
accountLimit.value = (limitRes as any)?.data ?? limitRes
}
// 暴露方法供父组件调用
@@ -48,11 +66,39 @@ async function onDelete(a: any) {
try {
await ElMessageBox.confirm(`确定删除账号 "${a?.name || a?.username || id}" 吗?`, '提示', { type: 'warning' })
} catch { return }
await zebraApi.removeAccount(id)
const api = curPlatform.value === 'genmai' ? genmaiApi : zebraApi
await api.removeAccount(id)
ElMessage({ message: '删除成功', type: 'success' })
await load()
emit('refresh') // 通知外层组件刷新账号列表
}
async function handleAddAccount() {
if (accountLimit.value.count >= accountLimit.value.limit) {
ElMessage({ message: `账号数量已达上限`, type: 'warning' })
return
}
formUsername.value = ''
formPassword.value = ''
accountDialogVisible.value = true
}
async function submitAccount() {
const api = curPlatform.value === 'genmai' ? genmaiApi : zebraApi
try {
await api.saveAccount({
username: formUsername.value,
password: formPassword.value,
status: 1
}, getUsernameFromToken())
ElMessage({ message: '添加成功', type: 'success' })
accountDialogVisible.value = false
await load()
emit('refresh')
} catch (e: any) {
ElMessage({ message: e.message || '添加失败', type: 'error' })
}
}
</script>
<script lang="ts">
@@ -69,8 +115,9 @@ export default defineComponent({ name: 'AccountManager' })
<div class="layout">
<aside class="sider">
<div class="sider-title">全账号管理</div>
<div class="nav only-zebra">
<div class="nav">
<div :class="['nav-item', {active: curPlatform==='zebra'}]" @click="switchPlatform('zebra')">斑马 ERP</div>
<div :class="['nav-item', {active: curPlatform==='genmai'}]" @click="switchPlatform('genmai')">跟卖精灵</div>
</div>
</aside>
<section class="content">
@@ -80,8 +127,8 @@ export default defineComponent({ name: 'AccountManager' })
<div class="head-main">
<div class="main-title">在线账号管理{{ accountLimit.count }}/{{ accountLimit.limit }}</div>
<div class="main-sub">
您当前订阅可同时托管{{ accountLimit.limit }}斑马账号<br>
<span v-if="accountLimit.limit < 3">如需扩增账号数量,请 <span class="upgrade">升级订阅</span></span>
您当前订阅可同时托管{{ accountLimit.limit }}{{ curPlatform === 'genmai' ? '跟卖精灵' : '斑马' }}账号<br>
<span v-if="accountLimit.limit < 3">如需扩增账号数量,请 <span class="upgrade" @click="showUpgradeDialog = true">升级订阅</span></span>
</div>
</div>
</div>
@@ -97,10 +144,32 @@ export default defineComponent({ name: 'AccountManager' })
</div>
</div>
<div class="footer">
<el-button type="primary" class="btn" @click="$emit('add')">添加账号</el-button>
<el-button type="primary" class="btn" @click="handleAddAccount">添加账号</el-button>
</div>
</section>
</div>
<!-- 添加账号对话框 -->
<el-dialog v-model="accountDialogVisible" width="420px" class="add-account-dialog">
<template #header>
<div class="aad-header">
<img class="aad-icon" src="/icon/image.png" alt="logo" />
<div class="aad-title">添加{{ curPlatform === 'genmai' ? '跟卖精灵' : '斑马' }}账号</div>
</div>
</template>
<div class="aad-row">
<el-input v-model="formUsername" :placeholder="curPlatform === 'genmai' ? '请输入账号nickname' : '请输入账号'" />
</div>
<div class="aad-row">
<el-input v-model="formPassword" placeholder="请输入密码" type="password" show-password />
</div>
<template #footer>
<el-button type="primary" class="btn-blue" style="width: 100%" @click="submitAccount">添加</el-button>
</template>
</el-dialog>
<!-- 升级订阅弹框 -->
<TrialExpiredDialog v-model="showUpgradeDialog" expired-type="subscribe" />
</el-dialog>
</template>
@@ -120,9 +189,19 @@ export default defineComponent({ name: 'AccountManager' })
.head-main { text-align:center; }
.main-title { font-size: 16px; font-weight: 600; color:#303133; margin-bottom: 4px; }
.main-sub { color:#909399; font-size: 11px; line-height: 1.4; }
.upgrade { color:#409EFF; cursor: pointer; }
.upgrade { color:#409EFF; cursor: pointer; font-weight: 600; transition: all 0.2s ease; }
.upgrade:hover { color:#0d5ed6; text-decoration: underline; }
.list { border:1px solid #ebeef5; border-radius: 6px; background: #fff; flex: 0 0 auto; width: 100%; max-height: 160px; overflow-y: auto; }
.list.compact { max-height: 48px; }
/* 添加账号对话框样式 */
.add-account-dialog .aad-header { display:flex; flex-direction: column; align-items:center; gap:8px; padding-top: 8px; width: 100%; }
.add-account-dialog .aad-icon { width: 120px; height: auto; }
.add-account-dialog .aad-title { font-weight: 600; font-size: 18px; text-align: center; }
.add-account-dialog .aad-row { margin-top: 12px; }
:deep(.add-account-dialog .el-dialog__header) { text-align: center; padding-right: 0; display: block; }
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
.btn-blue:hover { background: #0d5ed6; border-color: #0d5ed6; }
.row { display:grid; grid-template-columns: 8px 1fr 120px 60px; gap: 8px; align-items:center; padding: 4px 8px; border-bottom: 1px solid #f5f5f5; height: 28px; }
.row:last-child { border-bottom:none; }
.row:hover { background:#fafafa; }

View File

@@ -564,7 +564,7 @@ async function removeCurrentAccount() {
<!-- 试用期过期弹框 -->
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
<AccountManager ref="accountManagerRef" v-model="managerVisible" platform="zebra" @add="openAddAccount" @refresh="loadAccounts" />
<AccountManager ref="accountManagerRef" v-model="managerVisible" platform="zebra" @refresh="loadAccounts" />
</div>
</template>