feat(v4.0.0): 快速插入支持选中位置插入
- 先选中部分文本,再双击可在选中位置前/后插入 - 支持替换选中内容 - 自动识别选中文本并在原内容中定位
This commit is contained in:
@@ -1084,6 +1084,14 @@ INDEX_TEMPLATE = '''
|
|||||||
.markdown-content pre { font-size: 0.85em; overflow-x: auto; }
|
.markdown-content pre { font-size: 0.85em; overflow-x: auto; }
|
||||||
.markdown-content ul, .markdown-content ol { margin-bottom: 0.5em; }
|
.markdown-content ul, .markdown-content ol { margin-bottom: 0.5em; }
|
||||||
.markdown-content a { color: #0d6efd; }
|
.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; }
|
.star-btn { font-size: 11px; }
|
||||||
.status-pending { color: #ffc107; }
|
.status-pending { color: #ffc107; }
|
||||||
.status-in_progress { color: #17a2b8; }
|
.status-in_progress { color: #17a2b8; }
|
||||||
@@ -1789,6 +1797,55 @@ INDEX_TEMPLATE = '''
|
|||||||
</div>
|
</div>
|
||||||
</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 fade" id="moveToFolderModal" tabindex="-1">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
@@ -2691,7 +2748,19 @@ async function showDetail(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item.content) {
|
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) {
|
if (item.source) {
|
||||||
@@ -2711,7 +2780,19 @@ async function showDetail(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item.note) {
|
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>`;
|
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 文件夹管理 ============
|
// ============ 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() {
|
async function loadFolders() {
|
||||||
const res = await fetch(`${API_BASE}/folders`);
|
const res = await fetch(`${API_BASE}/folders`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|||||||
Reference in New Issue
Block a user