@@ -55,7 +55,8 @@ def list_items():
status = request . args . get ( ' status ' ) ,
tag = request . args . get ( ' tag ' ) ,
keyword = request . args . get ( ' keyword ' ) ,
starred = starred
starred = starred ,
folder_id = folder_id
)
return jsonify ( { ' success ' : True , ' data ' : items , ' total ' : total } )
@@ -872,6 +873,8 @@ INDEX_TEMPLATE = '''
margin-left: 200px;
padding: 20px;
padding-top: 0; /* 顶部按钮栏有 sticky, 这里去掉顶部 padding */
width: -webkit-fill-available;
width: fill-available;
}
/* 顶部操作栏固定在主内容区顶部 */
@@ -929,12 +932,49 @@ INDEX_TEMPLATE = '''
opacity: 0.8;
}
/* 文件夹列表样式 */
.sidebar-section .section-header { font-weight: 500 ; }
/* 文件夹列表样式 - 折叠式 */
.sidebar-section { margin-bottom: 2px ; }
.sidebar-section .section-header {
font-weight: 500;
display: flex;
justify-content: space-between;
align-items: center;
padding-right: 10px;
}
.sidebar-section .section-header .header-left {
display: flex;
align-items: center;
flex: 1;
}
.sidebar-section .section-header .toggle-arrow {
font-size: 10px;
margin-right: 8px;
transition: transform 0.2s;
}
.sidebar-section.expanded .section-header .toggle-arrow {
transform: rotate(90deg);
}
.sidebar-section .section-header .new-folder-btn {
font-size: 14px;
color: #adb5bd;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
transition: all 0.2s;
background: rgba(255,255,255,0.1);
}
.sidebar-section .section-header .new-folder-btn:hover {
color: #fff;
background: #28a745;
}
.folder-list {
padding-left: 10px;
max-height: 15 0px;
max-height: 20 0px;
overflow-y: auto;
display: none; /* 默认隐藏 */
}
.sidebar-section.expanded .folder-list {
display: block; /* 展开时显示 */
}
.folder-list a {
padding: 6px 20px;
@@ -995,6 +1035,39 @@ INDEX_TEMPLATE = '''
.item-card h6 { font-size: 14px; margin-bottom: 2px; }
.item-card p { margin-bottom: 2px; }
.item-card .text-muted.small { font-size: 12px; }
/* 卡片内容自适应布局 */
.item-card .card-content {
display: flex;
align-items: flex-start;
gap: 8px;
}
.item-card .card-main {
flex: 1;
min-width: 0;
}
.item-card .card-actions {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
.item-card .card-actions .btn {
white-space: nowrap;
}
/* 单行显示模式 */
.item-card.compact .card-content {
align-items: center;
}
.item-card.compact .card-main h6 {
margin-bottom: 0;
}
.item-card.compact .card-main p {
display: none;
}
/* 双行显示模式(默认) */
.item-card.double-line .card-main p {
display: block;
}
.type-text { border-left: 4px solid #17a2b8; }
.type-link { border-left: 4px solid #28a745; }
.type-column { border-left: 4px solid #6f42c1; }
@@ -1041,31 +1114,59 @@ INDEX_TEMPLATE = '''
<a href= " # " data-filter= " starred " ><i class= " bi bi-star-fill " style= " color:#ffc107; " ></i> 重点关注</a>
<!-- 文本类别 -->
<div class= " sidebar-section " >
<a href= " # " data-filter= " text " class=" section-header " ><i class= " bi bi-file- text" ></i> 文本</a >
<div class= " sidebar-section " id= " section-text " >
<a href= " # " class= " section-header " onclick= " toggleSection( ' text' ); return false; " >
<span class= " header-left " >
<span class= " toggle-arrow " ><i class= " bi bi-chevron-right " ></i></span>
<i class= " bi bi-file-text " ></i> 文本
</span>
<span class= " new-folder-btn " onclick= " event.stopPropagation(); showNewFolderModal( ' text ' ); return false; " >
<i class= " bi bi-folder-plus " ></i>
</span>
</a>
<div class= " folder-list " id= " folderList-text " ></div>
<a href= " # " class= " folder-action " onclick= " showNewFolderModal( ' text ' ); return false; " ><i class= " bi bi-folder-plus " ></i> 新建文件夹</a>
</div>
<!-- 链接类别 -->
<div class= " sidebar-section " >
<a href= " # " data-filter= " link " class=" section-header " ><i class= " bi bi-link-45deg " ></i> 链接</a >
<div class= " sidebar-section " id= " section-link " >
<a href= " # " class= " section-header " onclick= " toggleSection( ' link ' ); return false; " >
<span class= " header-left " >
<span class= " toggle-arrow " ><i class= " bi bi-chevron-right " ></i></span>
<i class= " bi bi-link-45deg " ></i> 链接
</span>
<span class= " new-folder-btn " onclick= " event.stopPropagation(); showNewFolderModal( ' link ' ); return false; " >
<i class= " bi bi-folder-plus " ></i>
</span>
</a>
<div class= " folder-list " id= " folderList-link " ></div>
<a href= " # " class= " folder-action " onclick= " showNewFolderModal( ' link ' ); return false; " ><i class= " bi bi-folder-plus " ></i> 新建文件夹</a>
</div>
<!-- 专栏类别 -->
<div class= " sidebar-section " >
<a href= " # " data-filter= " column " class=" section-header " ><i class= " bi bi-newspaper " ></i> 专栏</a >
<div class= " sidebar-section " id= " section-column " >
<a href= " # " class= " section-header " onclick= " toggleSection( ' column ' ); return false; " >
<span class= " header-left " >
<span class= " toggle-arrow " ><i class= " bi bi-chevron-right " ></i></span>
<i class= " bi bi-newspaper " ></i> 专栏
</span>
<span class= " new-folder-btn " onclick= " event.stopPropagation(); showNewFolderModal( ' column ' ); return false; " >
<i class= " bi bi-folder-plus " ></i>
</span>
</a>
<div class= " folder-list " id= " folderList-column " ></div>
<a href= " # " class= " folder-action " onclick= " showNewFolderModal( ' column ' ); return false; " ><i class= " bi bi-folder-plus " ></i> 新建文件夹</a>
</div>
<!-- 待办类别 -->
<div class= " sidebar-section " >
<a href= " # " data-filter= " todo " class=" section-header " ><i class= " bi bi-check2-square " ></i> 待办</a >
<div class= " sidebar-section " id= " section-todo " >
<a href= " # " class= " section-header " onclick= " toggleSection( ' todo ' ); return false; " >
<span class= " header-left " >
<span class= " toggle-arrow " ><i class= " bi bi-chevron-right " ></i></span>
<i class= " bi bi-check2-square " ></i> 待办
</span>
<span class= " new-folder-btn " onclick= " event.stopPropagation(); showNewFolderModal( ' todo ' ); return false; " >
<i class= " bi bi-folder-plus " ></i>
</span>
</a>
<div class= " folder-list " id= " folderList-todo " ></div>
<a href= " # " class= " folder-action " onclick= " showNewFolderModal( ' todo ' ); return false; " ><i class= " bi bi-folder-plus " ></i> 新建文件夹</a>
</div>
<hr class= " border-secondary " >
@@ -1117,6 +1218,9 @@ INDEX_TEMPLATE = '''
<button class= " btn btn-outline-info me-2 " onclick= " showAIAddModal() " title= " AI自动添加 " >
<i class= " bi bi-robot " ></i> AI添加
</button>
<button class= " btn btn-outline-secondary me-1 " onclick= " toggleDisplayMode() " title= " 切换显示模式 " id= " displayModeBtn " >
<i class= " bi bi-layout-text-sidebar " ></i>
</button>
<button class= " btn btn-outline-secondary me-1 " onclick= " showAddModal( ' text ' ) " title= " 添加文本 " >
<i class= " bi bi-file-text " ></i>
</button>
@@ -1841,6 +1945,9 @@ document.addEventListener('DOMContentLoaded', async () => {
// 启动连接状态检测
startConnectionCheck();
// 初始化显示模式
updateDisplayMode();
// 确保初始状态清空
document.getElementById( ' searchInput ' ).value = ' ' ;
document.getElementById( ' typeFilter ' ).value = ' ' ;
@@ -1938,11 +2045,13 @@ function renderItems(items) {
return;
}
const modeClass = displayMode === ' single ' ? ' compact ' : ' double-line ' ;
container.innerHTML = items.map(item => `
<div class= " card type-$ {item.type} item-card $ { item.is_starred ? ' is-starred ' : ' ' } " style= " cursor: pointer; " onclick= " showDetail($ {item.id} ) " >
<div class= " card type-$ {item.type} item-card $ {modeClass} $ { item.is_starred ? ' is-starred ' : ' ' } " style= " cursor: pointer; " onclick= " showDetail($ {item.id} ) " >
<div class= " card-body " >
<div class= " d-flex justify-content-between align-items-star t" >
<div style = " flex: 1; min-width: 0; " >
<div class= " card-conten t" >
<div class = " card-main " >
<h6 class= " card-title text-truncate mb-1 $ { item.type === ' todo ' && item.status === ' completed ' ? ' text-muted ' : ' ' } " >
$ { item.is_starred ? ' <i class= " bi bi-star-fill " style= " color:#ffc107; " ></i> ' : ' ' } $ { getTypeIcon(item.type)} $ { item.title || truncate(item.content || item.url, 30)}
$ { item.type === ' todo ' && item.status === ' completed ' ? ' ✓ ' : ' ' }
@@ -1958,7 +2067,7 @@ function renderItems(items) {
$ { item.type === ' todo ' ? `$ { getStatusLabelShort(item.status)} $ { getPriorityLabelShort(item.priority)} $ { item.due_date ? ' 📅 ' + formatDueDate(item.due_date) : ' ' }` : ' ' }
</p>
</div>
<div class= " d-flex align-items-center gap-1 flex-wrap ms-2 " onclick= " event.stopPropagation(); " >
<div class= " card-actions " onclick= " event.stopPropagation(); " >
$ { item.tags.slice(0, 2).map(t => `<span class= " badge bg-secondary " style= " font-size:10px; " >$ {t} </span>`).join( ' ' )}
<button class= " btn btn-sm btn-outline-warning py-0 px-1 star-btn " onclick= " toggleStar($ {item.id} ) " title= " $ { item.is_starred ? ' 取消重点关注 ' : ' 设为重点关注 ' } " >
<i class= " bi bi-star$ { item.is_starred ? ' -fill ' : ' ' } " style= " font-size:11px; $ { item.is_starred ? ' color:#ffc107; ' : ' ' } " ></i>
@@ -2023,6 +2132,28 @@ function renderPagination(total, page) {
container.innerHTML = html;
}
// 显示模式: single( 单行) 或 double( 双行)
let displayMode = localStorage.getItem( ' displayMode ' ) || ' double ' ;
// 切换显示模式
function toggleDisplayMode() {
displayMode = displayMode === ' single ' ? ' double ' : ' single ' ;
localStorage.setItem( ' displayMode ' , displayMode);
updateDisplayMode();
renderItems(currentItems);
}
// 更新显示模式按钮图标
function updateDisplayMode() {
const btn = document.getElementById( ' displayModeBtn ' );
if (btn) {
btn.innerHTML = displayMode === ' single '
? ' <i class= " bi bi-layout-text-sidebar-reverse " ></i> '
: ' <i class= " bi bi-layout-text-sidebar " ></i> ' ;
btn.title = displayMode === ' single ' ? ' 当前:单行(点击切换双行) ' : ' 当前:双行(点击切换单行) ' ;
}
}
// 加载统计
async function loadStats() {
const res = await fetch(`$ {API_BASE} /stats`);
@@ -2110,8 +2241,8 @@ function renderDrafts(drafts, total) {
container.innerHTML = header + drafts.map(draft => `
<div class= " card type-$ {draft.type} item-card " style= " opacity: 0.85; " >
<div class= " card-body " >
<div class= " d-flex justify-content-between align-items-star t" >
<div style = " flex: 1; min-width: 0; " >
<div class= " card-conten t" >
<div class = " card-main " >
<h6 class= " card-title text-truncate mb-1 " >
$ { getTypeIcon(draft.type)} $ { draft.title || ' (无标题) ' }
</h6>
@@ -2120,14 +2251,14 @@ function renderDrafts(drafts, total) {
</p>
<small class= " text-muted " >保存于: $ { formatShortDate(draft.updated_at)}</small>
</div>
<div class= " d-flex gap-1 " >
<button class= " btn btn-sm btn-outline-primary " onclick= " editDraft($ {draft.id} ) " title= " 编辑 " >
<div class= " card-actions " >
<button class= " btn btn-sm btn-outline-primary py-0 px-1 " onclick= " editDraft($ {draft.id} ) " title= " 编辑 " >
<i class= " bi bi-pencil " ></i>
</button>
<button class= " btn btn-sm btn-outline-success " onclick= " publishDraft($ {draft.id} ) " title= " 发布 " >
<button class= " btn btn-sm btn-outline-success py-0 px-1 " onclick= " publishDraft($ {draft.id} ) " title= " 发布 " >
<i class= " bi bi-send " ></i>
</button>
<button class= " btn btn-sm btn-outline-danger " onclick= " deleteDraft($ {draft.id} ) " title= " 删除 " >
<button class= " btn btn-sm btn-outline-danger py-0 px-1 " onclick= " deleteDraft($ {draft.id} ) " title= " 删除 " >
<i class= " bi bi-trash " ></i>
</button>
</div>
@@ -3269,8 +3400,8 @@ function renderTrash(items, total) {
container.innerHTML = header + items.map(item => `
<div class= " card type-$ {item.type} item-card " style= " opacity: 0.7; " >
<div class= " card-body " >
<div class= " d-flex justify-content-between align-items-star t" >
<div style = " flex: 1; min-width: 0; " >
<div class= " card-conten t" >
<div class = " card-main " >
<h6 class= " card-title text-truncate mb-1 " >
$ { getTypeIcon(item.type)} $ { item.title || truncate(item.content || item.url, 30)}
</h6>
@@ -3278,11 +3409,11 @@ function renderTrash(items, total) {
删除时间: $ { formatShortDate(item.deleted_at)}
</p>
</div>
<div class= " d-flex gap-1 " >
<button class= " btn btn-sm btn-outline-success " onclick= " restoreItem($ {item.id} ) " title= " 恢复 " >
<i class= " bi bi-arrow-counterclockwise " ></i> 恢复
<div class= " card-actions " >
<button class= " btn btn-sm btn-outline-success py-0 px-1 " onclick= " restoreItem($ {item.id} ) " title= " 恢复 " >
<i class= " bi bi-arrow-counterclockwise " ></i>
</button>
<button class= " btn btn-sm btn-outline-danger " onclick= " deletePermanently($ {item.id} ) " title= " 彻底删除 " >
<button class= " btn btn-sm btn-outline-danger py-0 px-1 " onclick= " deletePermanently($ {item.id} ) " title= " 彻底删除 " >
<i class= " bi bi-trash-fill " ></i>
</button>
</div>
@@ -3375,15 +3506,20 @@ async function loadFolders() {
function renderFolderList(type) {
const container = document.getElementById(`folderList-$ {type} `);
const section = document.getElementById(`section-$ {type} `);
if (!container) return;
const folders = allFolders[type] || [];
if (folders.length === 0) {
container.innerHTML = ' ' ;
if (section) section.classList.remove( ' expanded ' );
return;
}
// 默认不展开,保持折叠状态
// 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>
@@ -3391,6 +3527,24 @@ function renderFolderList(type) {
`).join( ' ' );
}
// 切换文件夹区域展开/折叠
// 切换文件夹区域展开/折叠,并过滤显示该类别数据
function toggleSection(type) {
const section = document.getElementById(`section-$ {type} `);
if (!section) return;
// 切换展开状态
section.classList.toggle( ' expanded ' );
// 更新侧边栏选中状态
document.querySelectorAll( ' .sidebar a ' ).forEach(a => a.classList.remove( ' active ' ));
section.querySelector( ' .section-header ' ).classList.add( ' active ' );
// 设置过滤条件:只过滤类型,不限制文件夹
currentFilter = { type, status: ' ' , starred: null, folder_id: null };
loadItems(1);
}
async function showNewFolderModal(type) {
// 离线检查
if (!checkOnlineBeforeAction( ' 新建文件夹 ' )) return;