UNPKG

bytefun

Version:

一个打通了原型设计、UI设计与代码转换、跨平台原生代码开发等的平台

1,517 lines (1,311 loc) 73.4 kB
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>枫叶小说APP 产品需求文档编辑器</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <script src="https://unpkg.com/mermaid@10/dist/mermaid.min.js"></script> <style> * { margin: 0; padding: 0; box-sizing: border-box; } :root { --bg-primary: #1B1B1B; --bg-secondary: #232323; --bg-tertiary: #2C2C2C; --bg-text: #494949; --text-primary: #FFFFFF; --text-secondary: #8E8E93; --border-color: #2C2C2C; --input-bg: #232323; --light-title-bg: #373737; } [data-theme="light"] { --bg-primary: #FDFDFD; --bg-secondary: #F5F5F5; --bg-tertiary: #E8E8E8; --bg-text: #E8E8E8; --text-primary: #1C1C1E; --text-secondary: #6D6D70; --border-color: #D1D1D6; --input-bg: #FFFFFF; --light-title-bg: #00000000; } body { font-family: 'Poppins', 'Arial', sans-serif; background-color: var(--bg-primary); color: var(--text-primary); line-height: 1.6; transition: background-color 0.3s ease, color 0.3s ease; padding: 0px; } .container { margin-left: 120px; margin-right: 0; transition: all 0.3s ease; min-height: 100vh; } .container.sidebar-collapsed { margin-left: 0; } .section { background: var(--bg-secondary); padding-top: 30px; padding-left: 30px; padding-right: 10px; } .section-title { font-size: 18px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; } .subsection { margin-bottom: 8px; } .subsection-title { font-size: 16px; font-weight: 500; color: var(--text-primary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; } .content-item-title { font-size: 14px; font-weight: 500; color: var(--text-primary); margin-bottom: 6px; display: flex; align-items: center; gap: 6px; } .content-text { font-size: 14px; color: var(--text-secondary); line-height: 1.5; } .title-input { background: var(--input-bg); border: none; border-radius: 4px; padding: 5px; color: var(--text-primary); font-size: 14px; width: 100%; transition: background-color 0.3s ease; } .title-input:focus { outline: none; } .section-title .title-input { font-size: 17px; font-weight: 700; } .content-item-title .title-input { font-size: 15px; font-weight: 700; } .content-textarea { background: var(--input-bg); border: none; border-radius: 4px; padding: 8px 12px; color: var(--text-primary); font-size: 14px; width: 100%; resize: none; font-family: inherit; transition: background-color 0.3s ease; overflow: hidden; box-sizing: border-box; } .content-textarea:focus { outline: none; } .title-with-add { display: block; } .title-with-add .section-title { margin-bottom: 0; display: flex; align-items: center; } .title-with-add .content-item-title { display: flex; align-items: center; } .content-item { background: var(--bg-secondary); border-radius: 6px; padding-right: 12px; position: relative; } .content-item .title-with-add { padding-right: 0; } .save-button { position: fixed; top: 20px; right: 130px; background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 20px; padding: 6px 12px; cursor: pointer; font-size: 12px; color: var(--text-primary); transition: all 0.3s ease; z-index: 1000; display: flex; align-items: center; gap: 6px; } .save-button:hover { background: var(--bg-tertiary); } .theme-toggle { position: fixed; top: 20px; right: 36px; background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 20px; padding: 6px 12px; cursor: pointer; font-size: 12px; color: var(--text-primary); transition: all 0.3s ease; z-index: 1000; display: flex; align-items: center; gap: 6px; } .theme-toggle:hover { background: var(--bg-tertiary); } .nested-content { margin-left: 20px; border-left: 2px solid var(--border-color); padding-left: 12px; } /* Mermaid图表样式 */ .mermaid-container { background: var(--bg-secondary); border-radius: 8px; padding: 0; margin: 12px 0; border: 1px solid var(--border-color); overflow: hidden; } /* Mermaid Tab样式 */ .mermaid-tabs { display: flex; background: var(--bg-tertiary); border-bottom: 1px solid var(--border-color); } .mermaid-tab { padding: 12px 20px; background: transparent; border: none; color: var(--text-secondary); cursor: pointer; font-size: 14px; transition: all 0.3s ease; border-bottom: 2px solid transparent; } .mermaid-tab:hover { background: var(--bg-secondary); color: var(--text-primary); } .mermaid-tab.active { background: var(--bg-secondary); color: var(--text-primary); } .mermaid-content { padding: 0px; } .mermaid-tab-panel { display: none; } .mermaid-tab-panel.active { display: block; } .mermaid { border-radius: 6px; margin-bottom: 12px; text-align: center; transform-origin: center center; transition: transform 0.2s ease; overflow: hidden; position: relative; cursor: grab; } .mermaid:active { cursor: grabbing; } .mermaid svg { max-width: none; height: auto; } /* Mermaid代码显示样式 */ .mermaid-code { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 6px; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 13px; line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; max-height: 400px; overflow-y: auto; border: 1px solid var(--border-color); } /* Mermaid代码编辑器样式 */ .mermaid-code-editor { width: 100%; min-height: 300px; max-height: 500px; background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 6px; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 13px; line-height: 1.5; border: 1px solid var(--border-color); resize: vertical; outline: none; transition: border-color 0.3s ease; } .mermaid-code-editor:focus { border-color: var(--border-color); box-shadow: none; } .edit-mermaid-btn { background: #007AFF; color: white; border: none; border-radius: 4px; padding: 6px 12px; font-size: 12px; cursor: pointer; transition: background-color 0.3s ease; display: flex; align-items: center; gap: 4px; } .edit-mermaid-btn:hover { background: #0056CC; } /* 重置流程图按钮 */ .reset-mermaid-btn { position: fixed; bottom: 20px; right: 20px; background: #1d1d1d; color: white; border: none; border-radius: 25px; padding: 12px 20px; font-size: 11px; font-weight: 500; cursor: pointer; box-shadow: 0 4px 12px rgba(73, 73, 73, 0.3); transition: all 0.3s ease; z-index: 1000; display: none; align-items: center; gap: 6px; } .reset-mermaid-btn:hover { background: #202020; transform: translateY(-1px); } .reset-mermaid-btn.show { display: flex; } .item-controls { display: flex; gap: 8px; margin-top: 8px; } .item-controls button { font-size: 12px; padding: 4px 8px; } /* 自定义确认弹框样式 */ .custom-confirm-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s ease; } .custom-confirm-overlay.show { opacity: 1; visibility: visible; } .custom-confirm-dialog { background: var(--bg-secondary); border-radius: 8px; padding: 24px; max-width: 400px; width: 90%; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); border: 1px solid var(--border-color); } .custom-confirm-message { color: var(--text-primary); font-size: 16px; margin-bottom: 20px; text-align: center; } .custom-confirm-buttons { display: flex; gap: 12px; justify-content: center; } .custom-confirm-btn { padding: 8px 16px; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; transition: background-color 0.3s ease; } .custom-confirm-btn.cancel { background: var(--bg-tertiary); color: var(--text-primary); } .custom-confirm-btn.cancel:hover { background: var(--bg-text); } .custom-confirm-btn.confirm { background: #FF3B30; color: white; } .custom-confirm-btn.confirm:hover { background: #E6342A; } /* 侧滑面板样式 */ .sidebar { position: fixed; left: 0; top: 0; width: 130px; height: 100vh; background: var(--bg-secondary); border-right: 1px solid var(--border-color); z-index: 999; overflow-y: auto; transition: transform 0.3s ease; transform: translateX(0); /* 隐藏滚动条但保持滚动功能 */ scrollbar-width: none; /* Firefox */ -ms-overflow-style: none; /* IE and Edge */ } /* 隐藏 Webkit 浏览器的滚动条 */ .sidebar::-webkit-scrollbar { display: none; } .sidebar.collapsed { transform: translateX(-130px); } /* 收起状态下的圆形箭头图标 */ .sidebar-expand-btn { position: fixed; left: 10px; top: 10px; width: 30px; height: 30px; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 50%; display: none; align-items: center; justify-content: center; cursor: pointer; z-index: 1000; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .sidebar-expand-btn:hover { background: var(--bg-tertiary); transform: scale(1.05); } .sidebar-expand-btn i { color: var(--text-primary); font-size: 16px; } .sidebar.collapsed~.sidebar-expand-btn { display: flex; } .sidebar-header { padding: 16px; border-bottom: 1px solid var(--border-color); display: flex; align-items: center; justify-content: space-between; } .sidebar-title { font-size: 16px; font-weight: 600; color: var(--text-primary); } .sidebar-toggle { background: none; border: none; color: var(--text-primary); cursor: pointer; padding: 4px; border-radius: 4px; transition: background-color 0.3s ease; } .sidebar-toggle:hover { background: var(--bg-tertiary); } .toc-container { padding: 0px; } .toc-item { display: flex; align-items: center; justify-content: space-between; padding: 4px 6px; color: var(--text-secondary); text-decoration: none; border-radius: 3px; margin-bottom: 1px; font-size: 11px; line-height: 1.3; cursor: pointer; transition: all 0.3s ease; position: relative; } .toc-item-text { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } .toc-item-buttons { display: none; gap: 2px; margin-left: 4px; flex-shrink: 0; } .toc-item:hover .toc-item-buttons { display: flex; } .toc-btn { width: 14px; height: 14px; border: none; border-radius: 2px; font-size: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; } .toc-add-btn { background: #34C759; color: white; margin-right: 2px; margin-bottom: 3px; } .toc-add-btn:hover { background: #30B050; } .toc-delete-btn { background: #FF3B30; color: white; margin-top: 1px; } .toc-delete-btn:hover { background: #E6342A; } .toc-item:hover { background: var(--bg-tertiary); color: var(--text-primary); } .toc-item.active { background: var(--light-title-bg); color: var(--text-primary); } .toc-item.level-1 { font-weight: 600; padding-left: 8px; font-size: 12px; } .toc-item.level-2 { padding-left: 15px; font-size: 11px; } .toc-item.level-3 { padding-left: 22px; font-size: 10px; } @media (max-width: 768px) { .container { margin-left: 120px; } .container.sidebar-collapsed { margin-left: 0; } .sidebar { width: 140px; } .sidebar.collapsed { transform: translateX(-140px); } .title-with-add { flex-direction: column; align-items: stretch; } .nested-content { margin-left: 10px; padding-left: 8px; } } </style> </head> <body> <!-- 侧滑面板 --> <div class="sidebar" id="sidebar"> <div class="sidebar-header"> <div class="sidebar-title">文档目录</div> <button class="sidebar-toggle" onclick="toggleSidebar()"> <i class="fas fa-chevron-left"></i> </button> </div> <div class="toc-container" id="toc-container"> <!-- 目录将通过JavaScript动态生成 --> </div> </div> <!-- 收起状态下的圆形箭头图标 --> <div class="sidebar-expand-btn" onclick="toggleSidebar()"> <i class="fas fa-chevron-right"></i> </div> <!-- 自定义确认弹框 --> <div id="customConfirmOverlay" class="custom-confirm-overlay"> <div class="custom-confirm-dialog"> <div id="customConfirmMessage" class="custom-confirm-message"></div> <div class="custom-confirm-buttons"> <button class="custom-confirm-btn cancel" onclick="hideCustomConfirm(false)">取消</button> <button class="custom-confirm-btn confirm" onclick="hideCustomConfirm(true)">确认</button> </div> </div> </div> <!-- 重置流程图按钮 --> <button id="resetMermaidBtn" class="reset-mermaid-btn" onclick="resetMermaidTransform()"> <i class="fas fa-undo"></i> 重置流程图 </button> <div class="container"> <div class="save-button" onclick="saveConfig()" style="position: fixed; z-index: 1000; margin: 0;"> <i class="fas fa-save"></i> <span>保存</span> </div> <div class="theme-toggle" onclick="toggleTheme()" style="position: fixed; z-index: 1000; margin: 0;"> <i class="fas fa-sun"></i> <span id="theme-text">亮色模式</span> </div> <!-- 动态生成的内容区域 --> <div id="content-container"> <!-- 内容将通过JavaScript动态生成 --> </div> </div> <script> // PRD数据结构 - 完整的JSON数据 let prdData = { "productRequirementDocument": { "title": "xxx产品需求文档", "sections": { "projectBackground": { "title": "一、项目的背景", "content": [ { "title": "1、项目概述", "content": "xxx" }, { "title": "2、用户痛点分析", "content": [ { "title": "阅读体验差", "content": "现有应用广告干扰严重,影响沉浸式阅读体验" }, { "title": "内容发现困难", "content": "缺乏精准的个性化推荐,用户难以找到喜欢的作品" } ] }, { "title": "3、目标用户群体", "content": [ { "title": "主要用户", "content": "18-35岁的年轻人群,喜欢利用碎片时间阅读" }, { "title": "次要用户", "content": "35岁以上的人群,对阅读有一定需求" } ] } ] }, "businessLogicFlow": { "title": "二、功能模块业务逻辑流程图", "content": "flowchart TD\n A[启动应用] --> B{是否首次启动}\n" }, "functionalRequirements": { "title": "三、功能模块列表与需要描述", "moduleList": [ { "title": "1、启动模块", "cnName": "启动模块", "enName": "startup", "content": [ { "title": "1.1、xxx功能", "content": "xxx(描述该功能作用)" }, { "title": "1.2、xxx功能", "content": "xxx(描述该功能作用)" } ] }, { "title": "2、账号模块", "cnName": "账号模块", "enName": "account", "content": [ { "title": "2.1、xxx功能", "content": "xxx(描述该功能作用)" }, { "title": "2.2、xxx功能", "content": "xxx(描述该功能作用)" } ] } ] } } } } // 记录数据来源,用于保存时确定保存路径 let fromWhere = 'prd' // 获取数据根对象 function getDataRoot() { if (prdData.productRequirementDocument) { return prdData.productRequirementDocument; } else if (prdData.moduleBusinessLogicDesign) { return prdData.moduleBusinessLogicDesign; } return null; } // 渲染所有内容 function renderAll() { const container = document.getElementById('content-container'); if (!container) { console.error('content-container element not found'); return; } // 检查数据是否已加载 const dataRoot = getDataRoot(); if (!dataRoot || !dataRoot.sections) { console.warn('数据尚未加载,跳过渲染'); return; } container.innerHTML = ''; // 更新文档标题 const titleElement = document.getElementById('document-title'); if (titleElement) { titleElement.textContent = dataRoot.title; } // 渲染各个章节 const sections = dataRoot.sections; Object.keys(sections).forEach(sectionKey => { const section = sections[sectionKey]; const sectionElement = createSection(sectionKey, section); container.appendChild(sectionElement); }); } // 创建章节 function createSection(sectionKey, sectionData) { const section = document.createElement('div'); section.className = 'section'; section.dataset.sectionKey = sectionKey; // 检查不同的内容数组字段:pageList, moduleList, content const contentData = sectionData.pageList || sectionData.moduleList || sectionData.content; section.innerHTML = ` <div class="title-with-add"> <h2 class="section-title"> <input type="text" class="title-input" value="${sectionData.title}" onchange="updateSectionTitle('${sectionKey}', this.value)"> </h2> </div> <div class="content-area" id="content-${sectionKey}"> <!-- 内容将动态生成 --> </div> `; // 渲染内容 const contentArea = section.querySelector(`#content-${sectionKey}`); renderSectionContent(contentArea, sectionKey, contentData); return section; } // 渲染章节内容 function renderSectionContent(container, sectionKey, content) { container.innerHTML = ''; if (typeof content === 'string') { // 检查是否是mermaid图表代码 if (content.trim().startsWith('flowchart') || content.trim().startsWith('graph') || content.trim().startsWith('sequenceDiagram') || content.trim().startsWith('gantt') || content.trim().startsWith('pie') || content.trim().startsWith('gitGraph')) { // Mermaid图表内容 const mermaidContainer = document.createElement('div'); mermaidContainer.className = 'mermaid-container'; // 创建tab导航 const tabsDiv = document.createElement('div'); tabsDiv.className = 'mermaid-tabs'; const chartTab = document.createElement('button'); chartTab.className = 'mermaid-tab active'; chartTab.textContent = '流程图'; chartTab.onclick = () => switchMermaidTab(mermaidContainer, 'chart'); const codeTab = document.createElement('button'); codeTab.className = 'mermaid-tab'; codeTab.textContent = 'Mermaid代码'; codeTab.onclick = () => switchMermaidTab(mermaidContainer, 'code'); tabsDiv.appendChild(chartTab); tabsDiv.appendChild(codeTab); // 创建内容区域 const contentDiv = document.createElement('div'); contentDiv.className = 'mermaid-content'; // 流程图面板 const chartPanel = document.createElement('div'); chartPanel.className = 'mermaid-tab-panel active'; chartPanel.setAttribute('data-panel', 'chart'); const mermaidDiv = document.createElement('div'); mermaidDiv.className = 'mermaid'; mermaidDiv.textContent = content; chartPanel.appendChild(mermaidDiv); // 代码面板 const codePanel = document.createElement('div'); codePanel.className = 'mermaid-tab-panel'; codePanel.setAttribute('data-panel', 'code'); const codeTextarea = document.createElement('textarea'); codeTextarea.className = 'mermaid-code-editor'; codeTextarea.value = content; codeTextarea.onchange = () => updateMermaidCode(sectionKey, codeTextarea.value, mermaidDiv); codePanel.appendChild(codeTextarea); contentDiv.appendChild(chartPanel); contentDiv.appendChild(codePanel); mermaidContainer.appendChild(tabsDiv); mermaidContainer.appendChild(contentDiv); container.appendChild(mermaidContainer); // 初始化mermaid渲染 setTimeout(() => { if (window.mermaid) { window.mermaid.init(undefined, mermaidDiv); } }, 100); } else { // 普通字符串内容 const textArea = document.createElement('textarea'); textArea.className = 'content-textarea'; textArea.value = content; textArea.onchange = () => updateSectionContent(sectionKey, textArea.value); container.appendChild(textArea); } } else if (Array.isArray(content)) { // 数组内容 content.forEach((item, index) => { const itemElement = createContentItem(sectionKey, item, index); container.appendChild(itemElement); }); } else if (typeof content === 'object' && content.mermaid) { // Mermaid图表内容 const mermaidContainer = document.createElement('div'); mermaidContainer.className = 'mermaid-container'; const mermaidDiv = document.createElement('div'); mermaidDiv.className = 'mermaid'; mermaidDiv.textContent = content.mermaid; const editButton = document.createElement('button'); editButton.className = 'edit-mermaid-btn'; editButton.innerHTML = '<i class="fas fa-edit"></i> 编辑图表'; editButton.onclick = () => editMermaidContent(sectionKey, content.mermaid); mermaidContainer.appendChild(mermaidDiv); mermaidContainer.appendChild(editButton); container.appendChild(mermaidContainer); // 初始化mermaid渲染 setTimeout(() => { if (window.mermaid) { window.mermaid.init(undefined, mermaidDiv); } }, 100); } } // 创建内容项 function createContentItem(sectionKey, item, index) { const itemDiv = document.createElement('div'); itemDiv.className = 'content-item'; itemDiv.dataset.index = index; itemDiv.innerHTML = ` <div class="title-with-add"> <h3 class="content-item-title" style="margin-left: 20px;"> <input type="text" class="title-input" value="${item.title}" style="background: var(--light-title-bg);" onchange="updateContentItemTitle('${sectionKey}', ${index}, this.value)"> </h3> </div> <div class="content-area" id="nested-content-${sectionKey}-${index}"> <!-- 嵌套内容将动态生成 --> </div> `; // 渲染嵌套内容 const nestedContentArea = itemDiv.querySelector(`#nested-content-${sectionKey}-${index}`); renderNestedContent(nestedContentArea, sectionKey, index, item.content); return itemDiv; } // 渲染嵌套内容 function renderNestedContent(container, sectionKey, itemIndex, content) { container.innerHTML = ''; if (typeof content === 'string') { // 字符串内容 const textArea = document.createElement('textarea'); textArea.className = 'content-textarea'; textArea.value = content; textArea.onchange = () => updateContentItemContent(sectionKey, itemIndex, textArea.value); container.appendChild(textArea); } else if (Array.isArray(content)) { // 数组内容(嵌套) const nestedDiv = document.createElement('div'); nestedDiv.className = 'nested-content'; content.forEach((nestedItem, nestedIndex) => { const nestedItemElement = createNestedContentItem(sectionKey, itemIndex, nestedItem, nestedIndex); nestedDiv.appendChild(nestedItemElement); }); container.appendChild(nestedDiv); } } // 创建嵌套内容项 function createNestedContentItem(sectionKey, itemIndex, nestedItem, nestedIndex) { const nestedItemDiv = document.createElement('div'); nestedItemDiv.className = 'content-item'; nestedItemDiv.dataset.nestedIndex = nestedIndex; nestedItemDiv.innerHTML = ` <div class="title-with-add"> <h4 class="content-item-title"> <input type="text" class="title-input" value="${nestedItem.title}" style="font-size: 13px;" onchange="updateNestedContentItemTitle('${sectionKey}', ${itemIndex}, ${nestedIndex}, this.value)"> </h4> </div> <textarea class="content-textarea" onchange="updateNestedContentItemContent('${sectionKey}', ${itemIndex}, ${nestedIndex}, this.value)">${nestedItem.content}</textarea> `; return nestedItemDiv; } // 更新文档标题 // 更新章节标题 function updateSectionTitle(sectionKey, newTitle) { const dataRoot = getDataRoot(); dataRoot.sections[sectionKey].title = newTitle; } // 获取章节内容数组的辅助函数 function getSectionContentArray(sectionKey) { const dataRoot = getDataRoot(); const section = dataRoot.sections[sectionKey]; return section.pageList || section.moduleList || section.content; } // 设置章节内容数组的辅助函数 function setSectionContentArray(sectionKey, contentArray) { const dataRoot = getDataRoot(); const section = dataRoot.sections[sectionKey]; if (section.pageList !== undefined) { section.pageList = contentArray; } else if (section.moduleList !== undefined) { section.moduleList = contentArray; } else { section.content = contentArray; } } // 更新章节内容 function updateSectionContent(sectionKey, newContent) { const dataRoot = getDataRoot(); const section = dataRoot.sections[sectionKey]; if (section.pageList !== undefined) { section.pageList = newContent; } else if (section.moduleList !== undefined) { section.moduleList = newContent; } else { section.content = newContent; } } // 更新内容项标题 function updateContentItemTitle(sectionKey, index, newTitle) { const contentArray = getSectionContentArray(sectionKey); contentArray[index].title = newTitle; } // 更新内容项内容 function updateContentItemContent(sectionKey, index, newContent) { const contentArray = getSectionContentArray(sectionKey); contentArray[index].content = newContent; } // 更新嵌套内容项标题 function updateNestedContentItemTitle(sectionKey, itemIndex, nestedIndex, newTitle) { const contentArray = getSectionContentArray(sectionKey); contentArray[itemIndex].content[nestedIndex].title = newTitle; } // 更新嵌套内容项内容 function updateNestedContentItemContent(sectionKey, itemIndex, nestedIndex, newContent) { const contentArray = getSectionContentArray(sectionKey); contentArray[itemIndex].content[nestedIndex].content = newContent; } // 添加内容项 function addContentItem(sectionKey) { const dataRoot = getDataRoot(); const section = dataRoot.sections[sectionKey]; // 根据章节类型创建不同的新项目结构 let newItem; if (section.pageList !== undefined) { // 对于pageList类型,添加页面相关的默认字段 newItem = { title: '新页面', content: [] }; } else { // 对于其他类型,使用标准结构 newItem = { title: '新内容项', content: '' }; } let contentArray = getSectionContentArray(sectionKey); if (!Array.isArray(contentArray)) { contentArray = []; setSectionContentArray(sectionKey, contentArray); } contentArray.push(newItem); renderAll(); } // 添加嵌套内容项 function addNestedContentItem(sectionKey, itemIndex) { const newNestedItem = { title: '新子内容项', content: '' }; const contentArray = getSectionContentArray(sectionKey); const item = contentArray[itemIndex]; if (!Array.isArray(item.content)) { item.content = []; } item.content.push(newNestedItem); renderAll(); } // 删除内容项 async function deleteContentItem(sectionKey, index) { const confirmed = await showCustomConfirm('确定要删除这个内容项吗?'); if (confirmed) { const contentArray = getSectionContentArray(sectionKey); contentArray.splice(index, 1); renderAll(); } } // 删除嵌套内容项 async function deleteNestedContentItem(sectionKey, itemIndex, nestedIndex) { const confirmed = await showCustomConfirm('确定要删除这个子内容项吗?'); if (confirmed) { const contentArray = getSectionContentArray(sectionKey); contentArray[itemIndex].content.splice(nestedIndex, 1); renderAll(); } } // 删除章节 async function deleteSection(sectionKey) { const confirmed = await showCustomConfirm('确定要删除整个章节吗?此操作不可撤销。'); if (confirmed) { delete prdData.productRequirementDocument.sections[sectionKey]; renderAll(); } } // 添加新章节 function addSection() { const sectionKey = 'newSection' + Date.now(); const newSection = { title: '新章节', content: [] }; prdData.productRequirementDocument.sections[sectionKey] = newSection; renderAll(); } // 保存配置功能 function saveConfig() { try { const dataStr = JSON.stringify(prdData, null, 2); console.log('💾 [prdDesign.html] 准备保存配置数据到VSCode'); // 通过VSCode webview API发送数据 if (vsCodeApi) { vsCodeApi.postMessage({ command: 'saveConfig', data: dataStr, fromWhere: fromWhere }); console.log('✅ [prdDesign.html] 配置数据已发送到VSCode,fromWhere:', fromWhere); } else { // 如果不在VSCode环境中,则下载文件作为备用方案 console.log('⚠️ [prdDesign.html] 不在VSCode环境中,使用下载备用方案'); const dataBlob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(dataBlob); const link = document.createElement('a'); link.href = url; link.download = 'prd-design-config.json'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } // 显示保存成功提示 const saveButton = document.querySelector('.save-button'); const originalText = saveButton.innerHTML; saveButton.innerHTML = '<i class="fas fa-check"></i><span>已保存</span>'; saveButton.style.background = '#34C759'; setTimeout(() => { saveButton.innerHTML = originalText; saveButton.style.background = ''; }, 2000); } catch (error) { console.error('保存配置失败:', error); // 保存失败时显示在按钮上 const saveButton = document.querySelector('.save-button'); const originalText = saveButton.innerHTML; saveButton.innerHTML = '<i class="fas fa-times"></i><span>保存失败</span>'; saveButton.style.background = '#FF3B30'; setTimeout(() => { saveButton.innerHTML = originalText; saveButton.style.background = ''; }, 2000); } } // 主题切换功能 function toggleTheme() { const body = document.body; const themeText = document.getElementById('theme-text'); const themeIcon = document.querySelector('.theme-toggle i'); if (body.getAttribute('data-theme') === 'light') { // 切换到暗色主题 body.removeAttribute('data-theme'); themeText.textContent = '亮色模式'; themeIcon.className = 'fas fa-sun'; localStorage.setItem('theme', 'dark'); } else { // 切换到亮色主题 body.setAttribute('data-theme', 'light'); themeText.textContent = '暗色模式'; themeIcon.className = 'fas fa-moon'; localStorage.setItem('theme', 'light'); } } // 初始化主题 function initTheme() { const savedTheme = localStorage.getItem('theme'); const themeText = document.getElementById('theme-text'); const themeIcon = document.querySelector('.theme-toggle i'); if (savedTheme === 'light') { document.body.setAttribute('data-theme', 'light'); themeText.textContent = '暗色模式'; themeIcon.className = 'fas fa-moon'; } else { themeText.textContent = '亮色模式'; themeIcon.className = 'fas fa-sun'; } } // 自定义确认删除弹框 let customConfirmCallback = null; function showCustomConfirm(message) { return new Promise((resolve) => { customConfirmCallback = resolve; document.getElementById('customConfirmMessage').textContent = message; const overlay = document.getElementById('customConfirmOverlay'); overlay.classList.add('show'); }); } function hideCustomConfirm(result) { const overlay = document.getElementById('customConfirmOverlay'); overlay.classList.remove('show'); if (customConfirmCallback) { customConfirmCallback(result); customConfirmCallback = null; } } // 侧滑面板功能 function toggleSidebar() { const sidebar = document.getElementById('sidebar'); const container = document.querySelector('.container'); const toggleIcon = document.querySelector('.sidebar-toggle i'); sidebar.classList.toggle('collapsed'); container.classList.toggle('sidebar-collapsed'); if (sidebar.classList.contains('collapsed')) { toggleIcon.className = 'fas fa-chevron-right'; } else { toggleIcon.className = 'fas fa-chevron-left'; } } // 生成文档目录 function generateTOC() { const tocContainer = document.getElementById('toc-container'); if (!tocContainer) { console.error('toc-container element not found'); return; } // 检查数据是否已加载 const dataRoot = getDataRoot(); if (!dataRoot || !dataRoot.sections) { console.warn('数据尚未加载,跳过目录生成'); tocContainer.innerHTML = ''; return; } tocContainer.innerHTML = ''; let headingIndex = 0; // 根据JSON数据结构生成目录 const sections = dataRoot.sections; Object.keys(sections).forEach(sectionKey => { const section = sections[sectionKey]; // 1级标题:章节标题 const sectionTocItem = createTocItem(section.title, 1, sectionKey, headingIndex++); tocContainer.appendChild(sectionTocItem); // 处理章节内容(支持pageList、moduleList和content) const contentArray = section.pageList || section.moduleList || section.content; if (Array.isArray(contentArray)) { contentArray.forEach((item, itemIndex) => { // 2级标题:内容项标题 const itemTocItem = createTocItem(item.title, 2, `${sectionKey}-${itemIndex}`, headingIndex++); tocContainer.appendChild(itemTocItem); // 处理嵌套内容 if (Array.isArray(item.content)) { item.content.forEach((nestedItem, nestedIndex) => { // 3级标题:嵌套内容项标题 const nestedTocItem = createTocItem(nestedItem.title, 3, `${sectionKey}-${itemIndex}-${nestedIndex}`, headingIndex++); tocContainer.appendChild(nestedTocItem); }); } }); } }); } // 创建目录项 function createTocItem(title, level, targetId, index) { const tocItem = document.createElement('div'); tocItem.className = `toc-item level-${level}`; tocItem.dataset.targetId = targetId; // 创建文本部分 const textSpan = document.createElement('span'); textSpan.className = 'toc-item-text'; textSpan.textContent = title; // 创建按钮容器 const buttonsDiv = document.createElement('div'); buttonsDiv.className = 'toc-item-buttons'; // 根据层级决定显示哪些按钮 const parts = targetId.split('-'); const sectionKey = parts[0]; // 1级标题(章节):只显示添加按钮 if (level === 1) { const addBtn = document.createElement('button'); addBtn.className = 'toc-btn toc-add-btn'; addBtn.innerHTML = '<i class="fas fa-plus"></i>'; addBtn.title = '添加内容项'; addBtn.onclick = (e) => { e.stopPropagation(); addContentItem(sectionKey); }; buttonsDiv.appendChild(addBtn); } // 2级标题(内容项):显示添加和删除按钮 else if (level === 2) { const addBtn = document.createElement('button'); addBtn.className = 'toc-btn toc-add-btn'; addBtn.innerHTML = '<i class="fas fa-plus"></i>'; addBtn.title = '添加子内容'; addBtn.onclick = (e) => { e.stopPropagation(); const itemIndex = parseInt(parts[1]); addNestedContentItem(sectionKey, itemIndex); }; const deleteBtn = document.createElement('button'); deleteBtn.className = 'toc-btn toc-delete-btn'; deleteBtn.innerHTML = '<i class="fas fa-times"></i>'; deleteBtn.title = '删除内容项'; deleteBtn.onclick = (e) => { e.stopPropagation(); const itemIndex = parseInt(parts[1]); deleteContentItem(sectionKey, itemIndex); }; buttonsDiv.appendChild(addBtn); buttonsDiv.appendChild(deleteBtn); } // 3级标题(嵌套内容项):只显示删除按钮 else if (level === 3) { const deleteBtn = document.createElement('button'); deleteBtn.className = 'toc-btn toc-delete-btn'; deleteBtn.innerHTML = '<i class="fas fa-times"></i>'; deleteBtn.title = '删除子内容项'; deleteBtn.onclick = (e) => { e.stopPropagation(); const itemIndex = parseInt(parts[1]); const nestedIndex = parseInt(parts[2]); deleteNestedContentItem(sectionKey, itemIndex, nestedIndex); }; buttonsDiv.appendChild(deleteBtn); } tocItem.appendChild(textSpan); tocItem.appendChild(buttonsDiv); // 点击文本部分的事件 textSpan.addEventListener('click', () => { const targetElement = findElementByDataPath(targetId); if (targetElement) { scrollToHeading(targetElement); setActiveTocItem(tocItem); } }); return tocItem; } // 根据数据路径查找对应的DOM元素 function findElementByDataPath(targetId) { const parts = targetId.split('-'); const sectionKey = parts[0]; if (parts.length === 1) { // 章节标题 return document.querySelector(`[data-section-key="${sectionKey}"] .section-title`); } else if (parts.length === 2) { // 内容项标题 const itemIndex = parts[1]; retu