Compare commits

..

14 Commits

Author SHA1 Message Date
d2f64f98a1 fix: 初始化时清空搜索框和筛选状态
- 页面加载时强制清空 searchInput
- 重置 typeFilter 和 currentFilter
- 删除重复的 pageSize 定义
2026-04-16 13:47:46 +08:00
161b93f368 docs: 更新版本历史 v2.3.2 2026-04-16 13:41:06 +08:00
31f2d8b428 fix: 搜索功能修复 - debounce函数定义顺序
- debounce函数在使用前必须先定义
- 之前定义在代码末尾,导致搜索事件绑定失败
- 现移到代码开头,确保正确执行
2026-04-16 13:40:44 +08:00
5d6dd10dfa docs: 更新版本历史 v2.3.1 2026-04-16 12:20:51 +08:00
d0f7b07260 fix: 日期放到标题后面,不增加卡片高度
- 日期紧跟标题后面,同一行显示
- 格式更紧凑:04-16 11:09 →04-16 12:00
- 字体10px,透明度0.5,不影响阅读
2026-04-16 12:20:32 +08:00
0be768ca8e docs: 更新版本历史 v2.3.0 2026-04-16 12:17:24 +08:00
68ecb16303 feat: 卡片显示创建和更新日期
- 每个收藏卡片底部显示日期
- 格式:04-16 11:09(月-日 时:分)
- 有更新时显示:创建 → 更新
- 字体更小更淡,不影响卡片高度
2026-04-16 12:16:54 +08:00
82d928f497 docs: 更新版本历史 v2.2.0 2026-04-16 11:45:57 +08:00
c99eca35f0 feat: 快捷添加按钮,一键选择类型
- 顶部按钮栏分离为4个快捷添加按钮
- 点击直接进入对应类型的添加弹窗
- 弹窗标题显示类型图标和名称
- 不再需要下拉选择类型
2026-04-16 11:45:25 +08:00
47b195ed1c docs: 更新版本历史 v2.1.0 2026-04-16 11:37:49 +08:00
1f1528979c feat: 待办截止时间支持日期+时间
- 截止日期改为日期时间选择器(datetime-local)
- 新增友好显示函数:今天/明天/昨天 + 时间
- 详情页显示完整日期时间格式
- 提醒弹窗显示友好格式
- 后端支持多种日期格式解析
- 只有日期的待办视为当天23:59到期
2026-04-16 11:37:04 +08:00
bcb24e474d docs: 更新版本历史 v2.0.1 2026-04-16 11:26:06 +08:00
a19b260ef7 fix: 转换弹窗优化
- 内容预览保留换行格式,提高可读性
- 转换方式默认改为复制创建
2026-04-16 11:25:49 +08:00
1d8ce5cfb9 docs: 更新版本历史 v2.0.0 2026-04-16 11:18:54 +08:00
3 changed files with 229 additions and 66 deletions

View File

@@ -141,6 +141,39 @@ xian-favor/
## 版本历史
- **v2.3.2** (2026-04-16): 搜索功能修复
- 修复 debounce 函数定义顺序问题
- 搜索框输入后可正常过滤列表
- **v2.3.1** (2026-04-16): 日期放到标题后面,不增加卡片高度
- 日期紧跟标题,同一行显示
- 格式紧凑:`04-16 11:09→04-16 12:00`
- **v2.3.0** (2026-04-16): 卡片显示创建和更新日期
- 每个收藏卡片底部显示日期
- 格式04-16 11:09月-日 时:分)
- 有更新时显示:创建 → 更新
- 字体更小更淡,不影响卡片高度
- **v2.2.0** (2026-04-16): 快捷添加按钮,一键选择类型
- 顶部按钮栏分离为4个快捷添加按钮文本、链接、待办、专栏
- 点击直接进入对应类型的添加弹窗
- 弹窗标题显示类型图标和名称
- 不再需要下拉选择类型,操作更快捷
- **v2.1.0** (2026-04-16): 待办截止时间支持日期+时间
- 截止日期改为日期时间选择器
- 列表显示友好格式:今天 18:30、明天 09:00 等
- 详情页显示完整格式2026年4月16日 18:30
- 后端支持多种日期格式解析
- 只有日期的待办视为当天 23:59 到期
- **v2.0.1** (2026-04-16): 转换弹窗优化
- 内容预览保留换行格式,提高可读性
- 转换方式默认改为复制创建
- **v2.0.0** (2026-04-16): 收藏转待办功能(大版本更新)
- 新增 `/api/items/<id>/convert` API
- **直接转换**:原收藏变为待办,数据合并
- **复制创建**:保留原收藏,新建待办任务
- 文本/链接/专栏类型可转换
- 内容自动合并到待办备注
- 前端列表和详情页添加转换按钮
- 弹窗配置:标题、状态、优先级、截止日期
- v1.11.0 (2026-04-16): 已完成待办可重新打开
- 新增 `/api/items/<id>/reopen` 接口
- 前端列表已完成待办显示重新打开按钮(↻图标)

View File

@@ -539,11 +539,11 @@ INDEX_TEMPLATE = '''
.content { padding: 20px; }
.card { margin-bottom: 8px; transition: transform 0.2s; }
.card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
.card-body { padding: 10px 15px; }
.card-body { padding: 8px 12px; }
.tag { margin-right: 5px; }
.item-card { font-size: 14px; }
.item-card h6 { font-size: 14px; margin-bottom: 4px; }
.item-card p { margin-bottom: 4px; }
.item-card h6 { font-size: 14px; margin-bottom: 2px; }
.item-card p { margin-bottom: 2px; }
.item-card .text-muted.small { font-size: 12px; }
.type-text { border-left: 4px solid #17a2b8; }
.type-link { border-left: 4px solid #28a745; }
@@ -609,12 +609,21 @@ 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="showAddModal('text')" title="添加文本">
<i class="bi bi-file-text"></i>
</button>
<button class="btn btn-outline-success me-1" onclick="showAddModal('link')" title="添加链接">
<i class="bi bi-link-45deg"></i>
</button>
<button class="btn btn-outline-warning me-1" onclick="showAddModal('todo')" title="添加待办">
<i class="bi bi-check2-square"></i>
</button>
<button class="btn btn-outline-primary me-1" onclick="showAddModal('column')" title="添加专栏">
<i class="bi bi-newspaper"></i>
</button>
<button class="btn btn-outline-success me-2" onclick="exportData()" title="导出JSON">
<i class="bi bi-download"></i> 导出
</button>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
<i class="bi bi-plus-lg"></i> 添加
</button>
</div>
<!-- 统计卡片 -->
@@ -667,20 +676,15 @@ INDEX_TEMPLATE = '''
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">添加条目</h5>
<h5 class="modal-title">
<span id="addModalIcon"></span>
<span id="addModalTitle">添加条目</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addForm">
<div class="mb-3">
<label class="form-label">类型</label>
<select id="addType" class="form-select">
<option value="text">📝 文本</option>
<option value="link">🔗 链接</option>
<option value="column">📰 专栏</option>
<option value="todo">✅ 待办</option>
</select>
</div>
<input type="hidden" id="addType" value="text">
<div class="mb-3">
<label class="form-label">标题</label>
<input type="text" id="addTitle" class="form-control" placeholder="可选">
@@ -691,11 +695,11 @@ INDEX_TEMPLATE = '''
</div>
<div class="mb-3" id="urlGroup" style="display:none;">
<label class="form-label">URL</label>
<input type="url" id="addUrl" class="form-control">
<input type="url" id="addUrl" class="form-control" placeholder="https://...">
</div>
<div class="mb-3" id="sourceGroup" style="display:none;">
<label class="form-label">来源</label>
<input type="text" id="addSource" class="form-control">
<input type="text" id="addSource" class="form-control" placeholder="专栏来源">
</div>
<div class="mb-3" id="todoFields" style="display:none;">
<div class="row">
@@ -718,8 +722,8 @@ INDEX_TEMPLATE = '''
</div>
</div>
<div class="mt-3">
<label class="form-label">截止日期</label>
<input type="date" id="addDueDate" class="form-control">
<label class="form-label">截止时间</label>
<input type="datetime-local" id="addDueDate" class="form-control">
</div>
</div>
<div class="mb-3">
@@ -825,8 +829,8 @@ INDEX_TEMPLATE = '''
</div>
</div>
<div class="mt-3">
<label class="form-label">截止日期</label>
<input type="date" id="editDueDate" class="form-control">
<label class="form-label">截止时间</label>
<input type="datetime-local" id="editDueDate" class="form-control">
</div>
</div>
<div class="mb-3">
@@ -1022,13 +1026,13 @@ INDEX_TEMPLATE = '''
<div class="mb-3">
<label class="form-label fw-bold">转换方式</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="convertMode" id="modeConvert" value="convert" checked>
<input class="form-check-input" type="radio" name="convertMode" id="modeConvert" value="convert">
<label class="form-check-label" for="modeConvert">
<strong>直接转换</strong> - 原收藏变为待办,数据合并
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="convertMode" id="modeCopy" value="copy">
<input class="form-check-input" type="radio" name="convertMode" id="modeCopy" value="copy" checked>
<label class="form-check-label" for="modeCopy">
<strong>复制创建</strong> - 保留原收藏,新建待办任务
</label>
@@ -1060,14 +1064,14 @@ INDEX_TEMPLATE = '''
</select>
</div>
<div class="col">
<label class="form-label">截止日期</label>
<input type="date" id="convertDueDate" class="form-control">
<label class="form-label">截止时间</label>
<input type="datetime-local" id="convertDueDate" class="form-control">
</div>
</div>
<div class="mb-3">
<label class="form-label text-muted">内容预览(转换后会合并到待办备注)</label>
<div id="convertPreview" class="border rounded p-2 bg-light" style="max-height: 150px; overflow-y: auto;">
<div id="convertPreview" class="border rounded p-2 bg-light" style="max-height: 150px; overflow-y: auto; white-space: pre-wrap; word-break: break-all;">
<!-- 动态填充 -->
</div>
</div>
@@ -1086,9 +1090,23 @@ INDEX_TEMPLATE = '''
<script>
const API_BASE = '/api';
let currentFilter = { type: '', status: '' };
let currentPage = 1;
const pageSize = 20;
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
// 确保初始状态清空
document.getElementById('searchInput').value = '';
document.getElementById('typeFilter').value = '';
currentFilter = { type: '', status: '' };
await loadStats(); // 先加载统计,确保总数可用
loadItems();
loadTags();
@@ -1104,15 +1122,6 @@ document.addEventListener('DOMContentLoaded', async () => {
// 标签搜索实时过滤
document.getElementById('tagSearch')?.addEventListener('input', debounce(loadTagManagerList, 300));
// 类型切换时显示/隐藏字段
document.getElementById('addType').addEventListener('change', (e) => {
const type = e.target.value;
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('editType').addEventListener('change', (e) => {
updateEditFieldsByType(e.target.value);
@@ -1148,9 +1157,6 @@ document.addEventListener('DOMContentLoaded', async () => {
});
// 加载列表
let currentPage = 1;
const pageSize = 20;
async function loadItems(page = 1) {
currentPage = page;
const keyword = document.getElementById('searchInput').value;
@@ -1184,10 +1190,13 @@ function renderItems(items) {
<h6 class="card-title text-truncate mb-1 ${item.type === 'todo' && item.status === 'completed' ? 'text-muted' : ''}">
${getTypeIcon(item.type)} ${item.title || truncate(item.content || item.url, 30)}
${item.type === 'todo' && item.status === 'completed' ? '' : ''}
<span style="font-size:10px; opacity:0.5; margin-left:8px;">
${formatShortDate(item.created_at)}${item.updated_at && item.updated_at !== item.created_at ? '' + formatShortDate(item.updated_at) : ''}
</span>
</h6>
<p class="card-text text-muted small mb-0 text-truncate" style="font-size:12px;">
${item.url ? truncate(item.url, 50) : item.content ? truncate(item.content, 50) : item.note ? truncate(item.note, 50) : ''}
${item.type === 'todo' ? `${getStatusLabelShort(item.status)} ${getPriorityLabelShort(item.priority)} ${item.due_date ? '📅' + item.due_date : ''}` : ''}
${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();">
@@ -1270,6 +1279,37 @@ async function refreshData() {
loadItems(currentPage);
}
// ============ 添加功能 ============
// 快捷添加按钮
function showAddModal(type) {
// 设置类型
document.getElementById('addType').value = type;
// 设置弹窗标题和图标
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();
// 打开弹窗
new bootstrap.Modal(document.getElementById('addModal')).show();
}
// 添加条目
async function addItem() {
const type = document.getElementById('addType').value;
@@ -1335,8 +1375,8 @@ async function showConvertModal(itemId) {
// 设置默认标题
document.getElementById('convertTitle').value = convertItemData.title || '';
// 重置选项
document.getElementById('modeConvert').checked = true;
// 重置选项(默认复制创建)
document.getElementById('modeCopy').checked = true;
document.getElementById('convertStatus').value = 'pending';
document.getElementById('convertPriority').value = 'medium';
document.getElementById('convertDueDate').value = '';
@@ -1437,7 +1477,7 @@ async function showDetail(id) {
html += `<div class="mb-3"><strong>状态:</strong> ${getStatusLabel(item.status)}</div>`;
html += `<div class="mb-3"><strong>优先级:</strong> ${getPriorityLabel(item.priority)}</div>`;
if (item.due_date) {
html += `<div class="mb-3"><strong>截止日期:</strong> ${item.due_date}</div>`;
html += `<div class="mb-3"><strong>截止时间:</strong> ${formatDueDateFull(item.due_date)}</div>`;
}
}
@@ -1509,7 +1549,21 @@ async function openEditModal(id) {
if (type === 'todo') {
document.getElementById('editStatus').value = item.status;
document.getElementById('editPriority').value = item.priority;
document.getElementById('editDueDate').value = item.due_date || '';
// 处理 datetime-local 格式
if (item.due_date) {
// 尝试解析日期时间格式
let dueDate = item.due_date;
if (dueDate.includes('T')) {
// ISO 格式,截取到分钟
dueDate = dueDate.substring(0, 16);
} else if (dueDate.length === 10) {
// 只有日期,添加默认时间 00:00
dueDate = dueDate + 'T00:00';
}
document.getElementById('editDueDate').value = dueDate;
} else {
document.getElementById('editDueDate').value = '';
}
}
new bootstrap.Modal(document.getElementById('editModal')).show();
@@ -1588,10 +1642,79 @@ function truncate(str, len) {
return str && str.length > len ? str.substring(0, len) + '...' : str || '';
}
// 简短日期格式(用于卡片显示)
function formatShortDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
const min = String(date.getMinutes()).padStart(2, '0');
return `${month}-${day} ${hour}:${min}`;
}
function formatDate(dateStr) {
return new Date(dateStr).toLocaleString('zh-CN');
}
// 格式化截止时间(友好显示)
function formatDueDate(dueDate) {
if (!dueDate) return '';
let date;
if (dueDate.includes('T')) {
date = new Date(dueDate);
} else if (dueDate.length === 10) {
// 只有日期
date = new Date(dueDate + 'T00:00:00');
} else {
date = new Date(dueDate);
}
// 格式化为友好格式
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const targetDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const daysDiff = Math.floor((targetDay - today) / (1000 * 60 * 60 * 24));
let datePart;
if (daysDiff === 0) {
datePart = '今天';
} else if (daysDiff === 1) {
datePart = '明天';
} else if (daysDiff === -1) {
datePart = '昨天';
} else {
datePart = date.toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'});
}
const timePart = date.toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'});
return `${datePart} ${timePart}`;
}
// 格式化截止时间(完整显示)
function formatDueDateFull(dueDate) {
if (!dueDate) return '';
let date;
if (dueDate.includes('T')) {
date = new Date(dueDate);
} else if (dueDate.length === 10) {
date = new Date(dueDate + 'T00:00:00');
} else {
date = new Date(dueDate);
}
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -2084,14 +2207,6 @@ async function sendItemEmail() {
}
}
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// ============ 提醒功能 ============
let reminderData = null;
@@ -2162,7 +2277,7 @@ function showRemindersModal() {
html += `<div class="list-group-item list-group-item-danger d-flex justify-content-between align-items-center">
<div>
<strong>${item.title || '(无标题)'}</strong>
<br><small class="text-muted">截止: ${item.due_date} | 已过期 ${item.days_overdue} 天</small>
<br><small class="text-muted">截止: ${formatDueDateFull(item.due_date)} | 已过期 ${item.days_overdue} 天</small>
</div>
<div>
<button class="btn btn-sm btn-success" onclick="completeReminder(${item.id})" title="完成">
@@ -2185,7 +2300,7 @@ function showRemindersModal() {
html += `<div class="list-group-item list-group-item-warning d-flex justify-content-between align-items-center">
<div>
<strong>${item.title || '(无标题)'}</strong>
<br><small class="text-muted">截止: ${item.due_date}</small>
<br><small class="text-muted">截止: ${formatDueDateFull(item.due_date)}</small>
</div>
<div>
<button class="btn btn-sm btn-success" onclick="completeReminder(${item.id})" title="完成">
@@ -2208,7 +2323,7 @@ function showRemindersModal() {
html += `<div class="list-group-item list-group-item-info d-flex justify-content-between align-items-center">
<div>
<strong>${item.title || '(无标题)'}</strong>
<br><small class="text-muted">截止: ${item.due_date}</small>
<br><small class="text-muted">截止: ${formatDueDateFull(item.due_date)}</small>
</div>
<div>
<button class="btn btn-sm btn-success" onclick="completeReminder(${item.id})" title="完成">

View File

@@ -464,21 +464,36 @@ class Database:
item['tags'] = self._get_item_tags(conn, item['id'])
try:
due_date = datetime.strptime(item['due_date'], '%Y-%m-%d')
# 计算距离到期的时间
days_left = (due_date.date() - now.date()).days
due_date_str = item['due_date']
# 支持多种日期格式
if 'T' in due_date_str:
# ISO 格式2026-04-16T14:30
due_date = datetime.strptime(due_date_str[:16], '%Y-%m-%dT%H:%M')
elif len(due_date_str) == 10:
# 只有日期2026-04-16视为当天 23:59:59
due_date = datetime.strptime(due_date_str, '%Y-%m-%d').replace(hour=23, minute=59, second=59)
else:
# 其他格式,尝试解析
due_date = datetime.strptime(due_date_str.split('.')[0], '%Y-%m-%dT%H:%M:%S')
if days_left < 0:
# 计算距离到期的时间
time_left = due_date - now
if time_left.total_seconds() < 0:
# 已过期
item['days_overdue'] = abs(days_left)
days_overdue = abs(int(time_left.total_seconds() / 86400))
item['days_overdue'] = days_overdue
reminders['overdue'].append(item)
elif days_left == 0:
# 今天到期
elif time_left.total_seconds() < 86400: # 24小时内
# 判断是今天还是明天
if due_date.date() == now.date():
reminders['due_today'].append(item)
else:
reminders['due_soon'].append(item)
elif due_date.date() == now.date():
# 今天到期超过24小时的情况比如现在凌晨截止时间是晚上
reminders['due_today'].append(item)
elif days_left == 1:
# 明天到期24小时内
reminders['due_soon'].append(item)
except ValueError:
except (ValueError, AttributeError) as e:
# 日期格式错误,跳过
continue