feat: 实现导航页基础功能

- 添加导航条目和分类数据模型
- 实现导航卡片网格展示
- 支持点击卡片打开链接
- 添加主题样式(渐变色、卡片阴影)
- 区分系统默认和自定义条目
- 添加分类统计信息"
This commit is contained in:
2026-09-19 23:17:36 +08:00
parent 1c98340fc5
commit 039ea70edf
26 changed files with 796 additions and 2583 deletions
@@ -10,13 +10,9 @@ import '../../data/home_repository.dart';
import '../../domain/models/home_data.dart';
import '../../domain/models/link_item.dart';
import '../../domain/models/link_section.dart';
import '../../domain/models/nav_node.dart';
import '../../domain/models/submit_site_draft.dart';
import '../widgets/back_to_top_button.dart';
import '../widgets/confirm_dialog.dart';
import '../widgets/link_section_view.dart';
import '../widgets/sidebar_nav.dart';
import '../widgets/site_form_dialog.dart';
import '../widgets/top_bar.dart';
/// 首页:可折叠侧栏 + 顶部控制栏 + 分组卡片网格。
@@ -153,100 +149,14 @@ class _HomePageState extends State<HomePage> {
void _openItem(LinkItem item) => widget.urlOpener.open(item.url);
/// 打开「提交网站」表单;提交成功后静默刷新一次,让新站点立刻出现在侧栏里。
Future<void> _openSubmitDialog(HomeData data) async {
final bool saved = await showSiteFormDialog(
context: context,
navNodes: data.navNodes,
heading: '提交网站',
onSubmit: (SubmitSiteDraft draft) => widget.repository.submitSite(draft),
);
if (!saved || !mounted) {
return;
}
_toast('已提交,感谢');
await _load(showLoading: false);
}
/// 编辑一个用户提交的站点:表单预填现有内容,保存走 `PATCH /api/sites/{id}`。
Future<void> _openEditDialog(HomeData data, LinkItem item) async {
final ({String group, String subGroup})? path = _groupPathOf(
data.navNodes,
item.categoryId,
);
final bool saved = await showSiteFormDialog(
context: context,
navNodes: data.navNodes,
heading: '编辑网站',
initial: SubmitSiteDraft(
title: item.title,
url: item.url,
description: item.description,
group: path?.group ?? '',
subGroup: path?.subGroup ?? '',
),
onSubmit: (SubmitSiteDraft draft) =>
widget.repository.updateSite(item.id, draft),
);
if (!saved || !mounted) {
return;
}
_toast('已保存');
await _load(showLoading: false);
}
/// 删除一个用户提交的站点:先确认,再走 `DELETE /api/sites/{id}`。
Future<void> _confirmDelete(LinkItem item) async {
final bool confirmed = await showConfirmDialog(
context: context,
title: '删除网站',
message: '确定要删除「${item.title}」吗?删掉之后不能恢复。',
confirmLabel: '删除',
danger: true,
);
if (!confirmed || !mounted) {
return;
}
try {
await widget.repository.deleteSite(item.id);
} catch (error) {
if (mounted) {
_toast('删除失败:$error');
}
return;
}
if (!mounted) {
return;
}
_toast('已删除');
await _load(showLoading: false);
}
void _toast(String message) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
}
/// 从侧栏树里反查某个分组 id 的「一级分组 + 子分组」名字。
/// 「提交网站」跳到后端配好的收集入口(Mattermost 之类)。
///
/// 后端只按分组名收更新,所以编辑时必须把 id 还原成名字;
/// 层级最多两级,这里只查两轮。
({String group, String subGroup})? _groupPathOf(
List<NavNode> nodes,
String categoryId,
) {
for (final NavNode root in nodes) {
if (root.id == categoryId) {
return (group: root.title, subGroup: '');
}
for (final NavNode child in root.children) {
if (child.id == categoryId) {
return (group: root.title, subGroup: child.title);
}
}
/// 地址在 `data/nav.json` 的 `site.submitUrl`;没配就什么都不做。
void _openSubmitUrl(HomeData data) {
if (data.submitUrl.isEmpty) {
return;
}
return null;
widget.urlOpener.open(data.submitUrl);
}
void _backToTop() {
@@ -293,7 +203,7 @@ class _HomePageState extends State<HomePage> {
openGroupIds: _openGroupIds,
onToggleGroup: _toggleGroup,
onSectionSelected: _goToSection,
onSubmitSite: () => unawaited(_openSubmitDialog(data)),
onSubmitSite: () => _openSubmitUrl(data),
onToggleCollapse: _toggleSidebar,
),
Expanded(child: _buildMain(data)),
@@ -334,9 +244,6 @@ class _HomePageState extends State<HomePage> {
key: _sectionKeys[section.id],
section: section,
onOpen: _openItem,
onEdit: (LinkItem item) =>
unawaited(_openEditDialog(data, item)),
onDelete: (LinkItem item) => unawaited(_confirmDelete(item)),
),
),
],