前端重构 Vite
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
<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>
|
||||
<button class="btn btn-secondary" @click="handleLogout">退出</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>
|
||||
Reference in New Issue
Block a user