前端重构 Vite

This commit is contained in:
2026-05-29 17:30:26 +08:00
parent cbd5d98993
commit 5bb9bc84ea
44 changed files with 7289 additions and 8769 deletions
+252
View File
@@ -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>