Files
photo-live/docs/designs/admin-preview-2026-09-22/admin.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

66 lines
56 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.
/* 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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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();