新增笔记功能 图标更新

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
+21
View File
@@ -11,6 +11,18 @@ int asInt(Object? value, {int fallback = 0}) {
return fallback;
}
/// 取可空整数:null / 无法解析都返回 null
///
/// 用于 folderId 这类"没有值"本身有业务含义的字段
/// (null 表示未分类,不能退化成 0)
int? asNullableInt(Object? value) {
if (value == null) return null;
if (value is int) return value;
if (value is num) return value.toInt();
if (value is String) return int.tryParse(value);
return null;
}
double asDouble(Object? value, {double fallback = 0}) {
if (value is double) return value;
if (value is num) return value.toDouble();
@@ -34,3 +46,12 @@ List<Map<String, dynamic>> asMapList(Object? value) {
if (value is! List) return const [];
return value.whereType<Map<String, dynamic>>().toList();
}
/// 取字符串数组(用于标签这类场景),非字符串元素丢弃,空串过滤掉
List<String> asStringList(Object? value) {
if (value is! List) return const [];
return value
.map((e) => e?.toString() ?? '')
.where((e) => e.isNotEmpty)
.toList();
}
+200
View File
@@ -0,0 +1,200 @@
import 'json_util.dart';
/// 笔记列表项(不含正文)
///
/// 后端列表接口刻意不返回 content —— 一页 20 条正文可能有几 MB,
/// 而这个后端在公网服务器上,列表页只需要摘要。
class NoteListItem {
final int id;
final int? folderId;
final String title;
final String summary;
final List<String> tags;
final bool pinned;
final String updateTime;
const NoteListItem({
required this.id,
required this.folderId,
required this.title,
required this.summary,
required this.tags,
required this.pinned,
required this.updateTime,
});
factory NoteListItem.fromJson(Map<String, dynamic> json) {
return NoteListItem(
id: asInt(json['id']),
folderId: asNullableInt(json['folderId']),
title: asNonEmpty(json['title'], '无标题笔记'),
summary: asString(json['summary']),
tags: asStringList(json['tagList']),
pinned: asInt(json['isPinned']) == 1,
updateTime: formatDateTime(json['updateTime']),
);
}
}
/// 笔记详情(含正文)
class NoteDetail {
final int id;
final int? folderId;
final String title;
final String content;
final List<String> tags;
final bool pinned;
final String createTime;
final String updateTime;
const NoteDetail({
required this.id,
required this.folderId,
required this.title,
required this.content,
required this.tags,
required this.pinned,
required this.createTime,
required this.updateTime,
});
factory NoteDetail.fromJson(Map<String, dynamic> json) {
return NoteDetail(
id: asInt(json['id']),
folderId: asNullableInt(json['folderId']),
title: asNonEmpty(json['title'], '无标题笔记'),
content: asString(json['content']),
tags: asStringList(json['tagList']),
pinned: asInt(json['isPinned']) == 1,
createTime: formatDateTime(json['createTime']),
updateTime: formatDateTime(json['updateTime']),
);
}
}
/// 分页结果
class NotePage {
final List<NoteListItem> items;
final int total;
final bool hasMore;
const NotePage({required this.items, required this.total, required this.hasMore});
static const empty = NotePage(items: [], total: 0, hasMore: false);
factory NotePage.fromJson(Map<String, dynamic> json) {
return NotePage(
items: asMapList(json['list']).map(NoteListItem.fromJson).toList(),
total: asInt(json['total']),
hasMore: json['hasMore'] == true,
);
}
}
/// 目录树节点
class FolderNode {
final int id;
final String name;
final int? parentId;
/// 该目录下的笔记数(**含所有子目录**),和后端列表接口的口径一致
final int noteCount;
final List<FolderNode> children;
const FolderNode({
required this.id,
required this.name,
required this.parentId,
required this.noteCount,
required this.children,
});
factory FolderNode.fromJson(Map<String, dynamic> json) {
return FolderNode(
id: asInt(json['id']),
name: asNonEmpty(json['name'], '未命名'),
parentId: asNullableInt(json['parentId']),
noteCount: asInt(json['noteCount']),
children: asMapList(json['children']).map(FolderNode.fromJson).toList(),
);
}
}
/// 目录树接口的完整返回
class FolderTree {
final List<FolderNode> folders;
final int uncategorizedCount;
final int totalCount;
const FolderTree({
required this.folders,
required this.uncategorizedCount,
required this.totalCount,
});
static const empty = FolderTree(folders: [], uncategorizedCount: 0, totalCount: 0);
factory FolderTree.fromJson(Map<String, dynamic> json) {
return FolderTree(
folders: asMapList(json['folders']).map(FolderNode.fromJson).toList(),
uncategorizedCount: asInt(json['uncategorizedCount']),
totalCount: asInt(json['totalCount']),
);
}
/// 按 id 找节点,用于拼当前目录的面包屑路径
List<FolderNode>? findPath(int id) {
List<FolderNode>? walk(List<FolderNode> nodes, List<FolderNode> trail) {
for (final node in nodes) {
final next = [...trail, node];
if (node.id == id) return next;
final found = walk(node.children, next);
if (found != null) return found;
}
return null;
}
return walk(folders, []);
}
}
/// 当前浏览的位置
///
/// 用三个 scope 对应目录树上的三种节点,而不是用 `folderId == null`
/// 来表达「全部」—— 那样就没法区分「全部」和「未分类」了。
enum NoteScope { all, uncategorized, folder }
class NoteLocation {
final NoteScope scope;
final int? folderId;
final String folderName;
const NoteLocation({
required this.scope,
this.folderId,
this.folderName = '全部',
});
static const all = NoteLocation(scope: NoteScope.all, folderName: '全部');
static const uncategorized =
NoteLocation(scope: NoteScope.uncategorized, folderName: '未分类');
/// 传给后端 list 接口的 scope 参数
String get apiScope {
switch (scope) {
case NoteScope.all:
return 'all';
case NoteScope.uncategorized:
return 'uncategorized';
case NoteScope.folder:
return 'folder';
}
}
}
/// 后端给的是 `yyyy-MM-dd HH:mm:ss`,这里裁成 `yyyy-MM-dd HH:mm`
/// 列表里显示到秒没有意义,还会把卡片撑宽。
String formatDateTime(Object? value) {
final text = asString(value);
return text.length >= 16 ? text.substring(0, 16) : text;
}