激光材料数据平台 1.3.0
公开数据看板、四位 Key 录入面板、管理后台与三级权限、动态字段配置、 PostgreSQL/SQLite 双支持、数据库备份,以及本版新增的成品图上传与预览。 图片相关: - 录入表单支持相册选图与手机端直接拍照,每条记录最多 6 张 - 浏览器内压缩到最长边 1600 并剥离 EXIF,入库统一为 JPG/PNG - 二进制直接入库,现有备份自动覆盖图片,部署无需新增卷 - 详情页缩略图宫格与全屏 lightbox,公开看板同样可见 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
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,'"').replace(/'/g,''');}
|
||||
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});
|
||||
}
|
||||
Reference in New Issue
Block a user