535 lines
19 KiB
Dart
535 lines
19 KiB
Dart
|
||
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;
|
||
|
||
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> {
|
||
String _loginStatus = '未登录';
|
||
String _userInfo = '';
|
||
String _punchCardStatus = '打卡未开启';
|
||
String _contentId = '';
|
||
|
||
// State for date navigation
|
||
DateTime _currentDate = DateTime.now();
|
||
|
||
// Settings variables
|
||
String _token = '';
|
||
String _account = '';
|
||
String _password = '';
|
||
String _deviceId = '';
|
||
|
||
// 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(); // 移除自动登录,仅加载配置
|
||
}
|
||
|
||
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') ?? '';
|
||
_deviceId = prefs.getString('device_id') ?? '';
|
||
});
|
||
}
|
||
|
||
Future<void> _saveString(String key, String value) async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.setString(key, value);
|
||
}
|
||
|
||
Future<void> _login({bool isAutoLogin = false}) async {
|
||
if (_account.isEmpty || _password.isEmpty || _token.isEmpty) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('请先在右上角设置中填写账号、密码和Token')),
|
||
);
|
||
return;
|
||
}
|
||
|
||
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 = '已登录';
|
||
_userInfo = '欢迎您,${data['data']['name']}';
|
||
_currentDate = DateTime.now(); // Reset date to today on login
|
||
});
|
||
await _refreshAllData(); // Fetch all data on login
|
||
} 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';
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
void _logout() {
|
||
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;
|
||
const url = 'https://m-zjt.kefang.net/api/zms/zhijiaotong/page/homePageData';
|
||
final headers = _getHeaders();
|
||
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(Uri.parse(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) {
|
||
final data = json.decode(response.body);
|
||
if (data['code'] == 0 && data['data'] != null && 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) {
|
||
setState(() {
|
||
_punchCardStatus = '打卡已开启';
|
||
_contentId = punchCardMessage['contentId'];
|
||
});
|
||
} else {
|
||
setState(() {
|
||
_punchCardStatus = '打卡未开启';
|
||
_contentId = ''; // Clear contentId if no punch card message is found
|
||
});
|
||
}
|
||
} else if (data['code'] != 0) {
|
||
setState(() {
|
||
_loginStatus = '会话过期,请重新登录';
|
||
_userInfo = '';
|
||
_punchCardStatus = '打卡未开启';
|
||
_contentId = ''; // Also clear contentId on session expiration
|
||
});
|
||
} else {
|
||
setState(() {
|
||
_punchCardStatus = '打卡未开启';
|
||
_contentId = ''; // Clear contentId if messages are null
|
||
});
|
||
}
|
||
}
|
||
} catch (e) {
|
||
developer.log('--- ❌ [HOME PAGE] Error ---', error: e, name: 'NetworkDebug');
|
||
setState(() {
|
||
_punchCardStatus = '获取主页数据异常';
|
||
_contentId = ''; // Clear contentId on error
|
||
});
|
||
}
|
||
}
|
||
|
||
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 (_token.isEmpty || _contentId.isEmpty) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('Token或打卡ID为空,请检查设置')),
|
||
);
|
||
return;
|
||
}
|
||
|
||
const url = 'https://m-zjt.kefang.net/api/zms/attendance/attendanceRecord/punchCourseByStudent';
|
||
final headers = _getHeaders();
|
||
final body = {'attendanceId': _contentId};
|
||
|
||
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(Uri.parse(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) {
|
||
final data = json.decode(response.body);
|
||
if (data['code'] == 0) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('打卡成功')),
|
||
);
|
||
setState(() {
|
||
_punchCardStatus = '已打卡';
|
||
});
|
||
await _getAttendanceCourses(); // Refresh punch times after successful punch
|
||
} else {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text('打卡失败: ${data['message']}')),
|
||
);
|
||
}
|
||
} else {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('打卡请求失败')),
|
||
);
|
||
}
|
||
} catch (e) {
|
||
developer.log('--- ❌ [PUNCH CARD] Error ---', error: e, name: 'NetworkDebug');
|
||
}
|
||
}
|
||
|
||
void _showSettingsDialog() async {
|
||
String tempAccount = _account;
|
||
String tempPassword = _password;
|
||
String tempToken = _token;
|
||
String tempDeviceId = _deviceId;
|
||
|
||
await showDialog(
|
||
context: context,
|
||
builder: (context) {
|
||
return AlertDialog(
|
||
title: const Text('设置'),
|
||
content: SingleChildScrollView(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
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: 'Token (Cookie)'),
|
||
controller: TextEditingController(text: tempToken),
|
||
onChanged: (value) => tempToken = value,
|
||
),
|
||
TextField(
|
||
decoration: const InputDecoration(labelText: '设备ID'),
|
||
controller: TextEditingController(text: tempDeviceId),
|
||
onChanged: (value) => tempDeviceId = 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('account', tempAccount);
|
||
await _saveString('password', tempPassword);
|
||
await _saveString('token', tempToken);
|
||
await _saveString('device_id', tempDeviceId);
|
||
await _loadSettings();
|
||
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>[
|
||
Text(_loginStatus, style: const TextStyle(fontSize: 20), textAlign: TextAlign.center,),
|
||
const SizedBox(height: 20),
|
||
if (_loginStatus == '已登录') ...[
|
||
Text(_userInfo, style: const TextStyle(fontSize: 18),),
|
||
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(),
|
||
],
|
||
const SizedBox(height: 40),
|
||
ElevatedButton(onPressed: () {
|
||
if (_loginStatus == '已登录') {
|
||
_logout();
|
||
} else {
|
||
_login(isAutoLogin: false);
|
||
}
|
||
}, child: Text(_loginStatus == '已登录' ? '退出' : '登录'),
|
||
),
|
||
const SizedBox(height: 20),
|
||
ElevatedButton(onPressed: _punchCardStatus == '打卡已开启' ? _punchCard : null, child: const Text('打卡'),),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|