Initial commit
This commit is contained in:
535
electron-vue-template/src/renderer/App.vue
Normal file
535
electron-vue-template/src/renderer/App.vue
Normal file
@@ -0,0 +1,535 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, computed, defineAsyncComponent } 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'
|
||||
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 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>('')
|
||||
|
||||
// 菜单配置 - 复刻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; user: any }) {
|
||||
isAuthenticated.value = true
|
||||
showAuthDialog.value = false
|
||||
|
||||
try {
|
||||
currentUsername.value = data?.user?.username || currentUsername.value
|
||||
userPermissions.value = data?.permissions || data?.user?.permissions || ''
|
||||
} catch {}
|
||||
|
||||
// 登录成功后自动注册设备 - 简化版
|
||||
try {
|
||||
const username = data?.user?.username || currentUsername.value
|
||||
if (username) {
|
||||
await deviceApi.register({ username })
|
||||
}
|
||||
} catch (e) {
|
||||
// 设备注册失败不影响登录流程,静默处理
|
||||
console.warn('设备注册失败:', e)
|
||||
}
|
||||
}
|
||||
async function handleUserClick() {
|
||||
if (!isAuthenticated.value) {
|
||||
showAuthDialog.value = true
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm('确认退出登录?', '提示', { type: 'warning', confirmButtonText: '退出', cancelButtonText: '取消' })
|
||||
const token = localStorage.getItem('token') || ''
|
||||
try { await authApi.logout(token) } catch {}
|
||||
try { localStorage.removeItem('token') } catch {}
|
||||
isAuthenticated.value = false
|
||||
currentUsername.value = ''
|
||||
userPermissions.value = ''
|
||||
showAuthDialog.value = true
|
||||
showDeviceDialog.value = false
|
||||
ElMessage.success('已退出登录')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
||||
function handleLoginCancel() {
|
||||
showAuthDialog.value = false
|
||||
}
|
||||
|
||||
function showRegisterDialog() {
|
||||
showAuthDialog.value = false
|
||||
showRegDialog.value = true
|
||||
}
|
||||
|
||||
function handleRegisterSuccess() {
|
||||
showRegDialog.value = false
|
||||
showAuthDialog.value = true
|
||||
}
|
||||
|
||||
function backToLogin() {
|
||||
showRegDialog.value = false
|
||||
showAuthDialog.value = true
|
||||
}
|
||||
|
||||
// 检查认证状态 - 复刻ERP客户端逻辑
|
||||
async function checkAuth() {
|
||||
const token = localStorage.getItem('token')
|
||||
const authRequiredMenus = ['rakuten', 'amazon', 'zebra', 'shopee']
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
const response = await authApi.verifyToken(token)
|
||||
if (response.success) {
|
||||
isAuthenticated.value = true
|
||||
if (!currentUsername.value) {
|
||||
const u = getUsernameFromToken(token)
|
||||
if (u) currentUsername.value = u
|
||||
}
|
||||
userPermissions.value = response.permissions || ''
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要显示登录弹框
|
||||
if (!isAuthenticated.value && authRequiredMenus.includes(activeMenu.value)) {
|
||||
showAuthDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function getClientIdFromToken(token?: string) {
|
||||
try {
|
||||
const t = token || localStorage.getItem('token') || ''
|
||||
const payload = JSON.parse(atob(t.split('.')[1] || ''))
|
||||
return payload.clientId || ''
|
||||
} catch { return '' }
|
||||
}
|
||||
|
||||
function getUsernameFromToken(token?: string) {
|
||||
try {
|
||||
const t = token || localStorage.getItem('token') || ''
|
||||
const payload = JSON.parse(atob(t.split('.')[1] || ''))
|
||||
return payload.username || ''
|
||||
} catch { return '' }
|
||||
}
|
||||
|
||||
async function openDeviceManager() {
|
||||
if (!isAuthenticated.value) {
|
||||
showAuthDialog.value = true
|
||||
return
|
||||
}
|
||||
showDeviceDialog.value = true
|
||||
await fetchDeviceData()
|
||||
}
|
||||
|
||||
async function fetchDeviceData() {
|
||||
const username = (currentUsername.value || getUsernameFromToken()).trim()
|
||||
if (!username) {
|
||||
ElMessage.warning('未获取到用户名,请重新登录')
|
||||
return
|
||||
}
|
||||
try {
|
||||
deviceLoading.value = true
|
||||
const [quota, list] = await Promise.all([
|
||||
deviceApi.getQuota(username),
|
||||
deviceApi.list(username),
|
||||
])
|
||||
deviceQuota.value = quota || { limit: 0, used: 0 }
|
||||
const clientId = getClientIdFromToken()
|
||||
devices.value = (list || []).map(d => ({ ...d, isCurrent: d.deviceId === clientId })) as any
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '获取设备列表失败')
|
||||
} 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)
|
||||
if (row.isCurrent) {
|
||||
// 当前设备被移除,清理登录状态
|
||||
isAuthenticated.value = false
|
||||
showAuthDialog.value = true
|
||||
try { localStorage.removeItem('token') } catch {}
|
||||
}
|
||||
ElMessage.success('已移除设备')
|
||||
} catch (e) {
|
||||
/* 用户取消或失败 */
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
showContent()
|
||||
await checkAuth()
|
||||
})
|
||||
</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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- 认证组件 -->
|
||||
<LoginDialog
|
||||
v-model="showAuthDialog"
|
||||
@login-success="handleLoginSuccess"
|
||||
@show-register="showRegisterDialog" />
|
||||
|
||||
<RegisterDialog
|
||||
v-model="showRegDialog"
|
||||
@register-success="handleRegisterSuccess"
|
||||
@back-to-login="backToLogin" />
|
||||
|
||||
<!-- 设备管理弹框 -->
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-config-provider>
|
||||
</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: 220px;
|
||||
min-width: 220px;
|
||||
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; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user