docs(ui05,ui06): admin analysis+HTML preview, app-preview evidence, UI06 unified visual plan
- admin-preview-2026-09-22/ 20页新版后台HTML原型+截图+validation(UI05 交付) - app-preview-2026-09-22/ 截图/validation/check 脚本入库及小改 - admin-redesign-concept 概念图 - UI05 管理后台分析与新版设计说明、用户列表分页计划文档 - UI06 全端视觉统一实施方案(以两个 HTML 预览为唯一视觉基准) - services/api g4-sync-once 手动同步脚本 Co-Authored-By: Claude Code <noreply@anthropic.com>
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 登录</title><link rel="stylesheet" href="styles.css"></head><body data-page="1"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 相册管理</title><link rel="stylesheet" href="styles.css"></head><body data-page="2"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 相册照片</title><link rel="stylesheet" href="styles.css"></head><body data-page="3"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 相册成员</title><link rel="stylesheet" href="styles.css"></head><body data-page="4"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 分类管理</title><link rel="stylesheet" href="styles.css"></head><body data-page="5"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 水印配置</title><link rel="stylesheet" href="styles.css"></head><body data-page="6"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 相册分享</title><link rel="stylesheet" href="styles.css"></head><body data-page="7"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 新建相册</title><link rel="stylesheet" href="styles.css"></head><body data-page="8"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 编辑相册</title><link rel="stylesheet" href="styles.css"></head><body data-page="9"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 账号管理</title><link rel="stylesheet" href="styles.css"></head><body data-page="10"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 用户同步</title><link rel="stylesheet" href="styles.css"></head><body data-page="11"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 存储与用量</title><link rel="stylesheet" href="styles.css"></head><body data-page="12"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 网站展示位</title><link rel="stylesheet" href="styles.css"></head><body data-page="13"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 配置展示位</title><link rel="stylesheet" href="styles.css"></head><body data-page="14"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 宣传图与资产</title><link rel="stylesheet" href="styles.css"></head><body data-page="15"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 照片大图</title><link rel="stylesheet" href="styles.css"></head><body data-page="16"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 无权限</title><link rel="stylesheet" href="styles.css"></head><body data-page="17"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 页面不存在</title><link rel="stylesheet" href="styles.css"></head><body data-page="18"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 服务异常</title><link rel="stylesheet" href="styles.css"></head><body data-page="19"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 同步批次详情</title><link rel="stylesheet" href="styles.css"></head><body data-page="20"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1,63 @@
|
||||
# PhotoLive 管理后台新版 HTML 原型
|
||||
|
||||
打开 **index.html**,使用左侧菜单浏览;点击“全部页面目录”查看20个入口。每个页面也有独立HTML文件,可双击打开。无需启动服务或访问互联网。
|
||||
|
||||
视觉基于Art Design Pro / Element Plus的后台布局,但此目录是独立原生HTML原型,没有改动 `apps/admin`。
|
||||
|
||||
## 交付文件
|
||||
|
||||
- `index.html`:默认相册管理入口。
|
||||
- `01-*.html` 至 `20-*.html`:独立页面,见下表。
|
||||
- `styles.css`、`admin.js`、`icons.js`、`pages.js`:共享外观、交互及索引。
|
||||
- `assets/`:离线示例图片与演示二维码。
|
||||
- `screenshots/01.png` 至 `20.png`:实际HTML渲染截图;mobile前缀为窄屏检查。
|
||||
- `validation.json`:本次浏览器检查结果。
|
||||
- `image-prompt.txt`:概念图完整生成提示词。
|
||||
- `../../plans/2026-09-22-UI05-管理后台分析与新版设计说明.md`:现状分析、接口差异及实施映射。
|
||||
|
||||
## 页面目录
|
||||
|
||||
| 编号 | 内容 | 文件 |
|
||||
|---|---|---|
|
||||
| 01 | 账号密码登录 | 01-login.html |
|
||||
| 02 | 相册卡片/表格列表 | 02-albums.html |
|
||||
| 03 | 相册照片网格/表格 | 03-photos.html |
|
||||
| 04 | 成员与所有者交接 | 04-members.html |
|
||||
| 05 | 双语分类 | 05-categories.html |
|
||||
| 06 | 水印预览、生成任务 | 06-watermark.html |
|
||||
| 07 | 分享卡、二维码、链接 | 07-sharing.html |
|
||||
| 08 | 新建相册表单 | 08-create-album.html |
|
||||
| 09 | 编辑资料、访问开关 | 09-edit-album.html |
|
||||
| 10 | 账号、角色与治理 | 10-users.html |
|
||||
| 11 | 用户目录同步 | 11-user-sync.html |
|
||||
| 12 | 用量、阈值与告警 | 12-usage.html |
|
||||
| 13 | 展示位、绑定、接入代码 | 13-embed-slots.html |
|
||||
| 14 | v1/v2配置与小窗预览 | 14-embed-editor.html |
|
||||
| 15 | 宣传图与资产入口 | 15-branding.html |
|
||||
| 16 | 管理大图及操作 | 16-photo-viewer.html |
|
||||
| 17 | 403 | 17-403.html |
|
||||
| 18 | 404 | 18-404.html |
|
||||
| 19 | 500 | 19-500.html |
|
||||
| 20 | 同步批次失败明细 | 20-sync-detail.html |
|
||||
|
||||
## 可以体验
|
||||
|
||||
- 中英文切换、相册搜索、卡片/表格切换、照片分类/状态筛选。
|
||||
- 查看大图、单张隐藏/恢复/软删除、选择已公开照片批量隐藏。
|
||||
- 创建和编辑示例相册、邀请内部示例用户、交接所有者、分类增改。
|
||||
- 水印文字/位置/透明度/字号即时预览。
|
||||
- 账号搜索、角色修改、重置密码流程、激活/锁定;自身账号角色不可改。
|
||||
- 同步运行模拟、失败详情、用量阈值对话框。
|
||||
- 展示位开关/绑定、v1/v2设置、固定195×280预览、收起展开、模拟气泡、复制示例代码。
|
||||
|
||||
## 重要边界
|
||||
|
||||
所有数据和操作为样例,不调用生产接口、不真正重置密码或上传/删除文件。请勿输入真实账号密码。原型状态只存在当前标签页内存,重新打开独立页会重置。
|
||||
|
||||
摄影局部来自本次生成的后台概念图,`assets/photo-reference.png` 保留离线来源。界面文字、表单、卡片和表格均为真实DOM。样例文件名、时间、人数和数据不能抄进生产默认值。
|
||||
|
||||
分享QR指向 `https://example.com/photolive-preview`,仅用于展示,不是相册链接。跨原型链接依赖上一层已生成的 `viewer-preview-2026-09-22.html` / `embed-preview-2026-09-22.html`;搬运目录时请一同携带,或修改为实际目标。
|
||||
|
||||
嵌入配置中的预览为简化CSS展示;正式组件须复用现有UI04代码。此次未完成真实角色权限、OSS、网络异常、水印生成、用户同步或Art Design Pro运行集成验收。
|
||||
|
||||
`build-preview.cjs` / `check-preview.cjs` 是本仓库的生成和检查辅助脚本。普通浏览无需执行它们。
|
||||
@@ -0,0 +1,65 @@
|
||||
/* PhotoLive admin design prototype. All writes are local sample-state changes. */
|
||||
const $=s=>document.querySelector(s),$$=s=>[...document.querySelectorAll(s)];
|
||||
const icon=n=>`<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="${window.ICONS[n]||window.ICONS.photo}"/></svg>`;
|
||||
const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
let lang='zh',page=2,albumIndex=0,albumMode='grid',photoMode='grid',photoFilter='all',photoCategory='all',photoQuery='',albumQuery='',albumStatus='all',currentPhoto=0,selected=new Set(),toastTimer,syncRunning=false,wmVersion=3,wmPosition='bottom-right',slotIndex=0;
|
||||
const T=(en,zh)=>lang==='zh'?zh:en;
|
||||
const albums=[{title:'Canton Fair · Autumn',zh:'广交会 · 秋季',date:'2026.10.15 – 10.19',venue:'Guangzhou',owner:'Alex Morgan',public:true,upload:true,photo:0},{title:'Product Launch 2026',zh:'2026 新品发布会',date:'2026.09.28',venue:'Shenzhen',owner:'Emma Chen',public:true,upload:true,photo:5},{title:'Team Stories',zh:'团队故事',date:'2026.09.22',venue:'Shanghai',owner:'Daniel Lee',public:false,upload:true,photo:2},{title:'Design Summit',zh:'设计峰会',date:'2026.10.08 – 10.09',venue:'Hangzhou',owner:'Alex Morgan',public:true,upload:false,photo:4},{title:'Partner Conference',zh:'合作伙伴大会',date:'2026.11.12',venue:'Singapore',owner:'Emma Chen',public:false,upload:true,photo:1},{title:'Innovation Week',zh:'创新周',date:'2026.11.20 – 11.24',venue:'Guangzhou',owner:'Daniel Lee',public:true,upload:true,photo:3}];
|
||||
const people=[{name:'Alex Morgan',account:'alex.morgan',role:'admin',source:'local',credential:'active',locked:false},{name:'Emma Chen',account:'emma.chen',role:'staff',source:'active',credential:'active',locked:false},{name:'Daniel Lee',account:'daniel.lee',role:'staff',source:'active',credential:'pending',locked:false},{name:'Olivia Wang',account:'olivia.wang',role:'staff',source:'disabled',credential:'active',locked:true},{name:'James Wilson',account:'james.wilson',role:'staff',source:'active',credential:'active',locked:false}];
|
||||
let memberIds=[0,1,2],cats=[{en:'Exhibition',zh:'展会现场'},{en:'People',zh:'人物交流'},{en:'Highlights',zh:'精彩瞬间'}];
|
||||
const photoStates=Array.from({length:12},(_,i)=>i===6?'hidden':i===10?'unpublished':i===11?'deleted':'published');
|
||||
const slots=[{name:'Corporate website',zh:'企业官网',album:0,enabled:true,mode:'v2',version:4,speed:16},{name:'Event landing page',zh:'活动专题页',album:1,enabled:true,mode:'v1',version:2,speed:16},{name:'Partner portal',zh:'合作伙伴门户',album:null,enabled:false,mode:'v2',version:1,speed:16}];
|
||||
// Photographic regions only; the full source board is never used as the UI.
|
||||
const pics=[[292,165,393,149],[612,565,292,117],[931,565,292,117],[1247,565,289,117],[279,786,295,118],[608,786,295,118],[934,786,289,118],[1250,786,287,118]];
|
||||
function photo(i,cls=''){const[x,y,w,h]=pics[(i+pics.length)%pics.length];return `<div class="art ${cls}" style="aspect-ratio:${w}/${h}"><svg width="100%" height="100%" viewBox="${x} ${y} ${w} ${h}" preserveAspectRatio="xMidYMid slice" aria-hidden="true" style="display:block"><image href="assets/photo-reference.png" width="1586" height="992"/></svg></div>`}
|
||||
function btn(label,action,cls='',ic=''){return `<button class="btn ${cls}" data-action="${action}">${ic?icon(ic):''}${label}</button>`}
|
||||
function pill(label,cls=''){return `<span class="pill ${cls}"><span class="dot"></span>${label}</span>`}
|
||||
function title(a=albums[albumIndex]){return esc(lang==='zh'?a.zh:a.title)}
|
||||
function avatar(name,cls=''){return `<span class="avatar ${cls}">${esc(name.split(' ').map(x=>x[0]).join(''))}</span>`}
|
||||
function field(label,id,value='',type='text',note=''){return `<label class="field">${label}<input id="${id}" type="${type}" value="${esc(value)}">${note?`<small>${note}</small>`:''}</label>`}
|
||||
function select(label,id,opts,value){return `<label class="field">${label}<select id="${id}">${opts.map(([v,s])=>`<option value="${v}" ${v===value?'selected':''}>${s}</option>`).join('')}</select></label>`}
|
||||
function switchRow(label,desc,id,on){return `<label class="switch-row"><span><strong>${label}</strong><p>${desc}</p></span><input class="switch" type="checkbox" id="${id}" ${on?'checked':''}></label>`}
|
||||
function table(headers,rows){return `<div class="data-wrap"><table class="data-table"><thead><tr>${headers.map(x=>`<th>${x}</th>`).join('')}</tr></thead><tbody>${rows.map(cols=>`<tr>${cols.map(x=>`<td>${x}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`}
|
||||
function heading(name,desc,actions=''){return `<div class="page-heading"><div><h1>${name}</h1><p>${desc}</p></div><div class="actions">${actions}</div></div>`}
|
||||
function tabs(){return `<nav class="tabs">${[[3,'Photos','照片'],[4,'Members','成员'],[5,'Categories','分类'],[6,'Watermark','水印'],[7,'Sharing','分享'],[15,'Branding','宣传图与资产']].map(([id,en,zh])=>`<a href="#${id}" class="${page===id?'active':''}">${T(en,zh)}</a>`).join('')}</nav>`}
|
||||
function albumHeader(){const a=albums[albumIndex];return heading(title(),T('Album workspace · Manage the moments that matter','相册工作区 · 管理活动中的精彩瞬间'),btn(T('Edit album','编辑相册'),'go:9','outline','edit')+btn(T('Share album','分享相册'),'go:7','primary','share'))+`<div class="card album-banner">${photo(a.photo,'cover')}<div class="info-columns"><div class="meta-item">${icon('calendar')}<div><strong>${a.date}</strong><p>${T('Event time · Asia/Shanghai','活动时间 · Asia/Shanghai')}</p></div></div><div class="meta-item">${icon('user')}<div><strong>${T('Owner','创建者')}</strong><p>${a.owner}</p></div></div><div class="meta-item">${icon('pin')}<div><strong>${a.venue}</strong><p>${T('Event venue','活动地点')}</p></div></div><div class="meta-item">${icon('globe')}<div><strong>${a.public?T('Public access on','允许公开访问'):T('Public access off','公开访问关闭')}</strong><p>${a.upload?T('Staff uploads enabled','工作人员上传已开启'):T('Staff uploads disabled','工作人员上传已关闭')}</p></div></div></div></div>`+tabs()}
|
||||
function shell(content){const selectedRoute=[2,3,4,5,6,7,8,9,15,16].includes(page)?2:[13,14].includes(page)?13:page===20?11:page;const link=(id,ic,en,zh)=>`<button class="side-link ${selectedRoute===id?'active':''}" data-action="go:${id}">${icon(ic)}${T(en,zh)}</button>`;return `<aside class="sidebar" id="sidebar"><div class="brand">${icon('logo')}PhotoLive</div><div class="group-label">${T('WORKSPACE','工作空间')}</div>${link(2,'album','Albums','相册管理')}${link(13,'photo','Embed widgets','网站展示位')}<div class="group-label">${T('ORGANIZATION','组织与治理')}</div>${link(10,'users','Accounts','账号管理')}${link(11,'flip','User sync','用户同步')}${link(12,'upload','Storage & usage','存储与用量')}<button class="preview-directory" data-action="directory">${T('Preview all pages ↗','全部页面目录 ↗')}</button><div class="side-bottom"><div class="person">${avatar('Alex Morgan')}<div><strong style="font-size:12px">Alex Morgan</strong><p class="muted small">${T('Company administrator','公司管理员')}</p></div></div><button class="text-btn" style="margin:8px 0 0 36px" data-action="go:1">${T('Sign out','退出登录')}</button></div></aside><div class="shell"><header class="topbar"><div class="breadcrumbs"><button class="mobile-menu" data-action="menu" aria-label="Menu">${icon('more')}</button><span>PhotoLive</span><span>/</span><span>${window.ADMIN_PAGES.find(p=>p.id===page)?.[lang]||T('Workspace','工作空间')}</span></div><div class="top-actions"><span class="demo-pill">${T('DESIGN PREVIEW · SAMPLE DATA','设计预览 · 示例数据')}</span><select id="language" aria-label="Language"><option value="zh" ${lang==='zh'?'selected':''}>中文</option><option value="en" ${lang==='en'?'selected':''}>EN</option></select><div class="person">${avatar('Alex Morgan')}<span class="small">Alex Morgan</span></div></div></header><main class="workspace">${content}</main></div>`}
|
||||
function login(){return `<div class="login"><section class="login-visual">${photo(0)}<div class="brand">${icon('logo')}PhotoLive</div><div class="login-copy"><p style="font-size:10px;letter-spacing:3px;margin-bottom:20px">EVERY EVENT. EVERY MOMENT.</p><h1>${T('Bring your events into focus.','每个精彩现场,<br>从这里开始。')}</h1><p>${T('One workspace for your event albums, team and shared moments.','集中管理活动相册、协作团队与照片,让每个精彩瞬间及时抵达观众。')}</p></div><p style="position:relative;z-index:2;color:#8093af;font-size:11px">PhotoLive · Staff workspace</p></section><section class="login-form-area"><form id="loginForm" class="login-form"><div class="brand">${icon('logo')}PhotoLive</div><h1>${T('Welcome back','欢迎回来')}</h1><p>${T('Sign in to your company workspace','登录企业影像工作空间')}</p>${field(T('Account','账号'),'loginAccount','demo.admin')}${field(T('Password','密码'),'loginPassword','','password')}<div class="error" id="formError"></div><button type="submit" class="btn primary">${T('Sign in to preview','进入预览')}</button><p class="small muted" style="margin-top:22px">${T('Accounts are provided by your administrator. No registration or Feishu sign-in.','账号由企业提供,不支持自行注册或飞书登录。')}</p><div class="info-strip" style="margin-top:26px">${icon('help')}${T('Offline prototype. Do not enter real credentials.','离线交互原型,请勿输入真实账号密码。')}</div></form></section></div>`}
|
||||
function albumList(){const rows=albums.map((a,i)=>({a,i})).filter(({a})=>(!albumQuery||JSON.stringify(a).toLowerCase().includes(albumQuery.toLowerCase()))&&(albumStatus==='all'||String(a.public)===albumStatus));return heading(T('Albums','相册管理'),T('A clear view of every event and the team behind it.','以活动为中心,管理相册、团队和每一张照片。'),btn(T('New album','新建相册'),'go:8','primary','plus'))+`<div class="toolbar"><div class="search">${icon('photo')}<input id="albumSearch" value="${esc(albumQuery)}" placeholder="${T('Search loaded albums','搜索当前已加载相册')}"></div><select id="albumStatus" aria-label="Album status"><option value="all">${T('All access states','全部公开状态')}</option><option value="true" ${albumStatus==='true'?'selected':''}>${T('Public','公开')}</option><option value="false" ${albumStatus==='false'?'selected':''}>${T('Private','未公开')}</option></select><div class="push seg"><button data-action="albumMode:grid" class="${albumMode==='grid'?'active':''}">${icon('photo')}</button><button data-action="albumMode:list" class="${albumMode==='list'?'active':''}">${icon('album')}</button></div></div><div class="summary-line"><span>${T('Loaded sample albums','已加载示例相册')} · ${rows.length}</span><span>${T('Sorted by creation time · cursor-based loading','按创建时间排序 · 沿用游标加载')}</span></div>${albumMode==='grid'?`<div class="album-grid">${rows.map(({a,i})=>`<article class="album"><button class="album-cover" data-action="album:${i}" aria-label="${title(a)}">${photo(a.photo)}${pill(a.public?T('Public','公开'):T('Private','未公开'),a.public?'green':'gray')}</button><div class="album-info"><h2><button data-action="album:${i}" style="padding:0;font-weight:650">${title(a)}</button></h2><p class="meta">${a.date} · ${a.venue}</p><div class="album-footer"><div class="person">${avatar(a.owner)}<span>${a.owner}</span></div>${pill(a.upload?T('Uploads on','可上传'):T('Uploads off','上传关闭'),a.upload?'':'gray')}</div></div></article>`).join('')}</div>`:`<div class="card">${table([T('Album','相册'),T('Owner','创建者'),T('Event date','活动日期'),T('Public access','公开访问'),T('Actions','操作')],rows.map(({a,i})=>[title(a),a.owner,a.date,pill(a.public?T('Public','公开'):T('Private','未公开'),a.public?'green':'gray'),btn(T('Manage','管理'),'album:'+i,'small outline')]))}</div>`}<div class="load-more">${rows.length?T('All sample albums are loaded','示例相册已全部加载'):T('No albums match these filters','没有符合筛选条件的相册')}</div>`}
|
||||
function visibility(state){return {published:T('Published','已公开'),hidden:T('Hidden','已隐藏'),unpublished:T('Unpublished','未公开'),deleted:T('Deleted','已删除')}[state]}
|
||||
function filteredPhotos(){return Array.from({length:12},(_,i)=>i).filter(i=>(photoFilter==='all'||photoStates[i]===photoFilter)&&(photoCategory==='all'||i%3===Number(photoCategory))&&(!photoQuery||('DSC0'+(4872+i)+'.jpg').toLowerCase().includes(photoQuery.toLowerCase())))}
|
||||
function photosPage(){const list=filteredPhotos();return albumHeader()+`<div class="toolbar"><div class="seg">${['all','published','hidden','unpublished','deleted'].map(s=>`<button class="${photoFilter===s?'active':''}" data-action="photoFilter:${s}">${s==='all'?T('All photos','全部照片'):visibility(s)}</button>`).join('')}</div><select id="photoCategory" aria-label="Category"><option value="all">${T('All categories','全部分类')}</option>${cats.map((c,i)=>`<option value="${i}" ${String(i)===photoCategory?'selected':''}>${esc(c[lang])}</option>`).join('')}</select><div class="search">${icon('photo')}<input id="photoSearch" value="${esc(photoQuery)}" placeholder="${T('Search sample filenames','搜索示例文件名')}"></div><div class="seg push"><button data-action="photoMode:grid" class="${photoMode==='grid'?'active':''}">${icon('photo')}</button><button data-action="photoMode:list" class="${photoMode==='list'?'active':''}">${icon('album')}</button></div></div><div class="selection-bar" id="selectionBar" ${selected.size?'':'hidden'}><span id="selectedCount">${selected.size} ${T('selected','张已选')}</span>${btn(T('Hide selected','隐藏选中照片'),'batchHide','small outline','eye')}<span class="small">${T('Only published photos can be selected','仅已公开照片可批量选择')}</span></div>${photoMode==='grid'?`<div class="photo-grid">${list.map(i=>`<article class="photo-item"><div class="photo-image ${selected.has(i)?'selected':''}"><button data-action="photo:${i}" aria-label="${T('View photo','查看照片')} ${i+1}">${photoStates[i]==='deleted'?`<div class="empty">${T('Deleted','已删除')}</div>`:photo(i)}</button><input class="photo-check" type="checkbox" value="${i}" aria-label="${T('Select photo','选择照片')} ${i+1}" ${selected.has(i)?'checked':''} ${photoStates[i]!=='published'?'disabled':''}>${pill(visibility(photoStates[i]),photoStates[i]==='published'?'green':'gray')}</div><div class="photo-caption"><div><strong>DSC0${4872+i}.jpg</strong><p>${people[i%3].name} · Oct 15, 2026</p></div><button data-action="photo:${i}" aria-label="${T('Photo actions','照片操作')}">${icon('more')}</button></div></article>`).join('')}</div>`:`<div class="card">${table([T('Photo','照片'),T('Processing','处理状态'),T('Visibility','可见性'),T('Uploader','上传者'),T('Likes (replica)','点赞数(副本)'),T('Actions','操作')],list.map(i=>[`<div class="person">${photo(i,'tiny-photo')}DSC0${4872+i}.jpg</div>`,pill(i===10?T('Processing','处理中'):T('Ready','已就绪'),i===10?'':'green'),pill(visibility(photoStates[i]),photoStates[i]==='published'?'green':'gray'),people[i%3].name,[128,86,54,102][i%4],btn(T('Details','详情'),'photo:'+i,'small')]))}</div>`}<div class="load-more">${T('Sample photos · Statistics replica updated at 14:32 · No live API connection','示例照片 · 统计副本更新时间 14:32 · 未连接真实接口')}</div>`}
|
||||
function membersPage(){return albumHeader()+`<div class="card"><div class="card-head"><div><h2>${T('Album members','相册成员')}</h2><p class="small muted" style="margin-top:6px">${T('Owners manage all photos. Collaborators manage their own uploads.','创建者管理全部照片;协作者管理自己上传的照片。')}</p></div>${btn(T('Invite colleague','邀请同事'),'invite','primary','users')}</div>${table([T('Member','成员'),T('Account','账号'),T('Role','角色'),T('Scope','权限范围'),T('Actions','操作')],memberIds.map((id,index)=>{const p=people[id];return [`<div class="person">${avatar(p.name)}<strong>${p.name}</strong></div>`,p.account,pill(index===0?T('Owner','创建者'):T('Collaborator','协作者'),index===0?'':'gray'),index===0?T('All photos and members','全部照片及成员'):T('Own uploaded photos','自己上传的照片'),index===0?btn(T('Transfer ownership','交接相册'),'transfer','small'):btn(T('Remove','移除'),'removeMember:'+id,'small danger')]}))}</div>`}
|
||||
function categoriesPage(){return albumHeader()+`<div class="card"><div class="card-head"><div><h2>${T('Photo categories','照片分类')}</h2><p class="small muted">${T('Keep both content languages together when saving.','保存时保留所有语言内容,避免覆盖其他语言。')}</p></div>${btn(T('New category','新增分类'),'category:new','primary','plus')}</div>${table([T('Category','分类名称'),T('Chinese','中文'),T('English','English'),T('Actions','操作')],cats.map((c,i)=>[`<div class="person"><span class="avatar">${icon('tag')}</span><strong>${esc(c[lang])}</strong></div>`,esc(c.zh),esc(c.en),btn(T('Edit','编辑'),'category:'+i,'small')+' '+btn(T('Delete','删除'),'deleteCategory:'+i,'small danger')]))}<p class="small muted" style="margin-top:18px">${T('A category with photos cannot be deleted.','已有照片的分类不能直接删除。')}</p></div>`}
|
||||
function watermarkPage(){return albumHeader()+`<div class="two-col equal"><div class="card"><div class="card-head"><h2>${T('Watermark settings','水印配置')}</h2>${pill('v'+wmVersion,'green')}</div><form id="watermarkForm">${switchRow(T('Enable watermark','启用水印'),T('Apply to generated viewing variants','应用于生成的浏览变体'),'wmOn',true)}<div class="form-grid" style="margin-top:20px">${field(T('Watermark text','水印文字'),'wmText','PhotoLive')}${select(T('Position','位置'),'wmPosition',[['bottom-right',T('Bottom right','右下')],['bottom-left',T('Bottom left','左下')],['top-right',T('Top right','右上')],['top-left',T('Top left','左上')],['center',T('Center','居中')]],wmPosition)}<label class="field">${T('Opacity','透明度')}<input id="wmOpacity" type="range" min="0" max="100" value="70"></label>${field(T('Font size','字号'),'wmFont','20','number')}</div><button type="submit" class="btn primary">${T('Save and generate','保存并生成')}</button><span class="small muted"> ${T('Demo job only','仅演示任务')}</span></form></div><div class="card"><div class="card-head"><h2>${T('Visual preview','效果预览')}</h2><span class="small muted">CSS ${T('simulation','示意')}</span></div><div class="watermark-preview">${photo(1)}<span class="watermark-text" id="wmPreview">PhotoLive</span></div><p class="small muted" style="margin-top:14px">${T('Preview placement only. Final rendering is produced by the server.','此处仅示意位置,最终水印由服务端生成。')}</p></div></div><div class="card"><div class="card-head"><h2>${T('Generation history','生成任务')}</h2>${btn(T('Regenerate','重新生成'),'regenerate','outline','flip')}</div>${table([T('Version','版本'),T('State','状态'),T('Processed','已处理'),T('Failed','失败'),T('Created','创建时间')],[['v'+wmVersion,pill(T('Completed','已完成'),'green'),'124 / 124','0','2026-09-22 14:20'],['v2',pill(T('Completed','已完成'),'green'),'118 / 118','0','2026-09-21 16:42']])}</div>`}
|
||||
function sharingPage(){return albumHeader()+`<div class="two-col equal"><div class="card share-card">${photo(albums[albumIndex].photo,'cover')}<h2>${title()}</h2><p>${albums[albumIndex].date} · ${albums[albumIndex].venue}</p><img class="qr" src="assets/demo-qr.svg" alt="Demonstration QR code"><p>${T('Demo code · example.com · not a public event link','演示二维码 · 指向 example.com · 非真实活动链接')}</p></div><div class="card"><h2>${T('Share your event','分享活动相册')}</h2><p class="muted" style="margin:14px 0 22px">${T('Visitors browse without signing in. Public access must be enabled.','观众凭链接或扫码免登录浏览,须先开启相册公开访问。')}</p><div class="info-strip ${albums[albumIndex].public?'':'warn'}">${icon('globe')}${albums[albumIndex].public?T('Public access is enabled in this sample.','当前示例已开启公开访问。'):T('Public access is disabled in this sample.','当前示例已关闭公开访问。')}</div>${field(T('Preview link','预览链接'),'shareLink',new URL('../viewer-preview-2026-09-22.html',location.href).href)}<div class="actions">${btn(T('Copy preview link','复制预览链接'),'copyShare','primary','link')}${btn(T('Open viewer','打开观众页'),'viewer','outline')}</div><p class="small muted" style="margin-top:24px">${T('Local file links work on this computer only. Production QR codes must use the public album URL.','本地链接仅用于本机预览,生产二维码必须使用真实公开相册 URL。')}</p></div></div>`}
|
||||
function albumForm(edit){const a=albums[albumIndex];return heading(edit?T('Edit album','编辑相册'):T('Create album','新建相册'),T('Event details, content languages and public access in one place.','统一配置活动资料、多语言内容与访问权限。'),btn(T('Cancel','取消'),'go:2'))+`<form id="albumForm"><div class="two-col"><div class="card"><div class="card-head"><h2>${T('Event details','活动资料')}</h2>${pill(T('Chinese + English','中文 + English'))}</div><div class="form-grid">${field(T('Title (Chinese)','标题(中文)'),'titleZh',edit?a.zh:'')}${field(T('Title (English)','标题(English)'),'titleEn',edit?a.title:'')}${field(T('Start date','开始日期'),'startDate','2026-10-15','date')}${field(T('End date','结束日期'),'endDate','2026-10-19','date')}${select(T('Time zone','时区'),'timezone',[['Asia/Shanghai','Asia/Shanghai'],['Europe/London','Europe/London'],['America/New_York','America/New_York']],'Asia/Shanghai')}${select(T('Default language','默认语言'),'contentLang',[['zh','中文'],['en','English']],'zh')}${field(T('Venue (Chinese)','地点(中文)'),'venueZh',edit?'广州':'')}${field(T('Venue (English)','地点(English)'),'venueEn',edit?a.venue:'')}<label class="field wide">${T('Description (Chinese)','说明(中文)')}<textarea id="descriptionZh">${edit?'欢迎浏览本次活动的精彩照片。':''}</textarea></label><label class="field wide">${T('Description (English)','说明(English)')}<textarea id="descriptionEn">${edit?'Discover the moments from this event.':''}</textarea></label></div></div><div><div class="card"><h2>${T('Access settings','访问设置')}</h2>${switchRow(T('Staff uploads','工作人员上传'),T('Team members can upload photos','团队成员可上传照片'),'albumUpload',a.upload)}${switchRow(T('Public access','公开访问'),T('Visitors with the link can browse','凭链接免登录浏览'),'albumPublic',a.public)}${switchRow(T('Photo downloads','允许下载'),T('Download viewing variants','观众可下载浏览图'),'albumDownload',true)}${switchRow(T('Original downloads','原图下载'),T('Requires photo downloads','须先允许下载'),'albumOriginal',false)}</div><div class="info-strip" style="margin-top:18px">${icon('help')}${T('Preview changes are stored in this tab only.','预览变更仅保留在当前标签页内。')}</div><div id="formError" class="error"></div><button class="btn primary" style="width:100%" type="submit">${T('Save album','保存相册')}</button></div></div></form>`}
|
||||
function usersPage(){return heading(T('Accounts','账号管理'),T('Provisioned company accounts, credential state and local access controls.','统一查看企业来源状态、登录凭据与本地账号治理。'),btn(T('View user sync','查看用户同步'),'go:11','outline','flip'))+`<div class="info-strip">${icon('help')}${T('User information is pulled from the company directory. No public registration. Source state and local lock are independent.','用户信息来自企业接口定期拉取,不开放注册;来源状态与本地锁定相互独立。')}</div><div class="card"><div class="toolbar"><div class="search">${icon('user')}<input id="userSearch" placeholder="${T('Search account or name','搜索账号或姓名')}"></div><select id="userStatus" aria-label="Account status"><option value="all">${T('All states','全部状态')}</option><option value="locked">${T('Locally locked','本地锁定')}</option><option value="pending">${T('Pending activation','待激活')}</option></select>${btn(T('Reset filters','重置筛选'),'resetUserFilters')}</div><div id="usersTable">${usersTable()}</div></div>`}
|
||||
function usersTable(query='',status='all'){return table([T('User','用户'),T('Role','角色'),T('Source status','来源状态'),T('Credentials','凭据状态'),T('Local lock','本地锁定'),T('Actions','操作')],people.map((p,i)=>({p,i})).filter(({p})=>(p.name+p.account).toLowerCase().includes(query.toLowerCase())&&(status==='all'||status==='locked'&&p.locked||status==='pending'&&p.credential==='pending')).map(({p,i})=>[`<div class="person">${avatar(p.name)}<div><strong>${p.name}</strong>${i===0?' '+pill(T('You','当前用户')):''}<p>${p.account}</p></div></div>`,pill(p.role==='admin'?T('Admin','管理员'):T('Staff','员工'),p.role==='admin'?'':'gray'),pill(p.source==='local'?T('Local','本地'):p.source==='active'?T('Active','来源有效'):T('Disabled','来源停用'),p.source==='disabled'?'red':'gray'),pill(p.credential==='pending'?T('Pending','待激活'):T('Active','已激活'),p.credential==='pending'?'amber':'green'),pill(p.locked?T('Locked','已锁定'):T('Unlocked','未锁定'),p.locked?'red':'gray'),`<div class="actions">${btn(T('Edit','编辑'),'userEdit:'+i,'small')}${btn(T('Reset password','重置密码'),'resetPassword:'+i,'small')}${i>0?btn(p.credential==='pending'?T('Activate','激活'):p.locked?T('Unlock','解锁'):T('Lock','锁定'),'userState:'+i,'small'):''}</div>`]))}
|
||||
function syncPage(){return heading(T('User sync','用户同步'),T('Review scheduled directory pulls and resolve failed records.','查看企业用户接口的定时拉取结果,定位失败记录。'),btn(syncRunning?T('Running…','运行中…'):T('Pull now','手动补拉'),'sync','primary','flip'))+`<div class="stats"><div class="stat"><p>${T('Engine state','同步引擎状态')}</p><strong style="font-size:23px">${syncRunning?T('Running','运行中'):T('Idle','空闲')}</strong><small>${T('Last success','最近成功')} 2026-09-22 14:30</small></div><div class="stat"><p>${T('Last batch','最近批次')}</p><strong>128 <span style="font-size:13px;color:#8294ae">${T('processed','条处理')}</span></strong><small>${T('3 created · 124 updated · 1 failed','新增 3 · 更新 124 · 失败 1')}</small></div><div class="stat"><p>${T('Source version','来源版本')}</p><strong style="font-size:23px">20260922.1430</strong><small>${T('User directory watermark, not photo replication','用户目录水位,并非照片跨境同步')}</small></div></div><div class="card"><div class="card-head"><h2>${T('Run history','运行历史')}</h2><span class="small muted">${T('Sample scheduled jobs','示例定时任务')}</span></div>${table([T('Triggered','触发方式'),T('State','运行状态'),T('Started','开始时间'),T('Created','新增'),T('Updated','更新'),T('Failed','失败'),T('Details','详情')],Array.from({length:4},(_,i)=>[pill(i===1?T('Manual','手动'):T('Scheduled','定时'),'gray'),pill(i===0?T('Partial failure','部分失败'):T('Completed','完成'),i===0?'amber':'green'),`2026-09-22 ${14-i}:30`,i===0?'3':'0',i===0?'124':'128',i===0?'1':'0',btn(T('View run','查看批次'),'syncDetail:'+i,'small outline')]))}</div>`}
|
||||
function usagePage(){return heading(T('Storage & usage','存储与用量'),T('Understand retained assets and act before thresholds are exceeded.','查看长期保留的照片资产与告警阈值。'),btn(T('Threshold settings','告警阈值'),'thresholds','outline','settings'))+`<div class="stats"><div class="stat"><p>${T('Total photos','照片总数')}</p><strong>18,420</strong><small>${T('Sample aggregate from the usage response','示例用量接口汇总')}</small></div><div class="stat"><p>${T('Stored assets','存储用量')}</p><strong>148.6 <span style="font-size:17px">GB</span></strong><small>${T('Originals + viewing variants','原图 + 浏览变体')}</small></div><div class="stat"><p>${T('Storage alert threshold','总存储告警阈值')}</p><strong id="thresholdValue">200 <span style="font-size:17px">GB</span></strong><small>${T('An alert threshold, not a hard storage quota','告警阈值,并非存储硬配额')}</small></div></div><div class="card"><div class="card-head"><h2>${T('Asset composition','资产构成')}</h2><span class="small muted">${T('OSS · retained by default','OSS · 默认长期保留')}</span></div><div class="storage-bar"><span style="width:68%"></span><span style="width:20%"></span></div><div class="legend"><span>● ${T('Originals','原图')} 114.8 GB</span><span>● ${T('Variants','变体')} 33.8 GB</span></div></div><div class="card"><div class="card-head"><h2>${T('Usage by album','相册用量')}</h2></div>${table([T('Album','相册'),T('Photos','照片数'),T('Originals','原图'),T('Variants','变体'),T('Total','合计')],albums.slice(0,4).map((a,i)=>[title(a),[6280,4210,3150,4780][i],[42.5,26.1,18.7,27.5][i]+' GB',[12.3,8.2,5.8,7.5][i]+' GB',[54.8,34.3,24.5,35][i]+' GB']))}</div><div class="card"><div class="card-head"><h2>${T('Alerts','用量告警')}</h2></div>${table([T('State','状态'),T('Kind','类别'),T('Message','说明'),T('Time','时间')],[[pill(T('Active','未解决'),'amber'),T('Per-album storage','单相册存储'),T('Canton Fair exceeds the 50 GB sample threshold.','广交会相册超过示例阈值 50 GB。'),'2026-09-22 14:20'],[pill(T('Resolved','已解决'),'gray'),T('Storage','存储'),T('Previous threshold alert resolved.','历史阈值告警已解除。'),'2026-09-20 09:30']])}</div>`}
|
||||
function slotsPage(){return heading(T('Embed widgets','网站展示位'),T('Publish a small live photo window on your websites.','在企业网站中展示活动照片,管理绑定与展示配置。'),btn(T('New widget','新增展示位'),'newSlot','primary','plus'))+`<div class="info-strip">${icon('photo')}${T('New design: 195 × 280 px on desktop and mobile. v1 carousel and v2 vertical stream remain distinct.','新版照片窗:电脑与手机统一 195 × 280 px;v1 轮播与 v2 上滚模式分开管理。')}</div><div class="card">${table([T('Widget','展示位名称'),T('Bound album','绑定相册'),T('Mode','展示模式'),T('Enabled','启用'),T('Config version','配置版本'),T('Actions','操作')],slots.map((s,i)=>[esc(lang==='zh'?s.zh:s.name),s.album===null?pill(T('Unbound','未绑定'),'gray'):title(albums[s.album]),pill(s.mode==='v2'?T('v2 · Vertical stream','v2 · 照片上滚'):T('v1 · Carousel','v1 · 旧版轮播')),`<input class="switch" type="checkbox" data-slot-toggle="${i}" aria-label="${esc(s.name)}" ${s.enabled?'checked':''}>`,'v'+s.version,`<div class="actions">${btn(T('Configure','配置'),'slot:'+i,'small outline')}${btn(T('Bind album','绑定相册'),'bind:'+i,'small')}${btn(T('Get code','获取代码'),'code:'+i,'small')}</div>`]))}</div>`}
|
||||
function embedPreview(){const seq=[0,1,2,0,1,2];return `<div class="preview-frame"><div class="mock-site">COMPANY WEBSITE / PREVIEW</div><div class="site-lines"><div></div><div style="width:64%"></div><div style="width:82%"></div><div style="width:56%"></div></div><div class="embedded" id="embedded"><div class="embed-track" id="embedTrack">${seq.map(i=>photo(i)).join('')}</div><span class="embed-hearts" id="embedHearts">♥</span><button class="embed-minus" data-action="collapseEmbed" aria-label="Collapse preview">−</button></div><button id="expandEmbed" class="btn" style="position:absolute;right:16px;bottom:24px" hidden data-action="expandEmbed">${T('Photos','照片')}</button></div>`}
|
||||
function slotEditor(){const s=slots[slotIndex];return heading(T('Configure widget','配置展示位'),esc(lang==='zh'?s.zh:s.name),btn(T('Back to widgets','返回展示位'),'go:13'))+`<div class="two-col equal"><div class="card"><form id="slotForm">${field(T('Name','名称'),'slotName',lang==='zh'?s.zh:s.name)}${select(T('Display mode','展示模式'),'slotMode',[['v2',T('v2 · Vertical stream','v2 · 照片上滚')],['v1',T('v1 · Existing carousel','v1 · 保留旧轮播')]],s.mode)}<div id="v2Fields"><div class="form-grid">${field(T('Width (fixed)','宽度(固定)'),'slotWidth','195','number')}${field(T('Height (fixed)','高度(固定)'),'slotHeight','280','number')}${field(T('Scroll speed (px/s)','滚动速度(px/s)'),'slotSpeed',s.speed,'number')}${select(T('Position','悬浮位置'),'slotPosition',[['right',T('Right','右侧')],['left',T('Left','左侧')]],'right')}</div>${switchRow(T('Like bubbles','点赞气泡'),T('Real reactions in production; simulated here','正式版使用真实事件,此处仅模拟'),'slotReactions',true)}</div><div id="v1Fields" hidden>${field(T('Width','宽度'),'legacyWidth','320','number')}${field(T('Interval (ms)','切图间隔(ms)'),'legacyInterval','5000','number')}<p class="small muted">${T('Legacy settings are retained. The preview at right depicts the new design.','旧版配置继续保留。右侧展示新版设计参考。')}</p></div><div class="form-grid" style="margin-top:18px">${field(T('Horizontal offset','水平偏移'),'offsetX','16','number')}${field(T('Bottom offset','底部偏移'),'offsetY','24','number')}</div>${switchRow(T('Start collapsed on mobile','手机默认收起'),T('User choice has priority','用户主动选择优先'),'slotCollapsed',true)}<div class="error" id="formError"></div><button class="btn primary" type="submit">${T('Save configuration','保存配置')}</button></form></div><div class="card"><div class="card-head"><h2>${T('Unsaved preview','未保存预览')}</h2>${pill('195 × 280 px')}</div>${embedPreview()}<div class="actions" style="margin-top:15px">${btn(T('Simulate a reaction','模拟点赞事件'),'embedHeart','small outline')}${btn(T('Open full widget demo','打开完整组件原型'),'widgetDemo','small')}</div><p class="small muted" style="margin-top:14px">${T('Presentation mock only. The standalone widget demo implements the precise 16 px/s stream.','此面板为样式示意;独立组件原型实现精确 16 px/s 上滚。')}</p></div></div>`}
|
||||
function brandingPage(){return albumHeader()+`<div class="two-col equal"><div class="card"><h2>${T('Public album banner','观众页宣传图')}</h2><p class="small muted" style="margin:9px 0 20px">${T('Prefer a wide image. Do not crop away embedded poster text.','建议使用横幅图片,保留海报中的文字内容。')}</p>${photo(albums[albumIndex].photo,'cover')}<div class="actions" style="margin-top:18px">${btn(T('Choose sample banner','选择示例宣传图'),'banner','primary','photo')}${btn(T('Preview public album','预览观众相册'),'viewer','outline')}</div></div><div class="card"><h2>${T('Asset workflow','资产管理')}</h2><div class="info-strip" style="margin-top:20px">${icon('upload')}${T('Init → OSS direct upload → completion verification','初始化凭证 → OSS 直传 → 完成核验')}</div><p class="muted">${T('The existing API accepts banner, cover and logo asset types. Production must keep signature expiry and upload verification behavior. This prototype only swaps sample photographs.','现有接口支持 banner、cover、logo 资产类型。正式实现保留签名过期与上传核验流程。本原型仅切换示例照片。')}</p><h3 style="margin:24px 0 10px">${T('Current sample','当前示例')}</h3><p class="small muted">event-banner.jpg · image/jpeg · ${T('Sample preview','示意预览')}</p></div></div>`}
|
||||
function exceptionPage(code){const text={403:[T('Access restricted','暂无访问权限'),T('Your current account cannot access this workspace.','当前账号无权访问此页面,请联系公司管理员。')],404:[T('Page not found','页面不存在'),T('The link may have changed or the resource is unavailable.','链接可能已失效,或资源暂不可用。')],500:[T('Something went wrong','服务暂时不可用'),T('Please try again. Your saved data has not been changed by this preview.','请稍后重试,已保存的数据不会因本次预览发生变化。')]}[code];return `<section class="exception"><strong>${code}</strong><h1>${text[0]}</h1><p>${text[1]}</p>${btn(T('Back to albums','返回相册'),'go:2','primary')}</section>`}
|
||||
function modal(name,body,footer='',wide=false){$('.modal')?.remove();const wrap=document.createElement('div');wrap.className='modal';wrap.innerHTML=`<section class="dialog ${wide?'wide':''}" role="dialog" aria-modal="true" aria-label="${esc(name)}"><div class="dialog-head"><h2>${name}</h2><button class="btn icon-only" data-action="close" aria-label="Close">${icon('close')}</button></div>${body}${footer?`<div class="dialog-foot">${footer}</div>`:''}</section>`;document.body.append(wrap);$('#root').inert=true;wrap.onclick=e=>{if(e.target===wrap)closeModal()};wrap.querySelector('input,button,select')?.focus();return wrap}
|
||||
function closeModal(){ $('.modal')?.remove();$('#root').inert=false;if(page===16)go(3);if(page===20)go(11)}
|
||||
function toast(text){$('.toast')?.remove();const el=document.createElement('div');el.className='toast';el.setAttribute('role','status');el.textContent=text;document.body.append(el);clearTimeout(toastTimer);toastTimer=setTimeout(()=>el.remove(),3200)}
|
||||
function go(id){if(page===Number(id)){render();return}location.hash=id}
|
||||
function render(){page=Number(location.hash.slice(1))||Number(document.body.dataset.page)||2;if(page<1||page>20)page=18;$('.modal')?.remove();$('#root').inert=false;document.documentElement.lang=lang==='zh'?'zh-CN':'en';if(page===1){$('#root').innerHTML=login();bind();return}let content='';switch(page){case 2:content=albumList();break;case 3:case 16:content=photosPage();break;case 4:content=membersPage();break;case 5:content=categoriesPage();break;case 6:content=watermarkPage();break;case 7:content=sharingPage();break;case 8:content=albumForm(false);break;case 9:content=albumForm(true);break;case 10:content=usersPage();break;case 11:case 20:content=syncPage();break;case 12:content=usagePage();break;case 13:content=slotsPage();break;case 14:content=slotEditor();break;case 15:content=brandingPage();break;case 17:content=exceptionPage(403);break;case 18:content=exceptionPage(404);break;case 19:content=exceptionPage(500);break}$('#root').innerHTML=shell(content);bind();if(page===16)openPhoto(currentPhoto);if(page===20)syncDetail(0);document.title='PhotoLive · '+(window.ADMIN_PAGES.find(p=>p.id===page)?.[lang]||'Preview')}
|
||||
function openPhoto(i){currentPhoto=i;const state=photoStates[i];if(state==='deleted'){toast(T('This sample photo has been deleted.','该示例照片已删除。'));return}const wrap=modal(T('Photo details','照片详情'),'');wrap.querySelector('.dialog').className='dialog viewer-dialog';wrap.querySelector('.dialog').innerHTML=`<div class="viewer-main"><button class="viewer-close btn icon-only" data-action="close" aria-label="Close">${icon('close')}</button>${photo(i)}</div><div class="viewer-info"><h2>DSC0${4872+i}.jpg</h2><p>${people[i%3].name}</p><p>2026-10-15 10:42<br>2400 × 1600 · 2.8 MB</p>${pill(visibility(state),state==='published'?'green':'gray')}<p>${T('Likes (statistics replica)','点赞数(统计副本)')} · ${[128,86,54,102][i%4]}</p>${state!=='unpublished'?btn(state==='hidden'?T('Publish again','恢复公开'):T('Hide photo','隐藏照片'),'visibility:'+i,'outline','eye'):''}${btn(T('Delete photo','删除照片'),'deletePhoto:'+i,'danger','trash')}<p class="small">${T('Staff management actions only. No public like or download controls.','此处为工作人员管理操作,不提供观众点赞或下载按钮。')}</p></div>`}
|
||||
function syncDetail(i){modal(T('Sync run details','同步批次详情'),`<div class="info-strip ${i===0?'warn':''}">${icon('flip')}${i===0?T('127 succeeded · 1 failed','127 条成功 · 1 条失败'):T('128 succeeded · 0 failed','128 条成功 · 0 条失败')}</div>${table([T('Record','记录'),T('Result','结果'),T('Reason','原因')],i===0?[['sample.user.129',pill(T('Failed','失败'),'red'),T('Missing required account field','缺少必填账号字段')]]:[['—',pill(T('Completed','完成'),'green'),T('No failed records','没有失败记录')]])}<p style="margin-top:18px">${T('Example failure details are redacted. Correct the source data before the next run.','失败明细为脱敏示例,请修正来源数据后再进行补拉。')}</p>`,btn(T('Close','关闭'),'close'))}
|
||||
function filterInput(id,current,assign){const el=$('#'+id);if(el)el.oninput=e=>{const start=e.target.selectionStart;assign(e.target.value);render();const next=$('#'+id);next.focus();next.setSelectionRange(start,start)}}
|
||||
function bind(){const locale=$('#language');if(locale)locale.onchange=e=>{lang=e.target.value;render()};const loginForm=$('#loginForm');if(loginForm)loginForm.onsubmit=e=>{e.preventDefault();if(!$('#loginAccount').value.trim()){$('#formError').textContent=T('Enter a sample account.','请输入演示账号。');return}go(2)};filterInput('albumSearch',albumQuery,x=>albumQuery=x);filterInput('photoSearch',photoQuery,x=>photoQuery=x);if($('#albumStatus'))$('#albumStatus').onchange=e=>{albumStatus=e.target.value;render()};if($('#photoCategory'))$('#photoCategory').onchange=e=>{photoCategory=e.target.value;render()};$$('.photo-check').forEach(el=>el.onchange=()=>{const i=Number(el.value);if(el.checked)selected.add(i);else selected.delete(i);render()});const filterUsers=()=>$('#usersTable').innerHTML=usersTable($('#userSearch').value,$('#userStatus').value);if($('#userSearch')){$('#userSearch').oninput=filterUsers;$('#userStatus').onchange=filterUsers}$$('[data-slot-toggle]').forEach(el=>el.onchange=()=>{const s=slots[Number(el.dataset.slotToggle)];s.enabled=el.checked;s.version++;render();toast(T('Sample widget state updated','示例展示位状态已更新'))});
|
||||
const form=$('#albumForm');if(form){$('#albumDownload').onchange=e=>{$('#albumOriginal').disabled=!e.target.checked;if(!e.target.checked)$('#albumOriginal').checked=false};form.onsubmit=e=>{e.preventDefault();if(!$('#titleZh').value.trim()||!$('#titleEn').value.trim()||!$('#startDate').value||$('#endDate').value<$('#startDate').value){$('#formError').textContent=T('Enter both sample titles and a valid date range.','请填写示例双语标题及有效日期范围。');return}const data={title:$('#titleEn').value,zh:$('#titleZh').value,date:$('#startDate').value+' – '+$('#endDate').value,venue:$('#venueEn').value||$('#venueZh').value,owner:'Alex Morgan',public:$('#albumPublic').checked,upload:$('#albumUpload').checked,photo:0};if(page===8){albums.unshift(data);albumIndex=0}else albums[albumIndex]={...albums[albumIndex],...data};go(3)}}
|
||||
if($('#watermarkForm')){const preview=()=>{const el=$('#wmPreview');el.textContent=$('#wmText').value;el.hidden=!$('#wmOn').checked;el.style.opacity=$('#wmOpacity').value/100;el.style.fontSize=Math.min(72,Math.max(8,Number($('#wmFont').value)||20))+'px';const pos=$('#wmPosition').value;el.style.top=pos.startsWith('top')?'18px':pos==='center'?'50%':'auto';el.style.bottom=pos.startsWith('bottom')?'18px':'auto';el.style.left=pos.endsWith('left')?'18px':pos==='center'?'50%':'auto';el.style.right=pos.endsWith('right')?'18px':'auto';el.style.transform=pos==='center'?'translate(-50%,-50%)':''};['wmText','wmOpacity','wmFont','wmPosition','wmOn'].forEach(id=>$('#'+id).oninput=preview);$('#watermarkForm').onsubmit=e=>{e.preventDefault();wmVersion++;toast(T('Sample generation job accepted. No images are modified.','示例生成任务已受理,未修改任何图片。'))}}
|
||||
if($('#slotForm')){const s=slots[slotIndex];$('#slotWidth').readOnly=true;$('#slotHeight').readOnly=true;$('#slotSpeed').min=8;$('#slotSpeed').max=32;const mode=()=>{$('#v2Fields').hidden=$('#slotMode').value!=='v2';$('#v1Fields').hidden=$('#slotMode').value!=='v1'};$('#slotMode').onchange=mode;mode();$('#offsetX').oninput=()=>{$('#embedded').style.right=Math.max(0,Number($('#offsetX').value))+'px'};$('#offsetY').oninput=()=>{$('#embedded').style.bottom=Math.max(0,Number($('#offsetY').value))+'px'};$('#slotPosition').onchange=e=>{$('#embedded').style.left=e.target.value==='left'?'16px':'auto';$('#embedded').style.right=e.target.value==='right'?'16px':'auto'};$('#slotForm').onsubmit=e=>{e.preventDefault();const speed=Number($('#slotSpeed').value);if(!$('#slotName').value.trim()||speed<8||speed>32){$('#formError').textContent=T('Name required. Speed must be between 8 and 32.','请填写名称,速度须在 8–32 之间。');return}s.name=$('#slotName').value;s.zh=s.name;s.speed=speed;s.mode=$('#slotMode').value;s.version++;go(13)}}}
|
||||
function confirm(titleText,body,callback){modal(titleText,`<p>${body}</p>`,btn(T('Cancel','取消'),'close')+btn(T('Confirm','确认'),'confirm','primary'));$('[data-action="confirm"]').onclick=()=>{closeModal();callback()}}
|
||||
function action(raw){const [name,...rest]=raw.split(':'),value=rest.join(':');switch(name){case 'go':go(value);break;case 'close':closeModal();break;case 'menu':$('#sidebar').classList.toggle('open');break;case 'directory':modal(T('All preview pages','全部预览页面'),`<div class="form-grid">${window.ADMIN_PAGES.map(p=>btn(String(p.id).padStart(2,'0')+' · '+p[lang],'go:'+p.id,'')).join('')}</div>`,'',true);break;case 'album':albumIndex=Number(value);go(3);break;case 'albumMode':albumMode=value;render();break;case 'photoMode':photoMode=value;render();break;case 'photoFilter':photoFilter=value;render();break;case 'photo':openPhoto(Number(value));break;case 'batchHide':if(!selected.size)return;confirm(T('Hide selected photos?','隐藏选中的照片?'),T('This changes sample visibility only. Real batches must report per-photo success and failure.','仅修改示例可见性。真实批处理需逐项反馈成功和失败。'),()=>{selected.forEach(i=>photoStates[i]='hidden');selected.clear();render();toast(T('Sample photos hidden','示例照片已隐藏'))});break;case 'visibility':{const i=Number(value);confirm(photoStates[i]==='hidden'?T('Publish this photo?','恢复公开这张照片?'):T('Hide this photo?','隐藏这张照片?'),T('Public visibility changes. The original remains stored.','改变公开可见性,原图仍保留在存储中。'),()=>{photoStates[i]=photoStates[i]==='hidden'?'published':'hidden';selected.delete(i);render()});break}case 'deletePhoto':confirm(T('Delete photo?','删除照片?'),T('Soft deletion removes the sample from active management. This does not immediately physically delete OSS files.','软删除会让照片退出有效管理列表,不代表立即物理删除 OSS 文件。'),()=>{photoStates[Number(value)]='deleted';selected.delete(Number(value));render()});break;
|
||||
case 'invite':{modal(T('Invite colleague','邀请同事'),`<div class="search" style="margin-bottom:15px">${icon('user')}<input id="inviteSearch" placeholder="${T('Search internal accounts','搜索企业内部账号')}"></div><div id="inviteRows">${people.map((p,i)=>!memberIds.includes(i)?`<button class="side-link candidate" data-action="addMember:${i}">${avatar(p.name)}<span>${p.name}<small class="muted"> · ${p.account}</small></span></button>`:'').join('')}</div><p>${T('No external email or registration invitation is sent.','仅选择内部账号,不发送外部邮件或注册邀请。')}</p>`);$('#inviteSearch').oninput=e=>$$('.candidate').forEach(el=>el.hidden=!el.textContent.toLowerCase().includes(e.target.value.toLowerCase()));break}case 'addMember':memberIds.push(Number(value));closeModal();render();break;case 'removeMember':confirm(T('Remove collaborator?','移除协作者?'),T('The sample account loses collaborator access to this album.','该示例账号将不再作为本相册协作者。'),()=>{memberIds=memberIds.filter(i=>i!==Number(value));render()});break;case 'transfer':modal(T('Transfer album ownership','交接相册'),select(T('New owner','新创建者'),'newOwner',people.slice(1).map((p,i)=>[String(i+1),p.name]),'1')+`<p>${T('Production must recheck the target account and creator permissions.','正式版需再次校验目标账号与创建者权限。')}</p>`,btn(T('Cancel','取消'),'close')+btn(T('Transfer sample','交接示例'),'confirmTransfer','primary'));break;case 'confirmTransfer':{const id=Number($('#newOwner').value);albums[albumIndex].owner=people[id].name;memberIds=[id,...memberIds.filter(i=>i!==id)];closeModal();render();break}
|
||||
case 'category':{const i=value==='new'?-1:Number(value);modal(i<0?T('New category','新增分类'):T('Edit category','编辑分类'),`<form id="categoryForm">${field('中文','catZh',i<0?'':cats[i].zh)}${field('English','catEn',i<0?'':cats[i].en)}<button class="btn primary" type="submit">${T('Save category','保存分类')}</button></form>`);$('#categoryForm').onsubmit=e=>{e.preventDefault();if(!$('#catZh').value.trim()||!$('#catEn').value.trim()){toast(T('Complete both sample language fields','请填写示例双语名称'));return}const item={zh:$('#catZh').value,en:$('#catEn').value};if(i<0)cats.push(item);else cats[i]=item;closeModal();render()};break}case 'deleteCategory':if(Number(value)<3)toast(T('This sample category contains photos and cannot be deleted.','该示例分类已有照片,不能删除。'));else confirm(T('Delete empty category?','删除空分类?'),T('Only this empty sample category is removed.','仅移除该空示例分类。'),()=>{cats.splice(Number(value),1);render()});break;case 'regenerate':confirm(T('Regenerate viewing variants?','重新生成浏览变体?'),T('Production creates an asynchronous job. This preview does not modify OSS.','正式版创建异步生成任务,此预览不修改 OSS。'),()=>{wmVersion++;render();toast(T('Sample job created','示例任务已创建'))});break;case 'copyShare':copy($('#shareLink').value);break;case 'viewer':window.open('../viewer-preview-2026-09-22.html','_blank','noopener,noreferrer');break;
|
||||
case 'userEdit':{const i=Number(value),p=people[i];modal(T('Edit account','编辑账号'),`<form id="userEditForm">${field(T('Display name','显示名称'),'userName',p.name)}${select(T('Role','角色'),'userRole',[['admin',T('Administrator','管理员')],['staff',T('Staff','员工')]],p.role)}<p>${i===0?T('Your own role cannot be changed here.','不能在这里修改自己的角色。'):T('Source status remains controlled by the company directory.','来源状态仍由企业用户接口控制。')}</p><button type="submit" class="btn primary" style="margin-top:20px">${T('Save','保存')}</button></form>`);$('#userRole').disabled=i===0;$('#userEditForm').onsubmit=e=>{e.preventDefault();p.name=$('#userName').value||p.name;if(i!==0)p.role=$('#userRole').value;closeModal();render()};break}case 'resetPassword':{const p=people[Number(value)];modal(T('Reset password','重置密码'),`<p style="margin-bottom:16px">${p.name} · ${p.account}</p><form id="resetForm">${field(T('Temporary password (demo only)','临时密码(仅演示)'),'tempPassword','','password')}<p>${T('Do not enter real credentials. Nothing is stored or sent.','请勿输入真实凭据,此处不保存或发送密码。')}</p><button class="btn primary" type="submit" style="margin-top:20px">${T('Confirm reset preview','确认重置演示')}</button></form>`);$('#resetForm').onsubmit=e=>{e.preventDefault();closeModal();toast(T('Password reset flow previewed. No credentials changed.','已演示密码重置流程,未修改真实凭据。'))};break}case 'userState':{const i=Number(value);if(i===0)return;const p=people[i];confirm(T('Change local account state?','修改本地账号状态?'),T('The company directory state remains unchanged.','来源接口状态不会随本地治理动作改变。'),()=>{if(p.credential==='pending')p.credential='active';else p.locked=!p.locked;render()});break}case 'resetUserFilters':$('#userSearch').value='';$('#userStatus').value='all';$('#usersTable').innerHTML=usersTable();break;case 'sync':if(syncRunning)return;confirm(T('Run a sample pull?','模拟一次用户补拉?'),T('This is a local animation; no company directory is contacted.','仅在本地演示运行状态,不请求企业用户接口。'),()=>{syncRunning=true;render();setTimeout(()=>{syncRunning=false;if([11,20].includes(page))render();toast(T('Sample pull completed','模拟补拉已完成'))},1800)});break;case 'syncDetail':syncDetail(Number(value));break;case 'thresholds':modal(T('Alert thresholds','告警阈值'),`<form id="thresholdForm">${field(T('Total storage threshold (GB)','总存储阈值(GB)'),'totalThreshold','200','number')}${field(T('Per-album threshold (GB)','单相册阈值(GB)'),'albumThreshold','50','number')}<p>${T('Thresholds trigger alerts, not automatic deletion.','达到阈值仅触发告警,不自动删除照片。')}</p><button class="btn primary" style="margin-top:20px" type="submit">${T('Save sample thresholds','保存示例阈值')}</button></form>`);$('#thresholdForm').onsubmit=e=>{e.preventDefault();const value=Number($('#totalThreshold').value);if(value<0){toast(T('Use a non-negative number','请输入非负数字'));return}closeModal();if($('#thresholdValue'))$('#thresholdValue').textContent=value+' GB';toast(T('Sample threshold updated','示例阈值已更新'))};break;
|
||||
case 'newSlot':slots.push({name:'New widget',zh:'新展示位',album:null,enabled:false,mode:'v2',version:1,speed:16});slotIndex=slots.length-1;go(14);break;case 'slot':slotIndex=Number(value);go(14);break;case 'bind':{slotIndex=Number(value);modal(T('Bind album','绑定相册'),select(T('Album','相册'),'bindAlbum',[['none',T('Unbind','解除绑定')],...albums.map((a,i)=>[String(i),lang==='zh'?a.zh:a.title])],slots[slotIndex].album===null?'none':String(slots[slotIndex].album))+`<p>${T('The same embedded slot will display the newly bound album after configuration refresh.','配置刷新后,使用此展示位的网站将展示新绑定的相册。')}</p>`,btn(T('Cancel','取消'),'close')+btn(T('Confirm binding','确认绑定'),'confirmBind','primary'));break}case 'confirmBind':slots[slotIndex].album=$('#bindAlbum').value==='none'?null:Number($('#bindAlbum').value);slots[slotIndex].version++;closeModal();render();break;case 'code':{const s=slots[Number(value)];const code='<script src="https://example.com/embed/'+s.mode+'/loader.js" data-slot="DEMO_SLOT" defer><'+'/script>';modal(T('Embed code','嵌入代码'),`<p>${T('Illustrative integration code. Replace example.com and DEMO_SLOT with real deployment values.','接入格式示例,须替换 example.com 和 DEMO_SLOT 为真实部署信息。')}</p><pre class="mono" id="embedCode">${esc(code)}</pre>`,btn(T('Copy sample code','复制示例代码'),'copyCode','primary'));break}case 'copyCode':copy($('#embedCode').textContent);break;case 'collapseEmbed':$('#embedded').hidden=true;$('#expandEmbed').hidden=false;break;case 'expandEmbed':$('#embedded').hidden=false;$('#expandEmbed').hidden=true;break;case 'embedHeart':if(!$('#slotReactions').checked)return;$('#embedded').hidden=false;$('#expandEmbed').hidden=true;$('#embedHearts').classList.remove('play');void $('#embedHearts').offsetWidth;$('#embedHearts').classList.add('play');break;case 'widgetDemo':window.open('../embed-preview-2026-09-22.html','_blank','noopener,noreferrer');break;case 'banner':modal(T('Choose sample banner','选择示例宣传图'),`<div class="album-grid">${[0,3,5].map(i=>`<button class="album-cover" data-action="setBanner:${i}">${photo(i)}</button>`).join('')}</div><p style="margin-top:20px">${T('No files are uploaded in this prototype.','此原型不会上传文件。')}</p>`,'',true);break;case 'setBanner':albums[albumIndex].photo=Number(value);closeModal();render();break}}
|
||||
function copy(text){if(navigator.clipboard)navigator.clipboard.writeText(text).then(()=>toast(T('Copied','已复制'))).catch(()=>prompt(T('Copy this text','复制以下内容'),text));else prompt(T('Copy this text','复制以下内容'),text)}
|
||||
document.addEventListener('click',e=>{const b=e.target.closest('[data-action]');if(b&&!b.disabled)action(b.dataset.action)});document.addEventListener('keydown',e=>{const modalEl=$('.modal');if(!modalEl)return;if(e.key==='Escape')closeModal();if(e.key==='Tab'){const focus=[...modalEl.querySelectorAll('button,input,select,textarea,a')].filter(el=>!el.disabled);if(e.shiftKey&&document.activeElement===focus[0]){e.preventDefault();focus.at(-1)?.focus()}else if(!e.shiftKey&&document.activeElement===focus.at(-1)){e.preventDefault();focus[0]?.focus()}}});window.addEventListener('hashchange',render);render();
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 33 33" shape-rendering="crispEdges"><path fill="#ffffff" d="M0 0h33v33H0z"/><path stroke="#172033" d="M2 2.5h7m2 0h4m1 0h1m1 0h5m1 0h7M2 3.5h1m5 0h1m2 0h3m1 0h5m4 0h1m5 0h1M2 4.5h1m1 0h3m1 0h1m1 0h3m3 0h3m5 0h1m1 0h3m1 0h1M2 5.5h1m1 0h3m1 0h1m1 0h5m2 0h1m1 0h1m1 0h1m2 0h1m1 0h3m1 0h1M2 6.5h1m1 0h3m1 0h1m1 0h2m3 0h1m2 0h1m2 0h2m1 0h1m1 0h3m1 0h1M2 7.5h1m5 0h1m1 0h1m1 0h1m2 0h4m5 0h1m5 0h1M2 8.5h7m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h7M10 9.5h2m1 0h1m1 0h1m1 0h1m2 0h1M2 10.5h1m1 0h5m2 0h1m1 0h1m2 0h1m2 0h1m2 0h1m1 0h5M3 11.5h1m5 0h2m3 0h1m3 0h5m1 0h3m3 0h1M4 12.5h3m1 0h2m2 0h2m3 0h1m1 0h1m2 0h2m1 0h2M2 13.5h5m2 0h1m1 0h1m4 0h1m1 0h1m2 0h3m1 0h1m1 0h1m1 0h1M2 14.5h1m2 0h1m2 0h1m1 0h1m6 0h3m1 0h1m5 0h2M2 15.5h2m3 0h1m1 0h1m1 0h2m2 0h2m1 0h4m1 0h4m3 0h1M3 16.5h3m1 0h2m1 0h2m3 0h4m3 0h1m1 0h5M6 17.5h2m1 0h1m2 0h1m2 0h1m1 0h2m1 0h1m1 0h2m2 0h1m2 0h1M2 18.5h2m2 0h1m1 0h5m1 0h1m1 0h2m1 0h1m1 0h1m5 0h2M2 19.5h1m1 0h1m1 0h2m1 0h1m1 0h4m3 0h9m1 0h1m1 0h1M2 20.5h1m1 0h1m2 0h2m8 0h1m2 0h1m1 0h1m3 0h1m1 0h1M2 21.5h1m1 0h1m6 0h2m1 0h1m1 0h1m4 0h2m6 0h1M2 22.5h1m2 0h4m3 0h2m3 0h3m1 0h6m1 0h3M10 23.5h2m1 0h1m1 0h2m1 0h1m3 0h1m3 0h5M2 24.5h7m3 0h6m1 0h1m1 0h2m1 0h1m1 0h3M2 25.5h1m5 0h1m1 0h1m1 0h2m1 0h1m1 0h1m2 0h3m3 0h1M2 26.5h1m1 0h3m1 0h1m1 0h5m1 0h4m2 0h5m1 0h1M2 27.5h1m1 0h3m1 0h1m1 0h3m1 0h2m2 0h1m1 0h4m1 0h1m1 0h4M2 28.5h1m1 0h3m1 0h1m1 0h1m1 0h4m1 0h2m4 0h7M2 29.5h1m5 0h1m2 0h2m3 0h3m3 0h1m1 0h4m1 0h1M2 30.5h7m1 0h3m1 0h2m3 0h3m1 0h4m1 0h1"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
@@ -0,0 +1,6 @@
|
||||
const fs=require('fs'),path=require('path'),root=__dirname;
|
||||
const pages=[['login','登录','Login'],['albums','相册管理','Albums'],['photos','相册照片','Album photos'],['members','相册成员','Members'],['categories','分类管理','Categories'],['watermark','水印配置','Watermark'],['sharing','相册分享','Sharing'],['create-album','新建相册','New album'],['edit-album','编辑相册','Edit album'],['users','账号管理','Accounts'],['user-sync','用户同步','User sync'],['usage','存储与用量','Storage & usage'],['embed-slots','网站展示位','Embed widgets'],['embed-editor','配置展示位','Widget configuration'],['branding','宣传图与资产','Album branding'],['photo-viewer','照片大图','Photo details'],['403','无权限','Access restricted'],['404','页面不存在','Page not found'],['500','服务异常','Server error'],['sync-detail','同步批次详情','Sync run details']].map(([slug,zh,en],i)=>({id:i+1,file:String(i+1).padStart(2,'0')+'-'+slug+'.html',zh,en}));
|
||||
fs.mkdirSync(path.join(root,'assets'),{recursive:true});fs.copyFileSync(path.join(root,'../admin-redesign-concept-2026-09-22.png'),path.join(root,'assets/photo-reference.png'));fs.copyFileSync(path.join(root,'../app-preview-2026-09-22/assets/demo-qr.svg'),path.join(root,'assets/demo-qr.svg'));
|
||||
const app=fs.readFileSync(path.join(root,'../app-preview-2026-09-22/app.js'),'utf8');const paths=app.match(/const paths=(\{.*?\});/s)[1];fs.writeFileSync(path.join(root,'icons.js'),'window.ICONS='+paths+';');fs.writeFileSync(path.join(root,'pages.js'),'window.ADMIN_PAGES='+JSON.stringify(pages,null,2)+';');
|
||||
const html=p=>`<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · ${p.zh}</title><link rel="stylesheet" href="styles.css"></head><body data-page="${p.id}"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>`;
|
||||
for(const p of pages)fs.writeFileSync(path.join(root,p.file),html(p));fs.writeFileSync(path.join(root,'index.html'),html(pages[1]));console.log('Generated 20 admin pages + index.');
|
||||
@@ -0,0 +1,5 @@
|
||||
const fs=require('fs'),path=require('path');const {chromium}=require(require.resolve('@playwright/test',{paths:[path.resolve(__dirname,'../../../apps/admin')]}));
|
||||
(async()=>{const browser=await chromium.launch({headless:true});const page=await browser.newPage({viewport:{width:1440,height:1000}});const errors=[];page.on('pageerror',e=>errors.push(e.message));fs.mkdirSync(path.join(__dirname,'screenshots'),{recursive:true});const files=fs.readdirSync(__dirname).filter(x=>/^\d\d-.*\.html$/.test(x)).sort(),results=[];const url=file=>'file:///'+path.join(__dirname,file).replaceAll('\\','/');for(const file of files){await page.goto(url(file));await page.evaluate(()=>Promise.all([...document.images].map(i=>i.decode().catch(()=>{}))));await page.screenshot({path:path.join(__dirname,'screenshots',file.slice(0,2)+'.png'),fullPage:true});results.push({file,overflow:await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth),brokenImages:await page.evaluate(()=>[...document.images].filter(i=>!i.complete||!i.naturalWidth).length)})}
|
||||
await page.goto(url('02-albums.html'));await page.locator('#albumSearch').fill('Product');if(await page.locator('.album').count()!==1)throw Error('Album search failed');await page.locator('.album-cover').click();await page.waitForURL('**#3');await page.locator('.photo-check').first().check();await page.locator('[data-action="batchHide"]').click();await page.locator('[data-action="confirm"]').click();await page.locator('[data-action="photoFilter:hidden"]').click();if(await page.locator('.photo-item').count()!==2)throw Error('Batch hide failed');await page.evaluate(()=>location.hash='10');await page.locator('#userSearch').fill('emma');if(await page.locator('.data-table tbody tr').count()!==1)throw Error('User search failed');await page.selectOption('#language','en');await page.waitForFunction(()=>document.documentElement.lang==='en');await page.goto(url('14-embed-editor.html'));const rect=await page.locator('#embedded').boundingBox();if(rect.width!==195||rect.height!==280)throw Error('Widget dimension mismatch');await page.locator('[data-action="collapseEmbed"]').click();await page.locator('[data-action="expandEmbed"]').click();await page.selectOption('#slotMode','v1');if(!await page.locator('#v1Fields').isVisible())throw Error('Legacy mode missing');await page.goto(url('06-watermark.html'));await page.locator('#wmText').fill('DEMO');if(await page.locator('#wmPreview').innerText()!=='DEMO')throw Error('Watermark preview mismatch');
|
||||
const narrow=[];await page.setViewportSize({width:390,height:844});for(const file of ['02-albums.html','03-photos.html','10-users.html','14-embed-editor.html']){await page.goto(url(file));narrow.push({file,overflow:await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth)});await page.screenshot({path:path.join(__dirname,'screenshots','mobile-'+file.slice(0,2)+'.png'),fullPage:true})}
|
||||
const result={date:'2026-09-22',scope:'Offline HTML only; production admin untouched',pages:results,narrow,errors,widget:{width:rect.width,height:rect.height},interactions:['album search','batch hide','visibility filter','account search','language','widget collapse','v1 mode','watermark preview']};fs.writeFileSync(path.join(__dirname,'validation.json'),JSON.stringify(result,null,2));console.log(JSON.stringify(result));await browser.close();if(errors.length||results.some(p=>p.overflow||p.brokenImages)||narrow.some(p=>p.overflow))process.exitCode=1})().catch(e=>{console.error(e);process.exit(1)});
|
||||
@@ -0,0 +1 @@
|
||||
window.ICONS={back:'m15 18-6-6 6-6',next:'m9 18 6-6-6-6',close:'m6 6 12 12M6 18 18 6',plus:'M12 5v14M5 12h14',more:'M5 12h.01M12 12h.01M19 12h.01',album:'M3 7h7l2-3h9v16H3ZM3 9h18',photo:'M3 4h18v16H3ZM4 16l5-5 4 4 3-3 5 5M15 8h.01',upload:'M7 18H6a4 4 0 0 1-1-8 7 7 0 0 1 13-2 5 5 0 0 1 0 10h-1M12 19V9m-4 4 4-4 4 4',settings:'m9 3-.5 3-2 1-3-.8L2 10l2.5 2L4 15l2 3 3-.5 2 3 4-1 .5-3 3-1 2-4-2-2V6l-4-.5L13 3ZM9 12a3 3 0 1 0 6 0 3 3 0 1 0-6 0',user:'M8 7a4 4 0 1 0 8 0 4 4 0 1 0-8 0M4 21v-3a8 8 0 0 1 16 0v3',users:'M4 8a3 3 0 1 0 6 0 3 3 0 1 0-6 0M14 8a3 3 0 1 0 6 0 3 3 0 1 0-6 0M1 21v-3a6 6 0 0 1 12 0v3M14 14a6 6 0 0 1 9 4v3',lock:'M5 10h14v11H5ZM8 10V7a4 4 0 0 1 8 0v3M12 14v3',eye:'M2 12s3-7 10-7 10 7 10 7-3 7-10 7S2 12 2 12ZM9 12a3 3 0 1 0 6 0 3 3 0 1 0-6 0',globe:'M2 12h20M12 2a10 10 0 1 0 0 20 10 10 0 1 0 0-20ZM12 2c-6 6-6 14 0 20 6-6 6-14 0-20',help:'M9 9a3 3 0 1 1 5 2c-2 1-2 2-2 3M12 17h.01M12 2a10 10 0 1 0 0 20 10 10 0 1 0 0-20',logout:'M9 3H3v18h6M9 12h13m-5-5 5 5-5 5',calendar:'M4 5h16v16H4ZM4 10h16M8 3v4M16 3v4M8 14h2M14 14h2',pin:'M12 22S4 13 4 9a8 8 0 0 1 16 0c0 4-8 13-8 13ZM9 9a3 3 0 1 0 6 0 3 3 0 1 0-6 0',tag:'M3 3h8l10 10-8 8L3 11ZM7 7h.01',share:'M9 8l6-3M9 15l6 4M3 12a3 3 0 1 0 6 0 3 3 0 1 0-6 0M15 4a3 3 0 1 0 6 0 3 3 0 1 0-6 0M15 20a3 3 0 1 0 6 0 3 3 0 1 0-6 0',edit:'m4 16 12-12 4 4L8 20H4ZM13 7l4 4',camera:'M3 7h4l2-3h6l2 3h4v14H3ZM8 14a4 4 0 1 0 8 0 4 4 0 1 0-8 0',flip:'M3 11a9 9 0 0 1 16-6M19 1v5h-5M21 13a9 9 0 0 1-16 6M5 23v-5h5',clock:'M12 2a10 10 0 1 0 0 20 10 10 0 1 0 0-20ZM12 6v6l4 2',check:'m5 12 4 4L19 6',trash:'M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7M14 10v7',link:'m9 15 6-6M8 16l-2 2a4 4 0 0 1-6-6l5-5a4 4 0 0 1 6 0M16 8l2-2a4 4 0 0 1 6 6l-5 5a4 4 0 0 1-6 0',logo:'M12 2 21 7v10l-9 5-9-5V7ZM12 2l4 7-4 7-8 1M21 7h-9l-4 7 4 8M21 17l-5-8H8L3 7'};
|
||||
@@ -0,0 +1,5 @@
|
||||
Generator: built-in image_gen, imagegen skill. Generated 2026-09-22.
|
||||
|
||||
Use case: ui-mockup. Create a polished high fidelity desktop admin web interface concept screenshot for PhotoLive, enterprise event photo live streaming management. One single full desktop screen, flat front-on 16:10 composition, about 1600x1000; no devices, no perspective, no outside annotations. It is a redesign on Art Design Pro / Element Plus design language, calm mature SaaS, not a marketing dashboard. White 216px sidebar, pale cool #F5F7FA main canvas, navy #172033 typography, blue #1A73E8 actions, thin #E5EAF1 borders, 12px rounded cards, subtle shadows, spacious structured hierarchy. Left sidebar PhotoLive blue aperture mark, small WORKSPACE label then Albums selected, Embed widgets; ORGANIZATION label then Accounts, User sync, Storage & usage; bottom administrator avatar and company workspace. Topbar breadcrumb Workspace / Albums / Canton Fair, language EN and avatar. Main title Canton Fair · Autumn with small Published and Uploads on status pills, right outlined Edit album and primary Share album. Beneath: compact horizontal hero info card, left venue photograph around 320x145, right event date Oct 15–19, 2026, Guangzhou China, owner Alex Morgan, timezone Asia/Shanghai, muted small Stats updated 2 min ago. Under hero: tabs Photos selected blue underline, Members, Categories, Watermark, Sharing. Photo toolbar with All photos, Published, Hidden, category dropdown, photo filename search, grid/list toggle. Actual main photo grid 4 columns by 2 rows of beautiful documentary international trade fair photos: venue, networking business people, robotics, audience, speakers. Each thumbnail has small checkbox top left and a subtle Published status, below images tiny filename and uploader; one image selected with blue border and checkbox. A small blue subdued selection toolbar '1 selected' and 'Hide selected' may appear within photo area, not floating over website. No giant charts, no fake revenue, no ecommerce, no public like button, no comment/upload/social buttons, no technical token as page headline. Text crisp legible English, consistent thin outline icons and clean typography. All images and counts are illustrative. Overall feel premium restrained professional photography management tool, photos dominate, realistic implementable HTML layout.
|
||||
|
||||
Observed variation: the generated image used an illustrative numeric uploads badge instead of the requested Uploads on text. The HTML uses access-state semantics and must take precedence for behavior and data interpretation. Counts and text in the concept image are not production requirements.
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PhotoLive · 相册管理</title><link rel="stylesheet" href="styles.css"></head><body data-page="2"><div id="root"></div><script src="icons.js"></script><script src="pages.js"></script><script src="admin.js"></script></body></html>
|
||||
@@ -0,0 +1,122 @@
|
||||
window.ADMIN_PAGES=[
|
||||
{
|
||||
"id": 1,
|
||||
"file": "01-login.html",
|
||||
"zh": "登录",
|
||||
"en": "Login"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"file": "02-albums.html",
|
||||
"zh": "相册管理",
|
||||
"en": "Albums"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"file": "03-photos.html",
|
||||
"zh": "相册照片",
|
||||
"en": "Album photos"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"file": "04-members.html",
|
||||
"zh": "相册成员",
|
||||
"en": "Members"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"file": "05-categories.html",
|
||||
"zh": "分类管理",
|
||||
"en": "Categories"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"file": "06-watermark.html",
|
||||
"zh": "水印配置",
|
||||
"en": "Watermark"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"file": "07-sharing.html",
|
||||
"zh": "相册分享",
|
||||
"en": "Sharing"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"file": "08-create-album.html",
|
||||
"zh": "新建相册",
|
||||
"en": "New album"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"file": "09-edit-album.html",
|
||||
"zh": "编辑相册",
|
||||
"en": "Edit album"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"file": "10-users.html",
|
||||
"zh": "账号管理",
|
||||
"en": "Accounts"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"file": "11-user-sync.html",
|
||||
"zh": "用户同步",
|
||||
"en": "User sync"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"file": "12-usage.html",
|
||||
"zh": "存储与用量",
|
||||
"en": "Storage & usage"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"file": "13-embed-slots.html",
|
||||
"zh": "网站展示位",
|
||||
"en": "Embed widgets"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"file": "14-embed-editor.html",
|
||||
"zh": "配置展示位",
|
||||
"en": "Widget configuration"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"file": "15-branding.html",
|
||||
"zh": "宣传图与资产",
|
||||
"en": "Album branding"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"file": "16-photo-viewer.html",
|
||||
"zh": "照片大图",
|
||||
"en": "Photo details"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"file": "17-403.html",
|
||||
"zh": "无权限",
|
||||
"en": "Access restricted"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"file": "18-404.html",
|
||||
"zh": "页面不存在",
|
||||
"en": "Page not found"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"file": "19-500.html",
|
||||
"zh": "服务异常",
|
||||
"en": "Server error"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"file": "20-sync-detail.html",
|
||||
"zh": "同步批次详情",
|
||||
"en": "Sync run details"
|
||||
}
|
||||
];
|
||||
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 688 KiB |
|
After Width: | Height: | Size: 955 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 369 KiB |
|
After Width: | Height: | Size: 335 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
After Width: | Height: | Size: 804 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 648 KiB |
|
After Width: | Height: | Size: 598 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 152 KiB |
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"date": "2026-09-22",
|
||||
"scope": "Offline HTML only; production admin untouched",
|
||||
"pages": [
|
||||
{
|
||||
"file": "01-login.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "02-albums.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "03-photos.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "04-members.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "05-categories.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "06-watermark.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "07-sharing.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "08-create-album.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "09-edit-album.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "10-users.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "11-user-sync.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "12-usage.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "13-embed-slots.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "14-embed-editor.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "15-branding.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "16-photo-viewer.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "17-403.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "18-404.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "19-500.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
},
|
||||
{
|
||||
"file": "20-sync-detail.html",
|
||||
"overflow": false,
|
||||
"brokenImages": 0
|
||||
}
|
||||
],
|
||||
"narrow": [
|
||||
{
|
||||
"file": "02-albums.html",
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"file": "03-photos.html",
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"file": "10-users.html",
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"file": "14-embed-editor.html",
|
||||
"overflow": false
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"widget": {
|
||||
"width": 195,
|
||||
"height": 280
|
||||
},
|
||||
"interactions": [
|
||||
"album search",
|
||||
"batch hide",
|
||||
"visibility filter",
|
||||
"account search",
|
||||
"language",
|
||||
"widget collapse",
|
||||
"v1 mode",
|
||||
"watermark preview"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.6 MiB |
@@ -4,8 +4,8 @@ const paths={back:'m15 18-6-6 6-6',next:'m9 18 6-6-6-6',close:'m6 6 12 12M6 18 1
|
||||
function icon(n){return `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="${paths[n]||paths.photo}"/></svg>`}
|
||||
let lang='en',page=1,returnPage=2,albumTitle='Canton Fair · Autumn',editTab='access',uploadFilter='all',photoFilter='all',currentPhoto=1,categoryNames=['Exhibition','People','Highlights'],members=['Emma Chen','Daniel Lee'],extraTasks=0,hiddenPhotos=new Set([6]),removedPhotos=new Set(),retried=false,publicAccess=true,uploadAllowed=true,downloads=true,original=false,draftLanguage='en',toastTimer;
|
||||
const T=(en,zh)=>lang==='zh'?zh:en, esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
const pics=[[262,486,230,128],[845,248,358,435],[264,661,224,122],[506,656,185,142],[265,824,224,126],[504,823,181,129],[260,100,477,193],[848,300,345,378]];
|
||||
function photo(i,cls='',height=''){const r=pics[((i%pics.length)+pics.length)%pics.length],[x,y,w,h]=r;return `<div class="art ${cls}" style="aspect-ratio:${w}/${h};${height?`height:${height};`:''}"><img src="assets/reference.png" alt="" style="width:${1536/w*100}%;height:${1024/h*100}%;left:${-x/w*100}%;top:${-y/h*100}%"></div>`}
|
||||
const pics=[[262,530,230,105],[845,248,358,435],[264,661,224,122],[506,656,185,142],[265,824,224,126],[504,823,181,129],[260,100,477,193],[848,300,345,378]];
|
||||
function photo(i,cls='',height=''){const r=pics[((i%pics.length)+pics.length)%pics.length],[x,y,w,h]=r;return `<div class="art ${cls}" style="aspect-ratio:${w}/${h};${height?`height:${height};`:''}"><svg width="100%" height="100%" viewBox="${x} ${y} ${w} ${h}" preserveAspectRatio="xMidYMid slice" aria-hidden="true" style="display:block"><image href="assets/reference.png" width="1536" height="1024"/></svg></div>`}
|
||||
function button(label,action,cls='primary',ic=''){return `<button class="btn ${cls}" data-action="${action}">${ic?icon(ic):''}${label}</button>`}
|
||||
function row(ic,label,sub,action='',trailing=''){return `<${action?'button':'div'} class="row" ${action?`data-action="${action}"`:''}>${icon(ic)}<span class="grow"><strong>${label}</strong>${sub?`<p>${sub}</p>`:''}</span>${trailing?`<span class="trailing">${trailing}</span>`:''}${action?`<span class="arrow">${icon('next')}</span>`:''}</${action?'button':'div'}>`}
|
||||
function bar(title,back=5,right='',root=false){return `<div class="safe-top"></div><header class="bar ${root?'root':''} ${[13,18,20].includes(page)?'dark':''}">${root?`<h1>${title}</h1>`:`<button class="back" data-go="${back}" aria-label="${T('Back','返回')}">${icon('back')}</button><span class="bar-title">${title}</span>`}${right}</header>`}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const fs=require('fs'),path=require('path');
|
||||
const {chromium}=require(require.resolve('@playwright/test',{paths:[path.resolve(__dirname,'../../../apps/viewer')]}));
|
||||
(async()=>{const browser=await chromium.launch({headless:true});const page=await browser.newPage({viewport:{width:390,height:844}});const errors=[];page.on('pageerror',e=>errors.push(e.message));const files=fs.readdirSync(__dirname).filter(x=>/^\d\d-.*\.html$/.test(x)).sort();fs.mkdirSync(path.join(__dirname,'screenshots'),{recursive:true});const rows=[];for(const file of files){await page.goto('file:///'+path.join(__dirname,file).replaceAll('\\','/'));await page.locator('#app').waitFor();await page.evaluate(()=>Promise.all([...document.images].map(i=>i.decode().catch(()=>{}))));await page.screenshot({path:path.join(__dirname,'screenshots',file.slice(0,2)+'.png')});rows.push({file,overflow:await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth),buttons:await page.locator('button').count()})}
|
||||
await page.goto('file:///'+path.join(__dirname,'01-login.html').replaceAll('\\','/'));await page.locator('#loginForm button[type=submit]').click();await page.waitForURL('**#05');await page.locator('.album-card').first().click();await page.waitForURL('**#06');await page.locator('[data-action="go:13"]').click();await page.waitForURL('**#13');await page.locator('.shutter').click();if(!(await page.locator('#cameraNote').textContent()).includes('1'))throw Error('Capture mock queue failed');await page.evaluate(()=>location.hash='20');await page.locator('[data-action="confirmHide"]').click();await page.waitForURL('**#18');await page.waitForFunction(()=>document.querySelector('.photo-meta')?.textContent.includes('Hidden'));await page.locator('[data-action="publish"]').click();await page.evaluate(()=>location.hash='04');await page.locator('input[value="zh"]').check();await page.locator('[data-action="saveLanguage"]').click();await page.waitForURL('**#02');await page.waitForFunction(()=>document.querySelector('h1')?.textContent.includes('设置'));
|
||||
const narrow=[];await page.setViewportSize({width:320,height:568});for(const id of [1,9,14,18,19]){await page.goto('file:///'+path.join(__dirname,files[id-1]).replaceAll('\\','/'));narrow.push({id,overflow:await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth)})}const result={date:'2026-09-22',type:'HTML prototype only',pages:rows,narrow,errors,interactions:['login','album navigation','simulated capture','hide','publish','language'],nativeAppTested:false};fs.writeFileSync(path.join(__dirname,'validation.json'),JSON.stringify(result,null,2));console.log(JSON.stringify(result));await browser.close();if(errors.length||rows.some(x=>x.overflow)||narrow.some(x=>x.overflow))process.exitCode=1})().catch(e=>{console.error(e);process.exit(1)});
|
||||
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 302 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 295 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 440 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 191 KiB |
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"date": "2026-09-22",
|
||||
"type": "HTML prototype only",
|
||||
"pages": [
|
||||
{
|
||||
"file": "01-login.html",
|
||||
"overflow": false,
|
||||
"buttons": 3
|
||||
},
|
||||
{
|
||||
"file": "02-settings.html",
|
||||
"overflow": false,
|
||||
"buttons": 7
|
||||
},
|
||||
{
|
||||
"file": "03-password.html",
|
||||
"overflow": false,
|
||||
"buttons": 5
|
||||
},
|
||||
{
|
||||
"file": "04-language.html",
|
||||
"overflow": false,
|
||||
"buttons": 8
|
||||
},
|
||||
{
|
||||
"file": "05-albums.html",
|
||||
"overflow": false,
|
||||
"buttons": 7
|
||||
},
|
||||
{
|
||||
"file": "06-album-detail.html",
|
||||
"overflow": false,
|
||||
"buttons": 18
|
||||
},
|
||||
{
|
||||
"file": "07-album-actions.html",
|
||||
"overflow": false,
|
||||
"buttons": 23
|
||||
},
|
||||
{
|
||||
"file": "08-empty-album.html",
|
||||
"overflow": false,
|
||||
"buttons": 4
|
||||
},
|
||||
{
|
||||
"file": "09-create-album.html",
|
||||
"overflow": false,
|
||||
"buttons": 2
|
||||
},
|
||||
{
|
||||
"file": "10-edit-album.html",
|
||||
"overflow": false,
|
||||
"buttons": 5
|
||||
},
|
||||
{
|
||||
"file": "11-members.html",
|
||||
"overflow": false,
|
||||
"buttons": 5
|
||||
},
|
||||
{
|
||||
"file": "12-categories.html",
|
||||
"overflow": false,
|
||||
"buttons": 9
|
||||
},
|
||||
{
|
||||
"file": "13-camera.html",
|
||||
"overflow": false,
|
||||
"buttons": 6
|
||||
},
|
||||
{
|
||||
"file": "14-uploads.html",
|
||||
"overflow": false,
|
||||
"buttons": 7
|
||||
},
|
||||
{
|
||||
"file": "15-upload-errors.html",
|
||||
"overflow": false,
|
||||
"buttons": 10
|
||||
},
|
||||
{
|
||||
"file": "16-upload-history.html",
|
||||
"overflow": false,
|
||||
"buttons": 1
|
||||
},
|
||||
{
|
||||
"file": "17-photos.html",
|
||||
"overflow": false,
|
||||
"buttons": 22
|
||||
},
|
||||
{
|
||||
"file": "18-photo-detail.html",
|
||||
"overflow": false,
|
||||
"buttons": 6
|
||||
},
|
||||
{
|
||||
"file": "19-share.html",
|
||||
"overflow": false,
|
||||
"buttons": 3
|
||||
},
|
||||
{
|
||||
"file": "20-hide-photo.html",
|
||||
"overflow": false,
|
||||
"buttons": 8
|
||||
}
|
||||
],
|
||||
"narrow": [
|
||||
{
|
||||
"id": 1,
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"overflow": false
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"interactions": [
|
||||
"login",
|
||||
"album navigation",
|
||||
"simulated capture",
|
||||
"hide",
|
||||
"publish",
|
||||
"language"
|
||||
],
|
||||
"nativeAppTested": false
|
||||
}
|
||||