feat(v4.0.0): 快速插入支持选中位置插入

- 先选中部分文本,再双击可在选中位置前/后插入
- 支持替换选中内容
- 自动识别选中文本并在原内容中定位
This commit is contained in:
2026-04-22 12:49:37 +08:00
parent 407a63f3cf
commit 6a0f8d7196

View File

@@ -1084,6 +1084,14 @@ INDEX_TEMPLATE = '''
.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; }
/* 快速插入区域样式 */
.quick-insert-area:hover {
background: #f0f8ff;
border-color: #0d6efd;
}
.quick-insert-area:active {
background: #e8f4ff;
}
.star-btn { font-size: 11px; }
.status-pending { color: #ffc107; }
.status-in_progress { color: #17a2b8; }
@@ -1789,6 +1797,55 @@ INDEX_TEMPLATE = '''
</div>
</div>
<!-- 快速插入模态框 -->
<div class="modal fade" id="quickInsertModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-plus-circle"></i> 快速插入内容</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="quickInsertItemId">
<input type="hidden" id="quickInsertField">
<div class="mb-3">
<label class="form-label">插入位置</label>
<select id="quickInsertPosition" class="form-select">
<option value="start">开头</option>
<option value="end" selected>末尾</option>
<option value="selection_before">选中内容之前</option>
<option value="selection_after">选中内容之后</option>
<option value="selection_replace">替换选中内容</option>
</select>
<input type="hidden" id="quickInsertSelection">
<div id="quickInsertSelectionInfo" class="form-text" style="display:none;">
选中内容: <code id="quickInsertSelectionPreview"></code>
</div>
</div>
<div class="mb-3">
<label class="form-label">要插入的内容</label>
<textarea id="quickInsertContent" class="form-control" rows="5" placeholder="输入要插入的内容..."></textarea>
</div>
<div class="mb-3">
<label class="form-label">插入格式</label>
<select id="quickInsertFormat" class="form-select">
<option value="raw">原文本</option>
<option value="newline">独立一行</option>
<option value="bullet">列表项 (- item)</option>
<option value="heading4">标题 ####</option>
</select>
</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="executeQuickInsert()">
<i class="bi bi-check"></i> 插入并保存
</button>
</div>
</div>
</div>
</div>
<!-- 移动到文件夹模态框 -->
<div class="modal fade" id="moveToFolderModal" tabindex="-1">
<div class="modal-dialog">
@@ -2691,7 +2748,19 @@ async function showDetail(id) {
}
if (item.content) {
html += `<div class="mb-3"><strong>内容:</strong><br><div class="border rounded p-3 bg-light markdown-content">${renderMarkdown(item.content)}</div></div>`;
html += `<div class="mb-3">
<div class="d-flex justify-content-between align-items-center">
<strong>内容:</strong>
<small class="text-muted"><i class="bi bi-hand-index"></i> 可先选中部分文本,再双击在此处插入</small>
</div>
<div class="border rounded p-3 bg-light markdown-content quick-insert-area"
data-field="content"
data-item-id="${item.id}"
ondblclick="showQuickInsert(${item.id}, 'content')"
style="cursor: pointer;">
${renderMarkdown(item.content)}
</div>
</div>`;
}
if (item.source) {
@@ -2711,7 +2780,19 @@ async function showDetail(id) {
}
if (item.note) {
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="mb-3">
<div class="d-flex justify-content-between align-items-center">
<strong>详情/备注:</strong>
<small class="text-muted"><i class="bi bi-hand-index"></i> 可先选中部分文本,再双击在此处插入</small>
</div>
<div class="border rounded p-3 bg-light markdown-content quick-insert-area"
data-field="note"
data-item-id="${item.id}"
ondblclick="showQuickInsert(${item.id}, 'note')"
style="cursor: pointer;">
${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>`;
@@ -3546,6 +3627,137 @@ async function emptyTrash() {
// ============ Folder 文件夹管理 ============
// ============ 快速插入功能 ============
// 保存当前详情数据,用于快速插入
let currentDetailItem = null;
// 显示快速插入模态框
function showQuickInsert(itemId, field) {
document.getElementById('quickInsertItemId').value = itemId;
document.getElementById('quickInsertField').value = field;
document.getElementById('quickInsertContent').value = '';
document.getElementById('quickInsertPosition').value = 'end';
document.getElementById('quickInsertFormat').value = 'newline';
// 获取当前选中的文本
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (selectedText) {
document.getElementById('quickInsertSelection').value = selectedText;
document.getElementById('quickInsertSelectionInfo').style.display = 'block';
document.getElementById('quickInsertSelectionPreview').textContent = selectedText.substring(0, 50) + (selectedText.length > 50 ? '...' : '');
document.getElementById('quickInsertPosition').value = 'selection_after'; // 默认在选中内容之后插入
} else {
document.getElementById('quickInsertSelection').value = '';
document.getElementById('quickInsertSelectionInfo').style.display = 'none';
}
new bootstrap.Modal(document.getElementById('quickInsertModal')).show();
}
// 执行快速插入
async function executeQuickInsert() {
const itemId = document.getElementById('quickInsertItemId').value;
const field = document.getElementById('quickInsertField').value;
const insertContent = document.getElementById('quickInsertContent').value.trim();
const position = document.getElementById('quickInsertPosition').value;
const format = document.getElementById('quickInsertFormat').value;
const selectedText = document.getElementById('quickInsertSelection').value;
if (!insertContent) {
alert('请输入要插入的内容');
return;
}
// 离线检查
if (!checkOnlineBeforeAction('快速插入')) return;
// 获取当前数据
const res = await fetch(`${API_BASE}/items/${itemId}`);
const data = await res.json();
if (!data.success) {
alert('获取数据失败');
return;
}
const item = data.data;
let originalContent = item[field] || '';
// 根据格式处理插入内容
let formattedContent = insertContent;
switch (format) {
case 'newline':
formattedContent = '\\n' + insertContent;
break;
case 'bullet':
formattedContent = '\\n- ' + insertContent;
break;
case 'heading4':
formattedContent = '\\n#### ' + insertContent;
break;
default:
formattedContent = insertContent;
}
// 根据位置插入
let newContent;
if (position === 'start') {
newContent = formattedContent + originalContent;
} else if (position === 'end') {
newContent = originalContent + formattedContent;
} else if (position.startsWith('selection_') && selectedText) {
// 在选中内容位置插入
const selectionIndex = originalContent.indexOf(selectedText);
if (selectionIndex === -1) {
alert('未在原内容中找到选中的文本可能是因为Markdown渲染后的文本与原文本不完全一致。\\n请尝试手动编辑。');
return;
}
const before = originalContent.substring(0, selectionIndex);
const selected = originalContent.substring(selectionIndex, selectionIndex + selectedText.length);
const after = originalContent.substring(selectionIndex + selectedText.length);
if (position === 'selection_before') {
newContent = before + formattedContent + selected + after;
} else if (position === 'selection_after') {
newContent = before + selected + formattedContent + after;
} else if (position === 'selection_replace') {
newContent = before + formattedContent + after;
}
} else {
// 没有选中内容但选择了选中位置选项,默认末尾
newContent = originalContent + formattedContent;
}
// 保存更新
const updateData = {};
updateData[field] = newContent;
const updateRes = await fetch(`${API_BASE}/items/${itemId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updateData)
});
const updateResult = await updateRes.json();
if (updateResult.success) {
bootstrap.Modal.getInstance(document.getElementById('quickInsertModal')).hide();
// 刷新详情显示
showDetail(itemId);
// 刷新列表
refreshData();
} else {
alert('保存失败: ' + updateResult.error);
}
}
async function loadFolders() {
const res = await fetch(`${API_BASE}/folders`);
const data = await res.json();