功能:新增VDI深浅色交互原型并完善终端登录流程
2
.gitignore
vendored
@ -9,3 +9,5 @@ CMakeCache.txt
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
test-output/
|
test-output/
|
||||||
screenshots/
|
screenshots/
|
||||||
|
.DS_Store
|
||||||
|
.playwright-cli/
|
||||||
|
|||||||
13
ui/README.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# VDI Cloud UI 设计稿
|
||||||
|
|
||||||
|
基于「百工万象」定版界面的深色 / 浅色视觉语言,为 VDI Cloud 云桌面控制台整理的可交互设计稿。
|
||||||
|
|
||||||
|
- `index.html`:设计稿入口,可切换深色 / 浅色与页面
|
||||||
|
- `screens/`:渲染后的页面截图
|
||||||
|
- `assets/`:品牌标识
|
||||||
|
|
||||||
|
页面范围:云桌面总览、桌面资源池、远程会话、终端与策略设置。
|
||||||
|
|
||||||
|
## ARM 终端设计稿
|
||||||
|
|
||||||
|
真实终端界面请打开 `terminal/index.html`。它对应 `terminal/kiosk-shell/src/login_page.cpp`、`desktop_list_page.cpp`、`desktop_settings_dialog.cpp` 和 `main_window.cpp` 的流程,覆盖登录、桌面列表、连接中、重连、管理菜单五个状态。
|
||||||
1
ui/assets/brand-logo.svg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
1
ui/assets/brand-mark.svg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
21
ui/design-spec.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# VDI Cloud UI 设计说明
|
||||||
|
|
||||||
|
## 视觉继承
|
||||||
|
|
||||||
|
- **色彩**:以百工万象定版的蜂蜜金橙 `#BD713B` 为主动作色,配合深色 `#151D27 / #1D2733 / #253241` 与浅色 `#F5F7FA / #FFFFFF / #F0F3F7`。
|
||||||
|
- **结构**:固定左侧组织与导航;右侧采用页面标题、动作区、卡片分组的工作台结构。
|
||||||
|
- **组件**:18px 大圆角、轻阴影、低对比边框;状态使用绿/黄/红三色,保持与数字员工运行中心一致。
|
||||||
|
- **交互**:页面在入口内可切换深浅主题;导航可以切换总览、资源池、远程会话、终端策略四个页面。
|
||||||
|
|
||||||
|
## VDI 页面映射
|
||||||
|
|
||||||
|
| 页面 | 目标 | 关键内容 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 云桌面总览 | 管理员快速判断系统状态 | 在线桌面、活跃会话、延迟、告警、趋势、资源健康 |
|
||||||
|
| 桌面资源池 | 管理桌面模板与分配 | Windows / Ubuntu 桌面卡片、GPU、分辨率、状态与连接 |
|
||||||
|
| 远程会话 | 观察实时串流与诊断 | 画面预览、2K/60 FPS、延迟、GPU/显存、断开操作 |
|
||||||
|
| 终端与策略 | 统一配置终端体验 | 显示规格、自动恢复、独立光标、USB、断线保护 |
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
直接打开 `index.html`,点击左侧导航查看页面;点击右上角月亮/太阳按钮切换主题。
|
||||||
12
ui/index.html
Normal file
48
ui/terminal/README.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# 越欣科技 ARM VDI 终端交互设计
|
||||||
|
|
||||||
|
打开 `index.html`,从登录进入桌面列表。保留原有登录页 Logo、互动图形和深浅色外观,补充表单校验、登录状态及弹窗操作;登录后沿用深蓝灰 / 浅色和铜橙色按钮。
|
||||||
|
|
||||||
|
## 登录体验
|
||||||
|
|
||||||
|
公开体验账号:`demo`,密码:`Demo123!`。点击登录页的「体验账号」可查看并填入,再点击登录或按 Enter 继续。
|
||||||
|
|
||||||
|
- 账号、密码为空时提示并聚焦;错误账号或密码会提示重试,清空密码。
|
||||||
|
- 支持显示 / 隐藏密码、登录中状态和防重复提交。
|
||||||
|
- 「记住工作账号」只保存成功登录的账号,不保存密码;取消勾选立即清除已记住的账号。退出后清空并隐藏密码。
|
||||||
|
- 顶部「云端服务」打开连接设置,可切换云端服务 / 企业部署。企业地址需为 HTTPS,本机预览允许 HTTP;设置可保存、取消并在刷新后恢复。
|
||||||
|
- 「企业 SSO」先检查企业连接配置,再展示体验身份;确认后进入桌面列表,取消或 Esc 会终止等待。
|
||||||
|
- 「忘记密码」提供联系越欣科技管理员的说明及体验账号入口。
|
||||||
|
- 「离线体验」确认后以独立体验身份进入桌面列表。
|
||||||
|
|
||||||
|
这是交互原型,账号校验和 SSO 均为本地模拟,不会访问真实认证服务或远程桌面。请使用公开体验信息。设备和桌面数据为示例,未移植到 Qt 或部署到 ARM 设备。
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- `index.html`:登录界面和工作区挂载位置。
|
||||||
|
- `login-interactions.js` / `login-interactions.css`:登录状态、校验、密码操作、连接设置、SSO 和弹窗样式。
|
||||||
|
- `workspace.css`:登录后工作区的布局、配色、字号和适配。
|
||||||
|
- `workspace.js`:桌面筛选、收藏、搜索、配置保存、连接状态、登录身份与设置导航。
|
||||||
|
- `assets/systems/`:Windows、Ubuntu、麒麟识别资源。
|
||||||
|
- [references/README.md](references/README.md):Microsoft Windows App、Amazon WorkSpaces 实际截图,以及 Horizon 官方操作流程与设计采用理由。
|
||||||
|
- `output/playwright/`:浏览器渲染与验证证据。
|
||||||
|
|
||||||
|
## 评审入口
|
||||||
|
|
||||||
|
- [review.html](review.html):评审页,主入口从登录开始。
|
||||||
|
- `index.html`:登录页。
|
||||||
|
- `index.html?preview=desktops&theme=dark`:直接预览深色桌面列表。
|
||||||
|
- `index.html?preview=desktops&theme=light`:直接预览浅色桌面列表。
|
||||||
|
- `index.html?preview=settings&theme=dark`:桌面设置。
|
||||||
|
- `index.html?preview=connecting&theme=dark`:连接中。
|
||||||
|
- `index.html?preview=reconnect&theme=dark`:重连中。
|
||||||
|
- `index.html?preview=error&theme=dark`:连接异常。
|
||||||
|
|
||||||
|
正常流程:登录 → 桌面列表 → 设置或连接;`Ctrl+Alt+Shift+M` 打开管理设置,`Esc` 返回列表。设置中的「设备信息」提供退出登录。直接预览参数和管理快捷键仅用于原型评审,不代表生产权限控制。
|
||||||
|
|
||||||
|
收藏、搜索、全部 / 已收藏筛选、设置分类、参数校验、保存、取消均可交互。账号、企业入口和桌面连接设置仅保存在浏览器本地。
|
||||||
|
|
||||||
|
## 验证记录
|
||||||
|
|
||||||
|
本次登录交互验证:[login-verification.json](output/playwright/login-verification.json),包含账号登录、记住账号、服务地址校验、SSO 取消、离线体验、键盘操作和深浅色布局检查;截图以 `login-` 开头。
|
||||||
|
|
||||||
|
此前桌面工作区记录:[verification.json](output/playwright/verification.json)。它记录工作区的 27 项流程与布局检查及补充检查;其中「登录与旧基线完全一致」「点击直接进入桌面」描述的是添加登录交互之前的版本,已由本次登录验证取代。桌面列表记录的最小文字对比度:深色 5.04:1、浅色 4.74:1。1280×720、1366×768、1920×1080 的浏览器检查不代表 ARM 实机验收。
|
||||||
61
ui/terminal/assets/login-art.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
(() => {
|
||||||
|
const host=document.getElementById('ill');
|
||||||
|
host.querySelectorAll('.px,.work-art,#sky').forEach(e=>e.remove());
|
||||||
|
const canvas=document.createElement('canvas');canvas.className='kinetic-art';
|
||||||
|
canvas.setAttribute('role','button');canvas.tabIndex=0;
|
||||||
|
canvas.setAttribute('aria-label','互动树形 Logo:移动鼠标探索,点击或按回车释放光环');
|
||||||
|
host.prepend(canvas);
|
||||||
|
const W=780,H=1060,dpr=Math.min(devicePixelRatio||1,2),ctx=canvas.getContext('2d');
|
||||||
|
canvas.width=W*dpr;canvas.height=H*dpr;ctx.scale(dpr,dpr);
|
||||||
|
const dark=()=>document.body.dataset.theme==='dark';
|
||||||
|
const reduced=matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
|
const mark=new Image();mark.src='assets/mark.svg';
|
||||||
|
const plate=document.createElement('canvas');plate.width=1000;plate.height=850;
|
||||||
|
const paint=plate.getContext('2d');
|
||||||
|
const grain=document.createElement('canvas');grain.width=1000;grain.height=850;
|
||||||
|
const grainPaint=grain.getContext('2d');let seed=37;
|
||||||
|
const random=()=>{seed=(seed*16807)%2147483647;return seed/2147483647;};
|
||||||
|
for(let i=0;i<2200;i++){const x=random()*1000,y=random()*850;grainPaint.strokeStyle=i%3?'rgba(255,255,255,.12)':'rgba(153,111,54,.06)';grainPaint.lineWidth=.5;grainPaint.beginPath();grainPaint.moveTo(x,y);grainPaint.lineTo(x+8+random()*45,y-1);grainPaint.stroke();}
|
||||||
|
const p={x:0,y:0,tx:0,ty:0,mx:-1000,my:-1000,inside:false};
|
||||||
|
let ready=false,frame=0,clicks=0,last=0,raf=0;
|
||||||
|
const rings=[];
|
||||||
|
const leaves=[[.10,.46],[.25,.26],[.50,.13],[.75,.26],[.90,.46]];
|
||||||
|
function locate(e){const r=host.getBoundingClientRect();p.mx=(e.clientX-r.left)*W/r.width;p.my=(e.clientY-r.top)*H/r.height;p.tx=(p.mx/W-.5)*2;p.ty=(p.my/H-.5)*2;p.inside=true;}
|
||||||
|
host.addEventListener('pointermove',locate);
|
||||||
|
host.addEventListener('pointerleave',()=>{p.inside=false;p.tx=p.ty=0;p.mx=p.my=-1000;});
|
||||||
|
function pulse(){clicks++;canvas.dataset.clicks=clicks;if(!reduced.matches)rings.push({start:performance.now()});}
|
||||||
|
canvas.addEventListener('pointerdown',e=>{locate(e);pulse();});
|
||||||
|
canvas.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();pulse();}});
|
||||||
|
mark.onload=()=>{ready=true;canvas.dataset.ready='true';};
|
||||||
|
function draw(now){
|
||||||
|
const dt=Math.min(40,now-last||16);last=now;frame++;
|
||||||
|
const smooth=1-Math.exp(-dt/140);p.x+=(p.tx-p.x)*smooth;p.y+=(p.ty-p.y)*smooth;
|
||||||
|
ctx.clearRect(0,0,W,H);
|
||||||
|
const bg=ctx.createLinearGradient(0,0,W,H);bg.addColorStop(0,dark()?'#1B222B':'#FCF8F5');bg.addColorStop(1,dark()?'#262930':'#F5E6DA');ctx.fillStyle=bg;ctx.fillRect(0,0,W,H);
|
||||||
|
const t=reduced.matches?0:now*.001;
|
||||||
|
const cx=344+(reduced.matches?0:p.x*22),cy=658+(reduced.matches?0:p.y*15+Math.sin(t*.7)*7);
|
||||||
|
const w=470,h=398,left=cx-w/2,top=cy-h/2;
|
||||||
|
const halo=ctx.createRadialGradient(cx,cy,30,cx,cy,345);halo.addColorStop(0,dark()?'rgba(230,161,94,.10)':'rgba(255,255,255,.95)');halo.addColorStop(1,'rgba(230,161,94,0)');ctx.fillStyle=halo;ctx.fillRect(0,330,780,730);
|
||||||
|
ctx.save();ctx.translate(cx,cy+235);ctx.scale(1,.13);const shadow=ctx.createRadialGradient(0,0,10,0,0,245);shadow.addColorStop(0,dark()?'rgba(0,0,0,.22)':'rgba(151,92,46,.09)');shadow.addColorStop(1,'rgba(151,92,46,0)');ctx.fillStyle=shadow;ctx.fillRect(-260,-260,520,520);ctx.restore();
|
||||||
|
if(ready){
|
||||||
|
paint.clearRect(0,0,1000,850);paint.globalCompositeOperation='source-over';paint.drawImage(mark,0,0,1000,850);paint.globalCompositeOperation='source-in';
|
||||||
|
const lightX=reduced.matches?0:p.x*120+Math.sin(t*.35)*35;
|
||||||
|
const g=paint.createLinearGradient(-100+lightX,50,950+lightX,760);g.addColorStop(0,'#FFF0CE');g.addColorStop(.23,'#F1CF92');g.addColorStop(.43,'#E3B76E');g.addColorStop(.58,'#F9DFA8');g.addColorStop(.72,'#E9BF7B');g.addColorStop(1,'#F7DCA5');paint.fillStyle=g;paint.fillRect(0,0,1000,850);
|
||||||
|
paint.globalCompositeOperation='source-atop';
|
||||||
|
const sheen=paint.createLinearGradient(lightX,0,1000+lightX,500);sheen.addColorStop(0,'rgba(255,255,255,0)');sheen.addColorStop(.42,'rgba(255,255,255,0)');sheen.addColorStop(.53,'rgba(255,252,237,.32)');sheen.addColorStop(.64,'rgba(255,255,255,0)');sheen.addColorStop(1,'rgba(255,255,255,0)');paint.fillStyle=sheen;paint.fillRect(0,0,1000,850);paint.drawImage(grain,0,0);
|
||||||
|
ctx.globalAlpha=.07;for(let i=4;i>0;i-=1)ctx.drawImage(mark,left+i*.45,top+i*.7,w,h);ctx.globalAlpha=1;
|
||||||
|
ctx.save();ctx.filter='brightness(1.12)';ctx.drawImage(plate,left-.6,top-.8,w,h);ctx.restore();
|
||||||
|
ctx.drawImage(plate,left,top,w,h);
|
||||||
|
leaves.forEach(([x,y],i)=>{const lx=left+x*w,ly=top+y*h,d=Math.hypot(lx-p.mx,ly-p.my),active=p.inside?Math.max(0,1-d/115):0;
|
||||||
|
if(active>0){ctx.fillStyle=`rgba(255,231,188,${active*.6})`;ctx.shadowColor='#F8CE91';ctx.shadowBlur=22*active;ctx.beginPath();ctx.arc(lx,ly,5+active*8,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0;}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Sparse orbital sparks react locally to the pointer, leaving the silhouette clear.
|
||||||
|
for(let i=0;i<38;i++){const a=i*2.399+t*.09,r=255+(i%5)*13;let x=cx+Math.cos(a)*r,y=cy+Math.sin(a)*r*.81;const d=Math.hypot(x-p.mx,y-p.my);if(p.inside&&d<130&&!reduced.matches){x+=(p.mx-x)*.22;y+=(p.my-y)*.22;ctx.strokeStyle='rgba(217,139,70,.2)';ctx.beginPath();ctx.moveTo(x,y);ctx.lineTo(p.mx,p.my);ctx.stroke();}ctx.fillStyle=dark()?'rgba(246,196,133,.55)':'rgba(189,113,59,.42)';ctx.beginPath();ctx.arc(x,y,i%4===0?2:1,0,Math.PI*2);ctx.fill();}
|
||||||
|
for(let i=rings.length-1;i>=0;i--){const age=(now-rings[i].start)/1500;if(age>=1){rings.splice(i,1);continue;}ctx.strokeStyle=`rgba(230,161,94,${(1-age)*.6})`;ctx.lineWidth=1.5;ctx.beginPath();ctx.ellipse(cx,cy,70+age*300,50+age*235,0,0,Math.PI*2);ctx.stroke();}
|
||||||
|
canvas.dataset.pointer=`${p.x.toFixed(3)},${p.y.toFixed(3)}`;canvas.dataset.frame=frame;canvas.dataset.rings=rings.length;canvas.dataset.reduced=String(reduced.matches);
|
||||||
|
raf=requestAnimationFrame(draw);
|
||||||
|
}
|
||||||
|
document.addEventListener('visibilitychange',()=>{cancelAnimationFrame(raf);if(!document.hidden){last=0;raf=requestAnimationFrame(draw);}});
|
||||||
|
raf=requestAnimationFrame(draw);
|
||||||
|
})();
|
||||||
1
ui/terminal/assets/logo.svg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
1
ui/terminal/assets/mark.svg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
ui/terminal/assets/systems/kylinsoft.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
1
ui/terminal/assets/systems/ubuntu.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Ubuntu</title><path d="M17.61.455a3.41 3.41 0 0 0-3.41 3.41 3.41 3.41 0 0 0 3.41 3.41 3.41 3.41 0 0 0 3.41-3.41 3.41 3.41 0 0 0-3.41-3.41zM12.92.8C8.923.777 5.137 2.941 3.148 6.451a4.5 4.5 0 0 1 .26-.007 4.92 4.92 0 0 1 2.585.737A8.316 8.316 0 0 1 12.688 3.6 4.944 4.944 0 0 1 13.723.834 11.008 11.008 0 0 0 12.92.8zm9.226 4.994a4.915 4.915 0 0 1-1.918 2.246 8.36 8.36 0 0 1-.273 8.303 4.89 4.89 0 0 1 1.632 2.54 11.156 11.156 0 0 0 .559-13.089zM3.41 7.932A3.41 3.41 0 0 0 0 11.342a3.41 3.41 0 0 0 3.41 3.409 3.41 3.41 0 0 0 3.41-3.41 3.41 3.41 0 0 0-3.41-3.41zm2.027 7.866a4.908 4.908 0 0 1-2.915.358 11.1 11.1 0 0 0 7.991 6.698 11.234 11.234 0 0 0 2.422.249 4.879 4.879 0 0 1-.999-2.85 8.484 8.484 0 0 1-.836-.136 8.304 8.304 0 0 1-5.663-4.32zm11.405.928a3.41 3.41 0 0 0-3.41 3.41 3.41 3.41 0 0 0 3.41 3.41 3.41 3.41 0 0 0 3.41-3.41 3.41 3.41 0 0 0-3.41-3.41z"/></svg>
|
||||||
|
After Width: | Height: | Size: 948 B |
53
ui/terminal/index.html
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>越欣科技 · VDI 云终端</title>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
|
<style>
|
||||||
|
:root{--bg:#f3f5f7;--bg-side:#e9edf1;--card:#fff;--field:#f5f7f8;--line:#e0e5ea;--text:#273340;--muted:#7c8995;--accent:#c8753b;--accent-soft:#f6e9df;--ok:#4c9277;--warn:#b4853f;--shadow:0 22px 60px rgba(42,55,68,.12)}
|
||||||
|
[data-theme=dark]{--bg:#171f28;--bg-side:#202a35;--card:#242f3b;--field:#2b3744;--line:#3b4a59;--text:#eef2f5;--muted:#98a5b3;--accent:#c9783d;--accent-soft:#443229;--shadow:0 24px 60px rgba(0,0,0,.28)}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
html,body{margin:0;min-width:320px;min-height:100%;font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased}
|
||||||
|
button,input,select{font:inherit}button{cursor:pointer}.shell{position:relative;min-height:100vh;overflow:hidden;background:var(--bg)}
|
||||||
|
.topbar{position:absolute;z-index:10;top:0;left:0;right:0;height:78px;display:flex;align-items:center;justify-content:space-between;padding:0 34px;pointer-events:none}.top-actions{display:flex;align-items:center;gap:10px;pointer-events:auto}
|
||||||
|
.top-pill{height:42px;display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:0 15px;border:1px solid var(--line);border-radius:13px;background:color-mix(in srgb,var(--card) 92%,transparent);color:var(--muted);font-size:12px;line-height:1;white-space:nowrap;box-shadow:0 8px 22px rgba(35,48,60,.05)}.theme-pill{min-width:116px}
|
||||||
|
.view{display:none;min-height:100vh}.view.active{display:block}
|
||||||
|
.login-view{min-height:100vh}.login-layout{display:grid;grid-template-columns:49% 51%;min-height:100vh}
|
||||||
|
.brand-pane{position:relative;display:flex;align-items:center;overflow:hidden;padding:110px 8vw 82px;background:var(--bg-side)}.brand-pane:before{content:"";position:absolute;right:-180px;bottom:-230px;width:620px;height:620px;border:1px solid color-mix(in srgb,var(--accent) 28%,transparent);border-radius:50%}.brand-pane:after{content:"";position:absolute;right:70px;bottom:88px;width:180px;height:180px;border:1px solid color-mix(in srgb,var(--accent) 18%,transparent);border-radius:50%}
|
||||||
|
.brand-content{position:relative;z-index:1;max-width:500px}.wordmark{display:flex;align-items:center;gap:12px;margin-bottom:74px}.wordmark-mark{display:grid;place-items:center;width:42px;height:42px;border:2px solid var(--accent);border-radius:12px;color:var(--accent);font-size:18px;font-weight:800;letter-spacing:-3px}.wordmark-name{font-size:21px;font-weight:700;letter-spacing:2px}.wordmark-sub{margin-top:4px;color:var(--muted);font-size:10px;letter-spacing:1.8px}
|
||||||
|
.kicker{color:var(--accent);font-size:11px;font-weight:700;letter-spacing:2.2px}.hero-title{margin:18px 0 0;max-width:430px;font-size:clamp(40px,4.3vw,64px);line-height:1.16;letter-spacing:-.055em}.hero-note{margin-top:22px;color:var(--muted);font-size:15px;line-height:1.8}
|
||||||
|
.terminal-badge{display:flex;align-items:center;gap:12px;margin-top:58px;color:var(--muted);font-size:12px}.terminal-dot{width:10px;height:10px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 5px color-mix(in srgb,var(--ok) 14%,transparent)}.terminal-badge b{color:var(--text);font-weight:600}
|
||||||
|
.work-mark{position:absolute;right:12%;bottom:90px;width:250px;height:250px;opacity:.78}.work-mark:before,.work-mark:after{content:"";position:absolute;border:1px solid var(--accent);border-radius:50%}.work-mark:before{inset:30px}.work-mark:after{inset:76px}.work-mark i{position:absolute;left:50%;top:18px;width:2px;height:214px;background:var(--accent);transform:translateX(-50%);opacity:.65}.work-mark em{position:absolute;left:18px;right:18px;top:50%;height:2px;background:var(--accent);transform:translateY(-50%);opacity:.65}
|
||||||
|
.workspace-pane{display:flex;align-items:center;justify-content:center;padding:100px 7vw;background:var(--bg)}
|
||||||
|
.login-card{width:min(440px,100%);padding:40px 42px 32px;border:1px solid var(--line);border-radius:22px;background:var(--card);box-shadow:var(--shadow)}.login-card h1{margin:0;font-size:29px;letter-spacing:-.03em}.login-card .sub{margin:10px 0 30px;color:var(--muted);font-size:13px}.field{display:grid;gap:8px;margin:17px 0}.field label{font-size:12px;color:var(--muted)}.input{width:100%;height:50px;padding:0 15px;border:1px solid transparent;border-radius:11px;outline:0;color:var(--text);background:var(--field)}.input:focus{border-color:color-mix(in srgb,var(--accent) 55%,var(--line));background:var(--card)}.input::placeholder{color:#9ca8b4}.login-row{display:flex;align-items:center;justify-content:space-between;margin:15px 0 24px;color:var(--muted);font-size:12px}.remember{display:flex;align-items:center;gap:8px}.remember input{width:17px;height:17px;accent-color:var(--accent)}.link{border:0;padding:0;background:none;color:var(--accent);font-size:12px}.primary{width:100%;height:52px;border:0;border-radius:11px;color:#fff;background:var(--accent);font-size:14px;font-weight:700;letter-spacing:3px;box-shadow:0 8px 16px color-mix(in srgb,var(--accent) 22%,transparent)}.login-foot{display:flex;align-items:center;justify-content:center;gap:18px;margin-top:27px;padding-top:20px;border-top:1px solid var(--line);color:var(--muted);font-size:12px}.login-foot button{border:0;padding:0;background:none;color:inherit}.login-foot i{width:1px;height:14px;background:var(--line)}.device-strip{display:flex;justify-content:space-between;margin-top:23px;color:var(--muted);font-size:10px}.ok{color:var(--ok)}
|
||||||
|
.statusline{position:absolute;bottom:26px;left:34px;right:34px;display:flex;justify-content:space-between;color:var(--muted);font-size:10px}
|
||||||
|
.content{width:min(1100px,calc(100% - 80px));margin:0 auto;padding-top:126px}.page-head{display:flex;align-items:end;justify-content:space-between;margin-bottom:28px}.eyebrow{color:var(--accent);font-size:11px;font-weight:700;letter-spacing:2px}.page-title{margin:11px 0 0;font-size:34px;letter-spacing:-.04em}.user-tag{padding:10px 13px;border:1px solid var(--line);border-radius:10px;color:var(--muted);background:var(--card);font-size:12px}.desktop-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:17px}.desktop-card{overflow:hidden;border:1px solid var(--line);border-radius:17px;background:var(--card);box-shadow:0 12px 28px rgba(42,55,68,.06)}.card-banner{position:relative;display:flex;align-items:flex-start;justify-content:space-between;height:122px;padding:18px 20px;background:var(--accent-soft)}.card-banner.windows{background:#4a342a}.card-banner.blue{background:#dfeaf4}.card-banner.green{background:#deeee8}.os-icon{display:grid;place-items:center;width:56px;height:56px;border-radius:15px;color:var(--accent);background:#263341;font-size:20px}.os-icon svg{width:34px;height:34px;display:block;fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}.os-icon.kylin-logo{font-size:24px;font-weight:800;letter-spacing:-2px}.banner-copy{display:flex;flex-direction:column;align-items:flex-end;gap:5px;margin-top:3px;text-align:right}.banner-copy strong{font-size:16px;letter-spacing:.05em;line-height:1.1}.banner-copy span{font-size:11px;line-height:1.1;opacity:.82}.card-banner.windows .banner-copy{color:#fff}.card-banner.blue .banner-copy{color:#263746}.card-banner.green .banner-copy{color:#28463e}.card-body{padding:20px}.desktop-name{font-size:16px;font-weight:700}.desktop-os{margin-top:7px;color:var(--muted);font-size:12px}.tags{display:flex;gap:7px;margin-top:16px}.tag{padding:6px 8px;border-radius:7px;color:var(--muted);background:var(--field);font-size:11px}.card-actions{display:flex;align-items:center;justify-content:space-between;margin-top:21px}.state{color:var(--ok);font-size:12px}.state.pending{color:var(--warn)}.connect,.outline{padding:9px 14px;border-radius:9px;font-size:12px}.connect{border:0;color:#fff;background:var(--accent);font-weight:700}.connect:disabled{color:var(--muted);background:var(--field);cursor:not-allowed}.outline{border:1px solid var(--line);color:var(--muted);background:transparent}.list-status{display:flex;justify-content:space-between;margin-top:26px;color:var(--muted);font-size:11px}
|
||||||
|
.state-card{width:min(580px,calc(100% - 40px));margin:110px auto 0;padding:42px;border:1px solid var(--line);border-radius:22px;background:var(--card);box-shadow:var(--shadow);text-align:center}.state-icon{display:grid;place-items:center;width:58px;height:58px;margin:0 auto 22px;border-radius:16px;color:var(--accent);background:var(--accent-soft);font-size:23px}.state-card h2{margin:0 0 10px;font-size:26px}.state-card p{margin:0;color:var(--muted);font-size:13px;line-height:1.8}.progress{height:6px;margin:27px 0 17px;border-radius:10px;background:var(--field);overflow:hidden}.progress i{display:block;width:60%;height:100%;border-radius:inherit;background:var(--accent);animation:load 2.2s ease-in-out infinite alternate}@keyframes load{from{width:42%}to{width:76%}}.steps{display:flex;justify-content:center;gap:8px;flex-wrap:wrap}.step{padding:8px 11px;border:1px solid var(--line);border-radius:8px;color:var(--muted);background:var(--field);font-size:11px}.step.done{color:var(--ok)}.step.active{color:var(--accent)}
|
||||||
|
.admin-layout{display:grid;grid-template-columns:220px 1fr;gap:17px;width:min(920px,calc(100% - 40px));margin:0 auto;padding-top:126px}.admin-nav,.admin-panel{border:1px solid var(--line);border-radius:17px;background:var(--card);box-shadow:0 12px 28px rgba(42,55,68,.06)}.admin-nav{height:max-content;padding:11px}.admin-nav button{display:block;width:100%;padding:12px;border:0;border-radius:9px;background:transparent;color:var(--muted);text-align:left}.admin-nav button.active{color:var(--accent);background:var(--accent-soft);font-weight:700}.admin-nav .danger{margin-top:8px;color:#b56a6a}.admin-panel{padding:28px}.form-title{margin-top:13px;font-size:21px;font-weight:700}.form-note{margin-top:7px;color:var(--muted);font-size:12px}.setting-row{display:flex;align-items:center;justify-content:space-between;gap:25px;padding:18px 0;border-bottom:1px solid var(--line)}.setting-row:last-of-type{border-bottom:0}.setting-row b{font-size:13px}.setting-row small{display:block;margin-top:5px;color:var(--muted);font-size:11px}.setting-row .input{width:310px;height:44px}.panel-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:21px}
|
||||||
|
@media(max-width:980px){.login-layout{grid-template-columns:1fr}.brand-pane{min-height:500px;padding:110px 9vw 80px}.workspace-pane{min-height:620px;padding:70px 7vw 80px}.work-mark{right:8%;bottom:55px}.statusline{position:static;padding:0 7vw 24px}.desktop-grid{grid-template-columns:1fr}.content{width:calc(100% - 40px)}.admin-layout{grid-template-columns:1fr;padding-top:105px}.admin-panel{padding:22px}.setting-row{align-items:flex-start;flex-direction:column;gap:11px}.setting-row .input{width:100%}}
|
||||||
|
@media(max-width:600px){.topbar{height:70px;padding:0 18px}.top-pill{height:38px;padding:0 11px;font-size:11px}.theme-pill{min-width:0;font-size:0;width:38px}.theme-pill:before{content:"☼";font-size:15px}.brand-pane{min-height:450px;padding:100px 26px 65px}.wordmark{margin-bottom:52px}.hero-title{font-size:40px}.hero-note{font-size:13px}.work-mark{width:180px;height:180px;right:5%;bottom:50px}.terminal-badge{margin-top:37px}.workspace-pane{padding:45px 20px 70px}.login-card{padding:30px 22px 26px}.content{padding-top:100px}.page-head{align-items:flex-start;flex-direction:column;gap:16px}.state-card{margin-top:55px;padding:30px 22px}}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.login-view{min-height:100vh}.login-layout{grid-template-columns:56.2% 43.8%;background:#181f27}.login-view .brand-pane{position:relative;min-height:100vh;padding:0;background:#1b222b;overflow:hidden}.reference-ill{position:absolute;inset:0;overflow:hidden}.reference-ill .kinetic-art{position:absolute;inset:0;width:100%;height:100%;display:block}.reference-logo{position:absolute;left:56px;top:44px;width:230px;height:76px;object-fit:contain}.reference-copy{position:absolute;left:56px;top:150px;color:#e4e8ed;font-size:46px;font-weight:800;letter-spacing:3px;line-height:60px;pointer-events:none}.reference-copy span{display:block;margin-top:18px;color:#a5b1bf;font-size:14px;font-weight:400;letter-spacing:.5px;line-height:1.5}.login-view .workspace-pane{padding:0;background:#181f27;align-items:flex-start;justify-content:flex-start}.login-view .login-card{position:absolute;left:50.4%;top:18%;width:33.8vw;height:70vh;padding:5.3vh 3.75vw;border-radius:26px;background:#242d38;border:1px solid #394452;box-shadow:0 30px 80px rgba(0,0,0,.28)}.login-view .login-card .card-title{margin:20px 0 8px;color:#e4e8ed;font-size:30px;letter-spacing:1px}.login-view .login-card .card-sub{margin:0;color:#a5b1bf;font-size:13px}.login-view .login-card .field{display:grid;gap:8px;margin:22px 0}.login-view .login-card .field label{color:#e4e8ed;font-size:11.5px}.login-view .login-card .input{height:50px;padding:0 15px;border:1px solid transparent;border-radius:12px;outline:0;background:#29333f;color:#e4e8ed}.login-view .login-card .login-options{display:flex;align-items:center;justify-content:space-between;margin:20px 0;color:#e4e8ed;font-size:12px}.login-view .login-card .check{display:inline-flex;align-items:center;gap:8px}.login-view .login-card .check input{width:18px;height:18px;accent-color:#bd713b}.login-view .login-card .link-button{padding:0;border:0;background:none;color:#e6ab7d;font-size:12px}.login-view .login-card .primary{height:52px;margin-top:32px;border:0;border-radius:15px;background:#bd713b;color:#fff;font-weight:700;letter-spacing:2px}.login-view .login-card .card-footer{display:flex;align-items:center;justify-content:center;gap:22px;margin-top:42px;padding-top:22px;border-top:1px solid #34404d}.login-view .login-card .card-footer button{border:0;padding:0;background:none;color:#e4e8ed;font-size:12px}.login-view .login-card .card-footer button+button{padding-left:22px;border-left:1px solid #34404d}.login-view .statusline{display:none}
|
||||||
|
body[data-theme=light] .login-layout{background:#fff}body[data-theme=light] .login-view .workspace-pane{background:#fff}body[data-theme=light] .login-view .brand-pane{background:#fdf8f5}body[data-theme=light] .login-view .login-card{background:#fff;border-color:#efe2d9;box-shadow:0 25px 80px rgba(60,75,95,.1)}body[data-theme=light] .login-view .login-card .card-title{color:#1f2235}body[data-theme=light] .login-view .login-card .card-sub,body[data-theme=light] .login-view .login-card .field label,body[data-theme=light] .login-view .login-card .login-options,body[data-theme=light] .login-view .login-card .card-footer button{color:#46505c}body[data-theme=light] .login-view .login-card .input{background:#f6f7fb;color:#1f2235}body[data-theme=light] .login-view .reference-copy{color:#1f2235}body[data-theme=light] .login-view .reference-copy span{color:#7b8690}
|
||||||
|
@media(max-width:1300px) and (min-width:901px){.login-view .login-card{top:18%;height:auto;min-height:0;padding:30px 3.75vw 28px}.login-view .login-card .card-title{margin:0 0 8px;font-size:28px}.login-view .login-card .field{margin:16px 0}.login-view .login-card .input{height:48px}.login-view .login-card .login-options{margin:14px 0 18px}.login-view .login-card .primary{height:48px;margin-top:20px}.login-view .login-card .card-footer{margin-top:22px;padding-top:16px}}
|
||||||
|
@media(max-width:900px){.login-layout{grid-template-columns:1fr}.login-view .brand-pane{min-height:480px}.login-view .workspace-pane{min-height:620px}.login-view .login-card{position:relative;left:auto;right:auto;top:auto;width:min(470px,calc(100% - 40px));margin:70px auto 90px}.reference-logo{left:28px;top:35px;width:200px;height:66px}.reference-copy{left:28px;top:125px;font-size:36px;line-height:46px}.reference-copy span{font-size:12px}.topbar{padding:0 22px}}
|
||||||
|
</style>
|
||||||
|
<link rel="stylesheet" href="workspace.css">
|
||||||
|
<link rel="stylesheet" href="login-interactions.css">
|
||||||
|
</head>
|
||||||
|
<body data-theme="dark">
|
||||||
|
<div class="shell">
|
||||||
|
<header class="topbar"><div></div><div class="top-actions"><button class="top-pill" id="auth-service" type="button" aria-haspopup="dialog" aria-label="云端服务,打开连接设置">◉ <span id="auth-service-name">云端服务</span> ⌄</button><button class="top-pill theme-text" id="theme" aria-label="切换主题">▣ 深色外观</button></div></header>
|
||||||
|
<section class="view login-view active" id="login"><div class="login-layout"><div class="brand-pane"><div class="reference-ill" id="ill"></div><img class="reference-logo" src="assets/logo.svg" alt="百工万象"><div class="reference-copy">百工协作,<br>万象更新。<span>连接数字员工,让对话、流程与电脑操作自然协作。</span></div></div><div class="workspace-pane"><form class="login-card auth-enhanced" id="auth-form" novalidate><h2 class="card-title">登录工作台</h2><p class="card-sub auth-intro"><span id="auth-subtitle">使用云端工作账号继续</span><button type="button" id="auth-demo" class="auth-text-button">体验账号</button></p><div class="field"><label for="username">工作账号</label><input id="username" name="username" class="input" maxlength="100" aria-describedby="auth-feedback" placeholder="name@company.com" autocomplete="username"></div><div class="field"><label for="password">密码</label><div class="auth-password"><input id="password" name="password" class="input" type="password" placeholder="请输入密码" autocomplete="current-password" maxlength="256" aria-describedby="auth-feedback"><button class="auth-eye" id="auth-toggle-password" type="button" aria-label="显示密码" aria-pressed="false"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg></button></div></div><div class="login-options"><label class="check"><input type="checkbox" id="auth-remember" checked> <span>记住工作账号</span></label><button class="link-button" type="button" id="auth-forgot">忘记密码</button></div><button class="primary" type="submit" id="auth-submit">登 录</button><p id="auth-feedback" class="auth-feedback" role="status" aria-live="polite" aria-atomic="true"></p><div class="card-footer"><button type="button" id="auth-sso">◉ 企业 SSO</button><button type="button" id="auth-offline">▣ 离线体验</button></div></form></div></div></section>
|
||||||
|
<div class="terminal-workspace" id="workspace" hidden></div>
|
||||||
|
</div>
|
||||||
|
<dialog class="auth-dialog" id="auth-dialog" aria-labelledby="auth-dialog-title"><div id="auth-dialog-content"></div></dialog>
|
||||||
|
<script src="workspace.js"></script>
|
||||||
|
<script src="login-interactions.js"></script>
|
||||||
|
<script src="assets/login-art.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
64
ui/terminal/login-interactions.css
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/* Login interactions extend the established login design; brand art and layout remain owned by index.html. */
|
||||||
|
.login-view .login-card.auth-enhanced{height:auto;min-height:70vh}
|
||||||
|
@media(max-width:1300px){.login-view .login-card.auth-enhanced{min-height:0}}
|
||||||
|
.login-view .auth-intro{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
||||||
|
.login-view .auth-text-button{border:0;background:none;padding:4px 0;color:#e6ab7d;font-size:12px;line-height:1.5;white-space:nowrap;cursor:pointer}
|
||||||
|
.login-view .auth-password{position:relative}
|
||||||
|
.login-view .auth-password .input{display:block;padding-right:49px!important}
|
||||||
|
.login-view .auth-eye{display:grid;place-items:center;position:absolute;right:5px;top:50%;transform:translateY(-50%);width:40px;height:40px;padding:0;border:0;border-radius:8px;background:none;color:#bec9d5;cursor:pointer}
|
||||||
|
.auth-eye svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
.login-view .auth-eye:hover{background:rgba(150,164,180,.12)}
|
||||||
|
.login-view .auth-enhanced .primary{display:inline-flex;justify-content:center;align-items:center;gap:10px;margin-top:20px!important;cursor:pointer}
|
||||||
|
.login-view .auth-feedback{min-height:20px;margin:10px 0 0;line-height:20px;letter-spacing:0;color:#b6c2cf;font-size:12px}
|
||||||
|
.login-view .auth-feedback.error{color:#ffb7ad}
|
||||||
|
.login-view .auth-enhanced .input[aria-invalid=true]{border:1px solid #e7978a!important}
|
||||||
|
.login-view .auth-enhanced .card-footer{margin-top:18px!important}
|
||||||
|
.login-view .auth-enhanced[aria-busy=true] .primary{opacity:.8}
|
||||||
|
.login-view .auth-enhanced :disabled{cursor:wait}
|
||||||
|
.login-view .auth-enhanced input:disabled{opacity:.75}
|
||||||
|
.login-view .auth-enhanced :focus-visible,#auth-service:focus-visible{outline:2px solid #edb78f;outline-offset:3px}
|
||||||
|
.auth-spinner{width:15px;height:15px;border-radius:50%;border:2px solid rgba(255,255,255,.35);border-top-color:#fff;animation:auth-spin .9s linear infinite}
|
||||||
|
@keyframes auth-spin{to{transform:rotate(360deg)}}
|
||||||
|
.auth-dialog{color-scheme:dark;--auth-bg:#242d38;--auth-input:#293540;--auth-text:#edf1f5;--auth-secondary:#bac5cf;--auth-line:#445362;--auth-accent:#efb38a;--auth-wash:#382e28;--auth-error:#ffb7ad;--auth-button:#a95829;width:min(480px,calc(100% - 40px));max-height:calc(100vh - 40px);max-height:calc(100dvh - 40px);overflow:auto;margin:auto;padding:30px;border:1px solid var(--auth-line);border-radius:24px;background:var(--auth-bg);color:var(--auth-text);box-shadow:0 25px 80px rgba(0,0,0,.3);font-size:14px;line-height:1.65}
|
||||||
|
.auth-dialog:not([open]){display:none}
|
||||||
|
.auth-dialog::backdrop{background:rgba(9,15,23,.6);backdrop-filter:blur(3px)}
|
||||||
|
.auth-dialog-header{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;margin-bottom:21px}
|
||||||
|
.auth-dialog-header h2{font-size:23px;line-height:1.4;margin:0;font-weight:650;letter-spacing:-.3px}
|
||||||
|
.auth-dialog-sub{font-size:13px;color:var(--auth-secondary);margin:8px 0 0}
|
||||||
|
.auth-dialog-close{display:grid;place-items:center;width:36px;height:36px;border:0;border-radius:9px;background:var(--auth-input);color:var(--auth-secondary);font-size:23px;line-height:1;flex-shrink:0}
|
||||||
|
.auth-dialog :focus-visible{outline:2px solid var(--auth-accent);outline-offset:3px}
|
||||||
|
.auth-dialog-tabs{display:grid;grid-template-columns:1fr 1fr;gap:5px;padding:4px;background:var(--auth-input);border:1px solid var(--auth-line);border-radius:12px;margin-bottom:24px}
|
||||||
|
.auth-dialog-tabs button{min-height:40px;border:0;border-radius:8px;background:none;color:var(--auth-secondary);font-size:14px}
|
||||||
|
.auth-dialog-tabs button[aria-selected=true]{color:var(--auth-accent);background:var(--auth-wash);font-weight:650}
|
||||||
|
.auth-dialog .auth-field{display:grid;gap:8px;margin:18px 0;color:var(--auth-text);font-size:14px}
|
||||||
|
.auth-dialog .auth-input{height:46px;width:100%;min-width:0;border:1px solid var(--auth-line);border-radius:11px;padding:0 13px;background:var(--auth-input);color:var(--auth-text);outline:none}
|
||||||
|
.auth-dialog .auth-input[aria-invalid=true]{border-color:var(--auth-error)}
|
||||||
|
.auth-dialog .auth-input::placeholder{color:var(--auth-secondary)}
|
||||||
|
.auth-dialog small{font-size:12px;color:var(--auth-secondary);line-height:1.7}
|
||||||
|
.auth-dialog .auth-note{padding:14px 16px;border:1px solid var(--auth-line);border-radius:12px;background:var(--auth-input);color:var(--auth-secondary);font-size:13px;line-height:1.8;margin:20px 0}
|
||||||
|
.auth-dialog .auth-note strong{color:var(--auth-text);font-weight:600}
|
||||||
|
.auth-dialog .auth-note p{margin:5px 0 0}
|
||||||
|
.auth-dialog .auth-note dd{margin:0;overflow-wrap:anywhere;color:var(--auth-text)}
|
||||||
|
.auth-dialog .auth-note dt{color:var(--auth-secondary);font-size:12px}
|
||||||
|
.auth-dialog .auth-note>div+div{margin-top:12px}
|
||||||
|
.auth-dialog-error{min-height:21px;font-size:13px;color:var(--auth-error);margin:12px 0 0}
|
||||||
|
.auth-dialog-actions{display:flex;justify-content:flex-end;align-items:center;gap:11px;margin-top:23px}
|
||||||
|
.auth-dialog-button{display:inline-flex;align-items:center;justify-content:center;gap:10px;min-height:44px;padding:0 17px;border:1px solid var(--auth-line);border-radius:11px;background:transparent;color:var(--auth-text);font-size:14px;white-space:nowrap}
|
||||||
|
.auth-dialog-button.primary{width:auto;margin:0;letter-spacing:0;border-color:var(--auth-button);background:var(--auth-button);color:#fff;box-shadow:none;height:auto}
|
||||||
|
.auth-dialog-button:disabled{opacity:.7;cursor:wait}
|
||||||
|
.auth-dialog .auth-checkbox{display:flex;align-items:flex-start;gap:9px;font-size:13px;color:var(--auth-secondary);margin-top:18px}
|
||||||
|
.auth-dialog .auth-checkbox input{accent-color:var(--auth-button);width:17px;height:17px;flex-shrink:0;margin-top:3px}
|
||||||
|
.auth-demo-credentials{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin:20px 0;background:var(--auth-input);border:1px solid var(--auth-line);border-radius:13px;padding:18px}
|
||||||
|
.auth-demo-credentials dt{color:var(--auth-secondary);font-size:13px}
|
||||||
|
.auth-demo-credentials dd{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:17px;color:var(--auth-text);margin:5px 0 0}
|
||||||
|
body[data-theme=light] .login-view .auth-text-button{color:#8b4720}
|
||||||
|
body[data-theme=light] .login-view .auth-eye{color:#667785}
|
||||||
|
body[data-theme=light] .login-view .auth-feedback{color:#536372}
|
||||||
|
body[data-theme=light] .login-view .auth-feedback.error{color:#aa3f32}
|
||||||
|
body[data-theme=light] .login-view .auth-enhanced .input[aria-invalid=true]{border-color:#bd5344!important}
|
||||||
|
body[data-theme=light] .login-view .auth-enhanced :focus-visible,body[data-theme=light] #auth-service:focus-visible{outline-color:#954b22}
|
||||||
|
body[data-theme=light] .auth-dialog{color-scheme:light;--auth-bg:#fff;--auth-input:#f5f6f7;--auth-text:#283440;--auth-secondary:#536372;--auth-line:#d8dfe5;--auth-accent:#954b22;--auth-wash:#faeee5;--auth-error:#aa3f32;--auth-button:#a95829;box-shadow:0 25px 80px rgba(25,38,50,.18)}
|
||||||
|
@media(max-width:600px){.auth-dialog{padding:24px 20px;border-radius:20px}.auth-dialog-header h2{font-size:21px}.auth-dialog-actions{flex-wrap:wrap}.auth-dialog-button{flex:1}.auth-demo-credentials{gap:12px;padding:14px}}
|
||||||
|
@media(prefers-reduced-motion:reduce){.auth-spinner{animation:none}}
|
||||||
|
|
||||||
|
.auth-dialog-status{min-height:21px;font-size:13px;color:var(--auth-secondary);margin:12px 0 0}
|
||||||
187
ui/terminal/login-interactions.js
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
(() => {
|
||||||
|
'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();
|
||||||
|
})();
|
||||||
BIN
ui/terminal/output/playwright/connecting-dark.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
ui/terminal/output/playwright/connecting-light.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
ui/terminal/output/playwright/desktops-system-logos-light.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
ui/terminal/output/playwright/desktops-system-logos.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
ui/terminal/output/playwright/error-dark.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
ui/terminal/output/playwright/error-light.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
ui/terminal/output/playwright/login-baseline-dark.png
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
ui/terminal/output/playwright/login-demo-dark.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
ui/terminal/output/playwright/login-demo-light.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
ui/terminal/output/playwright/login-dialog-390.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
ui/terminal/output/playwright/login-dialog-820.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
ui/terminal/output/playwright/login-enterprise-dark.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
ui/terminal/output/playwright/login-enterprise-light.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
ui/terminal/output/playwright/login-forgot-dark.png
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
ui/terminal/output/playwright/login-forgot-light.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
ui/terminal/output/playwright/login-interactive-dark-1280.png
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
ui/terminal/output/playwright/login-interactive-dark-1366.png
Normal file
|
After Width: | Height: | Size: 311 KiB |
BIN
ui/terminal/output/playwright/login-interactive-dark-1920.png
Normal file
|
After Width: | Height: | Size: 537 KiB |
BIN
ui/terminal/output/playwright/login-interactive-dark.png
Normal file
|
After Width: | Height: | Size: 311 KiB |
BIN
ui/terminal/output/playwright/login-interactive-light-1280.png
Normal file
|
After Width: | Height: | Size: 320 KiB |
BIN
ui/terminal/output/playwright/login-interactive-light-1366.png
Normal file
|
After Width: | Height: | Size: 325 KiB |
BIN
ui/terminal/output/playwright/login-interactive-light-1920.png
Normal file
|
After Width: | Height: | Size: 557 KiB |
BIN
ui/terminal/output/playwright/login-interactive-light.png
Normal file
|
After Width: | Height: | Size: 325 KiB |
BIN
ui/terminal/output/playwright/login-offline-dark.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
ui/terminal/output/playwright/login-offline-light.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
ui/terminal/output/playwright/login-preserved-dark.png
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
ui/terminal/output/playwright/login-preserved-light.png
Normal file
|
After Width: | Height: | Size: 322 KiB |
BIN
ui/terminal/output/playwright/login-sso-dark.png
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
ui/terminal/output/playwright/login-sso-light.png
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
ui/terminal/output/playwright/login-success-dark.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
ui/terminal/output/playwright/login-validation-dark.png
Normal file
|
After Width: | Height: | Size: 315 KiB |
BIN
ui/terminal/output/playwright/login-validation-light.png
Normal file
|
After Width: | Height: | Size: 329 KiB |
1054
ui/terminal/output/playwright/login-verification.json
Normal file
BIN
ui/terminal/output/playwright/reconnect-dark.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
ui/terminal/output/playwright/reconnect-light.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
ui/terminal/output/playwright/review.png
Normal file
|
After Width: | Height: | Size: 750 KiB |
BIN
ui/terminal/output/playwright/settings-dark.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
ui/terminal/output/playwright/settings-light.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
1308
ui/terminal/output/playwright/verification.json
Normal file
BIN
ui/terminal/output/playwright/workspace-dark-1280.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
ui/terminal/output/playwright/workspace-dark-1366.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
ui/terminal/output/playwright/workspace-dark-1920.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
ui/terminal/output/playwright/workspace-dark-390.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
ui/terminal/output/playwright/workspace-dark-820.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
ui/terminal/output/playwright/workspace-light-1280.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
ui/terminal/output/playwright/workspace-light-1366.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
ui/terminal/output/playwright/workspace-light-1920.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
61
ui/terminal/references/README.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# VDI 终端界面参考与设计依据
|
||||||
|
|
||||||
|
核对日期:2026-11-09。以下截图从官方文档的图片链接取得,作为研究材料保存在本目录,不放入产品界面。原始截图中的第三方品牌、组织和账号不用于越欣科技终端。
|
||||||
|
|
||||||
|
## 1. Microsoft Windows App:桌面资源选择
|
||||||
|
|
||||||
|
- 官方说明:https://learn.microsoft.com/en-us/windows-app/get-started-connect-devices-desktops-apps
|
||||||
|
- Windows 客户端截图:https://learn.microsoft.com/en-us/windows-app/media/get-started-connect-devices-desktops-apps/windows-devices.png
|
||||||
|
- Web 客户端截图:https://learn.microsoft.com/en-us/windows-app/media/get-started-connect-devices-desktops-apps/web-devices.png
|
||||||
|
- 本地材料:[Windows 客户端](windows-app-devices.png)、[Web 客户端](windows-app-web-devices.png)。
|
||||||
|
|
||||||
|
实际界面包含:设备页、桌面名称、服务类型、配置或组织、可连接状态、连接按钮,以及资源搜索和收藏入口。
|
||||||
|
|
||||||
|
采用:以桌面名称为主标题,状态靠近名称;每张桌面有独立连接入口;支持搜索和收藏。
|
||||||
|
|
||||||
|
调整:不采用截图的壁纸和文字叠层。越欣终端使用纯色卡片,让系统标识、名称、状态、配置、操作从上到下排列,消除深浅背景交叉造成的对比问题。三张桌面直接展示,不引入复杂导航或多工作区概念。
|
||||||
|
|
||||||
|
## 2. Amazon WorkSpaces:资源管理、设置与会话诊断
|
||||||
|
|
||||||
|
- 官方用户手册:https://docs.aws.amazon.com/workspaces/latest/userguide/amazon-workspaces-windows-client.html
|
||||||
|
- 资源列表截图:https://docs.aws.amazon.com/images/workspaces/latest/userguide/images/wsp-windows-newclient-workspace-list.png
|
||||||
|
- 显示设置截图:https://docs.aws.amazon.com/images/workspaces/latest/userguide/images/wsp-windows-newclient-display-fullscreen.png
|
||||||
|
- 会话诊断截图:https://docs.aws.amazon.com/images/workspaces/latest/userguide/images/wsp-windows-newclient-session-health-panel1.png
|
||||||
|
- 本地材料:[资源列表](workspaces-list.png)、[显示设置](workspaces-settings.png)、[会话诊断](workspaces-session-health.png)。
|
||||||
|
|
||||||
|
版本说明:官方手册将该组新客户端界面标为 Public Preview;它是成熟 WorkSpaces 产品中的新界面参考,不表述为已全面发布版本。
|
||||||
|
|
||||||
|
实际界面包含:收藏、资源名称与操作分离;设置按连接、显示、外观等类别组织;会话健康度以明确的网络状态和指标呈现。
|
||||||
|
|
||||||
|
采用:收藏与连接互不混用;设置入口固定;配置字段有名称与说明;连接中、取消、错误恢复有明确路径。
|
||||||
|
|
||||||
|
调整:当前 UI 原型不接入设备遥测或 WorkSpaces 服务,不显示捏造的延迟、带宽或实时健康度。网络状态只作示例,并在页脚标明。复杂诊断图表不塞入三台桌面的选择页。
|
||||||
|
|
||||||
|
## 3. Omnissa Horizon:按资源设置与连接流程
|
||||||
|
|
||||||
|
- 官方指南:https://docs.omnissa.com/HorizonClient-WindowsGuide-V2609/ConnecttoaRemoteDesktoporPublishedApplicationfromHorizonWindowsClient
|
||||||
|
|
||||||
|
已核对官方流程文字:登录后进入桌面与应用选择器;可在桌面资源上打开 Settings 调整显示;连接以具体资源为单位。本文仅作为操作流程依据,没有将其称为已取得的界面截图。
|
||||||
|
|
||||||
|
采用:每台桌面独立“设置”和“连接”;返回列表不退出账号;未配置地址时解释原因并保留设置入口。
|
||||||
|
|
||||||
|
## 4. 与本仓库代码的对应
|
||||||
|
|
||||||
|
- `terminal/kiosk-shell/src/desktop_list_page.cpp`:每张卡片都有“设置”,可用桌面有“连接”,忙碌时禁止重复操作。
|
||||||
|
- `terminal/kiosk-shell/src/desktop_settings_dialog.cpp`:服务器地址、应用名、分辨率、帧率;地址留空表示未分配,桌面不可连接。
|
||||||
|
- 原型的配置字段与上述真实代码对应;不增加没有来源的 CPU、内存、显卡容量或实时质量指标。
|
||||||
|
|
||||||
|
## 5. 保留的视觉系统与可读性规则
|
||||||
|
|
||||||
|
- 登录界面的 Logo、互动图形和深浅色视觉沿用原稿。后续按要求补充表单校验、登录状态、企业连接、SSO 和离线体验;相关浏览器验证见 `../output/playwright/login-verification.json`。
|
||||||
|
- 登录后沿用深蓝灰背景、铜橙色强调、圆角卡片与低对比边框;去掉整块浅蓝、浅绿色带。
|
||||||
|
- 系统识别只占卡片左上角:Windows 窗格图形、Ubuntu 官方形态图形、麒麟软件官方品牌图片,加明确的系统名称。色彩局部使用,连接按钮统一铜橙。
|
||||||
|
- Ubuntu 图形来自 Simple Icons 的 Ubuntu 资源:https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/ubuntu.svg 。
|
||||||
|
- 麒麟软件图片直接来自官网:https://www.kylinos.cn/upload/1/appLogo/20250820/1957999505938382848.png 。该图是麒麟软件公司标识,不冒充单独的银河麒麟产品 Logo。
|
||||||
|
- 主要桌面名称 22–24 px;正文和操作 14–16 px;辅助规格和设备状态 13 px。小字号正文按 WCAG AA 4.5:1 校验;大字号至少 3:1。
|
||||||
|
- 常用操作至少 44 px 高,有文字标签;收藏按钮同时提供名称与选中状态;不可连接状态同时提供原因。
|
||||||
|
- 深色、浅色、1280×720 / 1366×768 / 1920×1080 都独立渲染;不将“语法检查通过”等同于视觉验收。
|
||||||
|
|
||||||
|
## 6. 交付边界
|
||||||
|
|
||||||
|
这是 `ui/terminal` 下的 HTML 交互设计原型;连接配置保存在当前浏览器的本地存储供评审使用。真实终端认证、Moonlight 会话、设备遥测和 Qt 落地仍由 kiosk-shell 接入。原型不伪装远程连接成功,连接状态在预览等待后进入说明页,并提供检查设置的恢复路径。
|
||||||
32
ui/terminal/references/sources.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"file": "windows-app-devices.png",
|
||||||
|
"url": "https://learn.microsoft.com/en-us/windows-app/media/get-started-connect-devices-desktops-apps/windows-devices.png",
|
||||||
|
"sha256": "aabb6d5577ab57f665b9a975851d5b965d1841158d6d419b6e9bccd6de93b5e2",
|
||||||
|
"retrieved_on": "2026-11-09"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "windows-app-web-devices.png",
|
||||||
|
"url": "https://learn.microsoft.com/en-us/windows-app/media/get-started-connect-devices-desktops-apps/web-devices.png",
|
||||||
|
"sha256": "8092d18695fc6e98272dd2019698a82ffc21ddc987dcaebe990ba0520dde3964",
|
||||||
|
"retrieved_on": "2026-11-09"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "workspaces-list.png",
|
||||||
|
"url": "https://docs.aws.amazon.com/images/workspaces/latest/userguide/images/wsp-windows-newclient-workspace-list.png",
|
||||||
|
"sha256": "04290c829884ad531a8b2a75fb7901370ffcea00753b684ff0de22c8d892955f",
|
||||||
|
"retrieved_on": "2026-11-09"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "workspaces-settings.png",
|
||||||
|
"url": "https://docs.aws.amazon.com/images/workspaces/latest/userguide/images/wsp-windows-newclient-display-fullscreen.png",
|
||||||
|
"sha256": "169323403765285c30ac6106b8b53a59d6e17a6c43ef9edd30d37c44bd61811b",
|
||||||
|
"retrieved_on": "2026-11-09"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "workspaces-session-health.png",
|
||||||
|
"url": "https://docs.aws.amazon.com/images/workspaces/latest/userguide/images/wsp-windows-newclient-session-health-panel1.png",
|
||||||
|
"sha256": "16bdcd0ea25fffdd9957c0d9bb53f5aac474c6be81267c503c6db5987edb2a0e",
|
||||||
|
"retrieved_on": "2026-11-09"
|
||||||
|
}
|
||||||
|
]
|
||||||
BIN
ui/terminal/references/windows-app-devices.png
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
ui/terminal/references/windows-app-web-devices.png
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
ui/terminal/references/workspaces-list.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
ui/terminal/references/workspaces-session-health.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
ui/terminal/references/workspaces-settings.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
12
ui/terminal/review.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>越欣科技 VDI · 界面评审</title><link rel="icon" href="data:,"><style>
|
||||||
|
*{box-sizing:border-box}body{margin:0;background:#f5f6f7;color:#263340;font:15px/1.65 -apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif}main{max-width:1360px;margin:0 auto;padding:40px 32px 70px}header{display:flex;justify-content:space-between;align-items:center;gap:25px;margin-bottom:30px}h1{font-size:28px;letter-spacing:-.5px;margin:0 0 6px}p{margin:0;color:#536372}a{color:#a95121;text-decoration:none}a:hover{text-decoration:underline}.primary{display:inline-block;padding:12px 19px;border-radius:11px;background:#a95121;color:#fff;white-space:nowrap}section{margin-bottom:40px}h2{font-size:21px;margin:0 0 14px}h3{font-size:17px;margin:0 0 9px}figure{margin:0}figure img{width:100%;display:block;border:1px solid #dce2e7;border-radius:14px}figcaption{margin-top:10px;font-size:13px;color:#536372}.tabs{display:flex;gap:8px;margin-bottom:16px}.tabs button{font:inherit;border:1px solid #dce2e7;background:#fff;color:#536372;padding:8px 18px;border-radius:9px;cursor:pointer}.tabs button.active{background:#f8eee6;color:#a95121;border-color:#d7b29a}.grid{display:grid;grid-template-columns:1fr 1fr;gap:25px}.note{padding:23px 26px;background:#fff;border:1px solid #dce2e7;border-radius:14px}.note ul{padding-left:20px;margin:10px 0 0;color:#536372}.thumb img{height:340px;object-fit:contain;background:#fff}.small{font-size:13px;color:#536372}.metrics{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}.metrics strong{font-size:24px;display:block}.metrics span{font-size:13px;color:#536372}footer{border-top:1px solid #dce2e7;padding-top:23px;font-size:13px;color:#536372}@media(max-width:700px){main{padding:24px 18px}.grid,.metrics{grid-template-columns:1fr}header{align-items:flex-start;flex-direction:column}.thumb img{height:auto}}
|
||||||
|
</style></head><body><main>
|
||||||
|
<header><div><h1>越欣科技 VDI · 终端界面</h1><p>沿用深蓝灰、铜橙和圆角体系,包含登录校验、企业连接与桌面选择、设置和连接状态。</p></div><a class="primary" href="index.html">打开交互原型 ↗</a></header>
|
||||||
|
<section><div class="tabs"><button class="active" data-theme="dark">深色外观</button><button data-theme="light">浅色外观</button></div><figure><img id="main-preview" src="output/playwright/workspace-dark-1366.png" alt="越欣科技终端桌面选择页"><figcaption>1366 × 768 实际浏览器渲染。卡片使用统一底色,系统识别、桌面名称、状态、规格、操作依次排列。</figcaption></figure></section>
|
||||||
|
<section><h2>登录交互</h2><figure><a href="index.html"><img id="login-preview" src="output/playwright/login-interactive-dark.png" alt="可交互的终端登录界面"></a><figcaption>体验账号:demo · 密码:Demo123!。支持输入校验、记住账号、密码显示、企业连接、SSO 取消、忘记密码和离线体验;认证为本地模拟。</figcaption></figure></section>
|
||||||
|
<section class="metrics"><div class="note"><strong>5.04 : 1</strong><span>深色桌面列表的最小文字对比度</span></div><div class="note"><strong>4.74 : 1</strong><span>浅色桌面列表的最小文字对比度</span></div><div class="note"><strong>1280 × 720</strong><span>三张卡片、连接操作与状态条完整可见</span></div></section>
|
||||||
|
<section><h2>参考真实 VDI 界面,保留自己的视觉风格</h2><div class="grid"><figure class="thumb"><img src="references/windows-app-devices.png" alt="微软官方 Windows App 设备选择页"><figcaption><a href="https://learn.microsoft.com/en-us/windows-app/get-started-connect-devices-desktops-apps">Microsoft Windows App · 官方设备页</a></figcaption></figure><figure class="thumb"><img src="references/workspaces-list.png" alt="AWS 官方 WorkSpaces 资源管理界面"><figcaption><a href="https://docs.aws.amazon.com/workspaces/latest/userguide/amazon-workspaces-windows-client.html">Amazon WorkSpaces · 官方资源列表</a>(新客户端 Public Preview)</figcaption></figure></div><div class="grid" style="margin-top:20px"><div class="note"><h3>采用的结构</h3><ul><li>系统标识与桌面名称分层,状态靠近桌面名称。</li><li>搜索、收藏、单资源连接入口各自独立。</li><li>设置与连接分开,未配置时显示原因。</li></ul></div><div class="note"><h3>针对 ARM 终端调整</h3><ul><li>取消大块彩色卡头、壁纸叠字和重复系统名称。</li><li>连接操作至少 44 px,适配 720p / 1080p 显示器。</li><li>连接设置对应 kiosk-shell 的地址、应用、分辨率、帧率。</li></ul></div></div><p class="small" style="margin-top:16px">另核对 <a href="https://docs.omnissa.com/HorizonClient-WindowsGuide-V2609/ConnecttoaRemoteDesktoporPublishedApplicationfromHorizonWindowsClient">Omnissa Horizon 官方按桌面设置与连接流程</a>。完整来源和采用理由见 <a href="references/README.md">设计依据</a>。</p></section>
|
||||||
|
<section><h2>同一套风格覆盖设置与状态页</h2><div class="grid"><figure><a href="index.html?preview=settings&theme=dark"><img src="output/playwright/settings-dark.png" alt="桌面连接设置"></a><figcaption>连接设置:目标桌面、地址、应用名、分辨率与帧率。</figcaption></figure><figure><a href="index.html?preview=connecting&theme=dark"><img src="output/playwright/connecting-dark.png" alt="正在连接桌面"></a><figcaption>连接过程:选定桌面、当前步骤、取消和恢复路径。</figcaption></figure></div></section>
|
||||||
|
<footer>登录页保留原有品牌图形与布局,新增表单操作、状态提示和弹窗。交付为 HTML 交互设计原型,设备状态为示例;尚未接入认证、Moonlight 或 ARM 实机。<a href="README.md">使用说明</a> · <a href="output/playwright/login-verification.json">登录验证</a> · <a href="output/playwright/verification.json">此前工作区验证</a></footer>
|
||||||
|
</main><script>document.querySelectorAll('[data-theme]').forEach(button=>button.addEventListener('click',()=>{document.querySelectorAll('[data-theme]').forEach(b=>b.classList.toggle('active',b===button));document.getElementById('main-preview').src='output/playwright/workspace-'+button.dataset.theme+'-1366.png';document.getElementById('login-preview').src='output/playwright/login-interactive-'+button.dataset.theme+'.png'}));</script></body></html>
|
||||||
134
ui/terminal/workspace.css
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
/* Login has its own established styles. All rules here are scoped to the terminal workspace. */
|
||||||
|
body.in-workspace .topbar{display:none}
|
||||||
|
.terminal-workspace[hidden]{display:none!important}
|
||||||
|
.terminal-workspace{--ws-bg:#f8f9fa;--ws-panel:#fff;--ws-soft:#f0f3f5;--ws-hover:#f5f7f8;--ws-line:#dce2e7;--ws-ink:#25313d;--ws-secondary:#536372;--ws-muted:#596a79;--ws-accent:#a95121;--ws-button:#a95121;--ws-button-hover:#93451b;--ws-tint:#f9eee6;--ws-good:#24714f;--ws-good-bg:#e9f5ee;--ws-warn:#81581b;--ws-warn-bg:#f9f1df;--ws-shadow:0 6px 22px rgba(33,48,61,.045);min-height:100vh;background:var(--ws-bg);color:var(--ws-ink);font-size:15px;line-height:1.55;display:flex;flex-direction:column}
|
||||||
|
body[data-theme=dark] .terminal-workspace{color-scheme:dark;--ws-bg:#1b232d;--ws-panel:#242e39;--ws-soft:#2b3743;--ws-hover:#2b3642;--ws-line:#405060;--ws-ink:#edf1f5;--ws-secondary:#bac5cf;--ws-muted:#a4b2c0;--ws-accent:#edac7d;--ws-button:#aa5928;--ws-button-hover:#9c5125;--ws-tint:#3c3029;--ws-good:#9cdbbc;--ws-good-bg:#263d35;--ws-warn:#efcd8e;--ws-warn-bg:#413a2a;--ws-shadow:none}
|
||||||
|
.terminal-workspace *{box-sizing:border-box}
|
||||||
|
.terminal-workspace button,.terminal-workspace input,.terminal-workspace select{font:inherit}
|
||||||
|
.terminal-workspace button{cursor:pointer}
|
||||||
|
.terminal-workspace button:disabled{cursor:not-allowed}
|
||||||
|
.terminal-workspace :focus-visible{outline:3px solid var(--ws-accent);outline-offset:4px}
|
||||||
|
.ws-icon{width:20px;height:20px;display:block;flex-shrink:0;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
.ws-header{height:84px;flex-shrink:0;padding:0 max(32px,calc((100vw - 1280px)/2));display:flex;align-items:center;justify-content:space-between;gap:24px;border-bottom:1px solid var(--ws-line);background:var(--ws-panel)}
|
||||||
|
.ws-brand{display:flex;align-items:center;gap:18px;white-space:nowrap}
|
||||||
|
.ws-brand strong{font-size:22px;letter-spacing:2px;font-weight:650}
|
||||||
|
.ws-brand span{border-left:1px solid var(--ws-line);padding-left:18px;color:var(--ws-secondary);font-size:15px}
|
||||||
|
.ws-header-actions,.ws-action-group{display:flex;align-items:center;gap:10px}
|
||||||
|
.ws-btn{display:inline-flex;align-items:center;justify-content:center;gap:9px;min-height:44px;padding:0 16px;border:1px solid var(--ws-line);border-radius:11px;color:var(--ws-ink);background:var(--ws-panel);font-size:14px;font-weight:550;line-height:1.25;white-space:nowrap}
|
||||||
|
.ws-btn:hover{background:var(--ws-hover)}
|
||||||
|
.ws-btn.ws-primary{border-color:var(--ws-button);background:var(--ws-button);color:#fff}
|
||||||
|
.ws-btn.ws-primary:hover{background:var(--ws-button-hover);border-color:var(--ws-button-hover)}
|
||||||
|
.ws-btn:disabled{background:var(--ws-soft);border-color:var(--ws-line);color:var(--ws-muted)}
|
||||||
|
.ws-btn.ws-icon-btn{width:44px;padding:0}
|
||||||
|
.ws-account{display:flex;align-items:center;gap:10px;margin-left:8px;padding-left:20px;border-left:1px solid var(--ws-line);color:var(--ws-secondary);font-size:14px}
|
||||||
|
.ws-account .ws-icon{width:18px;height:18px}
|
||||||
|
.ws-main{width:min(1280px,calc(100% - 64px));margin:0 auto;padding:50px 0 46px;flex:1}
|
||||||
|
.ws-heading{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;margin-bottom:30px}
|
||||||
|
.ws-heading h1{font-size:30px;letter-spacing:-.5px;font-weight:650;line-height:1.3;margin:0 0 9px}
|
||||||
|
.ws-heading p{margin:0;color:var(--ws-secondary);font-size:15px}
|
||||||
|
.ws-count{font-size:14px;color:var(--ws-secondary);padding-bottom:3px;white-space:nowrap}
|
||||||
|
.ws-toolbar{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-bottom:24px}
|
||||||
|
.ws-tabs{display:flex;gap:6px;padding:4px;border:1px solid var(--ws-line);border-radius:12px;background:var(--ws-panel)}
|
||||||
|
.ws-tab{border:0;background:transparent;color:var(--ws-secondary);border-radius:8px;min-height:38px;padding:0 16px;font-size:14px}
|
||||||
|
.ws-tab[aria-selected=true]{color:var(--ws-accent);background:var(--ws-tint);font-weight:650}
|
||||||
|
.ws-tab span{margin-left:7px;font-size:12px}
|
||||||
|
.ws-search{display:flex;align-items:center;gap:10px;width:260px;min-height:46px;padding:0 14px;border:1px solid var(--ws-line);border-radius:11px;background:var(--ws-panel);color:var(--ws-muted)}
|
||||||
|
.ws-search input{width:100%;border:0;outline:0;background:transparent;color:var(--ws-ink);font-size:14px;min-width:0}
|
||||||
|
.ws-search:focus-within{outline:2px solid var(--ws-accent);outline-offset:2px}
|
||||||
|
.ws-search input::placeholder{color:var(--ws-muted)}
|
||||||
|
.ws-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:22px}
|
||||||
|
.ws-card{display:flex;flex-direction:column;min-width:0;border:1px solid var(--ws-line);border-radius:20px;background:var(--ws-panel);box-shadow:var(--ws-shadow);padding:26px 26px 22px;transition:border-color .15s}
|
||||||
|
.ws-card:hover{border-color:var(--ws-muted)}
|
||||||
|
.ws-card-top{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:26px}
|
||||||
|
.ws-system{display:flex;align-items:center;gap:14px;min-width:0}
|
||||||
|
.ws-os-mark{display:grid;place-items:center;width:64px;height:64px;border-radius:16px;flex-shrink:0}
|
||||||
|
.ws-os-mark svg{width:32px;height:32px;display:block;fill:currentColor}
|
||||||
|
.ws-os-mark img{width:34px;height:34px;object-fit:contain;display:block}
|
||||||
|
.ws-os-mark.windows{background:#e1edf9;color:#1268ab}
|
||||||
|
.ws-os-mark.ubuntu{background:#fbe5dc;color:#d24413}
|
||||||
|
.ws-os-mark.ubuntu img{filter:invert(31%) sepia(81%) saturate(2291%) hue-rotate(354deg) brightness(89%) contrast(96%)}
|
||||||
|
.ws-os-mark.kylin{background:#fff;border:1px solid #e2e6eb}
|
||||||
|
.ws-os-mark.kylin img{width:54px;height:38px}
|
||||||
|
body[data-theme=dark] .ws-os-mark.windows{background:#273e52;color:#7fc3f7}
|
||||||
|
body[data-theme=dark] .ws-os-mark.ubuntu{background:#4b3127;color:#ffa77c}
|
||||||
|
body[data-theme=dark] .ws-os-mark.ubuntu img{filter:invert(75%) sepia(44%) saturate(721%) hue-rotate(321deg) brightness(102%) contrast(102%)}
|
||||||
|
.ws-system-name{font-size:16px;font-weight:650;line-height:1.5;white-space:nowrap}
|
||||||
|
.ws-system-version{color:var(--ws-secondary);font-size:13px;line-height:1.5;margin-top:3px}
|
||||||
|
.ws-favorite{border:0;background:transparent;color:var(--ws-muted);width:36px;height:40px;display:grid;place-items:center;border-radius:9px;flex-shrink:0}
|
||||||
|
.ws-favorite:hover{background:var(--ws-soft)}
|
||||||
|
.ws-favorite[aria-pressed=true]{color:var(--ws-accent)}
|
||||||
|
.ws-favorite[aria-pressed=true] svg{fill:var(--ws-tint)}
|
||||||
|
.ws-card h2{font-size:22px;font-weight:650;letter-spacing:-.3px;line-height:1.35;margin:0 0 11px}
|
||||||
|
.ws-status{display:inline-flex;align-items:center;gap:7px;border-radius:6px;font-size:13px;font-weight:550;color:var(--ws-good);background:var(--ws-good-bg);width:fit-content;padding:3px 9px;line-height:1.6}
|
||||||
|
.ws-status:before,.ws-online:before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;flex-shrink:0}
|
||||||
|
.ws-status.pending{color:var(--ws-warn);background:var(--ws-warn-bg)}
|
||||||
|
.ws-specs{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:26px 0 0;padding:20px 0;border-top:1px solid var(--ws-line);border-bottom:1px solid var(--ws-line)}
|
||||||
|
.ws-specs div+div{border-left:1px solid var(--ws-line);padding-left:22px}
|
||||||
|
.ws-specs dt{color:var(--ws-secondary);font-size:13px;margin-bottom:5px}
|
||||||
|
.ws-specs dd{margin:0;font-size:15px;font-weight:550;white-space:nowrap}
|
||||||
|
.ws-card-note{margin:17px 0 22px;color:var(--ws-secondary);font-size:13px;min-height:21px}
|
||||||
|
.ws-card-actions{margin-top:auto;display:grid;grid-template-columns:1fr 1.45fr;gap:10px}
|
||||||
|
.ws-card-actions .ws-btn{min-height:46px}
|
||||||
|
.ws-footer{width:min(1280px,calc(100% - 64px));margin:0 auto;border-top:1px solid var(--ws-line);display:flex;align-items:center;justify-content:space-between;gap:18px;padding:20px 0 24px;color:var(--ws-secondary);font-size:13px}
|
||||||
|
.ws-footer-left{display:flex;align-items:center;gap:22px;flex-wrap:wrap}
|
||||||
|
.ws-online{display:inline-flex;align-items:center;gap:7px;color:var(--ws-good)}
|
||||||
|
.ws-prototype{font-size:12px;color:var(--ws-muted)}
|
||||||
|
.ws-empty{grid-column:1/-1;padding:70px 24px;border:1px dashed var(--ws-line);border-radius:18px;text-align:center;color:var(--ws-secondary)}
|
||||||
|
.ws-empty h2{font-size:20px;color:var(--ws-ink)}
|
||||||
|
.ws-empty p{margin:8px 0 22px}
|
||||||
|
.ws-view-title{display:flex;align-items:center;gap:16px;margin-bottom:26px}
|
||||||
|
.ws-view-title h1{font-size:26px;margin:0;font-weight:650}
|
||||||
|
.ws-settings{display:grid;grid-template-columns:218px 1fr;gap:26px;align-items:start}
|
||||||
|
.ws-settings-nav{padding:10px;border:1px solid var(--ws-line);border-radius:16px;background:var(--ws-panel);display:grid;gap:4px}
|
||||||
|
.ws-settings-nav button{display:flex;align-items:center;gap:11px;min-height:48px;border:0;border-radius:9px;padding:0 14px;background:transparent;color:var(--ws-secondary);text-align:left;font-size:14px}
|
||||||
|
.ws-settings-nav button[aria-current=page]{background:var(--ws-tint);color:var(--ws-accent);font-weight:650}
|
||||||
|
.ws-settings-panel{padding:30px 34px;border:1px solid var(--ws-line);border-radius:18px;background:var(--ws-panel);min-height:460px}
|
||||||
|
.ws-settings-panel h2{font-size:21px;line-height:1.4;font-weight:650;margin:0 0 9px}
|
||||||
|
.ws-settings-panel>p{font-size:14px;margin:0 0 28px;color:var(--ws-secondary)}
|
||||||
|
.ws-form-grid{display:grid;grid-template-columns:1fr 1fr;gap:22px}
|
||||||
|
.ws-field{display:grid;gap:9px;font-size:14px}
|
||||||
|
.ws-field.full{grid-column:1/-1}
|
||||||
|
.ws-input{width:100%;height:46px;border:1px solid var(--ws-line);border-radius:10px;background:var(--ws-soft);color:var(--ws-ink);padding:0 13px;outline:none;min-width:0}
|
||||||
|
.ws-field small{color:var(--ws-secondary);font-size:13px}
|
||||||
|
.ws-field.ws-invalid .ws-input{border-color:#d47d71}
|
||||||
|
.ws-error-text{color:var(--ws-warn);font-size:14px;margin:16px 0 0;min-height:24px}
|
||||||
|
.ws-form-actions{display:flex;justify-content:flex-end;gap:12px;margin-top:25px;padding-top:23px;border-top:1px solid var(--ws-line)}
|
||||||
|
.ws-info-list{margin:12px 0 0}
|
||||||
|
.ws-info-list>div{display:flex;justify-content:space-between;gap:26px;border-bottom:1px solid var(--ws-line);padding:17px 0;font-size:14px}
|
||||||
|
.ws-info-list dt{color:var(--ws-secondary)}
|
||||||
|
.ws-info-list dd{margin:0;text-align:right;overflow-wrap:anywhere}
|
||||||
|
.ws-session{width:min(660px,100%);margin:16px auto 20px;border:1px solid var(--ws-line);border-radius:22px;background:var(--ws-panel);padding:38px 42px;text-align:center}
|
||||||
|
.ws-session>.ws-os-mark{margin:0 auto 22px;width:72px;height:72px}
|
||||||
|
.ws-session h1{font-size:26px;font-weight:650;line-height:1.35;margin:0 0 9px}
|
||||||
|
.ws-session-sub{color:var(--ws-secondary);font-size:15px;margin:0}
|
||||||
|
.ws-session-progress{height:4px;margin:30px 0 27px;background:var(--ws-soft);border-radius:6px;overflow:hidden}
|
||||||
|
.ws-session-progress span{display:block;height:100%;width:28%;background:var(--ws-accent);border-radius:6px;transition:width .2s}
|
||||||
|
.ws-steps{list-style:none;text-align:left;padding:0;margin:0 0 28px;display:grid;gap:18px}
|
||||||
|
.ws-steps li{display:flex;align-items:center;gap:13px;font-size:14px;color:var(--ws-secondary)}
|
||||||
|
.ws-step-mark{display:grid;place-items:center;width:25px;height:25px;border-radius:50%;border:1px solid var(--ws-line);font-size:12px}
|
||||||
|
.ws-steps li.done{color:var(--ws-good)}
|
||||||
|
.ws-steps li.active{color:var(--ws-ink);font-weight:550}
|
||||||
|
.ws-steps li.active .ws-step-mark{border-color:var(--ws-accent);color:var(--ws-accent)}
|
||||||
|
.ws-session-note{background:var(--ws-soft);color:var(--ws-secondary);font-size:13px;text-align:left;border-radius:10px;padding:13px 16px;line-height:1.7;margin-bottom:25px}
|
||||||
|
.ws-session-actions{display:flex;justify-content:center;gap:12px;flex-wrap:wrap}
|
||||||
|
.ws-session-actions .ws-btn{min-width:140px}
|
||||||
|
.ws-spinner{width:14px;height:14px;border-radius:50%;border:2px solid var(--ws-line);border-top-color:var(--ws-accent);animation:ws-spin 1s linear infinite}
|
||||||
|
@keyframes ws-spin{to{transform:rotate(360deg)}}
|
||||||
|
.ws-toast{position:fixed;bottom:30px;left:50%;transform:translateX(-50%);padding:13px 22px;background:var(--ws-ink);color:var(--ws-panel);border-radius:12px;box-shadow:var(--ws-shadow);font-size:14px;z-index:15}
|
||||||
|
@media(min-width:1600px){.ws-main{padding-top:64px}.ws-card{padding:30px}.ws-grid{gap:26px}.ws-card h2{font-size:24px}.ws-card-note{margin-bottom:27px}}
|
||||||
|
@media(max-width:1120px){.ws-header{padding:0 26px}.ws-brand span{display:none}.ws-main,.ws-footer{width:calc(100% - 52px)}.ws-main{padding-top:35px}.ws-card{padding:22px 18px}.ws-system{gap:10px}.ws-os-mark{width:52px;height:52px;border-radius:13px}.ws-system-name{font-size:15px}.ws-favorite{width:28px}.ws-specs div+div{padding-left:12px}.ws-card h2{font-size:21px}.ws-card-actions{grid-template-columns:1fr 1.3fr}.ws-btn{padding:0 13px}}
|
||||||
|
@media(max-width:900px){.ws-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.ws-account{display:none}.ws-settings{grid-template-columns:175px 1fr;gap:18px}.ws-settings-panel{padding:25px}.ws-footer-left{gap:12px}}
|
||||||
|
@media(max-width:620px){.ws-header{height:72px;padding:0 18px}.ws-brand strong{font-size:19px}.ws-header-actions{gap:8px}.ws-header-actions .ws-settings-label{display:none}.ws-main,.ws-footer{width:calc(100% - 36px)}.ws-main{padding-top:28px}.ws-heading{align-items:flex-start;gap:12px}.ws-heading h1{font-size:26px}.ws-count{font-size:12px}.ws-toolbar{align-items:stretch;flex-direction:column;gap:14px}.ws-search{width:100%}.ws-grid{grid-template-columns:1fr}.ws-card{padding:24px}.ws-system-name{font-size:16px}.ws-os-mark{width:60px;height:60px}.ws-settings{grid-template-columns:1fr}.ws-settings-nav{grid-template-columns:repeat(3,1fr);padding:5px}.ws-settings-nav button{justify-content:center;padding:0 6px;font-size:13px;gap:6px}.ws-settings-nav .ws-icon{display:none}.ws-form-grid{grid-template-columns:1fr}.ws-settings-panel{padding:22px}.ws-session{padding:28px 24px;margin-top:0}.ws-footer{align-items:flex-start;flex-direction:column}.ws-footer-left{gap:12px;font-size:12px}.ws-prototype{font-size:11px}}
|
||||||
|
@media(prefers-reduced-motion:reduce){.terminal-workspace *{animation:none!important;transition:none!important}}
|
||||||
|
/* A 720p ARM display must keep the primary actions and device status in view. */
|
||||||
|
@media(min-width:901px) and (max-height:820px){
|
||||||
|
.ws-header{height:72px}
|
||||||
|
.ws-main{padding-top:28px;padding-bottom:26px}
|
||||||
|
.ws-heading{margin-bottom:20px}.ws-heading h1{font-size:28px;margin-bottom:6px}.ws-heading p{font-size:14px}
|
||||||
|
.ws-toolbar{margin-bottom:20px}.ws-tabs{padding:3px}.ws-tab{min-height:36px}
|
||||||
|
.ws-card{padding:22px 22px 20px}.ws-card-top{margin-bottom:20px}.ws-os-mark{height:54px;width:54px;border-radius:13px}
|
||||||
|
.ws-card h2{font-size:22px;margin-bottom:9px}.ws-specs{margin-top:20px;padding:15px 0}.ws-card-note{margin:14px 0 18px}
|
||||||
|
.ws-card-actions .ws-btn{min-height:44px}.ws-footer{padding-top:16px;padding-bottom:18px}
|
||||||
|
.ws-session{margin-top:0;padding:28px 38px}.ws-session>.ws-os-mark{width:56px;height:56px;margin-bottom:15px}
|
||||||
|
.ws-session-progress{margin:24px 0}.ws-steps{gap:13px;margin-bottom:22px}.ws-settings-panel{padding:24px 30px;min-height:0}
|
||||||
|
}
|
||||||
183
ui/terminal/workspace.js
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
(() => {
|
||||||
|
'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();
|
||||||
|
}
|
||||||
|
})();
|
||||||