Files
laser-data-hub/static/entry.js
T
broccoliandClaude Fable 5 edfb216561 激光材料数据平台 1.3.0
公开数据看板、四位 Key 录入面板、管理后台与三级权限、动态字段配置、
PostgreSQL/SQLite 双支持、数据库备份,以及本版新增的成品图上传与预览。

图片相关:
- 录入表单支持相册选图与手机端直接拍照,每条记录最多 6 张
- 浏览器内压缩到最长边 1600 并剥离 EXIF,入库统一为 JPG/PNG
- 二进制直接入库,现有备份自动覆盖图片,部署无需新增卷
- 详情页缩略图宫格与全屏 lightbox,公开看板同样可见

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 22:06:02 +08:00

63 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const $ = (selector, root=document) => root.querySelector(selector);
const $$ = (selector, root=document) => [...root.querySelectorAll(selector)];
let csrf=document.body.dataset.csrf;
let pendingPayload=null;
function escapeHtml(value){return String(value??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');}
function toast(message,error=false){const node=document.createElement('div');node.className=`toast ${error?'error':''}`;node.innerHTML=`<b>${error?'操作未完成':'保存成功'}</b><span>${escapeHtml(message)}</span>`;$('#toasts').append(node);setTimeout(()=>node.remove(),4500);}
class EntryExpired extends Error{constructor(){super('录入授权已过期');}}
async function api(url,options={}){
options.headers={...(options.headers||{}),'X-CSRF-Token':csrf};
if(options.json){options.body=JSON.stringify(options.json);options.headers['Content-Type']='application/json';delete options.json;}
const response=await fetch(url,options);
const data=await response.json().catch(()=>({})); // 反代返回 HTML 的 413/502 时不能让 JSON 解析吞掉真实状态码
if(response.status===401&&url!=='/api/entry/auth')throw new EntryExpired();
if(!response.ok)throw new Error(data.message||`请求失败(${response.status}`);
return data;
}
// 录入授权只有 30 分钟,而这张表有 30 多个必填项和最多 6 张照片,超时是常态。
// 过期后绝不能刷新页面:那会把填了半天的表单和已上传的图一起清空,只就地重新验证 Key。
async function reauthorize(){
const key=prompt('录入授权已过期(30 分钟)。请重新输入四位录入 Key,已填写的内容和已上传的图片都会保留:');
if(!key)return false;
try{
await api('/api/entry/auth',{method:'POST',json:{key:key.trim()}});
const session=await (await fetch('/api/entry/session')).json();
if(!session.authorized||!session.csrf_token)return false;
csrf=session.csrf_token; // cookie 换了,CSRF 是它的 HMAC,必须一起换掉
return true;
}catch(exception){toast(exception.message,true);return false;}
}
$('#keyForm')?.addEventListener('submit',async event=>{event.preventDefault();const button=event.target.querySelector('button'),error=$('#keyError');button.disabled=true;error.hidden=true;try{await api('/api/entry/auth',{method:'POST',json:{key:$('#entryKey').value}});location.reload();}catch(exception){error.textContent=exception.message;error.hidden=false;$('#entryKey').select();}finally{button.disabled=false;}});
$('#entryKey')?.addEventListener('input',event=>{event.target.value=event.target.value.replace(/\D/g,'').slice(0,4)});
const numeric=new Set(['apparent_density','uv_absorption_355nm','material_thickness','moisture_content','thermal_conductivity','initial_decomposition_temp','melting_vaporization_temp','specific_heat_capacity','carbon_residue_rate','surface_roughness','hardness','actual_output_power','display_current','scanning_speed','pulse_frequency','pulse_width','defocus_amount','scan_line_spacing','carbonized_edge_width','etching_depth','pattern_clarity_score','presentation_balance_score']);
function formPayload(form){const data={};for(const [key,value] of new FormData(form)){if(numeric.has(key))data[key]=value===''?null:Number(value);else if(['is_cut_through','is_fire_smolder'].includes(key))data[key]=value==='true';else data[key]=value;}data.custom_fields=DynamicFields.collect(form);Object.assign(data,ImageUpload.collect());return data;}
$('#publicRecordForm')?.addEventListener('submit',event=>{event.preventDefault();if(ImageUpload.pending()){toast('图片还在上传,请稍候再提交',true);return;}pendingPayload=formPayload(event.target);$('#confirmModal').hidden=false;setTimeout(()=>$('#confirmForm input').focus(),50);});
$('#confirmForm')?.addEventListener('submit',async event=>{
event.preventDefault();
const button=event.target.querySelector('button'),username=event.target.username.value.trim();
button.disabled=true;
const submit=()=>api('/api/entry/records',{method:'POST',json:{...pendingPayload,confirm_username:username}});
try{
let result;
try{result=await submit();}
catch(exception){
if(!(exception instanceof EntryExpired))throw exception;
if(!await reauthorize())throw new Error('未重新验证 Key,数据尚未保存;表单内容仍然保留');
result=await submit();
}
toast(`${result.data.experiment_id} 已写入数据库`);
$('#confirmModal').hidden=true;event.target.reset();$('#publicRecordForm').reset();ImageUpload.reset();
pendingPayload=null;window.scrollTo({top:0,behavior:'smooth'});
}catch(exception){toast(exception.message,true);}
finally{button.disabled=false;}
});
$$('[data-close]').forEach(node=>node.onclick=()=>{$('#confirmModal').hidden=true;});
$('#entryLogoutBtn')?.addEventListener('click',async()=>{try{await api('/api/entry/logout',{method:'POST'});}catch{}finally{location.reload();}});
if($('#publicRecordForm')){
DynamicFields.load().then(definitions=>DynamicFields.render($('#customFieldsGrid'),definitions)).catch(error=>toast(error.message,true));
ImageUpload.mount($('#recordImages'),{endpoint:'/api/entry/uploads/images',getCsrf:()=>csrf,onError:message=>toast(message,true),onUnauthorized:reauthorize});
}