253 lines
9.0 KiB
Vue
253 lines
9.0 KiB
Vue
<template>
|
||
<div class="app-container">
|
||
<div class="notification-container" v-if="store.notifications.length > 0">
|
||
<TransitionGroup name="notification">
|
||
<div
|
||
v-for="n in store.notifications"
|
||
:key="n.id"
|
||
:class="['notification', 'notification-' + n.type]"
|
||
>
|
||
<div class="notification-icon">
|
||
{{ n.type === 'success' ? '✓' : n.type === 'error' ? '✕' : n.type === 'warning' ? '!' : 'i' }}
|
||
</div>
|
||
<div class="notification-content">
|
||
<div class="notification-message">{{ n.message }}</div>
|
||
</div>
|
||
<button class="notification-close" @click="dismissNotification(n.id)">×</button>
|
||
</div>
|
||
</TransitionGroup>
|
||
</div>
|
||
|
||
<header class="app-header">
|
||
<div class="header-content">
|
||
<div class="logo" @click="router.push('/')">
|
||
<div class="logo-icon">G</div>
|
||
<div>
|
||
<div class="logo-text">Gemold</div>
|
||
<div class="logo-subtitle">模具制造管理系统</div>
|
||
</div>
|
||
</div>
|
||
|
||
<nav class="nav-menu" v-if="store.user">
|
||
<router-link
|
||
v-for="item in menuItems"
|
||
:key="item.path"
|
||
:to="item.path"
|
||
:class="['nav-item', { active: isActive(item.path) }]"
|
||
>
|
||
{{ item.label }}
|
||
</router-link>
|
||
</nav>
|
||
|
||
<div class="user-section">
|
||
<template v-if="store.user">
|
||
<div class="user-info">
|
||
<div class="user-avatar">{{ (store.user.full_name || store.user.username).charAt(0).toUpperCase() }}</div>
|
||
<span class="user-name">{{ store.user.full_name || store.user.username }}</span>
|
||
</div>
|
||
<t-button theme="default" variant="outline" size="small" @click="handleLogout">退出</t-button>
|
||
</template>
|
||
<template v-else>
|
||
<router-link to="/login" class="btn btn-primary">登录</router-link>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="main-content">
|
||
<router-view v-slot="{ Component }">
|
||
<transition name="fade" mode="out-in">
|
||
<component :is="Component" />
|
||
</transition>
|
||
</router-view>
|
||
</main>
|
||
|
||
<footer class="app-footer">
|
||
<div class="footer-content">
|
||
<a href="https://beian.miit.gov.cn/" target="_blank" class="beian-link">粤ICP备2025386132号-1</a>
|
||
</div>
|
||
</footer>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onMounted } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { useAppStore } from '@/stores/app'
|
||
import { apiRequest } from '@/shared/api'
|
||
import { clearAuth, initAuth } from '@/shared/auth'
|
||
import { addNotification } from '@/shared/notification'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const store = useAppStore()
|
||
|
||
const menuItems = computed(() => {
|
||
const items = [
|
||
{ path: '/', label: '首页', icon: '⌂' },
|
||
{ path: '/inventory', label: '进销存', icon: '⊞' },
|
||
{ path: '/moldinsight', label: 'MoldInsight', icon: '◈' },
|
||
]
|
||
if (store.user?.is_superuser) {
|
||
items.push({ path: '/users', label: '用户管理', icon: '👤' })
|
||
}
|
||
return items
|
||
})
|
||
|
||
const isActive = (path: string) => {
|
||
if (path === '/') return route.path === '/'
|
||
return route.path.startsWith(path)
|
||
}
|
||
|
||
const handleLogout = async () => {
|
||
try {
|
||
await apiRequest('/api/auth/logout', { method: 'POST' })
|
||
} catch { /* ignore */ }
|
||
clearAuth()
|
||
addNotification('已退出登录', 'success')
|
||
router.push('/login')
|
||
}
|
||
|
||
const dismissNotification = (id: number) => {
|
||
const notification = store.notifications.find(n => n.id === id)
|
||
if (notification) {
|
||
notification.visible = false
|
||
setTimeout(() => {
|
||
store.dismissNotification(id)
|
||
}, 300)
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
initAuth()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.app-container {
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: var(--bg-secondary);
|
||
padding-bottom: 48px;
|
||
}
|
||
|
||
.app-header {
|
||
background: var(--bg-primary);
|
||
border-bottom: 1px solid var(--border-light);
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
}
|
||
|
||
.header-content {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
padding: 0 var(--space-6);
|
||
height: 64px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.logo { display: flex; align-items: center; gap: var(--space-3); cursor: pointer; }
|
||
.logo-icon {
|
||
width: 36px; height: 36px;
|
||
background: var(--primary-500);
|
||
border-radius: var(--radius-lg);
|
||
display: flex; align-items: center; justify-content: center;
|
||
color: white; font-size: var(--text-lg); font-weight: var(--font-bold);
|
||
}
|
||
.logo-text { font-size: var(--text-xl); font-weight: var(--font-semibold); color: var(--text-primary); }
|
||
.logo-subtitle { font-size: var(--text-xs); color: var(--text-tertiary); }
|
||
|
||
.nav-menu { display: flex; align-items: center; gap: var(--space-1); }
|
||
.nav-item {
|
||
padding: var(--space-2) var(--space-4); border-radius: var(--radius-md);
|
||
color: var(--text-secondary); text-decoration: none;
|
||
font-size: var(--text-sm); font-weight: var(--font-medium);
|
||
transition: all var(--duration-fast) var(--ease-default);
|
||
}
|
||
.nav-item:hover { background: var(--bg-tertiary); color: var(--text-primary); }
|
||
.nav-item.active { background: var(--primary-50); color: var(--primary-600); }
|
||
.nav-item:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.22); }
|
||
|
||
.user-section { display: flex; align-items: center; gap: var(--space-4); }
|
||
.user-info { display: flex; align-items: center; gap: var(--space-3); }
|
||
.user-avatar {
|
||
width: 32px; height: 32px;
|
||
background: var(--primary-100); color: var(--primary-600);
|
||
border-radius: var(--radius-full);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: var(--text-sm); font-weight: var(--font-semibold);
|
||
}
|
||
.user-name { font-size: var(--text-sm); font-weight: var(--font-medium); color: var(--text-primary); }
|
||
|
||
.main-content {
|
||
flex: 1; width: 100%; margin: 0 auto;
|
||
padding: var(--space-8) var(--space-6);
|
||
padding-bottom: var(--space-8);
|
||
}
|
||
@media (max-width: 768px) { .main-content { padding: var(--space-6) var(--space-4); } }
|
||
@media (min-width: 1400px) { .main-content { max-width: 95vw; padding: var(--space-8) var(--space-8); } }
|
||
@media (min-width: 1600px) { .main-content { max-width: 90vw; } }
|
||
@media (min-width: 1920px) { .main-content { max-width: 85vw; } }
|
||
|
||
.app-footer {
|
||
position: fixed; bottom: 0; left: 0; right: 0;
|
||
background: var(--bg-primary); border-top: 1px solid var(--border-light);
|
||
padding: var(--space-3) 0; z-index: 99;
|
||
}
|
||
.footer-content { max-width: 1400px; margin: 0 auto; padding: 0 var(--space-6); text-align: center; }
|
||
.beian-link { font-size: var(--text-xs); color: var(--text-tertiary); text-decoration: none; transition: color var(--duration-fast) var(--ease-default); }
|
||
.beian-link:hover { color: var(--primary-600); text-decoration: underline; }
|
||
|
||
.notification-container {
|
||
position: fixed; top: var(--space-6); right: var(--space-6); z-index: 1000;
|
||
display: flex; flex-direction: column; gap: var(--space-3);
|
||
}
|
||
.notification {
|
||
display: flex; align-items: flex-start; gap: var(--space-3);
|
||
padding: var(--space-4); background: var(--bg-primary);
|
||
border: 1px solid var(--border-light); border-radius: var(--radius-lg);
|
||
box-shadow: var(--shadow-lg); min-width: 320px; max-width: 400px;
|
||
}
|
||
.notification-success { border-left: 3px solid var(--success); }
|
||
.notification-error { border-left: 3px solid var(--error); }
|
||
.notification-warning { border-left: 3px solid var(--warning); }
|
||
.notification-icon { width: 20px; height: 20px; flex-shrink: 0; }
|
||
.notification-content { flex: 1; }
|
||
.notification-message { font-size: var(--text-sm); color: var(--text-primary); }
|
||
.notification-close {
|
||
width: 20px; height: 20px; border: none; background: transparent;
|
||
color: var(--text-muted); cursor: pointer; padding: 0;
|
||
}
|
||
|
||
.notification-enter-active, .notification-leave-active { transition: all var(--duration-normal) var(--ease-default); }
|
||
.notification-enter-from, .notification-leave-to { opacity: 0; transform: translateX(100%); }
|
||
.fade-enter-active, .fade-leave-active { transition: opacity var(--duration-normal) var(--ease-default); }
|
||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||
|
||
.loading-overlay {
|
||
position: fixed; inset: 0; background: var(--bg-overlay);
|
||
display: flex; align-items: center; justify-content: center; z-index: 1000;
|
||
}
|
||
.loading-content { background: var(--bg-primary); padding: var(--space-8); border-radius: var(--radius-xl); text-align: center; }
|
||
.loading-spinner {
|
||
width: 24px; height: 24px; border: 2px solid var(--border-default);
|
||
border-top-color: var(--primary-500); border-radius: var(--radius-full);
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
@media (max-width: 768px) {
|
||
.header-content { padding: 0 var(--space-4); height: 56px; }
|
||
.logo-subtitle { display: none; }
|
||
.nav-menu { gap: 0; }
|
||
.nav-item { padding: var(--space-2) var(--space-3); font-size: var(--text-xs); }
|
||
.user-name { display: none; }
|
||
.footer-content { padding: 0 var(--space-4); }
|
||
.beian-link { font-size: 11px; }
|
||
}
|
||
</style>
|