dpl/public/admin-custom/mail-flow.js

100 lines
11 KiB
JavaScript
Vendored
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.

// Extend the existing email settings page using its authenticated admin API and styles.
const fieldClass='flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50';
const buttonClass='inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2';
function el(tag, text, className){const node=document.createElement(tag);if(text)node.textContent=text;if(className)node.className=className;return node;}
async function api(action, payload) {
let token; try { token=JSON.parse(localStorage.getItem('XBOARD_ACCESS_TOKEN')||'null'); } catch { throw new Error('请重新登录后台'); }
if(!token?.value || (token.expire!=null && token.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 url=base.pathname.replace(/\/$/,'')+'/api/v2/'+encodeURIComponent(window.settings.secure_path)+'/config/'+action;
const response=await fetch(url,{method:payload?'POST':'GET',credentials:'same-origin',headers:{Authorization:token.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;
}
const groups=[
['发送渠道', [['enabled','启用邮件记录与分流','checkbox'],['resend_enabled','验证码、登录链接和工单通知优先 Resend','checkbox'],['resend_key','Resend API 密钥','password'],['resend_from','Resend 发件地址','email'],['resend_webhook_secret','Resend 回调签名密钥(选填)','password'],['reply_to','统一回复地址(常规邮箱)','email'],['daily_limit','日额度上限','number'],['monthly_limit','月额度上限','number']]],
['常规邮箱收件与自动回复', [['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']]],
];
function control(key,label,type,value,configured){
const wrap=el('div',null,'space-y-2');
const id='mail-flow-'+key;
const caption=el('label',label,'font-medium text-base');caption.htmlFor=id;wrap.append(caption);
let input;
if(type==='checkbox'){
input=el('button');input.type='button';input.setAttribute('role','switch');input.checked=!!value;
input.className='peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input';
const thumb=el('span',null,'pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0');input.append(thumb);
const sync=()=>{const state=input.checked?'checked':'unchecked';input.dataset.state=state;thumb.dataset.state=state;input.setAttribute('aria-checked',String(input.checked));};
input.addEventListener('click',()=>{input.checked=!input.checked;sync();});sync();
const row=el('div',null,'flex items-center justify-between gap-4');wrap.replaceChildren(row);row.append(caption,input);
}else{
input=el(type==='textarea'?'textarea':'input');if(type!=='textarea')input.type=type;
input.className=fieldClass+(type==='textarea'?' min-h-24 py-2':'');
if(type==='password'){input.autocomplete='new-password';input.placeholder=configured?'已配置,留空保留':'尚未配置';}
else input.value=value??'';
if(type==='number'){input.step='1';input.min='1';if(key==='daily_limit')input.max='100';if(key==='monthly_limit')input.max='3000';if(key==='imap_port')input.max='65535';}
wrap.append(input);
}
input.id=id;input.name=key;
return {wrap,input,type};
}
async function render(host){
const loading=el('p','正在读取邮件渠道设置…','text-sm text-muted-foreground');host.append(loading);
try{
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 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'));
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);}
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(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='邮件渠道设置已保存。';}
catch(error){message.textContent=error.message;}
finally{save.disabled=false;}
});
host.replaceChildren(form);
const history=el('details',null,'border-t pt-6 mt-6');history.append(el('summary','邮件处理与汇报记录','cursor-pointer text-sm font-medium'));
history.append(el('pre',data.messages.map(m=>`${m.created_at} · ${m.purpose} · ${m.channel||'待选择'} · ${m.status}`).join('\n')||'暂无邮件记录','text-xs whitespace-pre-wrap mt-4'));
for(const incoming of data.messages.filter(m=>m.direction==='in')){
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);
}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);}
}
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);
}
let scheduled=false;
const schedule=()=>{if(scheduled)return;scheduled=true;requestAnimationFrame(()=>{scheduled=false;mount();});};
window.addEventListener('hashchange',schedule);
new MutationObserver(schedule).observe(document.getElementById('root'),{childList:true,subtree:true});schedule();