const APP_BASE = new URL("../", document.currentScript.src).pathname; const tabs = {news:'News · 新闻',case:'Cases · 案例',product:'Products · 产品',service:'Services · 服务',about:'About · 关于我们',contact:'联系团队',search:'搜索资料'}; const stage = document.querySelector('#stage'); const contentWindow = document.querySelector('#content-window'); const contentTitle = document.querySelector('#content-title'); const contentBody = document.querySelector('#content-body'); const detailTitle = document.querySelector('#detail-title'); const detailBody = document.querySelector('#detail-body'); const detailOpenLink = document.querySelector('#detail-open-link'); const detailCaption = document.querySelector('#detail-caption'); const contentReader = document.documentElement.classList.contains('content-reader'); const brandLogo = document.querySelector('.brand-logo'); if(brandLogo){ const showBrandFallback=()=>{brandLogo.hidden=true;document.querySelector('.brand-fallback').hidden=false}; brandLogo.addEventListener('error',showBrandFallback,{once:true}); if(brandLogo.complete&&!brandLogo.naturalWidth)showBrandFallback(); } const messages = document.querySelector('#chat-messages'); const chatHistoryPanel = document.querySelector('#chat-history'); const chatSizeToggle = document.querySelector('#chat-size-toggle'); function setChatExpanded(expanded){ const top=messages.scrollTop; const atBottom=messages.scrollHeight-messages.clientHeight-top<=8; chatHistoryPanel.classList.toggle('is-expanded',expanded); chatSizeToggle.setAttribute('aria-expanded',String(expanded)); chatSizeToggle.setAttribute('aria-label',expanded?'还原聊天记录':'放大聊天记录'); chatSizeToggle.title=expanded?'还原聊天记录(Escape)':'放大聊天记录'; chatSizeToggle.querySelector('.chat-size-label').textContent=expanded?'还原':'放大'; chatSizeToggle.querySelector('.chat-expand-icon').toggleAttribute('hidden',expanded); chatSizeToggle.querySelector('.chat-restore-icon').toggleAttribute('hidden',!expanded); // Keep the reading offset, or follow the latest message when already at the end. messages.scrollTop=atBottom?messages.scrollHeight:top; } chatSizeToggle.addEventListener('click',()=>setChatExpanded(!chatHistoryPanel.classList.contains('is-expanded'))); document.addEventListener('keydown',event=>{ if(event.key!=='Escape'||event.defaultPrevented||chatHistoryPanel.hidden||!chatHistoryPanel.classList.contains('is-expanded'))return; setChatExpanded(false); event.preventDefault(); event.stopPropagation(); },true); const dock = document.querySelector('#dock'); const dockMenu = document.querySelector('#dock-menu'); const chatForm = document.querySelector('#chat-form'); const dockToggle = document.querySelector('#dock-toggle'); const figure = document.querySelector('#figure'); const hostImage = document.querySelector('#host-image'); const frames = {idle:APP_BASE+'static/assets/vrfab-digital-host-v2.webp',listening:APP_BASE+'static/assets/vrfab-digital-host-listening-v1.webp',thinking:APP_BASE+'static/assets/vrfab-digital-host-thinking-v1.webp',explaining:APP_BASE+'static/assets/vrfab-digital-host-explaining-v1.webp'}; const today = () => new Intl.DateTimeFormat('sv-SE',{timeZone:'Asia/Shanghai'}).format(new Date()); const state = {tab:'news',contextId:null,request:0,detailRequest:0,selectedSlug:null,date:today(),month:today().slice(0,7),page:1,days:[],calendarRequest:0,newsRequest:0,perspective:true,panelsVisible:true,bannerTimer:null,aboutController:null,aboutSection:'intro',serviceController:null,recommendationRequest:0}; const publicationToken=()=>crypto.getRandomValues(new Uint32Array(4)).join('-'); function requestPublicationLayout(frame){ frame.contentWindow?.postMessage({type:'zhiwei-publication-layout',token:frame.dataset.layoutToken},'*'); } function clearPublicationTimers(frame){ clearTimeout(frame.layoutTimeout);clearTimeout(frame.probeTimeout);delete frame.layoutProbe; } function showPublicationFallback(frame){ if(!frame.isConnected)return; clearPublicationTimers(frame);frame.style.removeProperty('height');delete frame.dataset.autoHeight; frame.removeAttribute('scrolling');frame.dataset.layoutState='fallback'; frame.removeAttribute('aria-hidden');frame.removeAttribute('tabindex'); frame.closest('.html-publication-preview')?.querySelector('.html-publication-loading')?.setAttribute('hidden',''); } function publicationPreview(item){ const isCase=item.kind==='case'; const preview=node('div','html-publication-preview'); const loading=node('p','html-publication-loading',isCase?'正在加载案例…':'正在加载文章…');loading.setAttribute('role','status'); const frame=node('iframe','html-publication-frame');frame.name='zhiwei-publication';frame.title=item.title+(isCase?' · 案例网页':' · Blog 文章'); frame.dataset.layoutToken=publicationToken();frame.dataset.layoutState='loading'; frame.setAttribute('aria-hidden','true');frame.tabIndex=-1; frame.setAttribute('sandbox','allow-scripts allow-downloads allow-popups'); frame.addEventListener('load',()=>{ // Resource completion only checks the current document. It must not undo // sizing that already succeeded while images or video were still loading. clearTimeout(frame.probeTimeout);const probe=publicationToken();frame.layoutProbe=probe; frame.probeTimeout=setTimeout(()=>{if(frame.layoutProbe===probe)showPublicationFallback(frame)},700); frame.contentWindow?.postMessage({type:'zhiwei-publication-probe',probe},'*'); requestPublicationLayout(frame); }); frame.src=APP_BASE+item.html_package.url; // A slow network response is still loading. Once the body announces itself, // the shorter height-confirmation deadline below can detect a broken bridge. frame.layoutTimeout=setTimeout(()=>{if(frame.dataset.autoHeight!=='true')showPublicationFallback(frame)},15000); preview.append(loading,frame); const link=node('a','text-link',isCase?'打开完整案例 ↗':'打开完整文章 ↗');link.href=frame.src;link.target='_blank';link.rel='noopener'; return [link,preview]; } window.addEventListener('message',event=>{ const frame=detailBody.querySelector('.html-publication-frame'); if(!frame||event.source!==frame.contentWindow)return; const data=event.data; if(data?.type==='zhiwei-publication-ready'&&typeof data.documentId==='string'&&data.documentId.length<=200){ if(data.probe===frame.layoutProbe){clearTimeout(frame.probeTimeout);delete frame.layoutProbe;} if(frame.dataset.layoutDocument!==data.documentId){ frame.dataset.layoutDocument=data.documentId;frame.dataset.layoutToken=publicationToken(); frame.style.removeProperty('height');delete frame.dataset.autoHeight;frame.removeAttribute('scrolling'); frame.dataset.layoutState='loading';frame.setAttribute('aria-hidden','true');frame.tabIndex=-1; frame.closest('.html-publication-preview')?.querySelector('.html-publication-loading')?.removeAttribute('hidden'); clearTimeout(frame.layoutTimeout);frame.layoutTimeout=setTimeout(()=>{if(frame.dataset.autoHeight!=='true')showPublicationFallback(frame)},4000); } requestPublicationLayout(frame);return; } if(data?.token!=null&&data.token!==frame.dataset.layoutToken)return; if(data?.documentId!=null&&data.documentId!==frame.dataset.layoutDocument)return; if(data?.type==='zhiwei-publication-height'&&Number.isFinite(data.height)&&data.height>0&&data.height<=1000000){ // A fullscreen demo has no intrinsic document height. Its first report // may still use the preceding article's viewport before resize commits. const viewportHeight=frame.clientHeight||Math.max(1,(parseFloat(frame.style.height)||280)-2); const height=data.viewportLayout===true?viewportHeight:Math.ceil(data.height); frame.style.height=(height+2)+'px';frame.dataset.autoHeight='true';frame.setAttribute('scrolling','no'); clearPublicationTimers(frame);frame.dataset.layoutState='ready';frame.removeAttribute('aria-hidden');frame.removeAttribute('tabindex'); frame.closest('.html-publication-preview')?.querySelector('.html-publication-loading')?.setAttribute('hidden',''); frame.contentWindow.postMessage({type:'zhiwei-publication-sized',token:frame.dataset.layoutToken},'*'); }else if(data?.type==='zhiwei-publication-anchor'&&Number.isFinite(data.top)&&data.top>=0&&data.top<=frame.clientHeight&&state.panelsVisible){ detailBody.scrollTop=frame.offsetTop+data.top; } }); function node(tag,className='',text='') {const el=document.createElement(tag);if(className)el.className=className;if(text)el.textContent=text;return el;} function clear(el){el.replaceChildren();if(el===detailBody)updateDetailLink();} function updateDetailLink(item=null,options={}){ detailOpenLink.hidden=!item;detailCaption.hidden=Boolean(item); if(!item){detailOpenLink.removeAttribute('href');detailOpenLink.removeAttribute('title');return;} const url=new URL(APP_BASE,location.origin); url.searchParams.set('tab',item.kind==='company'?'about':item.kind);url.searchParams.set('content',item.slug); if(item.kind==='news'&&validNewsDate(item.news_date))url.searchParams.set('date',item.news_date); if(item.kind==='company')url.searchParams.set('about',state.aboutSection); for(const key of ['asset','t','page'])if(options[key]!=null)url.searchParams.set(key,options[key]); if(!contentReader)url.searchParams.set('view','content'); detailOpenLink.href=!contentReader&&item.html_package?APP_BASE+item.html_package.url:url.href; detailOpenLink.target=contentReader?'_self':'_blank'; detailOpenLink.textContent=contentReader?'返回知微':'新窗口查看 ↗'; detailOpenLink.title=(contentReader?'返回知微查看:':'在新标签页查看:')+item.title; } function showError(el,error){clear(el);el.append(node('p','feedback',error.message||'加载失败,请稍后重试。'));} async function api(url,options){const response=await fetch(url,options);let data;try{data=await response.json()}catch{data={detail:'服务暂不可用'}}if(!response.ok)throw new Error(typeof data.detail==='string'?data.detail:'服务暂不可用');return data;} function trackVisit(event_type,values={}){ if(navigator.doNotTrack==='1'||navigator.globalPrivacyControl===true)return; try{const body=JSON.stringify({event_id:crypto.randomUUID(),event_type,...values}); if(navigator.sendBeacon?.(APP_BASE+'api/analytics/events',new Blob([body],{type:'application/json'})))return; fetch(APP_BASE+'api/analytics/events',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json'},body,keepalive:true}).catch(()=>{}); }catch{/* Statistics must not interrupt browsing. */} } function setHost(mode){figure.dataset.state=mode;hostImage.src=frames[mode]||frames.idle;} function activateDock(tab){document.querySelectorAll('.dock [data-tab]').forEach(button=>button.setAttribute('aria-pressed',String(button.dataset.tab===tab)));} function heading(text){clear(contentTitle);contentTitle.append(node('span','pink-dot'),document.createTextNode(text));} function smallButton(label,callback){const button=node('button','text-link',label);button.type='button';button.addEventListener('click',callback);return button;} function contentCard(item){const card=node('article','content-card');card.dataset.slug=item.slug;card.classList.toggle('active',state.selectedSlug===item.slug);if(item.cover_asset_id||item.package_cover){const img=node('img','card-cover');img.src=item.cover_asset_id?APP_BASE+'api/media/'+item.cover_asset_id:APP_BASE+'publications/'+encodeURIComponent(item.slug)+'/'+item.package_cover.split('/').map(encodeURIComponent).join('/');img.alt=item.title;img.loading='lazy';img.addEventListener('error',()=>img.remove(),{once:true});card.append(img)}card.append(node('span','kicker',item.category||tabs[item.kind]||'内容'),node('h3','',item.title),node('p','',item.summary||'点击查看详细介绍'));if(item.html_package_id){const link=node('a','text-link','阅读文章 ↗');link.href=APP_BASE+'publications/'+encodeURIComponent(item.slug)+'/';link.addEventListener('click',event=>{if(event.ctrlKey||event.metaKey||event.shiftKey||event.altKey)return;event.preventDefault();openDetail(item.slug)});card.append(link)}else card.append(smallButton('查看详情 ↗',()=>openDetail(item.slug)));return card;} function videoCard(item){const card=node('article','video-card');if(item.thumbnail_url){const img=node('img','card-cover');img.src=item.thumbnail_url;img.alt='';img.loading='lazy';card.append(img)}const time=Math.floor(item.start_time/60)+':'+String(Math.floor(item.start_time%60)).padStart(2,'0');card.append(node('span','kicker','展览视频 · '+time),node('h3','',item.title),node('p','',item.summary||'打开视频查看对应片段'));const link=node('a','text-link','观看片段 ↗');link.href=item.url;link.target='_blank';link.rel='noopener';card.append(link);return card;} function writeUrl(values){const u=new URL(location.href);for(const [key,value] of Object.entries(values)){if(value==null)u.searchParams.delete(key);else u.searchParams.set(key,String(value))}history.pushState(null,'',u);} function resetDetail(text='选择左侧内容,在这里查看详情。'){ state.detailRequest++;state.contextId=null;state.selectedSlug=null; detailTitle.textContent=state.tab==='news'?'News · 新闻详情':state.tab==='case'?'Cases · 案例介绍':state.tab==='product'?'Products · 产品介绍':'内容详情'; clear(detailBody);detailBody.setAttribute('aria-busy','false');const empty=node('div','detail-placeholder'); if(state.tab==='case'||state.tab==='product')empty.append(caseIcon(state.tab==='product'?'enterprise':'space')); else empty.append(node('span','detail-symbol','▤')); empty.append(node('h3','',state.tab==='news'?'从日历中发现新动态':state.tab==='case'?'每一个案例,都是一次探索':state.tab==='product'?'找到适合你的数字体验产品':'了解更多'),node('p','',text));detailBody.append(empty); } function validNewsDate(value){ if(!/^\d{4}-\d{2}-\d{2}$/.test(value||''))return false; const parsed=new Date(value+'T00:00:00Z');return !Number.isNaN(parsed.valueOf())&&parsed.toISOString().slice(0,10)===value; } function calendar(){ const [year,month]=state.month.split('-').map(Number); const start=(new Date(year,month-1,1).getDay()+6)%7; const length=new Date(year,month,0).getDate(); const wrap=node('div','calendar');const top=node('div','calendar-title'); const controls=node('div','calendar-controls'); for(const [label,offset] of [['上一月',-1],['下一月',1]]){ const button=smallButton(offset<0?'‹':'›',()=>changeMonth(offset));button.setAttribute('aria-label',label);controls.append(button); } const now=smallButton('今天',()=>selectNewsDate(today(),true));controls.append(now); top.append(node('strong','',year+' / '+String(month).padStart(2,'0')),controls);wrap.append(top); const grid=node('div','calendar-grid'); for(const day of ['一','二','三','四','五','六','日'])grid.append(node('span','weekday',day)); for(let i=0;i[item.date,item.count])); for(let day=1;day<=length;day++){ const value=state.month+'-'+String(day).padStart(2,'0');const count=counts.get(value)||0; const button=node('button','calendar-day'+(value===today()?' is-today':'')+(value===state.date?' selected':'')+(count?' has-news':''));button.type='button';button.dataset.date=value; button.setAttribute('aria-pressed',String(value===state.date));button.setAttribute('aria-label',`${value},${count} 篇新闻`);button.title=count?`${count} 篇新闻`:'暂无新闻'; button.append(node('span','',String(day)));if(count)button.append(node('i','news-dot')); button.addEventListener('click',()=>selectNewsDate(value));grid.append(button); } // Reserve six weeks so month changes never move the banner or news list. for(let i=start+length;i<42;i++){const blank=node('span','calendar-blank');blank.setAttribute('aria-hidden','true');grid.append(blank);} wrap.append(grid,node('p','calendar-legend','● 有新闻 · 点击日期查看当天动态'));return wrap; } function redrawCalendar(){const old=contentBody.querySelector('.calendar');if(old)old.replaceWith(calendar());} async function loadCalendar(){ const token=++state.calendarRequest;const month=state.month; try{const data=await api(APP_BASE+'api/news/calendar?month='+month);if(token!==state.calendarRequest||state.tab!=='news')return;state.days=data.days;redrawCalendar()} catch(error){if(token===state.calendarRequest&&state.tab==='news'){const legend=contentBody.querySelector('.calendar-legend');if(legend){legend.textContent='日历暂时无法加载,请稍后重试。';legend.title=error.message;}}} } async function changeMonth(offset){ const [year,month]=state.month.split('-').map(Number);const next=new Date(year,month-1+offset,1); if(next.getFullYear()<1900||next.getFullYear()>9998)return; state.month=next.getFullYear()+'-'+String(next.getMonth()+1).padStart(2,'0');state.days=[]; await selectNewsDate(state.month+'-01',true); } async function selectNewsDate(value,reloadMonth=false){ navigation++; const differentMonth=state.month!==value.slice(0,7);state.date=value;state.page=1; if(differentMonth){state.month=value.slice(0,7);state.days=[];} resetDetail('选择 '+value+' 的一篇新闻,在这里阅读完整内容。');redrawCalendar(); writeUrl({tab:'news',date:value,content:null,asset:null,t:null,page:null}); await Promise.all([loadNewsList(),differentMonth||reloadMonth?loadCalendar():Promise.resolve()]); } async function loadNewsList({autoSelect=false}={}){ const navToken=navigation;const token=++state.newsRequest;const target=contentBody.querySelector('#news-list');if(!target)return; newsListPlaceholder(target,'正在读取当天新闻…','新闻加载后会显示在这里。');target.setAttribute('aria-busy','true'); if(autoSelect){clear(detailBody);detailBody.append(node('p','muted','正在打开新闻…'));detailBody.setAttribute('aria-busy','true');} try{ const data=await api(APP_BASE+'api/news?date='+state.date+'&page='+state.page+(autoSelect?'&auto_select=true':'')); if(token!==state.newsRequest||state.tab!=='news'||(autoSelect&&navToken!==navigation))return; if(autoSelect){ state.date=data.date;state.page=data.page; if(state.month!==data.date.slice(0,7)){state.month=data.date.slice(0,7);state.days=[];loadCalendar()} redrawCalendar(); } clear(target);const header=node('div','news-list-heading');header.append(node('h3','',state.date.replaceAll('-',' / ')),node('span','',data.total+' 篇新闻'));target.append(header); if(!data.items.length){const empty=node('div','empty-card news-empty');empty.append(node('strong','','这一天,静待新消息'),node('p','muted','当天还没有已发布新闻。试试日历中带圆点的日期。'));target.append(empty)} for(const [index,item] of data.items.entries()){ const card=node('button','news-list-item'+(state.selectedSlug===item.slug?' active':''));card.type='button';card.dataset.slug=item.slug;card.setAttribute('aria-pressed',String(state.selectedSlug===item.slug)); const number=node('span','news-number',String((data.page-1)*20+index+1).padStart(2,'0'));const copy=node('div','news-copy'); copy.append(node('span','kicker',item.category||'虚拟工场动态'),node('strong','',item.title));if(item.summary)copy.append(node('p','',item.summary)); card.append(number,copy,node('span','news-arrow','↗'));card.addEventListener('click',()=>openDetail(item.slug));target.append(card); } if(data.pages>1){const pager=node('div','news-pager');for(const [label,step] of [['上一页',-1],['下一页',1]]){const button=smallButton(label,()=>{state.page+=step;loadNewsList()});button.disabled=step<0?data.page===1:data.page===data.pages;pager.append(button)}pager.insertBefore(node('span','',`${data.page} / ${data.pages}`),pager.lastChild);target.append(pager)} if(autoSelect){ if(data.selected_slug)await openDetail(data.selected_slug,{updateHistory:false,focus:false}); else resetDetail('目前还没有可展示的已发布新闻,请稍后再来。'); } }catch(error){ if(token===state.newsRequest&&state.tab==='news'&&(!autoSelect||navToken===navigation)){ newsListPlaceholder(target,'新闻暂时无法加载',error.message||'请稍后重新选择日期。'); if(autoSelect){showError(detailBody,error);detailBody.append(smallButton('重新加载新闻',()=>loadNewsList({autoSelect:true})))} } }finally{ if(token===state.newsRequest){ target.setAttribute('aria-busy','false'); if(autoSelect&&navToken===navigation)detailBody.setAttribute('aria-busy','false'); } } } function newsListPlaceholder(target,title,message){ clear(target);const header=node('div','news-list-heading');header.append(node('h3','',state.date.replaceAll('-',' / ')),node('span','','— 篇新闻')); const empty=node('div','empty-card news-empty');empty.append(node('strong','',title),node('p','muted',message));target.append(header,empty); } function newsBannerPlaceholder(target,message='每一天,都有新发现'){ clear(target);const carousel=node('div','banner-carousel');const slides=node('div','banner-slides');const art=node('div','news-art');art.append(node('strong','','VRFAB NEWS'),node('span','',message));slides.append(art);carousel.append(slides);target.append(carousel); } async function renderNews(requestId,options={}){ contentBody.append(calendar());const banner=node('div','column-banner-slot');newsBannerPlaceholder(banner);contentBody.append(banner); const list=node('div');list.id='news-list';contentBody.append(list); await Promise.all([loadCalendar(),loadNewsList({autoSelect:!options.content&&!validNewsDate(options.date)}),renderBanners('news',banner,requestId)]); } async function renderBanners(column,target,requestId,publishedItems=null){ clearInterval(state.bannerTimer);state.bannerTimer=null; try{ const data=publishedItems?{items:publishedItems}:await api(APP_BASE+'api/banners?column='+column);if(requestId!==state.request)return; if(!data.items.length){if(column==='news')newsBannerPlaceholder(target);return} const carousel=node('section','banner-carousel');carousel.setAttribute('aria-label','栏目 Banner 轮播'); const slides=node('div','banner-slides');const dots=node('div','banner-dots');let active=0;let paused=false; data.items.forEach((item,index)=>{ const slide=node(item.content_slug?'button':'div','banner-slide');if(item.content_slug){slide.type='button';slide.addEventListener('click',()=>openDetail(item.content_slug))} const img=node('img');img.src=APP_BASE+'api/banners/'+item.id+'/image';img.alt=item.title; img.addEventListener('error',()=>{img.remove();const art=node('div','news-art');art.append(node('strong','','VRFAB'),node('span','','图片暂时无法加载'));slide.append(art)},{once:true}); slide.append(img);slides.append(slide); const dot=node('button','banner-dot');dot.type='button';dot.setAttribute('aria-label','查看第 '+(index+1)+' 张 Banner');dot.addEventListener('click',()=>show(index));dots.append(dot); }); function show(index){active=(index+data.items.length)%data.items.length;[...slides.children].forEach((slide,i)=>{slide.hidden=i!==active;slide.inert=i!==active});[...dots.children].forEach((dot,i)=>dot.setAttribute('aria-pressed',String(i===active)))} carousel.append(slides); if(data.items.length>1){ const bar=node('div','banner-bar');const prev=smallButton('‹',()=>show(active-1));prev.setAttribute('aria-label','上一张 Banner');const next=smallButton('›',()=>show(active+1));next.setAttribute('aria-label','下一张 Banner'); const pause=smallButton('暂停',()=>{paused=!paused;pause.textContent=paused?'播放':'暂停';pause.setAttribute('aria-pressed',String(paused))});pause.setAttribute('aria-label','暂停或播放 Banner 轮播');pause.setAttribute('aria-pressed','false');bar.append(prev,dots,next,pause);carousel.append(bar); if(!matchMedia('(prefers-reduced-motion: reduce)').matches)state.bannerTimer=setInterval(()=>{if(!paused&&state.panelsVisible&&!document.hidden&&!carousel.matches(':hover')&&!carousel.contains(document.activeElement))show(active+1)},5000); } clear(target);target.append(carousel);show(0); }catch{if(requestId===state.request&&column==='news')newsBannerPlaceholder(target,'Banner 暂时无法加载。')} } const caseIconPaths={ 'digital-human':["M21 10a7 7 0 1 0 0 14 7 7 0 0 0 0-14Z","M6 43v-3a15 15 0 0 1 30 0v3H6Z","M38 6l2.3 6.7L47 15l-6.7 2.3L38 24l-2.3-6.7L29 15l6.7-2.3Z"], space:['M24 5 43 16v22L24 49 5 38V16Z','M5 16 24 27l19-11M24 27v22M14 11l19 11v22'], interactive:['M19 7a8 8 0 1 0 0 16','m21 17 19 8-9 4-4 10Z','m32 33 7 10','M8 7 5 4M6 16H2M12 4V1'], education:['M24 15C16 9 8 9 3 12v28c6-3 14-2 21 3 7-5 15-6 21-3V12c-5-3-13-3-21 3Z','M24 15v28M9 18l9 3M9 26l9 3M30 21l9-3M30 29l9-3'], science:['M43 25c0 10-8 18-18 18S7 35 7 25 15 7 25 7s18 8 18 18Z','M7 25h36M25 7c12 11 12 25 0 36-12-11-12-25 0-36ZM12 13h26M12 37h26'], enterprise:['M9 44V9h29v35M4 44h39','M16 17h4M27 17h4M16 25h4M27 25h4M16 33h4M27 33h4M21 44v-6h6v6'], travel:['M44 25a19 19 0 1 1-38 0 19 19 0 0 1 38 0Z','m33 17-5 11-11 5 5-11Z','M25 6V2M25 48v-4M6 25H2M48 25h-4'] }; function caseIcon(key='space'){ const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');svg.setAttribute('viewBox','0 0 50 50');svg.setAttribute('aria-hidden','true');svg.setAttribute('focusable','false');svg.classList.add('case-icon'); for(const d of caseIconPaths[key]||caseIconPaths.space){const path=document.createElementNS(svg.namespaceURI,'path');path.setAttribute('d',d);path.setAttribute('fill','none');path.setAttribute('stroke','currentColor');path.setAttribute('stroke-width','1.6');path.setAttribute('stroke-linecap','round');path.setAttribute('stroke-linejoin','round');svg.append(path)} return svg; } function caseTile(item){ const card=node('button','case-tile');card.type='button';card.dataset.slug=item.slug;card.setAttribute('aria-pressed',String(state.selectedSlug===item.slug));card.classList.toggle('active',state.selectedSlug===item.slug);card.setAttribute('aria-label','查看案例:'+item.title); const iconKey=Object.hasOwn(caseIconPaths,item.attributes?.case_icon)?item.attributes.case_icon:'space'; const thumb=node('div','case-thumb');thumb.dataset.icon=iconKey;thumb.append(caseIcon(iconKey)); const packageCover=item.package_cover||item.html_package?.metadata?.cover_path; if(item.cover_asset_id||packageCover){const img=node('img','case-thumbnail');img.src=item.cover_asset_id?APP_BASE+'api/media/'+item.cover_asset_id:APP_BASE+'publications/'+encodeURIComponent(item.slug)+'/'+packageCover.split('/').map(encodeURIComponent).join('/');img.alt='';img.loading='lazy';img.addEventListener('error',()=>{img.remove();thumb.classList.remove('has-image')},{once:true});thumb.append(img);thumb.classList.add('has-image')} const copy=node('div','case-tile-copy');copy.append(node('span','kicker',item.category||'精选案例'),node('strong','case-tile-title',item.title)); const arrow=node('span','case-tile-arrow','↗');arrow.setAttribute('aria-hidden','true');copy.append(arrow);card.append(thumb,copy);card.addEventListener('click',()=>openDetail(item.slug));return card; } function updateCaseCount(grid){ const count=grid.closest('.case-browser')?.querySelector('.case-count');if(!count)return; const number=grid.querySelectorAll('.case-tile').length;count.textContent=grid.dataset.complete==='true'?number+' 个案例':'已加载 '+number+' 个'; } async function renderCases(requestId){ const section=node('section','case-browser');section.setAttribute('aria-label','案例列表'); const header=node('div','case-list-heading');const count=node('span','case-count','正在加载…');header.append(node('strong','','探索案例'),count); const hint=node('p','case-list-hint','选择案例,在右侧查看完整介绍');const grid=node('div','case-grid');grid.id='case-grid'; const feedback=node('p','feedback');feedback.setAttribute('role','status'); const more=smallButton('加载更多案例',()=>loadPage());more.classList.add('case-load-more');more.hidden=true; section.append(header,hint,grid,feedback,more);contentBody.append(section); let page=1;let busy=false;let complete=false;const seen=new Set(); async function loadPage(){ if(busy||complete||requestId!==state.request)return;busy=true;more.disabled=true;more.textContent='正在加载…';feedback.textContent='';grid.setAttribute('aria-busy','true'); try{ const data=await api(APP_BASE+'api/contents?kind=case&limit=12&page='+page);if(requestId!==state.request)return; for(const item of data.items){if(!seen.has(item.slug)){seen.add(item.slug);if(![...grid.children].some(card=>card.dataset.slug===item.slug))grid.append(caseTile(item))}} page++;complete=data.items.length<12;grid.dataset.complete=String(complete);more.hidden=complete;more.textContent='加载更多案例';updateCaseCount(grid); if(!seen.size){const empty=node('div','case-empty');empty.append(caseIcon('space'),node('strong','','新的案例,即将与你见面'),node('p','','案例发布后,会以图片或图标卡片展示在这里。'));grid.append(empty);hint.hidden=true} }catch(error){if(requestId!==state.request)return;feedback.textContent=error.message;count.textContent=seen.size?'已加载 '+seen.size+' 个':'暂未加载';more.hidden=false;more.textContent='重新加载案例'} finally{busy=false;more.disabled=false;grid.setAttribute('aria-busy','false')} } await loadPage(); } function productTile(item){ const card=node('button','case-tile product-tile');card.type='button';card.dataset.slug=item.slug;card.setAttribute('aria-pressed',String(state.selectedSlug===item.slug));card.classList.toggle('active',state.selectedSlug===item.slug);card.setAttribute('aria-label','查看产品:'+item.title); const thumb=node('div','case-thumb product-thumb');thumb.append(caseIcon('enterprise')); const cover=item.product_cover||(item.cover_asset_id?'api/media/'+item.cover_asset_id:null); if(cover){const image=node('img','case-thumbnail product-thumbnail');image.src=APP_BASE+cover;image.alt='';image.loading='lazy';image.addEventListener('error',()=>{image.remove();thumb.classList.remove('has-image')},{once:true});thumb.append(image);thumb.classList.add('has-image')} const copy=node('div','case-tile-copy');copy.append(node('span','kicker',item.category||'数字体验产品'),node('strong','case-tile-title',item.title));const arrow=node('span','case-tile-arrow','↗');arrow.setAttribute('aria-hidden','true');copy.append(arrow);card.append(thumb,copy);card.addEventListener('click',()=>openDetail(item.slug));return card; } function updateProductCount(grid){const count=grid.closest('.product-browser')?.querySelector('.product-count');if(count){const total=grid.querySelectorAll('.product-tile').length;count.textContent=grid.dataset.complete==='true'?total+' 个产品':'已加载 '+total+' 个'}} async function renderProducts(requestId){ const section=node('section','case-browser product-browser');section.setAttribute('aria-label','产品列表');const header=node('div','case-list-heading');const count=node('span','case-count product-count','正在加载…');header.append(node('strong','','探索产品'),count); const hint=node('p','case-list-hint','选择一个产品,了解构成、特点与参数');const grid=node('div','case-grid product-grid');grid.id='product-grid';const feedback=node('p','feedback');feedback.setAttribute('role','status');const more=smallButton('加载更多产品',()=>loadPage());more.classList.add('case-load-more');more.hidden=true;section.append(header,hint,grid,feedback,more);contentBody.append(section); let page=1,busy=false,complete=false;const seen=new Set(); async function loadPage(){ if(busy||complete||requestId!==state.request)return;busy=true;more.disabled=true;more.textContent='正在加载…';feedback.textContent='';grid.setAttribute('aria-busy','true'); try{const data=await api(APP_BASE+'api/contents?kind=product&limit=12&page='+page);if(requestId!==state.request)return; for(const item of data.items){seen.add(item.slug);if(![...grid.children].some(card=>card.dataset.slug===item.slug))grid.append(productTile(item))} page++;complete=data.items.length<12;grid.dataset.complete=String(complete);more.hidden=complete;more.textContent='加载更多产品';updateProductCount(grid); if(!seen.size){const empty=node('div','case-empty product-empty');empty.append(caseIcon('enterprise'),node('strong','','新的产品,即将与你见面'),node('p','','产品发布后,会以图片卡片展示在这里。'));grid.append(empty);hint.hidden=true} }catch(error){if(requestId!==state.request)return;feedback.textContent=error.message;count.textContent=seen.size?'已加载 '+seen.size+' 个':'暂未加载';more.hidden=false;more.textContent='重新加载产品'}finally{busy=false;more.disabled=false;grid.setAttribute('aria-busy','false')} }await loadPage(); } function renderContact({embedded=false,onBack=null}={}){ const card=node('div','lead-card');card.append(node('h3','','告诉我们你的项目需求'),node('p','muted','提交后由团队跟进。请仅填写必要的联系信息。')); const form=node('form','lead-form');form.setAttribute('aria-label','项目咨询'); const name=node('input');name.placeholder='称呼';name.setAttribute('aria-label','称呼');name.autocomplete='name';name.required=true;name.maxLength=80; const contact=node('input');contact.placeholder='电话或邮箱';contact.setAttribute('aria-label','电话或邮箱');contact.required=true;contact.minLength=3;contact.maxLength=200; const request=node('textarea');request.placeholder='项目场景、期望体验和已有条件';request.setAttribute('aria-label','项目需求');request.required=true;request.minLength=8;request.maxLength=4000; const label=node('label');const consent=node('input');consent.type='checkbox';consent.required=true;label.append(consent,document.createTextNode('同意团队使用上述信息联系我')); const submit=node('button','primary','提交需求');submit.type='submit';const result=node('p','feedback');result.setAttribute('role','status');form.append(name,contact,request,label,submit,result); form.addEventListener('submit',async event=>{event.preventDefault();submit.disabled=true;result.textContent='正在提交…';try{await api(APP_BASE+'api/leads',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name.value,contact:contact.value,request:request.value,consent:consent.checked})});result.textContent='已收到需求,团队会根据你留下的联系方式跟进。';form.reset()}catch(error){result.textContent=error.message}finally{submit.disabled=false}}); card.append(form);if(onBack)card.append(smallButton('返回联系方式 ↗',onBack));detailBody.append(card);detailBody.scrollTop=0; if(!embedded){detailTitle.textContent='联系团队';contentBody.append(node('p','muted','请在右侧填写项目需求,团队会根据留下的联系方式跟进。'))} } function renderSearch(){ const form=node('form');const input=node('input','search-input');input.placeholder='输入产品、案例或视频关键词';input.name='q';input.required=true;form.append(input); const results=node('div');contentBody.append(form,results); form.addEventListener('submit',async event=>{ event.preventDefault();clear(results);results.append(node('p','muted','正在搜索资料和视频…')); try{ const data=await api(APP_BASE+'api/search?q='+encodeURIComponent(input.value));trackVisit('search',{section:'search'});clear(results); if(!data.pages.length&&!data.knowledge.length&&!data.videos.length)results.append(node('p','muted','没有找到已发布资料或已公开的视频片段,请试试其他关键词。')); for(const item of data.videos)results.append(videoCard(item)); for(const item of data.pages)results.append(contentCard(item)); for(const item of data.knowledge){const card=node('div','search-card');card.append(node('span','kicker',item.kind==='video'?'视频片段':'资料片段'),node('strong','',item.title),node('p','body-text',item.excerpt.slice(0,200)));const link=node('a','text-link','查看来源 ↗');link.href=item.url;card.append(link);results.append(card)} if(data.video_unavailable)results.append(node('p','muted','视频检索暂不可用,已显示站内资料结果。')); }catch(error){showError(results,error)} });input.focus(); } async function setTab(tab,updateHistory=true,options={}){ if(updateHistory)navigation++; tab=tabs[tab]?tab:'news';state.tab=tab;state.request++;state.calendarRequest++;state.newsRequest++; state.aboutController=null;state.serviceController=null;state.recommendationRequest++; trackVisit('section_view',{section:tab}); clearInterval(state.bannerTimer);state.bannerTimer=null; if(validNewsDate(options.date)){state.date=options.date;state.month=options.date.slice(0,7);state.days=[];state.page=1;} stage.dataset.tab=tab;heading(tabs[tab]);activateDock(tab);clear(contentBody);resetDetail(tab==='news'?'选择左侧日期,再点击一篇新闻,在这里阅读完整内容。':tab==='case'?'点击左侧图片或图标卡片,在这里了解案例。':tab==='service'?'选择左侧服务,在这里阅读介绍并打开专门服务网站。':'选择左侧内容,在这里查看完整介绍。'); if(updateHistory)writeUrl({tab,date:tab==='news'?state.date:null,about:tab==='about'?(options.aboutSection||'intro'):null,content:null,asset:null,t:null,page:null}); if(tab!=='about')setRecommendation(); const requestId=state.request; try{ if(tab==='news')await renderNews(requestId,options); else { if(['product','case','service'].includes(tab)){const banner=node('div','column-banner-slot');contentBody.append(banner);renderBanners(tab,banner,requestId)} if(tab==='case')await renderCases(requestId);else if(tab==='product')await renderProducts(requestId);else if(tab==='service')await renderServices(requestId,options);else if(tab==='about')await renderAbout(requestId,options);else if(tab==='contact')renderContact();else renderSearch(); } }catch(error){if(requestId===state.request)showError(contentBody,error)} } async function openDetail(slug,options={}){ if(options.updateHistory!==false)navigation++; const token=++state.detailRequest;state.contextId=null;state.selectedSlug=slug; document.querySelectorAll('.news-list-item,.case-tile,.service-item').forEach(card=>{const active=card.dataset.slug===slug;card.classList.toggle('active',active);card.setAttribute('aria-pressed',String(active))}); detailTitle.textContent=state.tab==='news'?'News · 新闻详情':state.tab==='case'?'Cases · 案例介绍':state.tab==='service'?'Services · 服务介绍':'内容详情';clear(detailBody);detailBody.append(node('p','muted','正在读取…'));detailBody.setAttribute('aria-busy','true'); try{ const item=await api(APP_BASE+'api/contents/'+encodeURIComponent(slug));if(token!==state.detailRequest)return; detailTitle.textContent=item.kind==='case'?'Cases · 案例介绍':item.kind==='news'?'News · 新闻详情':item.kind==='product'?'Products · 产品介绍':item.kind==='service'?'Services · 服务介绍':'内容详情'; if(item.kind==='service'&&state.tab==='service')state.serviceController?.ensureSelected(item); if(item.kind==='case'&&state.tab==='case'){ const grid=contentBody.querySelector('#case-grid'); if(grid&&![...grid.children].some(card=>card.dataset.slug===item.slug)){grid.querySelector('.case-empty')?.remove();grid.prepend(caseTile(item));grid.closest('.case-browser').querySelector('.case-list-hint').hidden=false;updateCaseCount(grid)} } if(item.kind==='product'&&state.tab==='product'){const grid=contentBody.querySelector('#product-grid');if(grid&&![...grid.children].some(card=>card.dataset.slug===item.slug)){grid.querySelector('.product-empty')?.remove();grid.prepend(productTile(item));grid.closest('.product-browser').querySelector('.case-list-hint').hidden=false;updateProductCount(grid)}} state.contextId=item.id;clear(detailBody);const card=node('article','detail-card'); card.append(node('span','kicker',item.kind==='news'?(item.news_date||'')+' · '+(item.category||'虚拟工场动态'):item.category||tabs[item.kind]||'内容'),node('h3','',item.title)); if(item.summary&&item.kind!=='product')card.append(node('p','detail-summary',item.summary)); if(item.kind==='service')serviceIntroduction(card,item); const productRefs=item.kind==='product'?productDescription(card,item):new Set(); const partnerRefs=item.kind==='company'&&!item.html_package?companyPartnerDescription(card,item):null; const inlineArticleCover=item.attributes?.news_article_format==='article-v2'&&item.attributes.news_article_images?.some(pic=>pic.asset_id===item.cover_asset_id); if(item.kind!=='product'&&item.cover_asset_id&&!inlineArticleCover){const cover=node('img','detail-cover');cover.src=APP_BASE+'api/media/'+item.cover_asset_id;cover.alt=item.title;if(item.kind==='case'||item.kind==='service')cover.addEventListener('error',()=>cover.remove(),{once:true});card.append(cover)} let newsArticleRefs=new Set(); if(item.kind!=='product'){if(item.html_package)card.append(...publicationPreview(item));else if(item.body&&!partnerRefs){const refs=window.NewsArticleView?.render(card,item,APP_BASE);if(refs)newsArticleRefs=refs;else card.append(node('p','body-text',item.body));}} if(item.kind!=='news'&&item.attributes&&Object.keys(item.attributes).length){const attrs=node('div','asset-list');for(const [key,value] of Object.entries(item.attributes)){if(!['legacy_source','case_icon','review_required','about_section','service_type','service_icon','service_website_url','service_website_label','service_thumbnail_asset_id','service_thumbnail_focus'].includes(key)&&['string','number','boolean'].includes(typeof value))attrs.append(node('div','asset-row',key+':'+String(value)))}if(attrs.childElementCount)card.append(attrs)} const relatedMedia=(item.media||[]).filter(asset=>!(item.kind==='service'&&asset.id===item.attributes?.service_thumbnail_asset_id)&&!productRefs.has('asset:'+asset.id)&&!partnerRefs?.has('asset:'+asset.id)&&!newsArticleRefs.has(asset.id)&&(item.kind==='product'?asset.media_kind==='document':asset.id!==item.cover_asset_id)); if(relatedMedia.length){ card.append(node('h3','','相关资料'));const list=node('div','asset-list'); for(const asset of relatedMedia){ const row=node('div','asset-row');row.append(node('strong','',asset.original_name));const url=APP_BASE+'api/media/'+asset.id; if(asset.media_kind==='image'){const img=node('img','detail-cover');img.src=url;img.alt=asset.original_name;img.loading='lazy';row.append(img)} else if(asset.media_kind==='video'||asset.media_kind==='audio'){const media=node(asset.media_kind);media.controls=true;media.preload='metadata';media.src=url;if(options.asset===asset.id&&options.t){media.addEventListener('loadedmetadata',()=>{media.currentTime=Math.max(0,Math.min(Number(options.t)||0,media.duration||0))},{once:true})}row.append(media)} else {const link=node('a','','预览或下载文档 ↗');link.href=url;link.target='_blank';link.rel='noopener';row.append(link);if(options.asset===asset.id&&asset.mime_type==='application/pdf'){const frame=node('iframe');frame.title=asset.original_name;frame.src=url+(options.page?'#page='+Math.max(1,Number(options.page)||1):'');row.append(frame)}} list.append(row); } card.append(list); } if(item.kind==='company'&&state.tab==='about'&&state.aboutController)state.aboutController.decorate(card,item,options); detailBody.append(card);detailBody.scrollTop=0;updateDetailLink(item,options);trackVisit('content_view',{slug:item.slug}); if(contentReader)document.title=item.title+' · 虚拟工场'; if(!contentReader&&options.focus!==false&&(item.kind==='case'||item.kind==='product'||item.html_package||(item.kind==='company'&&options.aboutFocus)||(item.kind==='service'&&options.serviceFocus!==false))&&state.panelsVisible&&matchMedia('(max-width:800px)').matches){detailTitle.tabIndex=-1;detailTitle.focus({preventScroll:true});detailBody.closest('.detail-window').scrollIntoView({block:'start',behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'instant':'smooth'})} if(options.updateHistory!==false)writeUrl({tab:state.tab,date:state.tab==='news'?state.date:null,about:state.tab==='about'?state.aboutSection:null,content:slug,asset:options.asset||null,t:options.t||null,page:options.page||null}); }catch(error){if(token===state.detailRequest)showError(detailBody,error)}finally{if(token===state.detailRequest)detailBody.setAttribute('aria-busy','false')} } async function setRecommendation(){ const token=++state.recommendationRequest,tab=state.tab,body=document.querySelector('#recommend-body');clear(body); body.append(node('strong','','了解虚拟工场'),node('p','','从公司介绍、业务与能力,了解我们的工作与合作方式。'),smallButton('关于我们 ↗',()=>setTab('about'))); try{const [products,cases]=await Promise.all([api(APP_BASE+'api/contents?kind=product&limit=1'),api(APP_BASE+'api/contents?kind=case&limit=1')]);if(token!==state.recommendationRequest||state.tab!==tab||tab==='about')return;const item=products.items[0]||cases.items[0];if(!item)return; clear(body);body.append(node('strong','',item.title),node('p','',item.summary||'查看详情,了解更多。'),smallButton('打开内容窗口 ↗',async()=>{await setTab(item.kind);if(state.tab===item.kind)await openDetail(item.slug)})); }catch{/* Keep a usable company introduction shortcut. */} } document.querySelectorAll('.dock [data-tab]').forEach(button=>button.addEventListener('click',()=>setTab(button.dataset.tab))); const dockMotionPreference=matchMedia('(prefers-reduced-motion: reduce)'); const DOCK_FLIP_MS=900; let dockSwitchTimer=0,dockSwitchVersion=0,finishDockSwitch=null; function setDockMode(chatMode,{focus=true}={}){ const changed=dock.classList.contains('is-chat')!==chatMode; const version=++dockSwitchVersion; clearTimeout(dockSwitchTimer); finishDockSwitch=null; dock.classList.remove('is-switching'); dock.classList.toggle('is-chat',chatMode); stage.classList.toggle('chat-active',chatMode); dockMenu.inert=chatMode; dockMenu.setAttribute('aria-hidden',String(chatMode)); chatForm.inert=!chatMode; chatForm.setAttribute('aria-hidden',String(!chatMode)); dockToggle.setAttribute('aria-expanded',String(chatMode)); dockToggle.setAttribute('aria-label',chatMode?'切换到菜单栏':'切换到输入框'); dockToggle.querySelector('[aria-hidden]').textContent=chatMode?'☰':'✎'; dockToggle.querySelector('.dock-toggle-label').textContent=chatMode?'菜单':'输入'; const focusTarget=()=>{(chatMode?document.querySelector('#chat-input'):dockToggle).focus({preventScroll:true})}; if(!changed||!isDockMotionEnabled()){if(focus)focusTarget();return;} // Keep keyboard focus on the stable switch until the new face is readable. if(focus)dockToggle.focus({preventScroll:true}); void dock.offsetWidth; dock.classList.add('is-switching'); finishDockSwitch=()=>{ if(version!==dockSwitchVersion)return; clearTimeout(dockSwitchTimer); dock.classList.remove('is-switching'); finishDockSwitch=null; if(focus&&document.activeElement===dockToggle)focusTarget(); }; dockSwitchTimer=setTimeout(finishDockSwitch,DOCK_FLIP_MS); } const motionToggle=document.querySelector('#motion-toggle'); const dockMotionStorageKey='vrfab-zhiwei:dock-motion'; const requestedDockMotion=new URL(location.href).searchParams.get('motion'); let dockMotionSetting=['on','off'].includes(requestedDockMotion)?requestedDockMotion:null; try{ const saved=localStorage.getItem(dockMotionStorageKey); if(dockMotionSetting===null&&['on','off'].includes(saved))dockMotionSetting=saved; if(['on','off'].includes(requestedDockMotion))localStorage.setItem(dockMotionStorageKey,requestedDockMotion); }catch{/* The motion switch also works when browser storage is unavailable. */} const isDockMotionEnabled=()=>dockMotionSetting===null?!dockMotionPreference.matches:dockMotionSetting==='on'; function syncDockMotion(){ const enabled=isDockMotionEnabled(); stage.classList.toggle('dock-motion-enabled',enabled); motionToggle.textContent='动效:'+(enabled?'开':'关'); motionToggle.setAttribute('aria-pressed',String(enabled)); motionToggle.setAttribute('aria-label',enabled?'关闭工具栏动效':'开启工具栏动效'); motionToggle.title=enabled?'点击关闭工具栏翻转动画':'点击开启工具栏翻转动画'; if(!enabled)finishDockSwitch?.(); } motionToggle.addEventListener('click',()=>{ dockMotionSetting=isDockMotionEnabled()?'off':'on'; try{localStorage.setItem(dockMotionStorageKey,dockMotionSetting)}catch{/* Keep this page usable without storage. */} syncDockMotion(); }); dockMotionPreference.addEventListener('change',syncDockMotion); syncDockMotion(); dockToggle.addEventListener('click',()=>setDockMode(!dock.classList.contains('is-chat'))); document.querySelector('#chat-input').addEventListener('keydown',event=>{if(event.key==='Escape')setDockMode(false)}); const perspectiveToggle=document.querySelector('#perspective-toggle'); const panelsToggle=document.querySelector('#panels-toggle'); let panelReadingState=null,panelVisibilityVersion=0; perspectiveToggle.addEventListener('click',()=>{state.perspective=!state.perspective;stage.classList.toggle('perspective-off',!state.perspective);perspectiveToggle.textContent='透视:'+(state.perspective?'开':'关');perspectiveToggle.setAttribute('aria-pressed',String(state.perspective))}); panelsToggle.addEventListener('click',()=>{ const version=++panelVisibilityVersion; if(state.panelsVisible)panelReadingState={request:state.request,detailRequest:state.detailRequest,x:scrollX,y:scrollY,entries:[...document.querySelectorAll('[data-panel], [data-panel] *')].filter(el=>el.scrollTop||el.scrollLeft||['content-body','detail-body','chat-messages'].includes(el.id)).map(el=>({el,top:el.scrollTop,left:el.scrollLeft,child:el.firstElementChild}))}; state.panelsVisible=!state.panelsVisible; if(!state.panelsVisible)panelsToggle.focus(); document.querySelectorAll('[data-panel]').forEach(panel=>{panel.hidden=!state.panelsVisible;panel.inert=!state.panelsVisible}); stage.classList.toggle('panels-hidden',!state.panelsVisible);perspectiveToggle.hidden=!state.panelsVisible; panelsToggle.textContent=state.panelsVisible?'隐藏所有面板':'打开面板';panelsToggle.setAttribute('aria-expanded',String(state.panelsVisible)); if(!state.panelsVisible)setDockMode(true,{focus:false}); document.querySelectorAll('#right-windows video,#right-windows audio').forEach(media=>{if(!state.panelsVisible)media.pause()}); if(state.panelsVisible&&panelReadingState){ const saved=panelReadingState; const restore=()=>{ if(!state.panelsVisible||version!==panelVisibilityVersion||saved.request!==state.request||saved.detailRequest!==state.detailRequest)return; for(const entry of saved.entries)if(entry.el.isConnected&&entry.el.firstElementChild===entry.child)entry.el.scrollTo({top:entry.top,left:entry.left,behavior:'instant'}); window.scrollTo({top:saved.y,left:saved.x,behavior:'instant'}); }; restore();requestAnimationFrame(restore); } }); const chatHistory = []; let chatBusy = false; chatForm.addEventListener('submit',async event=>{ event.preventDefault();if(chatBusy)return; const input=document.querySelector('#chat-input');const message=input.value.trim();if(!message)return; const history=chatHistory.slice(-12);input.value='';chatBusy=true; const send=chatForm.querySelector('button[type="submit"]');send.disabled=true;chatForm.setAttribute('aria-busy','true'); messages.append(node('p','question',message));const pending=node('p','muted','知微正在思考…');messages.append(pending);messages.scrollTop=messages.scrollHeight;setHost('thinking'); try{ const data=await api(APP_BASE+'api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message,history,context_content_id:state.contextId})}); pending.remove();messages.append(node('p','',data.answer)); if(data.mode!=='chat_unavailable'){ chatHistory.push({role:'user',content:message},{role:'assistant',content:data.answer.slice(0,1500)}); if(chatHistory.length>12)chatHistory.splice(0,chatHistory.length-12); } if(data.mode==='retrieval')messages.append(node('p','muted','资料摘录 · 打开来源可查看全文')); if(data.citations?.length){const list=node('div','citation-list');for(const source of data.citations){const link=node('a','',source.title);link.href=source.url;list.append(link)}messages.append(list)} if(data.video_results?.length){const list=node('div','citation-list');for(const item of data.video_results){const link=node('a','','视频 · '+item.title+' ↗');link.href=item.url;link.target='_blank';link.rel='noopener';list.append(link)}messages.append(list)} if(data.actions?.length){const actions=node('div','citation-list');for(const action of data.actions){const link=node('a','',action.label+' ↗');link.href=action.url;actions.append(link)}messages.append(actions)} setHost('explaining');setTimeout(()=>{if(!chatBusy)setHost('idle')},3600); }catch(error){pending.textContent=error.message;if(!input.value)input.value=message;setHost('idle')} finally{chatBusy=false;send.disabled=false;chatForm.removeAttribute('aria-busy');messages.scrollTop=messages.scrollHeight} }); let navigation=0; async function navigateFromUrl(){ const token=++navigation;const params=new URLSearchParams(location.search);let tab=params.get('tab')||'news';let date=params.get('date');const slug=params.get('content'); if(contentReader){ state.tab=tabs[tab]?tab:'news';stage.dataset.tab=state.tab; if(slug)await openDetail(slug,{asset:params.get('asset'),t:params.get('t'),page:params.get('page'),updateHistory:false}); else resetDetail('请先在知微中选择一项内容,再打开新窗口查看。'); return; } if(slug&&(!params.get('tab')||['article','blog'].includes(tab))){try{const item=await api(APP_BASE+'api/contents/'+encodeURIComponent(slug));if(token!==navigation)return;tab=item.kind==='company'?'about':item.kind;date=item.news_date||date}catch{}} if(['article','blog'].includes(params.get('tab'))){tab='news';const url=new URL(location.href);url.searchParams.set('tab','news');if(validNewsDate(date))url.searchParams.set('date',date);history.replaceState(null,'',url)} if(token!==navigation)return;await setTab(tab,false,{date,aboutSection:params.get('about'),content:slug});if(token!==navigation)return; if(slug)await openDetail(slug,{asset:params.get('asset'),t:params.get('t'),page:params.get('page'),updateHistory:false}); } document.addEventListener('click',event=>{const link=event.target.closest('a');if(contentReader||!link||link.target==='_blank'||event.ctrlKey||event.metaKey||event.shiftKey||event.altKey)return;const url=new URL(link.href,location.href);if(url.origin===location.origin&&url.pathname===APP_BASE&&url.searchParams.has('content')){event.preventDefault();navigation++;openDetail(url.searchParams.get('content'),{asset:url.searchParams.get('asset'),t:url.searchParams.get('t'),page:url.searchParams.get('page')})}}); window.addEventListener('popstate',navigateFromUrl); navigateFromUrl();