This commit is contained in:
2026-05-11 16:31:56 +08:00
parent c9672d9685
commit af355f7f29
+188
View File
@@ -0,0 +1,188 @@
const { createApp, ref, reactive, computed, onMounted } = Vue;
const { createRouter, createWebHashHistory } = VueRouter;
function api(method, url, body) {
const opts = { method, headers: { 'Content-Type': 'application/json' } };
const t = localStorage.getItem('gemold_token');
if (t) opts.headers.Authorization = 'Bearer ' + t;
if (body) opts.body = JSON.stringify(body);
return fetch(url, opts).then(r => {
if (r.status === 401) { localStorage.removeItem('gemold_token'); localStorage.removeItem('gemold_user'); router.push('/login'); }
if (!r.ok) return r.json().then(e => Promise.reject(e));
const ct = r.headers.get('content-type') || '';
return ct.includes('application/json') ? r.json() : r.text();
});
}
function apiForm(url, form) {
const opts = { method: 'POST', body: form };
const t = localStorage.getItem('gemold_token');
if (t) opts.headers = { Authorization: 'Bearer ' + t };
return fetch(url, opts).then(r => {
if (r.status === 401) { localStorage.removeItem('gemold_token'); localStorage.removeItem('gemold_user'); router.push('/login'); }
if (!r.ok) return r.json().then(e => Promise.reject(e));
return r.json();
});
}
const store = reactive({
user: JSON.parse(localStorage.getItem('gemold_user') || 'null'),
tasks: [],
uploading: false,
async login(username, password) {
const form = new FormData();
form.append('username', username);
form.append('password', password);
const r = await apiForm('/api/auth/login', form);
store.user = r.user;
localStorage.setItem('gemold_token', r.access_token);
localStorage.setItem('gemold_user', JSON.stringify(r.user));
return r;
},
logout() {
localStorage.removeItem('gemold_token');
localStorage.removeItem('gemold_user');
store.user = null;
store.tasks = [];
router.push('/login');
},
async fetchTasks() {
const r = await api('GET', '/api/debug/tasks');
store.tasks = Object.values(r.tasks || {}).sort((a, b) => {
const da = a.created_at || a.upload_time || '';
const db = b.created_at || b.upload_time || '';
return db.localeCompare(da);
});
},
async uploadFile(file, material) {
store.uploading = true;
try {
const form = new FormData();
form.append('file', file);
if (material) form.append('material', material);
const r = await apiForm('/api/upload', form);
await store.fetchTasks();
return r;
} finally {
store.uploading = false;
}
},
});
const LoginPage = {
setup() {
const username = ref('');
const password = ref('');
const error = ref('');
const loading = ref(false);
const doLogin = async () => {
loading.value = true; error.value = '';
try { await store.login(username.value, password.value); router.push('/dashboard'); }
catch (e) { error.value = e.detail || '登录失败'; }
finally { loading.value = false; }
};
return { username, password, error, loading, doLogin };
},
template: `
<div style="display:flex;align-items:center;justify-content:center;min-height:100vh;background:linear-gradient(135deg,#1a1a2e 0%,#16213e 100%)">
<div style="background:#fff;border-radius:12px;padding:40px;width:380px;box-shadow:0 20px 60px rgba(0,0,0,.3)">
<h2 style="text-align:center;margin-bottom:4px;color:#1a1a2e">Gemold</h2>
<p style="text-align:center;color:#888;margin-bottom:28px;font-size:14px">模具制造管理系统</p>
<div v-if="error" style="background:#fff0f0;color:#e74c3c;padding:10px;border-radius:6px;margin-bottom:16px;font-size:13px">{{ error }}</div>
<input v-model="username" @keyup.enter="doLogin" placeholder="用户名" style="width:100%;padding:12px;margin-bottom:12px;border:1px solid #ddd;border-radius:6px;font-size:14px;box-sizing:border-box" autocomplete="username">
<input v-model="password" @keyup.enter="doLogin" type="password" placeholder="密码" style="width:100%;padding:12px;margin-bottom:20px;border:1px solid #ddd;border-radius:6px;font-size:14px;box-sizing:border-box" autocomplete="current-password">
<button @click="doLogin" :disabled="loading" style="width:100%;padding:12px;background:#2980b9;color:#fff;border:none;border-radius:6px;font-size:15px;cursor:pointer;font-weight:600">{{ loading ? '登录中...' : '登 录' }}</button>
</div>
</div>`
};
const DashboardPage = {
setup() {
const material = ref('ABS');
const fileInput = ref(null);
const triggerUpload = () => fileInput.value?.click();
const handleFile = async (e) => {
const f = e.target.files[0];
if (!f) return;
await store.uploadFile(f, material.value);
e.target.value = '';
};
onMounted(() => { store.fetchTasks(); const t = setInterval(() => store.fetchTasks(), 10000); onUnmounted(() => clearInterval(t)); });
const { onUnmounted } = Vue;
const formatSize = (s) => s ? (s > 1048576 ? (s/1048576).toFixed(1)+' MB' : (s/1024).toFixed(1)+' KB') : '-';
const statusLabel = (s) => ({ processing: '处理中', completed: '已完成', failed: '失败', pending: '等待中' }[s] || (s || '未知'));
const statusStyle = (s) => {
if (s === 'completed') return { background: '#d4edda', color: '#155724' };
if (s === 'failed') return { background: '#f8d7da', color: '#721c24' };
return { background: '#d1ecf1', color: '#0c5460' };
};
return { tasks: computed(() => store.tasks), uploading: computed(() => store.uploading), material, fileInput, triggerUpload, handleFile, formatSize, statusLabel, statusStyle };
},
template: `
<div style="max-width:900px;margin:0 auto;padding:24px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px">
<h1 style="margin:0;font-size:22px;color:#1a1a2e">模具分析</h1>
<div>
<span style="margin-right:16px;color:#555">{{ store.user?.username || '' }}</span>
<button @click="store.logout" style="padding:6px 16px;background:#e74c3c;color:#fff;border:none;border-radius:4px;cursor:pointer">退出</button>
</div>
</div>
<div style="background:#f8f9fa;border-radius:10px;padding:24px;margin-bottom:24px;border:2px dashed #ccc">
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
<span style="font-weight:600;color:#333">上传 STP 文件:</span>
<select v-model="material" style="padding:8px 12px;border:1px solid #ccc;border-radius:4px">
<option value="ABS">ABS</option><option value="PP">PP</option><option value="PC">PC</option><option value="PA">PA</option><option value="POM">POM</option><option value="PMMA">PMMA</option><option value="PBT">PBT</option><option value="PE">PE</option><option value="AlSi10Mg">AlSi10Mg (泡沫铝)</option><option value="Pure Al Foam">Pure Al Foam</option>
</select>
<input ref="fileInput" type="file" accept=".stp,.step" @change="handleFile" style="display:none">
<button @click="triggerUpload" :disabled="uploading" style="padding:8px 20px;background:#2980b9;color:#fff;border:none;border-radius:4px;font-weight:600;cursor:pointer">{{ uploading ? '上传中...' : '选择文件上传' }}</button>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<h3 style="margin:0;color:#333">处理任务 ({{ tasks.length }})</h3>
<button @click="store.fetchTasks()" style="padding:4px 12px;background:#eee;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:12px">刷新</button>
</div>
<div v-if="tasks.length === 0" style="text-align:center;padding:60px;color:#999">暂无任务,请上传 STP 文件开始分析</div>
<div v-for="t in tasks" :key="t.task_id" style="background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:16px;margin-bottom:12px">
<div style="display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:8px">
<div>
<strong style="font-size:14px">{{ t.filename || 'unknown' }}</strong>
<span style="margin-left:10px;font-size:11px;padding:2px 8px;border-radius:10px" :style="statusStyle(t.status)">{{ statusLabel(t.status) }}</span>
<span v-if="t.progress != null" style="margin-left:8px;font-size:12px;color:#888">{{ t.progress }}%</span>
</div>
<div style="font-size:11px;color:#999">
<span v-if="t.file_size">{{ formatSize(t.file_size) }}</span>
<span v-if="t.created_at" style="margin-left:8px">{{ t.created_at }}</span>
</div>
</div>
<div v-if="t.current_step && t.status === 'processing'" style="margin-top:6px;font-size:12px;color:#666">{{ t.current_step }}</div>
<div v-if="t.status === 'failed' && t.error" style="margin-top:8px;padding:8px;background:#fff5f5;border-radius:4px;color:#c0392b;font-size:12px">{{ t.error }}</div>
<div v-if="t.status === 'completed' && t.html_file" style="margin-top:8px">
<a :href="t.html_file" target="_blank" style="color:#2980b9;text-decoration:none;font-size:13px;font-weight:500">查看分析结果</a>
</div>
<div v-if="t.llm_report" style="margin-top:8px;padding:12px;background:#fef9e7;border-radius:6px;font-size:13px;line-height:1.6;max-height:200px;overflow-y:auto;white-space:pre-wrap">{{ t.llm_report }}</div>
</div>
</div>`
};
const routes = [
{ path: '/login', component: LoginPage },
{ path: '/dashboard', component: DashboardPage, meta: { requiresAuth: true } },
{ path: '/:pathMatch(.*)*', redirect: '/dashboard' },
];
const router = createRouter({ history: createWebHashHistory(), routes });
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !store.user) return next('/login');
if (to.path === '/login' && store.user) return next('/dashboard');
next();
});
const app = createApp({ template: '<router-view />' });
app.use(router);
app.mount('#app');