37d88da4bc
- 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>
45 lines
38 KiB
JavaScript
45 lines
38 KiB
JavaScript
/* 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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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();
|