From 13117587c86aa49dafad3467b04d90469f4de2ab Mon Sep 17 00:00:00 2001 From: hellcat Date: Sun, 11 Oct 2026 09:51:27 +0800 Subject: [PATCH] refactor(admin): split mail settings into dedicated tabs --- public/admin-custom/mail-flow.js | 60 +++++++++++++++++++++++++------- resources/js/admin/mail-flow.js | 60 +++++++++++++++++++++++++------- 2 files changed, 96 insertions(+), 24 deletions(-) diff --git a/public/admin-custom/mail-flow.js b/public/admin-custom/mail-flow.js index 382f87b..312dd2b 100644 --- a/public/admin-custom/mail-flow.js +++ b/public/admin-custom/mail-flow.js @@ -18,6 +18,8 @@ const groups=[ ['常规邮箱收件与自动回复', [['inbound_enabled','启用收件','checkbox'],['imap_host','IMAP 主机','text'],['imap_port','IMAP TLS 端口','number'],['imap_username','IMAP 用户名','text'],['imap_password','IMAP 密码','password'],['imap_folder','收件文件夹','text'],['auto_reply_enabled','自动确认收件(按发件地址限频)','checkbox'],['auto_reply_subject','自动回复标题','text'],['auto_reply_text','自动回复内容','textarea']]], ['每天八点汇报', [['report_enabled','启用 master 汇报','checkbox'],['master_url','Master 基础地址(HTTPS)','url'],['master_bot_code','Master 机器人代号','text'],['master_chat_code','Master 接收会话代号','text']]], ]; +let selectedTab=null; +const tabNames={resend:'Resend 与档位',inbox:'收件与自动回复',report:'用量汇报'}; function control(key,label,type,value,configured){ const wrap=el('div',null,'space-y-2'); const id='mail-flow-'+key; @@ -47,32 +49,33 @@ async function render(host){ const data=await api('mailFlowStatus');if(!host.isConnected)return; if(data.automatic_mail_level==null)throw new Error('自动邮件档位暂不可读取,请稍后重试'); const form=el('form',null,'space-y-6');const inputs={}; - const policy=el('section',null,'space-y-4');policy.append(el('h3','自动邮件档位','text-lg font-semibold')); + const policy=el('section',null,'space-y-4');policy.dataset.mailTab='resend';policy.append(el('h3','自动邮件档位','text-lg font-semibold')); const tierWrap=el('div',null,'space-y-2');const label=el('label','自动邮件档位','font-medium text-base');label.htmlFor='mail-flow-tier'; const tier=el('select');tier.id='mail-flow-tier';tier.name='automatic_mail_level';tier.className=fieldClass; for(const [value,text] of [[1,'一档:保持原有提醒'],[2,'二档:仅套餐到期时提醒'],[3,'三档:关闭自动批量提醒']]){const option=el('option',text);option.value=String(value);tier.append(option);} tier.value=String(data.automatic_mail_level);tierWrap.append(label,tier,el('p','管理员人工群发、验证码、登录邮件、工单回应通知和来信自动回复始终允许。到期通知须同时满足后台允许和用户未关闭提醒。','text-[0.8rem] text-muted-foreground'));policy.append(tierWrap);form.append(policy); for(const [title,fields] of groups){ - const section=el('section',null,'space-y-4 border-t pt-6');section.append(el('h3',title==='发送渠道'?'Resend 与邮件发送渠道':title,'text-lg font-semibold')); + const section=el('section',null,'space-y-4');section.dataset.mailTab=title==='发送渠道'?'resend':title==='每天八点汇报'?'report':'inbox';section.append(el('h3',title==='发送渠道'?'Resend 与邮件发送渠道':title,'text-lg font-semibold')); if(title==='发送渠道')section.append(el('p','Resend 额度不足时使用原常规邮箱。其他通知仍走常规邮箱;密钥留空保留现有值。','text-sm text-muted-foreground')); if(title==='常规邮箱收件与自动回复')section.append(el('p','收件及自动回复使用常规邮箱。每个发件地址北京时间每天仅回复一次,无需绑定网站账号。','text-sm text-muted-foreground')); if(title==='每天八点汇报')section.append(el('p','北京时间每天早上八点汇报;机器人和接收会话代号留空使用 master 默认配置。','text-sm text-muted-foreground')); - for(const [key,text,type] of fields){const item=control(key,text,type,data.settings[key],data.settings[key+'_configured']);inputs[key]=item;section.append(item.wrap);} + for(const [key,text,type] of fields){const item=control(key,text,type,data.settings[key],data.settings[key+'_configured']);item.tab=section.dataset.mailTab;inputs[key]=item;section.append(item.wrap);} form.append(section); } const message=el('p',null,'text-sm text-muted-foreground');message.setAttribute('role','status'); const save=el('button','保存邮件渠道设置',buttonClass);save.type='submit';form.append(message,save); form.addEventListener('submit',async event=>{ event.preventDefault();if(!form.reportValidity())return; - if((inputs.enabled.input.checked||Number(tier.value)===2)&&!data.database_ready){message.textContent='数据库尚未就绪,暂不可启用。';return;} - if(inputs.inbound_enabled.input.checked&&!data.settings.inbound_enabled&&!data.imap_available){message.textContent='收件需要安装 PHP IMAP 扩展。';return;} - const values={};for(const [key,{input,type}] of Object.entries(inputs)){ + if(selectedTab==='resend'&&(inputs.enabled.input.checked||Number(tier.value)===2)&&!data.database_ready){message.textContent='数据库尚未就绪,暂不可启用。';return;} + if(selectedTab==='inbox'&&inputs.inbound_enabled.input.checked&&!data.settings.inbound_enabled&&!data.imap_available){message.textContent='收件需要安装 PHP IMAP 扩展。';return;} + const values={};for(const [key,{input,type,tab}] of Object.entries(inputs)){ + if(tab!==selectedTab)continue; if(type==='checkbox')values[key]=input.checked; else if(type==='password'&&!input.value)continue; else values[key]=type==='number'?Number(input.value):input.value; } save.disabled=true;message.textContent='正在保存…'; - try{await api('save',{mail_flow:values,automatic_mail_level:Number(tier.value)});message.textContent='邮件渠道设置已保存。';} + try{await api('save',{mail_flow:values,...(selectedTab==='resend'?{automatic_mail_level:Number(tier.value)}:{})});message.textContent='邮件渠道设置已保存。';} catch(error){message.textContent=error.message;} finally{save.disabled=false;} }); @@ -83,15 +86,48 @@ async function render(host){ const view=el('button','查看来信:'+incoming.email,buttonClass+' mt-3');view.type='button';const content=el('pre',null,'text-sm whitespace-pre-wrap mt-3');history.append(view,content); view.addEventListener('click',async()=>{view.disabled=true;try{const mail=await api('mailFlowIncoming?id='+encodeURIComponent(incoming.id));content.textContent=mail.subject+'\n\n'+mail.text;}catch(error){content.textContent=error.message;}finally{view.disabled=false;}}); } - history.append(el('pre','最近汇报:\n'+(data.reports.map(r=>r.report_date+' · '+r.status).join('\n')||'暂无汇报记录'),'text-xs whitespace-pre-wrap mt-4'));host.append(history); + history.append(el('pre','最近汇报:\n'+(data.reports.map(r=>r.report_date+' · '+r.status).join('\n')||'暂无汇报记录'),'text-xs whitespace-pre-wrap mt-4'));history.dataset.mailTab='inbox';host.append(history);syncTabs(); }catch(error){if(!host.isConnected)return;host.replaceChildren(el('p',error.message,'text-sm text-destructive'));const retry=el('button','重新读取',buttonClass+' mt-3');retry.type='button';retry.addEventListener('click',()=>{host.replaceChildren();render(host);});host.append(retry);} } +const wired=new WeakSet(); +const nativeStates=new WeakMap(); +function syncTabs(){ + const host=document.getElementById('mail-flow-settings'); + if(!host)return; + const container=host.parentElement; + for(const panel of Array.from(container.children).filter(e=>e.getAttribute('role')==='tabpanel'&&e!==host)){ + panel.hidden=!!selectedTab||panel.dataset.state!=='active'; + } + host.hidden=!selectedTab; + if(selectedTab){host.setAttribute('aria-labelledby','mail-tab-'+selectedTab);} + for(const tab of container.querySelectorAll('[role="tab"]')){ + if(tab.dataset.mailTab){const active=tab.dataset.mailTab===selectedTab;tab.dataset.state=active?'active':'inactive';tab.setAttribute('aria-selected',String(active));tab.tabIndex=active?0:-1;} + else if(selectedTab){if(!nativeStates.has(tab))nativeStates.set(tab,tab.dataset.state);tab.dataset.state='inactive';tab.setAttribute('aria-selected','false');tab.tabIndex=-1;} + else{if(nativeStates.has(tab)){tab.dataset.state=nativeStates.get(tab);nativeStates.delete(tab);}tab.setAttribute('aria-selected',String(tab.dataset.state==='active'));tab.tabIndex=tab.dataset.state==='active'?0:-1;} + } + for(const section of host.querySelectorAll('[data-mail-tab]')){section.hidden=section.dataset.mailTab!==selectedTab;section.disabled=section.hidden;for(const field of section.querySelectorAll('input,select,textarea'))field.disabled=section.hidden;} +} function mount(){ const current=document.getElementById('mail-flow-settings'); - const panel=location.hash.split('?')[0]==='#/config/system/email'?document.querySelector('#root [role="tabpanel"][data-state="active"]'):null; - if(!panel?.querySelector('input[name="email_host"]')){current?.remove();return;} - if(current?.parentElement===panel)return; - current?.remove();const host=el('section',null,'border-t mt-6 pt-6');host.id='mail-flow-settings';panel.append(host);render(host); + const list=location.hash.split('?')[0]==='#/config/system/email'?document.querySelector('#root [role="tablist"]'):null; + if(!list){current?.remove();selectedTab=null;return;} + const native=Array.from(list.children).filter(e=>!e.dataset.mailTab&&e.getAttribute('role')==='tab'); + if(!native.some(e=>e.textContent==='基本设置'))return; + for(const button of native){ + if(wired.has(button))continue;wired.add(button); + button.addEventListener('click',()=>{selectedTab=null;for(const original of native){nativeStates.delete(original);original.dataset.state=original===button?'active':'inactive';}schedule();}); + } + list.classList.add('flex-wrap','h-auto'); + for(const [key,text] of Object.entries(tabNames)){ + if(list.querySelector('[data-mail-tab="'+key+'"]'))continue; + const button=el('button',text,native[0].className);button.type='button';button.id='mail-tab-'+key;button.dataset.mailTab=key;button.setAttribute('role','tab');button.setAttribute('aria-controls','mail-flow-settings'); + button.addEventListener('click',()=>{selectedTab=key;syncTabs();});list.append(button); + } + if(!wired.has(list)){ + wired.add(list);list.addEventListener('keydown',event=>{if(!['ArrowLeft','ArrowRight','Home','End'].includes(event.key))return;const tabs=Array.from(list.querySelectorAll('[role="tab"]'));const i=tabs.indexOf(document.activeElement);if(i<0)return;event.preventDefault();const next=event.key==='Home'?0:event.key==='End'?tabs.length-1:(i+(event.key==='ArrowRight'?1:-1)+tabs.length)%tabs.length;tabs[next].focus();tabs[next].click();}); + } + if(current?.parentElement===list.parentElement){syncTabs();return;} + current?.remove();const host=el('section',null,'mt-4');host.id='mail-flow-settings';host.setAttribute('role','tabpanel');list.parentElement.append(host);syncTabs();render(host); } let scheduled=false; const schedule=()=>{if(scheduled)return;scheduled=true;requestAnimationFrame(()=>{scheduled=false;mount();});}; diff --git a/resources/js/admin/mail-flow.js b/resources/js/admin/mail-flow.js index 382f87b..312dd2b 100644 --- a/resources/js/admin/mail-flow.js +++ b/resources/js/admin/mail-flow.js @@ -18,6 +18,8 @@ const groups=[ ['常规邮箱收件与自动回复', [['inbound_enabled','启用收件','checkbox'],['imap_host','IMAP 主机','text'],['imap_port','IMAP TLS 端口','number'],['imap_username','IMAP 用户名','text'],['imap_password','IMAP 密码','password'],['imap_folder','收件文件夹','text'],['auto_reply_enabled','自动确认收件(按发件地址限频)','checkbox'],['auto_reply_subject','自动回复标题','text'],['auto_reply_text','自动回复内容','textarea']]], ['每天八点汇报', [['report_enabled','启用 master 汇报','checkbox'],['master_url','Master 基础地址(HTTPS)','url'],['master_bot_code','Master 机器人代号','text'],['master_chat_code','Master 接收会话代号','text']]], ]; +let selectedTab=null; +const tabNames={resend:'Resend 与档位',inbox:'收件与自动回复',report:'用量汇报'}; function control(key,label,type,value,configured){ const wrap=el('div',null,'space-y-2'); const id='mail-flow-'+key; @@ -47,32 +49,33 @@ async function render(host){ const data=await api('mailFlowStatus');if(!host.isConnected)return; if(data.automatic_mail_level==null)throw new Error('自动邮件档位暂不可读取,请稍后重试'); const form=el('form',null,'space-y-6');const inputs={}; - const policy=el('section',null,'space-y-4');policy.append(el('h3','自动邮件档位','text-lg font-semibold')); + const policy=el('section',null,'space-y-4');policy.dataset.mailTab='resend';policy.append(el('h3','自动邮件档位','text-lg font-semibold')); const tierWrap=el('div',null,'space-y-2');const label=el('label','自动邮件档位','font-medium text-base');label.htmlFor='mail-flow-tier'; const tier=el('select');tier.id='mail-flow-tier';tier.name='automatic_mail_level';tier.className=fieldClass; for(const [value,text] of [[1,'一档:保持原有提醒'],[2,'二档:仅套餐到期时提醒'],[3,'三档:关闭自动批量提醒']]){const option=el('option',text);option.value=String(value);tier.append(option);} tier.value=String(data.automatic_mail_level);tierWrap.append(label,tier,el('p','管理员人工群发、验证码、登录邮件、工单回应通知和来信自动回复始终允许。到期通知须同时满足后台允许和用户未关闭提醒。','text-[0.8rem] text-muted-foreground'));policy.append(tierWrap);form.append(policy); for(const [title,fields] of groups){ - const section=el('section',null,'space-y-4 border-t pt-6');section.append(el('h3',title==='发送渠道'?'Resend 与邮件发送渠道':title,'text-lg font-semibold')); + const section=el('section',null,'space-y-4');section.dataset.mailTab=title==='发送渠道'?'resend':title==='每天八点汇报'?'report':'inbox';section.append(el('h3',title==='发送渠道'?'Resend 与邮件发送渠道':title,'text-lg font-semibold')); if(title==='发送渠道')section.append(el('p','Resend 额度不足时使用原常规邮箱。其他通知仍走常规邮箱;密钥留空保留现有值。','text-sm text-muted-foreground')); if(title==='常规邮箱收件与自动回复')section.append(el('p','收件及自动回复使用常规邮箱。每个发件地址北京时间每天仅回复一次,无需绑定网站账号。','text-sm text-muted-foreground')); if(title==='每天八点汇报')section.append(el('p','北京时间每天早上八点汇报;机器人和接收会话代号留空使用 master 默认配置。','text-sm text-muted-foreground')); - for(const [key,text,type] of fields){const item=control(key,text,type,data.settings[key],data.settings[key+'_configured']);inputs[key]=item;section.append(item.wrap);} + for(const [key,text,type] of fields){const item=control(key,text,type,data.settings[key],data.settings[key+'_configured']);item.tab=section.dataset.mailTab;inputs[key]=item;section.append(item.wrap);} form.append(section); } const message=el('p',null,'text-sm text-muted-foreground');message.setAttribute('role','status'); const save=el('button','保存邮件渠道设置',buttonClass);save.type='submit';form.append(message,save); form.addEventListener('submit',async event=>{ event.preventDefault();if(!form.reportValidity())return; - if((inputs.enabled.input.checked||Number(tier.value)===2)&&!data.database_ready){message.textContent='数据库尚未就绪,暂不可启用。';return;} - if(inputs.inbound_enabled.input.checked&&!data.settings.inbound_enabled&&!data.imap_available){message.textContent='收件需要安装 PHP IMAP 扩展。';return;} - const values={};for(const [key,{input,type}] of Object.entries(inputs)){ + if(selectedTab==='resend'&&(inputs.enabled.input.checked||Number(tier.value)===2)&&!data.database_ready){message.textContent='数据库尚未就绪,暂不可启用。';return;} + if(selectedTab==='inbox'&&inputs.inbound_enabled.input.checked&&!data.settings.inbound_enabled&&!data.imap_available){message.textContent='收件需要安装 PHP IMAP 扩展。';return;} + const values={};for(const [key,{input,type,tab}] of Object.entries(inputs)){ + if(tab!==selectedTab)continue; if(type==='checkbox')values[key]=input.checked; else if(type==='password'&&!input.value)continue; else values[key]=type==='number'?Number(input.value):input.value; } save.disabled=true;message.textContent='正在保存…'; - try{await api('save',{mail_flow:values,automatic_mail_level:Number(tier.value)});message.textContent='邮件渠道设置已保存。';} + try{await api('save',{mail_flow:values,...(selectedTab==='resend'?{automatic_mail_level:Number(tier.value)}:{})});message.textContent='邮件渠道设置已保存。';} catch(error){message.textContent=error.message;} finally{save.disabled=false;} }); @@ -83,15 +86,48 @@ async function render(host){ const view=el('button','查看来信:'+incoming.email,buttonClass+' mt-3');view.type='button';const content=el('pre',null,'text-sm whitespace-pre-wrap mt-3');history.append(view,content); view.addEventListener('click',async()=>{view.disabled=true;try{const mail=await api('mailFlowIncoming?id='+encodeURIComponent(incoming.id));content.textContent=mail.subject+'\n\n'+mail.text;}catch(error){content.textContent=error.message;}finally{view.disabled=false;}}); } - history.append(el('pre','最近汇报:\n'+(data.reports.map(r=>r.report_date+' · '+r.status).join('\n')||'暂无汇报记录'),'text-xs whitespace-pre-wrap mt-4'));host.append(history); + history.append(el('pre','最近汇报:\n'+(data.reports.map(r=>r.report_date+' · '+r.status).join('\n')||'暂无汇报记录'),'text-xs whitespace-pre-wrap mt-4'));history.dataset.mailTab='inbox';host.append(history);syncTabs(); }catch(error){if(!host.isConnected)return;host.replaceChildren(el('p',error.message,'text-sm text-destructive'));const retry=el('button','重新读取',buttonClass+' mt-3');retry.type='button';retry.addEventListener('click',()=>{host.replaceChildren();render(host);});host.append(retry);} } +const wired=new WeakSet(); +const nativeStates=new WeakMap(); +function syncTabs(){ + const host=document.getElementById('mail-flow-settings'); + if(!host)return; + const container=host.parentElement; + for(const panel of Array.from(container.children).filter(e=>e.getAttribute('role')==='tabpanel'&&e!==host)){ + panel.hidden=!!selectedTab||panel.dataset.state!=='active'; + } + host.hidden=!selectedTab; + if(selectedTab){host.setAttribute('aria-labelledby','mail-tab-'+selectedTab);} + for(const tab of container.querySelectorAll('[role="tab"]')){ + if(tab.dataset.mailTab){const active=tab.dataset.mailTab===selectedTab;tab.dataset.state=active?'active':'inactive';tab.setAttribute('aria-selected',String(active));tab.tabIndex=active?0:-1;} + else if(selectedTab){if(!nativeStates.has(tab))nativeStates.set(tab,tab.dataset.state);tab.dataset.state='inactive';tab.setAttribute('aria-selected','false');tab.tabIndex=-1;} + else{if(nativeStates.has(tab)){tab.dataset.state=nativeStates.get(tab);nativeStates.delete(tab);}tab.setAttribute('aria-selected',String(tab.dataset.state==='active'));tab.tabIndex=tab.dataset.state==='active'?0:-1;} + } + for(const section of host.querySelectorAll('[data-mail-tab]')){section.hidden=section.dataset.mailTab!==selectedTab;section.disabled=section.hidden;for(const field of section.querySelectorAll('input,select,textarea'))field.disabled=section.hidden;} +} function mount(){ const current=document.getElementById('mail-flow-settings'); - const panel=location.hash.split('?')[0]==='#/config/system/email'?document.querySelector('#root [role="tabpanel"][data-state="active"]'):null; - if(!panel?.querySelector('input[name="email_host"]')){current?.remove();return;} - if(current?.parentElement===panel)return; - current?.remove();const host=el('section',null,'border-t mt-6 pt-6');host.id='mail-flow-settings';panel.append(host);render(host); + const list=location.hash.split('?')[0]==='#/config/system/email'?document.querySelector('#root [role="tablist"]'):null; + if(!list){current?.remove();selectedTab=null;return;} + const native=Array.from(list.children).filter(e=>!e.dataset.mailTab&&e.getAttribute('role')==='tab'); + if(!native.some(e=>e.textContent==='基本设置'))return; + for(const button of native){ + if(wired.has(button))continue;wired.add(button); + button.addEventListener('click',()=>{selectedTab=null;for(const original of native){nativeStates.delete(original);original.dataset.state=original===button?'active':'inactive';}schedule();}); + } + list.classList.add('flex-wrap','h-auto'); + for(const [key,text] of Object.entries(tabNames)){ + if(list.querySelector('[data-mail-tab="'+key+'"]'))continue; + const button=el('button',text,native[0].className);button.type='button';button.id='mail-tab-'+key;button.dataset.mailTab=key;button.setAttribute('role','tab');button.setAttribute('aria-controls','mail-flow-settings'); + button.addEventListener('click',()=>{selectedTab=key;syncTabs();});list.append(button); + } + if(!wired.has(list)){ + wired.add(list);list.addEventListener('keydown',event=>{if(!['ArrowLeft','ArrowRight','Home','End'].includes(event.key))return;const tabs=Array.from(list.querySelectorAll('[role="tab"]'));const i=tabs.indexOf(document.activeElement);if(i<0)return;event.preventDefault();const next=event.key==='Home'?0:event.key==='End'?tabs.length-1:(i+(event.key==='ArrowRight'?1:-1)+tabs.length)%tabs.length;tabs[next].focus();tabs[next].click();}); + } + if(current?.parentElement===list.parentElement){syncTabs();return;} + current?.remove();const host=el('section',null,'mt-4');host.id='mail-flow-settings';host.setAttribute('role','tabpanel');list.parentElement.append(host);syncTabs();render(host); } let scheduled=false; const schedule=()=>{if(scheduled)return;scheduled=true;requestAnimationFrame(()=>{scheduled=false;mount();});};