diff --git a/xian_favor/api.py b/xian_favor/api.py index d6544c2..634e739 100644 --- a/xian_favor/api.py +++ b/xian_favor/api.py @@ -24,12 +24,21 @@ def list_items(): elif starred_param == 'false' or starred_param == '0': starred = False + # 文件夹ID参数 + folder_id_param = request.args.get('folder_id') + folder_id = None + if folder_id_param == '-1': + folder_id = -1 # 未分类 + elif folder_id_param: + folder_id = int(folder_id_param) + items = db.list_items( type=request.args.get('type'), status=request.args.get('status'), tag=request.args.get('tag'), keyword=request.args.get('keyword'), starred=starred, + folder_id=folder_id, sort_by=request.args.get('sort_by'), sort_order=request.args.get('sort_order'), limit=int(request.args.get('limit', 50)), @@ -76,7 +85,8 @@ def create_item(): due_date=data.get('due_date'), note=data.get('note'), tags=data.get('tags', []), - is_starred=data.get('is_starred', False) + is_starred=data.get('is_starred', False), + folder_id=data.get('folder_id') ) item = db.get_item(item_id) return jsonify({'success': True, 'data': item}), 201 @@ -742,6 +752,84 @@ def delete_backup(backup_name): return jsonify({'success': False, 'error': '备份不存在'}), 404 +# ============ Folder 文件夹管理 API ============ + +@app.route('/api/folders', methods=['GET']) +def list_folders(): + """列出文件夹""" + type_filter = request.args.get('type') + folders = db.list_folders(type=type_filter) + return jsonify({'success': True, 'data': folders}) + + +@app.route('/api/folders', methods=['POST']) +def create_folder(): + """创建文件夹""" + data = request.get_json() + + name = data.get('name', '').strip() + type_val = data.get('type', 'text') + + if not name: + return jsonify({'success': False, 'error': '文件夹名称不能为空'}), 400 + + if type_val not in ITEM_TYPES: + return jsonify({'success': False, 'error': f'无效类型: {type_val}'}), 400 + + try: + folder_id = db.create_folder(name=name, type=type_val) + folder = db.get_folder(folder_id) + return jsonify({'success': True, 'data': folder}), 201 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/folders/', methods=['GET']) +def get_folder(folder_id): + """获取文件夹""" + folder = db.get_folder(folder_id) + if not folder: + return jsonify({'success': False, 'error': '文件夹不存在'}), 404 + return jsonify({'success': True, 'data': folder}) + + +@app.route('/api/folders/', methods=['PUT']) +def update_folder(folder_id): + """更新文件夹""" + data = request.get_json() + name = data.get('name', '').strip() + + if not name: + return jsonify({'success': False, 'error': '文件夹名称不能为空'}), 400 + + if db.update_folder(folder_id, name=name): + folder = db.get_folder(folder_id) + return jsonify({'success': True, 'data': folder}) + return jsonify({'success': False, 'error': '文件夹不存在'}), 404 + + +@app.route('/api/folders/', methods=['DELETE']) +def delete_folder(folder_id): + """删除文件夹""" + move_to_root = request.args.get('move_items', 'true') == 'true' + + if db.delete_folder(folder_id, move_items_to_root=move_to_root): + return jsonify({'success': True}) + return jsonify({'success': False, 'error': '文件夹不存在'}), 404 + + +@app.route('/api/items//move', methods=['POST']) +def move_item_to_folder(item_id): + """将条目移动到文件夹""" + data = request.get_json() + folder_id = data.get('folder_id') # None 表示移出文件夹 + + if db.move_item_to_folder(item_id, folder_id): + item = db.get_item(item_id) + return jsonify({'success': True, 'data': item}) + return jsonify({'success': False, 'error': '条目不存在'}), 404 + + # ============ Web 页面 ============ @app.route('/') @@ -841,6 +929,30 @@ INDEX_TEMPLATE = ''' opacity: 0.8; } + /* 文件夹列表样式 */ + .sidebar-section .section-header { font-weight: 500; } + .folder-list { + padding-left: 10px; + max-height: 150px; + overflow-y: auto; + } + .folder-list a { + padding: 6px 20px; + font-size: 13px; + color: #adb5bd; + } + .folder-list a:hover, .folder-list a.active { + background: #495057; + color: #fff; + } + .folder-list a i { margin-right: 5px; } + .folder-action { + font-size: 12px; + color: #6c757d; + padding: 4px 20px; + } + .folder-action:hover { color: #adb5bd; } + /* 离线遮罩 */ .offline-overlay { position: fixed; @@ -927,10 +1039,35 @@ INDEX_TEMPLATE = '''