bytefun
Version:
一个打通了原型设计、UI设计与代码转换、跨平台原生代码开发等的平台
1,517 lines (1,311 loc) • 73.4 kB
HTML
<!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