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,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''');}
function toast(message,error=false){const node=document.createElement('div');node.className=`toast ${error?'error':''}`;node.innerHTML=`${error?'操作未完成':'保存成功'}${escapeHtml(message)}`;$('#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});
}