Files
photo-live/docs/designs/app-preview-2026-09-22/app.js
T
浅念 37d88da4bc 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>
2026-09-22 22:42:00 +08:00

45 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Offline design prototype. No account, camera, upload, or production API calls. */
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
const paths={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'};
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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
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>`}
function nav(selected){return `<nav class="nav" aria-label="${T('Main navigation','主导航')}">${[[5,'album',T('Albums','相册')],[14,'upload',T('Uploads','上传')],[2,'settings',T('Settings','设置')]].map(([id,ic,label])=>`<button data-go="${id}" class="${selected===id?'active':''}">${icon(ic)}${label}${id===14?`<span class="badge">${3+extraTasks}</span>`:''}</button>`).join('')}</nav>`}
function field(label,id,type='text',value='',placeholder=''){const pw=type==='password';return `<label class="field">${label}<span class="${pw?'input-wrap':''}">${pw?icon('lock'):''}<input id="${id}" name="${id}" type="${type}" value="${esc(value)}" placeholder="${esc(placeholder)}" ${type==='password'?'autocomplete="off"':''}>${pw?`<button type="button" data-eye="${id}" aria-label="${T('Show password','显示密码')}">${icon('eye')}</button>`:''}</span></label>`}
function seg(items,selected,action){return `<div class="tabs">${items.map(([id,label])=>`<button class="${id===selected?'active':''}" data-action="${action}:${id}">${label}</button>`).join('')}</div>`}
function status(){return `<div class="status-row"><span class="tag">${icon('globe')}${publicAccess?T('Public','公开'):T('Private','未公开')}</span><span class="tag">${icon('upload')}${uploadAllowed?T('Uploads on','可上传'):T('Uploads off','上传关闭')}</span></div>`}
function grid(count=9,offset=0){return `<div class="photo-grid">${Array.from({length:count},(_,k)=>k+offset).filter(i=>!removedPhotos.has(i)&&(photoFilter!=='hidden'||hiddenPhotos.has(i))&&(photoFilter!=='published'||!hiddenPhotos.has(i))).map(i=>`<button data-action="view:${i}" aria-label="${T('View photo','查看照片')} ${i+1}">${photo(i,hiddenPhotos.has(i)?'is-hidden':'')}${hiddenPhotos.has(i)?`<span class="flag">${icon('eye')}${T('Hidden','已隐藏')}</span>`:i===2?`<span class="flag">${T('Processing','处理中')}</span>`:''}</button>`).join('')}</div>`}
function login(){return `<div class="safe-top"></div><div class="scroll flush white"><div class="login-top"><button data-action="language">${lang==='en'?'EN':'中文'}⌄</button></div><div class="mosaic">${[2,4,0,3,6,1].map(i=>photo(i)).join('')}</div><div class="login-content"><div class="wordmark">${icon('logo')}PhotoLive</div><h1>${T('Your event. In focus.','让每个现场,都被看见。')}</h1><p class="muted">${T('Sign in to your company workspace','登录您的企业工作空间')}</p><form id="loginForm" class="login-form"><label class="field"><span class="input-wrap">${icon('user')}<input id="account" autocomplete="off" placeholder="${T('Account','账号')}" value="demo.staff"></span></label>${field('', 'password','password','','••••••••')}<button class="btn primary full" type="submit">${T('Sign in','登录')}</button><div class="error" id="formError"></div></form><p class="login-note">${T('Accounts are provided by your administrator.','账号由企业管理员提供。')}</p><p class="demo-label">${T('Demo mode · do not enter real credentials','演示模式 · 请勿输入真实账号密码')}</p></div></div>`}
function settings(){return bar(T('Settings','设置'),5,'',true)+`<div class="scroll stack"><div class="card"><div style="display:flex;gap:16px;align-items:center"><div class="avatar lg">AM</div><div><h3>Alex Morgan</h3><p class="note">alex.morgan · ${T('Staff','工作人员')}</p></div></div></div><div class="group">${row('globe',T('Language','语言'),'', 'language',lang==='en'?'English':'中文')}${row('lock',T('Change password','修改密码'),'', 'go:3')}${row('help',T('Upload help','上传帮助'),'', 'help')}${row('help',T('App version','应用版本'),'', '', 'Design preview')}</div><div class="group red">${row('logout',T('Sign out','退出登录'),'', 'logout')}</div><p class="demo-label">${T('Sample staff profile · offline prototype','示例工作人员 · 离线交互原型')}</p></div>`+nav(2)}
function password(){return bar(T('Change password','修改密码'),2)+`<div class="scroll"><div class="hint-card" style="margin-bottom:24px">${icon('lock')}<div><strong>${T('Keep your account secure','保障您的账号安全')}</strong><p class="note">${T('Update the password for your company account.','修改用于企业账号登录的密码。')}</p></div></div><form id="passwordForm">${field(T('Current password','当前密码'),'oldPassword','password','',T('Enter current password','输入当前密码'))}${field(T('New password','新密码'),'newPassword','password','',T('Enter new password','输入新密码'))}${field(T('Confirm password','确认密码'),'confirmPassword','password','',T('Enter new password again','再次输入新密码'))}<div id="formError" class="error"></div><button class="btn primary full" type="submit">${T('Save password','保存密码')}</button></form><div class="hint-card" style="margin-top:24px">${icon('help')}<p>${T('Preview only. No password is saved or sent. Production rules are enforced by the account service.','仅演示校验,不保存或发送密码。正式密码规则由账号服务执行。')}</p></div></div>`}
function albums(){return bar('PhotoLive',5,`<button class="plus" data-go="9" aria-label="${T('New album','新建相册')}">${icon('plus')}</button>`,true)+`<div class="scroll"><h2>${T('Albums','相册')}</h2><p class="note" style="margin:3px 0 20px">${T('Organize and manage event photos','组织活动相册,管理现场影像')}</p>${[[0,albumTitle,'Oct 15–19 · Guangzhou',6],[5,T('Product Launch','新品发布会'),'Sep 3 · Shenzhen',6],[2,T('Team Stories','团队故事'),'Aug 12–15 · Shanghai',8]].map(([i,title,date,link])=>`<button class="album-card" data-go="${link}">${photo(i)}<span class="scrim"></span><div class="caption"><h3>${esc(title)}</h3><p>${date}</p><div class="caption-tags"><span>${icon('globe')}${T('Public','公开')}</span><span>${icon('upload')}${T('Uploads on','可上传')}</span></div></div></button>`).join('')}</div>`+nav(5)}
function detail(empty=false){const title=empty?T('Team Stories','团队故事'):albumTitle;return bar('',5,`<button class="right" data-go="7" aria-label="${T('Album actions','相册操作')}">${icon('more')}</button>`)+`<div class="scroll flush white">${photo(empty?2:6,'album-hero')}<div class="album-info"><h2>${esc(title)}</h2><div class="metadata">${icon('calendar')}${empty?'Aug 12–15, 2026':'Oct 15–19, 2026'}</div><div class="metadata">${icon('pin')}${empty?'Shanghai, China':'Guangzhou, China'}</div>${status()}</div>${empty?`<div class="empty"><div class="empty-illustration">${icon('photo')}</div><h3>${T('Ready for your first photo','准备好记录第一张照片')}</h3><p>${T('Capture a moment or choose photos to begin.','拍摄精彩瞬间,或从手机相册选择照片。')}</p></div>`:`<div class="quick">${[[17,'photo',T('Photos','照片')],[11,'users',T('Members','成员')],[12,'tag',T('Categories','分类')],[19,'share',T('Share','分享')]].map(([id,ic,label])=>`<button data-go="${id}">${icon(ic)}${label}</button>`).join('')}</div><div class="recent"><div class="heading-row"><h3>${T('Recent photos','最近照片')}</h3><button data-go="17">${T('See all','查看全部')}</button></div>${grid(9)}</div>`}</div><div class="actions">${button(T('Take photos','拍摄照片'),uploadAllowed?'go:13':'uploadOff','primary','camera')}${button(T('Choose photos','选择照片'),uploadAllowed?'choose':'uploadOff','secondary','photo')}</div>`}
function albumForm(edit=false){const details=`<div class="card">${field(T('Album title','相册名称'),'albumTitle','text',edit?albumTitle:T('Canton Fair · Autumn','广交会 · 秋季'))}</div><div class="card">${field(T('Starts','开始日期'),'startDate','date','2026-10-15')}${field(T('Ends','结束日期'),'endDate','date','2026-10-19')}<label class="field">${T('Time zone','时区')}<select id="timezone"><option>Asia/Shanghai</option><option>Europe/London</option><option>America/New_York</option></select></label>${field(T('Venue','地点'),'venue','text','Guangzhou')}${field(T('Address','详细地址'),'address','text',T('China Import and Export Fair Complex','中国进出口商品交易会展馆'))}</div><div class="card"><h3 style="margin-bottom:16px">${T('Description & languages','说明与语言')}</h3><label class="field">${T('Default language','默认语言')}<select id="contentLanguage"><option>English</option><option>中文</option></select></label><label class="field">${T('Description','活动说明')}<textarea id="description">${T('Event photos from the Canton Fair Autumn session.','广交会秋季展会现场照片。')}</textarea></label></div>`;const access=`<div class="group">${[['uploads',T('Allow uploads','允许上传'),T('Team members can upload photos to this album.','团队成员可以向本相册上传照片。'),uploadAllowed],['public',T('Public access','公开访问'),T('Anyone with the link can view this album.','获得链接的观众可以浏览相册。'),publicAccess],['downloads',T('Allow downloads','允许下载'),T('Viewers can download photos from this album.','观众可以下载相册照片。'),downloads],['original',T('Original downloads','原图下载'),T('Allow downloading full-resolution files.','允许观众下载完整分辨率的原图。'),original]].map(([id,title,desc,on])=>`<label class="row access-row"><span class="grow"><strong>${title}</strong><p>${desc}</p></span><input class="switch" id="access-${id}" type="checkbox" ${on?'checked':''} ${id==='original'&&!downloads?'disabled':''} aria-label="${title}"></label>`).join('')}</div><div class="group">${row('globe',T('Content language','内容语言'),T('Default language for album content','相册内容的默认语言'),'contentLanguage','English')}</div>`;return bar(edit?T('Edit album','编辑相册'):T('New album','新建相册'),6)+`<div class="scroll"><form id="albumForm">${edit?seg([['details',T('Details','详情')],['access',T('Access','权限')]],editTab,'editTab'):''}<div class="stack" id="detailsFields" ${edit&&editTab!=='details'?'hidden':''}>${details}</div>${edit?`<div class="stack" id="accessFields" ${editTab!=='access'?'hidden':''}>${access}</div>`:''}<div class="error" id="formError"></div></form></div><div class="actions"><button class="btn primary" type="submit" form="albumForm">${edit?T('Save changes','保存修改'):T('Create album','创建相册')}</button></div>`}
function memberPage(){return bar(T('Members','成员'),6,`<button class="right blue" data-action="invite" aria-label="${T('Invite colleague','邀请同事')}">${icon('plus')}</button>`)+`<div class="scroll"><h3 class="section-label">${T('Owner','创建者')}</h3><div class="group"><div class="row"><div class="avatar">AM</div><div class="grow"><strong>Alex Morgan</strong><p>alex.morgan</p></div><span class="tag">${T('Owner','创建者')}</span></div></div><h3 class="section-label" style="margin-top:26px">${T('Collaborators','协作者')}</h3><div class="group">${members.map((name,i)=>`<div class="row"><div class="avatar ${i%2?'lilac':''}">${name.split(' ').map(n=>n[0]).join('')}</div><div class="grow"><strong>${name}</strong><p>${name.toLowerCase().replace(' ','.')}</p><p>${T('Can manage their own photos','可管理自己上传的照片')}</p></div><button data-action="removeMember:${i}" aria-label="${T('Remove member','移除成员')}">${icon('more')}</button></div>`).join('')}</div><div class="hint-card" style="margin-top:20px">${icon('help')}${T('Album owners manage all photos and members.','相册创建者管理全部照片和成员。')}</div></div><div class="actions">${button(T('Invite colleague','邀请同事'),'invite','secondary','users')}</div>`}
function categories(){return bar(T('Categories','分类'),6,`<button class="right blue" data-action="category:new" aria-label="${T('New category','新建分类')}">${icon('plus')}</button>`)+`<div class="scroll stack">${categoryNames.map((name,i)=>`<div class="card" style="display:flex;align-items:center;gap:14px"><div class="avatar" style="border-radius:12px">${icon('tag')}</div><strong style="flex:1">${esc(name)}</strong><button data-action="category:${i}" aria-label="${T('Edit category','编辑分类')}">${icon('edit')}</button></div>`).join('')}<p class="note">${T('Categories help your team organize event photos.','通过分类整理团队的活动照片。')}</p></div>`}
function camera(){return bar(`<span style="font-size:13px">${esc(albumTitle)}</span><br><button style="min-height:28px;padding:0;font-size:11px;color:#aab9cf" data-action="cameraCategory">${T('Exhibition','展会现场')} ⌄</button>`,6,`<button class="right" data-go="14" aria-label="${T('Upload queue','上传队列')}">${icon('upload')}</button>`)+`<div class="camera-view">${photo(1)}<div class="camera-note" id="cameraNote">${T('Camera preview · sample photo','相机效果演示 · 示例照片')}</div></div><div class="camera-chrome"><button class="camera-thumb" data-go="17" aria-label="${T('Recent photos','最近照片')}">${photo(0)}</button><button class="shutter" data-action="shutter" aria-label="${T('Simulate taking a photo','模拟拍照')}"></button><button data-action="flip" aria-label="${T('Switch camera preview','切换相机预览')}">${icon('flip')}</button></div><p class="demo-label">${T('No device camera or OSS connection','未调用设备相机,未连接 OSS')}</p>`}
function uploadItem(i,type='upload'){let name='IMG_'+String(85+i).padStart(3,'0')+'.jpg';const attention=type==='attention',history=type==='history';let state=attention?(i===0?T('Connection interrupted','连接中断'):T('Source file unavailable','源文件不可用')):history?[T('Uploaded','已上传'),T('Processing','处理中'),T('Ready','已就绪')][i%3]:[T('Uploading 68%','上传中 68%'),T('Waiting for network','等待网络'),T('Confirming upload','确认上传中'),T('Uploaded','已上传')][i%4];return `<article class="upload-row ${history?'history-row':''}">${photo(i+2,'upload-thumb')}<div class="upload-data"><strong>${name}</strong><p>${history?'Today 14:'+String(32-i*3):esc(albumTitle)}</p>${!history?`<p>${T('Exhibition','展会现场')}</p>`:''}<div class="upload-status ${attention?'red':history?(i%3===1?'blue':'green'):['blue','amber','blue','green'][i%4]}">${!attention&&i===0&&!history?'<div class="progress"><span></span></div>':icon(attention?'help':i%3===0?'check':'clock')}${state}</div>${attention?`<p style="margin-top:7px">${i===0?T('The network was lost during upload. You can retry this file.','上传时网络断开,可以重试此任务。'):T('The original file cannot be found on this device. Please select it again.','本机找不到源文件,请重新选择。')}</p>${button(i===0?T('Retry','重试'):T('Select again','重新选择'),i===0?'retry':'reselect','secondary')}`:history?`<p style="margin-top:7px">${i%2?T('Other device','其他设备'):T('This device','本机')}</p>`:''}</div></article>`}
function uploads(attention=false){const active=attention?'attention':uploadFilter;return bar(T('Uploads','上传'),5,`<button data-go="16" style="display:flex;align-items:center;gap:5px;font-size:12px">${icon('clock')}${T('History','历史')}</button>`,true)+`<div class="scroll"><div class="summary"><p class="muted">${T('Today · This device','今天 · 本机')}</p><div class="big">82 ${T('uploaded','已上传')}</div><p class="counts">${3+extraTasks} ${T('uploading','上传中')} · 13 ${T('waiting','等待中')} · ${retried?1:2} ${T('need attention','需处理')}</p></div>${seg([['all',T('All','全部')],['active',T('Active','进行中')],['attention',T('Needs attention','需处理')]],active,'uploadFilter')}${active==='attention'?`${retried?'':uploadItem(0,'attention')}${uploadItem(1,'attention')}${button(T('Retry eligible tasks','重试可恢复任务'),'retry','primary full')}`:Array.from({length:active==='active'?3:4},(_,i)=>uploadItem(i)).join('')}${extraTasks?`<div class="hint-card">${icon('camera')}${T('Simulated photos queued: ','模拟拍摄入队:')}${extraTasks}</div>`:''}</div>`+nav(14)}
function historyPage(){return bar(T('Upload history','上传历史'),14)+`<div class="scroll"><p class="muted" style="margin-bottom:16px">${T('Server records · All devices','服务端记录 · 所有设备')}</p><div style="display:flex;gap:10px;margin-bottom:10px"><label class="field" style="flex:2"><select id="historyAlbum" aria-label="Album"><option>${esc(albumTitle)}</option><option>Product Launch</option></select></label><label class="field" style="flex:1"><select id="historyDate" aria-label="Date"><option>${T('Today','今天')}</option><option>${T('Yesterday','昨天')}</option></select></label></div><div id="historyRows">${Array.from({length:5},(_,i)=>uploadItem(i,'history')).join('')}</div><p class="demo-label">${T('Sample server records · not combined with the local queue','示例服务端记录 · 不与本机队列合并计数')}</p></div>`}
function managePhotos(){return bar(T('Photos','照片'),6)+`<div class="scroll white"><p class="note" style="margin:-8px 0 16px">${esc(albumTitle)}</p>${seg([['all',T('All','全部')],['published',T('Published','已公开')],['hidden',T('Hidden','已隐藏')]],photoFilter,'photoFilter')}${grid(18)}</div>`}
function photoDetails(){const hidden=hiddenPhotos.has(currentPhoto);return bar('',17,`<button class="right" data-action="photoMore" aria-label="${T('More actions','更多操作')}">${icon('more')}</button>`)+`<div class="photo-stage" id="photoStage">${photo(currentPhoto,'large-photo')}<button class="arrow prev" data-action="photoPrev" aria-label="${T('Previous photo','上一张')}">${icon('back')}</button><button class="arrow next" data-action="photoNext" aria-label="${T('Next photo','下一张')}">${icon('next')}</button></div><div class="photo-meta"><div class="topline"><strong>Emma Chen</strong><span class="tag ${hidden?'':'success'}">${hidden?T('Hidden','已隐藏'):T('Published','已公开')}</span></div><p>Oct 15, 2026 · 10:42</p><p>2400 × 1600 · 2.8 MB</p></div><div class="actions">${button(hidden?T('Publish again','重新公开'):T('Hide photo','隐藏照片'),hidden?'publish':'go:20','secondary','eye')}${button(T('Delete','删除'),'delete','danger','trash')}</div>`}
function sharePage(){return bar(T('Share album','分享相册'),6)+`<div class="scroll"><div class="card share-card">${photo(6,'cover')}<div class="share-info"><h2>${esc(albumTitle)}</h2><div class="metadata">${icon('pin')}Guangzhou, China</div><div class="metadata">${icon('calendar')}Oct 15–19, 2026</div></div><div class="qr-area"><img src="assets/demo-qr.svg" alt="Demo QR code"><p>${T('Illustrative QR · opens example.com','演示二维码 · 指向 example.com')}</p></div>${button(T('Copy preview link','复制预览链接'),'copy','primary full','link')}<button class="btn full" data-action="openViewer">${T('Open viewer preview','打开观众页预览')} ↗</button><p class="share-note">${publicAccess?T('In production, anyone with the public link can view.','正式接入后,获得公开链接的观众可浏览。'):T('Public access is off. Viewers cannot open this album.','相册尚未公开,观众暂时无法浏览。')}</p></div></div>`}
function languageSheet(){return `<h2>${T('App language','应用语言')}</h2>${[['auto','Follow device','跟随系统'],['en','English','English'],['zh','中文','中文']].map(([value,en,zh])=>`<label class="row"><input type="radio" name="language" value="${value}" ${draftLanguage===value?'checked':''}><span class="grow"><strong>${T(en,zh)}</strong><p>${value==='auto'?T('Use your device’s system language','使用设备系统语言'):value==='en'?'English':'中文'}</p></span></label>`).join('')}${button(T('Done','完成'),'saveLanguage')}`}
function categorySheet(index='new'){const edit=index!=='new';return `<button class="sheet-close" data-action="closeSheet" aria-label="Close">${icon('close')}</button><h2>${edit?T('Edit category','编辑分类'):T('New category','新建分类')}</h2>${seg([['en','English'],['zh','中文']],'en','categoryLang')}<form id="categoryForm" data-index="${index}">${field(T('Category name','分类名称'),'categoryName','text',edit?categoryNames[Number(index)]:T('Behind the scenes','幕后花絮'))}<div class="error" id="categoryError"></div><button class="btn primary full" type="submit">${T('Save category','保存分类')}</button></form>`}
function hideSheet(){return `<h2>${T('Hide this photo?','隐藏这张照片?')}</h2><div class="preview-row">${photo(currentPhoto)}<p>${T('Viewers will no longer see this photo. Authorized staff can publish it again.','观众将无法继续看到这张照片,有权限的工作人员可以重新公开。')}</p></div>${button(T('Hide photo','隐藏照片'),'confirmHide')}${button(T('Cancel','取消'),'closeSheet','ghost cancel')}`}
function overlay(content,close='closeSheet'){const layer=document.createElement('div');layer.className='modal-layer';layer.innerHTML=`<section class="sheet" role="dialog" aria-modal="true" aria-label="${T('Action panel','操作面板')}"><div class="handle-line"></div>${content}</section>`;layer.onclick=e=>{if(e.target===layer)action(close)};$('#app').append(layer);[...$('#app').children].filter(el=>el!==layer).forEach(el=>el.inert=true);layer.querySelector('input,button')?.focus({preventScroll:true});bindForms();return layer}
function closeSheet(){const layer=$('.modal-layer');if(layer)layer.remove();[...$('#app').children].forEach(el=>el.inert=false);if([4,7,20].includes(page))go(page===4?returnPage:page===7?6:18);else $('.bar button')?.focus({preventScroll:true})}
function render(){page=Number(location.hash.slice(1))||Number(document.body.dataset.page)||1;page=Math.max(1,Math.min(page,20));$('#app').className='app'+([13,18,20].includes(page)?' dark':'');document.documentElement.lang=lang==='zh'?'zh-CN':'en';let html='';switch(page){case 1:html=login();break;case 2:case 4:html=settings();break;case 3:html=password();break;case 5:html=albums();break;case 6:case 7:html=detail();break;case 8:html=detail(true);break;case 9:html=albumForm();break;case 10:html=albumForm(true);break;case 11:html=memberPage();break;case 12:html=categories();break;case 13:html=camera();break;case 14:html=uploads();break;case 15:html=uploads(true);break;case 16:html=historyPage();break;case 17:html=managePhotos();break;case 18:case 20:html=photoDetails();break;case 19:html=sharePage();break}$('#app').innerHTML=html;bindForms();if(page===4){draftLanguage=lang;overlay(languageSheet())}if(page===7)overlay(`<h2>${T('Album actions','相册操作')}</h2>${row('edit',T('Edit album','编辑相册'),'','go:10')}${row('users',T('Manage members','管理成员'),'','go:11')}${row('tag',T('Categories','分类'),'','go:12')}${row('share',T('Share album','分享相册'),'','go:19')}${button(T('Cancel','取消'),'closeSheet','ghost')}`);if(page===12)overlay(categorySheet());if(page===20)overlay(hideSheet());fitPhoto();document.title=`PhotoLive · ${String(page).padStart(2,'0')} · App preview`;if(window.parent!==window)window.parent.postMessage({type:'photolive-preview-page',page},'*')}
function go(id){if(Number(id)===page){render();return}location.hash=String(id).padStart(2,'0')}
function toast(text){$('.toast')?.remove();const el=document.createElement('div');el.className='toast';el.setAttribute('role','status');el.textContent=text;$('#app').append(el);clearTimeout(toastTimer);toastTimer=setTimeout(()=>el.remove(),2800)}
function fitPhoto(){const stage=$('#photoStage');if(!stage)return;const r=pics[currentPhoto%pics.length],w=Math.min(stage.clientWidth,stage.clientHeight*r[2]/r[3]);$('.large-photo').style.width=w+'px';$('.large-photo').style.height=w*r[3]/r[2]+'px'}
function invite(){overlay(`<button class="sheet-close" data-action="closeSheet" aria-label="Close">${icon('close')}</button><h2>${T('Invite colleague','邀请同事')}</h2>${field(T('Search company accounts','搜索企业账号'),'searchMember','search','','Search')}<div id="candidates">${['Olivia Wang','James Wilson','Sophia Li'].map(name=>`<button class="row candidate" data-action="inviteName:${name}"><div class="avatar mint">${name.split(' ').map(x=>x[0]).join('')}</div><span class="grow"><strong>${name}</strong><p>${name.toLowerCase().replace(' ','.')}</p></span>${icon('plus')}</button>`).join('')}</div><p class="note">${T('Demo internal directory · no invitation is sent','示例企业通讯录 · 不发送真实邀请')}</p>`);$('#searchMember').oninput=e=>$$('.candidate').forEach(el=>el.hidden=!el.textContent.toLowerCase().includes(e.target.value.toLowerCase()))}
function action(raw){const [name,...rest]=raw.split(':'),value=rest.join(':');switch(name){case 'go':go(value);break;case 'language':returnPage=page;draftLanguage=lang;go(4);break;case 'saveLanguage':{const selected=$('input[name=language]:checked').value;lang=selected==='auto'?(navigator.language.startsWith('zh')?'zh':'en'):selected;go(returnPage);break}case 'closeSheet':closeSheet();break;case 'logout':overlay(`<h2>${T('Sign out of the preview?','退出预览账号?')}</h2><p class="note">${T('Production uploads remain tied to your account. This prototype contains sample tasks only.','正式版任务按账号保留,此原型中仅有示例任务。')}</p>${button(T('Sign out','退出'),'go:1','danger')}${button(T('Cancel','取消'),'closeSheet','ghost')}`);break;case 'help':overlay(`<button class="sheet-close" data-action="closeSheet" aria-label="Close">${icon('close')}</button><h2>${T('Upload help','上传帮助')}</h2><div class="help-copy"><p>${T('Keep the app connected to the network. Interrupted uploads can be retried from Uploads.','请保持网络连接。中断的上传可从上传中心重试。')}</p><p>${T('Background and lock-screen uploads depend on the operating system, permissions and power settings. Reopen the app after a force quit to resume pending work.','后台和锁屏上传受到操作系统、权限和省电设置影响。强制退出后请重新打开应用恢复任务。')}</p><p>${T('Uploaded does not mean publicly available; server processing and album access still apply.','已上传不代表已公开,仍需完成服务端处理并遵守相册权限。')}</p></div>${button(T('Got it','知道了'),'closeSheet')}`);break;case 'uploadOff':toast(T('Uploads are disabled for this album.','该相册已关闭上传。'));break;case 'choose':case 'reselect':overlay(`<h2>${T('Choose sample photos','选择示例照片')}</h2><p class="note">${T('This demonstrates the photo selection flow without accessing your device library.','这里演示选图流程,不访问您的设备相册。')}</p><div class="photo-grid" style="margin-top:16px">${[0,1,2].map(i=>`<button data-action="selectSample:${i}" aria-label="Select sample">${photo(i)}</button>`).join('')}</div>${button(T('Cancel','取消'),'closeSheet','ghost')}`);break;case 'selectSample':extraTasks++;closeSheet();toast(T('Sample photo added to the demo upload queue.','示例照片已加入演示上传队列。'));break;case 'editTab':{editTab=value;$('#detailsFields').hidden=value!=='details';$('#accessFields').hidden=value!=='access';$$('[data-action^="editTab:"]').forEach(el=>el.classList.toggle('active',el.dataset.action.endsWith(value)));break}case 'contentLanguage':overlay(`<h2>${T('Content language','内容语言')}</h2>${button('English','setContent:English','ghost')}${button('中文','setContent:中文','ghost')}`);break;case 'setContent':$('#contentLanguage').value=value;closeSheet();toast(T('Content language draft updated.','内容语言草稿已更新。'));break;case 'invite':invite();break;case 'inviteName':if(!members.includes(value))members.push(value);closeSheet();render();toast(T('Added to the sample team.','已加入示例协作团队。'));break;case 'removeMember':overlay(`<h2>${T('Remove collaborator?','移除协作者?')}</h2><p>${esc(members[Number(value)])}</p>${button(T('Remove from sample team','从示例团队移除'),'confirmRemove:'+value,'danger')}${button(T('Cancel','取消'),'closeSheet','ghost')}`);break;case 'confirmRemove':members.splice(Number(value),1);render();break;case 'category':overlay(categorySheet(value));break;case 'categoryLang':{const field=$('#categoryName');field.dataset[$('.tabs button.active')?.dataset.action.endsWith('zh')?'zh':'en']=field.value;field.value=field.dataset[value]||'';$$('[data-action^="categoryLang:"]').forEach(el=>el.classList.toggle('active',el.dataset.action.endsWith(value)));break}case 'cameraCategory':overlay(`<h2>${T('Select category','选择分类')}</h2>${categoryNames.map(n=>button(esc(n),'chooseCategory:'+n,'ghost')).join('')}`);break;case 'chooseCategory':closeSheet();toast(T('Selected: ','已选择:')+value);break;case 'shutter':extraTasks++;$('#cameraNote').textContent=T('Added to demo upload queue · ','已加入演示上传队列 · ')+extraTasks;break;case 'flip':$('.camera-view>.art').style.transform=$('.camera-view>.art').style.transform?'':'scaleX(-1)';toast(T('Simulated camera direction switch','已模拟切换相机方向'));break;case 'uploadFilter':uploadFilter=value;if(value==='attention')go(15);else if(page===15)go(14);else render();break;case 'retry':retried=true;render();toast(T('Eligible sample task queued again.','可恢复的示例任务已重新排队。'));break;case 'photoFilter':photoFilter=value;render();break;case 'view':currentPhoto=Number(value);go(18);break;case 'photoPrev':case 'photoNext':{let n=currentPhoto;do{n=(n+(name==='photoNext'?1:17))%18}while(removedPhotos.has(n)&&removedPhotos.size<18);currentPhoto=n;render();break}case 'photoMore':overlay(`<h2>${T('Photo actions','照片操作')}</h2>${button(T('View metadata','查看照片信息'),'metadata','ghost')}${button(T('Delete photo','删除照片'),'delete','danger')}${button(T('Cancel','取消'),'closeSheet','ghost')}`);break;case 'metadata':closeSheet();toast(T('Sample · Emma Chen · 2400 × 1600 · 2.8 MB','示例 · Emma Chen · 2400 × 1600 · 2.8 MB'));break;case 'confirmHide':hiddenPhotos.add(currentPhoto);go(18);break;case 'publish':hiddenPhotos.delete(currentPhoto);render();toast(T('Sample photo is visible again.','示例照片已重新公开。'));break;case 'delete':$('.modal-layer')?.remove();overlay(`<h2>${T('Delete this photo?','删除这张照片?')}</h2><p class="note">${T('This removes it from this preview only. Production deletion follows server retention rules; it does not promise immediate physical deletion from OSS.','此处仅从预览移除。正式删除遵循服务端保留规则,不代表立即从 OSS 物理删除。')}</p>${button(T('Delete from preview','从预览删除'),'confirmDelete','danger')}${button(T('Cancel','取消'),'closeSheet','ghost')}`);break;case 'confirmDelete':removedPhotos.add(currentPhoto);go(17);break;case 'copy':{const url=new URL('../viewer-preview-2026-09-22.html',location.href).href;if(navigator.clipboard)navigator.clipboard.writeText(url).then(()=>toast(T('Local preview link copied','本地预览链接已复制'))).catch(()=>prompt(T('Copy preview link','复制预览链接'),url));else prompt(T('Copy preview link','复制预览链接'),url);break}case 'openViewer':window.open('../viewer-preview-2026-09-22.html','_blank','noopener,noreferrer');break}}
function bindForms(){const login=$('#loginForm');if(login)login.onsubmit=e=>{e.preventDefault();if(!$('#account').value.trim()){$('#formError').textContent=T('Enter a demo account.','请输入演示账号。');return}go(5)};const password=$('#passwordForm');if(password)password.onsubmit=e=>{e.preventDefault();const a=$('#newPassword').value,b=$('#confirmPassword').value;if(!$('#oldPassword').value||!a||a!==b){$('#formError').textContent=T('Complete all fields and make sure the new passwords match.','请填写全部字段,并确保两次新密码一致。');return}go(2);setTimeout(()=>toast(T('Validation passed · no password was saved','演示校验通过 · 未保存密码')),60)};const form=$('#albumForm');if(form)form.onsubmit=e=>{e.preventDefault();if(!$('#albumTitle').value.trim()||!$('#startDate').value||!$('#endDate').value||$('#endDate').value<$('#startDate').value){$('#formError').textContent=T('Enter a title and valid start/end dates.','请填写相册名称及有效起止日期。');return}albumTitle=$('#albumTitle').value.trim();if(page===10){publicAccess=$('#access-public').checked;uploadAllowed=$('#access-uploads').checked;downloads=$('#access-downloads').checked;original=downloads&&$('#access-original').checked}go(6)};const cat=$('#categoryForm');if(cat)cat.onsubmit=e=>{e.preventDefault();const val=$('#categoryName').value.trim();if(!val){$('#categoryError').textContent=T('A name is required.','请填写分类名称。');return}if(cat.dataset.index==='new')categoryNames.push(val);else categoryNames[Number(cat.dataset.index)]=val;closeSheet();$('#app').innerHTML=categories();toast(T('Sample category saved.','示例分类已保存。'))};const download=$('#access-downloads');if(download)download.onchange=()=>{const original=$('#access-original');original.disabled=!download.checked;if(!download.checked)original.checked=false};['historyAlbum','historyDate'].forEach(id=>{const el=$('#'+id);if(el)el.onchange=()=>{$('#historyRows').innerHTML=Array.from({length:el.selectedIndex?2:5},(_,i)=>uploadItem(i+el.selectedIndex,'history')).join('');toast(T('Sample history filter updated','示例历史筛选已更新'))}})}
document.addEventListener('click',e=>{const eye=e.target.closest('[data-eye]');if(eye){const input=$('#'+eye.dataset.eye);input.type=input.type==='password'?'text':'password';return}const target=e.target.closest('[data-go],[data-action]');if(target){if(target.dataset.go)go(target.dataset.go);else action(target.dataset.action)}});document.addEventListener('dblclick',e=>{if(e.target.closest('.large-photo'))$('.large-photo').classList.toggle('zoom')});document.addEventListener('keydown',e=>{const layer=$('.modal-layer');if(layer){if(e.key==='Escape')closeSheet();if(e.key==='Tab'){const els=[...layer.querySelectorAll('button,input,select,textarea')].filter(x=>!x.disabled);if(els.length){if(e.shiftKey&&document.activeElement===els[0]){e.preventDefault();els.at(-1).focus()}else if(!e.shiftKey&&document.activeElement===els.at(-1)){e.preventDefault();els[0].focus()}}}}else if(page===18&&!e.target.matches('input,textarea')){if(e.key==='ArrowLeft')action('photoPrev');if(e.key==='ArrowRight')action('photoNext');if(e.key==='Escape')go(17)}});let touchStart=null;document.addEventListener('pointerdown',e=>{if(e.target.closest('.photo-stage')&&!e.target.closest('button'))touchStart=[e.clientX,e.clientY]});document.addEventListener('pointerup',e=>{if(touchStart&&page===18){const dx=e.clientX-touchStart[0],dy=e.clientY-touchStart[1];if(Math.abs(dx)>60&&Math.abs(dx)>Math.abs(dy))action(dx<0?'photoNext':'photoPrev')}touchStart=null});window.addEventListener('resize',fitPhoto);window.addEventListener('hashchange',render);render();