188 lines
15 KiB
JavaScript
188 lines
15 KiB
JavaScript
(() => {
|
||
'use strict';
|
||
const form = document.getElementById('auth-form');
|
||
const username = document.getElementById('username');
|
||
const password = document.getElementById('password');
|
||
const remember = document.getElementById('auth-remember');
|
||
const submit = document.getElementById('auth-submit');
|
||
const feedback = document.getElementById('auth-feedback');
|
||
const serviceButton = document.getElementById('auth-service');
|
||
const serviceName = document.getElementById('auth-service-name');
|
||
const subtitle = document.getElementById('auth-subtitle');
|
||
const passwordToggle = document.getElementById('auth-toggle-password');
|
||
const dialog = document.getElementById('auth-dialog');
|
||
const dialogContent = document.getElementById('auth-dialog-content');
|
||
const DEMO = Object.freeze({username:'demo', password:'Demo123!'});
|
||
const keys = {account:'yuexin-terminal-login-account',connection:'yuexin-terminal-login-connection'};
|
||
const escape = value => String(value).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
const read = key => {try{return JSON.parse(localStorage.getItem(key)||'null');}catch{return null;}};
|
||
const write = (key,value) => {try{if(value===null)localStorage.removeItem(key);else localStorage.setItem(key,JSON.stringify(value));return true;}catch{return false;}};
|
||
function validEndpoint(value) {
|
||
try {
|
||
const u = new URL(value);
|
||
if (u.username || u.password || u.search || u.hash) return null;
|
||
if (u.protocol !== 'https:' && !(u.protocol === 'http:' && ['localhost','127.0.0.1','[::1]'].includes(u.hostname))) return null;
|
||
return u.href.replace(/\/$/,'');
|
||
} catch {return null;}
|
||
}
|
||
const savedConnection = read(keys.connection);
|
||
let connection = savedConnection?.mode === 'enterprise' && validEndpoint(savedConnection.url)
|
||
? {mode:'enterprise',url:validEndpoint(savedConnection.url),organization:String(savedConnection.organization||'').slice(0,80)}
|
||
: {mode:'cloud',url:'',organization:''};
|
||
let mode = connection.mode, serviceIntent = '', busy = false, loginTimer, modalTimer, modalBusy = false;
|
||
let credentials = null;
|
||
let serviceDraft = {url:connection.url,organization:connection.organization};
|
||
function restoreAccount() {
|
||
const saved = read(keys.account);
|
||
username.value = typeof saved?.username === 'string' ? saved.username.slice(0,100) : '';
|
||
remember.checked = saved?.remember !== false;
|
||
}
|
||
function updateService() {
|
||
serviceName.textContent = connection.mode === 'enterprise' ? '企业部署' : '云端服务';
|
||
serviceButton.setAttribute('aria-label',serviceName.textContent+',打开连接设置');
|
||
serviceButton.title = connection.mode === 'enterprise' ? connection.url : '云端服务';
|
||
subtitle.textContent = connection.mode === 'enterprise' ? '使用企业工作账号继续' : '使用云端工作账号继续';
|
||
}
|
||
function message(text,error = false) {
|
||
feedback.textContent = text;
|
||
feedback.classList.toggle('error',error);
|
||
}
|
||
function clearInvalid() {
|
||
username.removeAttribute('aria-invalid');password.removeAttribute('aria-invalid');
|
||
}
|
||
function setBusy(value) {
|
||
busy = value;
|
||
form.setAttribute('aria-busy',String(value));
|
||
form.querySelectorAll('input,button').forEach(el=>{el.disabled=value;});
|
||
serviceButton.disabled=value;
|
||
submit.innerHTML=value?'<span class="auth-spinner" aria-hidden="true"></span>登录中…':'登 录';
|
||
}
|
||
function concealPassword() {
|
||
password.type='password';passwordToggle.setAttribute('aria-pressed','false');passwordToggle.setAttribute('aria-label','显示密码');
|
||
}
|
||
function enterWorkspace(identity) {
|
||
clearTimeout(loginTimer);credentials=null;password.value='';concealPassword();setBusy(false);message('');
|
||
document.dispatchEvent(new CustomEvent('terminal:login',{detail:identity}));
|
||
}
|
||
function submitLogin(event) {
|
||
event.preventDefault();
|
||
if(busy)return;
|
||
clearInvalid();
|
||
const account=username.value.trim();
|
||
if(!account){username.setAttribute('aria-invalid','true');message('请输入工作账号',true);username.focus();return;}
|
||
if(!password.value){password.setAttribute('aria-invalid','true');message('请输入密码',true);password.focus();return;}
|
||
credentials={username:account,password:password.value};
|
||
setBusy(true);message('正在验证工作账号…');
|
||
loginTimer=setTimeout(()=>{
|
||
const accepted=credentials?.username===DEMO.username&&credentials?.password===DEMO.password;
|
||
credentials=null;setBusy(false);
|
||
if(!accepted){password.value='';concealPassword();password.setAttribute('aria-invalid','true');message('账号或密码不正确,请重试或查看体验账号',true);password.focus();return;}
|
||
write(keys.account,{username:remember.checked?account:'',remember:remember.checked});
|
||
enterWorkspace({name:account,mode:'account',service:connection.mode,organization:connection.organization});
|
||
},900);
|
||
}
|
||
const heading = (title,sub) => `<div class="auth-dialog-header"><div><h2 id="auth-dialog-title">${title}</h2>${sub?`<p class="auth-dialog-sub">${sub}</p>`:''}</div><button type="button" class="auth-dialog-close" data-auth-action="close" aria-label="关闭对话框">×</button></div>`;
|
||
const cancel = (label='取消') => `<button type="button" class="auth-dialog-button" data-auth-action="close">${label}</button>`;
|
||
function showDialog(html) {
|
||
clearTimeout(modalTimer);modalBusy=false;
|
||
dialogContent.innerHTML=html;
|
||
if(!dialog.open)dialog.showModal();
|
||
}
|
||
function closeDialog() {clearTimeout(modalTimer);modalBusy=false;dialog.close();}
|
||
function rememberDraft() {
|
||
const url=dialogContent.querySelector('#auth-enterprise-url'),org=dialogContent.querySelector('#auth-enterprise-org');
|
||
if(url){serviceDraft.url=url.value;serviceDraft.organization=org.value;}
|
||
}
|
||
function serviceDialog(intent='') {
|
||
serviceIntent=intent;mode=intent==='sso'?'enterprise':connection.mode;
|
||
serviceDraft={url:connection.url,organization:connection.organization};
|
||
renderService();
|
||
}
|
||
function renderService() {
|
||
showDialog(heading('连接设置','选择云端服务或企业部署入口。')+
|
||
`<div class="auth-dialog-tabs" role="tablist" aria-label="服务模式"><button type="button" role="tab" id="auth-tab-cloud" aria-controls="auth-service-form" tabindex="${mode==='cloud'?0:-1}" aria-selected="${mode==='cloud'}" data-auth-action="service-mode" data-mode="cloud">云端服务</button><button type="button" role="tab" id="auth-tab-enterprise" aria-controls="auth-service-form" tabindex="${mode==='enterprise'?0:-1}" aria-selected="${mode==='enterprise'}" data-auth-action="service-mode" data-mode="enterprise">企业部署</button></div>`+
|
||
`<form id="auth-service-form" role="tabpanel" aria-labelledby="auth-tab-${mode}" novalidate>${mode==='enterprise'?`<label class="auth-field" for="auth-enterprise-url">企业服务地址<input class="auth-input" id="auth-enterprise-url" name="url" type="url" placeholder="https://vdi.company.com" autocomplete="off" value="${escape(serviceDraft.url)}" aria-describedby="auth-service-error" required><small>使用 HTTPS 地址;HTTP 仅支持本机预览。</small></label><label class="auth-field" for="auth-enterprise-org">租户或组织标识 <small>选填</small><input class="auth-input" id="auth-enterprise-org" name="organization" maxlength="80" placeholder="例如:越欣科技" value="${escape(serviceDraft.organization)}" autocomplete="off"></label>`:`<div class="auth-note"><strong>云端服务</strong><p>使用工作账号登录云桌面,无需填写企业服务地址。</p></div>`}<p class="auth-dialog-error" id="auth-service-error" role="status" aria-live="polite"></p><div class="auth-dialog-actions">${cancel()}<button class="auth-dialog-button primary" type="submit">${serviceIntent==='sso'&&mode==='enterprise'?'保存并继续':'保存设置'}</button></div></form>`);
|
||
}
|
||
function saveService(event) {
|
||
event.preventDefault();
|
||
rememberDraft();
|
||
let url='';
|
||
if(mode==='enterprise'){
|
||
const input=dialogContent.querySelector('#auth-enterprise-url');url=validEndpoint(input.value.trim());
|
||
if(!url){input.setAttribute('aria-invalid','true');dialogContent.querySelector('#auth-service-error').textContent='请输入有效的 HTTPS 企业服务地址';input.focus();return;}
|
||
}
|
||
connection=mode==='enterprise'?{mode,url,organization:serviceDraft.organization.trim()}:{mode:'cloud',url:'',organization:''};
|
||
const stored=write(keys.connection,connection);updateService();
|
||
if(serviceIntent==='sso'&&connection.mode==='enterprise'){ssoDialog();return;}
|
||
closeDialog();message(stored?'连接设置已保存':'连接设置已应用,本次预览内有效');username.focus();
|
||
}
|
||
function demoDialog() {
|
||
showDialog(heading('体验账号','用于预览登录、桌面选择和连接设置流程。')+
|
||
`<dl class="auth-demo-credentials"><div><dt>工作账号</dt><dd>${DEMO.username}</dd></div><div><dt>密码</dt><dd>${DEMO.password}</dd></div></dl><div class="auth-note">这是交互原型的公开体验账号,未连接真实认证服务。请使用体验信息。</div><div class="auth-dialog-actions">${cancel('返回')}<button type="button" class="auth-dialog-button primary" data-auth-action="fill-demo">填入体验账号</button></div>`);
|
||
}
|
||
function forgotDialog() {
|
||
showDialog(heading('忘记密码','企业工作账号由管理员统一管理。')+
|
||
`<div class="auth-note"><strong>请联系越欣科技管理员重置密码</strong><p>可提供工作账号与设备序列号 VDI-8F42A1,协助管理员定位。</p></div><p class="auth-dialog-sub">原型体验账号无法修改密码,可重新查看体验信息。</p><div class="auth-dialog-actions">${cancel('返回登录')}<button class="auth-dialog-button primary" type="button" data-auth-action="demo">查看体验账号</button></div>`);
|
||
}
|
||
function ssoDialog() {
|
||
if(connection.mode!=='enterprise'){serviceDialog('sso');return;}
|
||
showDialog(heading('企业账号登录','通过企业统一身份进入云桌面。')+
|
||
`<dl class="auth-note"><div><dt>企业服务</dt><dd>${escape(connection.url)}</dd></div>${connection.organization?`<div><dt>组织</dt><dd>${escape(connection.organization)}</dd></div>`:''}<div><dt>体验身份</dt><dd>越欣科技 · 体验用户</dd></div></dl><p class="auth-dialog-sub">当前为统一身份登录预览,不会跳转或访问真实企业身份服务。</p><p class="auth-dialog-status" id="auth-sso-status" role="status" aria-live="polite"></p><div class="auth-dialog-actions">${cancel('取消登录')}<button class="auth-dialog-button primary" type="button" data-auth-action="sso-continue">使用体验身份继续</button></div>`);
|
||
}
|
||
function ssoContinue(button) {
|
||
if(modalBusy)return;
|
||
modalBusy=true;button.disabled=true;button.innerHTML='<span class="auth-spinner" aria-hidden="true"></span>登录中…';
|
||
dialogContent.querySelector('#auth-sso-status').textContent='正在验证企业身份…';
|
||
modalTimer=setTimeout(()=>{closeDialog();enterWorkspace({name:'企业体验用户',mode:'sso',service:'enterprise',organization:connection.organization});},900);
|
||
}
|
||
function offlineDialog() {
|
||
showDialog(heading('离线体验','先了解桌面列表和终端设置。')+
|
||
`<div class="auth-note"><strong>无需工作账号</strong><p>可预览桌面、收藏和连接设置,设备与桌面信息为示例。配置仅保存在当前浏览器,不连接真实远程桌面。</p></div><div class="auth-dialog-actions">${cancel()}<button class="auth-dialog-button primary" type="button" data-auth-action="offline-continue">进入离线体验</button></div>`);
|
||
}
|
||
form.addEventListener('submit',submitLogin);
|
||
form.addEventListener('input',event=>{
|
||
if(event.target===username||event.target===password){event.target.removeAttribute('aria-invalid');if(!busy)message('');}
|
||
});
|
||
remember.addEventListener('change',()=>{
|
||
if(!remember.checked)write(keys.account,{username:'',remember:false});
|
||
});
|
||
passwordToggle.addEventListener('click',()=>{
|
||
const reveal=password.type==='password';password.type=reveal?'text':'password';passwordToggle.setAttribute('aria-pressed',String(reveal));passwordToggle.setAttribute('aria-label',reveal?'隐藏密码':'显示密码');
|
||
});
|
||
serviceButton.addEventListener('click',()=>serviceDialog());
|
||
document.getElementById('auth-demo').addEventListener('click',demoDialog);
|
||
document.getElementById('auth-forgot').addEventListener('click',forgotDialog);
|
||
document.getElementById('auth-sso').addEventListener('click',ssoDialog);
|
||
document.getElementById('auth-offline').addEventListener('click',offlineDialog);
|
||
dialog.addEventListener('submit',event=>{if(event.target.id==='auth-service-form')saveService(event);});
|
||
dialog.addEventListener('input',event=>{event.target.removeAttribute('aria-invalid');const error=dialogContent.querySelector('#auth-service-error');if(error)error.textContent='';});
|
||
dialog.addEventListener('click',event=>{
|
||
const button=event.target.closest('[data-auth-action]');
|
||
if(!button||button.disabled)return;
|
||
switch(button.dataset.authAction){
|
||
case 'close':closeDialog();break;
|
||
case 'service-mode':rememberDraft();mode=button.dataset.mode;renderService();dialogContent.querySelector(`[data-mode="${mode}"]`).focus();break;
|
||
case 'fill-demo':username.value=DEMO.username;password.value=DEMO.password;concealPassword();clearInvalid();message('已填入体验账号,点击登录继续');closeDialog();submit.focus();break;
|
||
case 'demo':demoDialog();break;
|
||
case 'sso-continue':ssoContinue(button);break;
|
||
case 'offline-continue':closeDialog();enterWorkspace({name:'离线体验',mode:'offline',service:'offline',organization:'越欣科技'});break;
|
||
}
|
||
});
|
||
dialog.addEventListener('keydown',event=>{
|
||
const tab=event.target.closest('[role=tab]');
|
||
if(!tab||!['ArrowLeft','ArrowRight','Home','End'].includes(event.key))return;
|
||
event.preventDefault();rememberDraft();
|
||
mode=event.key==='Home'?'cloud':event.key==='End'?'enterprise':mode==='cloud'?'enterprise':'cloud';
|
||
renderService();dialogContent.querySelector(`[data-mode="${mode}"]`).focus();
|
||
});
|
||
dialog.addEventListener('cancel',()=>{clearTimeout(modalTimer);modalBusy=false;});
|
||
dialog.addEventListener('close',()=>{clearTimeout(modalTimer);modalBusy=false;});
|
||
document.addEventListener('terminal:logout',()=>{
|
||
clearTimeout(loginTimer);clearTimeout(modalTimer);credentials=null;setBusy(false);password.value='';concealPassword();clearInvalid();restoreAccount();message('已退出登录');username.focus();
|
||
});
|
||
document.addEventListener('terminal:open-workspace',()=>{
|
||
clearTimeout(loginTimer);credentials=null;setBusy(false);password.value='';concealPassword();if(dialog.open)closeDialog();
|
||
});
|
||
restoreAccount();updateService();
|
||
})();
|