827 lines
20 KiB
Vue
827 lines
20 KiB
Vue
<script setup lang="ts">
|
||
import {onMounted, ref, computed, defineAsyncComponent, type Component, onUnmounted} from 'vue'
|
||
import {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'
|
||
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 UpdateDialog = defineAsyncComponent(() => import('./components/common/UpdateDialog.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'])
|
||
const currentHistoryIndex = ref(0)
|
||
|
||
// 应用状态
|
||
const activeMenu = ref('rakuten')
|
||
const isAuthenticated = ref(false)
|
||
const showAuthDialog = ref(false)
|
||
const showRegDialog = ref(false)
|
||
const zhCnLocale = zhCn
|
||
const currentUsername = ref('')
|
||
const showDeviceDialog = ref(false)
|
||
const deviceLoading = ref(false)
|
||
const devices = ref<DeviceItem[]>([])
|
||
const deviceQuota = ref<DeviceQuota>({limit: 0, used: 0})
|
||
const userPermissions = ref<string>('')
|
||
|
||
// 更新对话框状态
|
||
const showUpdateDialog = ref(false)
|
||
|
||
// 菜单配置 - 复刻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'},
|
||
]
|
||
|
||
// 权限检查 - 复刻ERP客户端逻辑
|
||
function hasPermission(module: string) {
|
||
// 默认显示的基础菜单(未登录时也显示)
|
||
const defaultModules = ['rakuten', 'amazon', 'zebra']
|
||
|
||
if (!isAuthenticated.value) {
|
||
return defaultModules.includes(module)
|
||
}
|
||
|
||
const permissions = userPermissions.value
|
||
if (!permissions) {
|
||
return defaultModules.includes(module) // 没有权限信息时显示默认菜单
|
||
}
|
||
|
||
// 简化权限检查:直接检查模块名是否在权限字符串中
|
||
return permissions.includes(module)
|
||
}
|
||
|
||
const visibleMenus = computed(() => menuConfig.filter(item => hasPermission(item.key)))
|
||
|
||
const canGoBack = computed(() => currentHistoryIndex.value > 0)
|
||
const canGoForward = computed(() => currentHistoryIndex.value < navigationHistory.value.length - 1)
|
||
|
||
function showContent() {
|
||
const loading = document.getElementById('loading')
|
||
if (loading) {
|
||
loading.style.opacity = '0'
|
||
setTimeout(() => {
|
||
loading.style.display = 'none'
|
||
}, 100)
|
||
}
|
||
const app = document.getElementById('app-root')
|
||
if (app) app.style.opacity = '1'
|
||
}
|
||
|
||
function addToHistory(menu: string) {
|
||
if (navigationHistory.value[currentHistoryIndex.value] !== menu) {
|
||
navigationHistory.value = navigationHistory.value.slice(0, currentHistoryIndex.value + 1)
|
||
navigationHistory.value.push(menu)
|
||
currentHistoryIndex.value = navigationHistory.value.length - 1
|
||
}
|
||
}
|
||
|
||
function goBack() {
|
||
if (canGoBack.value) {
|
||
currentHistoryIndex.value--
|
||
activeMenu.value = navigationHistory.value[currentHistoryIndex.value]
|
||
}
|
||
}
|
||
|
||
function goForward() {
|
||
if (canGoForward.value) {
|
||
currentHistoryIndex.value++
|
||
activeMenu.value = navigationHistory.value[currentHistoryIndex.value]
|
||
}
|
||
}
|
||
|
||
function reloadPage() {
|
||
window.location.reload()
|
||
}
|
||
|
||
function handleMenuSelect(key: string) {
|
||
// 检查是否需要认证
|
||
const authRequiredMenus = ['rakuten', 'amazon', 'zebra', 'shopee']
|
||
if (!isAuthenticated.value && authRequiredMenus.includes(key)) {
|
||
showAuthDialog.value = true
|
||
return
|
||
}
|
||
|
||
activeMenu.value = key
|
||
addToHistory(key)
|
||
}
|
||
|
||
async function handleLoginSuccess(data: { token: string; permissions?: string }) {
|
||
isAuthenticated.value = true
|
||
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})
|
||
|
||
// 建立SSE连接
|
||
SSEManager.connect()
|
||
} catch (e: any) {
|
||
// 设备注册失败时回滚登录状态
|
||
isAuthenticated.value = false
|
||
showAuthDialog.value = true
|
||
await authApi.deleteTokenCache()
|
||
ElMessage({
|
||
message: e?.message || '设备注册失败,请重试',
|
||
type: 'error'
|
||
})
|
||
}
|
||
}
|
||
|
||
async function logout() {
|
||
// 主动设置设备离线
|
||
try {
|
||
const deviceId = await getClientIdFromToken()
|
||
if (deviceId) {
|
||
await deviceApi.offline({ deviceId })
|
||
}
|
||
} catch (error) {
|
||
console.warn('离线通知失败:', error)
|
||
}
|
||
|
||
const token = await authApi.getToken()
|
||
if (token) {
|
||
await authApi.logout(token)
|
||
}
|
||
|
||
await authApi.deleteTokenCache()
|
||
// 清理前端状态
|
||
isAuthenticated.value = false
|
||
currentUsername.value = ''
|
||
userPermissions.value = ''
|
||
showAuthDialog.value = true
|
||
showDeviceDialog.value = false
|
||
|
||
// 关闭SSE连接
|
||
SSEManager.disconnect()
|
||
}
|
||
|
||
async function handleUserClick() {
|
||
if (!isAuthenticated.value) {
|
||
showAuthDialog.value = true
|
||
return
|
||
}
|
||
try {
|
||
await ElMessageBox.confirm('确认退出登录?', '提示', {
|
||
type: 'warning',
|
||
confirmButtonText: '退出',
|
||
cancelButtonText: '取消'
|
||
})
|
||
await logout()
|
||
ElMessage({
|
||
message: '已退出登录',
|
||
type: 'success'
|
||
})
|
||
} catch {
|
||
}
|
||
}
|
||
|
||
function showRegisterDialog() {
|
||
showAuthDialog.value = false
|
||
showRegDialog.value = true
|
||
}
|
||
|
||
function handleRegisterSuccess() {
|
||
showRegDialog.value = false
|
||
showAuthDialog.value = true
|
||
}
|
||
|
||
function backToLogin() {
|
||
showRegDialog.value = false
|
||
showAuthDialog.value = true
|
||
}
|
||
|
||
async function checkAuth() {
|
||
const authRequiredMenus = ['rakuten', 'amazon', 'zebra', 'shopee']
|
||
|
||
try {
|
||
await authApi.sessionBootstrap().catch(() => undefined)
|
||
const token = await authApi.getToken()
|
||
if (token) {
|
||
const response = await authApi.verifyToken(token)
|
||
if (response?.success) {
|
||
isAuthenticated.value = true
|
||
currentUsername.value = getUsernameFromToken(token) || ''
|
||
SSEManager.connect()
|
||
return
|
||
}
|
||
await authApi.deleteTokenCache()
|
||
}
|
||
} catch {
|
||
// 忽略
|
||
}
|
||
|
||
if (authRequiredMenus.includes(activeMenu.value)) {
|
||
showAuthDialog.value = true
|
||
}
|
||
}
|
||
|
||
async function getClientIdFromToken(token?: string) {
|
||
try {
|
||
let t = token
|
||
if (!t) {
|
||
t = await authApi.getToken()
|
||
}
|
||
if (!t) return ''
|
||
|
||
const payload = JSON.parse(atob(t.split('.')[1] || ''))
|
||
return payload.clientId || ''
|
||
} catch {
|
||
return ''
|
||
}
|
||
}
|
||
|
||
function getUsernameFromToken(token: string) {
|
||
try {
|
||
const payload = JSON.parse(atob(token.split('.')[1] || ''))
|
||
return payload.username || ''
|
||
} catch {
|
||
return ''
|
||
}
|
||
}
|
||
|
||
// SSE管理器
|
||
const SSEManager = {
|
||
connection: null as EventSource | null,
|
||
async connect() {
|
||
if (this.connection) 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')
|
||
if (resp.ok) {
|
||
const config = await resp.json()
|
||
sseUrl = config.sseUrl || sseUrl
|
||
}
|
||
} catch {
|
||
}
|
||
|
||
const src = new EventSource(`${sseUrl}?clientId=${clientId}&token=${token}`)
|
||
this.connection = src
|
||
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 {
|
||
// 处理ping心跳
|
||
if (e.type === 'ping') {
|
||
return // ping消息自动保持连接,无需处理
|
||
}
|
||
|
||
console.log('SSE消息:', e.data)
|
||
const payload = JSON.parse(e.data)
|
||
switch (payload.type) {
|
||
case 'ready':
|
||
console.log('SSE连接已就绪')
|
||
break
|
||
case 'DEVICE_REMOVED':
|
||
logout()
|
||
ElMessage({
|
||
message: '您的设备已被移除,请重新登录',
|
||
type: 'warning'
|
||
})
|
||
break
|
||
case 'FORCE_LOGOUT':
|
||
logout()
|
||
ElMessage({
|
||
message: '会话已失效,请重新登录',
|
||
type: 'warning'
|
||
})
|
||
break
|
||
case 'PERMISSIONS_UPDATED':
|
||
checkAuth()
|
||
break
|
||
}
|
||
} catch (err) {
|
||
console.error('SSE消息处理失败:', err, '原始数据:', e.data)
|
||
}
|
||
},
|
||
|
||
handleError() {
|
||
this.disconnect()
|
||
setTimeout(() => this.connect(), 3000)
|
||
},
|
||
|
||
disconnect() {
|
||
if (this.connection) {
|
||
try {
|
||
this.connection.close()
|
||
} catch {
|
||
}
|
||
this.connection = null
|
||
}
|
||
},
|
||
}
|
||
|
||
async function openDeviceManager() {
|
||
if (!isAuthenticated.value) {
|
||
showAuthDialog.value = true
|
||
return
|
||
}
|
||
showDeviceDialog.value = true
|
||
await fetchDeviceData()
|
||
}
|
||
|
||
async function fetchDeviceData() {
|
||
if (!currentUsername.value) {
|
||
ElMessage({
|
||
message: '未获取到用户名,请重新登录',
|
||
type: 'warning'
|
||
})
|
||
return
|
||
}
|
||
try {
|
||
deviceLoading.value = true
|
||
const [quota, list] = await Promise.all([
|
||
deviceApi.getQuota(currentUsername.value),
|
||
deviceApi.list(currentUsername.value),
|
||
])
|
||
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({
|
||
message: e?.message || '获取设备列表失败',
|
||
type: 'error'
|
||
})
|
||
} finally {
|
||
deviceLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) {
|
||
try {
|
||
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 = await getClientIdFromToken()
|
||
if (row.deviceId === clientId) {
|
||
await logout()
|
||
}
|
||
|
||
ElMessage({
|
||
message: '已移除设备',
|
||
type: 'success'
|
||
})
|
||
} catch (e: any) {
|
||
ElMessage({
|
||
message: '移除设备失败: ' + ((e as any)?.message || '未知错误'),
|
||
type: 'error'
|
||
})
|
||
}
|
||
}
|
||
|
||
|
||
onMounted(async () => {
|
||
showContent()
|
||
await checkAuth()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
SSEManager.disconnect()
|
||
})
|
||
|
||
|
||
|
||
</script>
|
||
|
||
<template>
|
||
|
||
|
||
<div>
|
||
<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>
|
||
|
||
<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 v-if="activeDashboard">
|
||
<component :is="activeDashboard" :key="activeMenu"/>
|
||
</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>
|
||
|
||
<!-- 认证组件 -->
|
||
<LoginDialog
|
||
v-model="showAuthDialog"
|
||
@login-success="handleLoginSuccess"
|
||
@show-register="showRegisterDialog"/>
|
||
|
||
<RegisterDialog
|
||
v-model="showRegDialog"
|
||
@register-success="handleRegisterSuccess"
|
||
@back-to-login="backToLogin"/>
|
||
|
||
<!-- 更新对话框 -->
|
||
<UpdateDialog v-model="showUpdateDialog" />
|
||
|
||
<!-- 设备管理弹框 -->
|
||
<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>
|
||
</template>
|
||
|
||
<style scoped>
|
||
|
||
.root {
|
||
position: fixed;
|
||
inset: 0;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background-color: #f5f5f5;
|
||
opacity: 0;
|
||
transition: opacity 0.1s ease;
|
||
}
|
||
|
||
.loading-container {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
height: 100vh;
|
||
width: 100%;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
background-color: #f5f5f5;
|
||
z-index: 9999;
|
||
transition: opacity 0.1s ease;
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 50px;
|
||
height: 50px;
|
||
border: 5px solid #e6e6e6;
|
||
border-top: 5px solid #409EFF;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.erp-container {
|
||
display: flex;
|
||
height: 100vh;
|
||
}
|
||
|
||
.sidebar {
|
||
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;
|
||
}
|
||
|
||
.user-avatar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid #e8eaec;
|
||
margin: 0 0 12px 0;
|
||
}
|
||
|
||
.user-avatar img {
|
||
width: 50px;
|
||
height: 50px;
|
||
border-radius: 50%;
|
||
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;
|
||
padding: 12px 16px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
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;
|
||
}
|
||
|
||
.main-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* 导航栏和认证相关样式已移至对应组件 */
|
||
|
||
.content-body {
|
||
position: relative;
|
||
flex: 1;
|
||
background: #fff;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.dashboard-home {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #ffffff;
|
||
z-index: 100;
|
||
}
|
||
|
||
.icon-container {
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.main-icon {
|
||
width: 400px;
|
||
height: 400px;
|
||
border-radius: 20px;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.placeholder {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #fff;
|
||
}
|
||
|
||
.placeholder-card {
|
||
background: #ffffff;
|
||
border: 1px solid #e8eaec;
|
||
border-radius: 12px;
|
||
padding: 24px 28px;
|
||
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;
|
||
}
|
||
.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; }
|
||
|
||
/* 浮动版本信息 */
|
||
.version-info {
|
||
position: fixed;
|
||
right: 10px;
|
||
bottom: 10px;
|
||
background: rgba(255,255,255,0.9);
|
||
padding: 5px 10px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
z-index: 1000;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
</style> |