172 lines
5.0 KiB
Dart
172 lines
5.0 KiB
Dart
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<NoteApi>((ref) => NoteApi(ref.watch(dioProvider)));
|
||
|
||
/// 笔记接口
|
||
class NoteApi {
|
||
final Dio _dio;
|
||
|
||
NoteApi(this._dio);
|
||
|
||
/// 分页列表(不含正文)
|
||
///
|
||
/// [scope] all / uncategorized / folder;[folderId] 仅在 scope=folder 时有意义,
|
||
/// 后端会连同该目录的所有子目录一起查。
|
||
Future<NotePage> 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<String, dynamic>) return NotePage.fromJson(data);
|
||
return NotePage.empty;
|
||
} on DioException catch (e) {
|
||
final err = e.error;
|
||
throw err is ApiException ? err : ApiException(-1, '笔记列表加载失败');
|
||
}
|
||
}
|
||
|
||
/// 目录树
|
||
Future<FolderTree> folderTree() async {
|
||
try {
|
||
final res = await _dio.get('/notes/folders');
|
||
final data = res.data;
|
||
if (data is Map<String, dynamic>) 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<void> 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<NoteDetail> 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<NoteDetail> 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<NoteDetail> 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<void> 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<List<String>> 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<String, dynamic> _asMap(Object? data) {
|
||
if (data is Map<String, dynamic>) return data;
|
||
if (data is Map && data['data'] is Map) {
|
||
return Map<String, dynamic>.from(data['data'] as Map);
|
||
}
|
||
return const {};
|
||
}
|
||
}
|
||
|
||
/// 标签接口返回的是裸数组,单独处理一下
|
||
List<String> 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 [];
|
||
}
|