This commit is contained in:
2025-09-23 17:20:58 +08:00
parent ca2b70dfbe
commit 5f3e9a08f6
25 changed files with 1471 additions and 1095 deletions

View File

@@ -1,17 +1,34 @@
<script setup lang="ts">
import { onMounted, ref, computed, defineAsyncComponent } from 'vue'
import { ElConfigProvider, ElMessage, ElMessageBox } from 'element-plus'
import {onMounted, ref, computed, defineAsyncComponent, type Component} from 'vue'
import {ElConfigProvider, ElMessage, ElMessageBox} from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
// 图标已移至对应组件
import 'element-plus/dist/index.css'
import { authApi } from './api/auth'
import { deviceApi, type DeviceItem, type DeviceQuota } from './api/device'
import ZebraDashboard from './components/zebra/ZebraDashboard.vue'
import {authApi} from './api/auth'
import {deviceApi, type DeviceItem, type DeviceQuota} from './api/device'
// 面板按需加载,互不影响且可缓存
const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue'))
const RegisterDialog = defineAsyncComponent(() => import('./components/auth/RegisterDialog.vue'))
const NavigationBar = defineAsyncComponent(() => import('./components/layout/NavigationBar.vue'))
const RakutenDashboard = defineAsyncComponent(() => import('./components/rakuten/RakutenDashboard.vue'))
const AmazonDashboard = defineAsyncComponent(() => import('./components/amazon/AmazonDashboard.vue'))
const ZebraDashboard = defineAsyncComponent(() => import('./components/zebra/ZebraDashboard.vue'))
const dashboardsMap: Record<string, Component> = {
rakuten: RakutenDashboard,
amazon: AmazonDashboard,
zebra: ZebraDashboard,
}
const activeDashboard = computed<Component | null>(() => {
if (!isAuthenticated.value) return null
return dashboardsMap[activeMenu.value] || null
})
const isDefaultPanel = computed(() => ['rakuten', 'amazon', 'zebra'].includes(activeMenu.value))
const showHomeSplash = computed(() => !isAuthenticated.value && isDefaultPanel.value)
const showPlaceholder = computed(() => !showHomeSplash.value && !activeDashboard.value)
// 导航历史栈
const navigationHistory = ref<string[]>(['rakuten'])
@@ -27,15 +44,15 @@ const currentUsername = ref('')
const showDeviceDialog = ref(false)
const deviceLoading = ref(false)
const devices = ref<DeviceItem[]>([])
const deviceQuota = ref<DeviceQuota>({ limit: 0, used: 0 })
const deviceQuota = ref<DeviceQuota>({limit: 0, used: 0})
const userPermissions = ref<string>('')
// 菜单配置 - 复刻ERP客户端格式
const menuConfig = [
{ key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R' },
{ key: 'amazon', name: 'Amazon', index: 'amazon', icon: 'A' },
{ key: 'zebra', name: 'Zebra', index: 'zebra', icon: 'Z' },
{ key: 'shopee', name: 'Shopee', index: 'shopee', icon: 'S' },
{key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R'},
{key: 'amazon', name: 'Amazon', index: 'amazon', icon: 'A'},
{key: 'zebra', name: 'Zebra', index: 'zebra', icon: 'Z'},
{key: 'shopee', name: 'Shopee', index: 'shopee', icon: 'S'},
]
// 权限检查 - 复刻ERP客户端逻辑
@@ -65,7 +82,9 @@ function showContent() {
const loading = document.getElementById('loading')
if (loading) {
loading.style.opacity = '0'
setTimeout(() => { loading.style.display = 'none' }, 100)
setTimeout(() => {
loading.style.display = 'none'
}, 100)
}
const app = document.getElementById('app-root')
if (app) app.style.opacity = '1'
@@ -114,10 +133,13 @@ async function handleLoginSuccess(data: { token: string; permissions?: string })
showAuthDialog.value = false
try {
// 保存token到本地数据库
await authApi.saveToken(data.token)
const username = getUsernameFromToken(data.token)
currentUsername.value = username
userPermissions.value = data?.permissions || ''
await deviceApi.register({ username })
await deviceApi.register({username})
// 建立SSE连接
SSEManager.connect()
@@ -126,15 +148,10 @@ async function handleLoginSuccess(data: { token: string; permissions?: string })
console.warn('设备注册失败:', e)
}
}
async function logout() {
try {
await fetch('/api/cache/delete?key=token', { method: 'POST' })
} catch (e) {
console.log('删除后端token缓存失败:', e)
}
async function logout() {
await authApi.deleteTokenCache()
// 清理前端状态
try { localStorage.removeItem('token') } catch {}
isAuthenticated.value = false
currentUsername.value = ''
userPermissions.value = ''
@@ -151,15 +168,15 @@ async function handleUserClick() {
return
}
try {
await ElMessageBox.confirm('确认退出登录?', '提示', { type: 'warning', confirmButtonText: '退出', cancelButtonText: '取消' })
await ElMessageBox.confirm('确认退出登录?', '提示', {
type: 'warning',
confirmButtonText: '退出',
cancelButtonText: '取消'
})
await logout()
ElMessage.success('已退出登录')
} catch {}
}
function handleLoginCancel() {
showAuthDialog.value = false
} catch {
}
}
function showRegisterDialog() {
@@ -177,69 +194,68 @@ function backToLogin() {
showAuthDialog.value = true
}
// 检查认证状态 - 复刻ERP客户端逻辑
async function checkAuth() {
const token = localStorage.getItem('token')
const authRequiredMenus = ['rakuten', 'amazon', 'zebra', 'shopee']
if (token) {
try {
try {
await authApi.sessionBootstrap().catch(() => undefined)
const token = await authApi.getToken()
if (token) {
const response = await authApi.verifyToken(token)
if (response.success) {
if (response?.success) {
isAuthenticated.value = true
if (!currentUsername.value) {
const u = getUsernameFromToken(token)
if (u) currentUsername.value = u
}
userPermissions.value = response.permissions || ''
// 认证成功后建立SSE连接
currentUsername.value = getUsernameFromToken(token) || ''
SSEManager.connect()
return
}
} catch {
localStorage.removeItem('token')
await authApi.deleteTokenCache()
}
} catch {
// 忽略
}
// 检查是否需要显示登录弹框
if (!isAuthenticated.value && authRequiredMenus.includes(activeMenu.value)) {
if (authRequiredMenus.includes(activeMenu.value)) {
showAuthDialog.value = true
}
}
function getClientIdFromToken(token?: string) {
async function getClientIdFromToken(token?: string) {
try {
const t = token || localStorage.getItem('token') || ''
let t = token
if (!t) {
t = await authApi.getToken()
}
if (!t) return ''
const payload = JSON.parse(atob(t.split('.')[1] || ''))
const clientId = payload.clientId || ''
console.log('从token解析clientId:', { token: t?.substring(0, 20) + '...', clientId })
return clientId
} catch (e) {
console.warn('解析token中的clientId失败:', e)
return payload.clientId || ''
} catch {
return ''
}
}
function getUsernameFromToken(token?: string) {
const t = token || localStorage.getItem('token') || ''
const payload = JSON.parse(atob(t.split('.')[1] || ''))
function getUsernameFromToken(token: string) {
try {
const payload = JSON.parse(atob(token.split('.')[1] || ''))
return payload.username || ''
} catch {
return ''
}
}
// SSE管理器 - 简化封装
// SSE管理器
const SSEManager = {
connection: null as EventSource | null,
async connect() {
if (this.connection) return
const token = localStorage.getItem('token')
const clientId = getClientIdFromToken(token)
if (!token || !clientId) return
try {
// 简化配置获取,失败时使用默认配置
const token = await authApi.getToken()
if (!token) return
const clientId = await getClientIdFromToken(token)
if (!clientId) return
let sseUrl = 'http://192.168.1.89:8080/monitor/account/events'
try {
const resp = await fetch('/api/config/server')
@@ -247,125 +263,59 @@ const SSEManager = {
const config = await resp.json()
sseUrl = config.sseUrl || sseUrl
}
} catch {}
} catch {
}
const src = new EventSource(`${sseUrl}?clientId=${clientId}&token=${token}`)
this.connection = src
const username = getUsernameFromToken(token)
console.log('=== SSE连接初始化 ===')
console.log('连接URL:', sseUrl)
console.log('用户名:', username)
console.log('客户端ID:', clientId)
console.log('预期sessionKey:', `${username}:${clientId}`)
console.log('完整连接URL:', `${sseUrl}?clientId=${clientId}&token=${token.substring(0, 20)}...`)
src.onopen = () => {
console.log('=== SSE连接成功 ===')
console.log('✅ SSE连接已成功打开')
console.log('连接状态:', src.readyState, '(0=CONNECTING, 1=OPEN, 2=CLOSED)')
console.log('连接URL:', src.url)
console.log('连接时间:', new Date().toLocaleTimeString())
}
src.onmessage = (e) => {
console.log('=== SSE消息接收 ===')
console.log('📨 SSE收到原始消息:', e)
console.log('事件类型:', e.type)
console.log('消息数据:', e.data)
console.log('接收时间:', new Date().toLocaleTimeString())
this.handleMessage(e)
}
src.onerror = (e) => {
console.log('=== SSE连接错误 ===')
console.error('❌ SSE连接错误:', e)
console.log('连接状态:', src.readyState)
console.log('错误时间:', new Date().toLocaleTimeString())
this.handleError()
}
} catch (e) {
console.warn('SSE连接失败:', e.message)
src.onopen = () => console.log('SSE连接已建立')
src.onmessage = (e) => this.handleMessage(e)
src.onerror = () => this.handleError()
} catch (e: any) {
console.warn('SSE连接失败:', e?.message || e)
}
},
handleMessage(e: MessageEvent) {
try {
console.log('=== SSE消息处理 ===')
console.log('原始消息数据:', e.data)
console.log('SSE消息:', e.data)
const payload = JSON.parse(e.data)
console.log('解析后的消息:', payload)
console.log('事件类型:', payload.type)
console.log('消息内容:', payload.message)
switch (payload.type) {
case 'ready':
console.log('SSE连接已就绪')
break
case 'DEVICE_REMOVED':
console.log('🚨 收到设备移除事件正在执行logout')
logout()
ElMessage.warning('您的设备已被移除,请重新登录')
break
case 'FORCE_LOGOUT':
console.log('🚨 收到强制退出事件正在执行logout')
logout()
ElMessage.warning('会话已失效,请重新登录')
break
case 'PERMISSIONS_UPDATED':
console.log('🔄 收到权限更新事件,重新检查权限')
checkAuth()
break
default:
console.log('❓ 收到未知SSE事件:', payload.type, payload)
}
} catch (err) {
console.error('SSE消息处理失败:', err)
console.error('原始数据:', e.data)
console.error('SSE消息处理失败:', err, '原始数据:', e.data)
}
},
handleError() {
console.log('=== SSE错误处理 ===')
console.log('准备断开并重连SSE')
this.disconnect()
setTimeout(() => {
console.log('🔄 开始重连SSE')
this.connect()
}, 3000)
setTimeout(() => this.connect(), 3000)
},
disconnect() {
if (this.connection) {
console.log('=== SSE断开连接 ===')
console.log('断开连接URL:', this.connection.url)
console.log('断开前状态:', this.connection.readyState)
try {
this.connection.close()
console.log('✅ SSE连接已主动关闭')
} catch (e) {
console.log('❌ SSE关闭时出错:', e.message)
} catch {
}
this.connection = null
} else {
console.log('⚠️ 尝试断开SSE但连接不存在')
}
},
// 检查连接状态
checkStatus() {
if (!this.connection) {
console.log('❌ SSE未连接')
return false
}
console.log('SSE连接状态:', this.connection.readyState, this.connection.url)
return this.connection.readyState === 1 // 1 = OPEN
},
// 强制重连
reconnect() {
console.log('🔄 强制重连SSE')
this.disconnect()
setTimeout(() => this.connect(), 1000)
}
}
async function openDeviceManager() {
@@ -378,20 +328,19 @@ async function openDeviceManager() {
}
async function fetchDeviceData() {
const username = (currentUsername.value || getUsernameFromToken()).trim()
if (!username) {
if (!currentUsername.value) {
ElMessage.warning('未获取到用户名,请重新登录')
return
}
try {
deviceLoading.value = true
const [quota, list] = await Promise.all([
deviceApi.getQuota(username),
deviceApi.list(username),
deviceApi.getQuota(currentUsername.value),
deviceApi.list(currentUsername.value),
])
deviceQuota.value = quota || { limit: 0, used: 0 }
const clientId = getClientIdFromToken()
devices.value = (list || []).map(d => ({ ...d, isCurrent: d.deviceId === clientId })) as any
deviceQuota.value = quota || {limit: 0, used: 0}
const clientId = await getClientIdFromToken()
devices.value = (list || []).map(d => ({...d, isCurrent: d.deviceId === clientId})) as any
} catch (e: any) {
ElMessage.error(e?.message || '获取设备列表失败')
} finally {
@@ -401,27 +350,25 @@ async function fetchDeviceData() {
async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) {
try {
await ElMessageBox.confirm('确定要移除该设备吗?', '你确定要移除设备吗?', { confirmButtonText: '确定移除', cancelButtonText: '取消', type: 'warning' })
console.log('正在移除设备:', row.deviceId)
await deviceApi.remove({ deviceId: row.deviceId })
console.log('✅ 移除设备API调用成功')
await ElMessageBox.confirm('确定要移除该设备吗?', '你确定要移除设备吗?', {
confirmButtonText: '确定移除',
cancelButtonText: '取消',
type: 'warning'
})
await deviceApi.remove({deviceId: row.deviceId})
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
deviceQuota.value.used = Math.max(0, (deviceQuota.value.used || 0) - 1)
// 如果是本机设备被移除执行logout
const clientId = getClientIdFromToken()
console.log('检查设备ID:', { removed: row.deviceId, current: clientId })
const clientId = await getClientIdFromToken()
if (row.deviceId === clientId) {
console.log('移除的是本机设备执行logout')
await logout()
}
ElMessage.success('已移除设备')
} catch (e) {
console.error('移除设备失败:', e)
ElMessage.error('移除设备失败: ' + (e?.message || '未知错误'))
} catch (e: any) {
ElMessage.error('移除设备失败: ' + ((e as any)?.message || '未知错误'))
}
}
@@ -429,122 +376,126 @@ onMounted(async () => {
showContent()
await checkAuth()
// 添加全局调试函数
window.debugSSE = {
status: () => SSEManager.checkStatus(),
reconnect: () => SSEManager.reconnect(),
disconnect: () => SSEManager.disconnect(),
getCurrentClientId: () => getClientIdFromToken(),
testLogout: () => logout()
}
console.log('🔧 调试工具已注册到 window.debugSSE')
})
</script>
<template>
<el-config-provider :locale="zhCnLocale">
<div id="app-root" class="root">
<div class="loading-container" id="loading">
<div class="loading-spinner"></div>
</div>
<div class="erp-container">
<div class="sidebar">
<div class="user-avatar">
<img src="/icon/icon.png" alt="logo" />
</div>
<div class="menu-group-title">电商平台</div>
<ul class="menu">
<li
v-for="item in visibleMenus"
:key="item.key"
class="menu-item"
:class="{ active: activeMenu === item.key }"
@click="handleMenuSelect(item.key)"
>
<span class="menu-text"><span class="menu-icon" :data-k="item.key">{{ item.icon }}</span>{{ item.name }}</span>
</li>
</ul>
<div id="app-root" class="root">
<div class="loading-container" id="loading">
<div class="loading-spinner"></div>
</div>
<div class="main-content">
<NavigationBar
:can-go-back="canGoBack"
:can-go-forward="canGoForward"
:active-menu="activeMenu"
@go-back="goBack"
@go-forward="goForward"
@reload="reloadPage"
@user-click="handleUserClick"
@open-device="openDeviceManager" />
<div class="content-body">
<div
class="dashboard-home"
v-if="!isAuthenticated && (activeMenu === 'rakuten' || activeMenu === 'amazon' || activeMenu === 'zebra')">
<div class="icon-container">
<img src="/image/111.png" alt="ERP Logo" class="main-icon" />
</div>
</div>
<ZebraDashboard v-if="activeMenu === 'zebra'" />
<RakutenDashboard v-else-if="activeMenu === 'rakuten'" />
<AmazonDashboard v-else-if="activeMenu === 'amazon'" />
<div v-else class="placeholder">
<div class="placeholder-card">
<div class="placeholder-title">{{ activeMenu.toUpperCase() }} 面板</div>
<div class="placeholder-desc">功能开发中...</div>
</div>
<div class="erp-container">
<div class="sidebar">
<div class="user-avatar">
<img src="/icon/icon.png" alt="logo"/>
</div>
<div class="menu-group-title">电商平台</div>
<ul class="menu">
<li
v-for="item in visibleMenus"
:key="item.key"
class="menu-item"
:class="{ active: activeMenu === item.key }"
@click="handleMenuSelect(item.key)"
>
<span class="menu-text"><span class="menu-icon" :data-k="item.key">{{ item.icon }}</span>{{
item.name
}}</span>
</li>
</ul>
</div>
<!-- 认证组件 -->
<LoginDialog
v-model="showAuthDialog"
@login-success="handleLoginSuccess"
@show-register="showRegisterDialog" />
<div class="main-content">
<NavigationBar
:can-go-back="canGoBack"
:can-go-forward="canGoForward"
:active-menu="activeMenu"
@go-back="goBack"
@go-forward="goForward"
@reload="reloadPage"
@user-click="handleUserClick"
@open-device="openDeviceManager"/>
<div class="content-body">
<div
class="dashboard-home"
v-if="showHomeSplash">
<div class="icon-container">
<img src="/image/111.png" alt="ERP Logo" class="main-icon"/>
</div>
</div>
<keep-alive>
<component v-if="activeDashboard" :is="activeDashboard"/>
</keep-alive>
<div v-if="showPlaceholder" class="placeholder">
<div class="placeholder-card">
<div class="placeholder-title">{{ activeMenu.toUpperCase() }} 面板</div>
<div class="placeholder-desc">功能开发中...</div>
</div>
</div>
</div>
<RegisterDialog
v-model="showRegDialog"
@register-success="handleRegisterSuccess"
@back-to-login="backToLogin" />
<!-- 认证组件 -->
<LoginDialog
v-model="showAuthDialog"
@login-success="handleLoginSuccess"
@show-register="showRegisterDialog"/>
<!-- 设备管理弹框 -->
<el-dialog
:title="`设备管理 (${deviceQuota.used || 0}/${deviceQuota.limit || 0})`"
v-model="showDeviceDialog"
width="560px"
:close-on-click-modal="false">
<div style="margin-bottom: 10px; color:#909399;">当前账号可以授权绑定 {{ deviceQuota.limit }} 台设备</div>
<el-table :data="devices" size="small" :loading="deviceLoading" style="width:100%" stripe>
<el-table-column label="设备名" min-width="180">
<template #default="scope">
<span>{{ scope.row.name || scope.row.deviceId }}</span>
<el-tag v-if="scope.row.isCurrent" size="small" type="success" style="margin-left:6px;">本机</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="scope">
<el-tag :type="scope.row.status==='online' ? 'success' : 'info'" size="small">{{ scope.row.status==='online' ? '已登录' : '已登出' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="最近" min-width="130">
<template #default="scope">
<span>{{ scope.row.lastActiveAt || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button type="text" size="small" style="color:#F56C6C" @click="confirmRemoveDevice(scope.row)">移除设备</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="showDeviceDialog=false">关闭</el-button>
</template>
</el-dialog>
<RegisterDialog
v-model="showRegDialog"
@register-success="handleRegisterSuccess"
@back-to-login="backToLogin"/>
<!-- 设备管理弹框 -->
<el-dialog
v-model="showDeviceDialog"
width="560px"
:close-on-click-modal="false"
align-center
class="device-dialog">
<template #header>
<div class="device-dialog-header">
<img src="/icon/img.png" alt="devices" class="device-illustration"/>
<div class="device-title">设备管理 <span class="device-count">({{ deviceQuota.used || 0 }}/{{ deviceQuota.limit || 0 }})</span></div>
<div class="device-subtitle">当前账号可以授权绑定 {{ deviceQuota.limit }} 台设备</div>
</div>
</template>
<el-table :data="devices" size="small" :loading="deviceLoading" style="width:100%" stripe>
<el-table-column label="设备名" min-width="180">
<template #default="scope">
<span>{{ scope.row.name || scope.row.deviceId }}</span>
<el-tag v-if="scope.row.isCurrent" size="small" type="success" style="margin-left:6px;">本机</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="scope">
<el-tag :type="scope.row.status==='online' ? 'success' : 'info'" size="small">
{{ scope.row.status === 'online' ? '已登录' : '已登出' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="最近" min-width="130">
<template #default="scope">
<span>{{ scope.row.lastActiveAt || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button type="text" size="small" style="color:#F56C6C" @click="confirmRemoveDevice(scope.row)">
移除设备
</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="showDeviceDialog=false">关闭</el-button>
</template>
</el-dialog>
</div>
</div>
</div>
</div>
</el-config-provider>
</template>
@@ -573,6 +524,7 @@ onMounted(async () => {
z-index: 9999;
transition: opacity 0.1s ease;
}
.loading-spinner {
width: 50px;
height: 50px;
@@ -581,9 +533,14 @@ onMounted(async () => {
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.erp-container {
@@ -592,16 +549,28 @@ onMounted(async () => {
}
.sidebar {
width: 220px;
min-width: 220px;
width: 180px;
min-width: 180px;
flex-shrink: 0;
background: #ffffff;
border-right: 1px solid #e8eaec;
padding: 16px 12px;
box-sizing: border-box;
}
.platform-icons { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
.picon { width: 28px; height: 28px; object-fit: contain; }
.platform-icons {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 12px;
}
.picon {
width: 28px;
height: 28px;
object-fit: contain;
}
.user-avatar {
display: flex;
align-items: center;
@@ -610,6 +579,7 @@ onMounted(async () => {
border-bottom: 1px solid #e8eaec;
margin: 0 0 12px 0;
}
.user-avatar img {
width: 50px;
height: 50px;
@@ -617,17 +587,20 @@ onMounted(async () => {
object-fit: contain;
background: #ffffff;
}
.menu-group-title {
font-size: 12px;
color: #909399;
margin: 8px 6px 10px;
text-align: left; /* “电商平台”四个字靠左 */
}
.menu {
list-style: none;
padding: 0;
margin: 0;
}
.menu-item {
display: flex;
align-items: center;
@@ -637,22 +610,53 @@ onMounted(async () => {
color: #333333;
margin-bottom: 4px;
}
.menu-item:hover {
background: #f5f7fa;
}
.menu-item.active {
background: #ecf5ff !important;
color: #409EFF !important;
}
.menu-text {
font-size: 14px;
}
.menu-text { display: inline-flex; align-items: center; gap: 6px; }
.menu-icon { display: inline-flex; width: 18px; height: 18px; border-radius: 4px; align-items: center; justify-content: center; font-size: 12px; color: #fff; }
.menu-icon[data-k="rakuten"] { background: #BF0000; }
.menu-icon[data-k="amazon"] { background: #FF9900; color: #1A1A1A; }
.menu-icon[data-k="zebra"] { background: #34495e; }
.menu-icon[data-k="shopee"] { background: #EE4D2D; }
.menu-text {
display: inline-flex;
align-items: center;
gap: 6px;
}
.menu-icon {
display: inline-flex;
width: 18px;
height: 18px;
border-radius: 4px;
align-items: center;
justify-content: center;
font-size: 12px;
color: #fff;
}
.menu-icon[data-k="rakuten"] {
background: #BF0000;
}
.menu-icon[data-k="amazon"] {
background: #FF9900;
color: #1A1A1A;
}
.menu-icon[data-k="zebra"] {
background: #34495e;
}
.menu-icon[data-k="shopee"] {
background: #EE4D2D;
}
.main-content {
flex: 1;
@@ -671,6 +675,7 @@ onMounted(async () => {
min-height: 0;
overflow: hidden;
}
.dashboard-home {
position: absolute;
inset: 0;
@@ -680,7 +685,12 @@ onMounted(async () => {
background: #ffffff;
z-index: 100;
}
.icon-container { display: flex; justify-content: center; }
.icon-container {
display: flex;
justify-content: center;
}
.main-icon {
width: 400px;
height: 400px;
@@ -696,6 +706,7 @@ onMounted(async () => {
justify-content: center;
background: #fff;
}
.placeholder-card {
background: #ffffff;
border: 1px solid #e8eaec;
@@ -704,6 +715,40 @@ onMounted(async () => {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
color: #2c3e50;
}
.placeholder-title { font-size: 18px; font-weight: 600; margin-bottom: 8px; }
.placeholder-desc { font-size: 13px; color: #606266; }
.placeholder-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
}
.placeholder-desc {
font-size: 13px;
color: #606266;
}
.device-dialog-header {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0 4px 0;
margin-left: 40px;
}
.device-dialog :deep(.el-dialog__header) {
text-align: center;
}
.device-dialog :deep(.el-dialog__body) { padding-top: 0; }
.device-illustration {
width: 180px;
height: auto;
object-fit: contain;
margin-bottom: 8px;
}
.device-title {
font-size: 18px;
font-weight: 600;
color: #303133;
margin-bottom: 6px;
}
.device-count { color: #909399; font-weight: 500; }
.device-subtitle { font-size: 12px; color: #909399; }
</style>