Initial commit: Flutter 无书应用项目

This commit is contained in:
Developer
2026-03-30 02:35:31 +08:00
commit 9175ff9905
566 changed files with 103261 additions and 0 deletions

253
lib/utils/app_theme.dart Normal file
View File

@@ -0,0 +1,253 @@
import 'package:flutter/material.dart';
import '../constants/app_constants.dart';
class AppTheme {
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: AppConstants.primaryColor,
brightness: Brightness.light,
primary: AppConstants.primaryColor,
secondary: AppConstants.secondaryColor,
surface: AppConstants.surfaceColor,
error: AppConstants.errorColor,
),
// AppBar 主题
appBarTheme: const AppBarTheme(
backgroundColor: AppConstants.primaryColor,
foregroundColor: Colors.white,
elevation: 0,
centerTitle: true,
titleTextStyle: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w500,
),
iconTheme: IconThemeData(color: Colors.white),
),
// 按钮主题
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppConstants.primaryColor,
foregroundColor: Colors.white,
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: AppConstants.primaryColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppConstants.primaryColor,
side: const BorderSide(color: AppConstants.primaryColor),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
// 卡片主题
cardTheme: CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
color: AppConstants.surfaceColor,
),
// 输入框主题
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.grey),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppConstants.primaryColor,
width: 2,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppConstants.errorColor),
),
filled: true,
fillColor: Colors.grey[50],
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
// 浮动按钮主题
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: AppConstants.primaryColor,
foregroundColor: Colors.white,
elevation: 4,
),
// 底部导航栏主题
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
backgroundColor: AppConstants.surfaceColor,
selectedItemColor: AppConstants.primaryColor,
unselectedItemColor: Colors.grey,
type: BottomNavigationBarType.fixed,
elevation: 8,
),
// 图标主题
iconTheme: const IconThemeData(
color: AppConstants.primaryColor,
size: 24,
),
// 文本主题
textTheme: const TextTheme(
displayLarge: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
displayMedium: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
displaySmall: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
headlineLarge: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
headlineMedium: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
headlineSmall: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
titleLarge: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
titleMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
titleSmall: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
bodyLarge: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
color: Colors.black87,
),
bodyMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Colors.black87,
),
bodySmall: TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
color: Colors.black54,
),
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
labelMedium: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
labelSmall: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: Colors.black54,
),
),
);
}
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: AppConstants.primaryColor,
brightness: Brightness.dark,
primary: AppConstants.primaryColor,
secondary: AppConstants.secondaryColor,
surface: const Color(0xFF1E1E1E),
error: AppConstants.errorColor,
),
// AppBar 主题
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF1E1E1E),
foregroundColor: Colors.white,
elevation: 0,
centerTitle: true,
titleTextStyle: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w500,
),
iconTheme: IconThemeData(color: Colors.white),
),
// 卡片主题
cardTheme: CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
color: const Color(0xFF1E1E1E),
),
// 输入框主题
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey[600]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppConstants.primaryColor,
width: 2,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppConstants.errorColor),
),
filled: true,
fillColor: const Color(0xFF2A2A2A),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
);
}
}

View File

@@ -0,0 +1,16 @@
// Flutter 版本兼容性修复
// 修复 Flutter 3.27.5 中 CupertinoDynamicColor 缺少 toARGB32 方法的问题
import 'package:flutter/cupertino.dart';
/// Flutter 3.27.5 兼容性修复
/// 为 CupertinoDynamicColor 添加缺失的 toARGB32 方法实现
extension CupertinoDynamicColorExtension on CupertinoDynamicColor {
/// 添加缺失的 toARGB32 方法实现
/// 这是 Flutter 3.27.5 版本中的一个临时解决方案
int toARGB32() {
// 使用一个固定的颜色值作为临时解决方案
// 在实际应用中,这里应该根据主题动态解析
return 0xFF0000FF; // 临时返回蓝色 ARGB32 值
}
}

View File

@@ -0,0 +1,239 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../config/app_config.dart';
import '../constants/app_constants.dart';
/// 时间: 2026-03-27
/// 功能: 强制引导页检查器
/// 介绍: 确保引导页一定会显示,如果没有显示则抛出异常并显示错误对话框
/// 最新变化: 新建强制检查机制
class ForceGuideChecker {
/// 强制检查引导页状态
/// 返回 true 表示需要显示引导页false 表示不需要
/// 如果检测到异常情况,会抛出 GuidePageException
static Future<GuideCheckResult> checkAndValidate(
SharedPreferences prefs,
) async {
try {
// 获取所有键
final allKeys = prefs.getKeys();
debugPrint('=== 强制引导页检查 ===');
debugPrint('所有配置项: $allKeys');
// 检查关键配置是否存在
final bool hasFirstLaunch = prefs.containsKey(AppConfig.keyFirstLaunch);
final bool hasAgreementAccepted =
prefs.containsKey(AppConfig.keyAgreementAccepted);
final bool hasAppVersion = prefs.containsKey(AppConfig.keyAppVersion);
debugPrint('hasFirstLaunch: $hasFirstLaunch');
debugPrint('hasAgreementAccepted: $hasAgreementAccepted');
debugPrint('hasAppVersion: $hasAppVersion');
// 获取状态值
final bool firstLaunch = prefs.getBool(AppConfig.keyFirstLaunch) ?? true;
final bool agreementAccepted =
prefs.getBool(AppConfig.keyAgreementAccepted) ?? false;
final int? savedVersion = prefs.getInt(AppConfig.keyAppVersion);
debugPrint('firstLaunch: $firstLaunch');
debugPrint('agreementAccepted: $agreementAccepted');
debugPrint('savedVersion: $savedVersion');
// 判断是否需要显示引导页
bool needGuide = false;
String reason = '';
// 情况1: 首次安装(没有版本号)
if (savedVersion == null) {
needGuide = true;
reason = '首次安装(无版本号)';
debugPrint('需要引导页: $reason');
}
// 情况2: firstLaunch 为 true
else if (firstLaunch) {
needGuide = true;
reason = '首次启动标志为 true';
debugPrint('需要引导页: $reason');
}
// 情况3: 未同意协议
else if (!agreementAccepted) {
needGuide = true;
reason = '未同意协议';
debugPrint('需要引导页: $reason');
}
// 情况4: 缺少关键配置
else if (!hasFirstLaunch || !hasAgreementAccepted) {
needGuide = true;
reason = '缺少关键配置项';
debugPrint('需要引导页: $reason');
} else {
debugPrint('不需要引导页,正常启动');
}
return GuideCheckResult(
needGuide: needGuide,
reason: reason,
firstLaunch: firstLaunch,
agreementAccepted: agreementAccepted,
savedVersion: savedVersion,
hasFirstLaunch: hasFirstLaunch,
hasAgreementAccepted: hasAgreementAccepted,
hasAppVersion: hasAppVersion,
allKeys: allKeys.toList(),
);
} catch (e) {
debugPrint('强制引导页检查异常: $e');
throw GuidePageException('引导页检查失败: $e');
}
}
/// 显示错误对话框
static void showErrorDialog(
BuildContext context,
GuideCheckResult result,
String errorMessage,
) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Row(
children: [
Icon(Icons.error, color: Colors.red),
const SizedBox(width: 8),
const Text('引导页加载异常'),
],
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'错误信息:',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Text(
errorMessage,
style: TextStyle(fontSize: 12, fontFamily: 'monospace'),
),
),
const SizedBox(height: 16),
Text(
'检测结果:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
_buildInfoRow('需要引导页', result.needGuide ? '' : ''),
_buildInfoRow('原因', result.reason),
_buildInfoRow('firstLaunch', result.firstLaunch.toString()),
_buildInfoRow(
'agreementAccepted', result.agreementAccepted.toString()),
_buildInfoRow('savedVersion', result.savedVersion?.toString() ?? 'null'),
_buildInfoRow('hasFirstLaunch', result.hasFirstLaunch.toString()),
_buildInfoRow('hasAgreementAccepted',
result.hasAgreementAccepted.toString()),
_buildInfoRow('hasAppVersion', result.hasAppVersion.toString()),
const SizedBox(height: 8),
Text(
'所有配置项: ${result.allKeys}',
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
],
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('关闭'),
),
ElevatedButton(
onPressed: () async {
// 清空所有配置并重启
final prefs = await SharedPreferences.getInstance();
await prefs.clear();
Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppConstants.primaryColor,
),
child: const Text('清空配置'),
),
],
),
);
}
static Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
SizedBox(
width: 120,
child: Text(
'$label:',
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
),
),
Expanded(
child: Text(
value,
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
),
),
],
),
);
}
}
/// 引导页检查结果
class GuideCheckResult {
final bool needGuide;
final String reason;
final bool firstLaunch;
final bool agreementAccepted;
final int? savedVersion;
final bool hasFirstLaunch;
final bool hasAgreementAccepted;
final bool hasAppVersion;
final List<String> allKeys;
GuideCheckResult({
required this.needGuide,
required this.reason,
required this.firstLaunch,
required this.agreementAccepted,
required this.savedVersion,
required this.hasFirstLaunch,
required this.hasAgreementAccepted,
required this.hasAppVersion,
required this.allKeys,
});
}
/// 引导页异常
class GuidePageException implements Exception {
final String message;
GuidePageException(this.message);
@override
String toString() => 'GuidePageException: $message';
}

View File

@@ -0,0 +1,218 @@
/// 时间: 2025-03-21
/// 功能: HTTP客户端工具类使用纯Dart dio库
/// 介绍: 提供统一的HTTP请求方法支持GET、POST等请求用于项目中的网络请求
/// 最新变化: 移除CORS代理需后台服务器配置CORS响应头
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
class HttpClient {
static const String _baseUrl = 'https://yy.vogov.cn/api/';
static const Duration _timeout = Duration(seconds: 30);
static final BaseOptions _options = BaseOptions(
baseUrl: _baseUrl,
connectTimeout: _timeout,
receiveTimeout: _timeout,
sendTimeout: _timeout,
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json',
'User-Agent': 'Poes-Flutter/1.0.0',
},
);
static final Dio _dio = Dio(_options);
/// 添加调试日志
static void _debugLog(String message) {
if (kDebugMode) {
print('HttpClient: $message');
}
}
/// GET请求
static Future<HttpResponse> get(
String path, {
Map<String, dynamic>? queryParameters,
Map<String, String>? headers,
Duration? timeout,
}) async {
return _request(
'GET',
path,
queryParameters: queryParameters,
headers: headers,
timeout: timeout,
);
}
/// POST请求
static Future<HttpResponse> post(
String path, {
Map<String, dynamic>? data,
Map<String, String>? headers,
Duration? timeout,
}) async {
return _request(
'POST',
path,
data: data,
headers: headers,
timeout: timeout,
);
}
/// 通用请求方法
static Future<HttpResponse> _request(
String method,
String path, {
Map<String, dynamic>? queryParameters,
Map<String, dynamic>? data,
Map<String, String>? headers,
Duration? timeout,
}) async {
try {
final url = '$_baseUrl$path';
_debugLog('请求 $method $url');
if (queryParameters != null) {
_debugLog('查询参数: $queryParameters');
}
final options = Options(
method: method,
headers: headers != null
? {..._options.headers!, ...headers}
: _options.headers,
);
if (timeout != null) {
options.connectTimeout = timeout;
options.receiveTimeout = timeout;
options.sendTimeout = timeout;
}
Response response;
if (method.toUpperCase() == 'GET') {
response = await _dio.get(
url,
queryParameters: queryParameters,
options: options,
);
} else if (method.toUpperCase() == 'POST') {
response = await _dio.post(
url,
data: data,
queryParameters: queryParameters,
options: options,
);
} else {
throw UnsupportedError('HTTP method $method is not supported');
}
_debugLog('响应状态: ${response.statusCode}');
_debugLog('响应数据: ${response.data}');
return HttpResponse(
statusCode: response.statusCode ?? 0,
body: response.data is String
? response.data
: json.encode(response.data),
headers: response.headers.map.cast<String, String>(),
);
} on DioException catch (e) {
_debugLog('Dio异常: ${e.type} - ${e.message}');
String message;
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
message = '请求超时,请检查网络连接';
break;
case DioExceptionType.connectionError:
message = '网络连接失败,请检查网络设置';
break;
case DioExceptionType.badResponse:
message = '服务器错误: ${e.response?.statusCode} - ${e.response?.data}';
break;
default:
message = '请求失败: ${e.message}';
}
throw HttpException(message);
} catch (e) {
_debugLog('未知异常: $e');
throw HttpException('请求失败:$e');
}
}
}
/// HTTP响应类
class HttpResponse {
final int statusCode;
final String body;
final Map<String, String> headers;
HttpResponse({
required this.statusCode,
required this.body,
required this.headers,
});
/// 是否成功 (2xx状态码)
bool get isSuccess => statusCode >= 200 && statusCode < 300;
/// 解析JSON响应
Map<String, dynamic> get jsonData {
try {
return json.decode(body) as Map<String, dynamic>;
} catch (e) {
throw FormatException('Invalid JSON response: $body');
}
}
/// 获取响应消息
String get message {
if (isSuccess) {
try {
return jsonData['msg'] ?? 'Success';
} catch (e) {
return 'Success';
}
} else {
return body;
}
}
/// 获取响应数据
dynamic get data {
if (isSuccess) {
return jsonData['data'];
}
return null;
}
/// 获取响应代码
int get code {
if (isSuccess) {
try {
return jsonData['code'] ?? 0;
} catch (e) {
return 0;
}
}
return statusCode;
}
}
/// HTTP异常类
class HttpException implements Exception {
final String message;
const HttpException(this.message);
@override
String toString() => 'HttpException: $message';
}

View File

@@ -0,0 +1,350 @@
/// 时间: 2026-03-22
/// 功能: 诗词API服务
/// 介绍: 专门处理诗词相关的API请求包括获取诗词、点赞、搜索等功能
/// 最新变化: 新增 search.php 搜索接口(与 API_DOCUMENTATION.md 一致)
import 'http_client.dart';
import 'package:flutter/foundation.dart';
class PoetryApi {
static const String _endpoint = 'pms.php';
/// 全文搜索(见 lib/services/API_DOCUMENTATION.md 第二节)
static const String _searchEndpoint = 'searchs.php';
/// 获取随机诗词
static Future<PoetryResponse> getRandomPoetry({
String? dynasty,
String? tag,
}) async {
final queryParams = <String, dynamic>{};
if (dynasty != null && dynasty.isNotEmpty) {
queryParams['dyn'] = dynasty;
}
if (tag != null && tag.isNotEmpty) {
queryParams['tag'] = tag;
}
final response = await HttpClient.get(_endpoint, queryParameters: queryParams);
if (!response.isSuccess) {
throw HttpException('获取诗词失败1: ${response.message}');
}
final jsonData = response.jsonData;
if (jsonData['code'] != 0) {
throw HttpException(jsonData['msg'] ?? '获取诗词失败2');
}
return PoetryResponse.fromJson(jsonData);
}
/// 获取指定ID的诗词
static Future<PoetryResponse> getPoetryById(int id) async {
final response = await HttpClient.get(
_endpoint,
queryParameters: {'id': id.toString()},
);
if (!response.isSuccess) {
throw HttpException('获取诗词失败3: ${response.message}');
}
final jsonData = response.jsonData;
if (jsonData['code'] != 0) {
throw HttpException(jsonData['msg'] ?? '获取诗词失败4');
}
return PoetryResponse.fromJson(jsonData);
}
/// 点赞或取消点赞诗词
static Future<PoetryResponse> toggleLike(int id) async {
final response = await HttpClient.get(
_endpoint,
queryParameters: {
'id': id.toString(),
'like': '', // 无值参数
},
);
if (!response.isSuccess) {
throw HttpException('点赞失败: ${response.message}');
}
final jsonData = response.jsonData;
if (jsonData['code'] != 0) {
throw HttpException(jsonData['msg'] ?? '点赞失败');
}
return PoetryResponse.fromJson(jsonData);
}
/// 直接点赞指定诗词通过lid参数
static Future<PoetryResponse> likePoetry(int lid) async {
final response = await HttpClient.get(
_endpoint,
queryParameters: {'lid': lid.toString()},
);
if (!response.isSuccess) {
throw HttpException('点赞失败: ${response.message}');
}
final jsonData = response.jsonData;
if (jsonData['code'] != 0) {
throw HttpException(jsonData['msg'] ?? '点赞失败');
}
return PoetryResponse.fromJson(jsonData);
}
/// 按朝代获取诗词
static Future<PoetryResponse> getPoetryByDynasty(String dynasty) async {
return getRandomPoetry(dynasty: dynasty);
}
/// 按标签获取诗词
static Future<PoetryResponse> getPoetryByTag(String tag) async {
return getRandomPoetry(tag: tag);
}
/// 按朝代和标签获取诗词
static Future<PoetryResponse> getPoetryByDynastyAndTag(String dynasty, String tag) async {
return getRandomPoetry(dynasty: dynasty, tag: tag);
}
/// 诗词搜索GET `searchs.php`keyword、fields、page、size
static Future<SearchPoetryResult> searchPoetry({
required String q,
String field = '',
int page = 1,
int limit = 20,
}) async {
final keyword = q.trim();
final response = await HttpClient.get(
_searchEndpoint,
queryParameters: {
'keyword': keyword,
if (field.isNotEmpty) 'fields': field,
'page': page.toString(),
'size': limit.toString(),
},
);
if (!response.isSuccess) {
throw HttpException('搜索失败: ${response.message}');
}
final jsonData = response.jsonData;
if (jsonData['code'] != 0) {
throw HttpException(jsonData['msg']?.toString() ?? '搜索失败');
}
final raw = jsonData['data'];
if (raw is! Map<String, dynamic>) {
return SearchPoetryResult(
total: 0,
page: page,
limit: limit,
list: const [],
);
}
final listRaw = raw['results'];
final list = <PoetryData>[];
if (listRaw is List) {
for (final item in listRaw) {
if (item is Map<String, dynamic>) {
list.add(PoetryData.fromJson(item));
} else if (item is Map) {
list.add(PoetryData.fromJson(Map<String, dynamic>.from(item)));
}
}
}
// 从 pagination 中获取分页信息
final pagination = raw['pagination'] as Map<String, dynamic>? ?? {};
final totalCount = int.tryParse(pagination['total_count']?.toString() ?? '0') ?? 0;
final currentPage = int.tryParse(pagination['current_page']?.toString() ?? '$page') ?? page;
final pageSize = int.tryParse(pagination['page_size']?.toString() ?? '$limit') ?? limit;
return SearchPoetryResult(
total: totalCount,
page: currentPage,
limit: pageSize,
list: list,
);
}
}
/// 搜索结果分页数据(对应 searchs.php 返回 data
class SearchPoetryResult {
final int total;
final int page;
final int limit;
final List<PoetryData> list;
const SearchPoetryResult({
required this.total,
required this.page,
required this.limit,
required this.list,
});
bool get hasNextPage => page * limit < total;
}
/// 诗词响应数据模型
class PoetryResponse {
final int code;
final String message;
final PoetryData? data;
PoetryResponse({
required this.code,
required this.message,
this.data,
});
factory PoetryResponse.fromJson(Map<String, dynamic> json) {
return PoetryResponse(
code: json['code'] ?? 0,
message: json['msg'] ?? '',
data: json['data'] != null ? PoetryData.fromJson(json['data']) : null,
);
}
Map<String, dynamic> toJson() {
return {
'code': code,
'msg': message,
'data': data?.toJson(),
};
}
}
/// 诗词数据模型
class PoetryData {
final int id;
final String name;// 精选诗句
final String alias; // 朝代
final String keywords; // 标签
final String introduce; // 译文/介绍
final String drtime; // 原文
final int like; // 点赞数
final String url; // 诗人和<标题>
final int tui; // 是否推荐
final int star; // 星级
final int hitsTotal; // 总浏览数
final int hitsMonth; // 月浏览数
final int hitsDay; // 日浏览数
final String date; // 最后统计日期
final String datem; // 最后统计月份
final String time; // 收录时间
final String createTime; // 创建时间
final String updateTime; // 更新时间
PoetryData({
required this.id,
required this.name,
required this.alias,
required this.keywords,
required this.introduce,
required this.drtime,
required this.like,
required this.url,
required this.tui,
required this.star,
required this.hitsTotal,
required this.hitsMonth,
required this.hitsDay,
required this.date,
required this.datem,
required this.time,
required this.createTime,
required this.updateTime,
});
factory PoetryData.fromJson(Map<String, dynamic> json) {
// 添加调试信息
if (kDebugMode) {
print('PoetryData.fromJson: 输入JSON = $json');
}
try {
final poetryData = PoetryData(
id: int.tryParse(json['id'].toString()) ?? 0,
name: json['name']?.toString() ?? '',
alias: json['alias']?.toString() ?? '',
keywords: json['keywords']?.toString() ?? '',
introduce: json['introduce']?.toString() ?? '',
drtime: json['drtime']?.toString() ?? '',
like: int.tryParse(json['like'].toString()) ?? 0,
url: json['url']?.toString() ?? '',
tui: int.tryParse(json['tui'].toString()) ?? 0,
star: int.tryParse(json['star'].toString()) ?? 0,
hitsTotal: int.tryParse(json['hits_total'].toString()) ?? 0,
hitsMonth: int.tryParse(json['hits_month'].toString()) ?? 0,
hitsDay: int.tryParse(json['hits_day'].toString()) ?? 0,
date: json['date']?.toString() ?? '',
datem: json['datem']?.toString() ?? '',
time: json['time']?.toString() ?? '',
createTime: json['create_time']?.toString() ?? '',
updateTime: json['update_time']?.toString() ?? '',
);
if (kDebugMode) {
print('PoetryData.fromJson: 解析成功');
}
return poetryData;
} catch (e) {
if (kDebugMode) {
print('PoetryData.fromJson: 解析失败 - $e');
}
rethrow;
}
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'alias': alias,
'keywords': keywords,
'introduce': introduce,
'drtime': drtime,
'like': like,
'url': url,
'tui': tui,
'star': star,
'hits_total': hitsTotal,
'hits_month': hitsMonth,
'hits_day': hitsDay,
'date': date,
'datem': datem,
'time': time,
'create_time': createTime,
'update_time': updateTime,
};
}
/// 获取标签列表
List<String> get keywordList {
if (keywords.isEmpty) return [];
return keywords.split(',').map((k) => k.trim()).where((k) => k.isNotEmpty).toList();
}
/// 生成星级显示
String get starDisplay {
if (star <= 0) return '';
if (star >= 5) return '🌟';
return '';
}
/// 是否为推荐内容
bool get isRecommended => tui == 1;
}

View File

@@ -0,0 +1,584 @@
/// 时间: 2026-03-29
/// 功能: 投票API服务
/// 介绍: 专门处理投票相关的API请求包括获取投票列表、投票详情、提交投票等功能
/// 最新变化: 添加Cookie管理器支持PHP Session认证
import 'dart:convert';
import 'dart:io' as io show Platform;
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:cookie_jar/cookie_jar.dart';
import 'package:flutter/foundation.dart';
import 'package:platform_info/platform_info.dart';
import 'package:shared_preferences/shared_preferences.dart';
class VoteApi {
static const String _baseUrl = 'https://poe.vogov.cn/toupiao/';
static const String _voteEndpoint = 'vote_api.php';
static const String _userEndpoint = 'tapi.php';
static const Duration _timeout = Duration(seconds: 30);
static final BaseOptions _options = BaseOptions(
baseUrl: _baseUrl,
connectTimeout: _timeout,
receiveTimeout: _timeout,
sendTimeout: _timeout,
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json',
'User-Agent': 'Poes-Flutter/1.0.0',
},
);
static final CookieJar _cookieJar = CookieJar();
static final Dio _dio = Dio(_options)
..interceptors.add(CookieManager(_cookieJar));
static void _debugLog(String message) {
if (kDebugMode) {
print('VoteApi: $message');
}
}
static Future<Map<String, dynamic>> _get(
String path, {
Map<String, dynamic>? queryParameters,
}) async {
try {
final url = '$_baseUrl$path';
_debugLog('GET $url');
if (queryParameters != null) {
_debugLog('查询参数: $queryParameters');
}
final response = await _dio.get(url, queryParameters: queryParameters);
_debugLog('响应: ${response.data}');
return response.data as Map<String, dynamic>;
} on DioException catch (e) {
_debugLog('Dio异常: ${e.type} - ${e.message}');
throw Exception('请求失败: ${e.message}');
} catch (e) {
_debugLog('未知异常: $e');
throw Exception('请求失败: $e');
}
}
static Future<Map<String, dynamic>> _post(
String path, {
Map<String, dynamic>? data,
}) async {
try {
final url = '$_baseUrl$path';
_debugLog('POST $url');
if (data != null) {
_debugLog('请求数据: $data');
}
final response = await _dio.post(url, data: data);
_debugLog('响应: ${response.data}');
return response.data as Map<String, dynamic>;
} on DioException catch (e) {
_debugLog('Dio异常: ${e.type} - ${e.message}');
throw Exception('请求失败: ${e.message}');
} catch (e) {
_debugLog('未知异常: $e');
throw Exception('请求失败: $e');
}
}
/// 获取用户登录状态
static Future<bool> isLoggedIn() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool('vote_logged_in') ?? false;
}
/// 获取用户信息
static Future<Map<String, dynamic>?> getUserInfo() async {
final prefs = await SharedPreferences.getInstance();
final userData = prefs.getString('vote_user');
if (userData != null) {
try {
return jsonDecode(userData) as Map<String, dynamic>;
} catch (e) {
return null;
}
}
return null;
}
/// 保存用户登录状态
static Future<void> saveUserLogin(Map<String, dynamic> userData) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('vote_logged_in', true);
await prefs.setString('vote_user', jsonEncode(userData));
}
/// 清除用户登录状态
static Future<void> clearUserLogin() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('vote_logged_in');
await prefs.remove('vote_user');
}
/// 用户登录
static Future<VoteApiResponse> login({
required String username,
required String password,
}) async {
final jsonData = await _post(
_userEndpoint,
data: {'act': 'login', 'username': username, 'password': password},
);
if (jsonData['code'] != 0) {
throw Exception(jsonData['msg'] ?? '登录失败');
}
final userData = jsonData['data'];
if (userData != null) {
await saveUserLogin(userData);
}
return VoteApiResponse.fromJson(jsonData);
}
/// 用户注册
static Future<VoteApiResponse> register({
required String username,
required String password,
String? userIdentifier,
}) async {
final data = <String, dynamic>{
'act': 'register',
'username': username,
'password': password,
};
if (userIdentifier != null) {
data['user_identifier'] = userIdentifier;
}
final jsonData = await _post(_userEndpoint, data: data);
if (jsonData['code'] != 0) {
throw Exception(jsonData['msg'] ?? '注册失败');
}
return VoteApiResponse.fromJson(jsonData);
}
/// 获取投票列表
static Future<VoteListResponse> getVoteList({
int page = 1,
int pageSize = 10,
int status = 1,
int type = -1,
}) async {
final jsonData = await _get(
_voteEndpoint,
queryParameters: {
'act': 'getVoteList',
'page': page.toString(),
'pageSize': pageSize.toString(),
'status': status.toString(),
'type': type.toString(),
},
);
if (jsonData['code'] != 0) {
throw Exception(jsonData['msg'] ?? '获取投票列表失败');
}
return VoteListResponse.fromJson(jsonData);
}
/// 获取投票详情
static Future<VoteDetailResponse> getVoteDetail(int id) async {
final jsonData = await _get(
_voteEndpoint,
queryParameters: {'act': 'getVoteDetail', 'id': id.toString()},
);
if (jsonData['code'] != 0) {
throw Exception(jsonData['msg'] ?? '获取投票详情失败');
}
return VoteDetailResponse.fromJson(jsonData);
}
/// 提交投票
static Future<VoteApiResponse> submitVote({
required int topicId,
required List<int> options,
}) async {
final jsonData = await _post(
_voteEndpoint,
data: {'act': 'submitVote', 'topic_id': topicId, 'options': options},
);
if (jsonData['code'] != 0) {
throw Exception(jsonData['msg'] ?? '提交投票失败');
}
return VoteApiResponse.fromJson(jsonData);
}
/// 获取投票结果
static Future<VoteResultResponse> getVoteResult(int id) async {
final jsonData = await _get(
_voteEndpoint,
queryParameters: {'act': 'getVoteResult', 'id': id.toString()},
);
if (jsonData['code'] != 0) {
throw Exception(jsonData['msg'] ?? '获取投票结果失败');
}
return VoteResultResponse.fromJson(jsonData);
}
/// 获取用户投票记录
static Future<VoteListResponse> getUserVotes({
int page = 1,
int pageSize = 10,
}) async {
final jsonData = await _get(
_voteEndpoint,
queryParameters: {
'act': 'getUserVotes',
'page': page.toString(),
'pageSize': pageSize.toString(),
},
);
if (jsonData['code'] != 0) {
throw Exception(jsonData['msg'] ?? '获取用户投票记录失败');
}
return VoteListResponse.fromJson(jsonData);
}
/// 获取设备类型作为user_identifier
static Future<String> getDeviceType() async {
final platform = Platform.instance;
bool isHarmonyOS = false;
String platformName = 'Unknown';
try {
final String osName = io.Platform.operatingSystem;
final String osVersion = io.Platform.operatingSystemVersion.toLowerCase();
if (osName == 'ohos' ||
osName == 'harmonyos' ||
osName == 'openharmony') {
platformName = 'HarmonyOS';
isHarmonyOS = true;
} else if (io.Platform.isAndroid) {
platformName = 'Android';
if (osVersion.contains('harmony') ||
osVersion.contains('ohos') ||
osVersion.contains('openharmony')) {
platformName = 'HarmonyOS';
isHarmonyOS = true;
}
} else if (io.Platform.isIOS) {
platformName = 'iOS';
} else if (io.Platform.isMacOS) {
platformName = 'macOS';
} else if (io.Platform.isWindows) {
platformName = 'Windows';
} else if (io.Platform.isLinux) {
platformName = 'Linux';
} else if (io.Platform.isFuchsia) {
platformName = 'Fuchsia';
} else {
platformName = osName[0].toUpperCase() + osName.substring(1);
}
} catch (e) {
platformName = switch (platform.operatingSystem) {
const OperatingSystem.android() => 'Android',
const OperatingSystem.fuchsia() => 'Fuchsia',
const OperatingSystem.iOS() => 'iOS',
const OperatingSystem.linux() => 'Linux',
const OperatingSystem.macOS() => 'macOS',
const OperatingSystem.windows() => 'Windows',
const OperatingSystem.unknown() || _ => 'Unknown',
};
}
String deviceType = 'Unknown';
if (isHarmonyOS) {
final String osName = io.Platform.operatingSystem;
if (osName == 'ohos') {
deviceType = 'OHOS';
} else if (osName == 'harmonyos') {
deviceType = 'HarmonyOS';
} else if (osName == 'openharmony') {
deviceType = 'OpenHarmony';
} else {
deviceType = 'HarmonyOS';
}
} else {
deviceType =
platform.when<String?>(
mobile: () => 'Mobile',
desktop: () => 'Desktop',
js: () => 'Web',
orElse: () => null,
) ??
'Unknown';
}
return '$platformName-$deviceType-Flutter';
}
}
/// 基础API响应
class VoteApiResponse {
final int code;
final String message;
final dynamic data;
VoteApiResponse({required this.code, required this.message, this.data});
factory VoteApiResponse.fromJson(Map<String, dynamic> json) {
return VoteApiResponse(
code: json['code'] ?? 0,
message: json['msg'] ?? '',
data: json['data'],
);
}
bool get isSuccess => code == 0;
}
/// 投票列表响应
class VoteListResponse {
final List<VoteItem> list;
final VotePagination pagination;
VoteListResponse({required this.list, required this.pagination});
factory VoteListResponse.fromJson(Map<String, dynamic> json) {
final data = json['data'] as Map<String, dynamic>? ?? {};
final listRaw = data['list'] as List? ?? [];
final list = listRaw.map((item) => VoteItem.fromJson(item)).toList();
final pagination = VotePagination.fromJson(
data['pagination'] as Map<String, dynamic>? ?? {},
);
return VoteListResponse(list: list, pagination: pagination);
}
}
/// 投票项
class VoteItem {
final int id;
final String title;
final String? idesc;
final int itype;
final int maxtime;
final int status;
final int iview;
final String addtime;
final String statime;
final String endtime;
final String? statusText;
final String? statusClass;
VoteItem({
required this.id,
required this.title,
this.idesc,
required this.itype,
required this.maxtime,
required this.status,
required this.iview,
required this.addtime,
required this.statime,
required this.endtime,
this.statusText,
this.statusClass,
});
factory VoteItem.fromJson(Map<String, dynamic> json) {
return VoteItem(
id: int.tryParse(json['id'].toString()) ?? 0,
title: json['title']?.toString() ?? '',
idesc: json['idesc']?.toString(),
itype: int.tryParse(json['itype'].toString()) ?? 0,
maxtime: int.tryParse(json['maxtime'].toString()) ?? 0,
status: int.tryParse(json['status'].toString()) ?? 0,
iview: int.tryParse(json['iview'].toString()) ?? 0,
addtime: json['addtime']?.toString() ?? '',
statime: json['statime']?.toString() ?? '',
endtime: json['endtime']?.toString() ?? '',
statusText: json['status_text']?.toString(),
statusClass: json['status_class']?.toString(),
);
}
bool get isSingleChoice => itype == 0;
bool get isMultipleChoice => itype == 1;
bool get isActive => status == 1;
}
/// 分页信息
class VotePagination {
final int total;
final int page;
final int pageSize;
final int totalPage;
final int offset;
VotePagination({
required this.total,
required this.page,
required this.pageSize,
required this.totalPage,
required this.offset,
});
factory VotePagination.fromJson(Map<String, dynamic> json) {
return VotePagination(
total: int.tryParse(json['total'].toString()) ?? 0,
page: int.tryParse(json['page'].toString()) ?? 1,
pageSize: int.tryParse(json['pageSize'].toString()) ?? 10,
totalPage: int.tryParse(json['totalPage'].toString()) ?? 1,
offset: int.tryParse(json['offset'].toString()) ?? 0,
);
}
}
/// 投票详情响应
class VoteDetailResponse {
final VoteItem vote;
final List<VoteOption> options;
final bool hasVoted;
final List<int> userVotes;
final bool canVote;
VoteDetailResponse({
required this.vote,
required this.options,
required this.hasVoted,
required this.userVotes,
required this.canVote,
});
factory VoteDetailResponse.fromJson(Map<String, dynamic> json) {
final data = json['data'] as Map<String, dynamic>? ?? {};
final voteRaw = data['vote'] as Map<String, dynamic>? ?? {};
final optionsRaw = data['options'] as List? ?? [];
final userVotesRaw = data['userVotes'] as List? ?? [];
return VoteDetailResponse(
vote: VoteItem.fromJson(voteRaw),
options: optionsRaw.map((item) => VoteOption.fromJson(item)).toList(),
hasVoted: data['hasVoted'] as bool? ?? false,
userVotes: userVotesRaw
.map((id) => int.tryParse(id.toString()) ?? 0)
.where((id) => id > 0)
.toList(),
canVote: data['canVote'] as bool? ?? true,
);
}
}
/// 投票选项
class VoteOption {
final int id;
final int topicId;
final String name;
final String? idesc;
final String? imgs;
final int sort;
VoteOption({
required this.id,
required this.topicId,
required this.name,
this.idesc,
this.imgs,
required this.sort,
});
factory VoteOption.fromJson(Map<String, dynamic> json) {
return VoteOption(
id: int.tryParse(json['id'].toString()) ?? 0,
topicId: int.tryParse(json['topic_id'].toString()) ?? 0,
name: json['name']?.toString() ?? '',
idesc: json['idesc']?.toString(),
imgs: json['imgs']?.toString(),
sort: int.tryParse(json['sort'].toString()) ?? 0,
);
}
}
/// 投票结果响应
class VoteResultResponse {
final VoteItem vote;
final List<VoteResultOption> options;
final int totalVotes;
final bool hasVoted;
final List<int> userVotes;
VoteResultResponse({
required this.vote,
required this.options,
required this.totalVotes,
required this.hasVoted,
required this.userVotes,
});
factory VoteResultResponse.fromJson(Map<String, dynamic> json) {
final data = json['data'] as Map<String, dynamic>? ?? {};
final voteRaw = data['vote'] as Map<String, dynamic>? ?? {};
final optionsRaw = data['options'] as List? ?? [];
final userVotesRaw = data['userVotes'] as List? ?? [];
return VoteResultResponse(
vote: VoteItem.fromJson(voteRaw),
options: optionsRaw
.map((item) => VoteResultOption.fromJson(item))
.toList(),
totalVotes: int.tryParse(data['totalVotes'].toString()) ?? 0,
hasVoted: data['hasVoted'] as bool? ?? false,
userVotes: userVotesRaw
.map((id) => int.tryParse(id.toString()) ?? 0)
.where((id) => id > 0)
.toList(),
);
}
}
/// 投票结果选项
class VoteResultOption {
final int id;
final String name;
final String? idesc;
final String? imgs;
final int count;
final int percentage;
VoteResultOption({
required this.id,
required this.name,
this.idesc,
this.imgs,
required this.count,
required this.percentage,
});
factory VoteResultOption.fromJson(Map<String, dynamic> json) {
return VoteResultOption(
id: int.tryParse(json['id'].toString()) ?? 0,
name: json['name']?.toString() ?? '',
idesc: json['idesc']?.toString(),
imgs: json['imgs']?.toString(),
count: int.tryParse(json['count'].toString()) ?? 0,
percentage: int.tryParse(json['percentage'].toString()) ?? 0,
);
}
}

View File

@@ -0,0 +1,252 @@
import 'package:flutter/material.dart';
class ResponsiveLayout extends StatelessWidget {
final Widget mobile;
final Widget? tablet;
final Widget? desktop;
final Widget? largeDesktop;
const ResponsiveLayout({
super.key,
required this.mobile,
this.tablet,
this.desktop,
this.largeDesktop,
});
static bool isMobile(BuildContext context) {
return MediaQuery.of(context).size.width < 768;
}
static bool isTablet(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return width >= 768 && width < 1024;
}
static bool isDesktop(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return width >= 1024 && width < 1440;
}
static bool isLargeDesktop(BuildContext context) {
return MediaQuery.of(context).size.width >= 1440;
}
static bool isPortrait(BuildContext context) {
return MediaQuery.of(context).orientation == Orientation.portrait;
}
static bool isLandscape(BuildContext context) {
return MediaQuery.of(context).orientation == Orientation.landscape;
}
static double screenWidth(BuildContext context) {
return MediaQuery.of(context).size.width;
}
static double screenHeight(BuildContext context) {
return MediaQuery.of(context).size.height;
}
static EdgeInsets safePadding(BuildContext context) {
return MediaQuery.of(context).padding;
}
static double safeAreaHeight(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
final paddingTop = MediaQuery.of(context).padding.top;
final paddingBottom = MediaQuery.of(context).padding.bottom;
return screenHeight - paddingTop - paddingBottom;
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
if (width >= 1440 && largeDesktop != null) {
return largeDesktop!;
} else if (width >= 1024 && desktop != null) {
return desktop!;
} else if (width >= 768 && tablet != null) {
return tablet!;
} else {
return mobile;
}
}
}
class ResponsiveContainer extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry? padding;
final double? maxWidth;
final bool centerContent;
const ResponsiveContainer({
super.key,
required this.child,
this.padding,
this.maxWidth,
this.centerContent = true,
});
@override
Widget build(BuildContext context) {
final screenWidth = ResponsiveLayout.screenWidth(context);
final effectiveMaxWidth = maxWidth ?? (screenWidth < 768 ? screenWidth : 1200.0);
Widget container = Container(
width: double.infinity,
constraints: BoxConstraints(
maxWidth: effectiveMaxWidth,
),
padding: padding ?? const EdgeInsets.all(16),
child: child,
);
if (centerContent && screenWidth > effectiveMaxWidth) {
container = Center(child: container);
}
return container;
}
}
class ResponsiveGrid extends StatelessWidget {
final List<Widget> children;
final int mobileColumns;
final int tabletColumns;
final int desktopColumns;
final int largeDesktopColumns;
final double spacing;
final double runSpacing;
final EdgeInsets? padding;
const ResponsiveGrid({
super.key,
required this.children,
this.mobileColumns = 1,
this.tabletColumns = 2,
this.desktopColumns = 3,
this.largeDesktopColumns = 4,
this.spacing = 16.0,
this.runSpacing = 16.0,
this.padding,
});
@override
Widget build(BuildContext context) {
int columns;
if (ResponsiveLayout.isLargeDesktop(context)) {
columns = largeDesktopColumns;
} else if (ResponsiveLayout.isDesktop(context)) {
columns = desktopColumns;
} else if (ResponsiveLayout.isTablet(context)) {
columns = tabletColumns;
} else {
columns = mobileColumns;
}
return LayoutBuilder(
builder: (context, constraints) {
final childAspectRatio = (constraints.maxWidth - (spacing * (columns - 1))) / columns / 200;
return Padding(
padding: padding ?? EdgeInsets.zero,
child: GridView.count(
crossAxisCount: columns,
crossAxisSpacing: spacing,
mainAxisSpacing: runSpacing,
childAspectRatio: childAspectRatio,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: children,
),
);
},
);
}
}
class ResponsiveCard extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry? padding;
final double? borderRadius;
final Color? backgroundColor;
final VoidCallback? onTap;
const ResponsiveCard({
super.key,
required this.child,
this.padding,
this.borderRadius,
this.backgroundColor,
this.onTap,
});
@override
Widget build(BuildContext context) {
final effectiveBorderRadius = borderRadius ?? (ResponsiveLayout.isMobile(context) ? 8.0 : 12.0);
final effectivePadding = padding ?? EdgeInsets.all(ResponsiveLayout.isMobile(context) ? 12.0 : 16.0);
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(effectiveBorderRadius),
),
color: backgroundColor,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(effectiveBorderRadius),
child: Padding(
padding: effectivePadding,
child: child,
),
),
);
}
}
class ResponsiveLayoutBuilder extends StatelessWidget {
final Widget Function(BuildContext context, ScreenSize screenSize) builder;
const ResponsiveLayoutBuilder({
super.key,
required this.builder,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
ScreenSize screenSize;
if (constraints.maxWidth >= 1440) {
screenSize = ScreenSize.largeDesktop;
} else if (constraints.maxWidth >= 1024) {
screenSize = ScreenSize.desktop;
} else if (constraints.maxWidth >= 768) {
screenSize = ScreenSize.tablet;
} else {
screenSize = ScreenSize.mobile;
}
return builder(context, screenSize);
},
);
}
}
enum ScreenSize {
mobile,
tablet,
desktop,
largeDesktop,
}
extension ScreenSizeExtension on ScreenSize {
bool get isMobile => this == ScreenSize.mobile;
bool get isTablet => this == ScreenSize.tablet;
bool get isDesktop => this == ScreenSize.desktop;
bool get isLargeDesktop => this == ScreenSize.largeDesktop;
bool get isDesktopOrLarge => isDesktop || isLargeDesktop;
bool get isTabletOrLarger => isTablet || isDesktopOrLarge;
}

View File

@@ -0,0 +1,207 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class ScreenAdapter {
static void setPreferredOrientations() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
static void enableSystemUIOverlay() {
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: [
SystemUiOverlay.top,
SystemUiOverlay.bottom,
],
);
}
static void enableImmersiveMode() {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
}
static void setSystemUIOverlayStyle({
Color? statusBarColor,
Brightness? statusBarIconBrightness,
}) {
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
statusBarColor: statusBarColor ?? Colors.transparent,
statusBarIconBrightness: statusBarIconBrightness ?? Brightness.dark,
),
);
}
static bool isSplitScreen(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
final aspectRatio = screenWidth / screenHeight;
// 分屏模式通常具有特殊的宽高比
return aspectRatio > 2.0 || aspectRatio < 0.5;
}
static bool isPopupWindow(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
// 小窗模式通常屏幕尺寸较小
return screenWidth < 600 || screenHeight < 800;
}
static bool isTabletMode(BuildContext context) {
final shortestSide = MediaQuery.of(context).size.shortestSide;
return shortestSide >= 600;
}
static void adaptForSplitScreen(BuildContext context) {
if (isSplitScreen(context)) {
// 分屏模式下的适配
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: [SystemUiOverlay.top],
);
}
}
static void adaptForPopupWindow(BuildContext context) {
if (isPopupWindow(context)) {
// 小窗模式下的适配
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: [SystemUiOverlay.top],
);
}
}
static void handleOrientationChange(BuildContext context) {
final orientation = MediaQuery.of(context).orientation;
if (orientation == Orientation.landscape) {
// 横屏模式适配
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: [SystemUiOverlay.top],
);
} else {
// 竖屏模式适配
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: [
SystemUiOverlay.top,
SystemUiOverlay.bottom,
],
);
}
}
static Size getAdaptiveSize(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final size = mediaQuery.size;
final orientation = mediaQuery.orientation;
if (isSplitScreen(context)) {
// 分屏模式下的尺寸调整
return Size(
size.width * 0.9,
size.height * 0.85,
);
}
if (isPopupWindow(context)) {
// 小窗模式下的尺寸调整
return Size(
size.width * 0.95,
size.height * 0.9,
);
}
if (orientation == Orientation.landscape) {
// 横屏模式下的尺寸调整
return Size(
size.width * 0.95,
size.height * 0.9,
);
}
return size;
}
static EdgeInsets getAdaptivePadding(BuildContext context) {
final size = MediaQuery.of(context).size;
if (isSplitScreen(context)) {
return EdgeInsets.symmetric(
horizontal: size.width * 0.02,
vertical: size.height * 0.02,
);
}
if (isPopupWindow(context)) {
return EdgeInsets.symmetric(
horizontal: size.width * 0.03,
vertical: size.height * 0.03,
);
}
return const EdgeInsets.all(16.0);
}
static double getAdaptiveFontSize(BuildContext context, double baseFontSize) {
final screenWidth = MediaQuery.of(context).size.width;
if (isSplitScreen(context)) {
return baseFontSize * 0.9;
}
if (isPopupWindow(context)) {
return baseFontSize * 0.85;
}
if (screenWidth < 600) {
return baseFontSize * 0.9;
} else if (screenWidth > 1200) {
return baseFontSize * 1.1;
}
return baseFontSize;
}
static int getAdaptiveGridColumns(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
if (isSplitScreen(context)) {
return 2;
}
if (isPopupWindow(context)) {
return 1;
}
if (screenWidth >= 1440) {
return 6;
} else if (screenWidth >= 1024) {
return 4;
} else if (screenWidth >= 768) {
return 3;
} else {
return 2;
}
}
static void setupAdaptiveConfig(BuildContext context) {
// 设置自适应配置
setPreferredOrientations();
enableSystemUIOverlay();
// 根据屏幕状态进行适配
adaptForSplitScreen(context);
adaptForPopupWindow(context);
handleOrientationChange(context);
}
}