manual.js 6.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. (() => {
  2. 'use strict';
  3. const data = window.WORKSHOP_MANUAL_DATA;
  4. const storageKey = 'ksf-workshop-manual-r1-20261109';
  5. let completed;
  6. try { completed = new Set(JSON.parse(localStorage.getItem(storageKey) || '[]').filter(id => data.steps.some(s => s.id === id))); }
  7. catch { completed = new Set(); }
  8. const byId = id => document.getElementById(id);
  9. const escape = value => String(value).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
  10. const fullPrompt = step => step.prompt || `${data.common}\n\n本节要回答:${step.question}\n\n先复用:${step.inputs}\n执行细则:${step.skillReference}\n\n本节任务:\n${step.task}\n\n本节应交付:\n${step.deliverables.map(d => `• ${d}`).join('\n')}`;
  11. let current = 0;
  12. let toastTimer;
  13. function toast(message) {
  14. byId('toast').textContent = message;
  15. byId('toast').classList.add('show');
  16. clearTimeout(toastTimer);
  17. toastTimer = setTimeout(() => byId('toast').classList.remove('show'), 2800);
  18. }
  19. function closeMenu() {
  20. document.querySelector('.sidebar').classList.remove('open');
  21. document.querySelector('.menu-button').setAttribute('aria-expanded', 'false');
  22. byId('menu-overlay').hidden = true;
  23. }
  24. function navigation() {
  25. byId('navigation').innerHTML = data.groups.map(group => `<div class="nav-group"><div class="nav-label">${escape(group.label)}</div>${data.steps.filter(s => s.group === group.id).map(s => `<a class="nav-item" href="#${s.id}" ${data.steps[current].id === s.id ? 'aria-current="step"' : ''}><span class="nav-num">${escape(s.number.replace('准备 ', ''))}</span><span>${escape(s.title)}</span>${completed.has(s.id) ? '<span class="nav-done" aria-label="已核对">✓</span>' : ''}</a>`).join('')}</div>`).join('');
  26. byId('progress-label').textContent = `${completed.size} / ${data.steps.length} 项已核对`;
  27. }
  28. function render(scroll = false) {
  29. const id = location.hash.slice(1);
  30. const found = data.steps.findIndex(s => s.id === id);
  31. current = found >= 0 ? found : 0;
  32. const step = data.steps[current];
  33. const group = data.groups.find(g => g.id === step.group);
  34. byId('group-label').textContent = group.label;
  35. const version = step.id === 'install' ? `<p class="version-note">工作坊 ${escape(data.release.version)} · VOC 0.3.42 <a href="${escape(data.release.guide)}" target="_blank" rel="noopener">安装说明 ↗</a></p>` : '';
  36. byId('chapter').innerHTML = `<header class="chapter-head"><h1><span class="chapter-index">${escape(step.number)}</span><span id="chapter-title">${escape(step.title)}</span></h1><label class="checkpoint" title="本机个人进度,不代替实际验收"><input type="checkbox" id="completed" ${completed.has(step.id) ? 'checked' : ''}>已核对</label></header>
  37. <div class="chapter-body"><p class="action-copy">${escape(step.action)}</p><div class="outputs-box"><b>产出</b><ul class="outputs">${step.deliverables.map(d => `<li>${escape(d)}</li>`).join('')}</ul></div>${version}
  38. <div class="prompt-box"><div class="prompt-top"><b>任务提示词</b><button class="copy-button" id="copy-prompt">复制提示词</button></div><pre class="prompt-text" id="prompt-text">${escape(fullPrompt(step))}</pre></div>
  39. <div class="quality"><h2>检查要点</h2><ul>${step.checks.map(c => `<li>${escape(c)}</li>`).join('')}</ul></div></div>
  40. <div class="chapter-footer"><button class="step-button" id="previous" ${current === 0 ? 'disabled' : ''}>← ${current > 0 ? escape(data.steps[current - 1].title) : '开始之前'}</button><button class="step-button step-next" id="next" ${current === data.steps.length - 1 ? 'disabled' : ''}>${current < data.steps.length - 1 ? escape(data.steps[current + 1].title) : '本组提案已整理'} →</button></div>`;
  41. byId('copy-prompt').addEventListener('click', () => copy(fullPrompt(step)));
  42. byId('completed').addEventListener('change', event => {
  43. event.target.checked ? completed.add(step.id) : completed.delete(step.id);
  44. try { localStorage.setItem(storageKey, JSON.stringify([...completed])); } catch { toast('浏览器未允许保存本机进度'); }
  45. navigation();
  46. });
  47. byId('previous').addEventListener('click', () => { location.hash = data.steps[current - 1].id; });
  48. byId('next').addEventListener('click', () => { location.hash = data.steps[current + 1].id; });
  49. navigation();
  50. closeMenu();
  51. document.title = `${step.title} · 康师傅工作坊操作手册`;
  52. if (scroll) {
  53. document.querySelector('.work-area').scrollIntoView({ behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'instant' : 'smooth', block: 'start' });
  54. byId('chapter').focus({ preventScroll: true });
  55. }
  56. }
  57. async function copy(text) {
  58. try {
  59. if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);
  60. else {
  61. const field = document.createElement('textarea');
  62. field.value = text;
  63. field.style.position = 'fixed';
  64. field.style.left = '-9999px';
  65. document.body.append(field);
  66. field.select();
  67. const ok = document.execCommand('copy');
  68. field.remove();
  69. if (!ok) throw new Error('COPY_UNAVAILABLE');
  70. }
  71. toast('完整提示词已复制,可发送给数字人');
  72. } catch {
  73. const selection = getSelection();
  74. const range = document.createRange();
  75. range.selectNodeContents(byId('prompt-text'));
  76. selection.removeAllRanges();
  77. selection.addRange(range);
  78. toast('请复制已选中的完整提示词');
  79. }
  80. }
  81. document.querySelector('.menu-button').addEventListener('click', event => {
  82. const open = !document.querySelector('.sidebar').classList.contains('open');
  83. document.querySelector('.sidebar').classList.toggle('open', open);
  84. event.currentTarget.setAttribute('aria-expanded', String(open));
  85. byId('menu-overlay').hidden = !open;
  86. });
  87. byId('menu-overlay').addEventListener('click', closeMenu);
  88. document.addEventListener('keydown', event => { if (event.key === 'Escape') closeMenu(); });
  89. window.addEventListener('hashchange', () => render(true));
  90. render(false);
  91. })();