1387 lines
52 KiB
JavaScript
1387 lines
52 KiB
JavaScript
;(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 = '<div style="padding: 8px 16px; color: #888; font-style: italic;">加载中...</div>'
|
||
|
||
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 = '<div style="padding: 8px 16px; color: #888; font-style: italic;">暂无子模块</div>'
|
||
return
|
||
}
|
||
|
||
modules.forEach(mod => {
|
||
const moduleItem = createModuleTreeItem(mod, sys)
|
||
container.appendChild(moduleItem)
|
||
})
|
||
} catch (e) {
|
||
console.error('加载子模块失败:', e)
|
||
container.innerHTML = '<div style="padding: 8px 16px; color: #f44747; font-style: italic;">加载失败</div>'
|
||
} 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 = '<div>加载中...</div>'
|
||
|
||
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 = '<div style="padding: 20px; text-align: center; color: #888;">暂无系统,请点击右上角"新增系统"创建</div>'
|
||
} 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 = `
|
||
<strong>🔧 ${mod.name || '未命名模块'}</strong><br>
|
||
<small>${mod.description || '暂无说明'}</small>
|
||
`
|
||
|
||
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 = '<div style="color:#f44747">加载失败</div>'
|
||
} 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 = `<strong>${typeIcon} ${note.note_type}: ${note.content?.substring(0, 50) || '无内容'}...</strong>`
|
||
|
||
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 = '<div style="color:#888">附件列表加载中...</div>'
|
||
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 = '<div style="color:#f44747">附件列表加载失败</div>'
|
||
}
|
||
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 = `
|
||
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
||
<span style="flex: 1; font-size: 12px;">📄 ${file.name} (${formatFileSize(file.size)})</span>
|
||
<progress value="0" max="100" style="width: 100px; margin: 0 10px;"></progress>
|
||
<span style="font-size: 12px;">0%</span>
|
||
</div>
|
||
`
|
||
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 = `
|
||
<strong>📊 上传结果:</strong><br>
|
||
✅ 成功: ${successCount} 个文件<br>
|
||
❌ 失败: ${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 => `
|
||
<label><input type="radio" name="filter-major" value="${c.name}" ${currentFilter === c.name ? 'checked' : ''}> ${c.name}</label>
|
||
`).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 => `
|
||
<label><input type="radio" name="sys-major" value="${c.name}"> ${c.name}</label>
|
||
`).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 = `
|
||
<div class="modal" style="max-width: 500px;">
|
||
<div class="modal-header">
|
||
<h3>🔧 大类管理</h3>
|
||
<button class="btn-icon close-modal" onclick="document.getElementById('category-manager-modal').style.display='none'">✖️</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div style="display:flex; gap:10px; margin-bottom:15px; border-bottom:1px solid #eee; padding-bottom:15px;">
|
||
<input id="new-cat-input" placeholder="输入新大类名称" style="flex:1;">
|
||
<button id="btn-add-cat" class="btn">添加</button>
|
||
</div>
|
||
<div id="cat-list" style="max-height:400px; overflow-y:auto;">
|
||
${current.categories.map(c => `
|
||
<div style="display:flex; align-items:center; padding:8px 0; border-bottom:1px solid #f5f5f5; justify-content: space-between;">
|
||
<span style="font-weight:500;">${c.name}</span>
|
||
<div>
|
||
<button class="btn-icon sm" onclick="window.aiLib_promptEditCategory('${c.id}', '${c.name}')" title="重命名">✏️</button>
|
||
<button class="btn-icon sm btn-delete" onclick="window.aiLib_confirmDeleteCategory('${c.id}', '${c.name}')" title="删除">🗑️</button>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn" onclick="document.getElementById('category-manager-modal').style.display='none'">关闭</button>
|
||
</div>
|
||
</div>
|
||
`
|
||
|
||
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 url = '/api/ai-lib/auth/local_creds' + (token ? '?token=' + encodeURIComponent(token) : '')
|
||
const r = await fetch(url, { 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);
|
||
}
|
||
})()
|