Initial commit: Flutter 无书应用项目
This commit is contained in:
253
lib/utils/app_theme.dart
Normal file
253
lib/utils/app_theme.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
16
lib/utils/flutter_compatibility_fix.dart
Normal file
16
lib/utils/flutter_compatibility_fix.dart
Normal 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 值
|
||||
}
|
||||
}
|
||||
239
lib/utils/force_guide_checker.dart
Normal file
239
lib/utils/force_guide_checker.dart
Normal 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';
|
||||
}
|
||||
218
lib/utils/http/http_client.dart
Normal file
218
lib/utils/http/http_client.dart
Normal 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';
|
||||
}
|
||||
350
lib/utils/http/poetry_api.dart
Normal file
350
lib/utils/http/poetry_api.dart
Normal 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;
|
||||
}
|
||||
584
lib/utils/http/vote_api.dart
Normal file
584
lib/utils/http/vote_api.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
252
lib/utils/responsive_layout.dart
Normal file
252
lib/utils/responsive_layout.dart
Normal 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;
|
||||
}
|
||||
207
lib/utils/screen_adapter.dart
Normal file
207
lib/utils/screen_adapter.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user