184 lines
18 KiB
JavaScript
184 lines
18 KiB
JavaScript
(() => {
|
||
'use strict';
|
||
const root = document.getElementById('workspace');
|
||
const login = document.getElementById('login');
|
||
const paths = {
|
||
monitor: '<rect x="3" y="4" width="18" height="13" rx="2"/><path d="M8 21h8m-4-4v4"/>',
|
||
settings: '<path d="m9 3 1-1h4l1 3 3 1 3-1 2 4-2 2v3l2 2-2 4-3-1-3 1-1 3h-4l-1-3-3-1-3 1-2-4 2-2v-3L1 9l2-4 3 1 3-1Z" transform="translate(1 0) scale(.92)"/><circle cx="12" cy="12" r="3"/>',
|
||
sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1.5 1.5m11 11L19 19M5 19l1.5-1.5m11-11L19 5"/>',
|
||
moon: '<path d="M20.5 13A8.5 8.5 0 0 1 11 3.5 8.5 8.5 0 1 0 20.5 13Z"/>',
|
||
user: '<circle cx="12" cy="8" r="3.5"/><path d="M5 21v-2a7 7 0 0 1 14 0v2"/>',
|
||
star: '<path d="m12 2.5 2.9 5.9 6.6 1-4.8 4.6 1.1 6.5-5.8-3.1-5.8 3.1 1.1-6.5-4.8-4.6 6.6-1Z"/>',
|
||
search: '<circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 5 5"/>',
|
||
refresh: '<path d="M20 8a8 8 0 1 0 0 8M20 3v5h-5"/>',
|
||
arrow: '<path d="M4 12h16m-5-5 5 5-5 5"/>',
|
||
back: '<path d="M20 12H4m5-5-5 5 5 5"/>',
|
||
network: '<path d="M12 12v7M5 19v-4h14v4"/><rect x="8" y="3" width="8" height="6" rx="1"/><path d="M2 19h6v3H2Zm14 0h6v3h-6Z"/>',
|
||
info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v6m0-10v.2"/>',
|
||
logout: '<path d="M9 3H4v18h5m6-5 4-4-4-4M8 12h11"/>',
|
||
check: '<path d="m5 12 4 4L19 6"/>',
|
||
close: '<path d="m6 6 12 12M6 18 18 6"/>'
|
||
};
|
||
const icon = name => `<svg class="ws-icon" viewBox="0 0 24 24" aria-hidden="true">${paths[name] || paths.monitor}</svg>`;
|
||
const escape = value => String(value).replace(/[&<>"']/g, char => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[char]));
|
||
const defaults = [
|
||
{ id:'windows', os:'Windows 10', version:'64 位 · Windows', name:'设计工作站', resolution:'2560x1440', fps:60, endpoint:'192.168.1.50', app:'Desktop', note:'图形设计与日常办公', favorite:true },
|
||
{ id:'ubuntu', os:'Ubuntu', version:'24.04 LTS · Linux', name:'研发环境', resolution:'2560x1440', fps:30, endpoint:'192.168.1.51', app:'Desktop', note:'开发工具与研发工作环境', favorite:false },
|
||
{ id:'kylin', os:'银河麒麟', version:'桌面操作系统 · Linux', name:'办公桌面', resolution:'1920x1080', fps:30, endpoint:'', app:'Desktop', note:'暂未配置服务器,请联系管理员', favorite:false }
|
||
];
|
||
const read = () => { try { return JSON.parse(localStorage.getItem('yuexin-terminal-ui-v3') || 'null'); } catch { return null; } };
|
||
const saved = read();
|
||
let desktops = defaults.map(d => {
|
||
const local = Array.isArray(saved) && saved.find(v => v.id === d.id);
|
||
return local ? { ...d, endpoint:String(local.endpoint ?? d.endpoint), app:String(local.app || d.app), resolution:['1920x1080','2560x1440','3840x2160','1280x720'].includes(local.resolution) ? local.resolution : d.resolution, fps:[24,30,60].includes(local.fps) ? local.fps : d.fps, favorite:!!local.favorite } : { ...d };
|
||
});
|
||
const persist = () => { try { localStorage.setItem('yuexin-terminal-ui-v3', JSON.stringify(desktops)); return true; } catch { return false; } };
|
||
let tab = 'all', search = '', view = 'desktops', settingsTab = 'desktop', selectedId = 'windows', sessionState = 'connecting';
|
||
let timer, toastTimer;
|
||
let identity = {name:'终端用户',mode:'preview',service:'cloud',organization:'越欣科技'};
|
||
const selected = () => desktops.find(d => d.id === selectedId) || desktops[0];
|
||
const mark = d => {
|
||
const art = d.id === 'windows'
|
||
? '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M1 3.5 10.5 2.2v9.3H1Zm11-1.6L23 .4v11.1H12ZM1 13h9.5v9.3L1 21Zm11 0h11v11l-11-1.5Z"/></svg>'
|
||
: `<img src="assets/systems/${d.id === 'ubuntu' ? 'ubuntu.svg' : 'kylinsoft.png'}" alt="${d.id === 'ubuntu' ? 'Ubuntu' : '麒麟软件'} 标识">`;
|
||
return `<div class="ws-os-mark ${d.id}">${art}</div>`;
|
||
};
|
||
const prettyResolution = d => d.resolution.replace('x', ' × ');
|
||
const themeLabel = () => document.body.dataset.theme === 'dark' ? '切换浅色' : '切换深色';
|
||
const header = () => `<header class="ws-header"><div class="ws-brand"><strong>越欣科技</strong><span>云桌面终端</span></div><div class="ws-header-actions"><button class="ws-btn" data-action="settings" aria-label="终端设置">${icon('settings')}<span class="ws-settings-label">终端设置</span></button><button class="ws-btn ws-icon-btn" data-action="theme" aria-label="${themeLabel()}" title="${themeLabel()}">${icon(document.body.dataset.theme === 'dark' ? 'sun' : 'moon')}</button><div class="ws-account">${icon('user')}<span>${escape(identity.name)}</span></div></div></header>`;
|
||
const footer = () => `<footer class="ws-footer"><div class="ws-footer-left">${identity.mode === 'offline' ? '<span>离线体验</span>' : '<span class="ws-online">网络已连接</span>'}<span>设备 SN:VDI-8F42A1</span><span>RK3528 · ARM64</span></div><span class="ws-prototype">交互原型 · 设备与桌面状态为示例</span></footer>`;
|
||
function card(d) {
|
||
const available = !!d.endpoint;
|
||
return `<article class="ws-card" aria-label="${escape(d.os+' '+d.name)}"><div class="ws-card-top"><div class="ws-system">${mark(d)}<div><div class="ws-system-name">${escape(d.os)}</div><div class="ws-system-version">${escape(d.version)}</div></div></div><button class="ws-favorite" data-action="favorite" data-id="${d.id}" aria-pressed="${d.favorite}" aria-label="${d.favorite ? '取消收藏' : '收藏'}${escape(d.name)}" title="${d.favorite ? '取消收藏' : '收藏'}">${icon('star')}</button></div><h2>${escape(d.name)}</h2><span class="ws-status ${available ? '' : 'pending'}">${available ? '可连接' : '待配置'}</span><dl class="ws-specs"><div><dt>分辨率</dt><dd>${prettyResolution(d)}</dd></div><div><dt>帧率</dt><dd>${d.fps} FPS</dd></div></dl><p class="ws-card-note">${available ? escape(d.id === 'kylin' ? '国产办公工作环境' : d.note) : '暂未配置服务器,请联系管理员'}</p><div class="ws-card-actions"><button class="ws-btn" data-action="edit" data-id="${d.id}">${icon('settings')}设置</button><button class="ws-btn ws-primary" data-action="connect" data-id="${d.id}" ${available ? '' : 'disabled'}>${available ? '连接桌面' : '暂不可连接'}${available ? icon('arrow') : ''}</button></div></article>`;
|
||
}
|
||
function list() {
|
||
return `<div class="ws-heading"><div><h1>我的云桌面</h1><p>选择工作环境,连接后即可开始使用。</p></div><div class="ws-count">${desktops.filter(d => d.endpoint).length} 台可连接 / 共 ${desktops.length} 台</div></div><div class="ws-toolbar"><div class="ws-tabs" role="tablist" aria-label="桌面筛选"><button class="ws-tab" role="tab" aria-selected="${tab === 'all'}" data-action="tab" data-tab="all">全部桌面<span>${desktops.length}</span></button><button class="ws-tab" role="tab" aria-selected="${tab === 'favorites'}" data-action="tab" data-tab="favorites">已收藏<span>${desktops.filter(d => d.favorite).length}</span></button></div><div class="ws-action-group"><label class="ws-search">${icon('search')}<input id="ws-search" type="search" aria-label="搜索云桌面" placeholder="搜索桌面或系统" value="${escape(search)}"></label><button class="ws-btn ws-icon-btn" data-action="refresh" aria-label="刷新桌面列表" title="刷新桌面列表">${icon('refresh')}</button></div></div><div class="ws-grid" id="ws-grid"></div>`;
|
||
}
|
||
function renderGrid() {
|
||
const rows = desktops.filter(d => (tab !== 'favorites' || d.favorite) && (d.os + d.name).toLowerCase().includes(search.trim().toLowerCase()));
|
||
root.querySelector('#ws-grid').innerHTML = rows.length ? rows.map(card).join('') : `<div class="ws-empty">${icon('search')}<h2>${search ? '没有找到匹配的桌面' : '还没有收藏的桌面'}</h2><p>${search ? '试试系统名称,或清除搜索条件。' : '点击桌面卡片右上角的星标即可收藏。'}</p><button class="ws-btn" data-action="clear-search">查看全部桌面</button></div>`;
|
||
}
|
||
const option = (value, label, current) => `<option value="${escape(value)}" ${value === current ? 'selected' : ''}>${escape(label)}</option>`;
|
||
function desktopSettings() {
|
||
const d = selected();
|
||
return `<h2>桌面连接设置</h2><p>配置当前桌面的连接地址和显示参数。</p><form id="ws-settings-form" novalidate><div class="ws-form-grid"><label class="ws-field full">目标桌面<select class="ws-input" id="ws-target">${desktops.map(v => option(v.id, v.os+' · '+v.name, d.id)).join('')}</select></label><label class="ws-field">服务器地址<input class="ws-input" name="endpoint" value="${escape(d.endpoint)}" placeholder="IP 地址或域名" autocomplete="off"><small>留空表示待配置,桌面将不可连接。</small></label><label class="ws-field">应用名<input class="ws-input" name="app" value="${escape(d.app)}" placeholder="Desktop" autocomplete="off"><small>与服务端发布的应用名保持一致。</small></label><label class="ws-field">分辨率<select class="ws-input" name="resolution">${['1280x720','1920x1080','2560x1440','3840x2160'].map(v => option(v,v.replace('x',' × '),d.resolution)).join('')}</select></label><label class="ws-field">帧率<select class="ws-input" name="fps">${[24,30,60].map(v => option(String(v),v+' FPS',String(d.fps))).join('')}</select></label></div><p class="ws-error-text" id="ws-form-error" role="alert"></p><div class="ws-form-actions"><button class="ws-btn" type="button" data-action="home">取消</button><button class="ws-btn ws-primary" type="submit">保存设置</button></div></form>`;
|
||
}
|
||
function settings() {
|
||
let panel;
|
||
if (settingsTab === 'desktop') panel = desktopSettings();
|
||
if (settingsTab === 'network') panel = `<h2>网络状态</h2><p>连接远程桌面前,请确认终端已接入工作网络。</p><dl class="ws-info-list"><div><dt>连接方式</dt><dd>有线网络</dd></div><div><dt>网络状态</dt><dd><span class="ws-online">已连接</span></dd></div><div><dt>地址获取</dt><dd>自动获取(DHCP)</dd></div></dl><p class="ws-session-note" style="margin-top:24px">此处为设备状态示例。实际网络配置由终端系统管理。</p>`;
|
||
if (settingsTab === 'device') panel = `<h2>设备信息</h2><p>联系管理员时,可提供设备序列号协助定位。</p><dl class="ws-info-list"><div><dt>设备序列号</dt><dd>VDI-8F42A1</dd></div><div><dt>设备型号</dt><dd>RK3528 云终端</dd></div><div><dt>处理器架构</dt><dd>ARM64</dd></div><div><dt>管理快捷键</dt><dd>Ctrl + Alt + Shift + M</dd></div></dl><div class="ws-form-actions"><button class="ws-btn" data-action="logout">${icon('logout')}退出登录</button></div>`;
|
||
return `<div class="ws-view-title"><button class="ws-btn ws-icon-btn" data-action="home" aria-label="返回云桌面">${icon('back')}</button><h1>终端设置</h1></div><div class="ws-settings"><nav class="ws-settings-nav" aria-label="设置分类">${[['desktop','monitor','桌面连接'],['network','network','网络状态'],['device','info','设备信息']].map(([key,i,label]) => `<button data-action="settings-tab" data-tab="${key}" ${key === settingsTab ? 'aria-current="page"' : ''}>${icon(i)}${label}</button>`).join('')}</nav><section class="ws-settings-panel">${panel}</section></div>`;
|
||
}
|
||
function session() {
|
||
const d = selected(), failure = sessionState === 'error', recovery = sessionState === 'reconnect';
|
||
const title = failure ? '暂时无法连接' : recovery ? '正在恢复连接' : '正在连接桌面';
|
||
const steps = recovery ? ['检测网络连接','恢复桌面会话','恢复桌面画面'] : ['检查连接配置','建立远程会话','准备桌面画面'];
|
||
return `<div class="ws-session">${mark(d)}<h1>${title}</h1><p class="ws-session-sub">${escape(d.os)} · ${escape(d.name)}</p>${failure ? '<div style="height:26px"></div>' : '<div class="ws-session-progress"><span style="width:38%"></span></div>'}${failure ? `<div class="ws-session-note"><strong>尚未接入远程桌面服务</strong><br>当前为交互原型,未向服务器发送连接请求。实际终端需由桌面服务返回连接结果。</div>` : `<ol class="ws-steps">${steps.map((step,i) => `<li class="${i === 0 ? 'done' : i === 1 ? 'active' : ''}"><span class="ws-step-mark">${i === 0 ? '✓' : i+1}</span><span>${step}</span>${i === 1 ? '<span class="ws-spinner" aria-hidden="true"></span>' : ''}</li>`).join('')}</ol><div class="ws-session-note">${recovery ? '连接中断后将尝试恢复原会话,请保持设备联网。' : '请保持设备联网。连接完成后,桌面将全屏打开。'}<br>当前预览不发起真实远程连接。</div>`}<div class="ws-session-actions"><button class="ws-btn" data-action="home">${failure ? '返回桌面列表' : '取消连接'}</button>${failure ? `<button class="ws-btn ws-primary" data-action="edit" data-id="${d.id}">检查连接设置</button>` : ''}</div></div>`;
|
||
}
|
||
function render() {
|
||
root.innerHTML = header() + `<main class="ws-main" id="ws-content">${view === 'desktops' ? list() : view === 'settings' ? settings() : session()}</main>` + footer();
|
||
if (view === 'desktops') renderGrid();
|
||
}
|
||
function showWorkspace(next = 'desktops') {
|
||
clearTimeout(timer);
|
||
document.dispatchEvent(new CustomEvent('terminal:open-workspace'));
|
||
view = next;
|
||
login.classList.remove('active');
|
||
root.hidden = false;
|
||
document.body.classList.add('in-workspace');
|
||
render();
|
||
window.scrollTo(0,0);
|
||
}
|
||
function home() { showWorkspace(); }
|
||
function leave() {
|
||
clearTimeout(timer);
|
||
root.hidden = true;
|
||
document.body.classList.remove('in-workspace');
|
||
login.classList.add('active');
|
||
identity = {name:'终端用户',mode:'preview',service:'cloud',organization:'越欣科技'};
|
||
tab='all';search='';
|
||
document.dispatchEvent(new CustomEvent('terminal:logout'));
|
||
window.scrollTo(0,0);
|
||
}
|
||
function notify(message) {
|
||
clearTimeout(toastTimer);
|
||
root.querySelector('.ws-toast')?.remove();
|
||
const toast = document.createElement('div');
|
||
toast.className = 'ws-toast'; toast.setAttribute('role','status'); toast.textContent = message; root.append(toast);
|
||
toastTimer = setTimeout(() => toast.remove(), 3000);
|
||
}
|
||
function start(id, state = 'connecting') {
|
||
selectedId = id;
|
||
if (!selected().endpoint) return;
|
||
sessionState = state;
|
||
showWorkspace('session');
|
||
if (state !== 'error') timer = setTimeout(() => { if (view === 'session') { sessionState = 'error'; render(); } }, 12000);
|
||
}
|
||
function toggleTheme() {
|
||
const dark = document.body.dataset.theme === 'dark';
|
||
document.body.dataset.theme = dark ? 'light' : 'dark';
|
||
const oldTheme = document.getElementById('theme');
|
||
oldTheme.textContent = dark ? '◉ 浅色外观' : '▣ 深色外观';
|
||
if (!root.hidden) {
|
||
const b = root.querySelector('[data-action="theme"]');
|
||
b.innerHTML = icon(dark ? 'moon' : 'sun');b.setAttribute('aria-label',themeLabel());b.title = themeLabel();
|
||
}
|
||
}
|
||
// Authentication interactions own login entry; direct preview URLs remain available for design review.
|
||
document.addEventListener('terminal:login', event => {
|
||
identity = {...identity,...event.detail};tab='all';search='';home();
|
||
const title = root.querySelector('h1');
|
||
if (title) { title.tabIndex = -1; title.focus({preventScroll:true}); }
|
||
});
|
||
document.getElementById('theme').addEventListener('click', toggleTheme);
|
||
root.addEventListener('input', event => {
|
||
if (event.target.id === 'ws-search') { search = event.target.value; renderGrid(); }
|
||
});
|
||
root.addEventListener('change', event => {
|
||
if (event.target.id === 'ws-target') { selectedId = event.target.value; render(); }
|
||
});
|
||
root.addEventListener('click', event => {
|
||
const b = event.target.closest('[data-action]');
|
||
if (!b || b.disabled) return;
|
||
switch (b.dataset.action) {
|
||
case 'home': home(); break;
|
||
case 'logout': leave(); break;
|
||
case 'theme': toggleTheme(); break;
|
||
case 'settings': settingsTab = 'desktop'; showWorkspace('settings'); break;
|
||
case 'edit': selectedId = b.dataset.id; settingsTab = 'desktop'; showWorkspace('settings'); break;
|
||
case 'settings-tab': settingsTab = b.dataset.tab; render(); break;
|
||
case 'connect': start(b.dataset.id); break;
|
||
case 'favorite': { const d = desktops.find(v => v.id === b.dataset.id); d.favorite = !d.favorite; persist(); render(); break; }
|
||
case 'tab': tab = b.dataset.tab; render(); break;
|
||
case 'clear-search': search = ''; tab = 'all'; render(); break;
|
||
case 'refresh': render(); notify('示例桌面列表已刷新'); break;
|
||
}
|
||
});
|
||
root.addEventListener('submit', event => {
|
||
if (event.target.id !== 'ws-settings-form') return;
|
||
event.preventDefault();
|
||
const form = event.target, values = new FormData(form), endpoint = String(values.get('endpoint')).trim(), app = String(values.get('app')).trim();
|
||
const field = form.querySelector('[name="endpoint"]');
|
||
if (endpoint && !/^(?:[a-z\d](?:[a-z\d.-]*[a-z\d])?)(?::\d{1,5})?$/i.test(endpoint)) {
|
||
form.querySelector('#ws-form-error').textContent = '请输入 IP 地址或域名,可附加端口号。';field.setAttribute('aria-invalid','true');field.focus();return;
|
||
}
|
||
if (!app) { form.querySelector('#ws-form-error').textContent = '请填写应用名,例如 Desktop。';form.querySelector('[name="app"]').focus();return; }
|
||
const d = selected();
|
||
Object.assign(d,{endpoint,app,resolution:String(values.get('resolution')),fps:Number(values.get('fps'))});
|
||
const stored = persist();home();notify(stored ? '设置已保存到本机预览' : '设置已应用,本次会话内有效');
|
||
});
|
||
document.addEventListener('keydown', event => {
|
||
if (event.ctrlKey && event.altKey && event.shiftKey && event.key.toLowerCase() === 'm') { event.preventDefault();settingsTab='desktop';showWorkspace('settings'); }
|
||
if (event.key === 'Escape' && !root.hidden && view !== 'desktops') home();
|
||
});
|
||
const params = new URLSearchParams(location.search);
|
||
const preview = params.get('preview');
|
||
if (preview) {
|
||
if (['dark','light'].includes(params.get('theme'))) document.body.dataset.theme = params.get('theme');
|
||
if (['connecting','reconnect','error'].includes(preview)) start('windows',preview);
|
||
else if (preview === 'settings') showWorkspace('settings');
|
||
else showWorkspace();
|
||
}
|
||
})();
|