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>
66 lines
56 KiB
JavaScript
66 lines
56 KiB
JavaScript
/* PhotoLive admin design prototype. All writes are local sample-state changes. */
|
||
const $=s=>document.querySelector(s),$$=s=>[...document.querySelectorAll(s)];
|
||
const icon=n=>`<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="${window.ICONS[n]||window.ICONS.photo}"/></svg>`;
|
||
const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
let lang='zh',page=2,albumIndex=0,albumMode='grid',photoMode='grid',photoFilter='all',photoCategory='all',photoQuery='',albumQuery='',albumStatus='all',currentPhoto=0,selected=new Set(),toastTimer,syncRunning=false,wmVersion=3,wmPosition='bottom-right',slotIndex=0;
|
||
const T=(en,zh)=>lang==='zh'?zh:en;
|
||
const albums=[{title:'Canton Fair · Autumn',zh:'广交会 · 秋季',date:'2026.10.15 – 10.19',venue:'Guangzhou',owner:'Alex Morgan',public:true,upload:true,photo:0},{title:'Product Launch 2026',zh:'2026 新品发布会',date:'2026.09.28',venue:'Shenzhen',owner:'Emma Chen',public:true,upload:true,photo:5},{title:'Team Stories',zh:'团队故事',date:'2026.09.22',venue:'Shanghai',owner:'Daniel Lee',public:false,upload:true,photo:2},{title:'Design Summit',zh:'设计峰会',date:'2026.10.08 – 10.09',venue:'Hangzhou',owner:'Alex Morgan',public:true,upload:false,photo:4},{title:'Partner Conference',zh:'合作伙伴大会',date:'2026.11.12',venue:'Singapore',owner:'Emma Chen',public:false,upload:true,photo:1},{title:'Innovation Week',zh:'创新周',date:'2026.11.20 – 11.24',venue:'Guangzhou',owner:'Daniel Lee',public:true,upload:true,photo:3}];
|
||
const people=[{name:'Alex Morgan',account:'alex.morgan',role:'admin',source:'local',credential:'active',locked:false},{name:'Emma Chen',account:'emma.chen',role:'staff',source:'active',credential:'active',locked:false},{name:'Daniel Lee',account:'daniel.lee',role:'staff',source:'active',credential:'pending',locked:false},{name:'Olivia Wang',account:'olivia.wang',role:'staff',source:'disabled',credential:'active',locked:true},{name:'James Wilson',account:'james.wilson',role:'staff',source:'active',credential:'active',locked:false}];
|
||
let memberIds=[0,1,2],cats=[{en:'Exhibition',zh:'展会现场'},{en:'People',zh:'人物交流'},{en:'Highlights',zh:'精彩瞬间'}];
|
||
const photoStates=Array.from({length:12},(_,i)=>i===6?'hidden':i===10?'unpublished':i===11?'deleted':'published');
|
||
const slots=[{name:'Corporate website',zh:'企业官网',album:0,enabled:true,mode:'v2',version:4,speed:16},{name:'Event landing page',zh:'活动专题页',album:1,enabled:true,mode:'v1',version:2,speed:16},{name:'Partner portal',zh:'合作伙伴门户',album:null,enabled:false,mode:'v2',version:1,speed:16}];
|
||
// Photographic regions only; the full source board is never used as the UI.
|
||
const pics=[[292,165,393,149],[612,565,292,117],[931,565,292,117],[1247,565,289,117],[279,786,295,118],[608,786,295,118],[934,786,289,118],[1250,786,287,118]];
|
||
function photo(i,cls=''){const[x,y,w,h]=pics[(i+pics.length)%pics.length];return `<div class="art ${cls}" style="aspect-ratio:${w}/${h}"><svg width="100%" height="100%" viewBox="${x} ${y} ${w} ${h}" preserveAspectRatio="xMidYMid slice" aria-hidden="true" style="display:block"><image href="assets/photo-reference.png" width="1586" height="992"/></svg></div>`}
|
||
function btn(label,action,cls='',ic=''){return `<button class="btn ${cls}" data-action="${action}">${ic?icon(ic):''}${label}</button>`}
|
||
function pill(label,cls=''){return `<span class="pill ${cls}"><span class="dot"></span>${label}</span>`}
|
||
function title(a=albums[albumIndex]){return esc(lang==='zh'?a.zh:a.title)}
|
||
function avatar(name,cls=''){return `<span class="avatar ${cls}">${esc(name.split(' ').map(x=>x[0]).join(''))}</span>`}
|
||
function field(label,id,value='',type='text',note=''){return `<label class="field">${label}<input id="${id}" type="${type}" value="${esc(value)}">${note?`<small>${note}</small>`:''}</label>`}
|
||
function select(label,id,opts,value){return `<label class="field">${label}<select id="${id}">${opts.map(([v,s])=>`<option value="${v}" ${v===value?'selected':''}>${s}</option>`).join('')}</select></label>`}
|
||
function switchRow(label,desc,id,on){return `<label class="switch-row"><span><strong>${label}</strong><p>${desc}</p></span><input class="switch" type="checkbox" id="${id}" ${on?'checked':''}></label>`}
|
||
function table(headers,rows){return `<div class="data-wrap"><table class="data-table"><thead><tr>${headers.map(x=>`<th>${x}</th>`).join('')}</tr></thead><tbody>${rows.map(cols=>`<tr>${cols.map(x=>`<td>${x}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`}
|
||
function heading(name,desc,actions=''){return `<div class="page-heading"><div><h1>${name}</h1><p>${desc}</p></div><div class="actions">${actions}</div></div>`}
|
||
function tabs(){return `<nav class="tabs">${[[3,'Photos','照片'],[4,'Members','成员'],[5,'Categories','分类'],[6,'Watermark','水印'],[7,'Sharing','分享'],[15,'Branding','宣传图与资产']].map(([id,en,zh])=>`<a href="#${id}" class="${page===id?'active':''}">${T(en,zh)}</a>`).join('')}</nav>`}
|
||
function albumHeader(){const a=albums[albumIndex];return heading(title(),T('Album workspace · Manage the moments that matter','相册工作区 · 管理活动中的精彩瞬间'),btn(T('Edit album','编辑相册'),'go:9','outline','edit')+btn(T('Share album','分享相册'),'go:7','primary','share'))+`<div class="card album-banner">${photo(a.photo,'cover')}<div class="info-columns"><div class="meta-item">${icon('calendar')}<div><strong>${a.date}</strong><p>${T('Event time · Asia/Shanghai','活动时间 · Asia/Shanghai')}</p></div></div><div class="meta-item">${icon('user')}<div><strong>${T('Owner','创建者')}</strong><p>${a.owner}</p></div></div><div class="meta-item">${icon('pin')}<div><strong>${a.venue}</strong><p>${T('Event venue','活动地点')}</p></div></div><div class="meta-item">${icon('globe')}<div><strong>${a.public?T('Public access on','允许公开访问'):T('Public access off','公开访问关闭')}</strong><p>${a.upload?T('Staff uploads enabled','工作人员上传已开启'):T('Staff uploads disabled','工作人员上传已关闭')}</p></div></div></div></div>`+tabs()}
|
||
function shell(content){const selectedRoute=[2,3,4,5,6,7,8,9,15,16].includes(page)?2:[13,14].includes(page)?13:page===20?11:page;const link=(id,ic,en,zh)=>`<button class="side-link ${selectedRoute===id?'active':''}" data-action="go:${id}">${icon(ic)}${T(en,zh)}</button>`;return `<aside class="sidebar" id="sidebar"><div class="brand">${icon('logo')}PhotoLive</div><div class="group-label">${T('WORKSPACE','工作空间')}</div>${link(2,'album','Albums','相册管理')}${link(13,'photo','Embed widgets','网站展示位')}<div class="group-label">${T('ORGANIZATION','组织与治理')}</div>${link(10,'users','Accounts','账号管理')}${link(11,'flip','User sync','用户同步')}${link(12,'upload','Storage & usage','存储与用量')}<button class="preview-directory" data-action="directory">${T('Preview all pages ↗','全部页面目录 ↗')}</button><div class="side-bottom"><div class="person">${avatar('Alex Morgan')}<div><strong style="font-size:12px">Alex Morgan</strong><p class="muted small">${T('Company administrator','公司管理员')}</p></div></div><button class="text-btn" style="margin:8px 0 0 36px" data-action="go:1">${T('Sign out','退出登录')}</button></div></aside><div class="shell"><header class="topbar"><div class="breadcrumbs"><button class="mobile-menu" data-action="menu" aria-label="Menu">${icon('more')}</button><span>PhotoLive</span><span>/</span><span>${window.ADMIN_PAGES.find(p=>p.id===page)?.[lang]||T('Workspace','工作空间')}</span></div><div class="top-actions"><span class="demo-pill">${T('DESIGN PREVIEW · SAMPLE DATA','设计预览 · 示例数据')}</span><select id="language" aria-label="Language"><option value="zh" ${lang==='zh'?'selected':''}>中文</option><option value="en" ${lang==='en'?'selected':''}>EN</option></select><div class="person">${avatar('Alex Morgan')}<span class="small">Alex Morgan</span></div></div></header><main class="workspace">${content}</main></div>`}
|
||
function login(){return `<div class="login"><section class="login-visual">${photo(0)}<div class="brand">${icon('logo')}PhotoLive</div><div class="login-copy"><p style="font-size:10px;letter-spacing:3px;margin-bottom:20px">EVERY EVENT. EVERY MOMENT.</p><h1>${T('Bring your events into focus.','每个精彩现场,<br>从这里开始。')}</h1><p>${T('One workspace for your event albums, team and shared moments.','集中管理活动相册、协作团队与照片,让每个精彩瞬间及时抵达观众。')}</p></div><p style="position:relative;z-index:2;color:#8093af;font-size:11px">PhotoLive · Staff workspace</p></section><section class="login-form-area"><form id="loginForm" class="login-form"><div class="brand">${icon('logo')}PhotoLive</div><h1>${T('Welcome back','欢迎回来')}</h1><p>${T('Sign in to your company workspace','登录企业影像工作空间')}</p>${field(T('Account','账号'),'loginAccount','demo.admin')}${field(T('Password','密码'),'loginPassword','','password')}<div class="error" id="formError"></div><button type="submit" class="btn primary">${T('Sign in to preview','进入预览')}</button><p class="small muted" style="margin-top:22px">${T('Accounts are provided by your administrator. No registration or Feishu sign-in.','账号由企业提供,不支持自行注册或飞书登录。')}</p><div class="info-strip" style="margin-top:26px">${icon('help')}${T('Offline prototype. Do not enter real credentials.','离线交互原型,请勿输入真实账号密码。')}</div></form></section></div>`}
|
||
function albumList(){const rows=albums.map((a,i)=>({a,i})).filter(({a})=>(!albumQuery||JSON.stringify(a).toLowerCase().includes(albumQuery.toLowerCase()))&&(albumStatus==='all'||String(a.public)===albumStatus));return heading(T('Albums','相册管理'),T('A clear view of every event and the team behind it.','以活动为中心,管理相册、团队和每一张照片。'),btn(T('New album','新建相册'),'go:8','primary','plus'))+`<div class="toolbar"><div class="search">${icon('photo')}<input id="albumSearch" value="${esc(albumQuery)}" placeholder="${T('Search loaded albums','搜索当前已加载相册')}"></div><select id="albumStatus" aria-label="Album status"><option value="all">${T('All access states','全部公开状态')}</option><option value="true" ${albumStatus==='true'?'selected':''}>${T('Public','公开')}</option><option value="false" ${albumStatus==='false'?'selected':''}>${T('Private','未公开')}</option></select><div class="push seg"><button data-action="albumMode:grid" class="${albumMode==='grid'?'active':''}">${icon('photo')}</button><button data-action="albumMode:list" class="${albumMode==='list'?'active':''}">${icon('album')}</button></div></div><div class="summary-line"><span>${T('Loaded sample albums','已加载示例相册')} · ${rows.length}</span><span>${T('Sorted by creation time · cursor-based loading','按创建时间排序 · 沿用游标加载')}</span></div>${albumMode==='grid'?`<div class="album-grid">${rows.map(({a,i})=>`<article class="album"><button class="album-cover" data-action="album:${i}" aria-label="${title(a)}">${photo(a.photo)}${pill(a.public?T('Public','公开'):T('Private','未公开'),a.public?'green':'gray')}</button><div class="album-info"><h2><button data-action="album:${i}" style="padding:0;font-weight:650">${title(a)}</button></h2><p class="meta">${a.date} · ${a.venue}</p><div class="album-footer"><div class="person">${avatar(a.owner)}<span>${a.owner}</span></div>${pill(a.upload?T('Uploads on','可上传'):T('Uploads off','上传关闭'),a.upload?'':'gray')}</div></div></article>`).join('')}</div>`:`<div class="card">${table([T('Album','相册'),T('Owner','创建者'),T('Event date','活动日期'),T('Public access','公开访问'),T('Actions','操作')],rows.map(({a,i})=>[title(a),a.owner,a.date,pill(a.public?T('Public','公开'):T('Private','未公开'),a.public?'green':'gray'),btn(T('Manage','管理'),'album:'+i,'small outline')]))}</div>`}<div class="load-more">${rows.length?T('All sample albums are loaded','示例相册已全部加载'):T('No albums match these filters','没有符合筛选条件的相册')}</div>`}
|
||
function visibility(state){return {published:T('Published','已公开'),hidden:T('Hidden','已隐藏'),unpublished:T('Unpublished','未公开'),deleted:T('Deleted','已删除')}[state]}
|
||
function filteredPhotos(){return Array.from({length:12},(_,i)=>i).filter(i=>(photoFilter==='all'||photoStates[i]===photoFilter)&&(photoCategory==='all'||i%3===Number(photoCategory))&&(!photoQuery||('DSC0'+(4872+i)+'.jpg').toLowerCase().includes(photoQuery.toLowerCase())))}
|
||
function photosPage(){const list=filteredPhotos();return albumHeader()+`<div class="toolbar"><div class="seg">${['all','published','hidden','unpublished','deleted'].map(s=>`<button class="${photoFilter===s?'active':''}" data-action="photoFilter:${s}">${s==='all'?T('All photos','全部照片'):visibility(s)}</button>`).join('')}</div><select id="photoCategory" aria-label="Category"><option value="all">${T('All categories','全部分类')}</option>${cats.map((c,i)=>`<option value="${i}" ${String(i)===photoCategory?'selected':''}>${esc(c[lang])}</option>`).join('')}</select><div class="search">${icon('photo')}<input id="photoSearch" value="${esc(photoQuery)}" placeholder="${T('Search sample filenames','搜索示例文件名')}"></div><div class="seg push"><button data-action="photoMode:grid" class="${photoMode==='grid'?'active':''}">${icon('photo')}</button><button data-action="photoMode:list" class="${photoMode==='list'?'active':''}">${icon('album')}</button></div></div><div class="selection-bar" id="selectionBar" ${selected.size?'':'hidden'}><span id="selectedCount">${selected.size} ${T('selected','张已选')}</span>${btn(T('Hide selected','隐藏选中照片'),'batchHide','small outline','eye')}<span class="small">${T('Only published photos can be selected','仅已公开照片可批量选择')}</span></div>${photoMode==='grid'?`<div class="photo-grid">${list.map(i=>`<article class="photo-item"><div class="photo-image ${selected.has(i)?'selected':''}"><button data-action="photo:${i}" aria-label="${T('View photo','查看照片')} ${i+1}">${photoStates[i]==='deleted'?`<div class="empty">${T('Deleted','已删除')}</div>`:photo(i)}</button><input class="photo-check" type="checkbox" value="${i}" aria-label="${T('Select photo','选择照片')} ${i+1}" ${selected.has(i)?'checked':''} ${photoStates[i]!=='published'?'disabled':''}>${pill(visibility(photoStates[i]),photoStates[i]==='published'?'green':'gray')}</div><div class="photo-caption"><div><strong>DSC0${4872+i}.jpg</strong><p>${people[i%3].name} · Oct 15, 2026</p></div><button data-action="photo:${i}" aria-label="${T('Photo actions','照片操作')}">${icon('more')}</button></div></article>`).join('')}</div>`:`<div class="card">${table([T('Photo','照片'),T('Processing','处理状态'),T('Visibility','可见性'),T('Uploader','上传者'),T('Likes (replica)','点赞数(副本)'),T('Actions','操作')],list.map(i=>[`<div class="person">${photo(i,'tiny-photo')}DSC0${4872+i}.jpg</div>`,pill(i===10?T('Processing','处理中'):T('Ready','已就绪'),i===10?'':'green'),pill(visibility(photoStates[i]),photoStates[i]==='published'?'green':'gray'),people[i%3].name,[128,86,54,102][i%4],btn(T('Details','详情'),'photo:'+i,'small')]))}</div>`}<div class="load-more">${T('Sample photos · Statistics replica updated at 14:32 · No live API connection','示例照片 · 统计副本更新时间 14:32 · 未连接真实接口')}</div>`}
|
||
function membersPage(){return albumHeader()+`<div class="card"><div class="card-head"><div><h2>${T('Album members','相册成员')}</h2><p class="small muted" style="margin-top:6px">${T('Owners manage all photos. Collaborators manage their own uploads.','创建者管理全部照片;协作者管理自己上传的照片。')}</p></div>${btn(T('Invite colleague','邀请同事'),'invite','primary','users')}</div>${table([T('Member','成员'),T('Account','账号'),T('Role','角色'),T('Scope','权限范围'),T('Actions','操作')],memberIds.map((id,index)=>{const p=people[id];return [`<div class="person">${avatar(p.name)}<strong>${p.name}</strong></div>`,p.account,pill(index===0?T('Owner','创建者'):T('Collaborator','协作者'),index===0?'':'gray'),index===0?T('All photos and members','全部照片及成员'):T('Own uploaded photos','自己上传的照片'),index===0?btn(T('Transfer ownership','交接相册'),'transfer','small'):btn(T('Remove','移除'),'removeMember:'+id,'small danger')]}))}</div>`}
|
||
function categoriesPage(){return albumHeader()+`<div class="card"><div class="card-head"><div><h2>${T('Photo categories','照片分类')}</h2><p class="small muted">${T('Keep both content languages together when saving.','保存时保留所有语言内容,避免覆盖其他语言。')}</p></div>${btn(T('New category','新增分类'),'category:new','primary','plus')}</div>${table([T('Category','分类名称'),T('Chinese','中文'),T('English','English'),T('Actions','操作')],cats.map((c,i)=>[`<div class="person"><span class="avatar">${icon('tag')}</span><strong>${esc(c[lang])}</strong></div>`,esc(c.zh),esc(c.en),btn(T('Edit','编辑'),'category:'+i,'small')+' '+btn(T('Delete','删除'),'deleteCategory:'+i,'small danger')]))}<p class="small muted" style="margin-top:18px">${T('A category with photos cannot be deleted.','已有照片的分类不能直接删除。')}</p></div>`}
|
||
function watermarkPage(){return albumHeader()+`<div class="two-col equal"><div class="card"><div class="card-head"><h2>${T('Watermark settings','水印配置')}</h2>${pill('v'+wmVersion,'green')}</div><form id="watermarkForm">${switchRow(T('Enable watermark','启用水印'),T('Apply to generated viewing variants','应用于生成的浏览变体'),'wmOn',true)}<div class="form-grid" style="margin-top:20px">${field(T('Watermark text','水印文字'),'wmText','PhotoLive')}${select(T('Position','位置'),'wmPosition',[['bottom-right',T('Bottom right','右下')],['bottom-left',T('Bottom left','左下')],['top-right',T('Top right','右上')],['top-left',T('Top left','左上')],['center',T('Center','居中')]],wmPosition)}<label class="field">${T('Opacity','透明度')}<input id="wmOpacity" type="range" min="0" max="100" value="70"></label>${field(T('Font size','字号'),'wmFont','20','number')}</div><button type="submit" class="btn primary">${T('Save and generate','保存并生成')}</button><span class="small muted"> ${T('Demo job only','仅演示任务')}</span></form></div><div class="card"><div class="card-head"><h2>${T('Visual preview','效果预览')}</h2><span class="small muted">CSS ${T('simulation','示意')}</span></div><div class="watermark-preview">${photo(1)}<span class="watermark-text" id="wmPreview">PhotoLive</span></div><p class="small muted" style="margin-top:14px">${T('Preview placement only. Final rendering is produced by the server.','此处仅示意位置,最终水印由服务端生成。')}</p></div></div><div class="card"><div class="card-head"><h2>${T('Generation history','生成任务')}</h2>${btn(T('Regenerate','重新生成'),'regenerate','outline','flip')}</div>${table([T('Version','版本'),T('State','状态'),T('Processed','已处理'),T('Failed','失败'),T('Created','创建时间')],[['v'+wmVersion,pill(T('Completed','已完成'),'green'),'124 / 124','0','2026-09-22 14:20'],['v2',pill(T('Completed','已完成'),'green'),'118 / 118','0','2026-09-21 16:42']])}</div>`}
|
||
function sharingPage(){return albumHeader()+`<div class="two-col equal"><div class="card share-card">${photo(albums[albumIndex].photo,'cover')}<h2>${title()}</h2><p>${albums[albumIndex].date} · ${albums[albumIndex].venue}</p><img class="qr" src="assets/demo-qr.svg" alt="Demonstration QR code"><p>${T('Demo code · example.com · not a public event link','演示二维码 · 指向 example.com · 非真实活动链接')}</p></div><div class="card"><h2>${T('Share your event','分享活动相册')}</h2><p class="muted" style="margin:14px 0 22px">${T('Visitors browse without signing in. Public access must be enabled.','观众凭链接或扫码免登录浏览,须先开启相册公开访问。')}</p><div class="info-strip ${albums[albumIndex].public?'':'warn'}">${icon('globe')}${albums[albumIndex].public?T('Public access is enabled in this sample.','当前示例已开启公开访问。'):T('Public access is disabled in this sample.','当前示例已关闭公开访问。')}</div>${field(T('Preview link','预览链接'),'shareLink',new URL('../viewer-preview-2026-09-22.html',location.href).href)}<div class="actions">${btn(T('Copy preview link','复制预览链接'),'copyShare','primary','link')}${btn(T('Open viewer','打开观众页'),'viewer','outline')}</div><p class="small muted" style="margin-top:24px">${T('Local file links work on this computer only. Production QR codes must use the public album URL.','本地链接仅用于本机预览,生产二维码必须使用真实公开相册 URL。')}</p></div></div>`}
|
||
function albumForm(edit){const a=albums[albumIndex];return heading(edit?T('Edit album','编辑相册'):T('Create album','新建相册'),T('Event details, content languages and public access in one place.','统一配置活动资料、多语言内容与访问权限。'),btn(T('Cancel','取消'),'go:2'))+`<form id="albumForm"><div class="two-col"><div class="card"><div class="card-head"><h2>${T('Event details','活动资料')}</h2>${pill(T('Chinese + English','中文 + English'))}</div><div class="form-grid">${field(T('Title (Chinese)','标题(中文)'),'titleZh',edit?a.zh:'')}${field(T('Title (English)','标题(English)'),'titleEn',edit?a.title:'')}${field(T('Start date','开始日期'),'startDate','2026-10-15','date')}${field(T('End date','结束日期'),'endDate','2026-10-19','date')}${select(T('Time zone','时区'),'timezone',[['Asia/Shanghai','Asia/Shanghai'],['Europe/London','Europe/London'],['America/New_York','America/New_York']],'Asia/Shanghai')}${select(T('Default language','默认语言'),'contentLang',[['zh','中文'],['en','English']],'zh')}${field(T('Venue (Chinese)','地点(中文)'),'venueZh',edit?'广州':'')}${field(T('Venue (English)','地点(English)'),'venueEn',edit?a.venue:'')}<label class="field wide">${T('Description (Chinese)','说明(中文)')}<textarea id="descriptionZh">${edit?'欢迎浏览本次活动的精彩照片。':''}</textarea></label><label class="field wide">${T('Description (English)','说明(English)')}<textarea id="descriptionEn">${edit?'Discover the moments from this event.':''}</textarea></label></div></div><div><div class="card"><h2>${T('Access settings','访问设置')}</h2>${switchRow(T('Staff uploads','工作人员上传'),T('Team members can upload photos','团队成员可上传照片'),'albumUpload',a.upload)}${switchRow(T('Public access','公开访问'),T('Visitors with the link can browse','凭链接免登录浏览'),'albumPublic',a.public)}${switchRow(T('Photo downloads','允许下载'),T('Download viewing variants','观众可下载浏览图'),'albumDownload',true)}${switchRow(T('Original downloads','原图下载'),T('Requires photo downloads','须先允许下载'),'albumOriginal',false)}</div><div class="info-strip" style="margin-top:18px">${icon('help')}${T('Preview changes are stored in this tab only.','预览变更仅保留在当前标签页内。')}</div><div id="formError" class="error"></div><button class="btn primary" style="width:100%" type="submit">${T('Save album','保存相册')}</button></div></div></form>`}
|
||
function usersPage(){return heading(T('Accounts','账号管理'),T('Provisioned company accounts, credential state and local access controls.','统一查看企业来源状态、登录凭据与本地账号治理。'),btn(T('View user sync','查看用户同步'),'go:11','outline','flip'))+`<div class="info-strip">${icon('help')}${T('User information is pulled from the company directory. No public registration. Source state and local lock are independent.','用户信息来自企业接口定期拉取,不开放注册;来源状态与本地锁定相互独立。')}</div><div class="card"><div class="toolbar"><div class="search">${icon('user')}<input id="userSearch" placeholder="${T('Search account or name','搜索账号或姓名')}"></div><select id="userStatus" aria-label="Account status"><option value="all">${T('All states','全部状态')}</option><option value="locked">${T('Locally locked','本地锁定')}</option><option value="pending">${T('Pending activation','待激活')}</option></select>${btn(T('Reset filters','重置筛选'),'resetUserFilters')}</div><div id="usersTable">${usersTable()}</div></div>`}
|
||
function usersTable(query='',status='all'){return table([T('User','用户'),T('Role','角色'),T('Source status','来源状态'),T('Credentials','凭据状态'),T('Local lock','本地锁定'),T('Actions','操作')],people.map((p,i)=>({p,i})).filter(({p})=>(p.name+p.account).toLowerCase().includes(query.toLowerCase())&&(status==='all'||status==='locked'&&p.locked||status==='pending'&&p.credential==='pending')).map(({p,i})=>[`<div class="person">${avatar(p.name)}<div><strong>${p.name}</strong>${i===0?' '+pill(T('You','当前用户')):''}<p>${p.account}</p></div></div>`,pill(p.role==='admin'?T('Admin','管理员'):T('Staff','员工'),p.role==='admin'?'':'gray'),pill(p.source==='local'?T('Local','本地'):p.source==='active'?T('Active','来源有效'):T('Disabled','来源停用'),p.source==='disabled'?'red':'gray'),pill(p.credential==='pending'?T('Pending','待激活'):T('Active','已激活'),p.credential==='pending'?'amber':'green'),pill(p.locked?T('Locked','已锁定'):T('Unlocked','未锁定'),p.locked?'red':'gray'),`<div class="actions">${btn(T('Edit','编辑'),'userEdit:'+i,'small')}${btn(T('Reset password','重置密码'),'resetPassword:'+i,'small')}${i>0?btn(p.credential==='pending'?T('Activate','激活'):p.locked?T('Unlock','解锁'):T('Lock','锁定'),'userState:'+i,'small'):''}</div>`]))}
|
||
function syncPage(){return heading(T('User sync','用户同步'),T('Review scheduled directory pulls and resolve failed records.','查看企业用户接口的定时拉取结果,定位失败记录。'),btn(syncRunning?T('Running…','运行中…'):T('Pull now','手动补拉'),'sync','primary','flip'))+`<div class="stats"><div class="stat"><p>${T('Engine state','同步引擎状态')}</p><strong style="font-size:23px">${syncRunning?T('Running','运行中'):T('Idle','空闲')}</strong><small>${T('Last success','最近成功')} 2026-09-22 14:30</small></div><div class="stat"><p>${T('Last batch','最近批次')}</p><strong>128 <span style="font-size:13px;color:#8294ae">${T('processed','条处理')}</span></strong><small>${T('3 created · 124 updated · 1 failed','新增 3 · 更新 124 · 失败 1')}</small></div><div class="stat"><p>${T('Source version','来源版本')}</p><strong style="font-size:23px">20260922.1430</strong><small>${T('User directory watermark, not photo replication','用户目录水位,并非照片跨境同步')}</small></div></div><div class="card"><div class="card-head"><h2>${T('Run history','运行历史')}</h2><span class="small muted">${T('Sample scheduled jobs','示例定时任务')}</span></div>${table([T('Triggered','触发方式'),T('State','运行状态'),T('Started','开始时间'),T('Created','新增'),T('Updated','更新'),T('Failed','失败'),T('Details','详情')],Array.from({length:4},(_,i)=>[pill(i===1?T('Manual','手动'):T('Scheduled','定时'),'gray'),pill(i===0?T('Partial failure','部分失败'):T('Completed','完成'),i===0?'amber':'green'),`2026-09-22 ${14-i}:30`,i===0?'3':'0',i===0?'124':'128',i===0?'1':'0',btn(T('View run','查看批次'),'syncDetail:'+i,'small outline')]))}</div>`}
|
||
function usagePage(){return heading(T('Storage & usage','存储与用量'),T('Understand retained assets and act before thresholds are exceeded.','查看长期保留的照片资产与告警阈值。'),btn(T('Threshold settings','告警阈值'),'thresholds','outline','settings'))+`<div class="stats"><div class="stat"><p>${T('Total photos','照片总数')}</p><strong>18,420</strong><small>${T('Sample aggregate from the usage response','示例用量接口汇总')}</small></div><div class="stat"><p>${T('Stored assets','存储用量')}</p><strong>148.6 <span style="font-size:17px">GB</span></strong><small>${T('Originals + viewing variants','原图 + 浏览变体')}</small></div><div class="stat"><p>${T('Storage alert threshold','总存储告警阈值')}</p><strong id="thresholdValue">200 <span style="font-size:17px">GB</span></strong><small>${T('An alert threshold, not a hard storage quota','告警阈值,并非存储硬配额')}</small></div></div><div class="card"><div class="card-head"><h2>${T('Asset composition','资产构成')}</h2><span class="small muted">${T('OSS · retained by default','OSS · 默认长期保留')}</span></div><div class="storage-bar"><span style="width:68%"></span><span style="width:20%"></span></div><div class="legend"><span>● ${T('Originals','原图')} 114.8 GB</span><span>● ${T('Variants','变体')} 33.8 GB</span></div></div><div class="card"><div class="card-head"><h2>${T('Usage by album','相册用量')}</h2></div>${table([T('Album','相册'),T('Photos','照片数'),T('Originals','原图'),T('Variants','变体'),T('Total','合计')],albums.slice(0,4).map((a,i)=>[title(a),[6280,4210,3150,4780][i],[42.5,26.1,18.7,27.5][i]+' GB',[12.3,8.2,5.8,7.5][i]+' GB',[54.8,34.3,24.5,35][i]+' GB']))}</div><div class="card"><div class="card-head"><h2>${T('Alerts','用量告警')}</h2></div>${table([T('State','状态'),T('Kind','类别'),T('Message','说明'),T('Time','时间')],[[pill(T('Active','未解决'),'amber'),T('Per-album storage','单相册存储'),T('Canton Fair exceeds the 50 GB sample threshold.','广交会相册超过示例阈值 50 GB。'),'2026-09-22 14:20'],[pill(T('Resolved','已解决'),'gray'),T('Storage','存储'),T('Previous threshold alert resolved.','历史阈值告警已解除。'),'2026-09-20 09:30']])}</div>`}
|
||
function slotsPage(){return heading(T('Embed widgets','网站展示位'),T('Publish a small live photo window on your websites.','在企业网站中展示活动照片,管理绑定与展示配置。'),btn(T('New widget','新增展示位'),'newSlot','primary','plus'))+`<div class="info-strip">${icon('photo')}${T('New design: 195 × 280 px on desktop and mobile. v1 carousel and v2 vertical stream remain distinct.','新版照片窗:电脑与手机统一 195 × 280 px;v1 轮播与 v2 上滚模式分开管理。')}</div><div class="card">${table([T('Widget','展示位名称'),T('Bound album','绑定相册'),T('Mode','展示模式'),T('Enabled','启用'),T('Config version','配置版本'),T('Actions','操作')],slots.map((s,i)=>[esc(lang==='zh'?s.zh:s.name),s.album===null?pill(T('Unbound','未绑定'),'gray'):title(albums[s.album]),pill(s.mode==='v2'?T('v2 · Vertical stream','v2 · 照片上滚'):T('v1 · Carousel','v1 · 旧版轮播')),`<input class="switch" type="checkbox" data-slot-toggle="${i}" aria-label="${esc(s.name)}" ${s.enabled?'checked':''}>`,'v'+s.version,`<div class="actions">${btn(T('Configure','配置'),'slot:'+i,'small outline')}${btn(T('Bind album','绑定相册'),'bind:'+i,'small')}${btn(T('Get code','获取代码'),'code:'+i,'small')}</div>`]))}</div>`}
|
||
function embedPreview(){const seq=[0,1,2,0,1,2];return `<div class="preview-frame"><div class="mock-site">COMPANY WEBSITE / PREVIEW</div><div class="site-lines"><div></div><div style="width:64%"></div><div style="width:82%"></div><div style="width:56%"></div></div><div class="embedded" id="embedded"><div class="embed-track" id="embedTrack">${seq.map(i=>photo(i)).join('')}</div><span class="embed-hearts" id="embedHearts">♥</span><button class="embed-minus" data-action="collapseEmbed" aria-label="Collapse preview">−</button></div><button id="expandEmbed" class="btn" style="position:absolute;right:16px;bottom:24px" hidden data-action="expandEmbed">${T('Photos','照片')}</button></div>`}
|
||
function slotEditor(){const s=slots[slotIndex];return heading(T('Configure widget','配置展示位'),esc(lang==='zh'?s.zh:s.name),btn(T('Back to widgets','返回展示位'),'go:13'))+`<div class="two-col equal"><div class="card"><form id="slotForm">${field(T('Name','名称'),'slotName',lang==='zh'?s.zh:s.name)}${select(T('Display mode','展示模式'),'slotMode',[['v2',T('v2 · Vertical stream','v2 · 照片上滚')],['v1',T('v1 · Existing carousel','v1 · 保留旧轮播')]],s.mode)}<div id="v2Fields"><div class="form-grid">${field(T('Width (fixed)','宽度(固定)'),'slotWidth','195','number')}${field(T('Height (fixed)','高度(固定)'),'slotHeight','280','number')}${field(T('Scroll speed (px/s)','滚动速度(px/s)'),'slotSpeed',s.speed,'number')}${select(T('Position','悬浮位置'),'slotPosition',[['right',T('Right','右侧')],['left',T('Left','左侧')]],'right')}</div>${switchRow(T('Like bubbles','点赞气泡'),T('Real reactions in production; simulated here','正式版使用真实事件,此处仅模拟'),'slotReactions',true)}</div><div id="v1Fields" hidden>${field(T('Width','宽度'),'legacyWidth','320','number')}${field(T('Interval (ms)','切图间隔(ms)'),'legacyInterval','5000','number')}<p class="small muted">${T('Legacy settings are retained. The preview at right depicts the new design.','旧版配置继续保留。右侧展示新版设计参考。')}</p></div><div class="form-grid" style="margin-top:18px">${field(T('Horizontal offset','水平偏移'),'offsetX','16','number')}${field(T('Bottom offset','底部偏移'),'offsetY','24','number')}</div>${switchRow(T('Start collapsed on mobile','手机默认收起'),T('User choice has priority','用户主动选择优先'),'slotCollapsed',true)}<div class="error" id="formError"></div><button class="btn primary" type="submit">${T('Save configuration','保存配置')}</button></form></div><div class="card"><div class="card-head"><h2>${T('Unsaved preview','未保存预览')}</h2>${pill('195 × 280 px')}</div>${embedPreview()}<div class="actions" style="margin-top:15px">${btn(T('Simulate a reaction','模拟点赞事件'),'embedHeart','small outline')}${btn(T('Open full widget demo','打开完整组件原型'),'widgetDemo','small')}</div><p class="small muted" style="margin-top:14px">${T('Presentation mock only. The standalone widget demo implements the precise 16 px/s stream.','此面板为样式示意;独立组件原型实现精确 16 px/s 上滚。')}</p></div></div>`}
|
||
function brandingPage(){return albumHeader()+`<div class="two-col equal"><div class="card"><h2>${T('Public album banner','观众页宣传图')}</h2><p class="small muted" style="margin:9px 0 20px">${T('Prefer a wide image. Do not crop away embedded poster text.','建议使用横幅图片,保留海报中的文字内容。')}</p>${photo(albums[albumIndex].photo,'cover')}<div class="actions" style="margin-top:18px">${btn(T('Choose sample banner','选择示例宣传图'),'banner','primary','photo')}${btn(T('Preview public album','预览观众相册'),'viewer','outline')}</div></div><div class="card"><h2>${T('Asset workflow','资产管理')}</h2><div class="info-strip" style="margin-top:20px">${icon('upload')}${T('Init → OSS direct upload → completion verification','初始化凭证 → OSS 直传 → 完成核验')}</div><p class="muted">${T('The existing API accepts banner, cover and logo asset types. Production must keep signature expiry and upload verification behavior. This prototype only swaps sample photographs.','现有接口支持 banner、cover、logo 资产类型。正式实现保留签名过期与上传核验流程。本原型仅切换示例照片。')}</p><h3 style="margin:24px 0 10px">${T('Current sample','当前示例')}</h3><p class="small muted">event-banner.jpg · image/jpeg · ${T('Sample preview','示意预览')}</p></div></div>`}
|
||
function exceptionPage(code){const text={403:[T('Access restricted','暂无访问权限'),T('Your current account cannot access this workspace.','当前账号无权访问此页面,请联系公司管理员。')],404:[T('Page not found','页面不存在'),T('The link may have changed or the resource is unavailable.','链接可能已失效,或资源暂不可用。')],500:[T('Something went wrong','服务暂时不可用'),T('Please try again. Your saved data has not been changed by this preview.','请稍后重试,已保存的数据不会因本次预览发生变化。')]}[code];return `<section class="exception"><strong>${code}</strong><h1>${text[0]}</h1><p>${text[1]}</p>${btn(T('Back to albums','返回相册'),'go:2','primary')}</section>`}
|
||
function modal(name,body,footer='',wide=false){$('.modal')?.remove();const wrap=document.createElement('div');wrap.className='modal';wrap.innerHTML=`<section class="dialog ${wide?'wide':''}" role="dialog" aria-modal="true" aria-label="${esc(name)}"><div class="dialog-head"><h2>${name}</h2><button class="btn icon-only" data-action="close" aria-label="Close">${icon('close')}</button></div>${body}${footer?`<div class="dialog-foot">${footer}</div>`:''}</section>`;document.body.append(wrap);$('#root').inert=true;wrap.onclick=e=>{if(e.target===wrap)closeModal()};wrap.querySelector('input,button,select')?.focus();return wrap}
|
||
function closeModal(){ $('.modal')?.remove();$('#root').inert=false;if(page===16)go(3);if(page===20)go(11)}
|
||
function toast(text){$('.toast')?.remove();const el=document.createElement('div');el.className='toast';el.setAttribute('role','status');el.textContent=text;document.body.append(el);clearTimeout(toastTimer);toastTimer=setTimeout(()=>el.remove(),3200)}
|
||
function go(id){if(page===Number(id)){render();return}location.hash=id}
|
||
function render(){page=Number(location.hash.slice(1))||Number(document.body.dataset.page)||2;if(page<1||page>20)page=18;$('.modal')?.remove();$('#root').inert=false;document.documentElement.lang=lang==='zh'?'zh-CN':'en';if(page===1){$('#root').innerHTML=login();bind();return}let content='';switch(page){case 2:content=albumList();break;case 3:case 16:content=photosPage();break;case 4:content=membersPage();break;case 5:content=categoriesPage();break;case 6:content=watermarkPage();break;case 7:content=sharingPage();break;case 8:content=albumForm(false);break;case 9:content=albumForm(true);break;case 10:content=usersPage();break;case 11:case 20:content=syncPage();break;case 12:content=usagePage();break;case 13:content=slotsPage();break;case 14:content=slotEditor();break;case 15:content=brandingPage();break;case 17:content=exceptionPage(403);break;case 18:content=exceptionPage(404);break;case 19:content=exceptionPage(500);break}$('#root').innerHTML=shell(content);bind();if(page===16)openPhoto(currentPhoto);if(page===20)syncDetail(0);document.title='PhotoLive · '+(window.ADMIN_PAGES.find(p=>p.id===page)?.[lang]||'Preview')}
|
||
function openPhoto(i){currentPhoto=i;const state=photoStates[i];if(state==='deleted'){toast(T('This sample photo has been deleted.','该示例照片已删除。'));return}const wrap=modal(T('Photo details','照片详情'),'');wrap.querySelector('.dialog').className='dialog viewer-dialog';wrap.querySelector('.dialog').innerHTML=`<div class="viewer-main"><button class="viewer-close btn icon-only" data-action="close" aria-label="Close">${icon('close')}</button>${photo(i)}</div><div class="viewer-info"><h2>DSC0${4872+i}.jpg</h2><p>${people[i%3].name}</p><p>2026-10-15 10:42<br>2400 × 1600 · 2.8 MB</p>${pill(visibility(state),state==='published'?'green':'gray')}<p>${T('Likes (statistics replica)','点赞数(统计副本)')} · ${[128,86,54,102][i%4]}</p>${state!=='unpublished'?btn(state==='hidden'?T('Publish again','恢复公开'):T('Hide photo','隐藏照片'),'visibility:'+i,'outline','eye'):''}${btn(T('Delete photo','删除照片'),'deletePhoto:'+i,'danger','trash')}<p class="small">${T('Staff management actions only. No public like or download controls.','此处为工作人员管理操作,不提供观众点赞或下载按钮。')}</p></div>`}
|
||
function syncDetail(i){modal(T('Sync run details','同步批次详情'),`<div class="info-strip ${i===0?'warn':''}">${icon('flip')}${i===0?T('127 succeeded · 1 failed','127 条成功 · 1 条失败'):T('128 succeeded · 0 failed','128 条成功 · 0 条失败')}</div>${table([T('Record','记录'),T('Result','结果'),T('Reason','原因')],i===0?[['sample.user.129',pill(T('Failed','失败'),'red'),T('Missing required account field','缺少必填账号字段')]]:[['—',pill(T('Completed','完成'),'green'),T('No failed records','没有失败记录')]])}<p style="margin-top:18px">${T('Example failure details are redacted. Correct the source data before the next run.','失败明细为脱敏示例,请修正来源数据后再进行补拉。')}</p>`,btn(T('Close','关闭'),'close'))}
|
||
function filterInput(id,current,assign){const el=$('#'+id);if(el)el.oninput=e=>{const start=e.target.selectionStart;assign(e.target.value);render();const next=$('#'+id);next.focus();next.setSelectionRange(start,start)}}
|
||
function bind(){const locale=$('#language');if(locale)locale.onchange=e=>{lang=e.target.value;render()};const loginForm=$('#loginForm');if(loginForm)loginForm.onsubmit=e=>{e.preventDefault();if(!$('#loginAccount').value.trim()){$('#formError').textContent=T('Enter a sample account.','请输入演示账号。');return}go(2)};filterInput('albumSearch',albumQuery,x=>albumQuery=x);filterInput('photoSearch',photoQuery,x=>photoQuery=x);if($('#albumStatus'))$('#albumStatus').onchange=e=>{albumStatus=e.target.value;render()};if($('#photoCategory'))$('#photoCategory').onchange=e=>{photoCategory=e.target.value;render()};$$('.photo-check').forEach(el=>el.onchange=()=>{const i=Number(el.value);if(el.checked)selected.add(i);else selected.delete(i);render()});const filterUsers=()=>$('#usersTable').innerHTML=usersTable($('#userSearch').value,$('#userStatus').value);if($('#userSearch')){$('#userSearch').oninput=filterUsers;$('#userStatus').onchange=filterUsers}$$('[data-slot-toggle]').forEach(el=>el.onchange=()=>{const s=slots[Number(el.dataset.slotToggle)];s.enabled=el.checked;s.version++;render();toast(T('Sample widget state updated','示例展示位状态已更新'))});
|
||
const form=$('#albumForm');if(form){$('#albumDownload').onchange=e=>{$('#albumOriginal').disabled=!e.target.checked;if(!e.target.checked)$('#albumOriginal').checked=false};form.onsubmit=e=>{e.preventDefault();if(!$('#titleZh').value.trim()||!$('#titleEn').value.trim()||!$('#startDate').value||$('#endDate').value<$('#startDate').value){$('#formError').textContent=T('Enter both sample titles and a valid date range.','请填写示例双语标题及有效日期范围。');return}const data={title:$('#titleEn').value,zh:$('#titleZh').value,date:$('#startDate').value+' – '+$('#endDate').value,venue:$('#venueEn').value||$('#venueZh').value,owner:'Alex Morgan',public:$('#albumPublic').checked,upload:$('#albumUpload').checked,photo:0};if(page===8){albums.unshift(data);albumIndex=0}else albums[albumIndex]={...albums[albumIndex],...data};go(3)}}
|
||
if($('#watermarkForm')){const preview=()=>{const el=$('#wmPreview');el.textContent=$('#wmText').value;el.hidden=!$('#wmOn').checked;el.style.opacity=$('#wmOpacity').value/100;el.style.fontSize=Math.min(72,Math.max(8,Number($('#wmFont').value)||20))+'px';const pos=$('#wmPosition').value;el.style.top=pos.startsWith('top')?'18px':pos==='center'?'50%':'auto';el.style.bottom=pos.startsWith('bottom')?'18px':'auto';el.style.left=pos.endsWith('left')?'18px':pos==='center'?'50%':'auto';el.style.right=pos.endsWith('right')?'18px':'auto';el.style.transform=pos==='center'?'translate(-50%,-50%)':''};['wmText','wmOpacity','wmFont','wmPosition','wmOn'].forEach(id=>$('#'+id).oninput=preview);$('#watermarkForm').onsubmit=e=>{e.preventDefault();wmVersion++;toast(T('Sample generation job accepted. No images are modified.','示例生成任务已受理,未修改任何图片。'))}}
|
||
if($('#slotForm')){const s=slots[slotIndex];$('#slotWidth').readOnly=true;$('#slotHeight').readOnly=true;$('#slotSpeed').min=8;$('#slotSpeed').max=32;const mode=()=>{$('#v2Fields').hidden=$('#slotMode').value!=='v2';$('#v1Fields').hidden=$('#slotMode').value!=='v1'};$('#slotMode').onchange=mode;mode();$('#offsetX').oninput=()=>{$('#embedded').style.right=Math.max(0,Number($('#offsetX').value))+'px'};$('#offsetY').oninput=()=>{$('#embedded').style.bottom=Math.max(0,Number($('#offsetY').value))+'px'};$('#slotPosition').onchange=e=>{$('#embedded').style.left=e.target.value==='left'?'16px':'auto';$('#embedded').style.right=e.target.value==='right'?'16px':'auto'};$('#slotForm').onsubmit=e=>{e.preventDefault();const speed=Number($('#slotSpeed').value);if(!$('#slotName').value.trim()||speed<8||speed>32){$('#formError').textContent=T('Name required. Speed must be between 8 and 32.','请填写名称,速度须在 8–32 之间。');return}s.name=$('#slotName').value;s.zh=s.name;s.speed=speed;s.mode=$('#slotMode').value;s.version++;go(13)}}}
|
||
function confirm(titleText,body,callback){modal(titleText,`<p>${body}</p>`,btn(T('Cancel','取消'),'close')+btn(T('Confirm','确认'),'confirm','primary'));$('[data-action="confirm"]').onclick=()=>{closeModal();callback()}}
|
||
function action(raw){const [name,...rest]=raw.split(':'),value=rest.join(':');switch(name){case 'go':go(value);break;case 'close':closeModal();break;case 'menu':$('#sidebar').classList.toggle('open');break;case 'directory':modal(T('All preview pages','全部预览页面'),`<div class="form-grid">${window.ADMIN_PAGES.map(p=>btn(String(p.id).padStart(2,'0')+' · '+p[lang],'go:'+p.id,'')).join('')}</div>`,'',true);break;case 'album':albumIndex=Number(value);go(3);break;case 'albumMode':albumMode=value;render();break;case 'photoMode':photoMode=value;render();break;case 'photoFilter':photoFilter=value;render();break;case 'photo':openPhoto(Number(value));break;case 'batchHide':if(!selected.size)return;confirm(T('Hide selected photos?','隐藏选中的照片?'),T('This changes sample visibility only. Real batches must report per-photo success and failure.','仅修改示例可见性。真实批处理需逐项反馈成功和失败。'),()=>{selected.forEach(i=>photoStates[i]='hidden');selected.clear();render();toast(T('Sample photos hidden','示例照片已隐藏'))});break;case 'visibility':{const i=Number(value);confirm(photoStates[i]==='hidden'?T('Publish this photo?','恢复公开这张照片?'):T('Hide this photo?','隐藏这张照片?'),T('Public visibility changes. The original remains stored.','改变公开可见性,原图仍保留在存储中。'),()=>{photoStates[i]=photoStates[i]==='hidden'?'published':'hidden';selected.delete(i);render()});break}case 'deletePhoto':confirm(T('Delete photo?','删除照片?'),T('Soft deletion removes the sample from active management. This does not immediately physically delete OSS files.','软删除会让照片退出有效管理列表,不代表立即物理删除 OSS 文件。'),()=>{photoStates[Number(value)]='deleted';selected.delete(Number(value));render()});break;
|
||
case 'invite':{modal(T('Invite colleague','邀请同事'),`<div class="search" style="margin-bottom:15px">${icon('user')}<input id="inviteSearch" placeholder="${T('Search internal accounts','搜索企业内部账号')}"></div><div id="inviteRows">${people.map((p,i)=>!memberIds.includes(i)?`<button class="side-link candidate" data-action="addMember:${i}">${avatar(p.name)}<span>${p.name}<small class="muted"> · ${p.account}</small></span></button>`:'').join('')}</div><p>${T('No external email or registration invitation is sent.','仅选择内部账号,不发送外部邮件或注册邀请。')}</p>`);$('#inviteSearch').oninput=e=>$$('.candidate').forEach(el=>el.hidden=!el.textContent.toLowerCase().includes(e.target.value.toLowerCase()));break}case 'addMember':memberIds.push(Number(value));closeModal();render();break;case 'removeMember':confirm(T('Remove collaborator?','移除协作者?'),T('The sample account loses collaborator access to this album.','该示例账号将不再作为本相册协作者。'),()=>{memberIds=memberIds.filter(i=>i!==Number(value));render()});break;case 'transfer':modal(T('Transfer album ownership','交接相册'),select(T('New owner','新创建者'),'newOwner',people.slice(1).map((p,i)=>[String(i+1),p.name]),'1')+`<p>${T('Production must recheck the target account and creator permissions.','正式版需再次校验目标账号与创建者权限。')}</p>`,btn(T('Cancel','取消'),'close')+btn(T('Transfer sample','交接示例'),'confirmTransfer','primary'));break;case 'confirmTransfer':{const id=Number($('#newOwner').value);albums[albumIndex].owner=people[id].name;memberIds=[id,...memberIds.filter(i=>i!==id)];closeModal();render();break}
|
||
case 'category':{const i=value==='new'?-1:Number(value);modal(i<0?T('New category','新增分类'):T('Edit category','编辑分类'),`<form id="categoryForm">${field('中文','catZh',i<0?'':cats[i].zh)}${field('English','catEn',i<0?'':cats[i].en)}<button class="btn primary" type="submit">${T('Save category','保存分类')}</button></form>`);$('#categoryForm').onsubmit=e=>{e.preventDefault();if(!$('#catZh').value.trim()||!$('#catEn').value.trim()){toast(T('Complete both sample language fields','请填写示例双语名称'));return}const item={zh:$('#catZh').value,en:$('#catEn').value};if(i<0)cats.push(item);else cats[i]=item;closeModal();render()};break}case 'deleteCategory':if(Number(value)<3)toast(T('This sample category contains photos and cannot be deleted.','该示例分类已有照片,不能删除。'));else confirm(T('Delete empty category?','删除空分类?'),T('Only this empty sample category is removed.','仅移除该空示例分类。'),()=>{cats.splice(Number(value),1);render()});break;case 'regenerate':confirm(T('Regenerate viewing variants?','重新生成浏览变体?'),T('Production creates an asynchronous job. This preview does not modify OSS.','正式版创建异步生成任务,此预览不修改 OSS。'),()=>{wmVersion++;render();toast(T('Sample job created','示例任务已创建'))});break;case 'copyShare':copy($('#shareLink').value);break;case 'viewer':window.open('../viewer-preview-2026-09-22.html','_blank','noopener,noreferrer');break;
|
||
case 'userEdit':{const i=Number(value),p=people[i];modal(T('Edit account','编辑账号'),`<form id="userEditForm">${field(T('Display name','显示名称'),'userName',p.name)}${select(T('Role','角色'),'userRole',[['admin',T('Administrator','管理员')],['staff',T('Staff','员工')]],p.role)}<p>${i===0?T('Your own role cannot be changed here.','不能在这里修改自己的角色。'):T('Source status remains controlled by the company directory.','来源状态仍由企业用户接口控制。')}</p><button type="submit" class="btn primary" style="margin-top:20px">${T('Save','保存')}</button></form>`);$('#userRole').disabled=i===0;$('#userEditForm').onsubmit=e=>{e.preventDefault();p.name=$('#userName').value||p.name;if(i!==0)p.role=$('#userRole').value;closeModal();render()};break}case 'resetPassword':{const p=people[Number(value)];modal(T('Reset password','重置密码'),`<p style="margin-bottom:16px">${p.name} · ${p.account}</p><form id="resetForm">${field(T('Temporary password (demo only)','临时密码(仅演示)'),'tempPassword','','password')}<p>${T('Do not enter real credentials. Nothing is stored or sent.','请勿输入真实凭据,此处不保存或发送密码。')}</p><button class="btn primary" type="submit" style="margin-top:20px">${T('Confirm reset preview','确认重置演示')}</button></form>`);$('#resetForm').onsubmit=e=>{e.preventDefault();closeModal();toast(T('Password reset flow previewed. No credentials changed.','已演示密码重置流程,未修改真实凭据。'))};break}case 'userState':{const i=Number(value);if(i===0)return;const p=people[i];confirm(T('Change local account state?','修改本地账号状态?'),T('The company directory state remains unchanged.','来源接口状态不会随本地治理动作改变。'),()=>{if(p.credential==='pending')p.credential='active';else p.locked=!p.locked;render()});break}case 'resetUserFilters':$('#userSearch').value='';$('#userStatus').value='all';$('#usersTable').innerHTML=usersTable();break;case 'sync':if(syncRunning)return;confirm(T('Run a sample pull?','模拟一次用户补拉?'),T('This is a local animation; no company directory is contacted.','仅在本地演示运行状态,不请求企业用户接口。'),()=>{syncRunning=true;render();setTimeout(()=>{syncRunning=false;if([11,20].includes(page))render();toast(T('Sample pull completed','模拟补拉已完成'))},1800)});break;case 'syncDetail':syncDetail(Number(value));break;case 'thresholds':modal(T('Alert thresholds','告警阈值'),`<form id="thresholdForm">${field(T('Total storage threshold (GB)','总存储阈值(GB)'),'totalThreshold','200','number')}${field(T('Per-album threshold (GB)','单相册阈值(GB)'),'albumThreshold','50','number')}<p>${T('Thresholds trigger alerts, not automatic deletion.','达到阈值仅触发告警,不自动删除照片。')}</p><button class="btn primary" style="margin-top:20px" type="submit">${T('Save sample thresholds','保存示例阈值')}</button></form>`);$('#thresholdForm').onsubmit=e=>{e.preventDefault();const value=Number($('#totalThreshold').value);if(value<0){toast(T('Use a non-negative number','请输入非负数字'));return}closeModal();if($('#thresholdValue'))$('#thresholdValue').textContent=value+' GB';toast(T('Sample threshold updated','示例阈值已更新'))};break;
|
||
case 'newSlot':slots.push({name:'New widget',zh:'新展示位',album:null,enabled:false,mode:'v2',version:1,speed:16});slotIndex=slots.length-1;go(14);break;case 'slot':slotIndex=Number(value);go(14);break;case 'bind':{slotIndex=Number(value);modal(T('Bind album','绑定相册'),select(T('Album','相册'),'bindAlbum',[['none',T('Unbind','解除绑定')],...albums.map((a,i)=>[String(i),lang==='zh'?a.zh:a.title])],slots[slotIndex].album===null?'none':String(slots[slotIndex].album))+`<p>${T('The same embedded slot will display the newly bound album after configuration refresh.','配置刷新后,使用此展示位的网站将展示新绑定的相册。')}</p>`,btn(T('Cancel','取消'),'close')+btn(T('Confirm binding','确认绑定'),'confirmBind','primary'));break}case 'confirmBind':slots[slotIndex].album=$('#bindAlbum').value==='none'?null:Number($('#bindAlbum').value);slots[slotIndex].version++;closeModal();render();break;case 'code':{const s=slots[Number(value)];const code='<script src="https://example.com/embed/'+s.mode+'/loader.js" data-slot="DEMO_SLOT" defer><'+'/script>';modal(T('Embed code','嵌入代码'),`<p>${T('Illustrative integration code. Replace example.com and DEMO_SLOT with real deployment values.','接入格式示例,须替换 example.com 和 DEMO_SLOT 为真实部署信息。')}</p><pre class="mono" id="embedCode">${esc(code)}</pre>`,btn(T('Copy sample code','复制示例代码'),'copyCode','primary'));break}case 'copyCode':copy($('#embedCode').textContent);break;case 'collapseEmbed':$('#embedded').hidden=true;$('#expandEmbed').hidden=false;break;case 'expandEmbed':$('#embedded').hidden=false;$('#expandEmbed').hidden=true;break;case 'embedHeart':if(!$('#slotReactions').checked)return;$('#embedded').hidden=false;$('#expandEmbed').hidden=true;$('#embedHearts').classList.remove('play');void $('#embedHearts').offsetWidth;$('#embedHearts').classList.add('play');break;case 'widgetDemo':window.open('../embed-preview-2026-09-22.html','_blank','noopener,noreferrer');break;case 'banner':modal(T('Choose sample banner','选择示例宣传图'),`<div class="album-grid">${[0,3,5].map(i=>`<button class="album-cover" data-action="setBanner:${i}">${photo(i)}</button>`).join('')}</div><p style="margin-top:20px">${T('No files are uploaded in this prototype.','此原型不会上传文件。')}</p>`,'',true);break;case 'setBanner':albums[albumIndex].photo=Number(value);closeModal();render();break}}
|
||
function copy(text){if(navigator.clipboard)navigator.clipboard.writeText(text).then(()=>toast(T('Copied','已复制'))).catch(()=>prompt(T('Copy this text','复制以下内容'),text));else prompt(T('Copy this text','复制以下内容'),text)}
|
||
document.addEventListener('click',e=>{const b=e.target.closest('[data-action]');if(b&&!b.disabled)action(b.dataset.action)});document.addEventListener('keydown',e=>{const modalEl=$('.modal');if(!modalEl)return;if(e.key==='Escape')closeModal();if(e.key==='Tab'){const focus=[...modalEl.querySelectorAll('button,input,select,textarea,a')].filter(el=>!el.disabled);if(e.shiftKey&&document.activeElement===focus[0]){e.preventDefault();focus.at(-1)?.focus()}else if(!e.shiftKey&&document.activeElement===focus.at(-1)){e.preventDefault();focus[0]?.focus()}}});window.addEventListener('hashchange',render);render();
|