;(function(){ // 全局错误捕获 try { const el = id => document.getElementById(id) // 声明 DOM 变量 (将在 init 中赋值) let $auth, $app let $systems, $modules, $notes, $attachments let $moduleNav, $noteNav let $systemDetail, $moduleDetail, $noteDetail let $attachmentFile, $btnUploadAttachment, $uploadProgress // 移除早期的检查,移至 init let current = { system: null, module: null, note: null, systems: [], modules: [], notes: [], attachments: [], categories: [] } const apiBase = location.pathname.startsWith('/tools/ai-lib/') ? '/tools/ai-lib/api/ai-lib' : '/api/ai-lib' // --- 核心逻辑函数 --- // 登录 async function login() { try { const userIn = el('login-user'); const passIn = el('login-pass'); if (!userIn || !passIn) return; const user = userIn.value.trim() const pass = passIn.value const s = el('auth-status'); if (s) { s.textContent = '正在登录...'; s.style.display = 'block'; } const res = await fetch(apiBase + '/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: user, password: pass }), credentials: 'same-origin' }) const data = await res.json() if (!data.ok) throw new Error(data.error || `登录失败 (HTTP ${res.status})`) $auth.style.display = 'none' $app.style.display = 'block' if (s) { s.textContent = ''; s.style.display = 'none'; } await loadCategories() await loadSystems() } catch (e) { const s = el('auth-status'); if (s) { s.textContent = String(e.message || e); s.style.display = 'block'; } alert('登录出错: ' + (e.message || e)); } } // 创建系统树形项 function createSystemTreeItem(sys, expanded = false) { const treeItem = document.createElement('div') treeItem.className = 'tree-item' treeItem.dataset.systemId = sys.id const header = document.createElement('div') header.className = `tree-item-header ${expanded ? 'expanded' : ''}` const toggle = document.createElement('span') toggle.className = `tree-toggle ${expanded ? 'expanded' : ''}` toggle.textContent = '▶' const icon = document.createElement('span') icon.className = 'system-icon' icon.textContent = '📦' const title = document.createElement('span') title.textContent = sys.name || '未命名系统' title.style.flex = '1' title.style.fontWeight = '600' const actions = document.createElement('div') actions.className = 'item-actions' const btnEdit = document.createElement('button') btnEdit.className = 'btn-icon btn-edit' btnEdit.textContent = '✏️' btnEdit.onclick = (e) => { e.stopPropagation(); editSystem(sys.id) } const btnDelete = document.createElement('button') btnDelete.className = 'btn-icon btn-delete' btnDelete.textContent = '🗑️' btnDelete.onclick = (e) => { e.stopPropagation(); deleteSystem(sys.id) } const btnAddChild = document.createElement('button') btnAddChild.className = 'btn-icon sm' btnAddChild.textContent = '✂️' btnAddChild.title = '新增子模块' btnAddChild.onclick = async (e) => { e.stopPropagation(); selectSystem(sys) await newModule() } actions.appendChild(btnEdit) actions.appendChild(btnDelete) actions.appendChild(btnAddChild) header.appendChild(toggle) header.appendChild(icon) header.appendChild(title) header.appendChild(actions) const content = document.createElement('div') content.className = `tree-item-content ${expanded ? 'expanded' : ''}` const childrenContainer = document.createElement('div') childrenContainer.className = 'tree-item-children' childrenContainer.innerHTML = '
加载中...
' content.appendChild(childrenContainer) treeItem.appendChild(header) treeItem.appendChild(content) // 点击展开/收缩 header.onclick = (e) => { if (e.target === toggle || e.target === header) { const isExpanded = header.classList.contains('expanded') header.classList.toggle('expanded') content.classList.toggle('expanded') toggle.classList.toggle('expanded') if (!isExpanded && content.classList.contains('expanded')) { // 展开时加载子模块 loadSystemModules(sys.id, childrenContainer) } } } // 点击系统标题选择系统 title.onclick = (e) => { e.stopPropagation() selectSystem(sys) } return treeItem } // 加载系统模块到树形结构 async function loadSystemModules(systemId, container) { showLoading('正在加载子模块...') try { const res = await fetch(`${apiBase}/modules?system_id=${systemId}`, { credentials: 'same-origin', cache: 'no-store' }) const data = await res.json() const modules = data.rows || [] const sys = (current.systems || []).find(s => String(s.id) === String(systemId)) container.innerHTML = '' if (modules.length === 0) { container.innerHTML = '
暂无子模块
' return } modules.forEach(mod => { const moduleItem = createModuleTreeItem(mod, sys) container.appendChild(moduleItem) }) } catch (e) { console.error('加载子模块失败:', e) container.innerHTML = '
加载失败
' } finally { hideLoading() } } // 创建子模块树形项(可展开显示开发纪要) function createModuleTreeItem(mod, sys) { const wrapper = document.createElement('div') wrapper.className = 'tree-module-wrapper' wrapper.dataset.moduleId = mod.id wrapper.dataset.moduleName = String(mod.name || '未命名模块') if (sys) { wrapper.dataset.systemId = String(sys.id) wrapper.dataset.systemName = String(sys.name || '未命名系统') } const header = document.createElement('div') header.className = 'tree-child-item' header.dataset.moduleId = mod.id const toggle = document.createElement('span') toggle.className = 'tree-toggle' toggle.textContent = '▶' const icon = document.createElement('span') icon.className = 'module-icon' icon.textContent = '🔧' const title = document.createElement('span') title.textContent = mod.name || '未命名模块' title.style.flex = '1' const actions = document.createElement('div') actions.className = 'item-actions' const btnAddModule = document.createElement('button') btnAddModule.className = 'btn-icon sm' btnAddModule.textContent = '➕' btnAddModule.title = '新增纪要' btnAddModule.onclick = async (e) => { e.stopPropagation(); if (sys) selectSystem(sys) selectModule(mod) await newNote() const notesContainer = wrapper.querySelector('.tree-note-list') if (notesContainer) await loadModuleNotes(mod.id, notesContainer) } const btnEdit = document.createElement('button') btnEdit.className = 'btn-icon btn-edit sm' btnEdit.textContent = '✏️' btnEdit.title = '编辑子模块' btnEdit.onclick = (e) => { e.stopPropagation(); if (sys) selectSystem(sys) selectModule(mod) } const btnDelete = document.createElement('button') btnDelete.className = 'btn-icon btn-delete sm' btnDelete.textContent = '🗑️' btnDelete.title = '删除子模块' btnDelete.onclick = async (e) => { e.stopPropagation(); await deleteModule(mod.id) } const btnAddChild = document.createElement('button') btnAddChild.className = 'btn-icon sm' btnAddChild.textContent = '✂️' btnAddChild.title = '新增纪要' btnAddChild.onclick = async (e) => { e.stopPropagation() if (sys) selectSystem(sys) selectModule(mod) await newNote() const notesContainer = wrapper.querySelector('.tree-note-list') if (notesContainer) await loadModuleNotes(mod.id, notesContainer) } actions.appendChild(btnAddModule) actions.appendChild(btnEdit) actions.appendChild(btnDelete) actions.appendChild(btnAddChild) header.appendChild(toggle) header.appendChild(icon) header.appendChild(title) header.appendChild(actions) const content = document.createElement('div') content.className = 'tree-item-content' const notesContainer = document.createElement('div') notesContainer.className = 'tree-note-list' notesContainer.innerHTML = '
加载中...
' content.appendChild(notesContainer) wrapper.appendChild(header) wrapper.appendChild(content) // 展开/收缩 header.onclick = async (e) => { if (e.target === header || e.target === toggle) { const isExpanded = content.classList.contains('expanded') content.classList.toggle('expanded') toggle.classList.toggle('expanded') if (!isExpanded) { await loadModuleNotes(mod.id, notesContainer) } } } // 点击标题选择模块 title.onclick = (e) => { e.stopPropagation() if (sys) selectSystem(sys) selectModule(mod) } return wrapper } // 加载系统列表 async function loadSystems() { showLoading('正在加载系统列表...') try { const res = await fetch(apiBase + '/systems', { credentials: 'same-origin', cache: 'no-store' }) const data = await res.json() current.systems = data.rows || [] const filterVal = document.querySelector('input[name="filter-major"]:checked')?.value || '' const filteredSystems = filterVal ? current.systems.filter(s => s.major_category === filterVal) : current.systems if ($systems) { $systems.innerHTML = '' if (filteredSystems.length === 0) { $systems.innerHTML = '
暂无系统,请点击右上角"新增系统"创建
' } else { const treeView = document.createElement('div') treeView.className = 'tree-view' filteredSystems.forEach((sys) => { const treeItem = createSystemTreeItem(sys, false) treeView.appendChild(treeItem) }) $systems.appendChild(treeView) } } // 保持当前导航状态,不要在首次加载时清空已渲染的系统列表 } catch (e) { console.error('加载系统失败:', e) alert('加载系统列表失败: ' + e.message) } finally { hideLoading() } } // 选择系统 function selectSystem(sys) { current.system = sys current.module = null current.note = null updatePath(`系统: ${sys.name}`) if ($systemDetail) $systemDetail.style.display = 'block' if ($moduleDetail) $moduleDetail.style.display = 'none' if ($noteDetail) $noteDetail.style.display = 'none' const elName = el('sys-name'), elIntro = el('sys-intro'); if (elName) elName.value = sys.name || '' if (elIntro) elIntro.value = sys.intro || '' const radios = document.querySelectorAll('input[name="sys-major"]') radios.forEach(r => r.checked = false) if (sys.major_category) { const target = document.querySelector(`input[name="sys-major"][value="${sys.major_category}"]`) if (target) target.checked = true } else { // Default for legacy data? Or just leave unchecked? // User requested "Initial data is 'Trae研发...'" but that's for new systems maybe? // If editing existing system without category, maybe better to leave unchecked or set default. // Let's check "Trae研发" if none is set, to encourage categorization. // const target = document.querySelector(`input[name="sys-major"][value="Trae研发"]`) // if (target) target.checked = true // Actually, if it's saved without category, showing a default might be misleading if they don't save. // But for better UX let's leave it unchecked if empty. } if ($moduleNav) $moduleNav.style.display = 'none' if ($noteNav) $noteNav.style.display = 'none' loadModules(sys.id) } // 加载子模块 async function loadModules(systemId) { showLoading('正在加载子模块...') try { const res = await fetch(`${apiBase}/modules?system_id=${systemId}`, { credentials: 'same-origin', cache: 'no-store' }) const data = await res.json() current.modules = data.rows || [] if ($modules) { $modules.innerHTML = '' current.modules.forEach(mod => { const item = document.createElement('div') item.className = 'module-item' const content = document.createElement('div') content.innerHTML = ` 🔧 ${mod.name || '未命名模块'}
${mod.description || '暂无说明'} ` const actions = document.createElement('div') actions.className = 'item-actions' const btnEdit = document.createElement('button') btnEdit.className = 'btn-icon btn-edit' btnEdit.textContent = '✏️' btnEdit.onclick = (e) => { e.stopPropagation(); editModule(mod.id) } const btnDelete = document.createElement('button') btnDelete.className = 'btn-icon btn-delete' btnDelete.textContent = '🗑️' btnDelete.onclick = (e) => { e.stopPropagation(); deleteModule(mod.id) } actions.appendChild(btnEdit) actions.appendChild(btnDelete) item.appendChild(content) item.appendChild(actions) item.onclick = () => selectModule(mod) $modules.appendChild(item) }) } } catch (e) { console.error('加载子模块失败:', e) alert('加载子模块失败: ' + e.message) } finally { hideLoading() } } // 选择子模块 function selectModule(mod) { current.module = mod current.note = null const sysName = (current.system && current.system.name) || (() => { const sid = mod && (mod.system_id || mod.systemId) const found = (current.systems || []).find(s => String(s.id) === String(sid)) return found ? found.name : '未选择系统' })() updatePath(`系统: ${sysName} → 子模块: ${mod.name}`) if ($systemDetail) $systemDetail.style.display = 'none' if ($moduleDetail) $moduleDetail.style.display = 'block' if ($noteDetail) $noteDetail.style.display = 'none' if (el('mod-name')) el('mod-name').value = mod.name || '' if (el('mod-desc')) el('mod-desc').value = mod.description || '' if (el('mod-addr')) el('mod-addr').value = mod.server_addr || '' if (el('mod-port')) el('mod-port').value = String(mod.server_port || '') if (el('mod-code')) el('mod-code').value = mod.code_type || '' if (el('mod-deploy')) el('mod-deploy').value = mod.deploy_method || '' if (el('mod-soft')) el('mod-soft').value = mod.software_versions || '' if ($noteNav) $noteNav.style.display = 'none' loadNotes(mod.id) } // 加载模块下的开发纪要到树 async function loadModuleNotes(moduleId, container) { showLoading('正在加载纪要...') try { const res = await fetch(`${apiBase}/notes?module_id=${moduleId}`, { credentials: 'same-origin', cache: 'no-store' }) const data = await res.json() const notes = data.rows || [] container.innerHTML = '' if (notes.length === 0) { const empty = document.createElement('div') empty.style.color = '#888' empty.textContent = '暂无开发纪要' container.appendChild(empty) const addRow = document.createElement('div') addRow.className = 'tree-note-item' const addBtn = document.createElement('button') addBtn.className = 'btn-icon sm' addBtn.textContent = '➕' addBtn.title = '新增纪要' addBtn.onclick = async (e) => { e.stopPropagation(); const mod = (current.modules || []).find(m => String(m.id) === String(moduleId)) if (mod) selectModule(mod) await newNote() await loadModuleNotes(moduleId, container) } addRow.appendChild(addBtn) container.appendChild(addRow) return } notes.forEach(note => { const row = createNoteTreeItem(note, moduleId) container.appendChild(row) }) } catch (e) { container.innerHTML = '
加载失败
' } finally { hideLoading() } } // 创建纪要树形项 function createNoteTreeItem(note, moduleId) { const row = document.createElement('div') row.className = 'tree-note-item' row.dataset.noteId = note.id row.dataset.moduleId = String(moduleId) const icon = document.createElement('span') icon.className = 'note-icon' icon.textContent = '📝' const title = document.createElement('span') const preview = (note.content || '').substring(0, 24) title.textContent = `${note.note_type} ${preview ? '· ' + preview + '...' : ''}` title.style.flex = '1' const actions = document.createElement('div') actions.className = 'item-actions' const btnAdd = document.createElement('button') btnAdd.className = 'btn-icon sm' btnAdd.textContent = '➕' btnAdd.title = '新增纪要' btnAdd.onclick = async (e) => { e.stopPropagation(); const mod = (current.modules || []).find(m => String(m.id) === String(moduleId)) if (mod) selectModule(mod) await newNote() const container = row.parentElement if (container) await loadModuleNotes(moduleId, container) } const btnEdit = document.createElement('button') btnEdit.className = 'btn-icon btn-edit sm' btnEdit.textContent = '✏️' btnEdit.title = '编辑纪要' btnEdit.onclick = (e) => { e.stopPropagation(); selectNote(note, moduleId) } const btnDelete = document.createElement('button') btnDelete.className = 'btn-icon btn-delete sm' btnDelete.textContent = '🗑️' btnDelete.title = '删除纪要' btnDelete.onclick = async (e) => { e.stopPropagation(); await deleteNote(note.id) const container = row.parentElement if (container) await loadModuleNotes(moduleId, container) } actions.appendChild(btnAdd) actions.appendChild(btnEdit) actions.appendChild(btnDelete) row.appendChild(icon) row.appendChild(title) row.appendChild(actions) row.onclick = (e) => { e.stopPropagation() selectNote(note, moduleId) } return row } // 加载开发纪要 async function loadNotes(moduleId) { showLoading('正在加载开发纪要...') try { const res = await fetch(`${apiBase}/notes?module_id=${moduleId}`, { credentials: 'same-origin', cache: 'no-store' }) const data = await res.json() current.notes = data.rows || [] if ($notes) { $notes.innerHTML = '' current.notes.forEach(note => { const item = document.createElement('div') item.className = 'note-item' const typeIcon = getNoteTypeIcon(note.note_type) const content = document.createElement('div') content.innerHTML = `${typeIcon} ${note.note_type}: ${note.content?.substring(0, 50) || '无内容'}...` const actions = document.createElement('div') actions.className = 'item-actions' const btnEdit = document.createElement('button') btnEdit.className = 'btn-icon btn-edit' btnEdit.textContent = '✏️' btnEdit.onclick = (e) => { e.stopPropagation(); editNote(note.id) } const btnDelete = document.createElement('button') btnDelete.className = 'btn-icon btn-delete' btnDelete.textContent = '🗑️' btnDelete.onclick = (e) => { e.stopPropagation(); deleteNote(note.id) } actions.appendChild(btnEdit) actions.appendChild(btnDelete) item.appendChild(content) item.appendChild(actions) item.onclick = () => selectNote(note) $notes.appendChild(item) }) } } catch (e) { console.error('加载纪要失败:', e) alert('加载纪要失败: ' + e.message) } finally { hideLoading() } } // 选择开发纪要 function selectNote(note, moduleId) { current.note = note const modIdStr = moduleId ? String(moduleId) : (note && note.module_id ? String(note.module_id) : '') const wrapper = modIdStr ? document.querySelector(`.tree-module-wrapper[data-module-id="${modIdStr}"]`) : null const sysName = (current.system && current.system.name) || (wrapper && wrapper.dataset.systemName) || '未选择系统' const modName = (current.module && current.module.name) || (wrapper && wrapper.dataset.moduleName) || '未选择子模块' if (!current.module && modIdStr) { const sysId = wrapper && wrapper.dataset.systemId ? parseInt(wrapper.dataset.systemId, 10) : null current.module = { id: parseInt(modIdStr, 10), name: modName, system_id: sysId } } updatePath(`系统: ${sysName} → 子模块: ${modName} → 纪要: ${note.note_type}`) if ($systemDetail) $systemDetail.style.display = 'none' if ($moduleDetail) $moduleDetail.style.display = 'none' if ($noteDetail) $noteDetail.style.display = 'block' if (el('note-type')) el('note-type').value = note.note_type || '需求' if (el('note-content')) el('note-content').value = note.content || '' // 加载附件 if ($attachments) $attachments.innerHTML = '
附件列表加载中...
' loadAttachments(note.id) // 显示附件上传区域 if ($noteDetail) { const attachmentSection = $noteDetail.querySelector('.attachment-section') if (attachmentSection) attachmentSection.style.display = 'block' } } // 加载附件 async function loadAttachments(noteId) { try { const res = await fetch(`${apiBase}/attachments?note_id=${noteId}`, { credentials: 'same-origin', cache: 'no-store' }) const data = await res.json() const rows = data.rows || [] // 防止竞态:若用户已切换到其他纪要,忽略过期结果 if (current.note && String(current.note.id) !== String(noteId)) return current.attachments = rows if ($attachments) { $attachments.innerHTML = '' if (rows.length === 0) { const empty = document.createElement('div') empty.style.color = '#888' empty.textContent = '暂无附件' $attachments.appendChild(empty) return } rows.forEach(att => { const item = document.createElement('div') item.className = 'attachment-item' const content = document.createElement('div') content.innerHTML = `📎 ${att.filename} (${formatFileSize(att.size)})` const actions = document.createElement('div') actions.className = 'item-actions' const btnDelete = document.createElement('button') btnDelete.className = 'btn-icon btn-delete' btnDelete.textContent = '🗑️' btnDelete.onclick = (e) => { e.stopPropagation(); deleteAttachment(att.id) } actions.appendChild(btnDelete) item.appendChild(content) item.appendChild(actions) item.onclick = () => downloadAttachment(att) $attachments.appendChild(item) }) } } catch (e) { if ($attachments) { $attachments.innerHTML = '
附件列表加载失败
' } console.error('加载附件失败:', e) } } // 工具函数 function getNoteTypeIcon(type) { const icons = { '需求': '📋', '开发': '💻', '部署': '🚀', '维护': '🔧' } return icons[type] || '📝' } function formatFileSize(bytes) { if (!bytes) return '0 B' const sizes = ['B', 'KB', 'MB', 'GB'] const i = Math.floor(Math.log(bytes) / Math.log(1024)) return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i] } function updatePath(text) { const p = el('current-path') if (p) p.textContent = text } function showLoading(text) { const overlay = el('loading-overlay') const msg = el('loading-text') if (msg && typeof text === 'string' && text) msg.textContent = text if (overlay) overlay.style.display = 'flex' } function hideLoading() { const overlay = el('loading-overlay') if (overlay) overlay.style.display = 'none' } function resetNavigation() { if ($moduleNav) $moduleNav.style.display = 'none' if ($noteNav) $noteNav.style.display = 'none' if ($systemDetail) $systemDetail.style.display = 'none' if ($moduleDetail) $moduleDetail.style.display = 'none' if ($noteDetail) $noteDetail.style.display = 'none' current.system = null current.module = null current.note = null if ($systems) $systems.innerHTML = '' // 注意:loadSystems 会重新填充 if ($modules) $modules.innerHTML = '' if ($notes) $notes.innerHTML = '' if ($attachments) $attachments.innerHTML = '' updatePath('请选择系统 → 子模块 → 开发纪要') if ($noteDetail) { const attachmentSection = $noteDetail.querySelector('.attachment-section') if (attachmentSection) { attachmentSection.style.display = 'none' } } } // 保存系统 async function saveSystem() { if (!current.system) return const name = (el('sys-name').value || '').trim() const intro = (el('sys-intro').value || '').trim() const major_category = document.querySelector('input[name="sys-major"]:checked')?.value || '' if (!name || !intro) { alert('系统名称与简介为必填'); return } const payload = { name, intro, major_category } showLoading('正在保存系统...') try { let res, data if (current.system && current.system.id) { res = await fetch(`${apiBase}/systems/${current.system.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), credentials: 'same-origin' }) data = await res.json() if (!data.ok) throw new Error('保存失败') alert('系统已保存') await loadSystems() } else { res = await fetch(`${apiBase}/systems`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), credentials: 'same-origin' }) data = await res.json() if (!data.ok) throw new Error(data.error || '创建失败') alert('系统已创建') await loadSystems() const created = (current.systems || []).find(s => String(s.id) === String(data.id)) if (created) selectSystem(created) } } catch (e) { alert('保存失败: ' + e.message) } finally { hideLoading() } } // 保存子模块 async function saveModule() { if (!current.system) return const name = (el('mod-name').value || '').trim() const description = (el('mod-desc').value || '').trim() const server_addr = (el('mod-addr').value || '').trim() const portStr = (el('mod-port').value || '').trim() const server_port = parseInt(portStr || '0', 10) const code_type = (el('mod-code').value || '').trim() const deploy_method = (el('mod-deploy').value || '').trim() const software_versions = (el('mod-soft').value || '').trim() if (!name || !description || !server_addr || !portStr || !(server_port > 0) || !code_type || !deploy_method || !software_versions) { alert('子模块所有字段均为必填,端口需为正整数'); return } const payload = { system_id: current.system.id, name, description, server_addr, server_port, code_type, deploy_method, software_versions } showLoading('正在保存子模块...') try { let res, data if (current.module && current.module.id) { payload.id = current.module.id res = await fetch(`${apiBase}/modules/${current.module.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), credentials: 'same-origin' }) } else { res = await fetch(`${apiBase}/modules`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), credentials: 'same-origin' }) } data = await res.json() if (!data.ok) throw new Error('保存失败') alert('子模块已保存') await loadModules(current.system.id) // 刷新树形中的当前系统子模块列表 const sysChildren = document.querySelector(`.tree-item[data-system-id="${current.system.id}"] .tree-item-children`) if (sysChildren) await loadSystemModules(current.system.id, sysChildren) else { await loadSystems() const sysNode = document.querySelector(`.tree-item[data-system-id="${current.system.id}"]`) if (sysNode) { const header = sysNode.querySelector('.tree-item-header') const content = sysNode.querySelector('.tree-item-content') const children = sysNode.querySelector('.tree-item-children') const toggle = header && header.querySelector('.tree-toggle') if (header) header.classList.add('expanded') if (content) content.classList.add('expanded') if (toggle) toggle.classList.add('expanded') if (children) await loadSystemModules(current.system.id, children) } } } catch (e) { alert('保存失败: ' + e.message) } finally { hideLoading() } } // 保存开发纪要 async function saveNote() { if (!current.module) return const note_type = (el('note-type').value || '').trim() const content = (el('note-content').value || '').trim() if (!note_type || !content) { alert('纪要类型与内容为必填'); return } const payload = { module_id: current.module.id, note_type, content } showLoading('正在保存开发纪要...') try { let res, data if (current.note && current.note.id) { payload.id = current.note.id res = await fetch(`${apiBase}/notes/${current.note.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), credentials: 'same-origin' }) } else { res = await fetch(`${apiBase}/notes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), credentials: 'same-origin' }) } data = await res.json() if (!data.ok) throw new Error('保存失败') alert('开发纪要已保存') await loadNotes(current.module.id) // 刷新树形中的当前模块纪要列表 const notesContainer = document.querySelector(`.tree-module-wrapper[data-module-id="${current.module.id}"] .tree-note-list`) if (notesContainer) await loadModuleNotes(current.module.id, notesContainer) } catch (e) { alert('保存失败: ' + e.message) } finally { hideLoading() } } // 新增系统:进入新增模式,不立即提交 async function newSystem() { current.system = { id: null, name: '', intro: '' } updatePath('系统: 新增') if ($systemDetail) $systemDetail.style.display = 'block' if ($moduleDetail) $moduleDetail.style.display = 'none' if ($noteDetail) $noteDetail.style.display = 'none' if (el('sys-name')) el('sys-name').value = '' if (el('sys-intro')) el('sys-intro').value = '' const radios = document.querySelectorAll('input[name="sys-major"]') radios.forEach(r => { if (r.value === 'Trae研发') r.checked = true else r.checked = false }) } // 新增子模块 async function newModule() { if (!current.system) return current.module = null updatePath(`系统: ${current.system.name} → 子模块: 新增`) if ($systemDetail) $systemDetail.style.display = 'none' if ($moduleDetail) $moduleDetail.style.display = 'block' if ($noteDetail) $noteDetail.style.display = 'none' if (el('mod-name')) el('mod-name').value = '' if (el('mod-desc')) el('mod-desc').value = '' if (el('mod-addr')) el('mod-addr').value = '' if (el('mod-port')) el('mod-port').value = '' if (el('mod-code')) el('mod-code').value = '' if (el('mod-deploy')) el('mod-deploy').value = '' if (el('mod-soft')) el('mod-soft').value = '' } // 新增开发纪要 async function newNote() { if (!current.module) return current.note = null updatePath(`系统: ${current.system ? current.system.name : ''} → 子模块: ${current.module.name} → 纪要: 新增`) if ($systemDetail) $systemDetail.style.display = 'none' if ($moduleDetail) $moduleDetail.style.display = 'none' if ($noteDetail) $noteDetail.style.display = 'block' if (el('note-type')) el('note-type').value = '需求' if (el('note-content')) el('note-content').value = '' const attachmentSection = $noteDetail && $noteDetail.querySelector('.attachment-section') if (attachmentSection) attachmentSection.style.display = 'none' } // 上传附件 async function uploadAttachments() { if (!$attachmentFile) return; const files = $attachmentFile.files if (files.length === 0) { alert('请选择至少一个文件') return } if (!current.note) { alert('请先选择开发纪要') return } if ($btnUploadAttachment) $btnUploadAttachment.disabled = true if ($uploadProgress) $uploadProgress.innerHTML = '' let successCount = 0 let errorCount = 0 for (let i = 0; i < files.length; i++) { const file = files[i] const progressId = 'progress-' + Date.now() + '-' + i const progressDiv = document.createElement('div') progressDiv.id = progressId progressDiv.innerHTML = `
📄 ${file.name} (${formatFileSize(file.size)}) 0%
` if ($uploadProgress) $uploadProgress.appendChild(progressDiv) try { const url = `${apiBase}/attachments/upload?note_id=${encodeURIComponent(current.note.id)}&filename=${encodeURIComponent(file.name)}&mime=${encodeURIComponent(file.type||'application/octet-stream')}` const res = await fetch(url, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/octet-stream' }, body: file }) const data = await res.json() if (!data.ok) throw new Error(data.error || '上传失败') successCount++ const progressBar = document.querySelector(`#${progressId} progress`) const progressText = document.querySelector(`#${progressId} span:last-child`) if (progressBar && progressText) { progressBar.value = 100 progressText.textContent = '✅ 完成' } } catch (e) { errorCount++ const progressBar = document.querySelector(`#${progressId} progress`) const progressText = document.querySelector(`#${progressId} span:last-child`) if (progressBar && progressText) { progressBar.value = 0 progressText.textContent = '❌ 失败: ' + e.message progressText.style.color = '#ff6b6b' } } } if ($btnUploadAttachment) $btnUploadAttachment.disabled = false if ((successCount > 0 || errorCount > 0) && $uploadProgress) { const resultDiv = document.createElement('div') resultDiv.style.marginTop = '16px' resultDiv.style.padding = '8px' resultDiv.style.background = successCount > 0 ? '#2b8a3e20' : '#c92a2a20' resultDiv.style.border = successCount > 0 ? '1px solid #2b8a3e' : '1px solid #c92a2a' resultDiv.style.borderRadius = '4px' resultDiv.innerHTML = ` 📊 上传结果:
✅ 成功: ${successCount} 个文件
❌ 失败: ${errorCount} 个文件 ` $uploadProgress.appendChild(resultDiv) if (successCount > 0) { await loadAttachments(current.note.id) } } } // 下载附件 async function downloadAttachment(attachment) { try { const res = await fetch(`${apiBase}/attachments/${attachment.id}/download`, { credentials: 'same-origin' }) if (!res.ok) throw new Error('下载失败') const blob = await res.blob() const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = attachment.filename document.body.appendChild(a) a.click() window.URL.revokeObjectURL(url) document.body.removeChild(a) } catch (e) { alert('下载失败: ' + e.message) } } // CRUD 操作 async function deleteSystem(systemId) { if (!confirm('确定要删除这个系统吗?此操作将删除所有相关的子模块、纪要和附件!')) return showLoading('正在删除系统...') try { const res = await fetch(`${apiBase}/systems/${systemId}`, { method: 'DELETE', credentials: 'same-origin' }) const data = await res.json() if (!data.ok) throw new Error('删除失败') alert('系统已删除') await loadSystems() } catch (e) { alert('删除失败: ' + e.message) } finally { hideLoading() } } async function editSystem(systemId) { const system = current.systems.find(s => s.id === systemId) if (system) selectSystem(system) } async function deleteModule(moduleId) { if (!confirm('确定要删除这个子模块吗?此操作将删除所有相关的纪要和附件!')) return showLoading('正在删除子模块...') try { const res = await fetch(`${apiBase}/modules/${moduleId}`, { method: 'DELETE', credentials: 'same-origin' }) const data = await res.json() if (!data.ok) throw new Error('删除失败') alert('子模块已删除') await loadModules(current.system.id) } catch (e) { alert('删除失败: ' + e.message) } finally { hideLoading() } } async function editModule(moduleId) { const module = current.modules.find(m => m.id === moduleId) if (module) selectModule(module) } async function deleteNote(noteId) { if (!confirm('确定要删除这个开发纪要吗?此操作将删除所有相关的附件!')) return showLoading('正在删除纪要...') try { const res = await fetch(`${apiBase}/notes/${noteId}`, { method: 'DELETE', credentials: 'same-origin' }) const data = await res.json() if (!data.ok) throw new Error('删除失败') alert('开发纪要已删除') await loadNotes(current.module.id) } catch (e) { alert('删除失败: ' + e.message) } finally { hideLoading() } } async function editNote(noteId) { const note = current.notes.find(n => n.id === noteId) if (note) selectNote(note) } async function deleteAttachment(attachmentId) { if (!confirm('确定要删除这个附件吗?')) return showLoading('正在删除附件...') try { const res = await fetch(`${apiBase}/attachments/${attachmentId}`, { method: 'DELETE', credentials: 'same-origin' }) const data = await res.json() if (!data.ok) throw new Error('删除失败') alert('附件已删除') await loadAttachments(current.note.id) } catch (e) { alert('删除失败: ' + e.message) } finally { hideLoading() } } // --- 大类管理逻辑 --- async function loadCategories() { try { const res = await fetch(apiBase + '/categories', { cache: 'no-store', credentials: 'same-origin' }) const data = await res.json() if (data.ok) { current.categories = data.rows || [] renderCategories() } } catch (e) { console.error('加载大类失败:', e) } } function renderCategories() { // 1. 渲染右上角筛选 const filterContainer = el('filter-major-dynamic') if (filterContainer) { // 保留当前选中的值 const currentFilter = document.querySelector('input[name="filter-major"]:checked')?.value || '' filterContainer.innerHTML = current.categories.map(c => ` `).join('') // 重新绑定筛选事件 const radios = document.querySelectorAll('input[name="filter-major"]') radios.forEach(r => { r.onchange = () => { resetNavigation() loadSystems() } }) } // 2. 渲染系统详情页的大类单选 const formContainer = el('sys-major-dynamic') if (formContainer) { formContainer.innerHTML = current.categories.map(c => ` `).join('') // 如果当前正在编辑系统,恢复选中值 if (current.system && current.system.major_category) { const r = formContainer.querySelector(`input[value="${current.system.major_category}"]`) if (r) r.checked = true } } } async function openCategoryManager() { let modal = el('category-manager-modal') if (!modal) { modal = document.createElement('div') modal.id = 'category-manager-modal' modal.className = 'modal-overlay open' modal.style.zIndex = '2000' document.body.appendChild(modal) } const renderModalContent = () => { modal.innerHTML = ` ` setTimeout(() => { const btnAdd = modal.querySelector('#btn-add-cat') const input = modal.querySelector('#new-cat-input') if (btnAdd && input) { btnAdd.onclick = async () => { const name = input.value.trim() if (!name) return alert('请输入名称') await apiCreateCategory(name) input.value = '' renderModalContent() } } }, 0) } window.aiLib_promptEditCategory = async (id, oldName) => { const newName = prompt('请输入新的大类名称:', oldName) if (newName && newName.trim() && newName !== oldName) { await apiUpdateCategory(id, newName.trim()) renderModalContent() } } window.aiLib_confirmDeleteCategory = async (id, name) => { if (confirm(`确定要删除大类“${name}”吗?`)) { await apiDeleteCategory(id) renderModalContent() } } renderModalContent() modal.style.display = 'flex' } async function apiCreateCategory(name) { try { const res = await fetch(apiBase + '/categories', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }), cache: 'no-store' }) const data = await res.json() if (!data.ok) throw new Error(data.error) await loadCategories() } catch(e) { alert('添加失败: ' + e.message) } } async function apiUpdateCategory(id, name) { try { const res = await fetch(apiBase + '/categories/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }), cache: 'no-store' }) const data = await res.json() if (!data.ok) throw new Error(data.error) await loadCategories() } catch(e) { alert('更新失败: ' + e.message) } } async function apiDeleteCategory(id) { try { const res = await fetch(apiBase + '/categories/' + id, { method: 'DELETE', cache: 'no-store' }) const data = await res.json() if (!data.ok) throw new Error(data.error) await loadCategories() } catch(e) { alert('删除失败: ' + e.message) } } // 绑定事件 function bindEvents() { const btnLogin = el('btn-login'); if (btnLogin) btnLogin.onclick = login ;(async function tryAutoFill() { try { const token = localStorage.getItem('toolbox_creds_token') || '' const r = await fetch('/api/ai-lib/auth/local_creds', { headers: token ? { 'X-Local-Creds-Token': token } : {}, credentials: 'same-origin', cache: 'no-store' }) const d = await r.json().catch(() => null) if (d && d.ok && d.creds) { if (d.creds.username) el('login-user').value = d.creds.username if (d.creds.password) el('login-pass').value = d.creds.password } } catch {} el('login-user').removeAttribute('readonly') el('login-pass').removeAttribute('readonly') })() const btnNewSystem = el('btn-new-system'); if (btnNewSystem) { btnNewSystem.onclick = newSystem } else { console.warn('Button btn-new-system not found in DOM') } const btnNewModule = el('btn-new-module'); if (btnNewModule) btnNewModule.onclick = newModule const btnNewNote = el('btn-new-note'); if (btnNewNote) btnNewNote.onclick = newNote const btnRefreshTree = el('btn-refresh-tree'); if (btnRefreshTree) btnRefreshTree.onclick = () => loadSystems() const btnManageCategories = el('btn-manage-categories'); if (btnManageCategories) btnManageCategories.onclick = openCategoryManager // 已移除详情标题处的新增按钮 const btnSaveSystem = el('btn-save-system'); if (btnSaveSystem) btnSaveSystem.onclick = saveSystem const btnSaveModule = el('btn-save-module'); if (btnSaveModule) btnSaveModule.onclick = saveModule const btnSaveNote = el('btn-save-note'); if (btnSaveNote) btnSaveNote.onclick = saveNote if ($btnUploadAttachment) $btnUploadAttachment.onclick = uploadAttachments const filterRadios = document.querySelectorAll('input[name="filter-major"]') filterRadios.forEach(r => { r.onchange = () => { resetNavigation() loadSystems() } }) } async function init() { console.log('AI Lib initializing...') // 初始化 DOM 引用 $auth = el('auth') $app = el('app') $systems = el('systems') $modules = el('modules') $notes = el('notes') $attachments = el('attachments') $moduleNav = el('module-nav') $noteNav = el('note-nav') $systemDetail = el('system-detail') $moduleDetail = el('module-detail') $noteDetail = el('note-detail') $attachmentFile = el('attachment-file') $btnUploadAttachment = el('btn-upload-attachment') $uploadProgress = el('upload-progress') if (!$auth || !$app || !$systems) { console.error('Critical elements missing:', { auth: !!$auth, app: !!$app, systems: !!$systems }) alert('系统初始化失败:关键页面元素缺失。请查看控制台了解详情。') return } bindEvents() const isLoggedIn = await loadCategories() if (isLoggedIn) { $auth.style.display = 'none' $app.style.display = 'block' await loadSystems() } resetNavigation() console.log('AI Lib initialized successfully.') // alert('系统脚本已更新并加载 (v2)') // Debug alert } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init) } else { init() } } catch (err) { console.error('Script initialization error:', err); alert('脚本初始化错误: ' + err.message); } })()