Compare commits

...

3 Commits

Author SHA1 Message Date
105f4d5492 feat(v4.3.0): 新增/编辑弹框中添加文件夹选择
- 新增数据时可选择所属文件夹
- 编辑数据时可更改所属文件夹
- 切换类型时自动更新文件夹列表
2026-04-22 18:08:41 +08:00
e92349e111 feat(v4.2.1): 文件夹按钮样式优化+新增数据到文件夹功能
- hover文件夹时按钮悬浮在右侧,高度一致
- 新增数据按钮(绿色+)直接添加数据到该文件夹
2026-04-22 16:59:27 +08:00
0912d658b8 feat(v4.2.0): 文件夹支持重命名和删除
- hover文件夹时显示编辑和删除按钮
- 编辑按钮打开重命名模态框
- 删除按钮确认后删除,数据移到未分类
2026-04-22 16:27:38 +08:00
2 changed files with 230 additions and 20 deletions

View File

@@ -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="createFolder()">
<i class="bi bi-check"></i> 创建
<button type="button" class="btn btn-primary" onclick="saveFolder()">
<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>`;
});
}
}

View File

@@ -391,7 +391,7 @@ class Database:
def update_item(self, item_id: int, **kwargs) -> bool:
"""更新条目"""
allowed_fields = ['type', 'title', 'content', 'url', 'source', 'status', 'priority', 'due_date', 'note', 'is_starred']
allowed_fields = ['type', 'title', 'content', 'url', 'source', 'status', 'priority', 'due_date', 'note', 'is_starred', 'folder_id']
update_fields = {k: v for k, v in kwargs.items() if k in allowed_fields}
# 只有 tags 变化也算有效更新