From 79f7269319ddd6fecc52a7bdf38f4e3a9c62d5ff Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 1 Apr 2026 04:45:33 +0800 Subject: [PATCH] =?UTF-8?q?ui=E7=BB=86=E8=8A=82=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 321 +++--- lib/CHANGELOG.md | 341 +----- lib/config/app_config.dart | 6 +- .../{ => document}/API_DOCUMENTATION.md | 0 lib/services/{ => document}/API使用文档.md | 0 lib/services/{ => document}/stats.php | 0 lib/services/document/搜索api.md | 366 ++++++ lib/services/{ => document}/统计API文档.md | 86 +- lib/views/active/category_page.dart | 187 +-- lib/views/active/popular_page.dart | 383 +++--- lib/views/active/tags/corr_page.dart | 780 +++++++++++++ lib/views/discover_page.dart | 73 +- lib/views/favorites_page.dart | 2 +- lib/views/footprint/all_list.dart | 16 +- lib/views/profile/app-info.dart | 88 +- .../profile/components/entire -page.dart | 0 lib/views/profile/components/entire_page.dart | 1024 +++++++++++++++++ lib/views/profile/components/pop-menu.dart | 2 +- .../components/server_info_dialog.dart | 359 ++++++ lib/views/profile/profile_page.dart | 6 +- lib/views/profile/settings/offline-data.dart | 91 +- lib/widgets/tabbed_nav_app_bar.dart | 51 +- pubspec.yaml | 2 +- 23 files changed, 3299 insertions(+), 885 deletions(-) rename lib/services/{ => document}/API_DOCUMENTATION.md (100%) rename lib/services/{ => document}/API使用文档.md (100%) rename lib/services/{ => document}/stats.php (100%) create mode 100644 lib/services/document/搜索api.md rename lib/services/{ => document}/统计API文档.md (64%) create mode 100644 lib/views/active/tags/corr_page.dart delete mode 100644 lib/views/profile/components/entire -page.dart create mode 100644 lib/views/profile/components/entire_page.dart create mode 100644 lib/views/profile/components/server_info_dialog.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b25fc6..2d02805 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,134 @@ All notable changes to this project will be documented in this file. --- +## [1.3.15] - 2026-04-01 + +### 修改 +- 🎨 **修改收藏页面标题** + - 将收藏页面 AppBar 标题从"收藏"改为"足迹" + - 底部导航栏标签保持"收藏"不变 + - 涉及文件: + - `lib/views/favorites_page.dart` - 修改页面标题 + +--- + +## [1.3.14] - 2026-04-01 + +### 修复 +- 🐛 **彻底修复 AppBar 标题不显示问题** + - 移除 `TabbedNavAppBar` 中的自定义 `toolbarHeight`,使用 Flutter 默认值 + - 移除 TabBar 的自定义高度包装(`PreferredSize` + `SizedBox`),直接使用 `TabBar` 作为 `bottom` + - 简化 TabBar 指示器配置,移除不必要的 `insets` + - 明确设置标题颜色为 `Colors.black87` + - 涉及文件: + - `lib/widgets/tabbed_nav_app_bar.dart` - 彻底修复 AppBar 标题显示 + +--- + +## [1.3.13] - 2026-04-01 + +### 优化 +- 🎨 **优化收藏页面布局** + - 去掉收藏页面卡片之间的空白间距 + - 在卡片之间添加黑色半透明分割线 + - 减少列表顶部padding,让内容更紧凑 + - 移除卡片自身的底部margin,改用统一的分割线间距 + - 涉及文件: + - `lib/views/footprint/all_list.dart` - 优化收藏页面布局 + +--- + +## [1.3.12] - 2026-04-01 + +### 新增 +- ✨ **新增全站统计页面** + - 创建了iOS风格的全站统计页面,展示网站统计数据 + - 页面包含:收录数量、热度统计、热门内容、建站时间等 + - 支持下拉刷新,实时获取最新统计数据 + - 使用主题色设计,与整体应用风格统一 + - 添加了网络状态检测和错误处理 + - 涉及文件: + - `lib/views/profile/components/entire-page.dart` - 新建全站统计页面 + - `lib/views/profile/profile_page.dart` - 添加跳转到全站统计页面 + +### 修复 +- 🐛 **修复全站统计页面布局溢出问题** + - 调整了数量统计网格的宽高比,从1.1改为0.9,再优化为0.9 + - 优化了统计卡片的内部间距和字体大小 + - 减少了图标容器(36→32→28)和图标大小(20→18→16) + - 减少数值字体大小(18→16→14)和标签字体大小(11→10→9) + - 减少内部间距(12→10→8,8→6→4,4→2) + - 确保在不同屏幕尺寸下都能正常显示 + - 涉及文件: + - `lib/views/profile/components/entire-page.dart` - 修复布局溢出 + +### 优化 +- ✨ **更新全站统计页面字段标签** + - 根据API文档更新,调整了统计字段的显示标签 + - "已开设分类" → "项目" + - "已收录诗句" → "收录诗句" + - "审核中申请" → "审核中" + - "已拒绝申请" → "已拒绝" + - "文章数量" → "每日一句"(图标改为太阳) + - "已发布公告" → "推送" + - "开发者人数" → "开发者" + - 涉及文件: + - `lib/views/profile/components/entire-page.dart` - 更新字段标签 + +### 改进 +- 🎨 **重新设计数量统计布局为3x3网格** + - 保持3x3网格布局,从列表改回网格视图 + - 每个网格项内部改为上下两行布局 + - 上行(flex: 2):icon和数据水平排列,比例1:1 + - 有icon时:icon和数据各占一半 + - 无icon时(收录诗句、分类标签):数据自动占满整行 + - 下行(flex: 1):描述单独一行,居中显示 + - 上下行比例:2:1 + - 优化了网格项宽高比为1.0 + - 增大了图标(24)和数值字体(22),提升可读性 + - 涉及文件: + - `lib/views/profile/components/entire-page.dart` - 重新设计3x3网格布局 + +- ✨ **建站时间卡片增加天数显示** + - 自动计算从建站日期到现在的天数 + - 在建站时间右侧显示"已运行 X 天"标签 + - 使用主题色背景的圆角标签样式 + - 涉及文件: + - `lib/views/profile/components/entire_page.dart` - 增加天数计算和显示 + +- 🔧 **修复代码规范问题** + - 添加 `library;` 指令修复悬空库文档注释警告 + - 重命名文件 `entire-page.dart` → `entire_page.dart` 符合 Dart 命名规范 + - 涉及文件: + - `lib/views/profile/components/entire_page.dart` - 文件重命名 + - `lib/views/profile/profile_page.dart` - 更新 import 路径 + +- ⚡ **优化全站统计页面加载体验** + - 移除全局转圈加载动画,改为骨架屏预加载 + - 页面进入时立即显示骨架屏布局,提升用户体验 + - API 数据加载完成后平滑过渡到实际内容 + - 涉及文件: + - `lib/views/profile/components/entire_page.dart` - 异步加载优化 + +- ✨ **全站统计页面头部添加刷新按钮** + - 在"情景诗词"标题右侧添加刷新图标 + - 点击可重新加载统计数据 + - 涉及文件: + - `lib/views/profile/components/entire_page.dart` - 添加刷新按钮 + +- ✨ **新增服务器信息弹窗组件** + - 创建美化的服务器信息弹窗组件 `ServerInfoDialog` + - 全站统计页面 AppBar 右侧添加信息图标,点击显示服务器信息 + - 离线数据页面同步使用新的美化弹窗 + - 弹窗显示:服务器时间、负载、响应时间、网络延迟等 + - 响应时间自动判断状态(极快/快速/正常/较慢) + - 涉及文件: + - `lib/views/profile/components/server_info_dialog.dart` - 新建弹窗组件 + - `lib/views/profile/components/entire_page.dart` - 添加信息图标 + - `lib/views/profile/settings/offline-data.dart` - 使用新弹窗组件 + +--- + ## [1.3.11] - 2026-03-31 ### 修复 @@ -21,196 +149,3 @@ All notable changes to this project will be documented in this file. - 优化了屏幕常亮功能的错误处理 - 增强了平台检测和日志输出 - 为不支持屏幕常亮的设备添加了专门的错误提示 -- 🐛 **修复昵称修改不生效问题** - - 修复了点击确认图标后昵称没有保存的问题 - - 确保在编辑模式下点击确认图标时正确保存昵称到 `widget.userData['nickname']` - - 涉及文件: - - `lib/views/profile/per_card.dart` - 修复昵称保存逻辑 -- ⚙️ **更新重置设置默认值** - - 自动刷新:关闭 - - 调试信息:关闭 - - 预加载:开启 - - 隐藏次要按钮:关闭 - - 声音反馈:关闭 - - 震动反馈:开启 - - 全局Tips:开启 - - 涉及文件: - - `lib/views/profile/settings/app_fun.dart` - 更新重置设置逻辑 -- 🎨 **优化离线数据页面温馨提示卡片** - - 增加了字体大小,使关键信息更明显 - - 调整了间距,使布局更清晰 - - 优化了开启离线状态的方法说明,使其更易理解 - - 涉及文件: - - `lib/views/profile/settings/offline-data.dart` - 优化温馨提示卡片 -- 🎨 **优化屏幕常亮提示弹窗** - - 增加了圆角边框,提升视觉效果 - - 添加了图标和标题,使弹窗更美观 - - 优化了提示内容的布局和格式,使信息更清晰 - - 调整了按钮样式,增强视觉层次感 - - 涉及文件: - - `lib/views/profile/profile_page.dart` - 优化屏幕常亮提示弹窗 -- 🎨 **优化登录注册对话框** - - 在标题下方添加了副标题,说明微信号、手机号或邮箱都可以作为投票凭证 - - 涉及文件: - - `lib/views/profile/components/login_register_dialog.dart` - 添加副标题 -- 🎨 **优化个人卡片显示** - - 删除了等级显示,只保留 UEP 标识(如果用户加入了用户体验计划) - - 涉及文件: - - `lib/views/profile/per_card.dart` - 删除等级显示,优化 UEP 标识显示 - ---- - -## [1.3.9] - 2026-03-31 - -### 新增 -- 🆔 **UDID 设备信息显示** - - 在应用信息页面的设备信息列表中添加 UDID 显示 - - 使用 `flutter_udid` 库获取跨平台设备唯一标识 - - 支持点击复制 UDID 功能,带主题色 SnackBar 提示 - - 将 AppInfoPage 从 StatelessWidget 改为 StatefulWidget - - 添加 `_loadUdid()` 方法异步加载 UDID - - 在技术栈卡片中添加 flutter_udid 说明 - - 在开源协议对话框中添加 flutter_udid 条目 - - 涉及文件: - - `lib/views/profile/app-info.dart` - 添加 UDID 获取和显示 - -### 升级 -- 📦 **flutter_udid 库升级至 4.1.2** - - 升级本地 flutter_udid 包到最新版本 4.1.2 - - Android 平台:更新 Gradle 到 8.5.0,Kotlin 到 2.0.21,使用新的 FlutterPlugin API - - iOS 平台:替换 SAMKeychain 为 KeychainAccess,支持 iOS 13.0+ - - MacOS 平台:使用 KeychainAccess 和 IOKit 获取硬件 UUID - - HarmonyOS 平台:保持 3.0.0 版本,使用 OAID 获取设备标识 - - 解决 Gradle 依赖冲突(jcenter 废弃问题) - - 统一 JVM 目标版本为 17 - - 涉及文件: - - `packages/flutter_udid/pubspec.yaml` - 更新版本和平台配置 - - `packages/flutter_udid/android/build.gradle` - 更新 Gradle 和 Kotlin 版本 - - `packages/flutter_udid/android/src/main/kotlin/.../FlutterUdidPlugin.kt` - 更新插件 API - - `packages/flutter_udid/ios/flutter_udid.podspec` - 更新依赖和部署目标 - - `packages/flutter_udid/ios/Classes/SwiftFlutterUdidPlugin.swift` - 更新 KeychainAccess - - `packages/flutter_udid/macos/Classes/FlutterUdidPlugin.swift` - 更新实现 - -### 新增 -- 👥 **QQ 交流群卡片** - - 在了解我们页面的"官方网站"卡片下方添加 QQ 群卡片 - - 群号显示:271129018 - - 点击群号可复制到剪贴板 - - 蓝色主题色(Icons.group) - - 提示"💡 点击群号可复制" - - 涉及文件: - - `lib/views/profile/settings/learn-us.dart` - 添加 QQ 群卡片 - ---- - -## [1.3.8] - 2026-03-31 - -### 新增 -- 🎛️ **隐藏次要按钮功能** - - 在功能设置页面添加"隐藏次要按钮"开关 - - 开启后隐藏主页的"上一条"和"分享"悬浮按钮 - - 默认关闭,状态保存到 SharedPreferences - - 使用 `SecondaryButtonsManager` 单例管理状态 - - 实时响应开关状态变化,无需重启应用 - - 涉及文件: - - `lib/views/profile/settings/app_fun.dart` - 添加开关和状态管理 - - `lib/views/home/home-load.dart` - 添加 SecondaryButtonsManager 管理类 - - `lib/views/home/home_page.dart` - 使用 ValueListenableBuilder 监听状态变化 - ---- - -## [1.3.7] - 2026-03-31 - -### 新增 -- 📸 **诗词卡片截图分享功能** - - 在主页点赞按钮上方添加悬浮分享按钮,使用 📤 icon - - 点击按钮可将当前诗词卡片生成高清图片并分享 - - 使用 `RepaintBoundary` 和 `GlobalKey` 实现 Widget 截图 - - 集成 `share_plus` 库实现跨平台图片分享 - - 包含生成中、成功、失败的用户提示 - - 涉及文件: - - `lib/views/home/home_components.dart` - 添加 ShareImageUtils 工具类和 FloatingShareButton 组件 - - `lib/views/home/home_page.dart` - 添加截图 Key 和分享按钮布局 - - `lib/views/home/home_part.dart` - 添加 RepaintBoundary 支持截图 - ---- - -## [1.3.6] - 2026-03-31 - -### 优化 -- 🏷️ **文件重命名与统一** - - 将 `sqlite_storage_controller.dart` 重命名为 `shared_preferences_storage_controller.dart` - - 将类名 `SQLiteStorageController` 重命名为 `SharedPreferencesStorageController` - - 更新所有引用该文件的导入语句和类名调用 - - 涉及文件: - - `lib/main.dart` - - `lib/controllers/history_controller.dart` - - `lib/views/profile/profile_page.dart` - - `lib/views/profile/level/poetry.dart` - - `lib/views/profile/level/distinguish.dart` - - 保持功能不变,仅统一命名规范 - ---- - -## [1.3.5] - 2026-03-30 - -### 新增 -- 📜 **投稿记录功能** - - 新增投稿记录页面 `lib/views/profile/expand/tougao.dart` - - 显示历史投稿记录列表,按时间倒序排列 - - 支持展开查看详细信息(分类、诗人和标题、关键词、平台、介绍) - - 提供清空所有记录功能(带确认提示) - - 修改 `lib/views/profile/expand/manu-script.dart` - - 在AppBar添加历史记录图标按钮,点击跳转至记录页面 - - 投稿成功后自动保存记录到SharedPreferences - - 最多保存50条记录,超出时自动删除最早的记录 - ---- - -## 软件特性功能 - -### 已开发完成 -- 🎛️ **隐藏次要按钮功能** - 在功能设置页面添加开关、开启后隐藏主页的"上一条"和"分享"悬浮按钮、默认关闭、状态保存到SharedPreferences、使用SecondaryButtonsManager单例管理、实时响应开关状态变化无需重启 - - 优先级:3 -- 📸 **诗词卡片截图分享功能** - 主页点赞按钮上方添加悬浮分享按钮、点击生成诗词卡片高清图片并分享、使用RepaintBoundary和GlobalKey实现Widget截图、集成share_plus库实现跨平台分享、包含生成中/成功/失败提示 - - 优先级:4 -- 📜 **诗词投稿功能** - 新增投稿页面、支持诗词收录申请表单、包含参考语句/分类选择/诗人和标题/关键词/诗词介绍/人机验证、实现相似度检测防止重复提交、平台字段自动获取设备类型 - - 优先级:3 -- ⚡ **离线数据下载优化** - 添加取消下载功能、下载过程中显示取消按钮、优化下载状态显示避免页面卡死、取消下载时显示相应提示 - - 优先级:4 -- 🌐 **网络状态自动检测** - 个人卡片加载时自动检测网络状态、无网络时自动调整为离线状态、避免网络异常导致的错误 - - 优先级:3 -- 📊 **服务器信息显示** - 在离线数据页面添加服务器信息卡片、显示API地址/版本/频率限制等信息 - - 优先级:2 -- 📱 **离线数据下载功能增强** - 新增下载类型选择(诗句和答题)、诗句数量选项(20/30/60/100条)、答题数量选项(20/50/80/100条)、100条下载需加入用户体验计划、实现下载一条写入一条、取消下载时保存已下载数据、实时更新缓存状态、返回上一页继续后台下载、清空缓存时弹窗选择清空内容、缓存状态同时显示诗句和答题数量 - - 优先级:5 -- �� **已知bug列表功能** - 从下到上弹出页面显示已知bug、解决方法和解决时间、支持下拉刷新和滚动查看、显示bug优先级和状态、提供详细解决方案描述、显示影响用户范围和时间信息 -- 📜 **投稿记录功能** - 投稿记录页面显示历史投稿列表、按时间倒序排列、支持展开查看详细信息、提供清空记录功能、投稿成功后自动保存到SharedPreferences、最多保存50条记录 -- 🗳️ **投票功能完整实现** - 用户登录/注册、获取投票列表、投票详情、提交投票、投票结果展示、API服务基础URL修改、登录注册逻辑简化(只需用户名、默认密码123456、自动注册登录、设备标识)、投票页面调试功能、user_identifier增加Flutter后缀、setState调用安全修复(添加mounted检查)、投票登录状态持久化修复(添加Cookie管理器支持PHP Session认证) -- 🎨 **个人卡片标签栏布局优化** - 将标签栏区域一分为二,左侧区域可以点击展开/收起个人卡片 -- 🎲 **题目随机化功能** - 进入答题页面时调用 fetch 接口获取新题,使用 Fisher-Yates 算法打乱题目 ID 顺序 -- 💬 **答对答错反馈信息修复** - 当 API 返回的提示信息为空时,自行添加提示内容 -- 📚 **App 自行管理题目 ID** - 实现题目 ID 管理逻辑,不再随机生成 -- 🔧 **API 接口路径和参数最终修复** - 确认 API 路径,调整随机题目 ID 范围 -- 🔧 **API 接口路径和参数修复** - 修复 API 路径和参数,使用正确的新 API 接口 -- 🐛 **HttpResponse 处理修复** - 修复 HttpResponse 对象处理方式,使用正确的属性访问 -- 🔧 **API 请求参数更新** - 根据新的 API 文档更新所有请求参数 -- 📊 **个人页面统计数字动态化** - 从SharedPreferences读取真实的答题统计数据 -- 📝 **记录页显示问题修复** - 修复答题记录页面显示未知题目和标签的问题 -- 📋 **答题统计弹窗功能** - 显示详细的答题统计数据,支持复制发送给AI评估 -- 📝 **诗词答题功能** - 完整的答题页面,支持题目加载、答案提交、提示获取 -- 🕐 **中国十二时辰制时间显示** - 时间显示改为十二时辰(子丑寅卯辰巳午未申酉戌亥) -- 🌤️ **天气功能集成** - 集成天气API获取城市、天气状态和温度 -- 📚 **朝代信息显示** - 在诗人名称左侧添加朝代信息显示 -- 🎨 **卡片样式优化** - 经典和现代样式颜色统一,毛玻璃样式优化 -- 🐛 **卡片设置页面重复打开问题修复** - 使用 router.replaceUrl 替换当前页面,避免堆叠 -- 📐 **2x2卡片布局优化** - 移除天气按钮组件,保留城市名称显示 -- 🌐 **离线模式支持** - 新增 `OfflineDataManager` 类管理离线数据加载、离线状态时从本地缓存加载诗句、在线状态时从网络加载、支持循环加载本地缓存的诗句、离线模式下隐藏点赞按钮、无缓存时显示网络错误提示、优化数据加载逻辑 -- 🔄 **个人卡片在线/离线状态切换** - 在个人卡片tips卡片内添加在线状态开关、开关状态保存到SharedPreferences、关闭后切换为离线状态、支持点击tips卡片切换祝福语功能、优化开关布局、状态切换时显示气泡消息提示 - -### 开发进度 -- 🏗️ **HarmonyOS桌面小组件** - 开发中,包含2x2布局、天气显示、诗句展示等功能 - - 优先级:3 -getx 加入 -二维码能力 -HarmonyOS HongMeng Kernel \ No newline at end of file diff --git a/lib/CHANGELOG.md b/lib/CHANGELOG.md index 9cb54bc..cb800bf 100644 --- a/lib/CHANGELOG.md +++ b/lib/CHANGELOG.md @@ -1,298 +1,47 @@ -# 更新日志 + ## 软件特性功能 -## v1.12.0 - 2026-03-26 +### 已开发完成 +- 🎛️ **隐藏次要按钮功能** - 在功能设置页面添加开关、开启后隐藏主页的"上一条"和"分享"悬浮按钮、默认关闭、状态保存到SharedPreferences、使用SecondaryButtonsManager单例管理、实时响应开关状态变化无需重启 + - 优先级:3 +- 📸 **诗词卡片截图分享功能** - 主页点赞按钮上方添加悬浮分享按钮、点击生成诗词卡片高清图片并分享、使用RepaintBoundary和GlobalKey实现Widget截图、集成share_plus库实现跨平台分享、包含生成中/成功/失败提示 + - 优先级:4 +- 📜 **诗词投稿功能** - 新增投稿页面、支持诗词收录申请表单、包含参考语句/分类选择/诗人和标题/关键词/诗词介绍/人机验证、实现相似度检测防止重复提交、平台字段自动获取设备类型 + - 优先级:3 +- ⚡ **离线数据下载优化** - 添加取消下载功能、下载过程中显示取消按钮、优化下载状态显示避免页面卡死、取消下载时显示相应提示 + - 优先级:4 +- 🌐 **网络状态自动检测** - 个人卡片加载时自动检测网络状态、无网络时自动调整为离线状态、避免网络异常导致的错误 + - 优先级:3 +- 📊 **服务器信息显示** - 在离线数据页面添加服务器信息卡片、显示API地址/版本/频率限制等信息 + - 优先级:2 +- 📱 **离线数据下载功能增强** - 新增下载类型选择(诗句和答题)、诗句数量选项(20/30/60/100条)、答题数量选项(20/50/80/100条)、100条下载需加入用户体验计划、实现下载一条写入一条、取消下载时保存已下载数据、实时更新缓存状态、返回上一页继续后台下载、清空缓存时弹窗选择清空内容、缓存状态同时显示诗句和答题数量 + - 优先级:5 +- <20><> **已知bug列表功能** - 从下到上弹出页面显示已知bug、解决方法和解决时间、支持下拉刷新和滚动查看、显示bug优先级和状态、提供详细解决方案描述、显示影响用户范围和时间信息 +- 📜 **投稿记录功能** - 投稿记录页面显示历史投稿列表、按时间倒序排列、支持展开查看详细信息、提供清空记录功能、投稿成功后自动保存到SharedPreferences、最多保存50条记录 +- 🗳️ **投票功能完整实现** - 用户登录/注册、获取投票列表、投票详情、提交投票、投票结果展示、API服务基础URL修改、登录注册逻辑简化(只需用户名、默认密码123456、自动注册登录、设备标识)、投票页面调试功能、user_identifier增加Flutter后缀、setState调用安全修复(添加mounted检查)、投票登录状态持久化修复(添加Cookie管理器支持PHP Session认证) +- 🎨 **个人卡片标签栏布局优化** - 将标签栏区域一分为二,左侧区域可以点击展开/收起个人卡片 +- 🎲 **题目随机化功能** - 进入答题页面时调用 fetch 接口获取新题,使用 Fisher-Yates 算法打乱题目 ID 顺序 +- 💬 **答对答错反馈信息修复** - 当 API 返回的提示信息为空时,自行添加提示内容 +- 📚 **App 自行管理题目 ID** - 实现题目 ID 管理逻辑,不再随机生成 +- 🔧 **API 接口路径和参数最终修复** - 确认 API 路径,调整随机题目 ID 范围 +- 🔧 **API 接口路径和参数修复** - 修复 API 路径和参数,使用正确的新 API 接口 +- 🐛 **HttpResponse 处理修复** - 修复 HttpResponse 对象处理方式,使用正确的属性访问 +- 🔧 **API 请求参数更新** - 根据新的 API 文档更新所有请求参数 +- 📊 **个人页面统计数字动态化** - 从SharedPreferences读取真实的答题统计数据 +- 📝 **记录页显示问题修复** - 修复答题记录页面显示未知题目和标签的问题 +- 📋 **答题统计弹窗功能** - 显示详细的答题统计数据,支持复制发送给AI评估 +- 📝 **诗词答题功能** - 完整的答题页面,支持题目加载、答案提交、提示获取 +- 🕐 **中国十二时辰制时间显示** - 时间显示改为十二时辰(子丑寅卯辰巳午未申酉戌亥) +- 🌤️ **天气功能集成** - 集成天气API获取城市、天气状态和温度 +- 📚 **朝代信息显示** - 在诗人名称左侧添加朝代信息显示 +- 🎨 **卡片样式优化** - 经典和现代样式颜色统一,毛玻璃样式优化 +- 🐛 **卡片设置页面重复打开问题修复** - 使用 router.replaceUrl 替换当前页面,避免堆叠 +- 📐 **2x2卡片布局优化** - 移除天气按钮组件,保留城市名称显示 +- 🌐 **离线模式支持** - 新增 `OfflineDataManager` 类管理离线数据加载、离线状态时从本地缓存加载诗句、在线状态时从网络加载、支持循环加载本地缓存的诗句、离线模式下隐藏点赞按钮、无缓存时显示网络错误提示、优化数据加载逻辑 +- 🔄 **个人卡片在线/离线状态切换** - 在个人卡片tips卡片内添加在线状态开关、开关状态保存到SharedPreferences、关闭后切换为离线状态、支持点击tips卡片切换祝福语功能、优化开关布局、状态切换时显示气泡消息提示 -### 📄 新增隐私政策与用户协议页面 -- **新建隐私政策与用户协议页面** - - 创建 `privacy.dart` 页面,包含隐私政策和用户协议两个标签页 - - 采用 TabBar 设计,便于在隐私政策和用户协议之间切换 - - 关键字段使用 ***** 占位,便于后续修改 - - 包含完整的隐私政策内容:信息收集、设备权限、联系方式等 - - 包含完整的用户协议内容:协议范围、服务内容、知识产权等 - - 地址信息:云南昆明 xx工作室 - - 软件名称:情景诗词 - -### 🔗 在线版本功能 -- **AppBar 右侧添加在线版本按钮** - - 添加链接图标按钮,点击显示在线协议弹窗 - - 弹窗显示在线协议链接(占位符:https://*****.github.io/privacy) - - 支持一键复制链接到剪贴板 - - 复制成功后显示 SnackBar 提示 - -### 📝 HTML 版协议 -- **创建独立的 HTML 版本协议文件** - - 文件位置:`privacy.html` - - 响应式设计,支持手机、平板、桌面端 - - 采用现代苹果风格设计,与 App 风格统一 - - Tab 切换动画,流畅的用户体验 - - 可用于部署到 GitHub Pages 或其他静态网站托管 - -### 🔗 页面导航优化 -- **个人页面添加跳转入口** - - 在 profile_page.dart 中添加隐私政策页面导入 - - 修改"软件协议"设置项,点击跳转到隐私政策页面 - - 更新图标为 Icons.description,更符合页面功能 - -### 👥 新增"了解我们"页面 -- **新建了解我们页面** - - 创建 `learn-us.dart` 页面,展示开发者信息 - - 页面头部卡片:显示应用名称、版本号和标语 - - 官方网站卡片:展示官网链接(占位符:https://*****.github.io) - - 开发者卡片:显示"微风暴工作室"信息 - - 团队信息卡片:展示团队成员及个性签名 - - 程序设计:*** - - UI/UX/Testing:**** - - 后端:*** - - 技术支持:技术 - - 备案信息卡片:显示备案号"滇ICP备2022000863号-15A" - - 备案号支持点击复制,复制成功后显示 SnackBar 提示 - -### 🎨 界面设计 -- **统一苹果风格设计** - - 使用与项目一致的设计系统和颜色变量 - - 采用卡片式布局,圆角和阴影保持统一 - - 关键字使用加粗显示,提升可读性 - - 底部添加"到底了"指示器,与其他页面保持一致 - ---- - -## v1.11.0 - 2026-03-25 - -### 🎨 界面优化 -- **新增下一条悬浮按钮** - - 在"上一条"按钮下方添加"下一条"悬浮按钮 - - 左侧垂直排列:上一条(上)、下一条(下) - - 下一条按钮使用与点击诗词卡片相同的逻辑 - - 保留诗词卡片点击加载下一条的功能 - - 增加底部预留空间,确保所有按钮都可见 - -### 🔘 按钮布局优化 -- **三悬浮按钮设计** - - 左侧垂直排列:上一条、下一条 - - 右侧:点赞按钮 - - 所有按钮保持圆形设计和阴影效果 - - 左侧两个按钮间距合理,便于操作 - ---- - -## v1.10.0 - 2026-03-25 - -### 🎨 界面重构 -- **诗词卡片交互优化** - - 移除"下一条"按钮,改为点击诗词卡片任意区域加载下一条诗词 - - 诗词卡片整体可点击,提升用户体验 - - 保留原有的长按复制功能 - -### 🔘 按钮布局调整 -- **双悬浮按钮设计** - - 点赞按钮改为悬浮固定按钮,位于右下角 - - 上一条按钮改为悬浮固定按钮,位于左下角 - - 双按钮采用圆形设计,带阴影效果,左右分布 - - 保持加载状态显示和触觉反馈 - - 按钮位于上层,诗词卡片在下层,确保按钮始终可见 - -### 🔄 导航按钮重构 -- **双悬浮按钮布局** - - 移除传统导航按钮,改为双悬浮按钮设计 - - 左右分布:上一条(左)、点赞(右) - - 简化界面,提升视觉层次感 - - 按钮位于上层,确保始终可见和可操作 - -### ⚡ 动态加载优化 -- **分步加载机制** - - 移除全局空白等待,改为固定布局动态加载 - - 不同区域显示不同的加载状态文字 - - "出处加载中..."、"诗句加载中..."、"原文加载中..."、"关键词加载中..."、"译文加载中..." - - 分步加载顺序:标题→诗句→原文→关键词→译文 - - 每个区域200ms间隔,提供流畅的视觉体验 - -### 🌐 网络权限配置 -- **Android端网络权限** - - 添加INTERNET权限用于网络访问 - - 添加ACCESS_NETWORK_STATE权限检查网络状态 - - 添加ACCESS_WIFI_STATE权限检查WiFi状态 - - 启用usesCleartextTraffic支持HTTP连接 - -### 🧹 代码优化 -- **组件重构** - - 移除ActionButtons组件,替换为FloatingLikeButton - - 移除NavigationButtons组件,替换为FloatingPreviousButton - - 移除未使用的swapped相关代码 - - 优化布局结构,使用Stack实现双层悬浮按钮定位 - - 为悬浮按钮预留底部空间,避免遮挡内容 - - 新增sectionLoadingStates状态管理 - - 实现分步加载逻辑_simulateSectionLoading - -### 📱 用户体验提升 -- **更直观的交互方式** - - 点击卡片即可切换到下一条诗词 - - 双悬浮按钮设计,左右分布 - - 上一条按钮在左,点赞按钮在右,符合操作习惯 - - 悬浮按钮位于上层,始终可见和可操作 - - 动态加载保持布局稳定,无页面跳动 - - 分步加载提供清晰的加载进度反馈 - - 简化界面,减少视觉干扰,突出内容展示 - - 保持原有的下拉刷新功能 - ---- - -## v1.9.0 - 2026-03-25 - -### 🛠 技术重构 -- **替换SQLite为SharedPreferences** - - 将SQLiteStorageController从SQLite实现改为SharedPreferences实现 - - 移除sqflite和sqflite_common_ffi依赖 - - 使用SharedPreferences实现本地键值对存储 - - 保持原有API接口不变,确保向后兼容 - -### 📦 依赖更新 -- **移除SQLite相关依赖** - - 移除 `sqflite: ^2.4.2` 依赖 - - 移除 `sqflite_common_ffi` 相关代码 - - 保留 `shared_preferences` 作为唯一本地存储方案 - -### 🎯 功能优化 -- **存储控制器优化** - - 简化初始化流程,无需数据库路径处理 - - 移除平台权限问题,提升兼容性 - - 减少包体积,降低依赖复杂度 - - 提升存储操作性能 - -### 🔧 API保持 -- **接口兼容性** - - 保持所有原有方法签名不变 - - setString/getString - - setInt/getInt - - setBool/getBool - - setDouble/getDouble - - remove/clear/getKeys/containsKey - - 新增 setStringList/getStringList 方法 - -### 📚 OHOS兼容性 -- **平台兼容保证** - - SharedPreferences天然支持OHOS平台 - - 无需处理数据库文件路径权限 - - 确保跨平台一致性体验 - ---- - -## v1.8.1 - 2026-03-23 - -### 🔧 错误修复 -- **修复OHOS平台SQLite数据库路径权限问题** - - 解决"PathAccessException: Creation failed, path = '/.dart_tool' (OS Error: Permission denied)"错误 - - 改进数据库路径获取逻辑,增加备用路径机制 - - 确保在OHOS平台上能够正常创建和访问数据库文件 - -## v1.8.0 - 2026-03-23 - -### 🛠 技术重构 -- **替换Hive为SQLite** - - 新增 `SQLiteStorageController` 存储控制器 - - 基于sqflite实现本地存储功能 - - 提供完整的键值对存储API - -### 🔧 错误修复 -- **彻底解决编译错误** - - 修复"Target of URI doesn't exist"错误 - - 修复"Undefined name 'SharedPreferences'"错误 - - 修复"The name 'SharedPreferences' isn't a type"错误 - -### 📦 依赖更新 -- **添加SQLite支持** - - 新增 `sqflite: ^2.3.0` 依赖 - - 新增 `path: ^1.8.3` 依赖 - - 使用dependency_overrides解决版本冲突 - -### 🎯 功能实现 -- **完整的存储功能** - - 支持String、int、bool、double等基本类型 - - 支持对象序列化和反序列化 - - 提供完整的CRUD操作接口 - - 自动数据库初始化和管理 - -### 📚 OHOS兼容性 -- **平台兼容保证** - - 使用SQLite确保跨平台兼容 - - 解决Flutter版本识别问题 - - 确保OHOS平台构建成功 - -## v1.7.0 - 2026-03-21 - -### 🛠 彻底重构 -- **完全移除shared_preferences** - - 删除shared_preferences_controller.dart文件 - - 统一使用HiveStorageController - - 修复所有编译错误和引用问题 - -### 🔄 代码统一 -- **存储控制器标准化** - - hive_storage_controller.dart重命名为HiveStorageController - - 移除重复的控制器定义 - - 统一API接口和调用方式 - -### 📦 依赖清理 -- **构建问题彻底解决** - - 移除所有shared_preferences相关依赖 - - 确保OHOS构建路径兼容性 - - 简化项目依赖结构 - -### 🔧 修复内容 -- **编译错误修复** - - 修复"uri_does_not_exist"错误 - - 修复"undefined_identifier"错误 - - 更新所有import语句 - -### 📚 OHOS兼容性 -- **构建成功保证** - - 使用纯Dart库确保平台兼容 - - 移除所有Flutter平台特定依赖 - - 确保HAP构建无路径错误 - ---- - -## v1.6.0 - 2026-03-21 - -### 🛠 技术重构 -- **完全移除shared_preferences依赖** - - 解决OHOS构建路径兼容性问题 - - 替换为纯Dart的Hive存储方案 - - 确保OHOS平台完全兼容 - -### 🔄 存储方案重构 -- **Hive存储控制器** - - 重写SharedPreferencesController为HiveStorageController - - 提供相同的API接口确保向后兼容 - - 支持所有数据类型:String, int, double, bool, List, Map - - 更好的性能和类型安全 - -### 📦 依赖清理 -- **移除问题依赖** - - 删除shared_preferences git依赖 - - 保留Hive作为唯一本地存储方案 - - 简化依赖树,减少构建复杂度 - -### 🔧 初始化优化 -- **Hive初始化流程** - - 在main.dart中正确初始化Hive - - 使用Hive.initFlutter()确保Flutter兼容 - - 统一的存储控制器初始化 - -### 📚 OHOS兼容性 -- **构建问题解决** - - 修复"srcPath is not a relative path"错误 - - 移除绝对路径依赖问题 - - 确保OHOS HAP构建成功 - ---- - -## 版本说明 -- 版本号格式:主版本号.次版本号.修订号 -- CHANGELOG.md 仅保留最近 5 个版本记录 -- 较早版本记录已开发完成的功能写入下方 - -1.主页 诗句加载 点赞 - - -侧感手势 -原生90/120帧率 \ No newline at end of file +### 开发进度 +- 🏗️ **HarmonyOS桌面小组件** - 开发中,包含2x2布局、天气显示、诗句展示等功能 + - 优先级:3 +getx 加入 +二维码能力 +HarmonyOS HongMeng Kernel \ No newline at end of file diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index e866112..2dd50a6 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -5,10 +5,10 @@ class AppConfig { // 应用基础配置 - static const String appName = 'Poes'; - static const String appVersion = '1.2.21'; + static const String appName = '情景诗词'; + static const String appVersion = '1.3.1'; static const String appDescription = 'A new Flutter project'; - static const int appVersionCode = 21; + static const int appVersionCode = 26033101; // 响应式布局断点 static const double mobileBreakpoint = 768.0; diff --git a/lib/services/API_DOCUMENTATION.md b/lib/services/document/API_DOCUMENTATION.md similarity index 100% rename from lib/services/API_DOCUMENTATION.md rename to lib/services/document/API_DOCUMENTATION.md diff --git a/lib/services/API使用文档.md b/lib/services/document/API使用文档.md similarity index 100% rename from lib/services/API使用文档.md rename to lib/services/document/API使用文档.md diff --git a/lib/services/stats.php b/lib/services/document/stats.php similarity index 100% rename from lib/services/stats.php rename to lib/services/document/stats.php diff --git a/lib/services/document/搜索api.md b/lib/services/document/搜索api.md new file mode 100644 index 0000000..3a6b4f0 --- /dev/null +++ b/lib/services/document/搜索api.md @@ -0,0 +1,366 @@ +# 搜索接口文档 + +## 接口概述 + +提供网站内容搜索功能,支持模糊搜索和精确搜索,多关键字搜索,以及自定义搜索字段。 + +| 属性 | 值 | +| ---- | ------------------ | +| 接口地址 | `/searchs.php` | +| 请求方式 | GET / POST | +| 响应格式 | JSON | +| 编码 | UTF-8 | + +*** + +## 请求参数 + +### 参数说明 + +| 参数名 | 类型 | 必填 | 默认值 | 说明 | +| -------------- | ------ | --- | ------------------ | --------------------------------- | +| keyword | string | ✅ 是 | - | 搜索关键词,支持多关键字(用空格或逗号分隔) | +| mode | string | 否 | fuzzy | 搜索模式:`fuzzy`(模糊搜索)或 `exact`(精确搜索) | +| page | int | 否 | 1 | 页码,最小值 1 | +| size | int | 否 | 20 | 每页条数,范围 1-100 | +| fields | string | 否 | name,url,introduce | 搜索字段,多个用逗号分隔 | +| return\_fields | string | 否 | 全部字段 | 返回字段,多个用逗号分隔,可减少数据传输 | +| truncate | int | 否 | 1 | 是否截取长文本:`1` 截取,`0` 返回完整内容 | + +### 可用搜索字段 + +| 字段名 | 说明 | +| --------- | ------- | +| name | 精选诗句 | +| url | 诗人 《标题》 | +| introduce | 译文 | +| keywords | 标签 | +| alias | 朝代 | +| drtime | 原文 | + +### 可用返回字段 (return\_fields) + +| 字段名 | 类型 | 说明 | +| ------------ | ------ | ------- | +| id | int | 记录ID | +| name | string | 精选诗句 | +| url | string | 诗人 《标题》 | +| alias | string | 朝代 | +| keywords | string | 标签 | +| introduce | string | 译文 | +| drtime | string | 原文 | +| like | int | 点赞数 | +| hits\_total | int | 总访问量 | +| hits\_month | int | 月访问量 | +| hits\_day | int | 日访问量 | +| star | int | 星标数 | +| tui | int | 推荐数 | +| time | string | 时间 | +| create\_time | string | 创建时间 | +| update\_time | string | 更新时间 | + +*** + +## 响应格式 + +### 响应结构 + +```json +{ + "code": 0, + "msg": "success", + "data": { + "keyword": "搜索关键词", + "mode": "搜索模式", + "fields": ["搜索字段数组"], + "results": [结果数组], + "pagination": {分页信息}, + "stats": {统计信息} + } +} +``` + +### 状态码说明 + +| code | 说明 | +| ---- | ----------- | +| 0 | 请求成功 | +| -1 | 参数错误(关键词为空) | +| -2 | 搜索失败(服务器错误) | +| 429 | 请求过于频繁 | + +### 结果对象 (results) + +| 字段 | 类型 | 说明 | +| ------------ | ------ | --------------- | +| id | int | 记录ID | +| name | string | 精选诗句 | +| url | string | 网站链接 | +| alias | string | 朝代 | +| keywords | string | 标签 | +| introduce | string | 译文(超过100字符自动截取) | +| drtime | string | 原文 | +| like | int | 点赞数 | +| hits\_total | int | 总访问量 | +| hits\_month | int | 月访问量 | +| hits\_day | int | 日访问量 | +| star | int | 星标数 | +| tui | int | 推荐数 | +| time | string | 时间 | +| create\_time | string | 创建时间 | +| update\_time | string | 更新时间 | + +### 分页信息 (pagination) + +| 字段 | 类型 | 说明 | +| ------------- | ---- | ------ | +| current\_page | int | 当前页码 | +| page\_size | int | 每页条数 | +| total\_count | int | 总记录数 | +| total\_pages | int | 总页数 | +| has\_next | bool | 是否有下一页 | +| has\_prev | bool | 是否有上一页 | + +### 统计信息 (stats) + +| 字段 | 类型 | 说明 | +| --------------- | ------ | ------ | +| search\_time | string | 搜索时间 | +| keyword\_length | int | 关键词字符数 | +| result\_count | int | 当前页结果数 | + +*** + +## 请求示例 + +### 1. 基本搜索 + +``` +GET /searchs.php?keyword=春天 +``` + +### 2. 多关键字搜索 + +``` +GET /searchs.php?keyword=春天 友情 +``` + +### 3. 精确搜索 + +``` +GET /searchs.php?keyword=春晓&mode=exact +``` + +### 4. 自定义字段搜索 + +``` +GET /searchs.php?keyword=唐代&fields=alias,keywords +``` + +### 5. 分页搜索 + +``` +GET /searchs.php?keyword=测试&page=2&size=10 +``` + +### 6. POST 请求 + +``` +POST /searchs.php +Content-Type: application/x-www-form-urlencoded + +keyword=春天&mode=fuzzy&page=1&size=20&fields=name,introduce +``` + +### 7. 指定返回字段(减少数据传输) + +``` +GET /searchs.php?keyword=春天&return_fields=id,name,keywords,alias +``` + +### 8. 返回完整内容(不截取) + +``` +GET /searchs.php?keyword=春天&truncate=0 +``` + +### 9. 组合使用(高效查询) + +``` +GET /searchs.php?keyword=春天&fields=keywords&return_fields=id,name,keywords,alias&truncate=0 +``` + +*** + +## 响应示例 + +### 成功响应 + +```json +{ + "code": 0, + "msg": "success", + "data": { + "keyword": "春天", + "mode": "fuzzy", + "fields": ["name", "url", "introduce"], + "return_fields": ["id", "name", "url", "alias", "keywords", "introduce", "drtime", "like", "hits_total", "hits_month", "hits_day", "star", "tui", "time", "create_time", "update_time"], + "truncate": 1, + "results": [ + { + "id": 1, + "name": "春晓", + "url": "刘禹锡《酬乐天扬州初逢席上见赠》", + "alias": "唐代", + "keywords": "春天,孟浩然", + "introduce": "春眠不觉晓,处处闻啼鸟...", + "drtime": "巴山楚水凄凉地,二十三年弃置身。怀旧空吟闻笛赋,到乡翻似烂柯人。沉舟侧畔千帆过,病树前头万木春。今日听君歌一曲,暂凭杯酒长精神。", + "like": 15, + "hits_total": 120, + "hits_month": 50, + "hits_day": 5, + "star": 10, + "tui": 8, + "time": "", + "create_time": "2024-01-01 10:00:00", + "update_time": "2024-01-01 12:00:00" + } + ], + "pagination": { + "current_page": 1, + "page_size": 20, + "total_count": 1, + "total_pages": 1, + "has_next": false, + "has_prev": false + }, + "stats": { + "search_time": "2024-03-14 20:17:00", + "keyword_length": 2, + "result_count": 1 + } + } +} +``` + +### 指定返回字段响应(精简数据) + +```json +{ + "code": 0, + "msg": "success", + "data": { + "keyword": "春天", + "mode": "fuzzy", + "fields": ["keywords"], + "return_fields": ["id", "name", "keywords", "alias"], + "truncate": 0, + "results": [ + { + "id": 1, + "name": "春晓", + "keywords": "春天,孟浩然,唐诗,五言绝句", + "alias": "唐代" + } + ], + "pagination": {...}, + "stats": {...} + } +} +``` + +### 错误响应 + +```json +{ + "code": -1, + "msg": "搜索关键词不能为空", + "data": null +} +``` + +### 频率限制响应 + +```json +{ + "code": 429, + "msg": "搜索过于频繁,请5秒后再试" +} +``` + +*** + +## 注意事项 + +1. **关键词要求**:必须提供搜索关键词,否则返回错误 +2. **多关键字**:模糊搜索支持多关键字,用空格或逗号分隔,任一关键字匹配即返回 +3. **字段限制**:只能搜索预定义的字段,无效字段将被忽略 +4. **分页限制**:每页最多100条记录,超过限制将自动调整为100 +5. **安全处理**:所有输入都会进行安全转义,防止SQL注入 +6. **简介截取**:`truncate=1` 时简介字段自动截取到100字符,`truncate=0` 返回完整内容 +7. **频率限制**:每个 IP 每 10 秒最多搜索 10 次,超出返回 429 错误 +8. **性能优化**:使用 `return_fields` 指定需要的字段,减少数据传输和服务器资源消耗 + +*** + +## App 集成建议 + +### 请求封装示例 (Swift) + +```swift +func search(keyword: String, + mode: String = "fuzzy", + page: Int = 1, + size: Int = 20, + fields: String = "name,url,introduce", + returnFields: String = "", + truncate: Int = 1) async throws -> SearchResponse { + var params = [ + "keyword": keyword, + "mode": mode, + "page": "\(page)", + "size": "\(size)", + "fields": fields, + "truncate": "\(truncate)" + ] + if !returnFields.isEmpty { + params["return_fields"] = returnFields + } + return try await request(endpoint: "/searchs.php", params: params) +} +``` + +### 请求封装示例 (Kotlin) + +```kotlin +suspend fun search( + keyword: String, + mode: String = "fuzzy", + page: Int = 1, + size: Int = 20, + fields: String = "name,url,introduce", + returnFields: String = "", + truncate: Int = 1 +): SearchResponse { + val params = mutableMapOf( + "keyword" to keyword, + "mode" to mode, + "page" to page.toString(), + "size" to size.toString(), + "fields" to fields, + "truncate" to truncate.toString() + ) + if (returnFields.isNotEmpty()) { + params["return_fields"] = returnFields + } + return request(endpoint = "/searchs.php", params = params) +} +``` + +### 错误处理建议 + +- 检测 `code == 429` 时,提示用户稍后重试 +- 检测 `code == -1` 时,提示用户输入关键词 +- 检测 `code == 0` 时,解析 `data.results` 显示结果列表 +- 使用 `pagination.has_next` 判断是否显示"加载更多"按钮 + diff --git a/lib/services/统计API文档.md b/lib/services/document/统计API文档.md similarity index 64% rename from lib/services/统计API文档.md rename to lib/services/document/统计API文档.md index 47f61e1..58dc34a 100644 --- a/lib/services/统计API文档.md +++ b/lib/services/document/统计API文档.md @@ -59,56 +59,56 @@ ### 基础字段 -| 字段名 | 类型 | 说明 | -|--------|------|------| -| ok | boolean | 请求是否成功 | -| data | object | 统计数据对象 | -| timestamp | int | 服务器时间戳 | +| 字段名 | 类型 | 说明 | +| --------- | ------- | ------ | +| ok | boolean | 请求是否成功 | +| data | object | 统计数据对象 | +| timestamp | int | 服务器时间戳 | ### data 对象字段 #### 数量统计 -| 字段名 | 类型 | 说明 | -|--------|------|------| -| count_category | int | 已开设分类数量 | -| count_site | int | 已收录诗句数量 | -| count_apply | int | 审核中的申请数量 | -| count_apply_reject | int | 已拒绝的申请数量 | -| count_article | int | 文章数量 | -| count_article_category | int | 文章分类数量 | -| count_notice | int | 已发布公告数量 | -| count_link | int | 开发者人数 | -| count_tags | int | 分类标签数量 | +| 字段名 | 类型 | 说明 | +| ------------------------ | --- | ---- | +| count\_category | int | 项目 | +| count\_site | int | 收录诗句 | +| count\_apply | int | 审核中 | +| count\_apply\_reject | int | 已拒绝 | +| count\_article | int | 每日一句 | +| count\_article\_category | int | 文章分类 | +| count\_notice | int | 推送 | +| count\_link | int | 开发者 | +| count\_tags | int | 分类标签 | #### 热度统计 -| 字段名 | 类型 | 说明 | -|--------|------|------| -| cumulative_hits | string | 累计热度次数 | -| cumulative_likes | string | 累计点赞数量 | +| 字段名 | 类型 | 说明 | +| ----------------- | ------ | ------ | +| cumulative\_hits | string | 累计热度次数 | +| cumulative\_likes | string | 累计点赞数量 | #### 热门内容 -| 字段名 | 类型 | 说明 | -|--------|------|------| -| top_hits_day | object/null | 当天热门诗句 | -| top_hits_month | object/null | 本月热门诗句 | -| top_hits_total | object/null | 历史最热诗句 | -| top_like | object/null | 最高点赞诗句 | +| 字段名 | 类型 | 说明 | +| ---------------- | ----------- | ------ | +| top\_hits\_day | object/null | 当天热门诗句 | +| top\_hits\_month | object/null | 本月热门诗句 | +| top\_hits\_total | object/null | 历史最热诗句 | +| top\_like | object/null | 最高点赞诗句 | #### 热门内容对象 -| 字段名 | 类型 | 说明 | -|--------|------|------| -| id | string | 诗句 ID | -| name | string | 诗句内容 | +| 字段名 | 类型 | 说明 | +| ---- | ------ | ----- | +| id | string | 诗句 ID | +| name | string | 诗句内容 | #### 其他 -| 字段名 | 类型 | 说明 | -|--------|------|------| -| build_time | string | 建站时间(格式:YYYY-MM-DD) | +| 字段名 | 类型 | 说明 | +| ----------- | ------ | ------------------- | +| build\_time | string | 建站时间(格式:YYYY-MM-DD) | ## 错误返回 @@ -135,7 +135,6 @@ fetch('https://yy.vogov.cn/api/app/stats.php') .catch(error => console.error('Error:', error)); ``` - ### Flutter (Dart) ```dart @@ -194,15 +193,15 @@ class TopContent { 数据来源于以下数据库表: -| 表名 | 说明 | -|------|------| -| pre_category | 分类表 | -| pre_site | 收录诗句表 | -| pre_apply | 申请收录表 | -| pre_article | 文章表 | -| pre_article_category | 文章分类表 | -| pre_notice | 公告表 | -| pre_link | 友情链接表 | +| 表名 | 说明 | +| ---------------------- | ----- | +| pre\_category | 分类表 | +| pre\_site | 收录诗句表 | +| pre\_apply | 申请收录表 | +| pre\_article | 文章表 | +| pre\_article\_category | 文章分类表 | +| pre\_notice | 公告表 | +| pre\_link | 友情链接表 | ## 注意事项 @@ -214,3 +213,4 @@ class TopContent { ## 更新日志 - **v1.0.14** (2026-03-30): 新增统计 API 接口 + diff --git a/lib/views/active/category_page.dart b/lib/views/active/category_page.dart index 8231df7..1360831 100644 --- a/lib/views/active/category_page.dart +++ b/lib/views/active/category_page.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import '../../constants/app_constants.dart'; +import 'tags/corr_page.dart'; -/// 时间: 2026-03-25 +/// 时间: 2026-04-01 /// 功能: 分类页面 /// 介绍: 展示诗词分类,包括场景分类和朝代分类 -/// 最新变化: 新建分类页面,支持场景和朝代两大分类 +/// 最新变化: 重新设计iOS风格布局,减少间距,加大字体,显示分类数量 class CategoryPage extends StatefulWidget { const CategoryPage({super.key}); @@ -16,7 +17,10 @@ class CategoryPage extends StatefulWidget { class _CategoryPageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; - final List _tabCategories = ['场景分类', '朝代分类']; + final List> _tabCategories = [ + {'label': '场景分类', 'icon': Icons.category}, + {'label': '朝代分类', 'icon': Icons.history}, + ]; static const sceneData = { "节日": ["七夕节", "中秋节", "元宵节", "寒食节", "清明节", "端午节", "重阳节", "春节", "节日"], @@ -90,76 +94,131 @@ class _CategoryPageState extends State @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - body: Column( - children: [ - // 自定义标题栏 - - // Tab栏 - Container( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TabBar( - controller: _tabController, - tabs: _tabCategories - .map((category) => Tab(text: category)) - .toList(), - labelColor: AppConstants.primaryColor, - unselectedLabelColor: Colors.grey[600], - indicatorColor: AppConstants.primaryColor, - indicatorWeight: 3, - labelStyle: const TextStyle(fontWeight: FontWeight.bold), + return Column( + children: [ + // Tab栏 + Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TabBar( + controller: _tabController, + tabs: _tabCategories + .map( + (category) => Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(category['icon'], size: 18), + const SizedBox(width: 6), + Text(category['label']), + ], + ), + ), + ) + .toList(), + labelColor: AppConstants.primaryColor, + unselectedLabelColor: Colors.grey[600], + indicatorColor: AppConstants.primaryColor, + indicatorWeight: 3, + labelStyle: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + ), + unselectedLabelStyle: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 16, ), ), - // 内容区域 - Expanded( + ), + Container(height: 0.5, color: const Color(0xFFE5E5EA)), + // 内容区域 + Expanded( + child: Container( + color: const Color(0xFFF2F2F7), child: TabBarView( controller: _tabController, children: [ - _buildCategoryGrid(sceneData, '场景分类'), - _buildCategoryGrid(dynastyData, '朝代分类'), + _buildCategoryList(sceneData, _tabCategories[0]['label']), + _buildCategoryList(dynastyData, _tabCategories[1]['label']), ], ), ), - ], - ), + ), + ], ); } - Widget _buildCategoryGrid( + Widget _buildCategoryList( Map> data, String categoryType, ) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: ListView.builder( - itemCount: data.keys.length, - itemBuilder: (context, index) { - final category = data.keys.elementAt(index); - final items = data[category]!; + return ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: data.keys.length, + separatorBuilder: (context, index) => const SizedBox(height: 8), + itemBuilder: (context, index) { + final category = data.keys.elementAt(index); + final items = data[category]!; - return Card( - margin: const EdgeInsets.only(bottom: 16.0), - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - category, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, - ), + Row( + children: [ + Expanded( + child: Text( + category, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), + decoration: BoxDecoration( + color: AppConstants.primaryColor.withValues( + alpha: 0.1, + ), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${items.length}', + style: TextStyle( + fontSize: 13, + color: AppConstants.primaryColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ], ), const SizedBox(height: 12), Wrap( - spacing: 8, - runSpacing: 8, + spacing: 10, + runSpacing: 10, children: items.map((item) { return _buildCategoryChip(item, categoryType); }).toList(), @@ -167,38 +226,38 @@ class _CategoryPageState extends State ], ), ), - ); - }, - ), + ), + ); + }, ); } Widget _buildCategoryChip(String label, String categoryType) { return GestureDetector( onTap: () { - // TODO: 跳转到对应分类的诗词列表页面 - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('点击了 $label'), - duration: const Duration(seconds: 1), + final searchType = categoryType == '朝代分类' ? 'alias' : 'keywords'; + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => CorrPage(label: label, searchType: searchType), ), ); }, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(16), + color: AppConstants.primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20), border: Border.all( - color: Theme.of(context).primaryColor.withValues(alpha: 0.3), + color: AppConstants.primaryColor.withValues(alpha: 0.3), width: 1, ), ), child: Text( label, style: TextStyle( - color: Theme.of(context).primaryColor, - fontSize: 12, + color: AppConstants.primaryColor, + fontSize: 14, fontWeight: FontWeight.w500, ), ), diff --git a/lib/views/active/popular_page.dart b/lib/views/active/popular_page.dart index ee2ab25..9ace848 100644 --- a/lib/views/active/popular_page.dart +++ b/lib/views/active/popular_page.dart @@ -3,6 +3,8 @@ import '../../constants/app_constants.dart'; import '../../utils/http/http_client.dart'; import '../../models/poetry_model.dart'; import '../../controllers/load/locally.dart'; +import '../../controllers/history_controller.dart'; +import '../../services/network_listener_service.dart'; /// 时间: 2026-03-25 /// 功能: 热门页面 @@ -19,7 +21,11 @@ class PopularPage extends StatefulWidget { class _PopularPageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; - final List _tabCategories = ['总榜', '日榜', '月榜']; + final List> _tabCategories = [ + {'label': '总榜', 'icon': Icons.bar_chart}, + {'label': '日榜', 'icon': Icons.today}, + {'label': '月榜', 'icon': Icons.calendar_today}, + ]; List _rankList = []; bool _loading = false; @@ -46,36 +52,46 @@ class _PopularPageState extends State @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - body: Column( - children: [ - // Tab栏 - Container( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TabBar( - controller: _tabController, - tabs: _tabCategories - .map((category) => Tab(text: category)) - .toList(), - labelColor: AppConstants.primaryColor, - unselectedLabelColor: Colors.grey[600], - indicatorColor: AppConstants.primaryColor, - indicatorWeight: 3, - labelStyle: const TextStyle(fontWeight: FontWeight.bold), - ), + return Column( + children: [ + // 黑色分割线 + Container(height: 1, color: Colors.black.withValues(alpha: 0.1)), + // Tab栏 + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: TabBar( + controller: _tabController, + tabs: _tabCategories + .map( + (category) => Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(category['icon'], size: 16), + const SizedBox(width: 6), + Text(category['label']), + ], + ), + ), + ) + .toList(), + labelColor: AppConstants.primaryColor, + unselectedLabelColor: Colors.grey[600], + indicatorColor: AppConstants.primaryColor, + indicatorWeight: 3, + labelStyle: const TextStyle(fontWeight: FontWeight.bold), ), - // 内容区域 - Expanded( - child: TabBarView( - controller: _tabController, - children: _tabCategories.map((category) { - return _buildRankContent(category); - }).toList(), - ), + ), + // 内容区域 + Expanded( + child: TabBarView( + controller: _tabController, + children: _tabCategories.map((category) { + return _buildRankContent(category['label']); + }).toList(), ), - ], - ), + ), + ], ); } @@ -158,8 +174,50 @@ class _PopularPageState extends State ); } + Future _createNoteFromPoetry(PoetryModel poetry) async { + try { + final title = poetry.name.isNotEmpty ? poetry.name : '诗词笔记'; + final category = poetry.alias.isNotEmpty ? poetry.alias : '诗词'; + final contentBuffer = StringBuffer(); + + if (poetry.url.isNotEmpty) { + contentBuffer.writeln('出处:${poetry.url}'); + } + if (poetry.name.isNotEmpty) { + contentBuffer.writeln('诗句:${poetry.name}'); + } + if (poetry.alias.isNotEmpty) { + contentBuffer.writeln('朝代:${poetry.alias}'); + } + + final noteId = await HistoryController.saveNote( + title: title, + content: contentBuffer.toString().trim(), + category: category, + ); + + if (noteId != null) { + NetworkListenerService().sendSuccessEvent( + NetworkEventType.noteUpdate, + data: noteId, + ); + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('已创建笔记'))); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('创建笔记失败: $e'))); + } + } + } + Widget _buildRankItem(PoetryModel poetry, int index) { - final rank = poetry.rank > 0 ? poetry.rank : index; // 优先使用API返回的rank + final rank = poetry.rank > 0 ? poetry.rank : index; final isTopThree = rank <= 3; return Card( @@ -179,120 +237,177 @@ class _PopularPageState extends State borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16), - child: Row( + child: Stack( + clipBehavior: Clip.none, children: [ - // 排名徽章 - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: isTopThree - ? AppConstants.primaryColor - : AppConstants.primaryColor.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(20), - border: isTopThree - ? null - : Border.all( - color: AppConstants.primaryColor.withValues( - alpha: 0.2, - ), - width: 1, - ), - ), - child: Center( - child: Text( - rank.toString(), - style: TextStyle( + Row( + children: [ + // 排名徽章 + Container( + width: 40, + height: 40, + decoration: BoxDecoration( color: isTopThree - ? Colors.white - : AppConstants.primaryColor.withValues(alpha: 0.8), - fontWeight: FontWeight.bold, - fontSize: 16, + ? AppConstants.primaryColor + : AppConstants.primaryColor.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(20), + border: isTopThree + ? null + : Border.all( + color: AppConstants.primaryColor.withValues( + alpha: 0.2, + ), + width: 1, + ), + ), + child: Center( + child: Text( + rank.toString(), + style: TextStyle( + color: isTopThree + ? Colors.white + : AppConstants.primaryColor.withValues( + alpha: 0.8, + ), + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), ), ), - ), - ), - const SizedBox(width: 12), + const SizedBox(width: 12), - // 内容区域 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 标题 - Text( - poetry.name, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - - // 朝代和作者 - if (poetry.alias.isNotEmpty || poetry.url.isNotEmpty) - Row( - children: [ - if (poetry.alias.isNotEmpty) ...[ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: AppConstants.primaryColor.withValues( - alpha: 0.1, - ), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - poetry.alias, - style: TextStyle( - fontSize: 10, - color: AppConstants.primaryColor, - ), - ), - ), - const SizedBox(width: 8), - ], - if (poetry.url.isNotEmpty) - Expanded( - child: Text( - poetry.url, - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - const SizedBox(height: 8), - - // 统计数据 - Row( + // 内容区域 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildStatItem( - '👁', - poetry.hitsTotal.toString(), - '总浏览', + // 标题 + Text( + poetry.name, + style: Theme.of(context).textTheme.titleMedium + ?.copyWith(fontWeight: FontWeight.bold), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - const SizedBox(width: 16), - _buildStatItem('💖', poetry.like.toString(), '点赞'), - const SizedBox(width: 16), - if (_tabController.index == 1) // 日榜 - _buildStatItem('📅', poetry.hitsDay.toString(), '今日'), - if (_tabController.index == 2) // 月榜 - _buildStatItem( - '📊', - poetry.hitsMonth.toString(), - '本月', + const SizedBox(height: 4), + + // 朝代和作者 + if (poetry.alias.isNotEmpty || poetry.url.isNotEmpty) + Row( + children: [ + if (poetry.alias.isNotEmpty) ...[ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: AppConstants.primaryColor.withValues( + alpha: 0.1, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + poetry.alias, + style: TextStyle( + fontSize: 10, + color: AppConstants.primaryColor, + ), + ), + ), + const SizedBox(width: 8), + ], + if (poetry.url.isNotEmpty) + Expanded( + child: Text( + poetry.url, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], ), + const SizedBox(height: 8), + + // 统计数据 + Row( + children: [ + _buildStatItem( + '👁', + poetry.hitsTotal.toString(), + '总浏览', + ), + const SizedBox(width: 16), + _buildStatItem('💖', poetry.like.toString(), '点赞'), + const SizedBox(width: 16), + if (_tabController.index == 1) + _buildStatItem( + '📅', + poetry.hitsDay.toString(), + '今日', + ), + if (_tabController.index == 2) + _buildStatItem( + '📊', + poetry.hitsMonth.toString(), + '本月', + ), + ], + ), ], ), - ], + ), + ], + ), + // 添加笔记按钮 - 位于右下角,可遮挡字段 + Positioned( + bottom: -8, + right: -8, + child: GestureDetector( + onTap: () => _createNoteFromPoetry(poetry), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.orange[700], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.15), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.note_add, + size: 14, + color: Colors.white, + ), + const SizedBox(width: 4), + const Text( + '笔记', + style: TextStyle( + fontSize: 11, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), ), ), ], diff --git a/lib/views/active/tags/corr_page.dart b/lib/views/active/tags/corr_page.dart new file mode 100644 index 0000000..3988469 --- /dev/null +++ b/lib/views/active/tags/corr_page.dart @@ -0,0 +1,780 @@ +/// 时间: 2026-04-01 +/// 功能: 标签/朝代诗词列表页面 +/// 介绍: 展示指定标签或朝代相关的诗词列表,支持搜索和浏览 +/// 最新变化: 新建页面,iOS风格设计,集成搜索API +library; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../../../constants/app_constants.dart'; +import '../../../utils/http/http_client.dart'; +import '../../../controllers/history_controller.dart'; +import '../../../services/network_listener_service.dart'; + +/// 标签诗词列表页面 +/// [label] 标签名称或朝代名称 +/// [searchType] 搜索类型:'keywords' 标签搜索, 'alias' 朝代搜索 +class CorrPage extends StatefulWidget { + final String label; + final String searchType; + + const CorrPage({ + super.key, + required this.label, + this.searchType = 'keywords', + }); + + @override + State createState() => _CorrPageState(); +} + +class _CorrPageState extends State + with SingleTickerProviderStateMixin { + List> _poetryList = []; + bool _isLoading = true; + bool _isLoadingMore = false; + bool _hasMore = true; + String? _errorMessage; + int _currentPage = 1; + int _totalCount = 0; + int _totalPages = 0; + final int _pageSize = 10; + final ScrollController _scrollController = ScrollController(); + late AnimationController _skeletonAnimationController; + late Animation _skeletonAnimation; + + @override + void initState() { + super.initState(); + _skeletonAnimationController = AnimationController( + duration: const Duration(seconds: 1), + vsync: this, + )..repeat(reverse: true); + _skeletonAnimation = Tween( + begin: 0.3, + end: 1.0, + ).animate(_skeletonAnimationController); + _loadData(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _skeletonAnimationController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 200 && + !_isLoadingMore && + _hasMore) { + _loadMoreData(); + } + } + + Future _loadData() async { + setState(() { + _isLoading = true; + _errorMessage = null; + _currentPage = 1; + _hasMore = true; + }); + + try { + final response = await HttpClient.get( + 'searchs.php', + queryParameters: { + 'keyword': widget.label, + 'mode': 'exact', + 'page': _currentPage.toString(), + 'size': _pageSize.toString(), + 'fields': widget.searchType, + 'return_fields': + 'id,name,url,alias,keywords,introduce,drtime,like,hits_total', + }, + ); + + if (mounted) { + if (response.isSuccess && response.jsonData['code'] == 0) { + final data = response.jsonData['data']; + final results = data['results'] as List? ?? []; + final pagination = data['pagination'] as Map?; + + setState(() { + _poetryList = results + .map((item) => item as Map) + .toList(); + _totalCount = pagination?['total_count'] as int? ?? 0; + _totalPages = pagination?['total_pages'] as int? ?? 0; + _hasMore = results.length >= _pageSize; + _isLoading = false; + }); + } else { + setState(() { + _errorMessage = response.jsonData['msg'] ?? '加载失败'; + _isLoading = false; + }); + } + } + } catch (e) { + if (mounted) { + setState(() { + _errorMessage = '网络错误: $e'; + _isLoading = false; + }); + } + } + } + + Future _loadMoreData() async { + if (_isLoadingMore || !_hasMore) return; + + setState(() { + _isLoadingMore = true; + }); + + try { + final nextPage = _currentPage + 1; + final response = await HttpClient.get( + 'searchs.php', + queryParameters: { + 'keyword': widget.label, + 'mode': 'exact', + 'page': nextPage.toString(), + 'size': _pageSize.toString(), + 'fields': widget.searchType, + 'return_fields': + 'id,name,url,alias,keywords,introduce,drtime,like,hits_total', + }, + ); + + if (mounted) { + if (response.isSuccess && response.jsonData['code'] == 0) { + final data = response.jsonData['data']; + final results = data['results'] as List? ?? []; + + setState(() { + _poetryList.addAll( + results.map((item) => item as Map), + ); + _currentPage = nextPage; + _hasMore = results.length >= _pageSize; + _isLoadingMore = false; + }); + } else { + setState(() { + _isLoadingMore = false; + }); + } + } + } catch (e) { + if (mounted) { + setState(() { + _isLoadingMore = false; + }); + } + } + } + + Future _toggleLike(Map poetry) async { + final poetryId = poetry['id'].toString(); + final isLiked = await HistoryController.isInLiked(poetryId); + + if (isLiked) { + await HistoryController.removeLikedPoetry(poetryId); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('已取消点赞'), + duration: Duration(seconds: 1), + ), + ); + } + } else { + await HistoryController.addToLiked(poetry); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('已点赞'), duration: Duration(seconds: 1)), + ); + } + } + if (mounted) { + setState(() {}); + } + } + + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: SystemUiOverlayStyle.dark, + child: Scaffold( + backgroundColor: const Color(0xFFF2F2F7), + appBar: _buildAppBar(), + body: _buildBody(), + ), + ); + } + + PreferredSizeWidget _buildAppBar() { + return AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon( + Icons.arrow_back_ios, + color: AppConstants.primaryColor, + ), + onPressed: () => Navigator.pop(context), + ), + title: Column( + children: [ + Text( + widget.label, + style: const TextStyle( + color: Colors.black, + fontSize: 17, + fontWeight: FontWeight.w600, + ), + ), + Text( + widget.searchType == 'alias' ? '朝代' : '标签', + style: TextStyle( + color: Colors.grey[600], + fontSize: 12, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + centerTitle: true, + actions: [ + if (!_isLoading) + Padding( + padding: const EdgeInsets.only(right: 16), + child: Center( + child: Text( + '$_totalCount 篇 / $_totalPages 页', + style: TextStyle( + color: Colors.grey[600], + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + bottom: PreferredSize( + preferredSize: const Size.fromHeight(0.5), + child: Container(height: 0.5, color: const Color(0xFFE5E5EA)), + ), + ); + } + + Widget _buildBody() { + if (_isLoading) { + return _buildSkeletonView(); + } + + if (_errorMessage != null) { + return _buildErrorView(); + } + + if (_poetryList.isEmpty) { + return _buildEmptyView(); + } + + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: RefreshIndicator( + key: const ValueKey('list'), + color: AppConstants.primaryColor, + onRefresh: _loadData, + child: ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(16), + itemCount: _poetryList.length + (_hasMore ? 1 : 0), + itemBuilder: (context, index) { + if (index >= _poetryList.length) { + return _buildLoadingMoreIndicator(); + } + return _buildPoetryCard(_poetryList[index]); + }, + ), + ), + ); + } + + Widget _buildSkeletonView() { + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: 5, + itemBuilder: (context, index) { + return _buildSkeletonCard(); + }, + ); + } + + Widget _buildSkeletonCard() { + return AnimatedBuilder( + animation: _skeletonAnimation, + builder: (context, child) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSkeletonLine(180, 20, _skeletonAnimation.value), + const SizedBox(height: 8), + _buildSkeletonLine( + double.infinity, + 16, + _skeletonAnimation.value * 0.9, + ), + const SizedBox(height: 6), + _buildSkeletonLine(250, 16, _skeletonAnimation.value * 0.8), + const SizedBox(height: 6), + _buildSkeletonLine(200, 16, _skeletonAnimation.value * 0.7), + const SizedBox(height: 12), + Row( + children: [ + _buildSkeletonLine(60, 16, _skeletonAnimation.value * 0.6), + const SizedBox(width: 16), + _buildSkeletonLine(60, 16, _skeletonAnimation.value * 0.5), + ], + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildSkeletonLine(double width, double height, double opacity) { + return Container( + width: width, + height: height, + decoration: BoxDecoration( + color: const Color( + 0xFFE5E5EA, + ).withValues(alpha: opacity.clamp(0.3, 0.8)), + borderRadius: BorderRadius.circular(4), + ), + ); + } + + Widget _buildErrorView() { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, color: Colors.red[300], size: 64), + const SizedBox(height: 16), + Text( + _errorMessage ?? '加载失败', + style: TextStyle(color: Colors.grey[600], fontSize: 14), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _loadData, + style: ElevatedButton.styleFrom( + backgroundColor: AppConstants.primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: const Text('重试'), + ), + ], + ), + ), + ); + } + + Widget _buildEmptyView() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.inbox_outlined, color: Colors.grey[400], size: 64), + const SizedBox(height: 16), + Text( + '暂无相关诗词', + style: TextStyle(color: Colors.grey[600], fontSize: 14), + ), + const SizedBox(height: 8), + Text( + '试试其他标签或朝代', + style: TextStyle(color: Colors.grey[400], fontSize: 12), + ), + ], + ), + ); + } + + Widget _buildLoadingMoreIndicator() { + return Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + color: AppConstants.primaryColor, + ), + ), + ), + ); + } + + Widget _buildPoetryCard(Map poetry) { + final name = poetry['name']?.toString() ?? '未知标题'; + final url = poetry['url']?.toString() ?? ''; + final alias = poetry['alias']?.toString() ?? ''; + final introduce = poetry['introduce']?.toString() ?? ''; + final like = poetry['like'] ?? 0; + final hitsTotal = poetry['hits_total'] ?? 0; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(12), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () => _showPoetryDetail(poetry), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + ), + if (alias.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: AppConstants.primaryColor.withValues( + alpha: 0.1, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + alias, + style: TextStyle( + fontSize: 12, + color: AppConstants.primaryColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + if (url.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + url, + style: TextStyle(fontSize: 13, color: Colors.grey[600]), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + if (introduce.isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + introduce, + style: TextStyle( + fontSize: 14, + color: Colors.grey[700], + height: 1.5, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + const SizedBox(height: 12), + Row( + children: [ + Icon( + Icons.remove_red_eye_outlined, + size: 16, + color: Colors.grey[400], + ), + const SizedBox(width: 4), + Text( + '$hitsTotal', + style: TextStyle(fontSize: 12, color: Colors.grey[500]), + ), + const SizedBox(width: 16), + Icon( + Icons.favorite_outline, + size: 16, + color: Colors.grey[400], + ), + const SizedBox(width: 4), + Text( + '$like', + style: TextStyle(fontSize: 12, color: Colors.grey[500]), + ), + const Spacer(), + FutureBuilder( + future: HistoryController.isInLiked( + poetry['id'].toString(), + ), + builder: (context, snapshot) { + final isLiked = snapshot.data ?? false; + return GestureDetector( + onTap: () => _toggleLike(poetry), + child: Icon( + isLiked ? Icons.favorite : Icons.favorite_border, + size: 20, + color: isLiked ? Colors.red : Colors.grey[400], + ), + ); + }, + ), + ], + ), + ], + ), + ), + ), + ), + ); + } + + Future _createNoteFromPoetry(Map poetry) async { + try { + final url = poetry['url']?.toString() ?? ''; + final alias = poetry['alias']?.toString() ?? ''; + final name = poetry['name']?.toString() ?? ''; + final drtime = poetry['drtime']?.toString() ?? ''; + final introduce = poetry['introduce']?.toString() ?? ''; + + final title = url.isNotEmpty ? url : '诗词笔记'; + final category = alias.isNotEmpty ? alias : '诗词'; + final contentBuffer = StringBuffer(); + if (name.isNotEmpty) { + contentBuffer.writeln(name); + contentBuffer.writeln(''); + } + if (drtime.isNotEmpty) { + contentBuffer.writeln(drtime); + contentBuffer.writeln(''); + } + if (introduce.isNotEmpty) { + contentBuffer.writeln('译文:'); + contentBuffer.writeln(introduce); + } + + final noteId = await HistoryController.saveNote( + title: title, + content: contentBuffer.toString().trim(), + category: category, + ); + + if (noteId != null) { + NetworkListenerService().sendSuccessEvent( + NetworkEventType.noteUpdate, + data: noteId, + ); + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('已创建笔记'))); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('创建笔记失败: $e'))); + } + } + } + + void _showPoetryDetail(Map poetry) { + final drtime = poetry['drtime']?.toString() ?? ''; + final introduce = poetry['introduce']?.toString() ?? ''; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + height: MediaQuery.of(context).size.height * 0.7, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(20), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: Text( + poetry['name']?.toString() ?? '诗词详情', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ], + ), + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (poetry['url']?.toString().isNotEmpty == true) ...[ + Text( + poetry['url'].toString(), + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + const SizedBox(height: 16), + ], + if (drtime.isNotEmpty) ...[ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF8F8F8), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + drtime, + style: const TextStyle( + fontSize: 16, + height: 1.8, + fontFamily: 'serif', + ), + ), + ), + const SizedBox(height: 20), + ], + if (introduce.isNotEmpty) ...[ + const Text( + '译文', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + Text( + introduce, + style: TextStyle( + fontSize: 15, + color: Colors.grey[700], + height: 1.8, + ), + ), + ], + const SizedBox(height: 20), + ], + ), + ), + ), + SafeArea( + child: Container( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.08), + blurRadius: 4, + offset: const Offset(0, -2), + ), + ], + ), + child: SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () { + Navigator.pop(context); + _createNoteFromPoetry(poetry); + }, + icon: const Icon(Icons.note_add, size: 18), + label: const Text('创建笔记'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange[700], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/discover_page.dart b/lib/views/discover_page.dart index ec4fc85..6b92213 100644 --- a/lib/views/discover_page.dart +++ b/lib/views/discover_page.dart @@ -6,6 +6,7 @@ import 'active/active_search_page.dart'; import 'active/category_page.dart'; import 'active/popular_page.dart'; import 'active/rate.dart'; +import '../controllers/shared_preferences_storage_controller.dart'; /// 时间: 2025-03-21 /// 功能: 发现页面 @@ -21,26 +22,52 @@ class DiscoverPage extends StatefulWidget { class _DiscoverPageState extends State with SingleTickerProviderStateMixin { - late TabController _tabController; - final List _categories = ['热门', '分类', '搜索', '活跃']; + TabController? _tabController; + List _categories = ['分类', '热门', '搜索']; bool _showTips = true; + bool _isDeveloperMode = false; + bool _isInitialized = false; OverlayEntry? _infoOverlayEntry; @override void initState() { super.initState(); - _tabController = TabController(length: _categories.length, vsync: this); - // 添加标签切换监听,以便更新UI - _tabController.addListener(() { - _removeInfoOverlay(); - setState(() {}); + _loadDeveloperMode(); + } + + Future _loadDeveloperMode() async { + final isEnabled = await SharedPreferencesStorageController.getBool( + 'developer_mode_enabled', + defaultValue: false, + ); + if (mounted) { + setState(() { + _isDeveloperMode = isEnabled; + _updateCategories(); + }); + } + } + + void _updateCategories() { + setState(() { + _categories = ['分类', '热门', '搜索']; + if (_isDeveloperMode) { + _categories.add('活跃'); + } + _tabController?.dispose(); + _tabController = TabController(length: _categories.length, vsync: this); + _tabController!.addListener(() { + _removeInfoOverlay(); + setState(() {}); + }); + _isInitialized = true; }); } @override void dispose() { _removeInfoOverlay(); - _tabController.dispose(); + _tabController?.dispose(); super.dispose(); } @@ -51,12 +78,16 @@ class _DiscoverPageState extends State @override Widget build(BuildContext context) { - final isHotTab = _categories[_tabController.index] == '热门'; + if (!_isInitialized) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + + final isHotTab = _categories[_tabController!.index] == '热门'; return Scaffold( appBar: TabbedNavAppBar.build( title: '发现', - tabController: _tabController, + tabController: _tabController!, tabLabels: _categories, leading: isHotTab ? _buildInfoButton(context) : null, actions: [ @@ -66,7 +97,7 @@ class _DiscoverPageState extends State body: Column( children: [ // 只有非搜索标签时才显示话题chips - if (_categories[_tabController.index] != '搜索' && _showTips) + if (_categories[_tabController!.index] != '搜索' && _showTips) _buildTopicChips(), Expanded( child: NotificationListener( @@ -77,14 +108,10 @@ class _DiscoverPageState extends State return false; }, child: TabBarView( - controller: _tabController, + controller: _tabController!, children: _categories.asMap().entries.map((entry) { final category = entry.value; - // 搜索标签显示 ActiveSearchPage - if (category == '搜索') { - return const ActiveSearchPage(); - } - // 分类标签跳转到分类页面 + // 分类标签显示分类页面 if (category == '分类') { return const CategoryPage(); } @@ -92,6 +119,10 @@ class _DiscoverPageState extends State if (category == '热门') { return const PopularPage(); } + // 搜索标签显示 ActiveSearchPage + if (category == '搜索') { + return const ActiveSearchPage(); + } // 活跃标签显示活跃统计页面 if (category == '活跃') { return const RatePage(); @@ -283,9 +314,11 @@ class _DiscoverPageState extends State Future _refreshContent() async { await Future.delayed(const Duration(seconds: 1)); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('内容已刷新'))); + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('内容已刷新'))); + } } void _showSearch() { diff --git a/lib/views/favorites_page.dart b/lib/views/favorites_page.dart index 5434141..87148ed 100644 --- a/lib/views/favorites_page.dart +++ b/lib/views/favorites_page.dart @@ -48,7 +48,7 @@ class _FavoritesPageState extends State Widget build(BuildContext context) { return Scaffold( appBar: TabbedNavAppBar.build( - title: '收藏', + title: '足迹', tabController: _tabController, tabLabels: _categories, tabBarScrollable: true, diff --git a/lib/views/footprint/all_list.dart b/lib/views/footprint/all_list.dart index 656fe13..5d823f6 100644 --- a/lib/views/footprint/all_list.dart +++ b/lib/views/footprint/all_list.dart @@ -142,9 +142,17 @@ class _AllListPageState extends State { return RefreshIndicator( onRefresh: _loadAllData, - child: ListView.builder( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 80), + child: ListView.separated( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 80), itemCount: _cards.length + 1, + separatorBuilder: (context, index) { + if (index == _cards.length) return const SizedBox.shrink(); + return Container( + height: 1, + color: Colors.black.withOpacity(0.1), + margin: const EdgeInsets.symmetric(vertical: 4), + ); + }, itemBuilder: (context, index) { if (index == _cards.length) { return _buildBottomIndicator(); @@ -210,7 +218,7 @@ class _AllListPageState extends State { // 构建点赞卡片 - 简洁紧凑样式 Widget _buildLikeCard(PoetryData poetry) { return Container( - margin: const EdgeInsets.only(bottom: 10), + margin: const EdgeInsets.only(bottom: 0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), @@ -372,7 +380,7 @@ class _AllListPageState extends State { } return Container( - margin: const EdgeInsets.only(bottom: 10), + margin: const EdgeInsets.only(bottom: 0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), diff --git a/lib/views/profile/app-info.dart b/lib/views/profile/app-info.dart index 584fc2b..b8b62ae 100644 --- a/lib/views/profile/app-info.dart +++ b/lib/views/profile/app-info.dart @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; import 'package:platform_info/platform_info.dart'; import 'package:flutter_udid/flutter_udid.dart'; import '../../../constants/app_constants.dart'; +import '../../../controllers/shared_preferences_storage_controller.dart'; /// 时间: 2026-03-26 /// 功能: 应用信息页面 @@ -21,11 +22,15 @@ class AppInfoPage extends StatefulWidget { class _AppInfoPageState extends State { String _udid = '获取中...'; + bool _isDeveloperMode = false; + int _tapCount = 0; + DateTime? _lastTapTime; @override void initState() { super.initState(); _loadUdid(); + _loadDeveloperMode(); } Future _loadUdid() async { @@ -45,6 +50,50 @@ class _AppInfoPageState extends State { } } + Future _loadDeveloperMode() async { + final isEnabled = await SharedPreferencesStorageController.getBool( + 'developer_mode_enabled', + defaultValue: false, + ); + if (mounted) { + setState(() { + _isDeveloperMode = isEnabled; + }); + } + } + + Future _saveDeveloperMode(bool enabled) async { + await SharedPreferencesStorageController.setBool( + 'developer_mode_enabled', + enabled, + ); + } + + void _onFrameworkTap() { + final now = DateTime.now(); + if (_lastTapTime != null && now.difference(_lastTapTime!).inSeconds > 2) { + _tapCount = 0; + } + _lastTapTime = now; + _tapCount++; + + if (_tapCount >= 5 && !_isDeveloperMode) { + setState(() { + _isDeveloperMode = true; + }); + _saveDeveloperMode(true); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('开发者模式激活'), + duration: Duration(seconds: 2), + ), + ); + } + _tapCount = 0; + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -64,6 +113,20 @@ class _AppInfoPageState extends State { icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), onPressed: () => Navigator.of(context).pop(), ), + actions: [ + if (_isDeveloperMode) + IconButton( + icon: const Icon(Icons.bug_report, color: Colors.green), + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('调试信息已激活'), + duration: Duration(seconds: 2), + ), + ); + }, + ), + ], ), body: ListView( padding: const EdgeInsets.all(16), @@ -177,9 +240,15 @@ class _AppInfoPageState extends State { color: Colors.white.withValues(alpha: 0.9), ), const SizedBox(width: 6), - const Text( - '框架 1.3', - style: TextStyle(fontSize: 12, color: Colors.white), + GestureDetector( + onTap: _onFrameworkTap, + child: const Text( + '框架 1.3', + style: TextStyle( + fontSize: 12, + color: Colors.white, + ), + ), ), const SizedBox(width: 8), Container( @@ -188,14 +257,21 @@ class _AppInfoPageState extends State { vertical: 2, ), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.2), + color: _isDeveloperMode + ? Colors.green.withValues(alpha: 0.3) + : Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), - child: const Text( + child: Text( '软件版本 1.5', style: TextStyle( fontSize: 10, - color: Colors.white, + color: _isDeveloperMode + ? Colors.green + : Colors.white, + fontWeight: _isDeveloperMode + ? FontWeight.bold + : FontWeight.normal, ), ), ), diff --git a/lib/views/profile/components/entire -page.dart b/lib/views/profile/components/entire -page.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/views/profile/components/entire_page.dart b/lib/views/profile/components/entire_page.dart new file mode 100644 index 0000000..e5c83db --- /dev/null +++ b/lib/views/profile/components/entire_page.dart @@ -0,0 +1,1024 @@ +/// 时间: 2026-04-01 +/// 功能: 全站统计页面 +/// 介绍: 展示网站统计数据,包括收录数量、热度统计、热门内容等 +/// 最新变化: 新建页面,iOS风格设计 +library; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../../../constants/app_constants.dart'; +import '../../../utils/http/http_client.dart'; +import '../../../services/network_listener_service.dart'; +import 'server_info_dialog.dart'; + +class EntirePage extends StatefulWidget { + const EntirePage({super.key}); + + @override + State createState() => _EntirePageState(); +} + +class _EntirePageState extends State + with NetworkListenerMixin, SingleTickerProviderStateMixin { + Map? _statsData; + String? _errorMessage; + late AnimationController _animationController; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeOut), + ); + _animationController.forward(); + _loadStatsData(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + Future _loadStatsData() async { + if (!mounted) return; + + setState(() { + _errorMessage = null; + }); + + startNetworkLoading('stats'); + + try { + final response = await HttpClient.get('app/stats.php'); + + if (!mounted) return; + + if (response.isSuccess && response.jsonData['ok'] == true) { + setState(() { + _statsData = response.jsonData['data'] as Map; + }); + sendRefreshEvent(); + } else { + setState(() { + _errorMessage = '加载失败:${response.message}'; + }); + } + } catch (e) { + if (!mounted) return; + + setState(() { + _errorMessage = '网络错误:$e'; + }); + } finally { + endNetworkLoading('stats'); + } + } + + Future _showServerInfo() async { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return const AlertDialog( + content: Row( + children: [ + CircularProgressIndicator(), + SizedBox(width: 16), + Text('正在检测网络状态...'), + ], + ), + ); + }, + ); + + try { + final response = await HttpClient.get('poe/load.php'); + + if (!mounted) return; + + Navigator.of(context).pop(); + + if (response.isSuccess) { + final data = response.jsonData; + if (data['status'] == 'success') { + ServerInfoDialog.show(context, data: data as Map?); + } else { + ServerInfoDialog.show(context); + } + } else { + ServerInfoDialog.show(context); + } + } catch (e) { + if (!mounted) return; + + Navigator.of(context).pop(); + ServerInfoDialog.show(context); + } + } + + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: SystemUiOverlayStyle.dark, + child: Scaffold( + backgroundColor: const Color(0xFFF2F2F7), + appBar: _buildAppBar(), + body: _buildBody(), + ), + ); + } + + PreferredSizeWidget _buildAppBar() { + return AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon( + Icons.arrow_back_ios, + color: AppConstants.primaryColor, + ), + onPressed: () => Navigator.pop(context), + ), + title: const Text( + '全站统计', + style: TextStyle( + color: Colors.black, + fontSize: 17, + fontWeight: FontWeight.w600, + ), + ), + centerTitle: true, + actions: [ + IconButton( + icon: const Icon( + Icons.info_outline, + color: AppConstants.primaryColor, + ), + onPressed: _showServerInfo, + tooltip: '服务器信息', + ), + ], + bottom: PreferredSize( + preferredSize: const Size.fromHeight(0.5), + child: Container(height: 0.5, color: const Color(0xFFE5E5EA)), + ), + ); + } + + Widget _buildBody() { + if (_errorMessage != null) { + return _buildErrorView(); + } + + if (_statsData == null) { + return _buildSkeletonView(); + } + + return _buildStatsContent(); + } + + Widget _buildSkeletonBox({ + double width = double.infinity, + double height = 16, + double radius = 8, + }) { + return Container( + width: width, + height: height, + decoration: BoxDecoration( + color: const Color(0xFFE5E5EA), + borderRadius: BorderRadius.circular(radius), + ), + ); + } + + Widget _buildSkeletonView() { + return FadeTransition( + opacity: _fadeAnimation, + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildSkeletonHeaderCard(), + const SizedBox(height: 16), + _buildSkeletonSection(), + const SizedBox(height: 16), + _buildSkeletonSection(), + const SizedBox(height: 16), + _buildSkeletonSection(), + const SizedBox(height: 16), + _buildSkeletonBuildTimeCard(), + const SizedBox(height: 32), + ], + ), + ); + } + + Widget _buildSkeletonHeaderCard() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFFE5E5EA), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + _buildSkeletonBox(width: 28, height: 28, radius: 14), + const SizedBox(width: 12), + _buildSkeletonBox(width: 100, height: 22), + ], + ), + const SizedBox(height: 12), + _buildSkeletonBox(width: 200, height: 14), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildSkeletonBox(width: 60, height: 40), + _buildSkeletonBox(width: 60, height: 40), + _buildSkeletonBox(width: 60, height: 40), + ], + ), + ], + ), + ); + } + + Widget _buildSkeletonSection() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + _buildSkeletonBox(width: 20, height: 20, radius: 10), + const SizedBox(width: 8), + _buildSkeletonBox(width: 80, height: 16), + ], + ), + const SizedBox(height: 16), + GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 3, + childAspectRatio: 1.0, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + children: List.generate(9, (index) => _buildSkeletonCountItem()), + ), + ], + ), + ); + } + + Widget _buildSkeletonCountItem() { + return Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFFF2F2F7), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Expanded( + flex: 2, + child: Row( + children: [ + Expanded( + child: _buildSkeletonBox( + width: double.infinity, + height: 28, + radius: 8, + ), + ), + const SizedBox(width: 8), + Expanded( + child: _buildSkeletonBox(width: double.infinity, height: 22), + ), + ], + ), + ), + const SizedBox(height: 4), + Expanded(flex: 1, child: _buildSkeletonBox(width: 50, height: 12)), + ], + ), + ); + } + + Widget _buildSkeletonBuildTimeCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + _buildSkeletonBox(width: 40, height: 40, radius: 10), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSkeletonBox(width: 60, height: 14), + const SizedBox(height: 4), + _buildSkeletonBox(width: 150, height: 16), + ], + ), + ), + ], + ), + ); + } + + Widget _buildErrorView() { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: const Color(0xFFFF3B30).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(32), + ), + child: const Icon( + Icons.error_outline, + color: Color(0xFFFF3B30), + size: 32, + ), + ), + const SizedBox(height: 16), + Text( + _errorMessage ?? '加载失败', + style: const TextStyle(color: Color(0xFF8E8E93), fontSize: 14), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _loadStatsData, + style: ElevatedButton.styleFrom( + backgroundColor: AppConstants.primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: const Text('重试'), + ), + ], + ), + ), + ); + } + + Widget _buildStatsContent() { + return FadeTransition( + opacity: _fadeAnimation, + child: RefreshIndicator( + color: AppConstants.primaryColor, + onRefresh: _loadStatsData, + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildHeaderCard(), + const SizedBox(height: 16), + _buildHotSection(), + const SizedBox(height: 16), + _buildCountSection(), + const SizedBox(height: 16), + _buildTopContentSection(), + const SizedBox(height: 16), + _buildBuildTimeCard(), + const SizedBox(height: 32), + ], + ), + ), + ); + } + + Widget _buildHeaderCard() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppConstants.primaryColor, + AppConstants.primaryColor.withValues(alpha: 0.8), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppConstants.primaryColor.withValues(alpha: 0.3), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.analytics, color: Colors.white, size: 28), + const SizedBox(width: 12), + const Text( + '情景诗词', + style: TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.refresh, color: Colors.white, size: 22), + onPressed: _loadStatsData, + tooltip: '刷新数据', + ), + ], + ), + const SizedBox(height: 12), + Text( + '诗意生活,触手可及', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.9), + fontSize: 14, + ), + ), + const SizedBox(height: 16), + Row( + children: [ + _buildHeaderStat( + '收录诗句', + _statsData?['count_site']?.toString() ?? '0', + ), + Container( + width: 1, + height: 40, + color: Colors.white.withValues(alpha: 0.3), + margin: const EdgeInsets.symmetric(horizontal: 20), + ), + _buildHeaderStat( + '累计热度', + _statsData?['cumulative_hits']?.toString() ?? '0', + ), + Container( + width: 1, + height: 40, + color: Colors.white.withValues(alpha: 0.3), + margin: const EdgeInsets.symmetric(horizontal: 20), + ), + _buildHeaderStat( + '累计点赞', + _statsData?['cumulative_likes']?.toString() ?? '0', + ), + ], + ), + ], + ), + ); + } + + Widget _buildHeaderStat(String label, String value) { + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + fontSize: 12, + ), + ), + ], + ), + ); + } + + Widget _buildCountSection() { + return _buildSection('数量统计', Icons.format_list_numbered, [ + _buildCountGrid(), + ]); + } + + Widget _buildCountGrid() { + final counts = [ + { + 'label': '项目', + 'value': _statsData?['count_category'] ?? 0, + 'icon': Icons.category, + 'color': const Color(0xFF007AFF), + 'showIcon': true, + }, + { + 'label': '收录诗句', + 'value': _statsData?['count_site'] ?? 0, + 'icon': Icons.article, + 'color': const Color(0xFF34C759), + 'showIcon': false, + }, + { + 'label': '审核中', + 'value': _statsData?['count_apply'] ?? 0, + 'icon': Icons.pending, + 'color': const Color(0xFFFF9500), + 'showIcon': true, + }, + { + 'label': '已拒审', + 'value': _statsData?['count_apply_reject'] ?? 0, + 'icon': Icons.block, + 'color': const Color(0xFFFF3B30), + 'showIcon': true, + }, + { + 'label': '每日一句', + 'value': _statsData?['count_article'] ?? 0, + 'icon': Icons.wb_sunny, + 'color': const Color(0xFF5856D6), + 'showIcon': true, + }, + { + 'label': '文章分类', + 'value': _statsData?['count_article_category'] ?? 0, + 'icon': Icons.folder, + 'color': const Color(0xFFAF52DE), + 'showIcon': true, + }, + { + 'label': '推送', + 'value': _statsData?['count_notice'] ?? 0, + 'icon': Icons.campaign, + 'color': const Color(0xFF32ADE6), + 'showIcon': true, + }, + { + 'label': '开发者', + 'value': _statsData?['count_link'] ?? 0, + 'icon': Icons.people, + 'color': const Color(0xFFFF2D55), + 'showIcon': true, + }, + { + 'label': '分类标签', + 'value': _statsData?['count_tags'] ?? 0, + 'icon': Icons.label, + 'color': const Color(0xFF64D2FF), + 'showIcon': false, + }, + ]; + + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: 1.0, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: counts.length, + itemBuilder: (context, index) { + final item = counts[index]; + return _buildCountItem( + item['label'] as String, + item['value'].toString(), + item['icon'] as IconData, + item['color'] as Color, + item['showIcon'] as bool, + ); + }, + ); + } + + Widget _buildCountItem( + String label, + String value, + IconData icon, + Color color, + bool showIcon, + ) { + return Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + // 上行:icon和数据,比例2:1(有icon时1:1,无icon时数据占满) + Expanded( + flex: 2, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (showIcon) ...[ + Expanded( + child: Container( + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: color, size: 24), + ), + ), + const SizedBox(width: 8), + ], + Expanded( + child: Text( + value, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + const SizedBox(height: 4), + // 下行:描述 + Expanded( + flex: 1, + child: Center( + child: Text( + label, + style: const TextStyle(fontSize: 12, color: Color(0xFF3C3C43)), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + ); + } + + Widget _buildHotSection() { + return _buildSection('热度统计', Icons.trending_up, [ + _buildHotItem( + '累计热度', + _statsData?['cumulative_hits']?.toString() ?? '0', + Icons.local_fire_department, + const Color(0xFFFF9500), + ), + const SizedBox(height: 12), + _buildHotItem( + '累计点赞', + _statsData?['cumulative_likes']?.toString() ?? '0', + Icons.favorite, + const Color(0xFFFF2D55), + ), + ]); + } + + Widget _buildHotItem(String label, String value, IconData icon, Color color) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: color, size: 24), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF8E8E93), + ), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildTopContentSection() { + return _buildSection('热门内容', Icons.star, [ + _buildTopContentItem( + '今日热门', + _statsData?['top_hits_day'], + Icons.today, + const Color(0xFFFF9500), + ), + const SizedBox(height: 12), + _buildTopContentItem( + '本月热门', + _statsData?['top_hits_month'], + Icons.calendar_month, + const Color(0xFF007AFF), + ), + const SizedBox(height: 12), + _buildTopContentItem( + '历史最热', + _statsData?['top_hits_total'], + Icons.history, + const Color(0xFF5856D6), + ), + const SizedBox(height: 12), + _buildTopContentItem( + '最高点赞', + _statsData?['top_like'], + Icons.thumb_up, + const Color(0xFF34C759), + ), + ]); + } + + Widget _buildTopContentItem( + String label, + dynamic data, + IconData icon, + Color color, + ) { + final hasData = data != null && data is Map; + final content = hasData ? data['name']?.toString() ?? '暂无数据' : '暂无数据'; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: color, size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + const SizedBox(height: 8), + Text( + content, + style: TextStyle( + fontSize: 13, + color: hasData + ? const Color(0xFF3C3C43) + : const Color(0xFF8E8E93), + height: 1.5, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildBuildTimeCard() { + final buildTime = _statsData?['build_time']?.toString() ?? '未知'; + + int days = 0; + try { + final buildDate = DateTime.parse(buildTime); + final now = DateTime.now(); + days = now.difference(buildDate).inDays; + if (days < 0) days = 0; + } catch (e) { + days = 0; + } + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppConstants.primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.cake, + color: AppConstants.primaryColor, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '建站时间', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + buildTime, + style: const TextStyle( + fontSize: 16, + color: Color(0xFF3C3C43), + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: AppConstants.primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '已运行 $days 天', + style: const TextStyle( + fontSize: 12, + color: AppConstants.primaryColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildSection(String title, IconData icon, List children) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon(icon, color: AppConstants.primaryColor, size: 20), + const SizedBox(width: 8), + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/profile/components/pop-menu.dart b/lib/views/profile/components/pop-menu.dart index 46270ee..02b910c 100644 --- a/lib/views/profile/components/pop-menu.dart +++ b/lib/views/profile/components/pop-menu.dart @@ -148,7 +148,7 @@ class PopMenu extends StatelessWidget { }), _buildBottomSheetItem( context, - '屏幕常亮', + '使用教程', Icons.screen_lock_rotation, () => toggleScreenWake(context), ), diff --git a/lib/views/profile/components/server_info_dialog.dart b/lib/views/profile/components/server_info_dialog.dart new file mode 100644 index 0000000..6d59199 --- /dev/null +++ b/lib/views/profile/components/server_info_dialog.dart @@ -0,0 +1,359 @@ +library; + +import 'package:flutter/material.dart'; +import '../../../constants/app_constants.dart'; + +class ServerInfoDialog { + static Future show(BuildContext context, {Map? data}) { + final server = data?['server'] as Map?; + final network = data?['network'] as Map?; + final timestamp = data?['timestamp'] as Map?; + + final load = server?['load'] as Map?; + final latency = network?['latency'] as List?; + final serverResponseTime = network?['server_response_time']; + + return showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Container( + constraints: const BoxConstraints(maxWidth: 340), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppConstants.primaryColor, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.cloud_outlined, + color: Colors.white, + size: 20, + ), + ), + const SizedBox(width: 12), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '广州 server-ls', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 2), + Text( + '服务器信息', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + _buildInfoCard( + icon: Icons.schedule, + iconColor: const Color(0xFF007AFF), + title: '服务器时间', + content: timestamp?['datetime'] ?? '--', + ), + const SizedBox(height: 12), + _buildInfoCard( + icon: Icons.speed, + iconColor: const Color(0xFF34C759), + title: '服务器负载', + content: + '1分钟: ${_formatLoad(load?['1min'])}\n5分钟: ${_formatLoad(load?['5min'])}\n15分钟: ${_formatLoad(load?['15min'])}', + ), + const SizedBox(height: 12), + _buildInfoCard( + icon: Icons.bolt, + iconColor: const Color(0xFFFF9500), + title: '服务器响应', + content: '${serverResponseTime ?? '--'} ms', + trailing: _buildResponseTimeIndicator( + serverResponseTime, + ), + ), + if (latency != null && latency.isNotEmpty) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFF2F2F7), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.public, + size: 16, + color: Colors.grey[600], + ), + const SizedBox(width: 8), + Text( + '网络延迟', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: Colors.grey[700], + ), + ), + ], + ), + const SizedBox(height: 12), + ...latency.map((item) { + final host = item['host'] as String?; + final ip = item['ip'] as String?; + final lat = item['latency']; + final status = item['status'] as String?; + final isOnline = status == 'online'; + + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: isOnline + ? const Color(0xFF34C759) + : const Color(0xFFFF3B30), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + host ?? '--', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + Text( + ip ?? '--', + style: TextStyle( + fontSize: 11, + color: Colors.grey[500], + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: isOnline + ? const Color( + 0xFF34C759, + ).withValues(alpha: 0.1) + : const Color( + 0xFFFF3B30, + ).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular( + 8, + ), + ), + child: Text( + isOnline ? '$lat ms' : '离线', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: isOnline + ? const Color(0xFF34C759) + : const Color(0xFFFF3B30), + ), + ), + ), + ], + ), + ); + }), + ], + ), + ), + ], + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), + child: SizedBox( + width: double.infinity, + child: TextButton( + onPressed: () => Navigator.of(context).pop(), + style: TextButton.styleFrom( + backgroundColor: const Color(0xFFF2F2F7), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + '关闭', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFF007AFF), + ), + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + static Widget _buildInfoCard({ + required IconData icon, + required Color iconColor, + required String title, + required String content, + Widget? trailing, + }) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFF2F2F7), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: iconColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: iconColor, size: 18), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + const SizedBox(height: 2), + Text( + content, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + if (trailing != null) trailing, + ], + ), + ); + } + + static String _formatLoad(dynamic value) { + if (value == null) return '--'; + + double? loadValue; + if (value is num) { + loadValue = value.toDouble(); + } else if (value is String) { + loadValue = double.tryParse(value); + } + + if (loadValue == null) return '--'; + + final percentage = (loadValue * 100).round(); + return '$percentage%'; + } + + static Widget _buildResponseTimeIndicator(dynamic responseTime) { + int? time; + if (responseTime is int) { + time = responseTime; + } else if (responseTime is String) { + time = int.tryParse(responseTime); + } + + if (time == null) { + return const SizedBox.shrink(); + } + + Color color; + String label; + if (time < 100) { + color = const Color(0xFF34C759); + label = '极快'; + } else if (time < 300) { + color = const Color(0xFF34C759); + label = '快速'; + } else if (time < 500) { + color = const Color(0xFFFF9500); + label = '正常'; + } else { + color = const Color(0xFFFF3B30); + label = '较慢'; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + label, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: color, + ), + ), + ); + } +} diff --git a/lib/views/profile/profile_page.dart b/lib/views/profile/profile_page.dart index 200aa2e..5b0b8e1 100644 --- a/lib/views/profile/profile_page.dart +++ b/lib/views/profile/profile_page.dart @@ -32,6 +32,7 @@ import 'expand/vote.dart'; import 'expand/manu-script.dart'; import 'components/bug_list_page.dart'; import 'components/pop-menu.dart'; +import 'components/entire_page.dart'; class ProfilePage extends StatefulWidget { const ProfilePage({super.key}); @@ -612,7 +613,10 @@ class _ProfilePageState extends State _buildSettingsItem( '查看全站统计', Icons.history, - () => _showSnackBar('查看全站统计'), + () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const EntirePage()), + ), ), _buildSettingsItem( '开发计划', diff --git a/lib/views/profile/settings/offline-data.dart b/lib/views/profile/settings/offline-data.dart index 2619419..c79a657 100644 --- a/lib/views/profile/settings/offline-data.dart +++ b/lib/views/profile/settings/offline-data.dart @@ -5,6 +5,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../../../constants/app_constants.dart'; import '../../../utils/http/http_client.dart'; import 'user-plan.dart'; +import '../components/server_info_dialog.dart'; /// 时间: 2026-03-29 /// 功能: 离线数据管理页面 @@ -504,95 +505,7 @@ class _OfflineDataPageState extends State { } void _displayServerInfoDialog(Map data) { - final server = data['server'] as Map?; - final network = data['network'] as Map?; - final timestamp = data['timestamp'] as Map?; - - final load = server?['load'] as Map?; - final latency = network?['latency'] as List?; - final serverResponseTime = network?['server_response_time']; - - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Row( - children: [ - Icon(Icons.cloud, color: AppConstants.primaryColor, size: 20), - const SizedBox(width: 8), - const Text('服务器信息', style: TextStyle(fontSize: 18)), - ], - ), - content: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - '广州 server-ls', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - const SizedBox(height: 12), - _buildInfoSection('⏰ 服务器时间', timestamp?['datetime'] ?? '--'), - const SizedBox(height: 12), - _buildInfoSection( - '📊 服务器负载', - '1分钟: ${load?['1min'] ?? '--'} | 5分钟: ${load?['5min'] ?? '--'} | 15分钟: ${load?['15min'] ?? '--'}', - ), - const SizedBox(height: 12), - _buildInfoSection( - '⚡ 服务器响应', - '${serverResponseTime ?? '--'} ms', - ), - const SizedBox(height: 16), - const Text( - '🌐 网络延迟', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), - ), - const SizedBox(height: 8), - if (latency != null) - ...latency.map((item) { - final host = item['host'] as String?; - final ip = item['ip'] as String?; - final lat = item['latency']; - final status = item['status'] as String?; - return Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Text( - '• $host ($ip): ${status == 'online' ? '$lat ms' : '离线'}', - style: const TextStyle( - fontSize: 12, - color: Colors.grey, - ), - ), - ); - }).toList(), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('关闭'), - ), - ], - ); - }, - ); - } - - Widget _buildInfoSection(String title, String content) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), - ), - const SizedBox(height: 4), - Text(content, style: const TextStyle(fontSize: 12, color: Colors.grey)), - ], - ); + ServerInfoDialog.show(context, data: data); } @override diff --git a/lib/widgets/tabbed_nav_app_bar.dart b/lib/widgets/tabbed_nav_app_bar.dart index 969f27f..af6ae20 100644 --- a/lib/widgets/tabbed_nav_app_bar.dart +++ b/lib/widgets/tabbed_nav_app_bar.dart @@ -21,8 +21,14 @@ class TabbedNavAppBar { EdgeInsetsGeometry? tabLabelPadding, }) { return AppBar( - toolbarHeight: AppConstants.tabbedPageToolbarHeight, - title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), + title: Text( + title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Colors.black87, + ), + ), backgroundColor: Colors.white, foregroundColor: Colors.black87, iconTheme: const IconThemeData(color: Colors.black87), @@ -33,34 +39,21 @@ class TabbedNavAppBar { centerTitle: true, leading: leading, actions: actions, - // 用 PreferredSize + 固定高度略小于默认 Tab 行,减少两页相同的「标题+Tab」总高度(当前 SDK 无 tabHeight 参数) - bottom: PreferredSize( - preferredSize: Size.fromHeight(AppConstants.tabbedPageTabBarHeight), - child: SizedBox( - height: AppConstants.tabbedPageTabBarHeight, - child: TabBar( - controller: tabController, - isScrollable: tabBarScrollable, - padding: - tabPadding ?? - (tabBarScrollable - ? const EdgeInsets.only( - left: AppConstants.pageHorizontalPadding, - right: 8, - ) - : null), - labelPadding: tabLabelPadding, - tabAlignment: tabBarScrollable ? TabAlignment.start : null, - dividerHeight: 0, - dividerColor: Colors.transparent, - tabs: tabLabels.map((String e) => Tab(text: e)).toList(), - labelColor: AppConstants.primaryColor, - unselectedLabelColor: Colors.grey[600], - indicatorColor: AppConstants.primaryColor, - indicatorWeight: 3, - labelStyle: const TextStyle(fontWeight: FontWeight.bold), - ), + bottom: TabBar( + controller: tabController, + isScrollable: tabBarScrollable, + padding: tabPadding, + labelPadding: tabLabelPadding, + tabAlignment: tabBarScrollable ? TabAlignment.start : null, + dividerHeight: 0, + dividerColor: Colors.transparent, + tabs: tabLabels.map((String e) => Tab(text: e)).toList(), + labelColor: AppConstants.primaryColor, + unselectedLabelColor: Colors.grey[600], + indicator: UnderlineTabIndicator( + borderSide: BorderSide(color: AppConstants.primaryColor, width: 3), ), + labelStyle: const TextStyle(fontWeight: FontWeight.bold), ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index 5d820b6..73e1036 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.1+2 +version: 1.3.12+3 environment: sdk: ^3.9.2