Initial commit: Flutter 无书应用项目
This commit is contained in:
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user