feat: 闲言APP v0.9.1 — 编辑器全面重写 + 迷你编辑器
- 标准编辑器: freezed数据模型 + 5Tab工具栏 + 多图层管理 + 文字增强 + 背景系统 + 导出链路 - 迷你编辑器: 极简6项功能(文字/字号/颜色/背景/预览/导出) + 三种调用方式(全屏/半屏/内嵌) - 共享组件: GlassSlider/ColorPicker/FontPicker/TipsView - 服务层: ExportService/ImageImportService/FontService/XycardService - 设计系统: 统一主题令牌 + Liquid Glass风格
This commit is contained in:
676
lib/core/registry/page_registry.dart
Normal file
676
lib/core/registry/page_registry.dart
Normal file
@@ -0,0 +1,676 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 页面注册表
|
||||
/// 创建时间: 2026-04-20
|
||||
/// 更新时间: 2026-04-20
|
||||
/// 作用: 所有页面统一注册 + 合法性检测 + 主题令牌 + 描述
|
||||
/// 上次更新: 初始创建 — 全页面注册+设计令牌映射+合法性校验
|
||||
/// ============================================================
|
||||
|
||||
import '../theme/app_spacing.dart';
|
||||
import '../theme/app_radius.dart';
|
||||
import '../router/app_router.dart';
|
||||
|
||||
// ============================================================
|
||||
// 页面注册条目
|
||||
// ============================================================
|
||||
|
||||
/// 页面注册条目模型
|
||||
class PageRegistryEntry {
|
||||
const PageRegistryEntry({
|
||||
required this.route,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.category,
|
||||
this.isTab = false,
|
||||
this.isFullscreen = false,
|
||||
this.designTokens = const [],
|
||||
this.status = PageStatus.active,
|
||||
});
|
||||
|
||||
/// 路由路径
|
||||
final String route;
|
||||
|
||||
/// 页面名称
|
||||
final String name;
|
||||
|
||||
/// 页面描述
|
||||
final String description;
|
||||
|
||||
/// 页面分类
|
||||
final PageCategory category;
|
||||
|
||||
/// 是否为 Tab 页面
|
||||
final bool isTab;
|
||||
|
||||
/// 是否全屏页面
|
||||
final bool isFullscreen;
|
||||
|
||||
/// 使用的设计令牌列表
|
||||
final List<DesignTokenRef> designTokens;
|
||||
|
||||
/// 页面状态
|
||||
final PageStatus status;
|
||||
}
|
||||
|
||||
/// 页面分类
|
||||
enum PageCategory {
|
||||
core('核心', '🏠'),
|
||||
editor('编辑器', '🎨'),
|
||||
discovery('发现', '🔍'),
|
||||
settings('设置', '⚙️'),
|
||||
membership('会员', '👑');
|
||||
|
||||
const PageCategory(this.label, this.emoji);
|
||||
|
||||
final String label;
|
||||
final String emoji;
|
||||
}
|
||||
|
||||
/// 页面状态
|
||||
enum PageStatus {
|
||||
active('已上线'),
|
||||
wip('开发中'),
|
||||
planned('规划中'),
|
||||
deprecated('已废弃');
|
||||
|
||||
const PageStatus(this.label);
|
||||
|
||||
final String label;
|
||||
}
|
||||
|
||||
/// 设计令牌引用
|
||||
class DesignTokenRef {
|
||||
const DesignTokenRef({
|
||||
required this.name,
|
||||
required this.type,
|
||||
required this.value,
|
||||
this.description,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final DesignTokenType type;
|
||||
final String value;
|
||||
final String? description;
|
||||
}
|
||||
|
||||
/// 设计令牌类型
|
||||
enum DesignTokenType { color, spacing, radius, typography, shadow, glass }
|
||||
|
||||
// ============================================================
|
||||
// 设计令牌参考表
|
||||
// ============================================================
|
||||
|
||||
/// 全局设计令牌参考
|
||||
class DesignTokenRegistry {
|
||||
DesignTokenRegistry._();
|
||||
|
||||
// ---- 颜色令牌 ----
|
||||
static const colorTokens = [
|
||||
DesignTokenRef(
|
||||
name: 'primary',
|
||||
type: DesignTokenType.color,
|
||||
value: '#6C63FF',
|
||||
description: '主色-薰衣草紫',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'secondary',
|
||||
type: DesignTokenType.color,
|
||||
value: '#FF6B6B',
|
||||
description: '辅助色-珊瑚红',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'accent',
|
||||
type: DesignTokenType.color,
|
||||
value: '#4ECDC4',
|
||||
description: '强调色-薄荷绿',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'success',
|
||||
type: DesignTokenType.color,
|
||||
value: '#10B981',
|
||||
description: '成功色',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'warning',
|
||||
type: DesignTokenType.color,
|
||||
value: '#F59E0B',
|
||||
description: '警告色',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'error',
|
||||
type: DesignTokenType.color,
|
||||
value: '#EF4444',
|
||||
description: '错误色',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'bgPrimary-light',
|
||||
type: DesignTokenType.color,
|
||||
value: '#FAFAFA',
|
||||
description: '日间主背景',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'bgPrimary-dark',
|
||||
type: DesignTokenType.color,
|
||||
value: '#1A1A2E',
|
||||
description: '夜间主背景',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'textPrimary-light',
|
||||
type: DesignTokenType.color,
|
||||
value: '#1A1A2E',
|
||||
description: '日间主文字',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'textPrimary-dark',
|
||||
type: DesignTokenType.color,
|
||||
value: '#E5E5E5',
|
||||
description: '夜间主文字',
|
||||
),
|
||||
];
|
||||
|
||||
// ---- 间距令牌 ----
|
||||
static const spacingTokens = [
|
||||
DesignTokenRef(
|
||||
name: 'xs',
|
||||
type: DesignTokenType.spacing,
|
||||
value: '${AppSpacing.xs}px',
|
||||
description: '紧凑间距',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'sm',
|
||||
type: DesignTokenType.spacing,
|
||||
value: '${AppSpacing.sm}px',
|
||||
description: '元素内间距',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'md',
|
||||
type: DesignTokenType.spacing,
|
||||
value: '${AppSpacing.md}px',
|
||||
description: '标准间距',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'lg',
|
||||
type: DesignTokenType.spacing,
|
||||
value: '${AppSpacing.lg}px',
|
||||
description: '区块间距',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'xl',
|
||||
type: DesignTokenType.spacing,
|
||||
value: '${AppSpacing.xl}px',
|
||||
description: '大区块间距',
|
||||
),
|
||||
];
|
||||
|
||||
// ---- 圆角令牌 ----
|
||||
static const radiusTokens = [
|
||||
DesignTokenRef(
|
||||
name: 'sm',
|
||||
type: DesignTokenType.radius,
|
||||
value: '${AppRadius.sm}px',
|
||||
description: '小按钮/标签',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'md',
|
||||
type: DesignTokenType.radius,
|
||||
value: '${AppRadius.md}px',
|
||||
description: '卡片/弹窗',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'lg',
|
||||
type: DesignTokenType.radius,
|
||||
value: '${AppRadius.lg}px',
|
||||
description: '大卡片/面板',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'xl',
|
||||
type: DesignTokenType.radius,
|
||||
value: '${AppRadius.xl}px',
|
||||
description: '底部弹窗',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'full',
|
||||
type: DesignTokenType.radius,
|
||||
value: '999px',
|
||||
description: '胶囊/圆形',
|
||||
),
|
||||
];
|
||||
|
||||
// ---- 字体令牌 ----
|
||||
static const typographyTokens = [
|
||||
DesignTokenRef(
|
||||
name: 'display',
|
||||
type: DesignTokenType.typography,
|
||||
value: '34px/Bold',
|
||||
description: '超大标题',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'title1',
|
||||
type: DesignTokenType.typography,
|
||||
value: '28px/Bold',
|
||||
description: '大标题',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'title2',
|
||||
type: DesignTokenType.typography,
|
||||
value: '22px/Bold',
|
||||
description: '中标题',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'title3',
|
||||
type: DesignTokenType.typography,
|
||||
value: '20px/SemiBold',
|
||||
description: '小标题',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'headline',
|
||||
type: DesignTokenType.typography,
|
||||
value: '17px/SemiBold',
|
||||
description: '头条',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'body',
|
||||
type: DesignTokenType.typography,
|
||||
value: '17px/Regular',
|
||||
description: '正文',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'callout',
|
||||
type: DesignTokenType.typography,
|
||||
value: '16px/Medium',
|
||||
description: '标注',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'subhead',
|
||||
type: DesignTokenType.typography,
|
||||
value: '15px/Regular',
|
||||
description: '副标题',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'footnote',
|
||||
type: DesignTokenType.typography,
|
||||
value: '13px/Regular',
|
||||
description: '脚注',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'caption1',
|
||||
type: DesignTokenType.typography,
|
||||
value: '12px/Regular',
|
||||
description: '说明文字',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'caption2',
|
||||
type: DesignTokenType.typography,
|
||||
value: '11px/Regular',
|
||||
description: '小说明文字',
|
||||
),
|
||||
];
|
||||
|
||||
// ---- 毛玻璃令牌 ----
|
||||
static const glassTokens = [
|
||||
DesignTokenRef(
|
||||
name: 'glassColorLight',
|
||||
type: DesignTokenType.glass,
|
||||
value: '#FFFFFF',
|
||||
description: '日间毛玻璃底色',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'glassColorDark',
|
||||
type: DesignTokenType.glass,
|
||||
value: '#2D2D44',
|
||||
description: '夜间毛玻璃底色',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'borderOpacity',
|
||||
type: DesignTokenType.glass,
|
||||
value: '0.3',
|
||||
description: '毛玻璃边框透明度',
|
||||
),
|
||||
];
|
||||
|
||||
/// 获取所有令牌
|
||||
static List<DesignTokenRef> get allTokens => [
|
||||
...colorTokens,
|
||||
...spacingTokens,
|
||||
...radiusTokens,
|
||||
...typographyTokens,
|
||||
...glassTokens,
|
||||
];
|
||||
|
||||
/// 按类型筛选令牌
|
||||
static List<DesignTokenRef> tokensByType(DesignTokenType type) {
|
||||
return allTokens.where((t) => t.type == type).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 页面注册表
|
||||
// ============================================================
|
||||
|
||||
/// 页面注册表 — 所有页面必须在此注册
|
||||
class PageRegistry {
|
||||
PageRegistry._();
|
||||
|
||||
/// 全部注册页面
|
||||
static const List<PageRegistryEntry> pages = [
|
||||
// ---- 核心 Tab 页面 ----
|
||||
PageRegistryEntry(
|
||||
route: AppRoutes.home,
|
||||
name: '首页',
|
||||
description: '句子阅读主页面,展示每日推荐 + 句子流 + Hitokoto API',
|
||||
category: PageCategory.core,
|
||||
isTab: true,
|
||||
designTokens: [
|
||||
DesignTokenRef(
|
||||
name: 'primary',
|
||||
type: DesignTokenType.color,
|
||||
value: '#6C63FF',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'md',
|
||||
type: DesignTokenType.spacing,
|
||||
value: '16px',
|
||||
),
|
||||
DesignTokenRef(name: 'lg', type: DesignTokenType.radius, value: '12px'),
|
||||
DesignTokenRef(
|
||||
name: 'title1',
|
||||
type: DesignTokenType.typography,
|
||||
value: '28px/Bold',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'body',
|
||||
type: DesignTokenType.typography,
|
||||
value: '17px/Regular',
|
||||
),
|
||||
],
|
||||
),
|
||||
PageRegistryEntry(
|
||||
route: AppRoutes.inspiration,
|
||||
name: '灵感',
|
||||
description: '灵感发现 + 分类浏览 + 热门标签 + 句子瀑布流',
|
||||
category: PageCategory.discovery,
|
||||
isTab: true,
|
||||
designTokens: [
|
||||
DesignTokenRef(
|
||||
name: 'accent',
|
||||
type: DesignTokenType.color,
|
||||
value: '#4ECDC4',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'md',
|
||||
type: DesignTokenType.spacing,
|
||||
value: '16px',
|
||||
),
|
||||
DesignTokenRef(name: 'md', type: DesignTokenType.radius, value: '8px'),
|
||||
DesignTokenRef(
|
||||
name: 'title3',
|
||||
type: DesignTokenType.typography,
|
||||
value: '20px/SemiBold',
|
||||
),
|
||||
],
|
||||
),
|
||||
PageRegistryEntry(
|
||||
route: AppRoutes.editor,
|
||||
name: '编辑器',
|
||||
description: '卡片/壁纸编辑器,包含画布预览+工具栏+底部面板',
|
||||
category: PageCategory.editor,
|
||||
isTab: true,
|
||||
designTokens: [
|
||||
DesignTokenRef(
|
||||
name: 'primary',
|
||||
type: DesignTokenType.color,
|
||||
value: '#6C63FF',
|
||||
),
|
||||
DesignTokenRef(name: 'sm', type: DesignTokenType.spacing, value: '8px'),
|
||||
DesignTokenRef(name: 'md', type: DesignTokenType.radius, value: '8px'),
|
||||
DesignTokenRef(
|
||||
name: 'glassColorLight',
|
||||
type: DesignTokenType.glass,
|
||||
value: '#FFFFFF',
|
||||
),
|
||||
],
|
||||
),
|
||||
PageRegistryEntry(
|
||||
route: AppRoutes.profile,
|
||||
name: '个人中心',
|
||||
description: '用户个人设置 + 主题切换 + 收藏/历史 + 关于',
|
||||
category: PageCategory.settings,
|
||||
isTab: true,
|
||||
designTokens: [
|
||||
DesignTokenRef(
|
||||
name: 'primary',
|
||||
type: DesignTokenType.color,
|
||||
value: '#6C63FF',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'md',
|
||||
type: DesignTokenType.spacing,
|
||||
value: '16px',
|
||||
),
|
||||
DesignTokenRef(name: 'lg', type: DesignTokenType.radius, value: '12px'),
|
||||
],
|
||||
),
|
||||
|
||||
// ---- 全屏页面 ----
|
||||
PageRegistryEntry(
|
||||
route: AppRoutes.search,
|
||||
name: '搜索',
|
||||
description: '句子搜索 + 热搜词 + 搜索历史 + 结果展示',
|
||||
category: PageCategory.discovery,
|
||||
isFullscreen: true,
|
||||
designTokens: [
|
||||
DesignTokenRef(
|
||||
name: 'primary',
|
||||
type: DesignTokenType.color,
|
||||
value: '#6C63FF',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'md',
|
||||
type: DesignTokenType.spacing,
|
||||
value: '16px',
|
||||
),
|
||||
DesignTokenRef(name: 'md', type: DesignTokenType.radius, value: '8px'),
|
||||
],
|
||||
),
|
||||
PageRegistryEntry(
|
||||
route: AppRoutes.source,
|
||||
name: '句子来源',
|
||||
description: '句子来源详情 — 书籍/影视/人物/动漫/歌词分类浏览',
|
||||
category: PageCategory.discovery,
|
||||
isFullscreen: true,
|
||||
designTokens: [
|
||||
DesignTokenRef(
|
||||
name: 'accent',
|
||||
type: DesignTokenType.color,
|
||||
value: '#4ECDC4',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'md',
|
||||
type: DesignTokenType.spacing,
|
||||
value: '16px',
|
||||
),
|
||||
DesignTokenRef(name: 'lg', type: DesignTokenType.radius, value: '12px'),
|
||||
],
|
||||
),
|
||||
PageRegistryEntry(
|
||||
route: AppRoutes.member,
|
||||
name: '会员中心',
|
||||
description: '会员权益展示 + 订阅管理 + FAQ',
|
||||
category: PageCategory.membership,
|
||||
isFullscreen: true,
|
||||
designTokens: [
|
||||
DesignTokenRef(
|
||||
name: 'warning',
|
||||
type: DesignTokenType.color,
|
||||
value: '#F59E0B',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'md',
|
||||
type: DesignTokenType.spacing,
|
||||
value: '16px',
|
||||
),
|
||||
DesignTokenRef(name: 'xl', type: DesignTokenType.radius, value: '16px'),
|
||||
],
|
||||
),
|
||||
PageRegistryEntry(
|
||||
route: '/settings/theme',
|
||||
name: '主题个性化',
|
||||
description: '主题模式+强调色+字体大小+毛玻璃强度管理',
|
||||
category: PageCategory.settings,
|
||||
isFullscreen: true,
|
||||
designTokens: [
|
||||
DesignTokenRef(
|
||||
name: 'primary',
|
||||
type: DesignTokenType.color,
|
||||
value: '#6C63FF',
|
||||
),
|
||||
DesignTokenRef(
|
||||
name: 'md',
|
||||
type: DesignTokenType.spacing,
|
||||
value: '16px',
|
||||
),
|
||||
DesignTokenRef(name: 'md', type: DesignTokenType.radius, value: '8px'),
|
||||
DesignTokenRef(
|
||||
name: 'glassColorLight',
|
||||
type: DesignTokenType.glass,
|
||||
value: '#FFFFFF',
|
||||
),
|
||||
],
|
||||
),
|
||||
PageRegistryEntry(
|
||||
route: AppRoutes.editor,
|
||||
name: '编辑器',
|
||||
description: '从首页/其他页面进入的全屏编辑器',
|
||||
category: PageCategory.editor,
|
||||
isFullscreen: true,
|
||||
designTokens: [
|
||||
DesignTokenRef(
|
||||
name: 'primary',
|
||||
type: DesignTokenType.color,
|
||||
value: '#6C63FF',
|
||||
),
|
||||
DesignTokenRef(name: 'sm', type: DesignTokenType.spacing, value: '8px'),
|
||||
DesignTokenRef(name: 'md', type: DesignTokenType.radius, value: '8px'),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
/// 按路由查找页面
|
||||
static PageRegistryEntry? findByRoute(String route) {
|
||||
for (final entry in pages) {
|
||||
if (entry.route == route) return entry;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 按分类筛选
|
||||
static List<PageRegistryEntry> byCategory(PageCategory category) {
|
||||
return pages.where((p) => p.category == category).toList();
|
||||
}
|
||||
|
||||
/// 获取 Tab 页面
|
||||
static List<PageRegistryEntry> get tabPages =>
|
||||
pages.where((p) => p.isTab).toList();
|
||||
|
||||
/// 获取全屏页面
|
||||
static List<PageRegistryEntry> get fullscreenPages =>
|
||||
pages.where((p) => p.isFullscreen).toList();
|
||||
|
||||
/// 检测路由是否合法
|
||||
static bool isRouteRegistered(String route) {
|
||||
return pages.any((p) => p.route == route);
|
||||
}
|
||||
|
||||
/// 检测所有页面是否都使用了设计令牌
|
||||
static List<String> validateDesignTokens() {
|
||||
final warnings = <String>[];
|
||||
for (final page in pages) {
|
||||
if (page.designTokens.isEmpty) {
|
||||
warnings.add('${page.name}(${page.route}) 未声明使用的设计令牌');
|
||||
}
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
/// 强制验证 — 路由表与注册表一致性
|
||||
///
|
||||
/// 检查所有 AppRoutes 中定义的路由是否都在注册表中注册。
|
||||
static List<String> validateRouteConsistency() {
|
||||
final errors = <String>[];
|
||||
final registeredRoutes = pages.map((p) => p.route).toSet();
|
||||
final appRoutes = [
|
||||
AppRoutes.home,
|
||||
AppRoutes.inspiration,
|
||||
AppRoutes.editor,
|
||||
AppRoutes.profile,
|
||||
AppRoutes.search,
|
||||
AppRoutes.source,
|
||||
AppRoutes.member,
|
||||
AppRoutes.themeSettings,
|
||||
];
|
||||
|
||||
for (final route in appRoutes) {
|
||||
if (!registeredRoutes.contains(route)) {
|
||||
errors.add('路由 $route 在 AppRoutes 中定义但未在 PageRegistry 中注册');
|
||||
}
|
||||
}
|
||||
|
||||
for (final route in registeredRoutes) {
|
||||
if (!appRoutes.contains(route)) {
|
||||
errors.add('路由 $route 在 PageRegistry 中注册但未在 AppRoutes 中定义');
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/// 强制验证 — 页面设计规范
|
||||
///
|
||||
/// 检查每个页面是否使用了正确的设计令牌类型。
|
||||
static List<String> validateDesignCompliance() {
|
||||
final errors = <String>[];
|
||||
for (final page in pages) {
|
||||
final hasColor = page.designTokens.any(
|
||||
(t) => t.type == DesignTokenType.color,
|
||||
);
|
||||
final hasSpacing = page.designTokens.any(
|
||||
(t) => t.type == DesignTokenType.spacing,
|
||||
);
|
||||
final hasRadius = page.designTokens.any(
|
||||
(t) => t.type == DesignTokenType.radius,
|
||||
);
|
||||
|
||||
if (!hasColor) {
|
||||
errors.add('${page.name}(${page.route}) 缺少颜色令牌声明');
|
||||
}
|
||||
if (!hasSpacing) {
|
||||
errors.add('${page.name}(${page.route}) 缺少间距令牌声明');
|
||||
}
|
||||
if (!hasRadius) {
|
||||
errors.add('${page.name}(${page.route}) 缺少圆角令牌声明');
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
/// 执行全部验证
|
||||
static List<String> validateAll() {
|
||||
return [
|
||||
...validateDesignTokens(),
|
||||
...validateRouteConsistency(),
|
||||
...validateDesignCompliance(),
|
||||
];
|
||||
}
|
||||
|
||||
/// 获取注册页面总数
|
||||
static int get pageCount => pages.length;
|
||||
|
||||
/// 获取各分类页面数量
|
||||
static Map<PageCategory, int> get categoryCounts {
|
||||
final counts = <PageCategory, int>{};
|
||||
for (final page in pages) {
|
||||
counts[page.category] = (counts[page.category] ?? 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user