Files
geMoldInsight/frontend/src/App.vue
T
2026-06-10 11:21:17 +08:00

253 lines
9.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>