Files
fuck-zhijiao/lib/main.dart
2025-12-24 16:54:43 +08:00

785 lines
27 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:crypto/crypto.dart';
import 'dart:developer' as developer;
// Network Service for API calls
class ApiService {
final String _baseUrl = 'https://m-zjt.kefang.net/api';
Map<String, String> _getHeaders(String token) {
return {
'User-Agent': 'Dart/3.6 (dart:io)',
'Accept-Encoding': 'gzip',
'Content-Type': 'application/x-www-form-urlencoded',
'Cookie': 'token=$token',
'host': 'm-zjt.kefang.net',
};
}
Future<Map<String, dynamic>> getHomePageData(String token) async {
final url = Uri.parse('$_baseUrl/zms/zhijiaotong/page/homePageData');
final headers = _getHeaders(token);
developer.log('--- 🚀 [HOME PAGE] Request ---', name: 'NetworkDebug');
developer.log('URL: $url', name: 'NetworkDebug');
developer.log('Headers: $headers', name: 'NetworkDebug');
try {
final response = await http.get(url, headers: headers);
developer.log('--- ✅ [HOME PAGE] Response ---', name: 'NetworkDebug');
developer.log('Status Code: ${response.statusCode}', name: 'NetworkDebug');
developer.log('Body: ${response.body}', name: 'NetworkDebug');
developer.log('------------------------------', name: 'NetworkDebug');
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
return {'code': response.statusCode, 'message': 'HTTP Error'};
}
} catch (e) {
developer.log('--- ❌ [HOME PAGE] Error ---', error: e, name: 'NetworkDebug');
return {'code': -1, 'message': e.toString()};
}
}
Future<Map<String, dynamic>> punchCard(String token, String attendanceId) async {
final url = Uri.parse('$_baseUrl/zms/attendance/attendanceRecord/punchCourseByStudent');
final headers = _getHeaders(token);
final body = {'attendanceId': attendanceId};
developer.log('--- 🚀 [PUNCH CARD] Request ---', name: 'NetworkDebug');
developer.log('URL: $url', name: 'NetworkDebug');
developer.log('Headers: $headers', name: 'NetworkDebug');
developer.log('Body: $body', name: 'NetworkDebug');
try {
final response = await http.put(url, headers: headers, body: body);
developer.log('--- ✅ [PUNCH CARD] Response ---', name: 'NetworkDebug');
developer.log('Status Code: ${response.statusCode}', name: 'NetworkDebug');
developer.log('Body: ${response.body}', name: 'NetworkDebug');
developer.log('-------------------------------', name: 'NetworkDebug');
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
return {'code': response.statusCode, 'message': 'HTTP Error'};
}
} catch (e) {
developer.log('--- ❌ [PUNCH CARD] Error ---', error: e, name: 'NetworkDebug');
return {'code': -1, 'message': e.toString()};
}
}
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '打卡',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final ApiService _apiService = ApiService();
String _loginStatus = '未登录';
String _userInfo = '';
String _punchCardStatus = '打卡未开启';
String _contentId = '';
// State for date navigation
DateTime _currentDate = DateTime.now();
// Settings variables
String _token = '';
String _tokens = '';
String _account = '';
String _password = '';
String _deviceId = '';
bool _autoLoginEnabled = false;
bool _isTokenMode = true;
bool _pollingEnabled = false;
Timer? _pollingTimer;
// New state for punch card times
Map<String, String> _punchCardTimes = {};
@override
void initState() {
super.initState();
developer.log('Start of UTC day timestamp: ${_getStartOfDayTimestamp(_currentDate)}', name: 'Lifecycle');
_loadSettings();
}
@override
void dispose() {
_stopPolling();
super.dispose();
}
int _getStartOfDayTimestamp(DateTime date) {
final localStart = DateTime(date.year, date.month, date.day, 0, 0, 0);
return localStart.millisecondsSinceEpoch;
}
String _formatTimestamp(int timestamp) {
if (timestamp == 0) return "--:--";
// Convert to local time for display
final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp).toLocal();
final hour = dateTime.hour.toString().padLeft(2, '0');
final minute = dateTime.minute.toString().padLeft(2, '0');
return '$hour:$minute';
}
String _generateMd5(String input) {
return md5.convert(utf8.encode(input)).toString();
}
Map<String, String> _getHeaders() {
return {
'User-Agent': 'Dart/3.6 (dart:io)',
'Accept-Encoding': 'gzip',
'Content-Type': 'application/x-www-form-urlencoded',
'Cookie': 'token=$_token',
'host': 'm-zjt.kefang.net',
};
}
Widget _buildPunchCardInfoGrid() {
return GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
childAspectRatio: 2.5,
padding: const EdgeInsets.all(8.0),
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: <Widget>[
_buildGridItem("上午首次", _punchCardTimes['上午首次'] ?? "--:--"),
_buildGridItem("上午灵活", _punchCardTimes['上午灵活'] ?? "--:--"),
_buildGridItem("下午首次", _punchCardTimes['下午首次'] ?? "--:--"),
_buildGridItem("下午灵活", _punchCardTimes['下午灵活'] ?? "--:--"),
],
);
}
Widget _buildGridItem(String title, String time) {
return Card(
elevation: 2.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.normal)),
const SizedBox(height: 5),
Text(time, style: const TextStyle(fontSize: 18, color: Colors.blueGrey, fontWeight: FontWeight.bold)),
],
),
);
}
bool _isToday(DateTime date) {
final now = DateTime.now();
return date.year == now.year && date.month == now.month && date.day == now.day;
}
String _formatDisplayDate(DateTime date) {
if (_isToday(date)) {
return '今天';
}
final yesterday = DateTime.now().subtract(const Duration(days: 1));
if (date.year == yesterday.year && date.month == yesterday.month && date.day == yesterday.day) {
return '昨天';
}
return '${date.month}/${date.day}';
}
Widget _buildDateNavigator() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: () => _changeDate(-1),
),
Text(
_formatDisplayDate(_currentDate),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: _isToday(_currentDate) ? null : () => _changeDate(1),
),
],
);
}
void _changeDate(int days) {
setState(() {
_currentDate = _currentDate.add(Duration(days: days));
// Clear old data while new data is being fetched
_punchCardTimes = {};
});
// Only fetch attendance courses for the new date
_getAttendanceCourses();
}
Future<void> _refreshAllData() async {
if (_loginStatus != '已登录') return;
setState(() {
_punchCardStatus = '加载中...';
_punchCardTimes = {};
});
await _getHomePageData();
await _getAttendanceCourses();
}
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_account = prefs.getString('account') ?? '';
_password = prefs.getString('password') ?? '';
_token = prefs.getString('token') ?? '';
_tokens = prefs.getString('tokens') ?? '';
_deviceId = prefs.getString('device_id') ?? '';
_autoLoginEnabled = prefs.getBool('auto_login_enabled') ?? false;
_isTokenMode = prefs.getBool('is_token_mode') ?? true;
_pollingEnabled = prefs.getBool('polling_enabled') ?? false;
});
if (_autoLoginEnabled) {
_login(isAutoLogin: true);
}
}
Future<void> _saveString(String key, String value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(key, value);
}
Future<void> _saveBool(String key, bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(key, value);
}
void _updatePollingState() {
if (_pollingEnabled && _loginStatus == '已登录') {
_startPolling();
} else {
_stopPolling();
}
}
void _startPolling() {
_stopPolling(); // Ensure no multiple timers
_pollingTimer = Timer.periodic(const Duration(seconds: 2), (timer) {
_getHomePageData();
});
}
void _stopPolling() {
_pollingTimer?.cancel();
}
Future<void> _login({bool isAutoLogin = false}) async {
if (_isTokenMode) {
await _loginWithTokens(isAutoLogin: isAutoLogin);
} else {
await _loginWithAccount(isAutoLogin: isAutoLogin);
}
_updatePollingState();
}
List<String> _parseTokens(String tokens) {
return tokens.split('\n').where((t) => t.trim().isNotEmpty).map((t) => t.trim()).toList();
}
Future<void> _loginWithTokens({bool isAutoLogin = false}) async {
final tokenList = _parseTokens(_tokens);
if (tokenList.isEmpty) {
if (!isAutoLogin) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请在设置中填写Token')),
);
}
return;
}
bool success = false;
for (final token in tokenList) {
// Set current token for ApiService to use
setState(() {
_token = token;
_loginStatus = '已登录'; // Tentatively set to logged in
});
await _getHomePageData();
// _getHomePageData will set _userInfo to contain '请求成功' on success
if (_userInfo.contains('请求成功')) {
success = true;
break; // Stop on first successful token
}
}
if (success) {
await _getAttendanceCourses();
} else {
setState(() {
_loginStatus = '未登录';
_userInfo = '所有Token均无效';
});
}
}
Future<void> _loginWithAccount({bool isAutoLogin = false}) async {
if (_token.isEmpty) {
if (!isAutoLogin) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请先在右上角设置中填写Token')),
);
}
return;
}
setState(() {
_userInfo = '';
});
if (_account.isNotEmpty && _password.isNotEmpty && _deviceId.isNotEmpty) {
const url = 'https://m-zjt.kefang.net/api/ums/user/user/signIn';
final hashedPassword = _generateMd5(_password);
final headers = _getHeaders();
final body = {
'phone': _account,
'password': hashedPassword,
'platform': '职教智慧云',
'deviceId': _deviceId,
'os': 'android',
};
developer.log('--- 🚀 [LOGIN] Request ---', name: 'NetworkDebug');
developer.log('URL: $url', name: 'NetworkDebug');
developer.log('Headers: $headers', name: 'NetworkDebug');
developer.log('Body: $body', name: 'NetworkDebug');
try {
final response = await http.post(Uri.parse(url), headers: headers, body: body);
developer.log('--- ✅ [LOGIN] Response ---', name: 'NetworkDebug');
developer.log('Status Code: ${response.statusCode}', name: 'NetworkDebug');
developer.log('Body: ${response.body}', name: 'NetworkDebug');
developer.log('--------------------------', name: 'NetworkDebug');
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['code'] == 0) {
setState(() {
_loginStatus = '已登录';
_currentDate = DateTime.now();
});
await _refreshAllData();
} else {
if (!isAutoLogin) {
setState(() {
_loginStatus = '登录失败: ${data['message']}';
});
}
}
} else {
if (!isAutoLogin) {
setState(() {
_loginStatus = '登录失败: HTTP ${response.statusCode}';
});
}
}
} catch (e) {
developer.log('--- ❌ [LOGIN] Error ---', error: e, name: 'NetworkDebug');
if (!isAutoLogin) {
setState(() {
_loginStatus = '登录异常: $e';
});
}
}
} else {
setState(() {
_loginStatus = '已登录';
_userInfo = 'token模式';
_currentDate = DateTime.now();
});
await _refreshAllData();
}
}
void _logout() {
_stopPolling();
setState(() {
_loginStatus = '未登录';
_userInfo = '';
_punchCardStatus = '打卡未开启';
_contentId = '';
_punchCardTimes = {}; // Reset punch card times on logout
_currentDate = DateTime.now(); // Reset date to today on logout
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('您已退出登录')),
);
}
Future<void> _getHomePageData() async {
if (_loginStatus != '已登录') return;
final isTokenLoginMode = _account.isEmpty || _password.isEmpty || _deviceId.isEmpty;
final data = await _apiService.getHomePageData(_token);
if (data['code'] == 0 && data['data'] != null) {
setState(() {
_loginStatus = '已登录'; // Mark as logged in on successful data fetch
});
if (isTokenLoginMode) {
setState(() {
_userInfo = 'token模式 - 请求成功';
});
} else {
final headerTeacher = data['data']['headerTeacher'];
setState(() {
_userInfo = '欢迎您,${headerTeacher ?? ''}';
});
}
if (data['data']['messages'] != null) {
final messages = data['data']['messages'] as List;
final punchCardMessage = messages.firstWhere((m) => m['type'] == '打卡' && m['tradeType'] == '到课打卡', orElse: () => null);
if (punchCardMessage != null) {
if (_punchCardStatus != '打卡已开启') {
setState(() {
_punchCardStatus = '打卡已开启';
_contentId = punchCardMessage['contentId'];
});
}
} else {
if (_punchCardStatus != '打卡未开启') {
setState(() {
_punchCardStatus = '打卡未开启';
_contentId = ''; // Clear contentId if no punch card message is found
});
}
}
}
} else {
if (_loginStatus == '已登录') { // Only show error if we were logged in
setState(() {
if (isTokenLoginMode) {
_userInfo = 'token模式 - 请求失败';
}
_loginStatus = '会话过期,请重新登录';
_punchCardStatus = '打卡未开启';
_contentId = ''; // Also clear contentId on session expiration
});
}
}
}
Future<void> _getAttendanceCourses() async {
if (_loginStatus != '已登录') return;
final timestamp = _getStartOfDayTimestamp(_currentDate);
final url = 'https://m-zjt.kefang.net/api/zms/attendance/attendanceCourse/listForStudent?attendanceDate=$timestamp';
final headers = _getHeaders();
developer.log('--- 🚀 [ATTENDANCE COURSES] Request ---', name: 'NetworkDebug');
developer.log('URL: $url', name: 'NetworkDebug');
developer.log('Headers: $headers', name: 'NetworkDebug');
try {
final response = await http.get(Uri.parse(url), headers: headers);
developer.log('--- ✅ [ATTENDANCE COURSES] Response ---', name: 'NetworkDebug');
developer.log('Status Code: ${response.statusCode}', name: 'NetworkDebug');
developer.log('Body: ${response.body}', name: 'NetworkDebug');
developer.log('-------------------------------------', name: 'NetworkDebug');
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['code'] == 0 && data['data'] != null) {
final courses = data['data'] as List;
final newTimes = <String, String>{};
for (var course in courses) {
final timeType = course['timeType'] as String?;
final punchTime = course['punchTime'] as int?;
if (timeType != null && punchTime != null && punchTime != 0) {
newTimes[timeType] = _formatTimestamp(punchTime);
}
}
setState(() {
_punchCardTimes = newTimes;
});
}
}
} catch (e) {
developer.log('--- ❌ [ATTENDANCE COURSES] Error ---', error: e, name: 'NetworkDebug');
}
}
Future<void> _punchCard() async {
if (_isTokenMode) {
final tokenList = _parseTokens(_tokens);
if (tokenList.isEmpty || _contentId.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Tokens或打卡ID为空请检查设置')),
);
return;
}
int successCount = 0;
int failureCount = 0;
String lastErrorMessage = '';
for (final token in tokenList) {
final data = await _apiService.punchCard(token, _contentId);
if (data['code'] == 0) {
successCount++;
} else {
failureCount++;
lastErrorMessage = data['message'] ?? '未知错误';
}
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('批量打卡完成:$successCount 成功,$failureCount 失败。${failureCount > 0 ? '最后错误: $lastErrorMessage' : ''}')),
);
if (successCount > 0) {
setState(() {
_punchCardStatus = '已打卡';
});
await _getAttendanceCourses();
}
} else {
if (_token.isEmpty || _contentId.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Token或打卡ID为空请检查设置')),
);
return;
}
final data = await _apiService.punchCard(_token, _contentId);
if (data['code'] == 0) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('打卡成功')),
);
setState(() {
_punchCardStatus = '已打卡';
});
await _getAttendanceCourses();
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('打卡失败: ${data['message']}')),
);
}
}
}
void _showSettingsDialog() async {
String tempAccount = _account;
String tempPassword = _password;
String tempToken = _token;
String tempTokens = _tokens;
String tempDeviceId = _deviceId;
bool tempAutoLogin = _autoLoginEnabled;
bool tempIsTokenMode = _isTokenMode;
bool tempPollingEnabled = _pollingEnabled;
await showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setStateInDialog) {
return AlertDialog(
title: const Text('设置'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SwitchListTile(
title: const Text('登录模式'),
subtitle: Text(tempIsTokenMode ? 'Token模式' : '账号密码模式'),
value: tempIsTokenMode,
onChanged: (value) {
setStateInDialog(() {
tempIsTokenMode = value;
});
},
),
if (tempIsTokenMode)
TextField(
decoration: const InputDecoration(labelText: 'Tokens (每行一个)', alignLabelWithHint: true),
controller: TextEditingController(text: tempTokens),
onChanged: (value) => tempTokens = value,
maxLines: 5,
)
else ...[
TextField(
decoration: const InputDecoration(labelText: 'Token (Cookie)'),
controller: TextEditingController(text: tempToken),
onChanged: (value) => tempToken = value,
),
TextField(
decoration: const InputDecoration(labelText: '账号 (手机号) - 可选'),
controller: TextEditingController(text: tempAccount),
onChanged: (value) => tempAccount = value,
),
TextField(
decoration: const InputDecoration(labelText: '密码 (明文) - 可选'),
controller: TextEditingController(text: tempPassword),
obscureText: true,
onChanged: (value) => tempPassword = value,
),
TextField(
decoration: const InputDecoration(labelText: '设备ID - 可选'),
controller: TextEditingController(text: tempDeviceId),
onChanged: (value) => tempDeviceId = value,
),
],
SwitchListTile(
title: const Text('打卡状态轮询'),
value: tempPollingEnabled,
onChanged: (value) {
setStateInDialog(() {
tempPollingEnabled = value;
});
},
),
SwitchListTile(
title: const Text('自动登录'),
value: tempAutoLogin,
onChanged: (value) {
setStateInDialog(() {
tempAutoLogin = value;
});
},
),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('取消'),),
TextButton(child: const Text('清空配置'), onPressed: () async {
final prefs = await SharedPreferences.getInstance();
await prefs.clear();
await _loadSettings();
setState(() {
_loginStatus = '未登录';
_userInfo = '';
_punchCardStatus = '打卡未开启';
_contentId = '';
_punchCardTimes = {};
});
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('所有配置已清空')),
);
},
),
TextButton(onPressed: () async {
await _saveString('token', tempToken);
await _saveString('tokens', tempTokens);
await _saveString('account', tempAccount);
await _saveString('password', tempPassword);
await _saveString('device_id', tempDeviceId);
await _saveBool('auto_login_enabled', tempAutoLogin);
await _saveBool('is_token_mode', tempIsTokenMode);
await _saveBool('polling_enabled', tempPollingEnabled);
await _loadSettings();
_updatePollingState();
Navigator.of(context).pop();
},
child: const Text('保存'),
),
],
);
},
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('打卡'),
actions: [
IconButton(icon: const Icon(Icons.settings), onPressed: _showSettingsDialog,),
],
),
body: RefreshIndicator(
onRefresh: _refreshAllData,
child: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (_userInfo.isNotEmpty) ...[
Text(_userInfo, style: const TextStyle(fontSize: 18), textAlign: TextAlign.center,),
const SizedBox(height: 20),
if (_isToday(_currentDate)) // Only show punch status for today
Text(_punchCardStatus, style: const TextStyle(fontSize: 18, color: Colors.green),),
const SizedBox(height: 10),
_buildDateNavigator(),
_buildPunchCardInfoGrid(),
] else if (_loginStatus == '已登录') ...[ // Show loading indicator only when logged in but no user info yet
const CircularProgressIndicator(),
const SizedBox(height: 20),
const Text('登录中...')
],
const SizedBox(height: 40),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(onPressed: () {
if (_loginStatus == '已登录') {
_logout();
} else {
_login(isAutoLogin: false);
}
}, child: Text(_loginStatus == '已登录' ? '退出' : '登录'),
),
const SizedBox(width: 20),
ElevatedButton(onPressed: _loginStatus == '已登录' ? _getHomePageData : null, child: const Text('刷新打卡状态'),),
],
),
const SizedBox(height: 20),
ElevatedButton(onPressed: _punchCardStatus == '打卡已开启' ? _punchCard : null, child: const Text('打卡'),),
],
),
),
),
),
);
}
}