新增笔记功能 图标更新

This commit is contained in:
2026-09-23 14:20:23 +08:00
parent a6e02ccbf3
commit e4cdfbc42f
34 changed files with 2511 additions and 0 deletions
+171
View File
@@ -0,0 +1,171 @@
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 [];
}