// Independent admin extension. No hooks into React or enforcement data. const host = document.createElement('div'); host.id = 'plan-device-display-tools'; const style = document.createElement('style'); style.textContent = ` #plan-device-display-tools .entry { position:fixed; right:24px; bottom:24px; z-index:40; border:1px solid #cbd5e1; border-radius:8px; padding:9px 14px; color:#0f172a; background:#fff; cursor:pointer; box-shadow:0 2px 8px #0001; } #plan-device-display-dialog { width:min(680px,calc(100vw - 40px)); max-height:80vh; overflow:auto; border:1px solid #cbd5e1; border-radius:12px; padding:24px; background:#fff; color:#0f172a; } #plan-device-display-dialog::backdrop {background:#0007} #plan-device-display-dialog h2 {font-size:18px;font-weight:600;margin:0 0 8px} #plan-device-display-dialog .hint {font-size:13px;color:#64748b;margin:8px 0 18px} #plan-device-display-dialog fieldset {border:1px solid #e2e8f0;border-radius:8px;padding:12px;margin:12px 0} #plan-device-display-dialog legend {padding:0 6px;font-weight:600} #plan-device-display-dialog label {font-size:13px;display:block;margin-bottom:6px} #plan-device-display-dialog input {border:1px solid #cbd5e1;border-radius:6px;padding:8px;width:160px;margin-right:12px} #plan-device-display-dialog button {border:1px solid #cbd5e1;border-radius:6px;padding:7px 12px;cursor:pointer;background:#f8fafc} #plan-device-display-dialog .message {font-size:13px;min-height:20px;margin-top:6px} `; host.append(style); const entry = document.createElement('button'); entry.className='entry'; entry.type='button'; entry.textContent='展示设备数设置'; host.append(entry); const dialog = document.createElement('dialog'); dialog.id='plan-device-display-dialog'; const heading=document.createElement('h2'); heading.textContent='套餐展示设备数'; dialog.append(heading); const hint=document.createElement('p'); hint.className='hint'; hint.textContent='仅用于页面展示。留空沿用实际限制,0 展示“不限制”。保存不会修改实际设备限制。'; dialog.append(hint); const rows=document.createElement('div'); dialog.append(rows); const close=document.createElement('button'); close.type='button'; close.textContent='关闭'; close.addEventListener('click',()=>dialog.close()); dialog.append(close); host.append(dialog); document.body.append(host); function syncVisibility() { const allowed=location.hash.split('?')[0]==='#/finance/plan' && document.querySelector('#root h2'); entry.hidden=!allowed; if (!allowed && dialog.open) dialog.close(); } window.addEventListener('hashchange',syncVisibility); syncVisibility(); new MutationObserver(syncVisibility).observe(document.getElementById('root'),{childList:true,subtree:true}); async function api(action,payload) { let stored; try {stored=JSON.parse(localStorage.getItem('XBOARD_ACCESS_TOKEN')||'null');} catch {throw new Error('请重新登录后台');} if (!stored?.value || (stored.expire!=null && stored.expire<=Date.now())) throw new Error('请重新登录后台'); const base=new URL(window.settings?.base_url||'/',location.origin); if (base.origin!==location.origin) throw new Error('后台接口必须同源'); const prefix=base.pathname.replace(/\/$/,'')+'/api/v2/'+encodeURIComponent(window.settings.secure_path)+'/plan/'; const response=await fetch(prefix+action,{method:payload?'POST':'GET',credentials:'same-origin',headers:{Authorization:stored.value,'Content-Type':'application/json','Content-Language':'zh-CN'},...(payload?{body:JSON.stringify(payload)}:{})}); const data=await response.json(); if (!response.ok || data.status==='fail' || data.data===false) throw new Error(data.message||'保存失败'); return data.data; } entry.addEventListener('click',async()=>{ rows.textContent='正在读取套餐…'; dialog.showModal(); entry.disabled=true; try { const plans=await api('fetch'); rows.replaceChildren(); for (const plan of plans) { const fieldset=document.createElement('fieldset'); const legend=document.createElement('legend'); legend.textContent=plan.name; fieldset.append(legend); const actual=document.createElement('p'); actual.className='hint'; actual.textContent='实际设备限制:'+(plan.device_limit==null || Number(plan.device_limit)<=0?'不限制':plan.device_limit); fieldset.append(actual); const label=document.createElement('label'); label.textContent='展示设备数'; label.htmlFor='display-device-'+plan.id; fieldset.append(label); const input=document.createElement('input'); input.id=label.htmlFor; input.type='number'; input.min='0'; input.max='4294967295'; input.step='1'; input.placeholder='留空沿用实际限制'; input.value=plan.display_device_limit==null?'':String(plan.display_device_limit); fieldset.append(input); const save=document.createElement('button'); save.type='button'; save.textContent='保存 '+plan.name; fieldset.append(save); const message=document.createElement('p'); message.className='message'; message.setAttribute('role','status'); fieldset.append(message); save.addEventListener('click',async()=>{ const value=input.value.trim(); const number=value===''?null:Number(value); if (number!==null && (!Number.isInteger(number)||number<0||number>4294967295)) {message.textContent='请填写非负整数,或留空';return;} save.disabled=true; message.textContent=''; try {await api('update',{id:plan.id,display_device_limit:number}); message.textContent='已保存;实际设备限制未修改。';} catch(error){message.textContent=error.message||'保存失败';} finally{save.disabled=false;} }); rows.append(fieldset); } } catch(error){rows.textContent=error.message||'读取失败';} finally{entry.disabled=false;} });