Compare commits

...

3 Commits

View File

@@ -873,6 +873,8 @@ INDEX_TEMPLATE = '''
margin-left: 200px;
padding: 20px;
padding-top: 0; /* 顶部按钮栏有 sticky这里去掉顶部 padding */
width: -webkit-fill-available;
width: fill-available;
}
/* 顶部操作栏固定在主内容区顶部 */
@@ -1036,28 +1038,52 @@ INDEX_TEMPLATE = '''
/* 卡片内容自适应布局 */
.item-card .card-content {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 8px;
}
.item-card .card-main {
flex: 1;
min-width: 150px;
min-width: 0;
}
.item-card .card-actions {
display: flex;
align-items: center;
gap: 2px;
flex-wrap: wrap;
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; }
.type-todo { border-left: 4px solid #ffc107; }
.is-starred { border-left: 4px solid #ffc107; background: #fffbe6; }
.is-starred:hover { background: #fff9e0; }
/* Markdown内容样式 */
.markdown-content { line-height: 1.6; }
.markdown-content h3 { font-size: 1.2em; margin-bottom: 0.5em; }
.markdown-content h4 { font-size: 1.1em; margin-bottom: 0.4em; }
.markdown-content h5 { font-size: 1.0em; margin-bottom: 0.3em; }
.markdown-content h6 { font-size: 0.9em; margin-bottom: 0.2em; }
.markdown-content code { font-size: 0.9em; }
.markdown-content pre { font-size: 0.85em; overflow-x: auto; }
.markdown-content ul, .markdown-content ol { margin-bottom: 0.5em; }
.markdown-content a { color: #0d6efd; }
.star-btn { font-size: 11px; }
.status-pending { color: #ffc107; }
.status-in_progress { color: #17a2b8; }
@@ -1202,6 +1228,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>
@@ -1795,6 +1824,7 @@ INDEX_TEMPLATE = '''
const API_BASE = '/api';
let currentFilter = { type: '', status: '', starred: null, folder_id: null };
let currentSort = { sort_by: '', sort_order: '' };
let currentItems = []; // 保存当前显示的数据,用于切换显示模式时重新渲染
let currentPage = 1;
const pageSize = 20;
let allFolders = {}; // 按类型存储文件夹
@@ -1926,6 +1956,9 @@ document.addEventListener('DOMContentLoaded', async () => {
// 启动连接状态检测
startConnectionCheck();
// 初始化显示模式
updateDisplayMode();
// 确保初始状态清空
document.getElementById('searchInput').value = '';
document.getElementById('typeFilter').value = '';
@@ -2010,6 +2043,7 @@ async function loadItems(page = 1) {
const data = await res.json();
if (data.success) {
currentItems = data.data; // 保存数据用于切换显示模式
renderItems(data.data);
renderPagination(data.total, page); // 使用API返回的total
}
@@ -2023,8 +2057,10 @@ 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="card-content">
<div class="card-main">
@@ -2108,6 +2144,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`);
@@ -2633,7 +2691,7 @@ async function showDetail(id) {
}
if (item.content) {
html += `<div class="mb-3"><strong>内容:</strong><br><div class="border rounded p-3 bg-light" style="white-space: pre-wrap; word-break: break-all;">${escapeHtml(item.content)}</div></div>`;
html += `<div class="mb-3"><strong>内容:</strong><br><div class="border rounded p-3 bg-light markdown-content">${renderMarkdown(item.content)}</div></div>`;
}
if (item.source) {
@@ -2653,7 +2711,7 @@ async function showDetail(id) {
}
if (item.note) {
html += `<div class="mb-3"><strong>详情/备注:</strong><br><div class="border rounded p-3 bg-light" style="white-space: pre-wrap; word-break: break-all;">${escapeHtml(item.note)}</div></div>`;
html += `<div class="mb-3"><strong>详情/备注:</strong><br><div class="border rounded p-3 bg-light markdown-content">${renderMarkdown(item.note)}</div></div>`;
}
html += `<div class="text-muted small"><strong>创建时间:</strong> ${formatDate(item.created_at)}<br><strong>更新时间:</strong> ${formatDate(item.updated_at)}</div>`;
@@ -2984,6 +3042,55 @@ function escapeHtml(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// 简单Markdown渲染支持标题、粗体、斜体、代码、链接、列表
function renderMarkdown(text) {
if (!text) return '';
// 先转义HTML
let html = escapeHtml(text);
// 代码块 ```code```
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre class="bg-dark text-light p-2 rounded"><code>$2</code></pre>');
// 行内代码 `code`
html = html.replace(/`([^`]+)`/g, '<code class="bg-light px-1 rounded">$1</code>');
// 标题 # ## ### ####
html = html.replace(/^#### (.+)$/gm, '<h6 class="mt-2">$1</h6>');
html = html.replace(/^### (.+)$/gm, '<h5 class="mt-2">$1</h5>');
html = html.replace(/^## (.+)$/gm, '<h4 class="mt-2">$1</h4>');
html = html.replace(/^# (.+)$/gm, '<h3 class="mt-2">$1</h3>');
// 粗体 **text**
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
// 斜体 *text*
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
// 链接 [text](url)
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
// 无序列表 - item
html = html.replace(/^- (.+)$/gm, '<li class="ms-3">$1</li>');
html = html.replace(/(<li.*<\/li>\n?)+/g, '<ul class="mb-2">$&</ul>');
// 有序列表 1. item
html = html.replace(/^\d+\. (.+)$/gm, '<li class="ms-3">$1</li>');
// 段落分隔(两个换行)
html = html.replace(/\n\n/g, '</p><p class="mb-2">');
// 单换行
html = html.replace(/\n/g, '<br>');
// 包装在段落中
if (!html.startsWith('<')) {
html = '<p class="mb-2">' + html + '</p>';
}
return html;
}
// 标签管理
let allTags = [];