x
This commit is contained in:
@@ -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');
|
||||
Reference in New Issue
Block a user