diff --git a/.gitignore b/.gitignore index f0a64ed..b08eb11 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ CMakeCache.txt __pycache__/ test-output/ screenshots/ +.DS_Store +.playwright-cli/ diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 0000000..16bf4af --- /dev/null +++ b/ui/README.md @@ -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` 的流程,覆盖登录、桌面列表、连接中、重连、管理菜单五个状态。 diff --git a/ui/assets/brand-logo.svg b/ui/assets/brand-logo.svg new file mode 100644 index 0000000..156d7bd --- /dev/null +++ b/ui/assets/brand-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/assets/brand-mark.svg b/ui/assets/brand-mark.svg new file mode 100644 index 0000000..da3a44d --- /dev/null +++ b/ui/assets/brand-mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/design-spec.md b/ui/design-spec.md new file mode 100644 index 0000000..f28e7bd --- /dev/null +++ b/ui/design-spec.md @@ -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`,点击左侧导航查看页面;点击右上角月亮/太阳按钮切换主题。 diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..ed4cf93 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,12 @@ + +VDI Cloud · 百工万象 UI 设计稿
VDI CLOUD / WORKSPACE
云桌面总览
+
在线桌面
128
↑ 12.4% 较昨日
活跃会话
86
↑ 8.1% 运行稳定
平均延迟
18 ms
↓ 3.6 ms 本周优化
异常告警
03
需要关注
桌面使用趋势
近 7 日连接与运行时长
最近 7 天⌄
周一
周二
周三
周四
周五
周六
周日
资源健康度
● 全部正常
计算节点24 / 24
串流网关12 / 12
存储容量76%
认证服务正常
+
桌面资源池
统一管理 Windows / Linux 云桌面与分配策略
全部 128运行中 86待分配 42
▣
设计工作站 · 2K
Windows 11 · NVIDIA GPU
2K / 60 FPS低延迟
● 运行中
◈
研发环境 · Ubuntu
Ubuntu 24.04 · 开发工具链
GPU 加速USB 重定向
● 运行中
⌘
办公桌面 · 标准
Windows 10 · 通用办公模板
1080p / 45 FPS自动休眠
● 待分配
+
远程画面预览 · 2K / 60 FPS · 18 ms
远程会话
设计工作站 · 王工
连接状态● 已连接
串流质量优秀
帧率60 FPS
网络延迟18 ms
GPU 使用率 64%
显存使用 42%
+
终端与策略
为不同网络与终端设备选择合适的默认体验。
默认显示规格
优先使用 2K 分辨率与 60 FPS
2K · 60 FPS⌄
自动恢复会话
终端重启后自动恢复上一次连接
独立鼠标光标
降低远程画面中的指针延迟
USB 设备即插即用
允许白名单设备自动映射到云桌面
断线保护
网络短暂中断时保留会话 15 分钟
+
diff --git a/ui/terminal/README.md b/ui/terminal/README.md new file mode 100644 index 0000000..bfe8f2d --- /dev/null +++ b/ui/terminal/README.md @@ -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 实机验收。 diff --git a/ui/terminal/assets/login-art.js b/ui/terminal/assets/login-art.js new file mode 100644 index 0000000..fbcd203 --- /dev/null +++ b/ui/terminal/assets/login-art.js @@ -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); +})(); diff --git a/ui/terminal/assets/logo.svg b/ui/terminal/assets/logo.svg new file mode 100644 index 0000000..156d7bd --- /dev/null +++ b/ui/terminal/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/terminal/assets/mark.svg b/ui/terminal/assets/mark.svg new file mode 100644 index 0000000..da3a44d --- /dev/null +++ b/ui/terminal/assets/mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/terminal/assets/systems/kylinsoft.png b/ui/terminal/assets/systems/kylinsoft.png new file mode 100644 index 0000000..415ec82 Binary files /dev/null and b/ui/terminal/assets/systems/kylinsoft.png differ diff --git a/ui/terminal/assets/systems/ubuntu.svg b/ui/terminal/assets/systems/ubuntu.svg new file mode 100644 index 0000000..227f86a --- /dev/null +++ b/ui/terminal/assets/systems/ubuntu.svg @@ -0,0 +1 @@ +Ubuntu \ No newline at end of file diff --git a/ui/terminal/index.html b/ui/terminal/index.html new file mode 100644 index 0000000..056dd77 --- /dev/null +++ b/ui/terminal/index.html @@ -0,0 +1,53 @@ + + + + + +越欣科技 · VDI 云终端 + + + + + + + + +
+
+
+ +
+
+ + + + + diff --git a/ui/terminal/login-interactions.css b/ui/terminal/login-interactions.css new file mode 100644 index 0000000..c4975ec --- /dev/null +++ b/ui/terminal/login-interactions.css @@ -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} diff --git a/ui/terminal/login-interactions.js b/ui/terminal/login-interactions.js new file mode 100644 index 0000000..4b66a0a --- /dev/null +++ b/ui/terminal/login-interactions.js @@ -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?'登录中…':'登 录'; + } + 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) => `

${title}

${sub?`

${sub}

`:''}
`; + const cancel = (label='取消') => ``; + 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('连接设置','选择云端服务或企业部署入口。')+ + `
`+ + `
${mode==='enterprise'?``:`
云端服务

使用工作账号登录云桌面,无需填写企业服务地址。

`}

${cancel()}
`); + } + 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('体验账号','用于预览登录、桌面选择和连接设置流程。')+ + `
工作账号
${DEMO.username}
密码
${DEMO.password}
这是交互原型的公开体验账号,未连接真实认证服务。请使用体验信息。
${cancel('返回')}
`); + } + function forgotDialog() { + showDialog(heading('忘记密码','企业工作账号由管理员统一管理。')+ + `
请联系越欣科技管理员重置密码

可提供工作账号与设备序列号 VDI-8F42A1,协助管理员定位。

原型体验账号无法修改密码,可重新查看体验信息。

${cancel('返回登录')}
`); + } + function ssoDialog() { + if(connection.mode!=='enterprise'){serviceDialog('sso');return;} + showDialog(heading('企业账号登录','通过企业统一身份进入云桌面。')+ + `
企业服务
${escape(connection.url)}
${connection.organization?`
组织
${escape(connection.organization)}
`:''}
体验身份
越欣科技 · 体验用户

当前为统一身份登录预览,不会跳转或访问真实企业身份服务。

${cancel('取消登录')}
`); + } + function ssoContinue(button) { + if(modalBusy)return; + modalBusy=true;button.disabled=true;button.innerHTML='登录中…'; + dialogContent.querySelector('#auth-sso-status').textContent='正在验证企业身份…'; + modalTimer=setTimeout(()=>{closeDialog();enterWorkspace({name:'企业体验用户',mode:'sso',service:'enterprise',organization:connection.organization});},900); + } + function offlineDialog() { + showDialog(heading('离线体验','先了解桌面列表和终端设置。')+ + `
无需工作账号

可预览桌面、收藏和连接设置,设备与桌面信息为示例。配置仅保存在当前浏览器,不连接真实远程桌面。

${cancel()}
`); + } + 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(); +})(); diff --git a/ui/terminal/output/playwright/connecting-dark.png b/ui/terminal/output/playwright/connecting-dark.png new file mode 100644 index 0000000..3859424 Binary files /dev/null and b/ui/terminal/output/playwright/connecting-dark.png differ diff --git a/ui/terminal/output/playwright/connecting-light.png b/ui/terminal/output/playwright/connecting-light.png new file mode 100644 index 0000000..3f144b9 Binary files /dev/null and b/ui/terminal/output/playwright/connecting-light.png differ diff --git a/ui/terminal/output/playwright/desktops-system-logos-light.png b/ui/terminal/output/playwright/desktops-system-logos-light.png new file mode 100644 index 0000000..7e0401b Binary files /dev/null and b/ui/terminal/output/playwright/desktops-system-logos-light.png differ diff --git a/ui/terminal/output/playwright/desktops-system-logos.png b/ui/terminal/output/playwright/desktops-system-logos.png new file mode 100644 index 0000000..2fbc1c4 Binary files /dev/null and b/ui/terminal/output/playwright/desktops-system-logos.png differ diff --git a/ui/terminal/output/playwright/error-dark.png b/ui/terminal/output/playwright/error-dark.png new file mode 100644 index 0000000..b6e4c51 Binary files /dev/null and b/ui/terminal/output/playwright/error-dark.png differ diff --git a/ui/terminal/output/playwright/error-light.png b/ui/terminal/output/playwright/error-light.png new file mode 100644 index 0000000..b215a7f Binary files /dev/null and b/ui/terminal/output/playwright/error-light.png differ diff --git a/ui/terminal/output/playwright/login-baseline-dark.png b/ui/terminal/output/playwright/login-baseline-dark.png new file mode 100644 index 0000000..64f7c6b Binary files /dev/null and b/ui/terminal/output/playwright/login-baseline-dark.png differ diff --git a/ui/terminal/output/playwright/login-demo-dark.png b/ui/terminal/output/playwright/login-demo-dark.png new file mode 100644 index 0000000..c8ae5c0 Binary files /dev/null and b/ui/terminal/output/playwright/login-demo-dark.png differ diff --git a/ui/terminal/output/playwright/login-demo-light.png b/ui/terminal/output/playwright/login-demo-light.png new file mode 100644 index 0000000..59e1761 Binary files /dev/null and b/ui/terminal/output/playwright/login-demo-light.png differ diff --git a/ui/terminal/output/playwright/login-dialog-390.png b/ui/terminal/output/playwright/login-dialog-390.png new file mode 100644 index 0000000..cbe3f0c Binary files /dev/null and b/ui/terminal/output/playwright/login-dialog-390.png differ diff --git a/ui/terminal/output/playwright/login-dialog-820.png b/ui/terminal/output/playwright/login-dialog-820.png new file mode 100644 index 0000000..28993f3 Binary files /dev/null and b/ui/terminal/output/playwright/login-dialog-820.png differ diff --git a/ui/terminal/output/playwright/login-enterprise-dark.png b/ui/terminal/output/playwright/login-enterprise-dark.png new file mode 100644 index 0000000..b537d54 Binary files /dev/null and b/ui/terminal/output/playwright/login-enterprise-dark.png differ diff --git a/ui/terminal/output/playwright/login-enterprise-light.png b/ui/terminal/output/playwright/login-enterprise-light.png new file mode 100644 index 0000000..f1a0c31 Binary files /dev/null and b/ui/terminal/output/playwright/login-enterprise-light.png differ diff --git a/ui/terminal/output/playwright/login-forgot-dark.png b/ui/terminal/output/playwright/login-forgot-dark.png new file mode 100644 index 0000000..bfb60d4 Binary files /dev/null and b/ui/terminal/output/playwright/login-forgot-dark.png differ diff --git a/ui/terminal/output/playwright/login-forgot-light.png b/ui/terminal/output/playwright/login-forgot-light.png new file mode 100644 index 0000000..7c15e9c Binary files /dev/null and b/ui/terminal/output/playwright/login-forgot-light.png differ diff --git a/ui/terminal/output/playwright/login-interactive-dark-1280.png b/ui/terminal/output/playwright/login-interactive-dark-1280.png new file mode 100644 index 0000000..019689a Binary files /dev/null and b/ui/terminal/output/playwright/login-interactive-dark-1280.png differ diff --git a/ui/terminal/output/playwright/login-interactive-dark-1366.png b/ui/terminal/output/playwright/login-interactive-dark-1366.png new file mode 100644 index 0000000..5a7e34d Binary files /dev/null and b/ui/terminal/output/playwright/login-interactive-dark-1366.png differ diff --git a/ui/terminal/output/playwright/login-interactive-dark-1920.png b/ui/terminal/output/playwright/login-interactive-dark-1920.png new file mode 100644 index 0000000..9c253a3 Binary files /dev/null and b/ui/terminal/output/playwright/login-interactive-dark-1920.png differ diff --git a/ui/terminal/output/playwright/login-interactive-dark.png b/ui/terminal/output/playwright/login-interactive-dark.png new file mode 100644 index 0000000..db67e02 Binary files /dev/null and b/ui/terminal/output/playwright/login-interactive-dark.png differ diff --git a/ui/terminal/output/playwright/login-interactive-light-1280.png b/ui/terminal/output/playwright/login-interactive-light-1280.png new file mode 100644 index 0000000..eb435c3 Binary files /dev/null and b/ui/terminal/output/playwright/login-interactive-light-1280.png differ diff --git a/ui/terminal/output/playwright/login-interactive-light-1366.png b/ui/terminal/output/playwright/login-interactive-light-1366.png new file mode 100644 index 0000000..a8abc32 Binary files /dev/null and b/ui/terminal/output/playwright/login-interactive-light-1366.png differ diff --git a/ui/terminal/output/playwright/login-interactive-light-1920.png b/ui/terminal/output/playwright/login-interactive-light-1920.png new file mode 100644 index 0000000..38b20ef Binary files /dev/null and b/ui/terminal/output/playwright/login-interactive-light-1920.png differ diff --git a/ui/terminal/output/playwright/login-interactive-light.png b/ui/terminal/output/playwright/login-interactive-light.png new file mode 100644 index 0000000..df63271 Binary files /dev/null and b/ui/terminal/output/playwright/login-interactive-light.png differ diff --git a/ui/terminal/output/playwright/login-offline-dark.png b/ui/terminal/output/playwright/login-offline-dark.png new file mode 100644 index 0000000..e032404 Binary files /dev/null and b/ui/terminal/output/playwright/login-offline-dark.png differ diff --git a/ui/terminal/output/playwright/login-offline-light.png b/ui/terminal/output/playwright/login-offline-light.png new file mode 100644 index 0000000..8d31807 Binary files /dev/null and b/ui/terminal/output/playwright/login-offline-light.png differ diff --git a/ui/terminal/output/playwright/login-preserved-dark.png b/ui/terminal/output/playwright/login-preserved-dark.png new file mode 100644 index 0000000..64f7c6b Binary files /dev/null and b/ui/terminal/output/playwright/login-preserved-dark.png differ diff --git a/ui/terminal/output/playwright/login-preserved-light.png b/ui/terminal/output/playwright/login-preserved-light.png new file mode 100644 index 0000000..59e3127 Binary files /dev/null and b/ui/terminal/output/playwright/login-preserved-light.png differ diff --git a/ui/terminal/output/playwright/login-sso-dark.png b/ui/terminal/output/playwright/login-sso-dark.png new file mode 100644 index 0000000..cf5b40f Binary files /dev/null and b/ui/terminal/output/playwright/login-sso-dark.png differ diff --git a/ui/terminal/output/playwright/login-sso-light.png b/ui/terminal/output/playwright/login-sso-light.png new file mode 100644 index 0000000..f731723 Binary files /dev/null and b/ui/terminal/output/playwright/login-sso-light.png differ diff --git a/ui/terminal/output/playwright/login-success-dark.png b/ui/terminal/output/playwright/login-success-dark.png new file mode 100644 index 0000000..8dce3f4 Binary files /dev/null and b/ui/terminal/output/playwright/login-success-dark.png differ diff --git a/ui/terminal/output/playwright/login-validation-dark.png b/ui/terminal/output/playwright/login-validation-dark.png new file mode 100644 index 0000000..4bf66d6 Binary files /dev/null and b/ui/terminal/output/playwright/login-validation-dark.png differ diff --git a/ui/terminal/output/playwright/login-validation-light.png b/ui/terminal/output/playwright/login-validation-light.png new file mode 100644 index 0000000..83d9964 Binary files /dev/null and b/ui/terminal/output/playwright/login-validation-light.png differ diff --git a/ui/terminal/output/playwright/login-verification.json b/ui/terminal/output/playwright/login-verification.json new file mode 100644 index 0000000..61d29f3 --- /dev/null +++ b/ui/terminal/output/playwright/login-verification.json @@ -0,0 +1,1054 @@ +{ + "verifiedAt": "2026-11-09T17:03:18.593143+08:00", + "scope": "Local HTML login interaction prototype, Chromium browser. No real authentication, remote desktop, Qt port, or ARM device acceptance.", + "result": "passed", + "checkCount": 90, + "checks": [ + { + "group": "flows", + "check": "Blank account is rejected and focused", + "passed": true + }, + { + "group": "flows", + "check": "Empty password is rejected and focused via Enter", + "passed": true + }, + { + "group": "flows", + "check": "Password reveal updates accessible state", + "passed": true + }, + { + "group": "flows", + "check": "Password conceal works", + "passed": true + }, + { + "group": "flows", + "check": "Login exposes busy state and blocks duplicate clicks", + "passed": true + }, + { + "group": "flows", + "check": "Invalid credentials stay on login and clear password", + "passed": true + }, + { + "group": "flows", + "check": "Demo helper shows public credentials", + "passed": true + }, + { + "group": "flows", + "check": "Demo helper fills credentials without bypassing login", + "passed": true + }, + { + "group": "flows", + "check": "Valid credentials log in via Enter with correct identity", + "passed": true + }, + { + "group": "flows", + "check": "Successful login clears and conceals password", + "passed": true + }, + { + "group": "flows", + "check": "Only remembered account is persisted; no password stored", + "passed": true + }, + { + "group": "flows", + "check": "Logout returns login with remembered account and empty password", + "passed": true + }, + { + "group": "flows", + "check": "Reload restores remembered account only", + "passed": true + }, + { + "group": "flows", + "check": "Unchecking remember removes saved account across reload", + "passed": true + }, + { + "group": "flows", + "check": "Forgot password provides administrator path", + "passed": true + }, + { + "group": "flows", + "check": "Forgot password links to demo recovery", + "passed": true + }, + { + "group": "flows", + "check": "Escape closes modal and restores trigger focus", + "passed": true + }, + { + "group": "flows", + "check": "Offline experience has distinct identity and footer", + "passed": true + }, + { + "group": "flows", + "check": "Admin shortcut cancels pending login without later redirect", + "passed": true + }, + { + "group": "flows", + "check": "No browser runtime errors during account flows", + "passed": true + }, + { + "group": "flows", + "check": "No external authentication or remote desktop requests", + "passed": true + }, + { + "group": "enterprise", + "check": "Cancel does not save service draft", + "passed": true + }, + { + "group": "enterprise", + "check": "SSO requires enterprise connection before identity preview", + "passed": true + }, + { + "group": "enterprise", + "check": "Reject unsafe enterprise endpoint: (empty)", + "passed": true + }, + { + "group": "enterprise", + "check": "Reject unsafe enterprise endpoint: invalid", + "passed": true + }, + { + "group": "enterprise", + "check": "Reject unsafe enterprise endpoint: http://vdi.example.com", + "passed": true + }, + { + "group": "enterprise", + "check": "Reject unsafe enterprise endpoint: https://user:secret@vdi.example.com", + "passed": true + }, + { + "group": "enterprise", + "check": "Reject unsafe enterprise endpoint: https://vdi.example.com/?token=secret", + "passed": true + }, + { + "group": "enterprise", + "check": "Reject unsafe enterprise endpoint: https://vdi.example.com/#secret", + "passed": true + }, + { + "group": "enterprise", + "check": "Service tabs support keyboard Home", + "passed": true + }, + { + "group": "enterprise", + "check": "Switching service mode preserves in-progress draft", + "passed": true + }, + { + "group": "enterprise", + "check": "Save-and-continue opens correct SSO identity preview", + "passed": true + }, + { + "group": "enterprise", + "check": "Escape cancels in-flight SSO without delayed login", + "passed": true + }, + { + "group": "enterprise", + "check": "Cancel button cancels in-flight SSO", + "passed": true + }, + { + "group": "enterprise", + "check": "SSO prototype enters workspace with enterprise identity", + "passed": true + }, + { + "group": "enterprise", + "check": "Successful login moves keyboard focus to workspace title", + "passed": true + }, + { + "group": "enterprise", + "check": "Saved enterprise mode survives reload", + "passed": true + }, + { + "group": "enterprise", + "check": "Normalized enterprise URL and organization persist", + "passed": true + }, + { + "group": "enterprise", + "check": "Local HTTP endpoint is accepted for prototype", + "passed": true + }, + { + "group": "enterprise", + "check": "Service can return to cloud mode", + "passed": true + }, + { + "group": "enterprise", + "check": "Account login still succeeds after SSO and service changes", + "passed": true + }, + { + "group": "layout", + "check": "dark 1280×720 login and footer fit viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1280 error state fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1280 #auth-demo dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1280 #auth-forgot dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1280 #auth-offline dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1280 #auth-sso dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1366×768 login and footer fit viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1366 error state fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1366 #auth-demo dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1366 #auth-forgot dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1366 #auth-offline dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1366 #auth-sso dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1920×1080 login and footer fit viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1920 error state fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1920 #auth-demo dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1920 #auth-forgot dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1920 #auth-offline dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark 1920 #auth-sso dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "dark dialog traps keyboard focus", + "passed": true + }, + { + "group": "layout", + "check": "light 1280×720 login and footer fit viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1280 error state fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1280 #auth-demo dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1280 #auth-forgot dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1280 #auth-offline dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1280 #auth-sso dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1366×768 login and footer fit viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1366 error state fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1366 #auth-demo dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1366 #auth-forgot dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1366 #auth-offline dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1366 #auth-sso dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1920×1080 login and footer fit viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1920 error state fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1920 #auth-demo dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1920 #auth-forgot dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1920 #auth-offline dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "light 1920 #auth-sso dialog fits viewport", + "passed": true + }, + { + "group": "layout", + "check": "light dialog traps keyboard focus", + "passed": true + }, + { + "group": "layout", + "check": "Narrow login 820 has no horizontal overflow", + "passed": true + }, + { + "group": "layout", + "check": "Narrow dialog 820 remains inside viewport", + "passed": true + }, + { + "group": "layout", + "check": "Narrow enterprise validation remains reachable 820", + "passed": true + }, + { + "group": "layout", + "check": "Narrow login 390 has no horizontal overflow", + "passed": true + }, + { + "group": "layout", + "check": "Narrow dialog 390 remains inside viewport", + "passed": true + }, + { + "group": "layout", + "check": "Narrow enterprise validation remains reachable 390", + "passed": true + }, + { + "group": "source", + "check": "Original brand HTML preserved against pre-task backup", + "passed": true + }, + { + "group": "source", + "check": "Original inline design CSS preserved against pre-task backup", + "passed": true + }, + { + "group": "source", + "check": "Workspace CSS preserved against pre-task backup", + "passed": true + }, + { + "group": "source", + "check": "Login and workspace JavaScript pass node --check", + "passed": true + }, + { + "group": "source", + "check": "Measured new helper and dialog text contrast is at least 4.5:1 in both themes", + "passed": true + } + ], + "demo": { + "username": "demo", + "password": "Demo123!", + "publicPrototypeCredentials": true + }, + "geometry": { + "dark-1280": { + "card": { + "top": 129.59375, + "bottom": 647.59375, + "left": 645.109375, + "right": 1077.734375 + }, + "footerBottom": 618.59375, + "width": 1280, + "height": 720 + }, + "dark-1366": { + "card": { + "top": 138.234375, + "bottom": 735.640625, + "left": 688.453125, + "right": 1150.15625 + }, + "footerBottom": 693.9375, + "width": 1366, + "height": 768 + }, + "dark-1920": { + "card": { + "top": 194.390625, + "bottom": 950.390625, + "left": 967.671875, + "right": 1616.625 + }, + "footerBottom": 766.625, + "width": 1920, + "height": 1080 + }, + "light-1280": { + "card": { + "top": 129.59375, + "bottom": 647.59375, + "left": 645.109375, + "right": 1077.734375 + }, + "footerBottom": 618.59375, + "width": 1280, + "height": 720 + }, + "light-1366": { + "card": { + "top": 138.234375, + "bottom": 735.640625, + "left": 688.453125, + "right": 1150.15625 + }, + "footerBottom": 693.9375, + "width": 1366, + "height": 768 + }, + "light-1920": { + "card": { + "top": 194.390625, + "bottom": 950.390625, + "left": 967.671875, + "right": 1616.625 + }, + "footerBottom": 766.625, + "width": 1920, + "height": 1080 + } + }, + "newInteractionTextContrast": { + "dark#auth-demo": [ + { + "text": "体验账号", + "ratio": 6.96 + }, + { + "text": "账号或密码不正确,请重试或查看体验账号", + "ratio": 8.38 + }, + { + "text": "体验账号", + "ratio": 12.27 + }, + { + "text": "用于预览登录、桌面选择和连接设置流程。", + "ratio": 7.94 + }, + { + "text": "×", + "ratio": 7.14 + }, + { + "text": "工作账号", + "ratio": 7.14 + }, + { + "text": "demo", + "ratio": 11.03 + }, + { + "text": "密码", + "ratio": 7.14 + }, + { + "text": "Demo123!", + "ratio": 11.03 + }, + { + "text": "这是交互原型的公开体验账号,未连接真实认证服务。请使用体验信息。", + "ratio": 7.14 + }, + { + "text": "返回", + "ratio": 12.27 + }, + { + "text": "填入体验账号", + "ratio": 5.1 + } + ], + "dark#auth-forgot": [ + { + "text": "体验账号", + "ratio": 6.96 + }, + { + "text": "账号或密码不正确,请重试或查看体验账号", + "ratio": 8.38 + }, + { + "text": "忘记密码", + "ratio": 12.27 + }, + { + "text": "企业工作账号由管理员统一管理。", + "ratio": 7.94 + }, + { + "text": "×", + "ratio": 7.14 + }, + { + "text": "请联系越欣科技管理员重置密码", + "ratio": 11.03 + }, + { + "text": "可提供工作账号与设备序列号 VDI-8F42A1,协助管理员定位。", + "ratio": 7.14 + }, + { + "text": "原型体验账号无法修改密码,可重新查看体验信息。", + "ratio": 7.94 + }, + { + "text": "返回登录", + "ratio": 12.27 + }, + { + "text": "查看体验账号", + "ratio": 5.1 + } + ], + "dark#auth-offline": [ + { + "text": "体验账号", + "ratio": 6.96 + }, + { + "text": "账号或密码不正确,请重试或查看体验账号", + "ratio": 8.38 + }, + { + "text": "离线体验", + "ratio": 12.27 + }, + { + "text": "先了解桌面列表和终端设置。", + "ratio": 7.94 + }, + { + "text": "×", + "ratio": 7.14 + }, + { + "text": "无需工作账号", + "ratio": 11.03 + }, + { + "text": "可预览桌面、收藏和连接设置,设备与桌面信息为示例。配置仅保存在当前浏览器,不连接", + "ratio": 7.14 + }, + { + "text": "取消", + "ratio": 12.27 + }, + { + "text": "进入离线体验", + "ratio": 5.1 + } + ], + "dark#auth-sso": [ + { + "text": "体验账号", + "ratio": 6.96 + }, + { + "text": "账号或密码不正确,请重试或查看体验账号", + "ratio": 8.38 + }, + { + "text": "连接设置", + "ratio": 12.27 + }, + { + "text": "选择云端服务或企业部署入口。", + "ratio": 7.94 + }, + { + "text": "×", + "ratio": 7.14 + }, + { + "text": "云端服务", + "ratio": 7.14 + }, + { + "text": "企业部署", + "ratio": 7.23 + }, + { + "text": "企业服务地址使用 HTTPS 地址;HTTP 仅支持本机预览。", + "ratio": 12.27 + }, + { + "text": "使用 HTTPS 地址;HTTP 仅支持本机预览。", + "ratio": 7.94 + }, + { + "text": "租户或组织标识 选填", + "ratio": 12.27 + }, + { + "text": "选填", + "ratio": 7.94 + }, + { + "text": "取消", + "ratio": 12.27 + }, + { + "text": "保存并继续", + "ratio": 5.1 + } + ], + "dark-sso": [ + { + "text": "体验账号", + "ratio": 6.96 + }, + { + "text": "账号或密码不正确,请重试或查看体验账号", + "ratio": 8.38 + }, + { + "text": "企业账号登录", + "ratio": 12.27 + }, + { + "text": "通过企业统一身份进入云桌面。", + "ratio": 7.94 + }, + { + "text": "×", + "ratio": 7.14 + }, + { + "text": "企业服务", + "ratio": 7.14 + }, + { + "text": "https://vdi.example.com", + "ratio": 11.03 + }, + { + "text": "组织", + "ratio": 7.14 + }, + { + "text": "越欣科技", + "ratio": 11.03 + }, + { + "text": "体验身份", + "ratio": 7.14 + }, + { + "text": "越欣科技 · 体验用户", + "ratio": 11.03 + }, + { + "text": "当前为统一身份登录预览,不会跳转或访问真实企业身份服务。", + "ratio": 7.94 + }, + { + "text": "取消登录", + "ratio": 12.27 + }, + { + "text": "使用体验身份继续", + "ratio": 5.1 + } + ], + "light#auth-demo": [ + { + "text": "体验账号", + "ratio": 6.95 + }, + { + "text": "账号或密码不正确,请重试或查看体验账号", + "ratio": 6.06 + }, + { + "text": "体验账号", + "ratio": 12.69 + }, + { + "text": "用于预览登录、桌面选择和连接设置流程。", + "ratio": 6.18 + }, + { + "text": "×", + "ratio": 5.72 + }, + { + "text": "工作账号", + "ratio": 5.72 + }, + { + "text": "demo", + "ratio": 11.72 + }, + { + "text": "密码", + "ratio": 5.72 + }, + { + "text": "Demo123!", + "ratio": 11.72 + }, + { + "text": "这是交互原型的公开体验账号,未连接真实认证服务。请使用体验信息。", + "ratio": 5.72 + }, + { + "text": "返回", + "ratio": 12.69 + }, + { + "text": "填入体验账号", + "ratio": 5.1 + } + ], + "light#auth-forgot": [ + { + "text": "体验账号", + "ratio": 6.95 + }, + { + "text": "账号或密码不正确,请重试或查看体验账号", + "ratio": 6.06 + }, + { + "text": "忘记密码", + "ratio": 12.69 + }, + { + "text": "企业工作账号由管理员统一管理。", + "ratio": 6.18 + }, + { + "text": "×", + "ratio": 5.72 + }, + { + "text": "请联系越欣科技管理员重置密码", + "ratio": 11.72 + }, + { + "text": "可提供工作账号与设备序列号 VDI-8F42A1,协助管理员定位。", + "ratio": 5.72 + }, + { + "text": "原型体验账号无法修改密码,可重新查看体验信息。", + "ratio": 6.18 + }, + { + "text": "返回登录", + "ratio": 12.69 + }, + { + "text": "查看体验账号", + "ratio": 5.1 + } + ], + "light#auth-offline": [ + { + "text": "体验账号", + "ratio": 6.95 + }, + { + "text": "账号或密码不正确,请重试或查看体验账号", + "ratio": 6.06 + }, + { + "text": "离线体验", + "ratio": 12.69 + }, + { + "text": "先了解桌面列表和终端设置。", + "ratio": 6.18 + }, + { + "text": "×", + "ratio": 5.72 + }, + { + "text": "无需工作账号", + "ratio": 11.72 + }, + { + "text": "可预览桌面、收藏和连接设置,设备与桌面信息为示例。配置仅保存在当前浏览器,不连接", + "ratio": 5.72 + }, + { + "text": "取消", + "ratio": 12.69 + }, + { + "text": "进入离线体验", + "ratio": 5.1 + } + ], + "light#auth-sso": [ + { + "text": "体验账号", + "ratio": 6.95 + }, + { + "text": "账号或密码不正确,请重试或查看体验账号", + "ratio": 6.06 + }, + { + "text": "连接设置", + "ratio": 12.69 + }, + { + "text": "选择云端服务或企业部署入口。", + "ratio": 6.18 + }, + { + "text": "×", + "ratio": 5.72 + }, + { + "text": "云端服务", + "ratio": 5.72 + }, + { + "text": "企业部署", + "ratio": 5.57 + }, + { + "text": "企业服务地址使用 HTTPS 地址;HTTP 仅支持本机预览。", + "ratio": 12.69 + }, + { + "text": "使用 HTTPS 地址;HTTP 仅支持本机预览。", + "ratio": 6.18 + }, + { + "text": "租户或组织标识 选填", + "ratio": 12.69 + }, + { + "text": "选填", + "ratio": 6.18 + }, + { + "text": "取消", + "ratio": 12.69 + }, + { + "text": "保存并继续", + "ratio": 5.1 + } + ], + "light-sso": [ + { + "text": "体验账号", + "ratio": 6.95 + }, + { + "text": "账号或密码不正确,请重试或查看体验账号", + "ratio": 6.06 + }, + { + "text": "企业账号登录", + "ratio": 12.69 + }, + { + "text": "通过企业统一身份进入云桌面。", + "ratio": 6.18 + }, + { + "text": "×", + "ratio": 5.72 + }, + { + "text": "企业服务", + "ratio": 5.72 + }, + { + "text": "https://vdi.example.com", + "ratio": 11.72 + }, + { + "text": "组织", + "ratio": 5.72 + }, + { + "text": "越欣科技", + "ratio": 11.72 + }, + { + "text": "体验身份", + "ratio": 5.72 + }, + { + "text": "越欣科技 · 体验用户", + "ratio": 11.72 + }, + { + "text": "当前为统一身份登录预览,不会跳转或访问真实企业身份服务。", + "ratio": 6.18 + }, + { + "text": "取消登录", + "ratio": 12.69 + }, + { + "text": "使用体验身份继续", + "ratio": 5.1 + } + ] + }, + "minMeasuredNewTextContrast": 5.1, + "screenshots": [ + "login-demo-dark.png", + "login-demo-light.png", + "login-dialog-390.png", + "login-dialog-820.png", + "login-enterprise-dark.png", + "login-enterprise-light.png", + "login-forgot-dark.png", + "login-forgot-light.png", + "login-interactive-dark-1280.png", + "login-interactive-dark-1366.png", + "login-interactive-dark-1920.png", + "login-interactive-dark.png", + "login-interactive-light-1280.png", + "login-interactive-light-1366.png", + "login-interactive-light-1920.png", + "login-interactive-light.png", + "login-offline-dark.png", + "login-offline-light.png", + "login-sso-dark.png", + "login-sso-light.png", + "login-success-dark.png", + "login-validation-dark.png", + "login-validation-light.png" + ], + "browserErrors": [], + "externalRequestsDuringAccountFlows": [], + "sourceHashes": { + "index.html": "02488524b72f857c3fa307e8c17d25d3a34fd38c8c761f3c9bac611410d524d1", + "login-interactions.js": "8b48c046b824d8ef48cffffba988d950c6d78de13b5a207ab3150b2e1d506342", + "login-interactions.css": "d468dd8619b2230bcef2de3b040acd47f3e429c3c749ba54f670f862fe4db62d", + "workspace.js": "eb42cd659e2d731a05b64a8e2a6fc92ec4cd14e47d36c558432d4ff2979eb968", + "workspace.css": "c1a0e30cf11a1c5f3e539ef4446890b3f54e204f6c2e00c84c694972ece4ea2d", + "review.html": "9244afde8b12ea3f1ff2c1fd26b743f36d13c0fd96d36023c34655d3460b9378" + }, + "priorReport": "verification.json describes the preceding workspace iteration. Its original login-baseline and direct-login checks are historical and superseded by this report." +} diff --git a/ui/terminal/output/playwright/reconnect-dark.png b/ui/terminal/output/playwright/reconnect-dark.png new file mode 100644 index 0000000..ef24bc4 Binary files /dev/null and b/ui/terminal/output/playwright/reconnect-dark.png differ diff --git a/ui/terminal/output/playwright/reconnect-light.png b/ui/terminal/output/playwright/reconnect-light.png new file mode 100644 index 0000000..b5559cf Binary files /dev/null and b/ui/terminal/output/playwright/reconnect-light.png differ diff --git a/ui/terminal/output/playwright/review.png b/ui/terminal/output/playwright/review.png new file mode 100644 index 0000000..cf9f464 Binary files /dev/null and b/ui/terminal/output/playwright/review.png differ diff --git a/ui/terminal/output/playwright/settings-dark.png b/ui/terminal/output/playwright/settings-dark.png new file mode 100644 index 0000000..df0b063 Binary files /dev/null and b/ui/terminal/output/playwright/settings-dark.png differ diff --git a/ui/terminal/output/playwright/settings-light.png b/ui/terminal/output/playwright/settings-light.png new file mode 100644 index 0000000..be5c141 Binary files /dev/null and b/ui/terminal/output/playwright/settings-light.png differ diff --git a/ui/terminal/output/playwright/verification.json b/ui/terminal/output/playwright/verification.json new file mode 100644 index 0000000..1d9a4f3 --- /dev/null +++ b/ui/terminal/output/playwright/verification.json @@ -0,0 +1,1308 @@ +{ + "checks": [ + "Login geometry and computed visual styles match baseline", + "Login opens all three desktop cards", + "Workspace contains only Yuexin organization", + "Unconfigured desktop connection is disabled with visible reason", + "Favorites filter reflects changed favorite state", + "Search finds desktop by operating system", + "Search empty state is actionable", + "Connection settings validate endpoint with visible message", + "Saved settings update desktop card", + "Desktop settings persist across reload", + "Connection page identifies selected desktop", + "Cancel connection returns to resource selector", + "Management keyboard shortcut opens settings", + "Network settings tab is functional", + "Device information tab is functional", + "dark visible text meets WCAG AA contrast: []", + "dark 1280×720 fits without scroll or clipping", + "dark 1366×768 fits without scroll or clipping", + "dark 1920×1080 fits without scroll or clipping", + "light visible text meets WCAG AA contrast: []", + "light 1280×720 fits without scroll or clipping", + "light 1366×768 fits without scroll or clipping", + "light 1920×1080 fits without scroll or clipping", + "Recovery cancel clears pending recovery and returns to selector", + "Error state provides settings recovery action", + "Tablet layout has no horizontal overflow", + "Narrow fallback has no horizontal overflow" + ], + "screenshots": [ + "workspace-dark-1280.png", + "workspace-dark-1366.png", + "workspace-dark-1920.png", + "workspace-light-1280.png", + "workspace-light-1366.png", + "workspace-light-1920.png" + ], + "contrast": { + "dark": [ + { + "text": "越欣科技", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "云桌面终端", + "color": "rgb(186, 197, 207)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 7.85, + "minimum": 4.5 + }, + { + "text": "终端设置", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "终端用户", + "color": "rgb(186, 197, 207)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 7.85, + "minimum": 4.5 + }, + { + "text": "我的云桌面", + "color": "rgb(237, 241, 245)", + "background": [ + 27, + 35, + 45 + ], + "ratio": 13.96, + "minimum": 3 + }, + { + "text": "选择工作环境,连接后即可开始使用。", + "color": "rgb(186, 197, 207)", + "background": [ + 27, + 35, + 45 + ], + "ratio": 9.04, + "minimum": 4.5 + }, + { + "text": "2 台可连接 / 共 3 台", + "color": "rgb(186, 197, 207)", + "background": [ + 27, + 35, + 45 + ], + "ratio": 9.04, + "minimum": 4.5 + }, + { + "text": "全部桌面3", + "color": "rgb(237, 172, 125)", + "background": [ + 60, + 48, + 41 + ], + "ratio": 6.56, + "minimum": 4.5 + }, + { + "text": "3", + "color": "rgb(237, 172, 125)", + "background": [ + 60, + 48, + 41 + ], + "ratio": 6.56, + "minimum": 4.5 + }, + { + "text": "已收藏1", + "color": "rgb(186, 197, 207)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 7.85, + "minimum": 4.5 + }, + { + "text": "1", + "color": "rgb(186, 197, 207)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 7.85, + "minimum": 4.5 + }, + { + "text": "Windows 10", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "64 位 · Windows", + "color": "rgb(186, 197, 207)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 7.85, + "minimum": 4.5 + }, + { + "text": "设计工作站", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "可连接", + "color": "rgb(156, 219, 188)", + "background": [ + 38, + 61, + 53 + ], + "ratio": 7.37, + "minimum": 4.5 + }, + { + "text": "分辨率", + "color": "rgb(186, 197, 207)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 7.85, + "minimum": 4.5 + }, + { + "text": "2560 × 1440", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "帧率", + "color": "rgb(186, 197, 207)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 7.85, + "minimum": 4.5 + }, + { + "text": "60 FPS", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "图形设计与日常办公", + "color": "rgb(186, 197, 207)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 7.85, + "minimum": 4.5 + }, + { + "text": "设置", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "连接桌面", + "color": "rgb(255, 255, 255)", + "background": [ + 170, + 89, + 40 + ], + "ratio": 5.04, + "minimum": 4.5 + }, + { + "text": "Ubuntu", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "24.04 LTS · Linux", + "color": "rgb(186, 197, 207)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 7.85, + "minimum": 4.5 + }, + { + "text": "研发环境", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "可连接", + "color": "rgb(156, 219, 188)", + "background": [ + 38, + 61, + 53 + ], + "ratio": 7.37, + "minimum": 4.5 + }, + { + "text": "分辨率", + "color": "rgb(186, 197, 207)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 7.85, + "minimum": 4.5 + }, + { + "text": "2560 × 1440", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "帧率", + "color": "rgb(186, 197, 207)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 7.85, + "minimum": 4.5 + }, + { + "text": "30 FPS", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "开发工具与研发工作环境", + "color": "rgb(186, 197, 207)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 7.85, + "minimum": 4.5 + }, + { + "text": "设置", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "连接桌面", + "color": "rgb(255, 255, 255)", + "background": [ + 170, + 89, + 40 + ], + "ratio": 5.04, + "minimum": 4.5 + }, + { + "text": "银河麒麟", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "桌面操作系统 · Linux", + "color": "rgb(186, 197, 207)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 7.85, + "minimum": 4.5 + }, + { + "text": "办公桌面", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "待配置", + "color": "rgb(239, 205, 142)", + "background": [ + 65, + 58, + 42 + ], + "ratio": 7.4, + "minimum": 4.5 + }, + { + "text": "分辨率", + "color": "rgb(186, 197, 207)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 7.85, + "minimum": 4.5 + }, + { + "text": "1920 × 1080", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "帧率", + "color": "rgb(186, 197, 207)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 7.85, + "minimum": 4.5 + }, + { + "text": "30 FPS", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "暂未配置服务器,请联系管理员", + "color": "rgb(186, 197, 207)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 7.85, + "minimum": 4.5 + }, + { + "text": "设置", + "color": "rgb(237, 241, 245)", + "background": [ + 36, + 46, + 57 + ], + "ratio": 12.13, + "minimum": 4.5 + }, + { + "text": "暂不可连接", + "color": "rgb(164, 178, 192)", + "background": [ + 43, + 55, + 67 + ], + "ratio": 5.61, + "minimum": 4.5 + }, + { + "text": "网络已连接", + "color": "rgb(156, 219, 188)", + "background": [ + 27, + 35, + 45 + ], + "ratio": 10.02, + "minimum": 4.5 + }, + { + "text": "设备 SN:VDI-8F42A1", + "color": "rgb(186, 197, 207)", + "background": [ + 27, + 35, + 45 + ], + "ratio": 9.04, + "minimum": 4.5 + }, + { + "text": "RK3528 · ARM64", + "color": "rgb(186, 197, 207)", + "background": [ + 27, + 35, + 45 + ], + "ratio": 9.04, + "minimum": 4.5 + }, + { + "text": "交互原型 · 设备与桌面状态为示例", + "color": "rgb(164, 178, 192)", + "background": [ + 27, + 35, + 45 + ], + "ratio": 7.33, + "minimum": 4.5 + } + ], + "light": [ + { + "text": "越欣科技", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 4.5 + }, + { + "text": "云桌面终端", + "color": "rgb(83, 99, 114)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 6.18, + "minimum": 4.5 + }, + { + "text": "终端设置", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 4.5 + }, + { + "text": "终端用户", + "color": "rgb(83, 99, 114)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 6.18, + "minimum": 4.5 + }, + { + "text": "我的云桌面", + "color": "rgb(37, 49, 61)", + "background": [ + 248, + 249, + 250 + ], + "ratio": 12.57, + "minimum": 3 + }, + { + "text": "选择工作环境,连接后即可开始使用。", + "color": "rgb(83, 99, 114)", + "background": [ + 248, + 249, + 250 + ], + "ratio": 5.87, + "minimum": 4.5 + }, + { + "text": "2 台可连接 / 共 3 台", + "color": "rgb(83, 99, 114)", + "background": [ + 248, + 249, + 250 + ], + "ratio": 5.87, + "minimum": 4.5 + }, + { + "text": "全部桌面3", + "color": "rgb(169, 81, 33)", + "background": [ + 249, + 238, + 230 + ], + "ratio": 4.74, + "minimum": 4.5 + }, + { + "text": "3", + "color": "rgb(169, 81, 33)", + "background": [ + 249, + 238, + 230 + ], + "ratio": 4.74, + "minimum": 4.5 + }, + { + "text": "已收藏1", + "color": "rgb(83, 99, 114)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 6.18, + "minimum": 4.5 + }, + { + "text": "1", + "color": "rgb(83, 99, 114)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 6.18, + "minimum": 4.5 + }, + { + "text": "Windows 10", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 4.5 + }, + { + "text": "64 位 · Windows", + "color": "rgb(83, 99, 114)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 6.18, + "minimum": 4.5 + }, + { + "text": "设计工作站", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 3 + }, + { + "text": "可连接", + "color": "rgb(36, 113, 79)", + "background": [ + 233, + 245, + 238 + ], + "ratio": 5.28, + "minimum": 4.5 + }, + { + "text": "分辨率", + "color": "rgb(83, 99, 114)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 6.18, + "minimum": 4.5 + }, + { + "text": "2560 × 1440", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 4.5 + }, + { + "text": "帧率", + "color": "rgb(83, 99, 114)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 6.18, + "minimum": 4.5 + }, + { + "text": "60 FPS", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 4.5 + }, + { + "text": "图形设计与日常办公", + "color": "rgb(83, 99, 114)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 6.18, + "minimum": 4.5 + }, + { + "text": "设置", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 4.5 + }, + { + "text": "连接桌面", + "color": "rgb(255, 255, 255)", + "background": [ + 169, + 81, + 33 + ], + "ratio": 5.4, + "minimum": 4.5 + }, + { + "text": "Ubuntu", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 4.5 + }, + { + "text": "24.04 LTS · Linux", + "color": "rgb(83, 99, 114)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 6.18, + "minimum": 4.5 + }, + { + "text": "研发环境", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 3 + }, + { + "text": "可连接", + "color": "rgb(36, 113, 79)", + "background": [ + 233, + 245, + 238 + ], + "ratio": 5.28, + "minimum": 4.5 + }, + { + "text": "分辨率", + "color": "rgb(83, 99, 114)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 6.18, + "minimum": 4.5 + }, + { + "text": "2560 × 1440", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 4.5 + }, + { + "text": "帧率", + "color": "rgb(83, 99, 114)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 6.18, + "minimum": 4.5 + }, + { + "text": "30 FPS", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 4.5 + }, + { + "text": "开发工具与研发工作环境", + "color": "rgb(83, 99, 114)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 6.18, + "minimum": 4.5 + }, + { + "text": "设置", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 4.5 + }, + { + "text": "连接桌面", + "color": "rgb(255, 255, 255)", + "background": [ + 169, + 81, + 33 + ], + "ratio": 5.4, + "minimum": 4.5 + }, + { + "text": "银河麒麟", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 4.5 + }, + { + "text": "桌面操作系统 · Linux", + "color": "rgb(83, 99, 114)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 6.18, + "minimum": 4.5 + }, + { + "text": "办公桌面", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 3 + }, + { + "text": "待配置", + "color": "rgb(129, 88, 27)", + "background": [ + 249, + 241, + 223 + ], + "ratio": 5.58, + "minimum": 4.5 + }, + { + "text": "分辨率", + "color": "rgb(83, 99, 114)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 6.18, + "minimum": 4.5 + }, + { + "text": "1920 × 1080", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 4.5 + }, + { + "text": "帧率", + "color": "rgb(83, 99, 114)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 6.18, + "minimum": 4.5 + }, + { + "text": "30 FPS", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 4.5 + }, + { + "text": "暂未配置服务器,请联系管理员", + "color": "rgb(83, 99, 114)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 6.18, + "minimum": 4.5 + }, + { + "text": "设置", + "color": "rgb(37, 49, 61)", + "background": [ + 255, + 255, + 255 + ], + "ratio": 13.25, + "minimum": 4.5 + }, + { + "text": "暂不可连接", + "color": "rgb(89, 106, 121)", + "background": [ + 240, + 243, + 245 + ], + "ratio": 5.01, + "minimum": 4.5 + }, + { + "text": "网络已连接", + "color": "rgb(36, 113, 79)", + "background": [ + 248, + 249, + 250 + ], + "ratio": 5.61, + "minimum": 4.5 + }, + { + "text": "设备 SN:VDI-8F42A1", + "color": "rgb(83, 99, 114)", + "background": [ + 248, + 249, + 250 + ], + "ratio": 5.87, + "minimum": 4.5 + }, + { + "text": "RK3528 · ARM64", + "color": "rgb(83, 99, 114)", + "background": [ + 248, + 249, + 250 + ], + "ratio": 5.87, + "minimum": 4.5 + }, + { + "text": "交互原型 · 设备与桌面状态为示例", + "color": "rgb(89, 106, 121)", + "background": [ + 248, + 249, + 250 + ], + "ratio": 5.29, + "minimum": 4.5 + } + ] + }, + "geometry": { + "dark-1280": { + "h": 720, + "w": 1280, + "viewportH": 720, + "viewportW": 1280, + "cards": [ + { + "x": 32, + "right": 422.65625, + "bottom": 630.953125 + }, + { + "x": 444.65625, + "right": 835.328125, + "bottom": 630.953125 + }, + { + "x": 857.328125, + "right": 1247.984375, + "bottom": 630.953125 + } + ] + }, + "dark-1366": { + "h": 768, + "w": 1366, + "viewportH": 768, + "viewportW": 1366, + "cards": [ + { + "x": 43, + "right": 455, + "bottom": 630.953125 + }, + { + "x": 477, + "right": 889, + "bottom": 630.953125 + }, + { + "x": 911, + "right": 1323, + "bottom": 630.953125 + } + ] + }, + "dark-1920": { + "h": 1080, + "w": 1920, + "viewportH": 1080, + "viewportW": 1920, + "cards": [ + { + "x": 320, + "right": 729.328125, + "bottom": 770.828125 + }, + { + "x": 755.328125, + "right": 1164.65625, + "bottom": 770.828125 + }, + { + "x": 1190.65625, + "right": 1600, + "bottom": 770.828125 + } + ] + }, + "light-1280": { + "h": 720, + "w": 1280, + "viewportH": 720, + "viewportW": 1280, + "cards": [ + { + "x": 32, + "right": 422.65625, + "bottom": 630.953125 + }, + { + "x": 444.65625, + "right": 835.328125, + "bottom": 630.953125 + }, + { + "x": 857.328125, + "right": 1247.984375, + "bottom": 630.953125 + } + ] + }, + "light-1366": { + "h": 768, + "w": 1366, + "viewportH": 768, + "viewportW": 1366, + "cards": [ + { + "x": 43, + "right": 455, + "bottom": 630.953125 + }, + { + "x": 477, + "right": 889, + "bottom": 630.953125 + }, + { + "x": 911, + "right": 1323, + "bottom": 630.953125 + } + ] + }, + "light-1920": { + "h": 1080, + "w": 1920, + "viewportH": 1080, + "viewportW": 1920, + "cards": [ + { + "x": 320, + "right": 729.328125, + "bottom": 770.828125 + }, + { + "x": 755.328125, + "right": 1164.65625, + "bottom": 770.828125 + }, + { + "x": 1190.65625, + "right": 1600, + "bottom": 770.828125 + } + ] + } + }, + "date": "2026-11-09", + "scope": "HTML interaction prototype, no ARM or backend verification", + "login_sha256": "9f84a6961c32d854e7ede4ba7bfd732b8ca0dd21bae39726f14a22486ba4e017", + "login_assets_unchanged": true, + "additional": { + "lightLoginPreserved": true, + "contrast": { + "dark-settings": { + "minimum": 5.04, + "count": 23 + }, + "dark-connecting": { + "minimum": 6.92, + "count": 18 + }, + "dark-reconnect": { + "minimum": 6.92, + "count": 18 + }, + "dark-error": { + "minimum": 5.04, + "count": 14 + }, + "light-settings": { + "minimum": 4.74, + "count": 23 + }, + "light-connecting": { + "minimum": 5.29, + "count": 18 + }, + "light-reconnect": { + "minimum": 5.29, + "count": 18 + }, + "light-error": { + "minimum": 5.29, + "count": 14 + } + }, + "stateLayouts": { + "dark-settings": "no horizontal clipping", + "dark-connecting": "no horizontal clipping", + "dark-reconnect": "no horizontal clipping", + "dark-error": "no horizontal clipping", + "light-settings": "no horizontal clipping", + "light-connecting": "no horizontal clipping", + "light-reconnect": "no horizontal clipping", + "light-error": "no horizontal clipping" + }, + "connectionPreviewTerminates": true, + "errorRecovery": true, + "logout": true + }, + "source_files": { + "index.html": "49d01270b527030a13a086347af28f5a3fd47ec8c1063442e20acece4b9e7a4d", + "workspace.css": "c1a0e30cf11a1c5f3e539ef4446890b3f54e204f6c2e00c84c694972ece4ea2d", + "workspace.js": "79e4899622aa80a7119b0566fa9769d27c5fdd7be2fd044b26b5194802522326" + }, + "browserConsole": { + "errors": 0, + "warnings": 0 + }, + "reviewPage": { + "themeSwitcher": true, + "localImagesLoaded": true + } +} \ No newline at end of file diff --git a/ui/terminal/output/playwright/workspace-dark-1280.png b/ui/terminal/output/playwright/workspace-dark-1280.png new file mode 100644 index 0000000..613ed72 Binary files /dev/null and b/ui/terminal/output/playwright/workspace-dark-1280.png differ diff --git a/ui/terminal/output/playwright/workspace-dark-1366.png b/ui/terminal/output/playwright/workspace-dark-1366.png new file mode 100644 index 0000000..df4937c Binary files /dev/null and b/ui/terminal/output/playwright/workspace-dark-1366.png differ diff --git a/ui/terminal/output/playwright/workspace-dark-1920.png b/ui/terminal/output/playwright/workspace-dark-1920.png new file mode 100644 index 0000000..0f5dfa5 Binary files /dev/null and b/ui/terminal/output/playwright/workspace-dark-1920.png differ diff --git a/ui/terminal/output/playwright/workspace-dark-390.png b/ui/terminal/output/playwright/workspace-dark-390.png new file mode 100644 index 0000000..e7e12bc Binary files /dev/null and b/ui/terminal/output/playwright/workspace-dark-390.png differ diff --git a/ui/terminal/output/playwright/workspace-dark-820.png b/ui/terminal/output/playwright/workspace-dark-820.png new file mode 100644 index 0000000..5f03689 Binary files /dev/null and b/ui/terminal/output/playwright/workspace-dark-820.png differ diff --git a/ui/terminal/output/playwright/workspace-light-1280.png b/ui/terminal/output/playwright/workspace-light-1280.png new file mode 100644 index 0000000..a21ca30 Binary files /dev/null and b/ui/terminal/output/playwright/workspace-light-1280.png differ diff --git a/ui/terminal/output/playwright/workspace-light-1366.png b/ui/terminal/output/playwright/workspace-light-1366.png new file mode 100644 index 0000000..112b5fe Binary files /dev/null and b/ui/terminal/output/playwright/workspace-light-1366.png differ diff --git a/ui/terminal/output/playwright/workspace-light-1920.png b/ui/terminal/output/playwright/workspace-light-1920.png new file mode 100644 index 0000000..74efb3b Binary files /dev/null and b/ui/terminal/output/playwright/workspace-light-1920.png differ diff --git a/ui/terminal/references/README.md b/ui/terminal/references/README.md new file mode 100644 index 0000000..f23da4e --- /dev/null +++ b/ui/terminal/references/README.md @@ -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 接入。原型不伪装远程连接成功,连接状态在预览等待后进入说明页,并提供检查设置的恢复路径。 diff --git a/ui/terminal/references/sources.json b/ui/terminal/references/sources.json new file mode 100644 index 0000000..2d61a0f --- /dev/null +++ b/ui/terminal/references/sources.json @@ -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" + } +] \ No newline at end of file diff --git a/ui/terminal/references/windows-app-devices.png b/ui/terminal/references/windows-app-devices.png new file mode 100644 index 0000000..62516c4 Binary files /dev/null and b/ui/terminal/references/windows-app-devices.png differ diff --git a/ui/terminal/references/windows-app-web-devices.png b/ui/terminal/references/windows-app-web-devices.png new file mode 100644 index 0000000..a72a61f Binary files /dev/null and b/ui/terminal/references/windows-app-web-devices.png differ diff --git a/ui/terminal/references/workspaces-list.png b/ui/terminal/references/workspaces-list.png new file mode 100644 index 0000000..b450979 Binary files /dev/null and b/ui/terminal/references/workspaces-list.png differ diff --git a/ui/terminal/references/workspaces-session-health.png b/ui/terminal/references/workspaces-session-health.png new file mode 100644 index 0000000..9879aa0 Binary files /dev/null and b/ui/terminal/references/workspaces-session-health.png differ diff --git a/ui/terminal/references/workspaces-settings.png b/ui/terminal/references/workspaces-settings.png new file mode 100644 index 0000000..3a44067 Binary files /dev/null and b/ui/terminal/references/workspaces-settings.png differ diff --git a/ui/terminal/review.html b/ui/terminal/review.html new file mode 100644 index 0000000..385b6bc --- /dev/null +++ b/ui/terminal/review.html @@ -0,0 +1,12 @@ + +越欣科技 VDI · 界面评审
+

越欣科技 VDI · 终端界面

沿用深蓝灰、铜橙和圆角体系,包含登录校验、企业连接与桌面选择、设置和连接状态。

打开交互原型 ↗
+
越欣科技终端桌面选择页
1366 × 768 实际浏览器渲染。卡片使用统一底色,系统识别、桌面名称、状态、规格、操作依次排列。
+

登录交互

可交互的终端登录界面
体验账号:demo · 密码:Demo123!。支持输入校验、记住账号、密码显示、企业连接、SSO 取消、忘记密码和离线体验;认证为本地模拟。
+
5.04 : 1深色桌面列表的最小文字对比度
4.74 : 1浅色桌面列表的最小文字对比度
1280 × 720三张卡片、连接操作与状态条完整可见
+

参考真实 VDI 界面,保留自己的视觉风格

采用的结构

  • 系统标识与桌面名称分层,状态靠近桌面名称。
  • 搜索、收藏、单资源连接入口各自独立。
  • 设置与连接分开,未配置时显示原因。

针对 ARM 终端调整

  • 取消大块彩色卡头、壁纸叠字和重复系统名称。
  • 连接操作至少 44 px,适配 720p / 1080p 显示器。
  • 连接设置对应 kiosk-shell 的地址、应用、分辨率、帧率。

另核对 Omnissa Horizon 官方按桌面设置与连接流程。完整来源和采用理由见 设计依据。

+

同一套风格覆盖设置与状态页

桌面连接设置
连接设置:目标桌面、地址、应用名、分辨率与帧率。
正在连接桌面
连接过程:选定桌面、当前步骤、取消和恢复路径。
+ +
diff --git a/ui/terminal/workspace.css b/ui/terminal/workspace.css new file mode 100644 index 0000000..d4d56df --- /dev/null +++ b/ui/terminal/workspace.css @@ -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} +} diff --git a/ui/terminal/workspace.js b/ui/terminal/workspace.js new file mode 100644 index 0000000..a5ea4cb --- /dev/null +++ b/ui/terminal/workspace.js @@ -0,0 +1,183 @@ +(() => { + 'use strict'; + const root = document.getElementById('workspace'); + const login = document.getElementById('login'); + const paths = { + monitor: '', + settings: '', + sun: '', + moon: '', + user: '', + star: '', + search: '', + refresh: '', + arrow: '', + back: '', + network: '', + info: '', + logout: '', + check: '', + close: '' + }; + const icon = name => ``; + 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' + ? '' + : `${d.id === 'ubuntu' ? 'Ubuntu' : '麒麟软件'} 标识`; + return `
${art}
`; + }; + const prettyResolution = d => d.resolution.replace('x', ' × '); + const themeLabel = () => document.body.dataset.theme === 'dark' ? '切换浅色' : '切换深色'; + const header = () => `
越欣科技云桌面终端
`; + const footer = () => `
交互原型 · 设备与桌面状态为示例
`; + function card(d) { + const available = !!d.endpoint; + return `
${mark(d)}
${escape(d.os)}
${escape(d.version)}

${escape(d.name)}

${available ? '可连接' : '待配置'}
分辨率
${prettyResolution(d)}
帧率
${d.fps} FPS

${available ? escape(d.id === 'kylin' ? '国产办公工作环境' : d.note) : '暂未配置服务器,请联系管理员'}

`; + } + function list() { + return `

我的云桌面

选择工作环境,连接后即可开始使用。

${desktops.filter(d => d.endpoint).length} 台可连接 / 共 ${desktops.length} 台
`; + } + 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('') : `
${icon('search')}

${search ? '没有找到匹配的桌面' : '还没有收藏的桌面'}

${search ? '试试系统名称,或清除搜索条件。' : '点击桌面卡片右上角的星标即可收藏。'}

`; + } + const option = (value, label, current) => ``; + function desktopSettings() { + const d = selected(); + return `

桌面连接设置

配置当前桌面的连接地址和显示参数。

`; + } + function settings() { + let panel; + if (settingsTab === 'desktop') panel = desktopSettings(); + if (settingsTab === 'network') panel = `

网络状态

连接远程桌面前,请确认终端已接入工作网络。

连接方式
有线网络
网络状态
已连接
地址获取
自动获取(DHCP)

此处为设备状态示例。实际网络配置由终端系统管理。

`; + if (settingsTab === 'device') panel = `

设备信息

联系管理员时,可提供设备序列号协助定位。

设备序列号
VDI-8F42A1
设备型号
RK3528 云终端
处理器架构
ARM64
管理快捷键
Ctrl + Alt + Shift + M
`; + return `

终端设置

${panel}
`; + } + function session() { + const d = selected(), failure = sessionState === 'error', recovery = sessionState === 'reconnect'; + const title = failure ? '暂时无法连接' : recovery ? '正在恢复连接' : '正在连接桌面'; + const steps = recovery ? ['检测网络连接','恢复桌面会话','恢复桌面画面'] : ['检查连接配置','建立远程会话','准备桌面画面']; + return `
${mark(d)}

${title}

${escape(d.os)} · ${escape(d.name)}

${failure ? '
' : '
'}${failure ? `
尚未接入远程桌面服务
当前为交互原型,未向服务器发送连接请求。实际终端需由桌面服务返回连接结果。
` : `
    ${steps.map((step,i) => `
  1. ${i === 0 ? '✓' : i+1}${step}${i === 1 ? '' : ''}
  2. `).join('')}
${recovery ? '连接中断后将尝试恢复原会话,请保持设备联网。' : '请保持设备联网。连接完成后,桌面将全屏打开。'}
当前预览不发起真实远程连接。
`}
${failure ? `` : ''}
`; + } + function render() { + root.innerHTML = header() + `
${view === 'desktops' ? list() : view === 'settings' ? settings() : session()}
` + 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(); + } +})();