import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/network/api_exception.dart'; import '../core/network/dio_client.dart'; import '../models/note.dart'; final noteApiProvider = Provider((ref) => NoteApi(ref.watch(dioProvider))); /// 笔记接口 class NoteApi { final Dio _dio; NoteApi(this._dio); /// 分页列表(不含正文) /// /// [scope] all / uncategorized / folder;[folderId] 仅在 scope=folder 时有意义, /// 后端会连同该目录的所有子目录一起查。 Future list({ int page = 1, int size = 20, String scope = 'all', int? folderId, String sort = 'updated', String? keyword, String? tag, }) async { try { final res = await _dio.get('/notes', queryParameters: { 'page': page, 'size': size, 'scope': scope, 'sort': sort, 'folderId': ?folderId, if (keyword != null && keyword.isNotEmpty) 'keyword': keyword, if (tag != null && tag.isNotEmpty) 'tag': tag, }); final data = res.data; if (data is Map) return NotePage.fromJson(data); return NotePage.empty; } on DioException catch (e) { final err = e.error; throw err is ApiException ? err : ApiException(-1, '笔记列表加载失败'); } } /// 目录树 Future folderTree() async { try { final res = await _dio.get('/notes/folders'); final data = res.data; if (data is Map) return FolderTree.fromJson(data); return FolderTree.empty; } on DioException catch (e) { final err = e.error; throw err is ApiException ? err : ApiException(-1, '目录加载失败'); } } /// 把笔记移动到某个目录,[folderId] 为 null 表示移到未分类 Future assignFolder(int noteId, int? folderId) async { try { await _dio.put('/notes/$noteId/folder', data: {'folderId': folderId}); } on DioException catch (e) { final err = e.error; throw err is ApiException ? err : ApiException(-1, '移动失败'); } } /// 详情(含正文) Future detail(int id) async { try { final res = await _dio.get('/notes/$id'); return NoteDetail.fromJson(_asMap(res.data)); } on DioException catch (e) { final err = e.error; throw err is ApiException ? err : ApiException(-1, '笔记加载失败'); } } /// 新建 Future create({ required String title, required String content, required String tags, }) async { try { final res = await _dio.post('/notes', data: { 'title': title, 'content': content, 'tags': tags, 'isPinned': 0, }); return NoteDetail.fromJson(_asMap(res.data)); } on DioException catch (e) { final err = e.error; throw err is ApiException ? err : ApiException(-1, '笔记保存失败'); } } /// 更新 /// /// 后端是部分更新语义:字段传 null 表示不改动, /// 所以这里只塞需要改的字段,不必回传全部。 Future update( int id, { String? title, String? content, String? tags, bool? pinned, }) async { try { final res = await _dio.put('/notes/$id', data: { // `?value` 是 Dart 的空感知元素:值为 null 时整个键值对不写入 map, // 正好对应后端"字段为 null 表示不改动"的部分更新语义 'title': ?title, 'content': ?content, 'tags': ?tags, if (pinned != null) 'isPinned': pinned ? 1 : 0, }); return NoteDetail.fromJson(_asMap(res.data)); } on DioException catch (e) { final err = e.error; throw err is ApiException ? err : ApiException(-1, '笔记保存失败'); } } /// 删除(后端是逻辑删除) Future delete(int id) async { try { await _dio.delete('/notes/$id'); } on DioException catch (e) { final err = e.error; throw err is ApiException ? err : ApiException(-1, '笔记删除失败'); } } /// 标签列表,按使用次数排序 Future> tags() async { try { final res = await _dio.get('/notes/tags'); return asStringListFrom(res.data); } on DioException catch (e) { final err = e.error; throw err is ApiException ? err : ApiException(-1, '标签加载失败'); } } Map _asMap(Object? data) { if (data is Map) return data; if (data is Map && data['data'] is Map) { return Map.from(data['data'] as Map); } return const {}; } } /// 标签接口返回的是裸数组,单独处理一下 List asStringListFrom(Object? data) { if (data is List) { return data.map((e) => e?.toString() ?? '').where((e) => e.isNotEmpty).toList(); } if (data is Map && data['data'] is List) { return (data['data'] as List) .map((e) => e?.toString() ?? '') .where((e) => e.isNotEmpty) .toList(); } return const []; }