@@ -1085,6 +1085,41 @@ INDEX_TEMPLATE = '''
color: #fff;
}
.folder-list a i { margin-right: 5px; }
.folder-list .folder-item {
display: flex;
align-items: center;
padding: 6px 10px;
position: relative;
}
.folder-list .folder-item a {
padding: 6px 10px;
flex-grow: 1;
display: flex;
align-items: center;
}
.folder-list .folder-actions {
position: absolute;
right: 10px;
display: none;
gap: 4px;
background: #343a40;
padding: 4px 8px;
border-radius: 4px;
}
.folder-list .folder-item:hover .folder-actions {
display: flex;
}
.folder-list .folder-actions .btn {
padding: 2px 6px;
font-size: 12px;
line-height: 1;
}
.folder-list .folder-item:hover {
background: #495057;
}
.folder-list .folder-item:hover a {
color: #fff;
}
.folder-action {
font-size: 12px;
color: #6c757d;
@@ -1474,6 +1509,12 @@ INDEX_TEMPLATE = '''
</label>
</div>
</div>
<div class= " mb-3 " >
<label class= " form-label " >所属文件夹</label>
<select id= " addFolder " class= " form-select " >
<option value= " " >未分类(根目录)</option>
</select>
</div>
</form>
</div>
<div class= " modal-footer " >
@@ -1588,6 +1629,12 @@ INDEX_TEMPLATE = '''
</label>
</div>
</div>
<div class= " mb-3 " >
<label class= " form-label " >所属文件夹</label>
<select id= " editFolder " class= " form-select " >
<option value= " " >未分类(根目录)</option>
</select>
</div>
</form>
</div>
<div class= " modal-footer " >
@@ -1867,29 +1914,30 @@ INDEX_TEMPLATE = '''
</div>
</div>
<!-- 新建文件夹模态框 -->
<!-- 新建/编辑 文件夹模态框 -->
<div class= " modal fade " id= " newFolderModal " tabindex= " -1 " >
<div class= " modal-dialog " >
<div class= " modal-content " >
<div class= " modal-header " >
<h5 class= " modal-title " ><i class= " bi bi-folder-plus " ></i> 新建文件夹</h5>
<h5 class= " modal-title " id= " folderModalTitle " ><i class=" bi bi-folder-plus " ></i> 新建文件夹</h5>
<button type= " button " class= " btn-close " data-bs-dismiss= " modal " ></button>
</div>
<div class= " modal-body " >
<input type= " hidden " id= " newFolderType " >
<input type= " hidden " id= " editFolderId " >
<div class= " mb-3 " >
<label class= " form-label " >文件夹名称</label>
<input type= " text " id= " newFolderName " class= " form-control " placeholder= " 输入文件夹名称 " >
</div>
<div class= " mb-3 " >
<div class= " mb-3 " id= " folderTypeRow " >
<label class= " form-label " >所属类别</label>
<input type= " text " id= " newFolderTypeName " class= " form-control " readonly>
</div>
</div>
<div class= " modal-footer " >
<button type= " button " class= " btn btn-secondary " data-bs-dismiss= " modal " >取消</button>
<button type= " button " class= " btn btn-primary " onclick= " creat eFolder()" >
<i class= " bi bi-check " ></i> 创建
<button type= " button " class= " btn btn-primary " onclick= " sav eFolder()" >
<i class= " bi bi-check " ></i> <span id= " folderSaveBtnText " >创建</span>
</button>
</div>
</div>
@@ -2177,6 +2225,8 @@ document.addEventListener('DOMContentLoaded', async () => {
// 编辑时类型切换
document.getElementById( ' editType ' ).addEventListener( ' change ' , (e) => {
updateEditFieldsByType(e.target.value);
// 切换类型时更新文件夹列表
loadFolderSelect(e.target.value, ' editFolder ' );
});
// 搜索 - 直接绑定,不用 debounce
@@ -2648,6 +2698,9 @@ function showAddModal(type) {
// 设置类型
document.getElementById( ' addType ' ).value = type;
// 重置文件夹ID( 从顶部按钮添加时不指定文件夹)
currentAddFolderId = null;
// 只有不是编辑草稿时才重置
// currentDraftId 在 editDraft 中已设置,不要覆盖
@@ -2677,6 +2730,9 @@ function showAddModal(type) {
// 打开弹窗
new bootstrap.Modal(document.getElementById( ' addModal ' )).show();
// 加载该类型下的文件夹列表
loadFolderSelect(type, ' addFolder ' , currentAddFolderId);
// 启动自动保存
startAutoSave();
@@ -2704,7 +2760,8 @@ async function addItem() {
due_date: type === ' todo ' ? document.getElementById( ' addDueDate ' ).value : null,
note: document.getElementById( ' addNote ' ).value,
tags: document.getElementById( ' addTags ' ).value.split( ' , ' ).map(t => t.trim()).filter(t => t),
is_starred: document.getElementById( ' addStarred ' ).checked
is_starred: document.getElementById( ' addStarred ' ).checked,
folder_id: document.getElementById( ' addFolder ' ).value ? parseInt(document.getElementById( ' addFolder ' ).value) : currentAddFolderId
};
const res = await fetch(`$ {API_BASE} /items`, {
@@ -2725,6 +2782,7 @@ async function addItem() {
stopAutoSave();
hideDraftIndicator();
currentAddFolderId = null; // 重置文件夹ID
refreshData();
}
}
@@ -3266,6 +3324,9 @@ async function openEditModal(id) {
// 设置重点关注状态
document.getElementById( ' editStarred ' ).checked = item.is_starred === 1;
// 加载文件夹列表并设置当前文件夹
await loadFolderSelect(type, ' editFolder ' , item.folder_id);
// 保存原始数据用于比较
window.editOriginalData = {
type,
@@ -3354,7 +3415,8 @@ async function saveEdit() {
due_date: type === ' todo ' ? document.getElementById( ' editDueDate ' ).value : null,
note: document.getElementById( ' editNote ' ).value,
tags: document.getElementById( ' editTags ' ).value.split( ' , ' ).map(t => t.trim()).filter(t => t),
is_starred: document.getElementById( ' editStarred ' ).checked ? 1 : 0
is_starred: document.getElementById( ' editStarred ' ).checked ? 1 : 0,
folder_id: document.getElementById( ' editFolder ' ).value ? parseInt(document.getElementById( ' editFolder ' ).value) : null
};
try {
@@ -4173,9 +4235,22 @@ function renderFolderList(type) {
// if (section) section.classList.add( ' expanded ' ); // 已移除
container.innerHTML = folders.map(f => `
<a href= " # " data-folder= " $ {f.id} " onclick= " filterByFolder( ' $ {type} ' , $ {f.id} ); return false; " >
<i class= " bi bi -folder " ></i> $ {f.name} <small class= " text-muted " >($ { f.item_count || 0})</small >
</a >
<div class= " d-flex justify-content-between align-items-center folder-item " >
<a href= " # " data -folder= " $ {f.id} " onclick= " filterByFolder( ' $ {type} ' , $ {f.id} ); return false; " class= " flex-grow-1 " >
<i class= " bi bi-folder " ></i> $ {f.name} <small class= " text-muted " >($ { f.item_count || 0})</small >
</a>
<div class= " folder-actions " >
<button class= " btn btn-outline-success " onclick= " event.stopPropagation(); showAddToFolderModal( ' $ {type} ' , $ {f.id} ); return false; " title= " 新增数据到此文件夹 " >
<i class= " bi bi-plus " ></i>
</button>
<button class= " btn btn-outline-secondary " onclick= " event.stopPropagation(); showEditFolderModal($ {f.id} , ' $ {f.name} ' ); return false; " title= " 重命名 " >
<i class= " bi bi-pencil " ></i>
</button>
<button class= " btn btn-outline-danger " onclick= " event.stopPropagation(); deleteFolderConfirm($ {f.id} , ' $ {f.name} ' ); return false; " title= " 删除 " >
<i class= " bi bi-trash " ></i>
</button>
</div>
</div>
`).join( ' ' );
}
@@ -4202,16 +4277,35 @@ async function showNewFolderModal(type) {
if (!checkOnlineBeforeAction( ' 新建文件夹 ' )) return;
document.getElementById( ' newFolderType ' ).value = type;
document.getElementById( ' editFolderId ' ).value = ' ' ;
document.getElementById( ' newFolderName ' ).value = ' ' ;
const typeLabels = { text: ' 📝 文本 ' , link: ' 🔗 链接 ' , column: ' 📰 专栏 ' , todo: ' ✅ 待办 ' };
document.getElementById( ' newFolderTypeName ' ).value = typeLabels[type] || type;
document.getElementById( ' folderModalTitle ' ).innerHTML = ' <i class= " bi bi-folder-plus " ></i> 新建文件夹 ' ;
document.getElementById( ' folderSaveBtnText ' ).textContent = ' 创建 ' ;
document.getElementById( ' folderTypeRow ' ).style.display = ' block ' ;
new bootstrap.Modal(document.getElementById( ' newFolderModal ' )).show();
}
async function createFolder() {
const type = document.getElementById( ' newFolderType ' ).value;
// 显示编辑文件夹模态框
function showEditFolderModal(folderId, folderName) {
document.getElementById( ' editFolderId ' ).value = folderId;
document.getElementById( ' newFolderName ' ).value = folderName;
document.getElementById( ' newFolderType ' ).value = ' ' ;
document.getElementById( ' folderModalTitle ' ).innerHTML = ' <i class= " bi bi-pencil " ></i> 重命名文件夹 ' ;
document.getElementById( ' folderSaveBtnText ' ).textContent = ' 保存 ' ;
document.getElementById( ' folderTypeRow ' ).style.display = ' none ' ;
new bootstrap.Modal(document.getElementById( ' newFolderModal ' )).show();
}
// 保存文件夹(创建或编辑)
async function saveFolder() {
const editId = document.getElementById( ' editFolderId ' ).value;
const name = document.getElementById( ' newFolderName ' ).value.trim();
if (!name) {
@@ -4219,20 +4313,136 @@ async function createFolder() {
return;
}
const res = await fetch(`$ {API_BASE} /folders`, {
method: ' POST ' ,
headers: { ' Content-Type ' : ' application/json ' },
body: JSON.stringify( { name, type })
});
// 离线检查
if (!checkOnlineBeforeAction(editId ? ' 重命名文件夹 ' : ' 新建文件夹 ' )) return;
if (editId) {
// 编辑
const res = await fetch(`$ {API_BASE} /folders/$ {editId} `, {
method: ' PUT ' ,
headers: { ' Content-Type ' : ' application/json ' },
body: JSON.stringify( { name })
});
const data = await res.json();
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById( ' newFolderModal ' )).hide();
loadFolders();
} else {
alert( ' 重命名失败: ' + data.error);
}
} else {
// 创建
const type = document.getElementById( ' newFolderType ' ).value;
const res = await fetch(`$ {API_BASE} /folders`, {
method: ' POST ' ,
headers: { ' Content-Type ' : ' application/json ' },
body: JSON.stringify( { name, type })
});
const data = await res.json();
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById( ' newFolderModal ' )).hide();
loadFolders();
loadStats();
} else {
alert( ' 创建失败: ' + data.error);
}
}
}
// 删除文件夹确认
async function deleteFolderConfirm(folderId, folderName) {
// 离线检查
if (!checkOnlineBeforeAction( ' 删除文件夹 ' )) return;
if (!confirm(`确认删除文件夹 " $ {folderName} " ? \n 文件夹内的数据将移到未分类。`)) return;
const res = await fetch(`$ {API_BASE} /folders/$ {folderId} ?move_to_root=1`, {
method: ' DELETE '
});
const data = await res.json();
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById( ' newFolderModal ' )).hide();
loadFolders();
loadStats();
// 如果当前正在过滤该文件夹,返回首页
if (currentFilter.folder_id === folderId) {
currentFilter.folder_id = null;
loadItems(1);
}
} else {
alert( ' 创建 失败: ' + data.error);
alert( ' 删除 失败: ' + data.error);
}
}
// 显示新增数据到文件夹的模态框
function showAddToFolderModal(type, folderId) {
// 离线检查
if (!checkOnlineBeforeAction( ' 添加数据 ' )) return;
// 设置类型
document.getElementById( ' addType ' ).value = type;
document.getElementById( ' editFolderId ' ).value = ' ' ; // 重置编辑ID
// 设置弹窗标题和图标
const typeInfo = {
text: { icon: ' 📝 ' , title: ' 添加文本 ' },
link: { icon: ' 🔗 ' , title: ' 添加链接 ' },
column: { icon: ' 📰 ' , title: ' 添加专栏 ' },
todo: { icon: ' ✅ ' , title: ' 添加待办 ' }
};
document.getElementById( ' addModalIcon ' ).textContent = typeInfo[type].icon;
document.getElementById( ' addModalTitle ' ).textContent = typeInfo[type].title;
// 显示/隐藏对应字段
document.getElementById( ' contentGroup ' ).style.display = type === ' text ' ? ' block ' : ' none ' ;
document.getElementById( ' urlGroup ' ).style.display = [ ' link ' , ' column ' ].includes(type) ? ' block ' : ' none ' ;
document.getElementById( ' sourceGroup ' ).style.display = type === ' column ' ? ' block ' : ' none ' ;
document.getElementById( ' todoFields ' ).style.display = type === ' todo ' ? ' block ' : ' none ' ;
// 清空表单
document.getElementById( ' addForm ' ).reset();
hideDraftIndicator();
// 设置默认文件夹(隐藏字段,需要在提交时带上)
currentAddFolderId = folderId;
// 打开弹窗
new bootstrap.Modal(document.getElementById( ' addModal ' )).show();
// 加载该类型下的文件夹列表,并选中指定文件夹
loadFolderSelect(type, ' addFolder ' , folderId);
// 启动自动保存
startAutoSave();
// 弹框关闭时停止自动保存并重置文件夹ID
document.getElementById( ' addModal ' ).addEventListener( ' hidden.bs.modal ' , () => {
stopAutoSave();
currentAddFolderId = null;
}, { once: true });
}
// 当前新增时的文件夹ID
let currentAddFolderId = null;
// 加载文件夹选择列表
async function loadFolderSelect(type, selectId, selectedFolderId = null) {
const res = await fetch(`$ {API_BASE} /folders?type=$ {type} `);
const data = await res.json();
const select = document.getElementById(selectId);
if (!select) return;
select.innerHTML = ' <option value= " " >未分类(根目录)</option> ' ;
if (data.success && data.data.length > 0) {
data.data.forEach(f => {
const selected = f.id == selectedFolderId ? ' selected ' : ' ' ;
select.innerHTML += `<option value= " $ {f.id} " $ {selected} >$ {f.name} </option>`;
});
}
}