feat: 网页端支持查看详情和编辑功能
This commit is contained in:
@@ -364,6 +364,103 @@ INDEX_TEMPLATE = '''
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 详情模态框 -->
|
||||||
|
<div class="modal fade" id="detailModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><span id="detailTypeIcon"></span> <span id="detailTitle"></span></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="detailContent">
|
||||||
|
<!-- 动态填充 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-primary" onclick="openEditModalFromDetail()">
|
||||||
|
<i class="bi bi-pencil"></i> 编辑
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑模态框 -->
|
||||||
|
<div class="modal fade" id="editModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">编辑条目</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="editForm">
|
||||||
|
<input type="hidden" id="editId">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">类型</label>
|
||||||
|
<input type="text" id="editType" class="form-control" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">标题</label>
|
||||||
|
<input type="text" id="editTitle" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" id="editContentGroup">
|
||||||
|
<label class="form-label">内容</label>
|
||||||
|
<textarea id="editContent" class="form-control" rows="5"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" id="editUrlGroup" style="display:none;">
|
||||||
|
<label class="form-label">URL</label>
|
||||||
|
<input type="url" id="editUrl" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" id="editSourceGroup" style="display:none;">
|
||||||
|
<label class="form-label">来源</label>
|
||||||
|
<input type="text" id="editSource" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" id="editTodoFields" style="display:none;">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label">状态</label>
|
||||||
|
<select id="editStatus" class="form-select">
|
||||||
|
<option value="pending">⏳ 待处理</option>
|
||||||
|
<option value="in_progress">🔄 进行中</option>
|
||||||
|
<option value="completed">✅ 已完成</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label">优先级</label>
|
||||||
|
<select id="editPriority" class="form-select">
|
||||||
|
<option value="low">🟢 低</option>
|
||||||
|
<option value="medium">🟡 中</option>
|
||||||
|
<option value="high">🟠 高</option>
|
||||||
|
<option value="urgent">🔴 紧急</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<label class="form-label">截止日期</label>
|
||||||
|
<input type="date" id="editDueDate" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">标签 (逗号分隔)</label>
|
||||||
|
<input type="text" id="editTags" class="form-control" placeholder="标签1, 标签2">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">备注</label>
|
||||||
|
<input type="text" id="editNote" class="form-control">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</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="saveEdit()">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const API_BASE = '/api';
|
const API_BASE = '/api';
|
||||||
@@ -437,15 +534,15 @@ function renderItems(items) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = items.map(item => `
|
container.innerHTML = items.map(item => `
|
||||||
<div class="card type-${item.type}">
|
<div class="card type-${item.type}" style="cursor: pointer;" onclick="showDetail(${item.id})">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between align-items-start">
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
<div>
|
<div style="flex: 1;">
|
||||||
<h6 class="card-title">
|
<h6 class="card-title">
|
||||||
${getTypeIcon(item.type)} ${item.title || truncate(item.content || item.url, 50)}
|
${getTypeIcon(item.type)} ${item.title || truncate(item.content || item.url, 50)}
|
||||||
</h6>
|
</h6>
|
||||||
<p class="card-text text-muted small mb-2">
|
<p class="card-text text-muted small mb-2">
|
||||||
${item.url ? `<a href="${item.url}" target="_blank">${item.url}</a><br>` : ''}
|
${item.url ? `<a href="${item.url}" target="_blank" onclick="event.stopPropagation();">${item.url}</a><br>` : ''}
|
||||||
${item.content ? truncate(item.content, 100) : ''}
|
${item.content ? truncate(item.content, 100) : ''}
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
@@ -457,7 +554,8 @@ function renderItems(items) {
|
|||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm" onclick="event.stopPropagation();">
|
||||||
|
<button class="btn btn-outline-primary" onclick="openEditModal(${item.id})" title="编辑"><i class="bi bi-pencil"></i></button>
|
||||||
${item.type === 'todo' && item.status !== 'completed' ?
|
${item.type === 'todo' && item.status !== 'completed' ?
|
||||||
`<button class="btn btn-outline-success" onclick="completeItem(${item.id})" title="完成"><i class="bi bi-check-lg"></i></button>` : ''}
|
`<button class="btn btn-outline-success" onclick="completeItem(${item.id})" title="完成"><i class="bi bi-check-lg"></i></button>` : ''}
|
||||||
<button class="btn btn-outline-danger" onclick="deleteItem(${item.id})" title="删除"><i class="bi bi-trash"></i></button>
|
<button class="btn btn-outline-danger" onclick="deleteItem(${item.id})" title="删除"><i class="bi bi-trash"></i></button>
|
||||||
@@ -526,19 +624,148 @@ async function deleteItem(id) {
|
|||||||
loadStats();
|
loadStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 当前查看的条目ID
|
||||||
|
let currentDetailId = null;
|
||||||
|
|
||||||
|
// 显示详情
|
||||||
|
async function showDetail(id) {
|
||||||
|
currentDetailId = id;
|
||||||
|
const res = await fetch(`${API_BASE}/items/${id}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!data.success) return;
|
||||||
|
|
||||||
|
const item = data.data;
|
||||||
|
document.getElementById('detailTypeIcon').textContent = getTypeIcon(item.type);
|
||||||
|
document.getElementById('detailTitle').textContent = item.title || '(无标题)';
|
||||||
|
|
||||||
|
let html = `<div class="mb-3"><strong>类型:</strong> ${getTypeLabel(item.type)}</div>`;
|
||||||
|
|
||||||
|
if (item.url) {
|
||||||
|
html += `<div class="mb-3"><strong>URL:</strong> <a href="${item.url}" target="_blank">${item.url}</a></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.source) {
|
||||||
|
html += `<div class="mb-3"><strong>来源:</strong> ${escapeHtml(item.source)}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === 'todo') {
|
||||||
|
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>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.tags.length) {
|
||||||
|
html += `<div class="mb-3"><strong>标签:</strong> ${item.tags.map(t => `<span class="badge bg-secondary">${escapeHtml(t)}</span>`).join(' ')}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.note) {
|
||||||
|
html += `<div class="mb-3"><strong>备注:</strong> ${escapeHtml(item.note)}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `<div class="text-muted small"><strong>创建时间:</strong> ${formatDate(item.created_at)}<br><strong>更新时间:</strong> ${formatDate(item.updated_at)}</div>`;
|
||||||
|
|
||||||
|
document.getElementById('detailContent').innerHTML = html;
|
||||||
|
|
||||||
|
new bootstrap.Modal(document.getElementById('detailModal')).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从详情页打开编辑
|
||||||
|
function openEditModalFromDetail() {
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('detailModal')).hide();
|
||||||
|
setTimeout(() => openEditModal(currentDetailId), 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开编辑模态框
|
||||||
|
async function openEditModal(id) {
|
||||||
|
currentDetailId = id;
|
||||||
|
const res = await fetch(`${API_BASE}/items/${id}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!data.success) return;
|
||||||
|
|
||||||
|
const item = data.data;
|
||||||
|
const type = item.type;
|
||||||
|
|
||||||
|
document.getElementById('editId').value = id;
|
||||||
|
document.getElementById('editType').value = getTypeLabel(type);
|
||||||
|
document.getElementById('editTitle').value = item.title || '';
|
||||||
|
|
||||||
|
// 根据类型显示/隐藏字段
|
||||||
|
document.getElementById('editContentGroup').style.display = type === 'text' ? 'block' : 'none';
|
||||||
|
document.getElementById('editUrlGroup').style.display = ['link', 'column'].includes(type) ? 'block' : 'none';
|
||||||
|
document.getElementById('editSourceGroup').style.display = type === 'column' ? 'block' : 'none';
|
||||||
|
document.getElementById('editTodoFields').style.display = type === 'todo' ? 'block' : 'none';
|
||||||
|
|
||||||
|
document.getElementById('editContent').value = item.content || '';
|
||||||
|
document.getElementById('editUrl').value = item.url || '';
|
||||||
|
document.getElementById('editSource').value = item.source || '';
|
||||||
|
document.getElementById('editTags').value = item.tags.join(', ');
|
||||||
|
document.getElementById('editNote').value = item.note || '';
|
||||||
|
|
||||||
|
if (type === 'todo') {
|
||||||
|
document.getElementById('editStatus').value = item.status;
|
||||||
|
document.getElementById('editPriority').value = item.priority;
|
||||||
|
document.getElementById('editDueDate').value = item.due_date || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
new bootstrap.Modal(document.getElementById('editModal')).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存编辑
|
||||||
|
async function saveEdit() {
|
||||||
|
const id = document.getElementById('editId').value;
|
||||||
|
const type = currentDetailId ? (await fetch(`${API_BASE}/items/${currentDetailId}`).then(r => r.json())).data.type : 'text';
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
title: document.getElementById('editTitle').value,
|
||||||
|
content: type === 'text' ? document.getElementById('editContent').value : null,
|
||||||
|
url: ['link', 'column'].includes(type) ? document.getElementById('editUrl').value : null,
|
||||||
|
source: type === 'column' ? document.getElementById('editSource').value : null,
|
||||||
|
status: type === 'todo' ? document.getElementById('editStatus').value : null,
|
||||||
|
priority: type === 'todo' ? document.getElementById('editPriority').value : null,
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}/items/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
|
||||||
|
loadItems();
|
||||||
|
loadStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 工具函数
|
// 工具函数
|
||||||
function getTypeIcon(type) {
|
function getTypeIcon(type) {
|
||||||
const icons = { text: '📝', link: '🔗', column: '📰', todo: '✅' };
|
const icons = { text: '📝', link: '🔗', column: '📰', todo: '✅' };
|
||||||
return icons[type] || '📄';
|
return icons[type] || '📄';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTypeLabel(type) {
|
||||||
|
const labels = { text: '📝 文本', link: '🔗 链接', column: '📰 专栏', todo: '✅ 待办' };
|
||||||
|
return labels[type] || type;
|
||||||
|
}
|
||||||
|
|
||||||
function getStatusLabel(status) {
|
function getStatusLabel(status) {
|
||||||
const labels = { pending: '待处理', in_progress: '进行中', completed: '已完成' };
|
const labels = { pending: '⏳ 待处理', in_progress: '🔄 进行中', completed: '✅ 已完成' };
|
||||||
return labels[status] || status;
|
return labels[status] || status;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPriorityLabel(priority) {
|
function getPriorityLabel(priority) {
|
||||||
const labels = { low: '低', medium: '中', high: '高', urgent: '紧急' };
|
const labels = { low: '🟢 低', medium: '🟡 中', high: '🟠 高', urgent: '🔴 紧急' };
|
||||||
return labels[priority] || priority;
|
return labels[priority] || priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,6 +777,11 @@ function formatDate(dateStr) {
|
|||||||
return new Date(dateStr).toLocaleString('zh-CN');
|
return new Date(dateStr).toLocaleString('zh-CN');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
function debounce(fn, delay) {
|
function debounce(fn, delay) {
|
||||||
let timer;
|
let timer;
|
||||||
return function(...args) {
|
return function(...args) {
|
||||||
|
|||||||
Reference in New Issue
Block a user