385385 color : var (--text-secondary );
386386 }
387387
388+ /* === Copy Link Button === */
389+ .case-header {
390+ display : flex;
391+ justify-content : flex-end;
392+ margin-bottom : 4px ;
393+ }
394+
395+ .copy-link-btn {
396+ background : none;
397+ border : 1px solid var (--border );
398+ border-radius : var (--radius-sm );
399+ padding : 3px 10px ;
400+ font-size : 0.7rem ;
401+ color : var (--text-muted );
402+ cursor : pointer;
403+ transition : all 0.15s ;
404+ letter-spacing : 0.02em ;
405+ }
406+
407+ .copy-link-btn : hover {
408+ color : var (--text-primary );
409+ border-color : var (--text-secondary );
410+ background : var (--bg-body );
411+ }
412+
413+ .copy-link-btn .copied {
414+ color : # 22c55e ;
415+ border-color : # 22c55e ;
416+ }
417+
388418 /* === Footer Disclaimer === */
389419 .footer-disclaimer {
390420 max-width : 900px ;
@@ -492,7 +522,9 @@ <h1>MiniCPM-o 4.5 Audio Demo Page</h1>
492522 noData : '数据未加载' ,
493523 ensureFile : '请确保 data.js 文件存在' ,
494524 noCases : '暂无案例' ,
495- addCases : '请在配置中添加案例'
525+ addCases : '请在配置中添加案例' ,
526+ copyLink : '🔗 复制链接' ,
527+ copied : '✓ 已复制'
496528 } ,
497529 en : {
498530 systemSetting : 'System Setting' ,
@@ -504,7 +536,9 @@ <h1>MiniCPM-o 4.5 Audio Demo Page</h1>
504536 noData : 'Data not loaded' ,
505537 ensureFile : 'Please ensure data.js file exists' ,
506538 noCases : 'No cases available' ,
507- addCases : 'Please add cases in configuration'
539+ addCases : 'Please add cases in configuration' ,
540+ copyLink : '🔗 Link' ,
541+ copied : '✓ Copied'
508542 }
509543 } ;
510544
@@ -623,6 +657,7 @@ <h3 class="subsection-title">${getText(sub.name)}</h3>
623657 </div>
624658 ${ cases . map ( c => `
625659 <div class="case-content ${ c . id === activeCase ? 'active' : '' } "
660+ id="case-${ c . id } "
626661 data-section="${ sectionId } " data-case="${ c . id } ">
627662 ${ renderCaseContent ( c ) }
628663 </div>
@@ -635,6 +670,9 @@ <h3 class="subsection-title">${getText(sub.name)}</h3>
635670
636671 function renderCaseContent ( caseData ) {
637672 return `
673+ <div class="case-header">
674+ <button class="copy-link-btn" data-copy-case="${ caseData . id } ">${ t ( 'copyLink' ) } </button>
675+ </div>
638676 ${ renderSystemBlock ( caseData . system ) }
639677 ${ renderConversation ( caseData . turns ) }
640678 ` ;
@@ -731,6 +769,25 @@ <h3 class="subsection-title">${getText(sub.name)}</h3>
731769 document . querySelectorAll ( `.case-content[data-section="${ sectionId } "]` ) . forEach ( c => {
732770 c . classList . toggle ( 'active' , c . dataset . case === caseId ) ;
733771 } ) ;
772+
773+ // Update URL hash (不触发 scroll)
774+ history . replaceState ( null , '' , '#' + caseId ) ;
775+ } ) ;
776+ } ) ;
777+
778+ // Copy link buttons
779+ document . querySelectorAll ( '.copy-link-btn[data-copy-case]' ) . forEach ( btn => {
780+ btn . addEventListener ( 'click' , ( ) => {
781+ const caseId = btn . dataset . copyCase ;
782+ const url = window . location . origin + window . location . pathname + '#' + caseId ;
783+ navigator . clipboard . writeText ( url ) . then ( ( ) => {
784+ btn . classList . add ( 'copied' ) ;
785+ btn . textContent = t ( 'copied' ) ;
786+ setTimeout ( ( ) => {
787+ btn . classList . remove ( 'copied' ) ;
788+ btn . textContent = t ( 'copyLink' ) ;
789+ } , 1500 ) ;
790+ } ) ;
734791 } ) ;
735792 } ) ;
736793
@@ -742,6 +799,30 @@ <h3 class="subsection-title">${getText(sub.name)}</h3>
742799 } ) ;
743800 }
744801
802+ // === Hash Navigation ===
803+ function navigateToCase ( caseId ) {
804+ const el = document . getElementById ( 'case-' + caseId ) ;
805+ if ( ! el ) return false ;
806+
807+ const sectionId = el . dataset . section ;
808+
809+ // Activate tab
810+ state . activeCase [ sectionId ] = caseId ;
811+ const tabGroup = document . querySelector ( `.tab-group[data-section="${ sectionId } "]` ) ;
812+ if ( tabGroup ) {
813+ tabGroup . querySelectorAll ( '.tab-btn' ) . forEach ( b => {
814+ b . classList . toggle ( 'active' , b . dataset . case === caseId ) ;
815+ } ) ;
816+ }
817+ document . querySelectorAll ( `.case-content[data-section="${ sectionId } "]` ) . forEach ( c => {
818+ c . classList . toggle ( 'active' , c . dataset . case === caseId ) ;
819+ } ) ;
820+
821+ // Scroll into view
822+ el . scrollIntoView ( { behavior : 'smooth' , block : 'center' } ) ;
823+ return true ;
824+ }
825+
745826 // === Init ===
746827 document . addEventListener ( 'DOMContentLoaded' , ( ) => {
747828 // Init lang buttons
@@ -750,6 +831,18 @@ <h3 class="subsection-title">${getText(sub.name)}</h3>
750831 } ) ;
751832 document . documentElement . lang = state . lang === 'zh' ? 'zh-CN' : 'en' ;
752833 render ( ) ;
834+
835+ // Handle hash navigation on load
836+ if ( window . location . hash ) {
837+ const caseId = window . location . hash . slice ( 1 ) ;
838+ setTimeout ( ( ) => navigateToCase ( caseId ) , 150 ) ;
839+ }
840+ } ) ;
841+
842+ // Handle hash changes (e.g. back/forward)
843+ window . addEventListener ( 'hashchange' , ( ) => {
844+ const caseId = window . location . hash . slice ( 1 ) ;
845+ if ( caseId ) navigateToCase ( caseId ) ;
753846 } ) ;
754847 </ script >
755848</ body >
0 commit comments