Compare commits

..

6 Commits

3 changed files with 319 additions and 16 deletions

View File

@@ -136,6 +136,9 @@ xian-favor/
## 版本历史
- v1.7.0 (2026-04-13): 编辑收藏时支持更改类型,动态显示对应字段
- v1.6.0 (2026-04-13): 标签管理添加编辑功能,支持修改标签名称
- v1.5.1 (2026-04-13): 标签管理添加搜索功能,实时过滤标签列表
- v1.5.0 (2026-04-13): 首页添加分页功能每页20条记录
- v1.4.0 (2026-04-13): 首页添加导出按钮一键导出所有收藏为JSON文件
- v1.3.2 (2026-04-13): 编辑和添加模态框改为大尺寸(modal-lg)

View File

@@ -132,6 +132,23 @@ def create_tag():
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/tags/<int:tag_id>', methods=['PUT'])
def update_tag(tag_id):
"""更新标签"""
data = request.get_json()
name = data.get('name', '').strip()
if not name:
return jsonify({'success': False, 'error': '标签名不能为空'}), 400
try:
if db.update_tag(tag_id, name):
return jsonify({'success': True, 'data': {'id': tag_id, 'name': name}})
return jsonify({'success': False, 'error': '标签不存在或名称已存在'}), 404
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/tags/<int:tag_id>', methods=['DELETE'])
def delete_tag(tag_id):
"""删除标签"""
@@ -147,6 +164,82 @@ def get_stats():
return jsonify({'success': True, 'data': stats})
@app.route('/api/ai-process', methods=['POST'])
def ai_process():
"""AI处理文本"""
import requests
data = request.get_json()
text = data.get('text', '').strip()
if not text:
return jsonify({'success': False, 'error': '请输入文本内容'}), 400
# 大模型配置
llm_url = "http://192.168.2.17:19007/v1/chat/completions"
llm_key = "xxxx"
prompt = f"""请分析以下文本内容,识别其类型并提取关键信息。
文本内容:
{text}
请按以下JSON格式返回结果只返回JSON不要其他内容
{
"type": "text/link/column/todo",
"title": "提取的标题(简短概括)",
"content": "主要内容(如果是文本类型)",
"url": "如果是链接或专栏提取URL",
"source": "如果是专栏,提取来源",
"tags": ["相关标签1", "标签2"],
"note": "补充说明或备注",
"status": "如果是待办默认pending",
"priority": "如果是待办默认medium"
}
类型判断规则:
- link: 包含http/https链接且不是专栏订阅地址
- column: 专栏订阅地址或RSS链接
- todo: 包含任务、待办、提醒等关键词
- text: 其他文本内容"""
try:
response = requests.post(
llm_url,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {llm_key}"
},
json={
"model": "auto",
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.3
},
timeout=30
)
if response.status_code != 200:
return jsonify({'success': False, 'error': f'模型调用失败: {response.status_code}'}), 500
result = response.json()
content = result['choices'][0]['message']['content']
# 解析JSON
import json
import re
# 提取JSON部分
json_match = re.search(r'\{.*\}', content, re.DOTALL)
if json_match:
parsed = json.loads(json_match.group())
return jsonify({'success': True, 'data': parsed})
else:
return jsonify({'success': False, 'error': '无法解析模型返回'}), 500
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/search', methods=['GET'])
def search_items():
"""搜索条目"""
@@ -246,6 +339,9 @@ INDEX_TEMPLATE = '''
<option value="todo">待办</option>
</select>
</div>
<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-success me-2" onclick="exportData()" title="导出JSON">
<i class="bi bi-download"></i> 导出
</button>
@@ -415,7 +511,12 @@ INDEX_TEMPLATE = '''
<input type="hidden" id="editId">
<div class="mb-3">
<label class="form-label">类型</label>
<input type="text" id="editType" class="form-control" readonly>
<select id="editType" class="form-select">
<option value="text">📝 文本</option>
<option value="link">🔗 链接</option>
<option value="column">📰 专栏</option>
<option value="todo">✅ 待办</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">标题</label>
@@ -511,6 +612,44 @@ INDEX_TEMPLATE = '''
</div>
</div>
<!-- AI自动添加模态框 -->
<div class="modal fade" id="aiAddModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-robot"></i> AI自动添加</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">输入文本内容</label>
<textarea id="aiInputText" class="form-control" rows="6" placeholder="粘贴文本、链接、笔记等AI会自动识别并整理..."></textarea>
</div>
<div id="aiResult" style="display:none;">
<hr>
<h6>识别结果:</h6>
<div id="aiResultContent" class="border rounded p-3 bg-light"></div>
</div>
<div id="aiLoading" style="display:none;">
<div class="text-center py-3">
<div class="spinner-border text-primary" role="status"></div>
<div class="mt-2">AI正在分析...</div>
</div>
</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" id="aiProcessBtn" onclick="processAIInput()">
<i class="bi bi-magic"></i> 分析并添加
</button>
<button type="button" class="btn btn-success" id="aiConfirmBtn" style="display:none;" onclick="confirmAIAdd()">
<i class="bi bi-check"></i> 确认添加
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const API_BASE = '/api';
@@ -538,6 +677,11 @@ document.addEventListener('DOMContentLoaded', async () => {
document.getElementById('todoFields').style.display = type === 'todo' ? 'block' : 'none';
});
// 编辑时类型切换
document.getElementById('editType').addEventListener('change', (e) => {
updateEditFieldsByType(e.target.value);
});
// 搜索
document.getElementById('searchInput').addEventListener('input', debounce(loadItems, 300));
@@ -798,14 +942,11 @@ async function openEditModal(id) {
const type = item.type;
document.getElementById('editId').value = id;
document.getElementById('editType').value = getTypeLabel(type);
document.getElementById('editType').value = 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';
updateEditFieldsByType(type);
document.getElementById('editContent').value = item.content || '';
document.getElementById('editUrl').value = item.url || '';
@@ -822,16 +963,21 @@ async function openEditModal(id) {
new bootstrap.Modal(document.getElementById('editModal')).show();
}
// 根据类型更新编辑表单字段显示
function updateEditFieldsByType(type) {
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';
}
// 保存编辑
async function saveEdit() {
const id = document.getElementById('editId').value;
// 获取当前条目的类型
const detailRes = await fetch(`${API_BASE}/items/${currentDetailId}`);
const detailData = await detailRes.json();
const type = detailData.data.type;
const type = document.getElementById('editType').value; // 从下拉框获取新类型
const data = {
type: type, // 包含类型变更
title: document.getElementById('editTitle').value,
content: type === 'text' ? document.getElementById('editContent').value : null,
url: ['link', 'column'].includes(type) ? document.getElementById('editUrl').value : null,
@@ -989,14 +1135,28 @@ async function loadTagManagerList() {
}
container.innerHTML = tags.map(tag => `
<div class="d-flex justify-content-between align-items-center p-2 border-bottom">
<div>
<div class="d-flex justify-content-between align-items-center p-2 border-bottom" id="tag-row-${tag.id}">
<div id="tag-display-${tag.id}">
<span class="badge bg-secondary">${tag.name}</span>
<span class="text-muted small ms-2">${tag.item_count || 0} 个条目</span>
</div>
<button class="btn btn-sm btn-outline-danger" onclick="deleteTagManager(${tag.id}, '${tag.name}')">
<i class="bi bi-trash"></i>
</button>
<div id="tag-edit-${tag.id}" style="display:none;">
<input type="text" class="form-control form-control-sm" id="edit-tag-name-${tag.id}" value="${tag.name}" style="width:150px;">
</div>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" id="tag-edit-btn-${tag.id}" onclick="showEditTag(${tag.id})" title="编辑">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-outline-success" id="tag-save-btn-${tag.id}" style="display:none;" onclick="saveEditTag(${tag.id})" title="保存">
<i class="bi bi-check"></i>
</button>
<button class="btn btn-outline-secondary" id="tag-cancel-btn-${tag.id}" style="display:none;" onclick="cancelEditTag(${tag.id}, '${tag.name}')" title="取消">
<i class="bi bi-x"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteTagManager(${tag.id}, '${tag.name}')" title="删除">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`).join('');
}
@@ -1027,6 +1187,45 @@ async function deleteTagManager(id, name) {
loadItems();
}
// 编辑标签
function showEditTag(id) {
document.getElementById(`tag-display-${id}`).style.display = 'none';
document.getElementById(`tag-edit-${id}`).style.display = 'block';
document.getElementById(`tag-edit-btn-${id}`).style.display = 'none';
document.getElementById(`tag-save-btn-${id}`).style.display = 'inline-block';
document.getElementById(`tag-cancel-btn-${id}`).style.display = 'inline-block';
document.getElementById(`edit-tag-name-${id}`).focus();
}
function cancelEditTag(id, oldName) {
document.getElementById(`edit-tag-name-${id}`).value = oldName;
document.getElementById(`tag-display-${id}`).style.display = 'block';
document.getElementById(`tag-edit-${id}`).style.display = 'none';
document.getElementById(`tag-edit-btn-${id}`).style.display = 'inline-block';
document.getElementById(`tag-save-btn-${id}`).style.display = 'none';
document.getElementById(`tag-cancel-btn-${id}`).style.display = 'none';
}
async function saveEditTag(id) {
const newName = document.getElementById(`edit-tag-name-${id}`).value.trim();
if (!newName) return;
const res = await fetch(`${API_BASE}/tags/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName })
});
if (res.ok) {
loadTagManagerList();
loadTags();
loadItems();
} else {
const data = await res.json();
alert(data.error || '更新失败');
}
}
// 导出数据
async function exportData() {
const res = await fetch(`${API_BASE}/items?limit=1000`);
@@ -1050,6 +1249,95 @@ async function exportData() {
URL.revokeObjectURL(url);
}
// AI自动添加
let aiParsedData = null;
function showAIAddModal() {
document.getElementById('aiInputText').value = '';
document.getElementById('aiResult').style.display = 'none';
document.getElementById('aiLoading').style.display = 'none';
document.getElementById('aiProcessBtn').style.display = 'inline-block';
document.getElementById('aiConfirmBtn').style.display = 'none';
aiParsedData = null;
new bootstrap.Modal(document.getElementById('aiAddModal')).show();
}
async function processAIInput() {
const text = document.getElementById('aiInputText').value.trim();
if (!text) {
alert('请输入文本内容');
return;
}
// 显示加载
document.getElementById('aiLoading').style.display = 'block';
document.getElementById('aiProcessBtn').disabled = true;
try {
const res = await fetch(`${API_BASE}/ai-process`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
const data = await res.json();
document.getElementById('aiLoading').style.display = 'none';
document.getElementById('aiProcessBtn').disabled = false;
if (!data.success) {
alert('AI分析失败: ' + data.error);
return;
}
aiParsedData = data.data;
// 显示结果
const typeLabels = { text: '📝 文本', link: '🔗 链接', column: '📰 专栏', todo: '✅ 待办' };
let html = `<div><strong>类型:</strong> ${typeLabels[aiParsedData.type] || aiParsedData.type}</div>`;
if (aiParsedData.title) html += `<div><strong>标题:</strong> ${aiParsedData.title}</div>`;
if (aiParsedData.url) html += `<div><strong>URL:</strong> ${aiParsedData.url}</div>`;
if (aiParsedData.content) html += `<div><strong>内容:</strong> ${aiParsedData.content.substring(0, 200)}${aiParsedData.content.length > 200 ? '...' : ''}</div>`;
if (aiParsedData.tags && aiParsedData.tags.length) html += `<div><strong>标签:</strong> ${aiParsedData.tags.join(', ')}</div>`;
if (aiParsedData.note) html += `<div><strong>备注:</strong> ${aiParsedData.note.substring(0, 100)}${aiParsedData.note.length > 100 ? '...' : ''}</div>`;
if (aiParsedData.type === 'todo') {
const statusLabels = { pending: '⏳ 待处理', in_progress: '🔄 进行中', completed: '✅ 已完成' };
const priorityLabels = { low: '🟢 低', medium: '🟡 中', high: '🟠 高', urgent: '🔴 紧急' };
html += `<div><strong>状态:</strong> ${statusLabels[aiParsedData.status] || 'pending'}</div>`;
html += `<div><strong>优先级:</strong> ${priorityLabels[aiParsedData.priority] || 'medium'}</div>`;
}
document.getElementById('aiResultContent').innerHTML = html;
document.getElementById('aiResult').style.display = 'block';
document.getElementById('aiProcessBtn').style.display = 'none';
document.getElementById('aiConfirmBtn').style.display = 'inline-block';
} catch (e) {
document.getElementById('aiLoading').style.display = 'none';
document.getElementById('aiProcessBtn').disabled = false;
alert('请求失败: ' + e.message);
}
}
async function confirmAIAdd() {
if (!aiParsedData) return;
const res = await fetch(`${API_BASE}/items`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(aiParsedData)
});
if (res.ok) {
bootstrap.Modal.getInstance(document.getElementById('aiAddModal')).hide();
refreshData();
alert('添加成功!');
} else {
const data = await res.json();
alert('添加失败: ' + data.error);
}
}
function debounce(fn, delay) {
let timer;
return function(...args) {

View File

@@ -244,6 +244,18 @@ class Database:
""")
return [dict(row) for row in cursor.fetchall()]
def update_tag(self, tag_id: int, name: str) -> bool:
"""更新标签名称"""
with self.get_conn() as conn:
cursor = conn.cursor()
# 检查名称是否已存在(排除自己)
cursor.execute("SELECT id FROM tags WHERE name = ? AND id != ?", (name, tag_id))
if cursor.fetchone():
return False # 名称已存在
cursor.execute("UPDATE tags SET name = ? WHERE id = ?", (name, tag_id))
conn.commit()
return cursor.rowcount > 0
def delete_tag(self, tag_id: int = None, name: str = None) -> bool:
"""删除标签"""
with self.get_conn() as conn: