;(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 = `
${current.categories.map(c => `
`).join('')}
`
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);
}
})()