Files

172 lines
5.0 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 [];
}