Compare commits
3 Commits
ab9961853d
...
91ef47f991
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91ef47f991 | ||
|
|
79f7269319 | ||
|
|
6517a78c7e |
341
CHANGELOG.md
341
CHANGELOG.md
@@ -4,219 +4,148 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [1.3.10] - 2026-03-31
|
||||
## [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
|
||||
|
||||
### 新增
|
||||
- 💡 **屏幕常亮功能**
|
||||
- 在个人设置页面添加了屏幕常亮开关
|
||||
- 实现了开关状态的管理
|
||||
- 添加了 OLED 屏幕提示对话框,告知用户可能的屏幕老化风险
|
||||
- 支持 OpenHarmony 平台
|
||||
- ✨ **新增全站统计页面**
|
||||
- 创建了iOS风格的全站统计页面,展示网站统计数据
|
||||
- 页面包含:收录数量、热度统计、热门内容、建站时间等
|
||||
- 支持下拉刷新,实时获取最新统计数据
|
||||
- 使用主题色设计,与整体应用风格统一
|
||||
- 添加了网络状态检测和错误处理
|
||||
- 涉及文件:
|
||||
- `lib/views/profile/profile_page.dart` - 添加屏幕常亮开关和功能实现
|
||||
- `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/pop-menu.dart` - 修改弹出菜单项
|
||||
- `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
|
||||
|
||||
### 修复
|
||||
- 🐛 **修复 Web 平台兼容性问题**
|
||||
- 修复了 `wakelock_plus` 库在 Web 平台上不可用的问题(`dart:ffi` 在 Web 平台上不可用)
|
||||
- 在 Web 平台上禁用了屏幕常亮功能,并显示相应的提示信息
|
||||
- 在 Web 平台上隐藏了屏幕常亮设置项
|
||||
- 涉及文件:
|
||||
- `lib/views/profile/profile_page.dart` - 添加 Web 平台检查
|
||||
- `lib/views/profile/components/pop-menu.dart` - 添加 Web 平台检查
|
||||
|
||||
---
|
||||
|
||||
### 修复
|
||||
- 优化了屏幕常亮功能的错误处理
|
||||
- 增强了平台检测和日志输出
|
||||
- 为不支持屏幕常亮的设备添加了专门的错误提示
|
||||
- 🐛 **修复昵称修改不生效问题**
|
||||
- 修复了点击确认图标后昵称没有保存的问题
|
||||
- 确保在编辑模式下点击确认图标时正确保存昵称到 `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
|
||||
- <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卡片切换祝福语功能、优化开关布局、状态切换时显示气泡消息提示
|
||||
|
||||
### 开发进度
|
||||
- 🏗️ **HarmonyOS桌面小组件** - 开发中,包含2x2布局、天气显示、诗句展示等功能
|
||||
- 优先级:3
|
||||
getx 加入
|
||||
二维码能力
|
||||
HarmonyOS HongMeng Kernel
|
||||
421
README.md
421
README.md
@@ -1,37 +1,410 @@
|
||||
# 无书 (wushu)
|
||||
# 情景诗词
|
||||
|
||||
一款 Flutter 跨平台诗词阅读应用。
|
||||
一款优雅的 Flutter 跨平台诗词阅读应用,融合中国传统文化与现代科技,让诗词之美触手可及。
|
||||
|
||||
## 功能特点
|
||||
***
|
||||
|
||||
- 📖 诗词阅读与欣赏
|
||||
- ❤️ 收藏喜欢的诗词
|
||||
- 📝 足迹记录阅读历史
|
||||
- 🔍 搜索诗词功能
|
||||
- 📱 支持多平台 (Android, iOS, Web, Windows, macOS, 鸿蒙)
|
||||
## 📖 软件介绍
|
||||
|
||||
## 开始使用
|
||||
情景诗词是一款专注于中国古典诗词阅读与欣赏的移动应用,采用 Flutter 框架开发,支持 Android、iOS、Web、Windows、macOS 和鸿蒙多平台。应用以"情景"为核心,通过精美的卡片设计、智能推荐和丰富的互动功能,让用户在碎片化时间中感受诗词的魅力。
|
||||
|
||||
本项目是一个 Flutter 应用。
|
||||
## 🎯 软件 Slogan
|
||||
|
||||
### 环境要求
|
||||
**诗意生活,触手可及**
|
||||
|
||||
- Flutter SDK
|
||||
- Dart SDK
|
||||
## ✨ 一句话介绍
|
||||
|
||||
### 运行项目
|
||||
一款融合中国传统文化与现代科技的诗词阅读应用,让诗词之美触手可及。
|
||||
|
||||
```bash
|
||||
flutter pub get
|
||||
flutter run
|
||||
```
|
||||
## 📝 一段话介绍
|
||||
|
||||
## 项目结构
|
||||
情景诗词是一款优雅的 Flutter 跨平台诗词阅读应用,致力于将中国古典诗词以现代方式呈现给用户。应用采用精美的卡片式设计,支持多种主题风格,提供诗词阅读、收藏、搜索、答题等丰富功能。无论是晨起时的一句"床前明月光",还是午后的一首"春眠不觉晓",都能让您在快节奏的生活中找到片刻的诗意与宁静。应用支持离线模式,让您随时随地都能享受诗词之美,真正实现诗意生活,触手可及。
|
||||
|
||||
- `lib/` - 主要源代码
|
||||
- `assets/` - 资源文件
|
||||
- `android/`, `ios/`, `web/`, `windows/`, `macos/`, `ohos/` - 各平台配置
|
||||
## 🔑 关键字
|
||||
|
||||
## 许可证
|
||||
诗词、古典文学、中国文化、阅读、收藏、答题、离线、跨平台、Flutter、鸿蒙、iOS、Android、传统文化、诗词欣赏、诗词学习
|
||||
|
||||
MIT License
|
||||
***
|
||||
|
||||
## 🌟 主要功能
|
||||
|
||||
### <20> 诗词阅读
|
||||
|
||||
- **精美卡片展示**:采用经典、现代、毛玻璃三种主题风格,支持自定义颜色和圆角
|
||||
- **智能推荐**:根据时间和情景推荐合适的诗词
|
||||
- **诗词详情**:显示诗词标题、作者、朝代、正文、注释和赏析
|
||||
- **关键词标签**:展示诗词主题标签,方便分类浏览
|
||||
- **精选诗句**:突出显示诗词中的经典名句
|
||||
|
||||
### ❤️ 收藏功能
|
||||
|
||||
- **收藏诗词**:一键收藏喜欢的诗词
|
||||
- **收藏管理**:在收藏页面查看和管理所有收藏的诗词
|
||||
- **收藏统计**:显示收藏数量和收藏时间
|
||||
|
||||
### 🔍 搜索功能
|
||||
|
||||
- **关键词搜索**:通过关键词搜索诗词
|
||||
- **分类浏览**:按朝代、作者、主题等分类浏览
|
||||
- **热门诗词**:查看最受欢迎的诗词
|
||||
- **活跃排行**:查看活跃度最高的诗词
|
||||
|
||||
### 📝 足迹记录
|
||||
|
||||
- **阅读历史**:自动记录阅读过的诗词
|
||||
- **浏览统计**:统计今日浏览、本周浏览、累计浏览等数据
|
||||
- **使用天数**:记录首次使用时间和累计使用天数
|
||||
- **数据占用**:显示应用数据占用的存储空间
|
||||
|
||||
### 🎮 诗词答题
|
||||
|
||||
- **答题挑战**:参与诗词答题挑战,测试诗词知识
|
||||
- **题目随机化**:使用 Fisher-Yates 算法随机打乱题目顺序
|
||||
- **答题记录**:记录答题历史和成绩
|
||||
- **答题统计**:统计今日答题、累计答题、正确率等数据
|
||||
- **提示功能**:遇到困难时可以获取提示
|
||||
|
||||
### 🌐 离线模式
|
||||
|
||||
- **离线数据下载**:支持下载诗词和答题数据到本地
|
||||
- **下载选项**:可选择下载 20/30/60/100 条诗词或答题
|
||||
- **后台下载**:返回上一页后继续后台下载
|
||||
- **缓存管理**:清空缓存时可选择清空内容
|
||||
- **自动切换**:无网络时自动切换到离线模式
|
||||
|
||||
### 🎨 个性化设置
|
||||
|
||||
- **主题切换**:支持浅色/深色主题,跟随系统设置
|
||||
- **卡片样式**:经典、现代、毛玻璃三种样式可选
|
||||
- **颜色自定义**:自定义主题颜色和背景颜色
|
||||
- **圆角调整**:调整卡片圆角大小
|
||||
- **字体大小**:调整字体大小
|
||||
|
||||
### ⚙️ 功能设置
|
||||
|
||||
- **自动刷新**:开启后自动刷新诗词内容
|
||||
- **预加载**:预加载下一条诗词,提升加载速度
|
||||
- **声音反馈**:开启后播放配音
|
||||
- **震动反馈**:开启后提供震动反馈
|
||||
- **全局Tips**:显示/隐藏提示信息
|
||||
- **隐藏次要按钮**:隐藏主页的"上一条"和"分享"按钮
|
||||
- **屏幕常亮**:保持屏幕常亮,方便阅读
|
||||
|
||||
### 👤 个人中心
|
||||
|
||||
- **个人信息**:编辑昵称、头像等个人信息
|
||||
- **统计数据**:显示今日浏览、本周浏览、累计浏览、今日点赞、今日答题等统计
|
||||
- **数据隐藏**:可隐藏统计和答题数据
|
||||
- **用户计划**:加入用户体验计划,享受更多功能
|
||||
- **应用信息**:查看应用版本、设备信息、UDID 等
|
||||
|
||||
### 📸 分享功能
|
||||
|
||||
- **诗词分享**:将诗词生成精美图片分享给朋友
|
||||
- **软件分享**:分享软件给朋友
|
||||
- **复制功能**:复制诗词内容、UDID、QQ群号等
|
||||
|
||||
### 🗳️ 投票功能
|
||||
|
||||
- **用户投票**:参与软件功能投票,影响软件发展方向
|
||||
- **投票结果**:查看投票结果和统计
|
||||
- **登录注册**:支持用户登录和注册
|
||||
|
||||
### <20> 投稿功能
|
||||
|
||||
- **诗词投稿**:向软件投稿诗词,帮助软件完善内容
|
||||
- **投稿记录**:查看历史投稿记录
|
||||
- **相似度检测**:防止重复投稿
|
||||
|
||||
### 🌤️ 天气功能
|
||||
|
||||
- **天气显示**:在卡片上显示当前天气信息
|
||||
- **城市显示**:显示所在城市名称
|
||||
- **十二时辰**:使用中国十二时辰制显示时间
|
||||
|
||||
### 🐛 Bug 反馈
|
||||
|
||||
- **已知 Bug 列表**:查看已知 Bug 和解决方案
|
||||
- **用户反馈**:向开发者反馈问题和建议
|
||||
- **功能建议**:提交功能建议
|
||||
|
||||
### 📱 多平台支持
|
||||
|
||||
- **Android**:支持 Android 8.0+ 系统
|
||||
- **iOS**:支持 iOS 16.0+ 系统
|
||||
- **Web**:支持现代浏览器
|
||||
- **Windows**:支持 Windows 11 系统
|
||||
- **macOS**:支持 macOS 10.15+ 系统
|
||||
- **鸿蒙**:支持 HarmonyOS 5.0+ 系统
|
||||
|
||||
***
|
||||
|
||||
## 🗺️ 页面导航
|
||||
|
||||
### 主页
|
||||
|
||||
- **诗词卡片**:显示当前诗词,支持点赞、分享、上一条、下一条操作
|
||||
- **悬浮按钮**:分享按钮(生成图片分享)、上一条按钮
|
||||
- **下拉刷新**:下拉刷新诗词内容
|
||||
- **长按复制**:长按诗词卡片复制内容
|
||||
- **提示信息**:显示"点击任意区域加载下一条,长按复制,下拉刷新"
|
||||
|
||||
### 发现页
|
||||
|
||||
- **分类浏览**:按朝代、作者、主题等分类浏览诗词
|
||||
- **热门诗词**:查看最受欢迎的诗词
|
||||
- **活跃排行**:查看活跃度最高的诗词
|
||||
- **搜索功能**:搜索诗词
|
||||
|
||||
### 收藏页
|
||||
|
||||
- **收藏列表**:显示所有收藏的诗词
|
||||
- **取消收藏**:取消收藏诗词
|
||||
- **收藏统计**:显示收藏数量
|
||||
|
||||
### 个人页
|
||||
|
||||
- **个人卡片**:显示头像、昵称、等级、UEP 标识
|
||||
- **统计卡片**:显示今日浏览、本周浏览、累计浏览、今日点赞、今日答题等统计
|
||||
- **功能入口**:设置、了解我们、权限管理、应用信息、数据管理、帮助中心等
|
||||
|
||||
### 设置页
|
||||
|
||||
- **功能设置**:自动刷新、调试信息、预加载、隐藏次要按钮、声音反馈、震动反馈、全局Tips、屏幕常亮
|
||||
- **卡片设置**:卡片样式、主题颜色、背景颜色、圆角大小、字体大小
|
||||
- **离线数据**:下载离线数据、清空缓存、查看缓存状态
|
||||
- **用户计划**:加入用户体验计划
|
||||
|
||||
### 了解我们页
|
||||
|
||||
- **官方网站**:访问官方网站
|
||||
- **QQ交流群**:复制 QQ 群号加入交流群
|
||||
- **开源协议**:查看开源协议
|
||||
- **开发团队**:了解开发团队
|
||||
|
||||
### 权限管理页
|
||||
|
||||
- **权限说明**:说明应用需要的权限
|
||||
- **沙盒机制**:说明应用的沙盒机制
|
||||
- **用户反馈**:反馈问题和建议
|
||||
- **功能建议**:提交功能建议
|
||||
|
||||
### 应用信息页
|
||||
|
||||
- **应用信息**:应用名称、版本号、包名等
|
||||
- **设备信息**:设备型号、系统版本、UDID 等
|
||||
- **技术栈**:显示使用的技术栈
|
||||
- **开源协议**:查看开源协议
|
||||
|
||||
### 数据管理页
|
||||
|
||||
- **数据统计**:显示数据占用情况
|
||||
- **清空数据**:清空应用数据
|
||||
- **数据备份**:备份应用数据
|
||||
|
||||
### 帮助中心页
|
||||
|
||||
- **使用指南**:查看使用指南
|
||||
- **常见问题**:查看常见问题解答
|
||||
- **联系我们**:联系开发者
|
||||
|
||||
### 投票页
|
||||
|
||||
- **投票列表**:查看可参与的投票
|
||||
- **投票详情**:查看投票详情和选项
|
||||
- **提交投票**:提交投票
|
||||
|
||||
### 投稿页
|
||||
|
||||
- **投稿表单**:填写诗词投稿信息
|
||||
- **投稿记录**:查看历史投稿记录
|
||||
- **清空记录**:清空投稿记录
|
||||
|
||||
### Bug 列表页
|
||||
|
||||
- **Bug 列表**:查看已知 Bug
|
||||
- **解决方案**:查看 Bug 的解决方案
|
||||
- **Bug 状态**:查看 Bug 的修复状态
|
||||
|
||||
***
|
||||
|
||||
## 📖 使用说明
|
||||
|
||||
### 首次使用
|
||||
|
||||
1. 下载并安装应用
|
||||
2. 打开应用,阅读并同意用户协议
|
||||
3. 进入主页,开始浏览诗词
|
||||
|
||||
### 浏览诗词
|
||||
|
||||
1. **加载诗词**:点击诗词卡片任意区域加载下一条诗词
|
||||
2. **查看详情**:诗词卡片显示标题、作者、朝代、正文、注释和赏析
|
||||
3. **点赞诗词**:点击心形图标点赞诗词
|
||||
4. **分享诗词**:点击分享按钮生成图片并分享
|
||||
5. **切换诗词**:点击"上一条"或"下一条"按钮切换诗词
|
||||
|
||||
### 收藏诗词
|
||||
|
||||
1. 在诗词卡片上点击收藏图标
|
||||
2. 在收藏页查看所有收藏的诗词
|
||||
3. 点击收藏图标取消收藏
|
||||
|
||||
### 搜索诗词
|
||||
|
||||
1. 进入发现页
|
||||
2. 点击搜索框输入关键词
|
||||
3. 选择分类浏览诗词
|
||||
4. 查看热门诗词和活跃排行
|
||||
|
||||
### 参与答题
|
||||
|
||||
1. 在诗词卡片上点击答题按钮
|
||||
2. 查看题目和选项
|
||||
3. 选择答案并提交
|
||||
4. 查看答题结果和提示
|
||||
|
||||
### 下载离线数据
|
||||
|
||||
1. 进入设置页
|
||||
2. 点击"离线数据"
|
||||
3. 选择下载类型和数量
|
||||
4. 点击"开始下载"
|
||||
5. 等待下载完成
|
||||
|
||||
### 个性化设置
|
||||
|
||||
1. 进入设置页
|
||||
2. 点击"卡片设置"
|
||||
3. 选择卡片样式、主题颜色、背景颜色、圆角大小、字体大小
|
||||
4. 点击"功能设置"调整功能开关
|
||||
|
||||
### 查看统计数据
|
||||
|
||||
1. 进入个人页
|
||||
2. 查看统计卡片中的数据
|
||||
3. 点击隐藏按钮隐藏统计数据
|
||||
|
||||
### 投稿诗词
|
||||
|
||||
1. 进入个人页
|
||||
2. 点击"诗词投稿"
|
||||
3. 填写投稿表单
|
||||
4. 点击"提交投稿"
|
||||
|
||||
### 参与投票
|
||||
|
||||
1. 进入个人页
|
||||
2. 点击"功能投票"
|
||||
3. 选择投票并提交
|
||||
|
||||
### 反馈问题
|
||||
|
||||
1. 进入个人页
|
||||
2. 点击"权限管理"
|
||||
3. 点击"用户反馈"或"功能建议"
|
||||
4. 填写反馈内容
|
||||
|
||||
***
|
||||
|
||||
## 🚀 未来开发功能
|
||||
|
||||
### 🏗️ HarmonyOS 桌面小组件
|
||||
|
||||
- 2x2 布局小组件
|
||||
- 天气显示
|
||||
- 诗词展示
|
||||
- 优先级:3
|
||||
|
||||
### 🎯 GetX 状态管理
|
||||
|
||||
- 引入 GetX 框架进行状态管理
|
||||
- 优化应用性能
|
||||
- 优先级:4
|
||||
|
||||
### 📱 二维码能力
|
||||
|
||||
- 生成二维码分享应用
|
||||
- 扫描二维码添加好友
|
||||
- 优先级:3
|
||||
|
||||
### 🌟 HarmonyOS HongMeng Kernel
|
||||
|
||||
- 深度适配鸿蒙内核
|
||||
- 优化鸿蒙平台性能
|
||||
- 优先级:4
|
||||
|
||||
### 🎨 更多主题样式
|
||||
|
||||
- 新增更多卡片主题样式
|
||||
- 支持自定义主题
|
||||
- 优先级:3
|
||||
|
||||
### 📊 数据可视化
|
||||
|
||||
- 统计数据可视化展示
|
||||
- 阅读习惯分析
|
||||
- 优先级:4
|
||||
|
||||
### 🤖 AI 智能推荐
|
||||
|
||||
- 基于 AI 的诗词智能推荐
|
||||
- 个性化内容推荐
|
||||
- 优先级:5
|
||||
|
||||
### 🎵 诗词朗诵
|
||||
|
||||
- 添加诗词朗诵音频
|
||||
- 支持多种朗诵风格
|
||||
- 优先级:4
|
||||
|
||||
### 📚 诗词学习
|
||||
|
||||
- 添加诗词学习课程
|
||||
- 诗词知识问答
|
||||
- 优先级:4
|
||||
|
||||
### 👥 社交功能
|
||||
|
||||
- 添加好友功能
|
||||
- 诗词分享到社交平台
|
||||
- 优先级:5
|
||||
|
||||
***
|
||||
|
||||
## 📖 一篇小文章
|
||||
|
||||
### 诗意生活,触手可及——情景诗词,让古典诗词走进现代生活
|
||||
|
||||
在这个快节奏的数字时代,我们常常被各种信息轰炸,很少有时间静下心来品味古典诗词的韵味。然而,中国古典诗词作为中华文化的瑰宝,承载着千年的智慧和情感,值得我们去细细品味。
|
||||
|
||||
情景诗词应用应运而生,它将中国古典诗词与现代科技完美融合,让诗词之美触手可及。无论您是在清晨的地铁上,还是在午后的咖啡馆里,只需打开应用,就能欣赏到精美的诗词卡片,感受诗人的情感与智慧。
|
||||
|
||||
应用采用 Flutter 框架开发,支持 Android、iOS、Web、Windows、macOS 和鸿蒙多平台,让用户在任何设备上都能享受诗词之美。精美的卡片设计,经典、现代、毛玻璃三种主题风格,让诗词阅读成为一种视觉享受。
|
||||
|
||||
应用不仅提供诗词阅读功能,还支持收藏、搜索、答题等丰富功能。用户可以收藏喜欢的诗词,通过关键词搜索诗词,参与诗词答题挑战,测试自己的诗词知识。离线模式让用户随时随地都能享受诗词之美,真正实现诗意生活,触手可及。
|
||||
|
||||
情景诗词应用致力于将中国古典诗词以现代方式呈现给用户,让更多人在碎片化时间中感受诗词的魅力。无论是晨起时的一句"床前明月光",还是午后的一首"春眠不觉晓",都能让您在快节奏的生活中找到片刻的诗意与宁静。
|
||||
|
||||
让我们一起,用情景诗词应用,让古典诗词走进现代生活,让诗意生活,触手可及。
|
||||
|
||||
***
|
||||
|
||||
## 📄 开源协议
|
||||
|
||||
本项目采用开源协议,具体信息请查看应用内的开源协议页面。
|
||||
|
||||
## 📞 联系我们
|
||||
|
||||
- QQ 交流群:271129018
|
||||
- 官方网站:请查看应用内的官方网站链接
|
||||
|
||||
***
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
感谢所有为情景诗词应用做出贡献的开发者和用户,是你们的支持让这个应用不断完善和发展。
|
||||
|
||||
***
|
||||
|
||||
*情景诗词,诗意生活,触手可及*
|
||||
|
||||
1071
index.html
Normal file
1071
index.html
Normal file
File diff suppressed because it is too large
Load Diff
341
lib/CHANGELOG.md
341
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帧率
|
||||
### 开发进度
|
||||
- 🏗️ **HarmonyOS桌面小组件** - 开发中,包含2x2布局、天气显示、诗句展示等功能
|
||||
- 优先级:3
|
||||
getx 加入
|
||||
二维码能力
|
||||
HarmonyOS HongMeng Kernel
|
||||
@@ -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;
|
||||
|
||||
545
lib/services/document/API使用文档.md
Normal file
545
lib/services/document/API使用文档.md
Normal file
@@ -0,0 +1,545 @@
|
||||
# 诗词收录系统 - API 使用文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述诗词收录系统的 API 接口使用方法。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **API 地址**: `api.php`
|
||||
- **请求方式**: GET/POST
|
||||
- **返回格式**: JSON
|
||||
- **字符编码**: UTF-8
|
||||
|
||||
## API 接口
|
||||
|
||||
### 1. 获取分类列表
|
||||
|
||||
获取所有可用的诗词分类。
|
||||
|
||||
**接口地址**: `api.php?api=categories`
|
||||
|
||||
**请求方式**: GET
|
||||
|
||||
**请求参数**: 无
|
||||
|
||||
**返回示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"categories": [
|
||||
{
|
||||
"id": "1",
|
||||
"sid": "1",
|
||||
"icon": "fa-paper-plane",
|
||||
"catename": "诗词句",
|
||||
"alias": null,
|
||||
"create_time": "2026-03-12 04:17:50",
|
||||
"update_time": "2026-03-13 02:12:54"
|
||||
}
|
||||
],
|
||||
"debug": {
|
||||
"current_dir": "/www/wwwroot/yy.vogov.cn/api/app",
|
||||
"categories_count": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 检查诗词名称是否存在
|
||||
|
||||
检查指定的诗词名称是否已存在于数据库中(支持相似度检查)。
|
||||
|
||||
**接口地址**: `api.php?api=check-name`
|
||||
|
||||
**请求方式**: POST
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| name | string | 是 | 诗词名称/参考语句 |
|
||||
| threshold | int | 否 | 相似度阈值(0-100),默认 80 |
|
||||
|
||||
**请求示例**:
|
||||
|
||||
```javascript
|
||||
const formData = new FormData();
|
||||
formData.append('name', '盈盈一水间,脉脉不得语');
|
||||
formData.append('threshold', 80);
|
||||
|
||||
const response = await fetch('api.php?api=check-name', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
```
|
||||
|
||||
**返回示例**:
|
||||
|
||||
**无相似内容**:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"exists": false,
|
||||
"similar_count": 0,
|
||||
"max_similarity": 0,
|
||||
"threshold": 80
|
||||
}
|
||||
```
|
||||
|
||||
**发现相似内容**:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"exists": true,
|
||||
"similar_count": 2,
|
||||
"max_similarity": 95,
|
||||
"threshold": 80
|
||||
}
|
||||
```
|
||||
|
||||
**返回字段说明**:
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| ok | boolean | 请求是否成功 |
|
||||
| exists | boolean | 是否存在相似内容,true=存在,false=不存在 |
|
||||
| similar_count | int | 相似内容条数 |
|
||||
| max_similarity | float | 最高相似度百分比(0-100) |
|
||||
| threshold | int | 使用的相似度阈值 |
|
||||
|
||||
---
|
||||
|
||||
### 3. 提交诗词收录申请
|
||||
|
||||
提交诗词收录申请到数据库。
|
||||
|
||||
**接口地址**: `api.php?api=submit`
|
||||
|
||||
**请求方式**: POST
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| name | string | 是 | 诗词名称/参考语句 |
|
||||
| catename | string | 是 | 分类名称 |
|
||||
| url | string | 是 | 诗人和标题 |
|
||||
| keywords | string | 是 | 关键词,多个用逗号分隔 |
|
||||
| introduce | string | 是 | 诗词介绍 |
|
||||
| img | string | 否 | 平台/配图,默认值: 'default' |
|
||||
| captcha | string | 是 | 人机验证码 |
|
||||
| threshold | int | 否 | 相似度阈值(0-100),默认 80 |
|
||||
|
||||
**请求示例**:
|
||||
|
||||
```javascript
|
||||
const formData = new FormData();
|
||||
formData.append('name', '盈盈一水间,脉脉不得语');
|
||||
formData.append('catename', '诗词句');
|
||||
formData.append('url', '古诗十九首');
|
||||
formData.append('keywords', '爱情,古诗,离别');
|
||||
formData.append('introduce', '《迢迢牵牛星》是产生于汉代的一首文人五言诗...');
|
||||
formData.append('img', 'iOS Swift');
|
||||
formData.append('captcha', '1234');
|
||||
formData.append('threshold', 80);
|
||||
|
||||
const response = await fetch('api.php?api=submit', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
```
|
||||
|
||||
**返回示例**:
|
||||
|
||||
**成功**:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"message": "✅ 提交成功!等待审核",
|
||||
"debug": {
|
||||
"input_data": {...},
|
||||
"insert_result": true,
|
||||
"last_insert_id": "123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**失败**:
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"error": "该诗词已存在!",
|
||||
"debug": {...}
|
||||
}
|
||||
```
|
||||
|
||||
**返回字段说明**:
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| ok | boolean | 请求是否成功 |
|
||||
| message | string | 成功消息(仅成功时返回) |
|
||||
| error | string | 错误消息(仅失败时返回) |
|
||||
| debug | object | 调试信息 |
|
||||
|
||||
---
|
||||
|
||||
## 在 App 中的使用方法
|
||||
|
||||
### Android (Kotlin)
|
||||
|
||||
```kotlin
|
||||
// 获取分类
|
||||
suspend fun getCategories(): List<Category> {
|
||||
val response = OkHttpClient().newCall(
|
||||
Request.Builder()
|
||||
.url("https://your-domain.com/api.php?api=categories")
|
||||
.build()
|
||||
).execute()
|
||||
|
||||
val json = JSONObject(response.body?.string())
|
||||
val categoriesArray = json.getJSONArray("categories")
|
||||
|
||||
val categories = mutableListOf<Category>()
|
||||
for (i in 0 until categoriesArray.length()) {
|
||||
val cat = categoriesArray.getJSONObject(i)
|
||||
categories.add(Category(cat.getString("catename")))
|
||||
}
|
||||
|
||||
return categories
|
||||
}
|
||||
|
||||
// 检查名称
|
||||
suspend fun checkName(name: String, threshold: Int = 80): CheckResult {
|
||||
val formBody = FormBody.Builder()
|
||||
.add("name", name)
|
||||
.add("threshold", threshold.toString())
|
||||
.build()
|
||||
|
||||
val response = OkHttpClient().newCall(
|
||||
Request.Builder()
|
||||
.url("https://your-domain.com/api.php?api=check-name")
|
||||
.post(formBody)
|
||||
.build()
|
||||
).execute()
|
||||
|
||||
val json = JSONObject(response.body?.string())
|
||||
return CheckResult(
|
||||
exists = json.getBoolean("exists"),
|
||||
similarCount = json.getInt("similar_count"),
|
||||
maxSimilarity = json.getDouble("max_similarity"),
|
||||
threshold = json.getInt("threshold")
|
||||
)
|
||||
}
|
||||
|
||||
// 提交收录
|
||||
suspend fun submitPoem(data: PoemData): Boolean {
|
||||
val formBody = FormBody.Builder()
|
||||
.add("name", data.name)
|
||||
.add("catename", data.catename)
|
||||
.add("url", data.url)
|
||||
.add("keywords", data.keywords)
|
||||
.add("introduce", data.introduce)
|
||||
.add("img", data.img ?: "default")
|
||||
.add("captcha", data.captcha)
|
||||
.add("threshold", data.threshold?.toString() ?: "80")
|
||||
.build()
|
||||
|
||||
val response = OkHttpClient().newCall(
|
||||
Request.Builder()
|
||||
.url("https://your-domain.com/api.php?api=submit")
|
||||
.post(formBody)
|
||||
.build()
|
||||
).execute()
|
||||
|
||||
val json = JSONObject(response.body?.string())
|
||||
return json.getBoolean("ok")
|
||||
}
|
||||
```
|
||||
|
||||
### iOS (Swift)
|
||||
|
||||
```swift
|
||||
// 获取分类
|
||||
func getCategories(completion: @escaping ([String]?, Error?) -> Void) {
|
||||
guard let url = URL(string: "https://your-domain.com/api.php?api=categories") else {
|
||||
completion(nil, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.shared.dataTask(with: url) { data, response, error in
|
||||
if let error = error {
|
||||
completion(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
completion(nil, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data"]))
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let categories = json["categories"] as? [[String: Any]] {
|
||||
let categoryNames = categories.compactMap { $0["catename"] as? String }
|
||||
completion(categoryNames, nil)
|
||||
}
|
||||
} catch {
|
||||
completion(nil, error)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
// 检查名称
|
||||
func checkName(name: String, threshold: Int = 80, completion: @escaping (CheckResult?, Error?) -> Void) {
|
||||
guard let apiUrl = URL(string: "https://your-domain.com/api.php?api=check-name") else {
|
||||
completion(nil, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: apiUrl)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
let parameters = [
|
||||
"name": name,
|
||||
"threshold": "\(threshold)"
|
||||
]
|
||||
|
||||
request.httpBody = parameters.percentEncoded()
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
completion(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
completion(nil, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data"]))
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let exists = json["exists"] as? Bool,
|
||||
let similarCount = json["similar_count"] as? Int,
|
||||
let maxSimilarity = json["max_similarity"] as? Double,
|
||||
let threshold = json["threshold"] as? Int {
|
||||
let result = CheckResult(
|
||||
exists: exists,
|
||||
similarCount: similarCount,
|
||||
maxSimilarity: maxSimilarity,
|
||||
threshold: threshold
|
||||
)
|
||||
completion(result, nil)
|
||||
}
|
||||
} catch {
|
||||
completion(nil, error)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
// 提交收录
|
||||
func submitPoem(name: String, catename: String, url: String, keywords: String, introduce: String, img: String?, captcha: String, threshold: Int = 80, completion: @escaping (Bool, String?) -> Void) {
|
||||
guard let apiUrl = URL(string: "https://your-domain.com/api.php?api=submit") else {
|
||||
completion(false, "Invalid URL")
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: apiUrl)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
let parameters = [
|
||||
"name": name,
|
||||
"catename": catename,
|
||||
"url": url,
|
||||
"keywords": keywords,
|
||||
"introduce": introduce,
|
||||
"img": img ?? "default",
|
||||
"captcha": captcha,
|
||||
"threshold": "\(threshold)"
|
||||
]
|
||||
|
||||
request.httpBody = parameters.percentEncoded()
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
completion(false, error.localizedDescription)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
completion(false, "No data")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let ok = json["ok"] as? Bool {
|
||||
let message = json["message"] as? String ?? json["error"] as? String
|
||||
completion(ok, message)
|
||||
}
|
||||
} catch {
|
||||
completion(false, error.localizedDescription)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
extension Dictionary {
|
||||
func percentEncoded() -> Data? {
|
||||
return map { key, value in
|
||||
let escapedKey = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
|
||||
let escapedValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
|
||||
return escapedKey + "=" + escapedValue
|
||||
}
|
||||
.joined(separator: "&")
|
||||
.data(using: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
extension CharacterSet {
|
||||
static let urlQueryValueAllowed: CharacterSet = {
|
||||
let generalDelimitersToEncode = ":#[]@"
|
||||
let subDelimitersToEncode = "!$&'()*+,;="
|
||||
|
||||
var allowed = CharacterSet.urlQueryAllowed
|
||||
allowed.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
|
||||
return allowed
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
### Flutter (Dart)
|
||||
|
||||
```dart
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
|
||||
// 获取分类
|
||||
Future<List<String>> getCategories() async {
|
||||
final response = await http.get(
|
||||
Uri.parse('https://your-domain.com/api.php?api=categories'),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
final List<dynamic> categories = data['categories'];
|
||||
return categories.map((cat) => cat['catename'] as String).toList();
|
||||
} else {
|
||||
throw Exception('Failed to load categories');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查名称
|
||||
Future<CheckResult> checkName({
|
||||
required String name,
|
||||
int threshold = 80,
|
||||
}) async {
|
||||
final response = await http.post(
|
||||
Uri.parse('https://your-domain.com/api.php?api=check-name'),
|
||||
body: {
|
||||
'name': name,
|
||||
'threshold': threshold.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
return CheckResult(
|
||||
exists: data['exists'] as bool,
|
||||
similarCount: data['similar_count'] as int,
|
||||
maxSimilarity: (data['max_similarity'] as num).toDouble(),
|
||||
threshold: data['threshold'] as int,
|
||||
);
|
||||
} else {
|
||||
throw Exception('Failed to check name');
|
||||
}
|
||||
}
|
||||
|
||||
// 提交收录
|
||||
Future<bool> submitPoem({
|
||||
required String name,
|
||||
required String catename,
|
||||
required String url,
|
||||
required String keywords,
|
||||
required String introduce,
|
||||
String? img,
|
||||
required String captcha,
|
||||
int threshold = 80,
|
||||
}) async {
|
||||
final response = await http.post(
|
||||
Uri.parse('https://your-domain.com/api.php?api=submit'),
|
||||
body: {
|
||||
'name': name,
|
||||
'catename': catename,
|
||||
'url': url,
|
||||
'keywords': keywords,
|
||||
'introduce': introduce,
|
||||
'img': img ?? 'default',
|
||||
'captcha': captcha,
|
||||
'threshold': threshold.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
return data['ok'] as bool;
|
||||
} else {
|
||||
throw Exception('Failed to submit');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误码说明
|
||||
|
||||
| 错误信息 | 说明 |
|
||||
|----------|------|
|
||||
| 缺少必填字段:xxx | 必填字段未填写 |
|
||||
| 该诗词已存在! | 诗词名称已在数据库中,或相似度超过阈值 |
|
||||
| ❌ 数据库写入失败:无法插入数据 | 数据库插入失败 |
|
||||
| 验证码错误,请重新输入 | 人机验证码错误 |
|
||||
| 提交过于频繁,请稍后再试 | 频率限制,1分钟内只能提交3次 |
|
||||
|
||||
---
|
||||
|
||||
## 相似度说明
|
||||
|
||||
系统使用 **Levenshtein 距离算法** 计算文本相似度:
|
||||
|
||||
1. **文本清理**:自动去除标点符号和空格后比较
|
||||
2. **阈值设置**:0-100%,默认 80%
|
||||
3. **判断规则**:相似度 ≥ 阈值 则认为是重复内容
|
||||
|
||||
**示例**:
|
||||
- "盈盈一水间,脉脉不得语"
|
||||
- "盈盈一水间,脉脉不得语。"(相似度约 95%)
|
||||
- "盈盈一水间,脉脉不得"(相似度约 85%)
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **字符编码**: 所有请求和响应都使用 UTF-8 编码
|
||||
2. **人机验证**: 提交接口必须提供正确的验证码
|
||||
3. **频率限制**: 同一 IP 1分钟内最多提交 3 次
|
||||
4. **相似度检查**: check-name 和 submit 接口都会进行相似度检查
|
||||
5. **数据安全**: 所有用户输入都会经过安全处理
|
||||
6. **调试信息**: API 返回包含 debug 字段,方便开发调试,生产环境可忽略
|
||||
|
||||
---
|
||||
|
||||
## 更新日志
|
||||
|
||||
- **v1.0.12**: 添加相似度验证功能,支持可配置阈值
|
||||
- **v1.0.11**: 修改验证表为 pre_site
|
||||
- **v1.0.10**: 添加人机验证功能和频率限制
|
||||
- **v1.0.9**: 添加结果Modal对话框
|
||||
- **v1.0.8**: 添加检测按钮和提交前确认
|
||||
66
lib/services/document/stats.php
Normal file
66
lib/services/document/stats.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
require_once('../../includes/common.php');
|
||||
|
||||
|
||||
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
|
||||
try {
|
||||
$stats = [];
|
||||
|
||||
$stats['count_category'] = $DB->count('category');
|
||||
$stats['count_site'] = $DB->count('site');
|
||||
$stats['count_apply'] = $DB->count('apply', array('reject' => 0));
|
||||
$stats['count_apply_reject'] = $DB->count('apply', array('reject' => 1));
|
||||
$stats['count_article'] = $DB->count('article');
|
||||
$stats['count_article_category'] = $DB->count('article_category');
|
||||
$stats['count_notice'] = $DB->count('notice');
|
||||
$stats['count_link'] = $DB->count('link');
|
||||
$stats['count_tags'] = 131;
|
||||
|
||||
$top_hits_day = $DB->find('site', 'id, name', array('date' => date("Y-m-d", time())), '`hits_day` desc');
|
||||
$top_hits_month = $DB->find('site', 'id, name', array('datem' => date("Y-m", time())), '`hits_month` desc');
|
||||
$top_hits_total = $DB->find('site', 'id, name', null, '`hits_total` desc');
|
||||
$top_like = $DB->find('site', 'id, name', null, '`like` desc');
|
||||
|
||||
$cumulative_hits_result = $DB->query("SELECT SUM(hits_total) as total FROM pre_site")->fetch();
|
||||
$cumulative_likes_result = $DB->query("SELECT SUM(`like`) as total FROM pre_site")->fetch();
|
||||
|
||||
$stats['cumulative_hits'] = $cumulative_hits_result['total'] ?? 0;
|
||||
$stats['cumulative_likes'] = $cumulative_likes_result['total'] ?? 0;
|
||||
|
||||
$stats['top_hits_day'] = $top_hits_day ? [
|
||||
'id' => $top_hits_day['id'],
|
||||
'name' => $top_hits_day['name']
|
||||
] : null;
|
||||
|
||||
$stats['top_hits_month'] = $top_hits_month ? [
|
||||
'id' => $top_hits_month['id'],
|
||||
'name' => $top_hits_month['name']
|
||||
] : null;
|
||||
|
||||
$stats['top_hits_total'] = $top_hits_total ? [
|
||||
'id' => $top_hits_total['id'],
|
||||
'name' => $top_hits_total['name']
|
||||
] : null;
|
||||
|
||||
$stats['top_like'] = $top_like ? [
|
||||
'id' => $top_like['id'],
|
||||
'name' => $top_like['name']
|
||||
] : null;
|
||||
|
||||
$stats['build_time'] = $conf['build_time'] ?? '';
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'data' => $stats,
|
||||
'timestamp' => time()
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => $e->getMessage()
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
366
lib/services/document/搜索api.md
Normal file
366
lib/services/document/搜索api.md
Normal file
@@ -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` 判断是否显示"加载更多"按钮
|
||||
|
||||
216
lib/services/document/统计API文档.md
Normal file
216
lib/services/document/统计API文档.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# 统计 API 接口文档
|
||||
|
||||
## 概述
|
||||
|
||||
获取网站统计信息的 API 接口,返回分类数量、收录数量、热度统计等数据。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **接口地址**: `https://yy.vogov.cn/api/app/stats.php`
|
||||
- **请求方式**: GET
|
||||
- **返回格式**: JSON
|
||||
- **字符编码**: UTF-8
|
||||
- **支持跨域**: 是(`Access-Control-Allow-Origin: *`)
|
||||
|
||||
## 请求参数
|
||||
|
||||
无需任何参数,直接 GET 请求即可。
|
||||
|
||||
## 返回示例
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"count_category": 3,
|
||||
"count_site": 15807,
|
||||
"count_apply": 14,
|
||||
"count_apply_reject": 3,
|
||||
"count_article": 2,
|
||||
"count_article_category": 4,
|
||||
"count_notice": 2,
|
||||
"count_link": 3,
|
||||
"count_tags": 131,
|
||||
"cumulative_hits": "16887",
|
||||
"cumulative_likes": "272",
|
||||
"top_hits_day": {
|
||||
"id": "7559",
|
||||
"name": "除却天边月,没人知。人有悲欢离合,月有阴晴圆缺,此事古难全。"
|
||||
},
|
||||
"top_hits_month": {
|
||||
"id": "461",
|
||||
"name": "人有悲欢离合,月有阴晴圆缺,此事古难全。"
|
||||
},
|
||||
"top_hits_total": {
|
||||
"id": "1",
|
||||
"name": "井鱼焉知身在渊,错把方寸作世间"
|
||||
},
|
||||
"top_like": {
|
||||
"id": "5876",
|
||||
"name": "世间无比酒,天下有名楼。"
|
||||
},
|
||||
"build_time": "2026-03-04"
|
||||
},
|
||||
"timestamp": 1774977191
|
||||
}
|
||||
```
|
||||
|
||||
## 返回字段说明
|
||||
|
||||
### 基础字段
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| --------- | ------- | ------ |
|
||||
| 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 | 分类标签 |
|
||||
|
||||
#### 热度统计
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| ----------------- | ------ | ------ |
|
||||
| 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 | 最高点赞诗句 |
|
||||
|
||||
#### 热门内容对象
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| ---- | ------ | ----- |
|
||||
| id | string | 诗句 ID |
|
||||
| name | string | 诗句内容 |
|
||||
|
||||
#### 其他
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| ----------- | ------ | ------------------- |
|
||||
| build\_time | string | 建站时间(格式:YYYY-MM-DD) |
|
||||
|
||||
## 错误返回
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"error": "错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
## 调用示例
|
||||
|
||||
### JavaScript (Fetch)
|
||||
|
||||
```javascript
|
||||
fetch('https://yy.vogov.cn/api/app/stats.php')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
console.log('已收录诗句:', data.data.count_site);
|
||||
console.log('当天热门:', data.data.top_hits_day.name);
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error:', error));
|
||||
```
|
||||
|
||||
### Flutter (Dart)
|
||||
|
||||
```dart
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
|
||||
Future<StatsData?> getStats() async {
|
||||
final response = await http.get(
|
||||
Uri.parse('https://yy.vogov.cn/api/app/stats.php'),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
if (data['ok'] == true) {
|
||||
return StatsData.fromJson(data['data']);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
class StatsData {
|
||||
final int countSite;
|
||||
final String cumulativeHits;
|
||||
final TopContent? topHitsDay;
|
||||
|
||||
StatsData({
|
||||
required this.countSite,
|
||||
required this.cumulativeHits,
|
||||
this.topHitsDay,
|
||||
});
|
||||
|
||||
factory StatsData.fromJson(Map<String, dynamic> json) {
|
||||
return StatsData(
|
||||
countSite: json['count_site'],
|
||||
cumulativeHits: json['cumulative_hits'],
|
||||
topHitsDay: json['top_hits_day'] != null
|
||||
? TopContent.fromJson(json['top_hits_day'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TopContent {
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
TopContent({required this.id, required this.name});
|
||||
|
||||
factory TopContent.fromJson(Map<String, dynamic> json) {
|
||||
return TopContent(id: json['id'], name: json['name']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 数据来源
|
||||
|
||||
数据来源于以下数据库表:
|
||||
|
||||
| 表名 | 说明 |
|
||||
| ---------------------- | ----- |
|
||||
| pre\_category | 分类表 |
|
||||
| pre\_site | 收录诗句表 |
|
||||
| pre\_apply | 申请收录表 |
|
||||
| pre\_article | 文章表 |
|
||||
| pre\_article\_category | 文章分类表 |
|
||||
| pre\_notice | 公告表 |
|
||||
| pre\_link | 友情链接表 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **缓存建议**:统计数据变化频率较低,建议客户端缓存 5-10 分钟
|
||||
2. **空值处理**:热门内容字段可能为 `null`,请做好空值判断
|
||||
3. **跨域支持**:接口已配置 CORS,支持前端直接调用
|
||||
4. **数据类型**:`cumulative_hits` 和 `cumulative_likes` 返回的是字符串类型
|
||||
|
||||
## 更新日志
|
||||
|
||||
- **v1.0.14** (2026-03-30): 新增统计 API 接口
|
||||
|
||||
11
lib/services/wakelock_service.dart
Normal file
11
lib/services/wakelock_service.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'wakelock_service_web.dart' if (dart.library.io) 'wakelock_service_io.dart';
|
||||
|
||||
abstract class WakelockService {
|
||||
static final WakelockService instance = getWakelockService();
|
||||
|
||||
Future<void> enable();
|
||||
Future<void> disable();
|
||||
Future<bool> isEnabled();
|
||||
}
|
||||
|
||||
WakelockService getWakelockService() => getWakelockServiceImpl();
|
||||
21
lib/services/wakelock_service_io.dart
Normal file
21
lib/services/wakelock_service_io.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'wakelock_service.dart';
|
||||
|
||||
class WakelockServiceIO implements WakelockService {
|
||||
@override
|
||||
Future<void> enable() async {
|
||||
await WakelockPlus.enable();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disable() async {
|
||||
await WakelockPlus.disable();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isEnabled() async {
|
||||
return await WakelockPlus.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
WakelockService getWakelockServiceImpl() => WakelockServiceIO();
|
||||
20
lib/services/wakelock_service_web.dart
Normal file
20
lib/services/wakelock_service_web.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'wakelock_service.dart';
|
||||
|
||||
class WakelockServiceWeb implements WakelockService {
|
||||
@override
|
||||
Future<void> enable() async {
|
||||
// Web 平台不支持屏幕常亮功能
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disable() async {
|
||||
// Web 平台不支持屏幕常亮功能
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isEnabled() async {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
WakelockService getWakelockServiceImpl() => WakelockServiceWeb();
|
||||
@@ -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<CategoryPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
final List<String> _tabCategories = ['场景分类', '朝代分类'];
|
||||
final List<Map<String, dynamic>> _tabCategories = [
|
||||
{'label': '场景分类', 'icon': Icons.category},
|
||||
{'label': '朝代分类', 'icon': Icons.history},
|
||||
];
|
||||
|
||||
static const sceneData = {
|
||||
"节日": ["七夕节", "中秋节", "元宵节", "寒食节", "清明节", "端午节", "重阳节", "春节", "节日"],
|
||||
@@ -90,76 +94,131 @@ class _CategoryPageState extends State<CategoryPage>
|
||||
|
||||
@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<String, List<String>> 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<CategoryPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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<PopularPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
final List<String> _tabCategories = ['总榜', '日榜', '月榜'];
|
||||
final List<Map<String, dynamic>> _tabCategories = [
|
||||
{'label': '总榜', 'icon': Icons.bar_chart},
|
||||
{'label': '日榜', 'icon': Icons.today},
|
||||
{'label': '月榜', 'icon': Icons.calendar_today},
|
||||
];
|
||||
|
||||
List<PoetryModel> _rankList = [];
|
||||
bool _loading = false;
|
||||
@@ -46,36 +52,46 @@ class _PopularPageState extends State<PopularPage>
|
||||
|
||||
@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<PopularPage>
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _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<PopularPage>
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
780
lib/views/active/tags/corr_page.dart
Normal file
780
lib/views/active/tags/corr_page.dart
Normal file
@@ -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<CorrPage> createState() => _CorrPageState();
|
||||
}
|
||||
|
||||
class _CorrPageState extends State<CorrPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
List<Map<String, dynamic>> _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<double> _skeletonAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_skeletonAnimationController = AnimationController(
|
||||
duration: const Duration(seconds: 1),
|
||||
vsync: this,
|
||||
)..repeat(reverse: true);
|
||||
_skeletonAnimation = Tween<double>(
|
||||
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<void> _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<dynamic>? ?? [];
|
||||
final pagination = data['pagination'] as Map<String, dynamic>?;
|
||||
|
||||
setState(() {
|
||||
_poetryList = results
|
||||
.map((item) => item as Map<String, dynamic>)
|
||||
.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<void> _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<dynamic>? ?? [];
|
||||
|
||||
setState(() {
|
||||
_poetryList.addAll(
|
||||
results.map((item) => item as Map<String, dynamic>),
|
||||
);
|
||||
_currentPage = nextPage;
|
||||
_hasMore = results.length >= _pageSize;
|
||||
_isLoadingMore = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_isLoadingMore = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoadingMore = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleLike(Map<String, dynamic> 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<SystemUiOverlayStyle>(
|
||||
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<String, dynamic> 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<bool>(
|
||||
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<void> _createNoteFromPoetry(Map<String, dynamic> 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<String, dynamic> 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<DiscoverPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
final List<String> _categories = ['热门', '分类', '搜索', '活跃'];
|
||||
TabController? _tabController;
|
||||
List<String> _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<void> _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<DiscoverPage>
|
||||
|
||||
@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<DiscoverPage>
|
||||
body: Column(
|
||||
children: [
|
||||
// 只有非搜索标签时才显示话题chips
|
||||
if (_categories[_tabController.index] != '搜索' && _showTips)
|
||||
if (_categories[_tabController!.index] != '搜索' && _showTips)
|
||||
_buildTopicChips(),
|
||||
Expanded(
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
@@ -77,14 +108,10 @@ class _DiscoverPageState extends State<DiscoverPage>
|
||||
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<DiscoverPage>
|
||||
if (category == '热门') {
|
||||
return const PopularPage();
|
||||
}
|
||||
// 搜索标签显示 ActiveSearchPage
|
||||
if (category == '搜索') {
|
||||
return const ActiveSearchPage();
|
||||
}
|
||||
// 活跃标签显示活跃统计页面
|
||||
if (category == '活跃') {
|
||||
return const RatePage();
|
||||
@@ -283,9 +314,11 @@ class _DiscoverPageState extends State<DiscoverPage>
|
||||
|
||||
Future<void> _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() {
|
||||
|
||||
@@ -48,7 +48,7 @@ class _FavoritesPageState extends State<FavoritesPage>
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: TabbedNavAppBar.build(
|
||||
title: '收藏',
|
||||
title: '足迹',
|
||||
tabController: _tabController,
|
||||
tabLabels: _categories,
|
||||
tabBarScrollable: true,
|
||||
|
||||
@@ -142,9 +142,17 @@ class _AllListPageState extends State<AllListPage> {
|
||||
|
||||
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<AllListPage> {
|
||||
// 构建点赞卡片 - 简洁紧凑样式
|
||||
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<AllListPage> {
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
margin: const EdgeInsets.only(bottom: 0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
|
||||
@@ -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<AppInfoPage> {
|
||||
String _udid = '获取中...';
|
||||
bool _isDeveloperMode = false;
|
||||
int _tapCount = 0;
|
||||
DateTime? _lastTapTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadUdid();
|
||||
_loadDeveloperMode();
|
||||
}
|
||||
|
||||
Future<void> _loadUdid() async {
|
||||
@@ -45,6 +50,50 @@ class _AppInfoPageState extends State<AppInfoPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadDeveloperMode() async {
|
||||
final isEnabled = await SharedPreferencesStorageController.getBool(
|
||||
'developer_mode_enabled',
|
||||
defaultValue: false,
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isDeveloperMode = isEnabled;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _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<AppInfoPage> {
|
||||
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<AppInfoPage> {
|
||||
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<AppInfoPage> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
1024
lib/views/profile/components/entire_page.dart
Normal file
1024
lib/views/profile/components/entire_page.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import '../../../constants/app_constants.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import '../../../services/wakelock_service.dart';
|
||||
import 'dart:io' as io;
|
||||
|
||||
class PopMenu extends StatelessWidget {
|
||||
@@ -44,6 +45,16 @@ class PopMenu extends StatelessWidget {
|
||||
}
|
||||
|
||||
static Future<void> toggleScreenWake(BuildContext context) async {
|
||||
// Web 平台不支持 wakelock_plus
|
||||
if (kIsWeb) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Web 平台不支持屏幕常亮功能')));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用 io.Platform 检测平台
|
||||
final String osName = io.Platform.operatingSystem;
|
||||
@@ -51,7 +62,7 @@ class PopMenu extends StatelessWidget {
|
||||
print('Current platform: $osName, version: $osVersion');
|
||||
|
||||
// 直接尝试启用屏幕常亮
|
||||
await WakelockPlus.enable();
|
||||
await WakelockService.instance.enable();
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
@@ -59,7 +70,7 @@ class PopMenu extends StatelessWidget {
|
||||
).showSnackBar(const SnackBar(content: Text('屏幕常亮已开启')));
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
print('WakelockPlus error: $e');
|
||||
print('WakelockService error: $e');
|
||||
print('Stack trace: $stackTrace');
|
||||
if (context.mounted) {
|
||||
// 检查错误类型,判断是否是设备不支持
|
||||
@@ -137,7 +148,7 @@ class PopMenu extends StatelessWidget {
|
||||
}),
|
||||
_buildBottomSheetItem(
|
||||
context,
|
||||
'屏幕常亮',
|
||||
'使用教程',
|
||||
Icons.screen_lock_rotation,
|
||||
() => toggleScreenWake(context),
|
||||
),
|
||||
|
||||
359
lib/views/profile/components/server_info_dialog.dart
Normal file
359
lib/views/profile/components/server_info_dialog.dart
Normal file
@@ -0,0 +1,359 @@
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../constants/app_constants.dart';
|
||||
|
||||
class ServerInfoDialog {
|
||||
static Future<void> show(BuildContext context, {Map<String, dynamic>? data}) {
|
||||
final server = data?['server'] as Map<String, dynamic>?;
|
||||
final network = data?['network'] as Map<String, dynamic>?;
|
||||
final timestamp = data?['timestamp'] as Map<String, dynamic>?;
|
||||
|
||||
final load = server?['load'] as Map<String, dynamic>?;
|
||||
final latency = network?['latency'] as List<dynamic>?;
|
||||
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<Widget>((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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
0
lib/views/profile/guide/beginner_page.dart
Normal file
0
lib/views/profile/guide/beginner_page.dart
Normal file
@@ -9,11 +9,12 @@ import 'dart:io' as io;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
|
||||
import '../../constants/app_constants.dart';
|
||||
import '../../controllers/history_controller.dart';
|
||||
import '../../controllers/shared_preferences_storage_controller.dart';
|
||||
import '../../services/wakelock_service.dart';
|
||||
import 'history_page.dart';
|
||||
import 'per_card.dart';
|
||||
import 'settings/app_fun.dart';
|
||||
@@ -31,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});
|
||||
@@ -611,7 +613,10 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
_buildSettingsItem(
|
||||
'查看全站统计',
|
||||
Icons.history,
|
||||
() => _showSnackBar('查看全站统计'),
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const EntirePage()),
|
||||
),
|
||||
),
|
||||
_buildSettingsItem(
|
||||
'开发计划',
|
||||
@@ -802,6 +807,11 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
}
|
||||
|
||||
Widget _buildScreenWakeItem() {
|
||||
// Web 平台不支持屏幕常亮功能,不显示该项
|
||||
if (kIsWeb) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// === 屏幕常亮设置项:显示图标、标题和开关 ===
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
@@ -912,6 +922,16 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
}
|
||||
|
||||
Future<void> _toggleScreenWake(bool enable) async {
|
||||
// Web 平台不支持 wakelock_plus
|
||||
if (kIsWeb) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Web 平台不支持屏幕常亮功能')));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用 io.Platform 检测平台
|
||||
final String osName = io.Platform.operatingSystem;
|
||||
@@ -919,14 +939,14 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
print('Current platform: $osName, version: $osVersion');
|
||||
|
||||
if (enable) {
|
||||
await WakelockPlus.enable();
|
||||
await WakelockService.instance.enable();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('屏幕常亮已开启')));
|
||||
}
|
||||
} else {
|
||||
await WakelockPlus.disable();
|
||||
await WakelockService.instance.disable();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
@@ -938,7 +958,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
_isScreenWakeEnabled = enable;
|
||||
});
|
||||
} catch (e, stackTrace) {
|
||||
print('WakelockPlus error: $e');
|
||||
print('WakelockService error: $e');
|
||||
print('Stack trace: $stackTrace');
|
||||
if (mounted) {
|
||||
// 检查错误类型,判断是否是设备不支持
|
||||
|
||||
@@ -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<OfflineDataPage> {
|
||||
}
|
||||
|
||||
void _displayServerInfoDialog(Map<String, dynamic> data) {
|
||||
final server = data['server'] as Map<String, dynamic>?;
|
||||
final network = data['network'] as Map<String, dynamic>?;
|
||||
final timestamp = data['timestamp'] as Map<String, dynamic>?;
|
||||
|
||||
final load = server?['load'] as Map<String, dynamic>?;
|
||||
final latency = network?['latency'] as List<dynamic>?;
|
||||
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<Widget>((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
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user