前端重构 Vite
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,5 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
@@ -0,0 +1,76 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Gemold - 模具制造管理系统</title>
|
||||
<script>
|
||||
(function () {
|
||||
var params = new URLSearchParams(location.search);
|
||||
var uiParam = params.get('ui');
|
||||
var themeParam = params.get('theme');
|
||||
|
||||
var storedUi = null;
|
||||
try { storedUi = localStorage.getItem('gemold_ui_version'); } catch (e) {}
|
||||
|
||||
var storedPercent = null;
|
||||
try { storedPercent = localStorage.getItem('gemold_ui_rollout_percent'); } catch (e) {}
|
||||
var percent = storedPercent ? Number(storedPercent) : 0;
|
||||
if (!Number.isFinite(percent) || percent < 0) percent = 0;
|
||||
if (percent > 100) percent = 100;
|
||||
|
||||
var uid = null;
|
||||
try { uid = localStorage.getItem('gemold_uid'); } catch (e) {}
|
||||
if (!uid) {
|
||||
uid = 'u_' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
||||
try { localStorage.setItem('gemold_uid', uid); } catch (e) {}
|
||||
}
|
||||
|
||||
function hash32(str) {
|
||||
var h = 2166136261;
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
h ^= str.charCodeAt(i);
|
||||
h = Math.imul(h, 16777619);
|
||||
}
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
function chooseUiVersion() {
|
||||
if (uiParam === 'v1' || uiParam === 'v2') return uiParam;
|
||||
if (storedUi === 'v1' || storedUi === 'v2') return storedUi;
|
||||
if (percent <= 0) return 'v1';
|
||||
var bucket = hash32(uid) % 100;
|
||||
return bucket < percent ? 'v2' : 'v1';
|
||||
}
|
||||
|
||||
function chooseTheme() {
|
||||
var storedTheme = null;
|
||||
try { storedTheme = localStorage.getItem('gemold_theme'); } catch (e) {}
|
||||
if (themeParam === 'light' || themeParam === 'dark') return themeParam;
|
||||
if (storedTheme === 'light' || storedTheme === 'dark') return storedTheme;
|
||||
return 'light';
|
||||
}
|
||||
|
||||
var ui = chooseUiVersion();
|
||||
var theme = chooseTheme();
|
||||
document.documentElement.dataset.ui = ui;
|
||||
document.documentElement.dataset.theme = theme;
|
||||
try {
|
||||
if (uiParam === 'v1' || uiParam === 'v2') localStorage.setItem('gemold_ui_version', uiParam);
|
||||
if (themeParam === 'light' || themeParam === 'dark') localStorage.setItem('gemold_theme', themeParam);
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="loading-overlay">
|
||||
<div class="loading-content">
|
||||
<div class="loading-spinner"></div>
|
||||
<p style="margin-top: 16px; color: var(--text-secondary);">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1453
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"air-datepicker": "^3.6.0",
|
||||
"chart.js": "^4.5.1",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.34",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.12.3",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
"typescript": "~6.0.2",
|
||||
"vite": "^8.0.12",
|
||||
"vue-tsc": "^3.2.8"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,394 @@
|
||||
:root {
|
||||
--primary-50: #eff6ff;
|
||||
--primary-100: #dbeafe;
|
||||
--primary-200: #bfdbfe;
|
||||
--primary-300: #93c5fd;
|
||||
--primary-400: #60a5fa;
|
||||
--primary-500: #3b82f6;
|
||||
--primary-600: #2563eb;
|
||||
--primary-700: #1d4ed8;
|
||||
--primary-800: #1e40af;
|
||||
--primary-900: #1e3a8a;
|
||||
|
||||
--gray-50: #fafafa;
|
||||
--gray-100: #f5f5f5;
|
||||
--gray-200: #e5e5e5;
|
||||
--gray-300: #d4d4d4;
|
||||
--gray-400: #a3a3a3;
|
||||
--gray-500: #737373;
|
||||
--gray-600: #525252;
|
||||
--gray-700: #404040;
|
||||
--gray-800: #262626;
|
||||
--gray-900: #171717;
|
||||
|
||||
--success: #22c55e;
|
||||
--success-bg: #f0fdf4;
|
||||
--warning: #eab308;
|
||||
--warning-bg: #fefce8;
|
||||
--error: #ef4444;
|
||||
--error-bg: #fef2f2;
|
||||
--info: #3b82f6;
|
||||
--info-bg: #eff6ff;
|
||||
|
||||
--text-primary: #171717;
|
||||
--text-secondary: #404040;
|
||||
--text-tertiary: #737373;
|
||||
--text-muted: #a3a3a3;
|
||||
--text-inverse: #ffffff;
|
||||
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #fafafa;
|
||||
--bg-tertiary: #f5f5f5;
|
||||
--bg-elevated: #ffffff;
|
||||
--bg-overlay: rgba(0, 0, 0, 0.4);
|
||||
|
||||
--border-light: #f0f0f0;
|
||||
--border-default: #e5e5e5;
|
||||
--border-strong: #d4d4d4;
|
||||
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-4: 1rem;
|
||||
--space-5: 1.25rem;
|
||||
--space-6: 1.5rem;
|
||||
--space-8: 2rem;
|
||||
--space-10: 2.5rem;
|
||||
--space-12: 3rem;
|
||||
--space-16: 4rem;
|
||||
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-mono: 'SF Mono', Monaco, Consolas, monospace;
|
||||
|
||||
--text-xs: 0.75rem;
|
||||
--text-sm: 0.875rem;
|
||||
--text-base: 1rem;
|
||||
--text-lg: 1.125rem;
|
||||
--text-xl: 1.25rem;
|
||||
--text-2xl: 1.5rem;
|
||||
--text-3xl: 1.875rem;
|
||||
--text-4xl: 2.25rem;
|
||||
|
||||
--font-normal: 400;
|
||||
--font-medium: 500;
|
||||
--font-semibold: 600;
|
||||
--font-bold: 700;
|
||||
|
||||
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
--radius-2xl: 1.25rem;
|
||||
--radius-full: 9999px;
|
||||
|
||||
--ease-default: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--duration-fast: 150ms;
|
||||
--duration-normal: 200ms;
|
||||
--duration-slow: 300ms;
|
||||
}
|
||||
|
||||
[data-ui="v2"] {
|
||||
--primary-50: #eef2ff;
|
||||
--primary-100: #e0e7ff;
|
||||
--primary-200: #c7d2fe;
|
||||
--primary-300: #a5b4fc;
|
||||
--primary-400: #818cf8;
|
||||
--primary-500: #6366f1;
|
||||
--primary-600: #4f46e5;
|
||||
--primary-700: #4338ca;
|
||||
--primary-800: #3730a3;
|
||||
--primary-900: #312e81;
|
||||
|
||||
--gray-50: #fafafa;
|
||||
--gray-100: #f4f4f5;
|
||||
--gray-200: #e4e4e7;
|
||||
--gray-300: #d4d4d8;
|
||||
--gray-400: #a1a1aa;
|
||||
--gray-500: #71717a;
|
||||
--gray-600: #52525b;
|
||||
--gray-700: #3f3f46;
|
||||
--gray-800: #27272a;
|
||||
--gray-900: #18181b;
|
||||
|
||||
--success: #16a34a;
|
||||
--success-bg: #f0fdf4;
|
||||
--warning: #d97706;
|
||||
--warning-bg: #fffbeb;
|
||||
--error: #dc2626;
|
||||
--error-bg: #fef2f2;
|
||||
--info: var(--primary-600);
|
||||
--info-bg: var(--primary-50);
|
||||
|
||||
--text-primary: #111827;
|
||||
--text-secondary: #374151;
|
||||
--text-tertiary: #6b7280;
|
||||
--text-muted: #9ca3af;
|
||||
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-tertiary: #f8fafc;
|
||||
--bg-elevated: #ffffff;
|
||||
--bg-overlay: rgba(15, 23, 42, 0.5);
|
||||
|
||||
--border-light: rgba(15, 23, 42, 0.08);
|
||||
--border-default: rgba(15, 23, 42, 0.12);
|
||||
--border-strong: rgba(15, 23, 42, 0.18);
|
||||
|
||||
--radius-sm: 0.5rem;
|
||||
--radius-md: 0.75rem;
|
||||
--radius-lg: 1rem;
|
||||
--radius-xl: 1.25rem;
|
||||
--radius-2xl: 1.5rem;
|
||||
|
||||
--shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.06);
|
||||
--shadow-sm: 0 2px 10px rgba(15, 23, 42, 0.08);
|
||||
--shadow-md: 0 12px 30px rgba(15, 23, 42, 0.12);
|
||||
--shadow-lg: 0 24px 60px rgba(15, 23, 42, 0.14);
|
||||
|
||||
--duration-fast: 140ms;
|
||||
--duration-normal: 180ms;
|
||||
--duration-slow: 260ms;
|
||||
}
|
||||
|
||||
[data-ui="v2"] body {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--text-primary: #f3f4f6;
|
||||
--text-secondary: #d1d5db;
|
||||
--text-tertiary: #9ca3af;
|
||||
--text-muted: #6b7280;
|
||||
|
||||
--bg-primary: #0b1220;
|
||||
--bg-secondary: #0f172a;
|
||||
--bg-tertiary: #111c33;
|
||||
--bg-elevated: #0f172a;
|
||||
--bg-overlay: rgba(0, 0, 0, 0.6);
|
||||
|
||||
--border-light: rgba(255, 255, 255, 0.06);
|
||||
--border-default: rgba(255, 255, 255, 0.1);
|
||||
--border-strong: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
[data-ui="v2"][data-theme="dark"] {
|
||||
--primary-50: rgba(99, 102, 241, 0.12);
|
||||
--primary-100: rgba(99, 102, 241, 0.18);
|
||||
--bg-primary: #0a1020;
|
||||
--bg-secondary: #0b1326;
|
||||
--bg-tertiary: #0d1930;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root {
|
||||
--duration-fast: 1ms;
|
||||
--duration-normal: 1ms;
|
||||
--duration-slow: 1ms;
|
||||
}
|
||||
*, *::before, *::after {
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 1ms !important;
|
||||
animation-duration: 1ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-base);
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-secondary);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--gray-300);
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--gray-400);
|
||||
}
|
||||
|
||||
select.form-input {
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23737373' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right var(--space-3) center;
|
||||
padding-right: var(--space-8);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
|
||||
padding: var(--space-3) var(--space-5); font-size: var(--text-sm); font-weight: var(--font-medium);
|
||||
border-radius: var(--radius-md); border: none; cursor: pointer;
|
||||
transition: all var(--duration-fast) var(--ease-default); text-decoration: none;
|
||||
}
|
||||
[data-ui="v2"] .btn { border-radius: var(--radius-lg); }
|
||||
.btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.22); }
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-500) 0%, var(--primary-600) 100%);
|
||||
color: white; box-shadow: 0 2px 8px rgba(59, 130, 246, 0.25);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, var(--primary-600) 0%, var(--primary-700) 100%);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.35); transform: translateY(-1px);
|
||||
}
|
||||
.btn-primary:active { transform: translateY(0); box-shadow: 0 1px 4px rgba(59, 130, 246, 0.2); }
|
||||
.btn-secondary { background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border-default); }
|
||||
.btn-secondary:hover { background: var(--gray-200); }
|
||||
.btn-danger { background: var(--error); color: white; }
|
||||
.btn-danger:hover { background: #dc2626; }
|
||||
.btn-lg { padding: var(--space-4) var(--space-6); font-size: var(--text-base); }
|
||||
.btn-sm { padding: var(--space-2) var(--space-3); font-size: var(--text-xs); }
|
||||
.btn:disabled, .btn-disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; }
|
||||
|
||||
.card {
|
||||
background: var(--bg-primary); border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-xl); padding: var(--space-6); margin-bottom: var(--space-6);
|
||||
}
|
||||
[data-ui="v2"] .card { border-color: var(--border-default); box-shadow: var(--shadow-sm); }
|
||||
[data-ui="v2"][data-theme="dark"] .card { box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35); }
|
||||
.card-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-bottom: var(--space-4); padding-bottom: var(--space-4);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
.card-title { font-size: var(--text-lg); font-weight: var(--font-semibold); color: var(--text-primary); }
|
||||
.card-body { padding: var(--space-2) 0; }
|
||||
|
||||
.form-group { margin-bottom: var(--space-5); }
|
||||
.form-label {
|
||||
display: block; font-size: var(--text-sm); font-weight: var(--font-medium);
|
||||
color: var(--text-primary); margin-bottom: var(--space-2);
|
||||
}
|
||||
.form-input {
|
||||
width: 100%; padding: var(--space-3) var(--space-4); font-size: var(--text-base);
|
||||
color: var(--text-primary); background: var(--bg-primary);
|
||||
border: 1px solid var(--border-default); border-radius: var(--radius-md);
|
||||
transition: all var(--duration-fast) var(--ease-default);
|
||||
}
|
||||
.form-input:focus {
|
||||
outline: none; border-color: var(--primary-500);
|
||||
box-shadow: 0 0 0 3px var(--primary-100);
|
||||
}
|
||||
.form-input::placeholder { color: var(--text-muted); }
|
||||
.form-error { font-size: var(--text-sm); color: var(--error); margin-top: var(--space-1); }
|
||||
.form-select {
|
||||
width: 100%; padding: 10px 12px; font-size: 14px; border: 1px solid #ddd;
|
||||
border-radius: 6px; background: white; color: #333; cursor: pointer;
|
||||
}
|
||||
.form-select:focus { outline: none; border-color: #1976d2; box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.1); }
|
||||
|
||||
.data-table { width: 100%; border-collapse: collapse; }
|
||||
.data-table th, .data-table td {
|
||||
padding: var(--space-4) var(--space-4); text-align: left;
|
||||
border-bottom: 1px solid var(--border-light); vertical-align: top;
|
||||
}
|
||||
.data-table th {
|
||||
font-size: var(--text-xs); font-weight: var(--font-medium); color: var(--text-tertiary);
|
||||
text-transform: uppercase; letter-spacing: 0.05em; background: var(--bg-secondary);
|
||||
}
|
||||
.data-table td { font-size: var(--text-sm); color: var(--text-primary); }
|
||||
.data-table tbody tr:hover { background: var(--bg-secondary); }
|
||||
|
||||
.badge {
|
||||
display: inline-flex; align-items: center; padding: var(--space-1) var(--space-2);
|
||||
font-size: var(--text-xs); font-weight: var(--font-medium); border-radius: var(--radius-full);
|
||||
}
|
||||
.badge-success { background: var(--success-bg); color: var(--success); }
|
||||
.badge-warning { background: var(--warning-bg); color: var(--warning); }
|
||||
.badge-error { background: var(--error-bg); color: var(--error); }
|
||||
.badge-info { background: var(--info-bg); color: var(--info); }
|
||||
|
||||
.tabs {
|
||||
display: flex; gap: var(--space-2); background: var(--bg-secondary);
|
||||
padding: var(--space-1); border-radius: var(--radius-xl); margin-bottom: var(--space-6);
|
||||
overflow-x: auto;
|
||||
}
|
||||
.tab {
|
||||
padding: var(--space-3) var(--space-5); font-size: var(--text-sm); font-weight: var(--font-medium);
|
||||
color: var(--text-secondary); background: transparent; border: none;
|
||||
border-radius: var(--radius-lg); cursor: pointer;
|
||||
transition: all var(--duration-fast) var(--ease-default); white-space: nowrap;
|
||||
}
|
||||
.tab:hover { color: var(--text-primary); background: var(--bg-primary); }
|
||||
.tab.active { color: var(--primary-600); background: var(--bg-primary); box-shadow: var(--shadow-sm); }
|
||||
|
||||
.page-header { margin-bottom: var(--space-8); }
|
||||
.page-title { font-size: var(--text-3xl); font-weight: var(--font-bold); color: var(--text-primary); margin-bottom: var(--space-2); }
|
||||
.page-subtitle { font-size: var(--text-base); color: var(--text-tertiary); }
|
||||
|
||||
.progress-bar { width: 100%; height: 8px; background: var(--bg-tertiary); border-radius: var(--radius-full); overflow: hidden; }
|
||||
.progress-fill { height: 100%; background: var(--primary-500); border-radius: var(--radius-full); transition: width var(--duration-normal) var(--ease-default); }
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center;
|
||||
z-index: 1000; padding: var(--space-4);
|
||||
}
|
||||
.modal-content {
|
||||
background: var(--bg-primary); border-radius: var(--radius-xl);
|
||||
width: 100%; max-width: 1000px; max-height: 90vh; overflow-y: auto;
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
.modal-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: var(--space-5) var(--space-6); border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
.modal-header h3 { font-size: var(--text-lg); font-weight: var(--font-semibold); color: var(--text-primary); margin: 0; }
|
||||
.modal-close {
|
||||
width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
|
||||
background: transparent; border: none; font-size: var(--text-xl);
|
||||
color: var(--text-tertiary); cursor: pointer; border-radius: var(--radius-md);
|
||||
transition: all var(--duration-fast) var(--ease-default);
|
||||
}
|
||||
.modal-close:hover { background: var(--bg-secondary); color: var(--text-primary); }
|
||||
.modal-body { padding: var(--space-6); }
|
||||
.modal-footer {
|
||||
display: flex; justify-content: flex-end; gap: var(--space-3);
|
||||
padding-top: var(--space-4); margin-top: var(--space-4);
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: var(--space-3) var(--space-4); background: var(--error-bg);
|
||||
color: var(--error); border-radius: var(--radius-md); font-size: var(--text-sm);
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.empty-state { text-align: center; padding: var(--space-12) 0; }
|
||||
.empty-icon { width: 64px; height: 64px; margin: 0 auto var(--space-4); color: var(--text-muted); }
|
||||
.empty-title { font-size: var(--text-lg); font-weight: var(--font-medium); color: var(--text-primary); margin-bottom: var(--space-2); }
|
||||
.empty-desc { font-size: var(--text-sm); color: var(--text-tertiary); }
|
||||
|
||||
.text-center { text-align: center; }
|
||||
.w-full { width: 100%; }
|
||||
.hidden { display: none; }
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">设计体系</h1>
|
||||
<p class="page-subtitle">迁移中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">灰度发布与回滚</h1>
|
||||
<p class="page-subtitle">迁移中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
import './assets/styles/main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="hero-section">
|
||||
<h1 class="hero-title">Gemold 模具制造管理系统</h1>
|
||||
<p class="hero-subtitle">智能模具设计与制造一体化平台,从零件分析到模具生成,全流程数字化解决方案</p>
|
||||
</div>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">◈</div>
|
||||
<h3 class="feature-title">MoldInsight</h3>
|
||||
<p class="feature-desc">智能模具分析引擎,支持 STP/STEP 文件解析、分模设计、CAM 刀路规划</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">⊞</div>
|
||||
<h3 class="feature-title">进销存管理</h3>
|
||||
<p class="feature-desc">产品管理、采购销售、库存管理、财务管理,一站式 ERP 解决方案</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">⚙</div>
|
||||
<h3 class="feature-title">AI 驱动</h3>
|
||||
<p class="feature-desc">AI 分型面检测、模具方案评分、智能设计建议,提升设计效率</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero-section { text-align: center; padding: var(--space-16) 0; }
|
||||
.hero-title { font-size: var(--text-4xl); font-weight: var(--font-bold); color: var(--text-primary); margin-bottom: var(--space-4); }
|
||||
.hero-subtitle { font-size: var(--text-lg); color: var(--text-tertiary); max-width: 600px; margin: 0 auto var(--space-8); }
|
||||
.features-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--space-6); margin-top: var(--space-12); }
|
||||
.feature-card { background: var(--bg-primary); border: 1px solid var(--border-light); border-radius: var(--radius-xl); padding: var(--space-6); text-align: left; }
|
||||
.feature-icon { width: 40px; height: 40px; background: var(--primary-50); border-radius: var(--radius-lg); display: flex; align-items: center; justify-content: center; color: var(--primary-500); font-size: var(--text-lg); margin-bottom: var(--space-4); }
|
||||
.feature-title { font-size: var(--text-base); font-weight: var(--font-semibold); color: var(--text-primary); margin-bottom: var(--space-2); }
|
||||
.feature-desc { font-size: var(--text-sm); color: var(--text-tertiary); line-height: 1.6; }
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<div class="login-logo">G</div>
|
||||
<h1 class="login-title">Gemold</h1>
|
||||
<p class="login-subtitle">模具制造管理系统</p>
|
||||
</div>
|
||||
<form @submit.prevent="handleLogin">
|
||||
<div class="form-group">
|
||||
<label class="form-label">用户名</label>
|
||||
<input v-model="state.username" type="text" class="form-input" placeholder="请输入用户名" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">密码</label>
|
||||
<input v-model="state.password" type="password" class="form-input" placeholder="请输入密码" required />
|
||||
</div>
|
||||
<div class="form-error" v-if="state.error">{{ state.error }}</div>
|
||||
<button type="submit" class="btn btn-primary w-full mt-4" :disabled="state.loading">
|
||||
{{ state.loading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { apiRequest } from '@/shared/api'
|
||||
import { saveAuth } from '@/shared/auth'
|
||||
import { addNotification } from '@/shared/notification'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const state = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
error: '',
|
||||
loading: false,
|
||||
})
|
||||
|
||||
const handleLogin = async () => {
|
||||
state.error = ''
|
||||
state.loading = true
|
||||
try {
|
||||
const data = await apiRequest<{ access_token: string; token_type: string; user: Record<string, unknown> }>('/api/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username: state.username, password: state.password }),
|
||||
})
|
||||
saveAuth(data.access_token, data.user)
|
||||
addNotification('登录成功', 'success')
|
||||
router.push('/')
|
||||
} catch (err: any) {
|
||||
state.error = err.message || '登录失败'
|
||||
} finally {
|
||||
state.loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: var(--bg-secondary); padding: var(--space-6); }
|
||||
.login-card { width: 100%; max-width: 400px; background: var(--bg-primary); border: 1px solid var(--border-light); border-radius: var(--radius-xl); padding: var(--space-8); }
|
||||
.login-header { text-align: center; margin-bottom: var(--space-8); }
|
||||
.login-logo { width: 48px; height: 48px; background: var(--primary-500); border-radius: var(--radius-lg); display: flex; align-items: center; justify-content: center; color: white; font-size: var(--text-xl); font-weight: var(--font-bold); margin: 0 auto var(--space-4); }
|
||||
.login-title { font-size: var(--text-2xl); font-weight: var(--font-bold); color: var(--text-primary); margin-bottom: var(--space-2); }
|
||||
.login-subtitle { font-size: var(--text-sm); color: var(--text-tertiary); }
|
||||
</style>
|
||||
@@ -0,0 +1,476 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h1>注塑模 STP 分析</h1>
|
||||
<p>上传 STEP/STP 产品件,完成自动分模、工程建议与导出</p>
|
||||
</div>
|
||||
|
||||
<div class="moldinsight-intro-grid">
|
||||
<div class="intro-card">
|
||||
<div class="intro-card-title">输入</div>
|
||||
<div class="intro-card-text">STEP/STP 产品件,面向注塑模主流程</div>
|
||||
</div>
|
||||
<div class="intro-card">
|
||||
<div class="intro-card-title">输出</div>
|
||||
<div class="intro-card-text">分模方案、DFM 风险、注塑模系统摘要与 CAD 导出</div>
|
||||
</div>
|
||||
<div class="intro-card">
|
||||
<div class="intro-card-title">目标</div>
|
||||
<div class="intro-card-text">先确认推荐方案,再进入导出与 CAM 准备</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-layout">
|
||||
<div class="upload-main-card">
|
||||
<h2 class="section-title">1. 上传产品件</h2>
|
||||
<div
|
||||
:class="['upload-zone', { 'drag-over': state.dragOver }]"
|
||||
@dragover.prevent="state.dragOver = true"
|
||||
@dragleave.prevent="state.dragOver = false"
|
||||
@drop="handleDrop"
|
||||
@click="fileInput?.click()"
|
||||
>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept=".stp,.step"
|
||||
@change="handleFileChange"
|
||||
hidden
|
||||
/>
|
||||
<div class="upload-icon">📁</div>
|
||||
<div class="upload-text">
|
||||
<span class="upload-title">点击选择或拖拽 STP/STEP 文件</span>
|
||||
<span class="upload-hint">支持注塑模产品件分析,最大 100MB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="state.selectedFile" class="file-info">
|
||||
<span class="file-name">{{ state.selectedFile.name }}</span>
|
||||
<span class="file-size">{{ formatFileSize(state.selectedFile.size) }}</span>
|
||||
<button class="btn-clear" @click="state.selectedFile = null" title="清除文件">×</button>
|
||||
</div>
|
||||
|
||||
<div v-if="state.error" class="error-message">{{ state.error }}</div>
|
||||
|
||||
<div v-if="state.polling" class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: state.progress + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-side-card">
|
||||
<h2 class="section-title">2. 注塑模参数</h2>
|
||||
<div class="material-panel compact-panel">
|
||||
<div class="form-group">
|
||||
<label class="form-label">产品材料</label>
|
||||
<select v-model="state.selectedMaterial" class="form-select">
|
||||
<option value="ABS">ABS (1.05 g/cm³)</option>
|
||||
<option value="PP">PP (0.90 g/cm³)</option>
|
||||
<option value="PE">PE (0.95 g/cm³)</option>
|
||||
<option value="PC">PC (1.20 g/cm³)</option>
|
||||
<option value="PA">PA (1.14 g/cm³)</option>
|
||||
<option value="POM">POM (1.41 g/cm³)</option>
|
||||
<option value="PMMA">PMMA (1.18 g/cm³)</option>
|
||||
<option value="PBT">PBT (1.31 g/cm³)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<details class="advanced-params">
|
||||
<summary>高级工艺参数</summary>
|
||||
<div class="advanced-params-body">
|
||||
<div class="inline-note">默认值适用于多数注塑件;仅在已知工艺约束时再调整。</div>
|
||||
<div class="advanced-param-grid">
|
||||
<div class="param-input-card" v-for="field in moldParamFields" :key="field.key">
|
||||
<label class="form-label">{{ field.label }}</label>
|
||||
<div class="param-input-row">
|
||||
<input
|
||||
type="number"
|
||||
class="form-input param-number-input"
|
||||
v-model.number="state.moldParams[field.key as keyof typeof state.moldParams]"
|
||||
:min="field.min"
|
||||
:max="field.max"
|
||||
:step="field.step"
|
||||
/>
|
||||
<span class="param-unit">{{ field.unit }}</span>
|
||||
</div>
|
||||
<div class="param-meta-row">
|
||||
<span class="range-value">默认 {{ field.defaultValue }}{{ field.unit }}</span>
|
||||
<span class="range-value">范围 {{ field.min }} - {{ field.max }}{{ field.unit }}</span>
|
||||
</div>
|
||||
<div class="param-hint">{{ field.hint }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="state.selectedFile"
|
||||
class="btn-primary upload-submit-btn"
|
||||
@click="uploadFile"
|
||||
:disabled="state.uploading || state.polling"
|
||||
>
|
||||
{{ state.uploading ? '上传中...' : state.polling ? '分析中...' : '3. 开始注塑模分析' }}
|
||||
</button>
|
||||
<div v-else class="inline-note">先选择 STP 文件,再填写材料并开始分析。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details v-if="state.history?.files?.length" class="history-collapsible section">
|
||||
<summary class="history-summary">分析历史({{ state.history.files.length }} 个文件)</summary>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>文件名</th>
|
||||
<th>上传次数</th>
|
||||
<th>文件大小</th>
|
||||
<th>最新状态</th>
|
||||
<th>最新分析时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="file in state.history.files" :key="file.filename">
|
||||
<tr>
|
||||
<td>{{ file.filename }}</td>
|
||||
<td>
|
||||
<span class="badge badge-info">{{ file.upload_count }} 次</span>
|
||||
</td>
|
||||
<td>{{ formatFileSize(file.file_size) }}</td>
|
||||
<td>
|
||||
<span :class="['badge', file.latest_status === 'completed' ? 'badge-success' : file.latest_status === 'failed' ? 'badge-error' : 'badge-warning']">
|
||||
{{ file.latest_status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ formatDateTime(file.latest_upload_time) }}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button v-if="file.latest_status === 'completed'" class="btn-sm btn-primary" @click="viewResult({ task_id: file.latest_task_id })">
|
||||
查看最新
|
||||
</button>
|
||||
<button class="btn-sm btn-icon" @click="toggleFileHistory(file.filename)" :title="state.expandedFiles[file.filename] ? '收起' : '展开历史记录'">
|
||||
<span class="dropdown-icon" :class="{ expanded: state.expandedFiles[file.filename] }">▼</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="state.expandedFiles[file.filename]" class="history-detail-row">
|
||||
<td colspan="6">
|
||||
<div class="history-dropdown">
|
||||
<table class="data-table inner-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>上传时间</th>
|
||||
<th>文件大小</th>
|
||||
<th>状态</th>
|
||||
<th>体积 (mm³)</th>
|
||||
<th>表面积 (mm²)</th>
|
||||
<th>重量 (g)</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="record in state.expandedFiles[file.filename]" :key="record.id">
|
||||
<td>{{ formatDateTime(record.upload_time) }}</td>
|
||||
<td>{{ formatFileSize(record.file_size) }}</td>
|
||||
<td>
|
||||
<span :class="['badge', record.status === 'completed' ? 'badge-success' : record.status === 'failed' ? 'badge-error' : 'badge-warning']">
|
||||
{{ record.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ record.volume ? formatNumber(record.volume) : '-' }}</td>
|
||||
<td>{{ record.surface_area ? formatNumber(record.surface_area) : '-' }}</td>
|
||||
<td>{{ record.product_weight ? record.product_weight.toFixed(2) : '-' }}</td>
|
||||
<td>
|
||||
<button v-if="record.has_analysis" class="btn-sm btn-primary" @click="viewResult(record)">
|
||||
查看详情
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { apiRequest } from '@/shared/api'
|
||||
import { handleApiError, addNotification } from '@/shared/notification'
|
||||
import { clearAuth } from '@/shared/auth'
|
||||
import { formatFileSize, formatDateTime, formatNumber } from '@/shared/utils'
|
||||
|
||||
interface HistoryRecord {
|
||||
id: number
|
||||
upload_time: string
|
||||
file_size: number
|
||||
status: string
|
||||
volume?: number
|
||||
surface_area?: number
|
||||
product_weight?: number
|
||||
has_analysis?: boolean
|
||||
task_id?: string
|
||||
}
|
||||
|
||||
interface HistoryFile {
|
||||
filename: string
|
||||
upload_count: number
|
||||
file_size: number
|
||||
latest_status: string
|
||||
latest_upload_time: string
|
||||
latest_task_id?: string
|
||||
}
|
||||
|
||||
interface HistoryData {
|
||||
files: HistoryFile[]
|
||||
}
|
||||
|
||||
interface MoldParams {
|
||||
draftAngle: number
|
||||
shrinkageRate: number
|
||||
partingPrecision: number
|
||||
cavityMatch: number
|
||||
}
|
||||
|
||||
interface TaskInfo {
|
||||
task_id: string
|
||||
status: string
|
||||
filename?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const state = reactive({
|
||||
selectedFile: null as File | null,
|
||||
selectedMaterial: 'ABS',
|
||||
moldParams: {
|
||||
draftAngle: 2.0,
|
||||
shrinkageRate: 0.5,
|
||||
partingPrecision: 0.1,
|
||||
cavityMatch: 95,
|
||||
} as MoldParams,
|
||||
uploading: false,
|
||||
error: '',
|
||||
currentTask: null as TaskInfo | null,
|
||||
task: null as TaskInfo | null,
|
||||
polling: false,
|
||||
dragOver: false,
|
||||
progress: 0,
|
||||
history: null as HistoryData | null,
|
||||
expandedFiles: {} as Record<string, HistoryRecord[] | null>,
|
||||
})
|
||||
|
||||
const loadHistory = async () => {
|
||||
try {
|
||||
state.history = await apiRequest<HistoryData>('/api/history')
|
||||
} catch (e) {
|
||||
console.error('加载历史记录失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleFileHistory = async (filename: string) => {
|
||||
if (state.expandedFiles[filename]) {
|
||||
state.expandedFiles[filename] = null
|
||||
} else {
|
||||
try {
|
||||
const records = await apiRequest<HistoryRecord[]>(`/api/history/${encodeURIComponent(filename)}`)
|
||||
state.expandedFiles[filename] = records
|
||||
} catch (e) {
|
||||
handleApiError(e, '加载文件历史')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const viewResult = (record: { task_id?: string }) => {
|
||||
router.push(`/moldinsight/result/${record.task_id}`)
|
||||
}
|
||||
|
||||
const handleFileChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (!file) return
|
||||
validateAndSelectFile(file)
|
||||
}
|
||||
|
||||
const validateAndSelectFile = (file: File) => {
|
||||
const lowerName = file.name.toLowerCase()
|
||||
if (!lowerName.endsWith('.stp') && !lowerName.endsWith('.step')) {
|
||||
state.error = '请选择 STP 或 STEP 格式文件'
|
||||
state.selectedFile = null
|
||||
return
|
||||
}
|
||||
if (file.size > 100 * 1024 * 1024) {
|
||||
state.error = '文件大小不能超过 100MB'
|
||||
state.selectedFile = null
|
||||
return
|
||||
}
|
||||
state.error = ''
|
||||
state.selectedFile = file
|
||||
addNotification(`已选择文件: ${file.name}`, 'success')
|
||||
}
|
||||
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
state.dragOver = false
|
||||
const files = event.dataTransfer?.files
|
||||
if (files && files.length > 0) validateAndSelectFile(files[0])
|
||||
}
|
||||
|
||||
const uploadFile = async () => {
|
||||
if (!state.selectedFile) return
|
||||
|
||||
if (!appStore.token) {
|
||||
state.error = '请先登录后再上传文件'
|
||||
addNotification('请先登录', 'warning')
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
|
||||
if (appStore.token === 'demo') {
|
||||
state.error = '演示模式不支持文件上传,请使用完整账户登录'
|
||||
addNotification('演示模式不支持上传', 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
state.uploading = true
|
||||
state.error = ''
|
||||
state.progress = 0
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', state.selectedFile)
|
||||
formData.append('material', state.selectedMaterial)
|
||||
formData.append('draft_angle', String(state.moldParams.draftAngle))
|
||||
formData.append('shrinkage_rate', String(state.moldParams.shrinkageRate))
|
||||
formData.append('parting_precision', String(state.moldParams.partingPrecision))
|
||||
formData.append('cavity_match', String(state.moldParams.cavityMatch))
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${appStore.token}` },
|
||||
body: formData,
|
||||
})
|
||||
if (res.status === 401) {
|
||||
clearAuth()
|
||||
state.error = '登录已过期,请重新登录'
|
||||
addNotification('登录已过期,请重新登录', 'warning')
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
if (!res.ok) throw new Error(`上传失败: ${res.status}`)
|
||||
const data = await res.json()
|
||||
state.currentTask = { task_id: data.task_id, status: 'processing', filename: data.file_info?.filename }
|
||||
addNotification('文件上传成功,开始分析...', 'success')
|
||||
startPolling(data.task_id)
|
||||
} catch (e) {
|
||||
state.error = handleApiError(e, '文件上传')
|
||||
} finally {
|
||||
state.uploading = false
|
||||
}
|
||||
}
|
||||
|
||||
const startPolling = async (taskId: string) => {
|
||||
console.log('[Polling] 开始轮询任务:', taskId)
|
||||
state.polling = true
|
||||
state.progress = 10
|
||||
let pollCount = 0
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
pollCount++
|
||||
state.progress = Math.min(90, 10 + pollCount * 0.5)
|
||||
|
||||
const task = await apiRequest<TaskInfo>(`/api/status/${taskId}`, { method: 'POST' })
|
||||
state.currentTask = task
|
||||
state.task = task
|
||||
|
||||
if (task.status === 'completed') {
|
||||
state.polling = false
|
||||
state.progress = 100
|
||||
addNotification('分析完成', 'success')
|
||||
loadHistory()
|
||||
router.push(`/moldinsight/result/${taskId}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (task.status === 'failed') {
|
||||
state.polling = false
|
||||
state.error = task.error || '分析失败'
|
||||
addNotification('分析失败', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (pollCount < 300) setTimeout(poll, 2000)
|
||||
} catch (e) {
|
||||
state.polling = false
|
||||
state.error = handleApiError(e, '轮询状态')
|
||||
}
|
||||
}
|
||||
|
||||
poll()
|
||||
}
|
||||
|
||||
const moldParamFields = [
|
||||
{
|
||||
key: 'draftAngle',
|
||||
label: '拔模角',
|
||||
unit: '°',
|
||||
min: 1,
|
||||
max: 10,
|
||||
step: 0.5,
|
||||
defaultValue: 2.0,
|
||||
hint: '常规注塑件建议从 1.5° 到 3° 起步',
|
||||
},
|
||||
{
|
||||
key: 'shrinkageRate',
|
||||
label: '收缩率',
|
||||
unit: '%',
|
||||
min: 0.5,
|
||||
max: 3.0,
|
||||
step: 0.1,
|
||||
defaultValue: 0.5,
|
||||
hint: '按材料牌号校核,默认值用于首轮方案评估',
|
||||
},
|
||||
{
|
||||
key: 'partingPrecision',
|
||||
label: '分型精度',
|
||||
unit: 'mm',
|
||||
min: 0.01,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
defaultValue: 0.1,
|
||||
hint: '用于控制分型面拟合与边界容差',
|
||||
},
|
||||
{
|
||||
key: 'cavityMatch',
|
||||
label: '型腔匹配度',
|
||||
unit: '%',
|
||||
min: 80,
|
||||
max: 100,
|
||||
step: 1,
|
||||
defaultValue: 95,
|
||||
hint: '数值越高越偏向紧配合与严格封合',
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
if (!appStore.user) {
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
loadHistory()
|
||||
})
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>用户管理</h1>
|
||||
<p>管理系统用户和权限</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="openUserModal()">+ 添加用户</button>
|
||||
</div>
|
||||
|
||||
<div v-if="state.loading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户名</th>
|
||||
<th>邮箱</th>
|
||||
<th>姓名</th>
|
||||
<th>状态</th>
|
||||
<th>角色</th>
|
||||
<th>注册时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in state.users" :key="user.id">
|
||||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ user.full_name || '-' }}</td>
|
||||
<td>
|
||||
<span :class="['badge', user.is_active ? 'badge-success' : 'badge-error']">
|
||||
{{ user.is_active ? '正常' : '禁用' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-for="role in (user as any).roles" :key="role" class="badge badge-info" style="margin-right: 4px;">
|
||||
{{ role }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ formatDateTime(user.created_at) }}</td>
|
||||
<td>
|
||||
<div class="action-buttons" style="display:flex; gap:4px;">
|
||||
<button class="btn btn-sm" style="background:var(--primary-500);color:white;" @click="openUserModal(user)">编辑</button>
|
||||
<button class="btn btn-sm" style="background:#eab308;color:white;" @click="resetPassword(user)">重置密码</button>
|
||||
<button v-if="(user as any).id !== storeUser?.id" class="btn btn-sm" style="background:var(--error);color:white;" @click="deleteUser(user)">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-if="state.showUserModal" class="modal-overlay" @click.self="state.showUserModal = false">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>{{ state.editingUser ? '编辑用户' : '添加用户' }}</h2>
|
||||
<button class="modal-close" @click="state.showUserModal = false">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">用户名</label>
|
||||
<input v-model="state.userForm.username" type="text" class="form-input" :disabled="!!state.editingUser" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">邮箱</label>
|
||||
<input v-model="state.userForm.email" type="email" class="form-input" />
|
||||
</div>
|
||||
<div class="form-group" v-if="!state.editingUser">
|
||||
<label class="form-label">密码</label>
|
||||
<input v-model="state.userForm.password" type="password" class="form-input" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">姓名</label>
|
||||
<input v-model="state.userForm.full_name" type="text" class="form-input" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">角色</label>
|
||||
<div class="checkbox-group" style="flex-direction: column; gap: 8px;">
|
||||
<label v-for="role in state.roles" :key="(role as any).id" class="checkbox-label">
|
||||
<input type="checkbox" :value="(role as any).id" v-model="state.userForm.role_ids" class="checkbox-input" />
|
||||
{{ (role as any).name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" @click="state.showUserModal = false">取消</button>
|
||||
<button class="btn btn-primary" @click="saveUser">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { apiRequest } from '@/shared/api'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { addNotification } from '@/shared/notification'
|
||||
import { formatDateTime } from '@/shared/utils'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useAppStore()
|
||||
const storeUser = store.user
|
||||
|
||||
interface UserItem {
|
||||
id: number
|
||||
username: string
|
||||
email: string
|
||||
full_name: string | null
|
||||
is_active: boolean
|
||||
roles: string[]
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface RoleItem {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
users: [] as UserItem[],
|
||||
roles: [] as RoleItem[],
|
||||
loading: true,
|
||||
showUserModal: false,
|
||||
editingUser: null as UserItem | null,
|
||||
userForm: {
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
full_name: '',
|
||||
role_ids: [] as number[],
|
||||
},
|
||||
})
|
||||
|
||||
const loadUsers = async () => {
|
||||
try {
|
||||
state.users = await apiRequest<UserItem[]>('/api/auth/users')
|
||||
} catch (e) {
|
||||
const err = e as Error
|
||||
addNotification(err.message || '加载用户列表失败', 'error')
|
||||
} finally {
|
||||
state.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadRoles = async () => {
|
||||
try {
|
||||
state.roles = await apiRequest<RoleItem[]>('/api/auth/roles')
|
||||
} catch (e) {
|
||||
const err = e as Error
|
||||
addNotification(err.message || '加载角色列表失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const openUserModal = (user: UserItem | null = null) => {
|
||||
state.editingUser = user
|
||||
if (user) {
|
||||
state.userForm = {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
password: '',
|
||||
full_name: user.full_name || '',
|
||||
role_ids: user.roles
|
||||
.map(r => {
|
||||
const role = state.roles.find(role => role.code === r)
|
||||
return role ? role.id : null
|
||||
})
|
||||
.filter(id => id !== null) as number[],
|
||||
}
|
||||
} else {
|
||||
state.userForm = { username: '', email: '', password: '', full_name: '', role_ids: [] }
|
||||
}
|
||||
state.showUserModal = true
|
||||
}
|
||||
|
||||
const saveUser = async () => {
|
||||
if (!state.userForm.username || !state.userForm.email) {
|
||||
addNotification('请填写用户名和邮箱', 'error')
|
||||
return
|
||||
}
|
||||
if (!state.editingUser && !state.userForm.password) {
|
||||
addNotification('请填写密码', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (state.editingUser) {
|
||||
await apiRequest(`/api/auth/users/${(state.editingUser as UserItem).id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
email: state.userForm.email,
|
||||
full_name: state.userForm.full_name || null,
|
||||
role_ids: state.userForm.role_ids,
|
||||
}),
|
||||
})
|
||||
addNotification('用户更新成功', 'success')
|
||||
} else {
|
||||
await apiRequest('/api/auth/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(state.userForm),
|
||||
})
|
||||
addNotification('用户创建成功', 'success')
|
||||
}
|
||||
state.showUserModal = false
|
||||
loadUsers()
|
||||
} catch (e) {
|
||||
const err = e as Error
|
||||
addNotification(err.message || '保存用户失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const deleteUser = async (user: UserItem) => {
|
||||
if (!confirm(`确定要删除用户 ${user.username} 吗?`)) return
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/auth/users/${user.id}`, { method: 'DELETE' })
|
||||
addNotification('用户已删除', 'success')
|
||||
loadUsers()
|
||||
} catch (e) {
|
||||
const err = e as Error
|
||||
addNotification(err.message || '删除用户失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const resetPassword = async (user: UserItem) => {
|
||||
const newPassword = prompt(`请输入 ${user.username} 的新密码:`)
|
||||
if (!newPassword || newPassword.length < 6) {
|
||||
addNotification('密码长度至少6位', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/auth/users/${user.id}/reset-password`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(newPassword),
|
||||
})
|
||||
addNotification('密码已重置', 'success')
|
||||
} catch (e) {
|
||||
const err = e as Error
|
||||
addNotification(err.message || '重置密码失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!store.user?.is_superuser) {
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
await loadRoles()
|
||||
loadUsers()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: () => import('@/modules/home/HomeView.vue') },
|
||||
{ path: '/login', component: () => import('@/modules/login/LoginView.vue') },
|
||||
{ path: '/users', component: () => import('@/modules/users/UsersView.vue') },
|
||||
{ path: '/moldinsight', component: () => import('@/modules/moldinsight/MoldInsightView.vue') },
|
||||
{ path: '/moldinsight/result/:taskId', component: () => import('@/modules/moldinsight/ResultView.vue') },
|
||||
{ path: '/inventory', component: () => import('@/modules/inventory/InventoryView.vue') },
|
||||
{ path: '/_design-system', component: () => import('@/design-system/DesignSystemView.vue') },
|
||||
{ path: '/_release', component: () => import('@/design-system/ReleaseView.vue') },
|
||||
],
|
||||
})
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const publicPages = ['/login', '/_design-system', '/_release']
|
||||
const authRequired = !publicPages.includes(to.path)
|
||||
const store = useAppStore()
|
||||
|
||||
if (authRequired && !store.user) {
|
||||
return next('/login')
|
||||
}
|
||||
|
||||
if (to.path === '/login' && store.user) {
|
||||
return next('/')
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
async function parseErrorMessage(response: Response): Promise<string> {
|
||||
try {
|
||||
const contentType = response.headers.get('content-type') || ''
|
||||
if (contentType.includes('application/json')) {
|
||||
const body = await response.json().catch(() => null)
|
||||
if (body?.detail) {
|
||||
if (Array.isArray(body.detail)) {
|
||||
const lines = body.detail
|
||||
.map((e: { loc?: string[]; msg?: string }) => {
|
||||
const loc = Array.isArray(e?.loc) ? e.loc.join('.') : ''
|
||||
const msg = e?.msg ? String(e.msg) : '校验失败'
|
||||
return loc ? `${loc}: ${msg}` : msg
|
||||
})
|
||||
.filter(Boolean)
|
||||
return lines.length ? lines.join('\n') : '请求校验失败'
|
||||
}
|
||||
if (typeof body.detail === 'object') return JSON.stringify(body.detail)
|
||||
return String(body.detail)
|
||||
}
|
||||
if (body?.message) return String(body.message)
|
||||
return '请求失败'
|
||||
}
|
||||
const text = await response.text().catch(() => '')
|
||||
const normalized = (text || '').trim()
|
||||
if (!normalized) return '请求失败'
|
||||
return normalized.length > 200 ? normalized.slice(0, 200) + '...' : normalized
|
||||
} catch {
|
||||
return '请求失败'
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiRequest<T = any>(url: string, options: RequestInit = {}): Promise<T> {
|
||||
const store = useAppStore()
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers as Record<string, string> || {}),
|
||||
}
|
||||
|
||||
if (store.token) {
|
||||
headers['Authorization'] = `Bearer ${store.token}`
|
||||
}
|
||||
|
||||
const response = await fetch(url, { ...options, headers })
|
||||
|
||||
if (response.status === 401) {
|
||||
store.user = null
|
||||
store.token = null
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
throw new Error('登录已过期,请重新登录')
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const message = await parseErrorMessage(response)
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
export function saveAuth(token: string, user: Record<string, unknown>): void {
|
||||
const store = useAppStore()
|
||||
store.token = token
|
||||
store.user = user as any
|
||||
localStorage.setItem('token', token)
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
}
|
||||
|
||||
export function clearAuth(): void {
|
||||
const store = useAppStore()
|
||||
store.token = null
|
||||
store.user = null
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
}
|
||||
|
||||
export function initAuth(): void {
|
||||
const store = useAppStore()
|
||||
const token = localStorage.getItem('token')
|
||||
const userStr = localStorage.getItem('user')
|
||||
|
||||
if (token && userStr) {
|
||||
try {
|
||||
store.token = token
|
||||
store.user = JSON.parse(userStr)
|
||||
} catch {
|
||||
clearAuth()
|
||||
}
|
||||
}
|
||||
store.initialized = true
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useAppStore, type Notification } from '@/stores/app'
|
||||
|
||||
let notificationId = 0
|
||||
const notificationDedup = new Map<string, number>()
|
||||
|
||||
export function addNotification(message: string, type: Notification['type'] = 'info'): void {
|
||||
const dedupKey = `${type}:${message}`
|
||||
const now = Date.now()
|
||||
const lastAt = notificationDedup.get(dedupKey) || 0
|
||||
if (now - lastAt < 2500) return
|
||||
notificationDedup.set(dedupKey, now)
|
||||
|
||||
const store = useAppStore()
|
||||
const id = ++notificationId
|
||||
const notification: Notification = { id, message, type, timestamp: new Date(), visible: true }
|
||||
store.notifications.push(notification)
|
||||
|
||||
setTimeout(() => {
|
||||
const notification = store.notifications.find(n => n.id === id)
|
||||
if (notification) {
|
||||
notification.visible = false
|
||||
setTimeout(() => {
|
||||
store.dismissNotification(id)
|
||||
}, 300)
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
export function handleApiError(error: unknown, context = ''): string {
|
||||
console.error(`API错误 [${context}]:`, error)
|
||||
const message = error instanceof Error ? error.message : '请求失败,请稍后重试'
|
||||
addNotification(message, 'error')
|
||||
return message
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
export function formatFileSize(bytes: number | null | undefined): string {
|
||||
if (!bytes || bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
export function formatNumber(num: number | null | undefined): string {
|
||||
if (num === null || num === undefined) return 'N/A'
|
||||
if (num >= 1_000_000) return (num / 1_000_000).toFixed(2) + 'M'
|
||||
if (num >= 1_000) return (num / 1_000).toFixed(2) + 'K'
|
||||
return num.toFixed ? num.toFixed(2) : String(num)
|
||||
}
|
||||
|
||||
export function formatDateTime(dateString: string | null | undefined): string {
|
||||
if (!dateString) return 'N/A'
|
||||
try {
|
||||
const date = new Date(dateString)
|
||||
if (isNaN(date.getTime())) return dateString
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`
|
||||
} catch {
|
||||
return dateString
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDate(dateString: string | null | undefined): string {
|
||||
if (!dateString) return 'N/A'
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('zh-CN')
|
||||
} catch {
|
||||
return dateString
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCurrency(amount: number | null | undefined): string {
|
||||
if (amount === null || amount === undefined) return '¥0.00'
|
||||
return '¥' + Number(amount).toFixed(2)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface Notification {
|
||||
id: number
|
||||
message: string
|
||||
type: 'info' | 'success' | 'error' | 'warning'
|
||||
timestamp: Date
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
export interface AppUser {
|
||||
id: number
|
||||
username: string
|
||||
full_name: string | null
|
||||
is_superuser: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
const user = ref<AppUser | null>(null)
|
||||
const token = ref<string | null>(null)
|
||||
const loading = ref(false)
|
||||
const notifications = ref<Notification[]>([])
|
||||
const initialized = ref(false)
|
||||
|
||||
function dismissNotification(id: number) {
|
||||
const index = notifications.value.findIndex(n => n.id === id)
|
||||
if (index > -1) notifications.value.splice(index, 1)
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
token,
|
||||
loading,
|
||||
notifications,
|
||||
initialized,
|
||||
dismissNotification,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"types": ["vite/client"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"ignoreDeprecations": "6.0",
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "esnext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: '../static',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
+18
@@ -226,6 +226,24 @@ async def users():
|
||||
return FileResponse(os.path.join(os.getcwd(), "static", "index.html"))
|
||||
|
||||
|
||||
@app.get("/moldinsight/result/{task_id:path}")
|
||||
async def moldinsight_result(task_id: str):
|
||||
from fastapi.responses import FileResponse
|
||||
return FileResponse(os.path.join(os.getcwd(), "static", "index.html"))
|
||||
|
||||
|
||||
@app.get("/_design-system")
|
||||
async def design_system():
|
||||
from fastapi.responses import FileResponse
|
||||
return FileResponse(os.path.join(os.getcwd(), "static", "index.html"))
|
||||
|
||||
|
||||
@app.get("/_release")
|
||||
async def release():
|
||||
from fastapi.responses import FileResponse
|
||||
return FileResponse(os.path.join(os.getcwd(), "static", "index.html"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
from config.settings import settings
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import{E as e,_ as t,t as n,y as r}from"./index-DkoUSxK0.js";var i={};function a(n,i){return e(),r(`div`,null,[...i[0]||=[t(`div`,{class:`page-header`},[t(`h1`,{class:`page-title`},`设计体系`),t(`p`,{class:`page-subtitle`},`迁移中...`)],-1)]])}var o=n(i,[[`render`,a]]);export{o as default};
|
||||
@@ -0,0 +1 @@
|
||||
.hero-section[data-v-a374362f]{text-align:center;padding:var(--space-16) 0}.hero-title[data-v-a374362f]{font-size:var(--text-4xl);font-weight:var(--font-bold);color:var(--text-primary);margin-bottom:var(--space-4)}.hero-subtitle[data-v-a374362f]{font-size:var(--text-lg);color:var(--text-tertiary);max-width:600px;margin:0 auto var(--space-8)}.features-grid[data-v-a374362f]{gap:var(--space-6);margin-top:var(--space-12);grid-template-columns:repeat(auto-fit,minmax(280px,1fr));display:grid}.feature-card[data-v-a374362f]{background:var(--bg-primary);border:1px solid var(--border-light);border-radius:var(--radius-xl);padding:var(--space-6);text-align:left}.feature-icon[data-v-a374362f]{background:var(--primary-50);border-radius:var(--radius-lg);width:40px;height:40px;color:var(--primary-500);font-size:var(--text-lg);margin-bottom:var(--space-4);justify-content:center;align-items:center;display:flex}.feature-title[data-v-a374362f]{font-size:var(--text-base);font-weight:var(--font-semibold);color:var(--text-primary);margin-bottom:var(--space-2)}.feature-desc[data-v-a374362f]{font-size:var(--text-sm);color:var(--text-tertiary);line-height:1.6}
|
||||
@@ -0,0 +1 @@
|
||||
import{E as e,b as t,t as n,y as r}from"./index-DkoUSxK0.js";var i={};function a(n,i){return e(),r(`div`,null,[...i[0]||=[t(`<div class="hero-section" data-v-a374362f><h1 class="hero-title" data-v-a374362f>Gemold 模具制造管理系统</h1><p class="hero-subtitle" data-v-a374362f>智能模具设计与制造一体化平台,从零件分析到模具生成,全流程数字化解决方案</p></div><div class="features-grid" data-v-a374362f><div class="feature-card" data-v-a374362f><div class="feature-icon" data-v-a374362f>◈</div><h3 class="feature-title" data-v-a374362f>MoldInsight</h3><p class="feature-desc" data-v-a374362f>智能模具分析引擎,支持 STP/STEP 文件解析、分模设计、CAM 刀路规划</p></div><div class="feature-card" data-v-a374362f><div class="feature-icon" data-v-a374362f>⊞</div><h3 class="feature-title" data-v-a374362f>进销存管理</h3><p class="feature-desc" data-v-a374362f>产品管理、采购销售、库存管理、财务管理,一站式 ERP 解决方案</p></div><div class="feature-card" data-v-a374362f><div class="feature-icon" data-v-a374362f>⚙</div><h3 class="feature-title" data-v-a374362f>AI 驱动</h3><p class="feature-desc" data-v-a374362f>AI 分型面检测、模具方案评分、智能设计建议,提升设计效率</p></div></div>`,2)]])}var o=n(i,[[`render`,a],[`__scopeId`,`data-v-a374362f`]]);export{o as default};
|
||||
@@ -0,0 +1 @@
|
||||
.mb-12[data-v-15993ef5]{margin-bottom:12px}.mb-16[data-v-15993ef5]{margin-bottom:16px}.mt-16[data-v-15993ef5]{margin-top:16px}.mr-8[data-v-15993ef5]{margin-right:8px}.text-secondary[data-v-15993ef5]{color:var(--text-secondary)}.flex-row-center[data-v-15993ef5]{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.w-260[data-v-15993ef5]{width:260px}.w-inline-120[data-v-15993ef5]{width:120px;display:inline-block}.w-inline-140[data-v-15993ef5]{width:140px;display:inline-block}.info-display[data-v-15993ef5]{background:var(--bg-secondary);padding:8px 12px}.form-group-spaced[data-v-15993ef5]{margin-bottom:var(--space-4)}.bom-add-btn[data-v-15993ef5]{margin-bottom:8px}.btn-disabled[data-v-15993ef5]{opacity:.5;cursor:not-allowed}.datetime-field[data-v-15993ef5]{cursor:pointer;position:relative}.air-datetime-input[data-v-15993ef5]{cursor:pointer;background:var(--bg-primary)}.datetime-native[data-v-15993ef5]{opacity:0;cursor:pointer;width:100%;position:absolute;inset:0}.datetime-field-icon[data-v-15993ef5]{pointer-events:none;font-size:1rem;position:absolute;top:50%;right:10px;transform:translateY(-50%)}.required[data-v-15993ef5]{color:var(--error);font-weight:var(--font-bold)}.sales-order-items[data-v-15993ef5]{flex-direction:column;gap:16px;display:flex}.sales-order-item[data-v-15993ef5]{border:1px solid var(--border-default);border-radius:var(--radius-md);overflow:hidden}.sales-order-item-header[data-v-15993ef5]{background:var(--bg-tertiary);border-bottom:1px solid var(--border-default);gap:0;display:flex}.sales-order-item-header .btn[data-v-15993ef5]{color:var(--text-secondary);background:0 0;border:none;border-radius:0}.sales-order-item-header .btn.active[data-v-15993ef5]{background:var(--primary-500);color:#fff}.sales-order-item-content[data-v-15993ef5]{padding:12px 16px}.sales-order-item-row[data-v-15993ef5]{flex-wrap:wrap;align-items:flex-end;gap:12px;display:flex}.sales-order-item-column[data-v-15993ef5]{flex:1;min-width:120px}.sales-order-item-actions[data-v-15993ef5]{align-items:flex-end;padding-bottom:4px;display:flex}.purchase-order-items[data-v-15993ef5]{flex-direction:column;gap:16px;display:flex}.purchase-order-item[data-v-15993ef5]{border:1px solid var(--border-default);border-radius:var(--radius-md);overflow:hidden}.purchase-order-item-content[data-v-15993ef5]{padding:12px 16px}.purchase-order-item-row[data-v-15993ef5]{flex-wrap:wrap;align-items:flex-end;gap:12px;display:flex}.purchase-order-item-column[data-v-15993ef5]{flex:1;min-width:120px}.purchase-order-item-actions[data-v-15993ef5]{align-items:flex-end;padding-bottom:4px;display:flex}.material-consumption-list[data-v-15993ef5]{border:1px solid var(--border-default);border-radius:var(--radius-md);overflow:hidden}.material-consumption-header[data-v-15993ef5]{background:var(--bg-tertiary);font-weight:var(--font-semibold);border-bottom:1px solid var(--border-default);display:flex}.material-consumption-row[data-v-15993ef5]{border-bottom:1px solid var(--border-light);display:flex}.material-consumption-row[data-v-15993ef5]:last-child{border-bottom:none}.material-consumption-total[data-v-15993ef5]{background:var(--bg-tertiary);font-weight:var(--font-bold);border-top:1px solid var(--border-default);display:flex}.material-consumption-item[data-v-15993ef5]{flex:1;min-width:80px;padding:8px 12px}.bom-section[data-v-15993ef5]{border-top:1px solid var(--border-default);margin-top:12px;padding-top:12px}.form-section[data-v-15993ef5]{margin-bottom:16px}.section-header[data-v-15993ef5]{justify-content:space-between;align-items:center;margin-bottom:12px;display:flex}.restock-modal[data-v-15993ef5]{max-width:900px}.restock-col-material[data-v-15993ef5]{flex:2}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
.login-container[data-v-792eed9c]{background:var(--bg-secondary);min-height:100vh;padding:var(--space-6);justify-content:center;align-items:center;display:flex}.login-card[data-v-792eed9c]{background:var(--bg-primary);border:1px solid var(--border-light);border-radius:var(--radius-xl);width:100%;max-width:400px;padding:var(--space-8)}.login-header[data-v-792eed9c]{text-align:center;margin-bottom:var(--space-8)}.login-logo[data-v-792eed9c]{background:var(--primary-500);border-radius:var(--radius-lg);color:#fff;width:48px;height:48px;font-size:var(--text-xl);font-weight:var(--font-bold);margin:0 auto var(--space-4);justify-content:center;align-items:center;display:flex}.login-title[data-v-792eed9c]{font-size:var(--text-2xl);font-weight:var(--font-bold);color:var(--text-primary);margin-bottom:var(--space-2)}.login-subtitle[data-v-792eed9c]{font-size:var(--text-sm);color:var(--text-tertiary)}
|
||||
@@ -0,0 +1 @@
|
||||
import{E as e,O as t,P as n,S as r,_ as i,a,f as o,k as s,l as c,m as l,n as u,o as d,t as f,v as p,y as m}from"./index-DkoUSxK0.js";var h={class:`login-container`},g={class:`login-card`},_={class:`form-group`},v={class:`form-group`},y={key:0,class:`form-error`},b=[`disabled`],x=f(r({__name:`LoginView`,setup(r){let f=c(),x=s({username:``,password:``,error:``,loading:!1}),S=async()=>{x.error=``,x.loading=!0;try{let e=await d(`/api/auth/login`,{method:`POST`,body:JSON.stringify({username:x.username,password:x.password})});a(e.access_token,e.user),u(`登录成功`,`success`),f.push(`/`)}catch(e){x.error=e.message||`登录失败`}finally{x.loading=!1}};return(r,a)=>(e(),m(`div`,h,[i(`div`,g,[a[4]||=i(`div`,{class:`login-header`},[i(`div`,{class:`login-logo`},`G`),i(`h1`,{class:`login-title`},`Gemold`),i(`p`,{class:`login-subtitle`},`模具制造管理系统`)],-1),i(`form`,{onSubmit:l(S,[`prevent`])},[i(`div`,_,[a[2]||=i(`label`,{class:`form-label`},`用户名`,-1),t(i(`input`,{"onUpdate:modelValue":a[0]||=e=>x.username=e,type:`text`,class:`form-input`,placeholder:`请输入用户名`,required:``},null,512),[[o,x.username]])]),i(`div`,v,[a[3]||=i(`label`,{class:`form-label`},`密码`,-1),t(i(`input`,{"onUpdate:modelValue":a[1]||=e=>x.password=e,type:`password`,class:`form-input`,placeholder:`请输入密码`,required:``},null,512),[[o,x.password]])]),x.error?(e(),m(`div`,y,n(x.error),1)):p(``,!0),i(`button`,{type:`submit`,class:`btn btn-primary w-full mt-4`,disabled:x.loading},n(x.loading?`登录中...`:`登录`),9,b)],32)])]))}}),[[`__scopeId`,`data-v-792eed9c`]]);export{x as default};
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{E as e,_ as t,t as n,y as r}from"./index-DkoUSxK0.js";var i={};function a(n,i){return e(),r(`div`,null,[...i[0]||=[t(`div`,{class:`page-header`},[t(`h1`,{class:`page-title`},`灰度发布与回滚`),t(`p`,{class:`page-subtitle`},`迁移中...`)],-1)]])}var o=n(i,[[`render`,a]]);export{o as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
function e(e){if(!e||e===0)return`0 B`;let t=1024,n=[`B`,`KB`,`MB`,`GB`],r=Math.floor(Math.log(e)/Math.log(t));return parseFloat((e/t**r).toFixed(2))+` `+n[r]}function t(e){return e==null?`N/A`:e>=1e6?(e/1e6).toFixed(2)+`M`:e>=1e3?(e/1e3).toFixed(2)+`K`:e.toFixed?e.toFixed(2):String(e)}function n(e){if(!e)return`N/A`;try{let t=new Date(e);return isNaN(t.getTime())?e:`${t.getFullYear()}/${String(t.getMonth()+1).padStart(2,`0`)}/${String(t.getDate()).padStart(2,`0`)} ${String(t.getHours()).padStart(2,`0`)}:${String(t.getMinutes()).padStart(2,`0`)}:${String(t.getSeconds()).padStart(2,`0`)}`}catch{return e}}function r(e){if(!e)return`N/A`;try{return new Date(e).toLocaleDateString(`zh-CN`)}catch{return e}}function i(e){return e==null?`¥0.00`:`¥`+Number(e).toFixed(2)}export{t as a,e as i,r as n,n as r,i as t};
|
||||
+5
-11
@@ -1,8 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Gemold - 模具制造管理系统</title>
|
||||
<script>
|
||||
(function () {
|
||||
@@ -61,8 +61,8 @@
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/air-datepicker@3.5.3/air-datepicker.css">
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260419-01">
|
||||
<script type="module" crossorigin src="/assets/index-DkoUSxK0.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CpCSYLtb.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
@@ -73,11 +73,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script src="https://unpkg.com/vue-router@4/dist/vue-router.global.prod.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/air-datepicker@3.5.3/air-datepicker.js"></script>
|
||||
<script src="/static/vue-app.js?v=20260506-01"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
-3476
File diff suppressed because it is too large
Load Diff
-5282
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user