306 lines
9.9 KiB
Dart
306 lines
9.9 KiB
Dart
import 'dart:ffi';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:http/http.dart' as http;
|
||
import 'dart:convert';
|
||
import '../services/auth_service.dart';
|
||
import '../services/kodbox_service.dart';
|
||
import 'package:timeago/timeago.dart' as timeago;
|
||
import 'package:url_launcher/url_launcher.dart';
|
||
|
||
class KodBoxCard extends StatefulWidget {
|
||
const KodBoxCard({super.key});
|
||
|
||
@override
|
||
State<KodBoxCard> createState() => _KodBoxCardState();
|
||
}
|
||
|
||
class _KodBoxCardState extends State<KodBoxCard> {
|
||
List<dynamic> _logs = [];
|
||
bool _isLoading = false;
|
||
String? _error;
|
||
final _kodboxService = KodBoxService();
|
||
|
||
Future<void> _fetchLogs() async {
|
||
setState(() {
|
||
_isLoading = true;
|
||
_error = null;
|
||
});
|
||
|
||
try {
|
||
final authService = Provider.of<AuthService>(context, listen: false);
|
||
final token = await _kodboxService.getValidToken(authService);
|
||
|
||
// 计算时间范围(最近7天)
|
||
final now = DateTime.now();
|
||
final sevenDaysAgo = now.subtract(const Duration(days: 7));
|
||
final timeTo = (now.millisecondsSinceEpoch / 1000).round();
|
||
final timeFrom = (sevenDaysAgo.millisecondsSinceEpoch / 1000).round();
|
||
|
||
final request = http.MultipartRequest(
|
||
'POST',
|
||
Uri.parse('${KodBoxService.baseUrl}/?admin/log/get&accessToken=$token'),
|
||
);
|
||
|
||
request.fields.addAll({
|
||
'page': '1',
|
||
'pageNum': '50',
|
||
'sortField': 'createTime',
|
||
'sortType': 'down',
|
||
'timeFrom': timeFrom.toString(),
|
||
'timeTo': timeTo.toString(),
|
||
});
|
||
|
||
final response = await request.send();
|
||
final responseBody = await response.stream.bytesToString();
|
||
|
||
if (response.statusCode == 200) {
|
||
final data = jsonDecode(responseBody);
|
||
if (data['code'] == true) {
|
||
setState(() {
|
||
_logs = data['data'];
|
||
_isLoading = false;
|
||
});
|
||
} else {
|
||
throw Exception('请求失败: ${data['info'] ?? '未知错误'}');
|
||
}
|
||
} else {
|
||
throw Exception('请求失败: ${response.statusCode}');
|
||
}
|
||
} catch (e) {
|
||
setState(() {
|
||
_error = e.toString();
|
||
_isLoading = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
String _getFilePath(Map<String, dynamic> desc) {
|
||
if (desc['sourceInfo'] == false) {
|
||
// 处理删除文件的情况
|
||
final parentInfo = desc['parentInfo'];
|
||
final fileName = desc['desc']?['content'] ?? '未知文件';
|
||
return '${parentInfo['pathDisplay']}$fileName';
|
||
} else if (desc['sourceInfo'] != null) {
|
||
return desc['sourceInfo']['pathDisplay'] ?? '未知路径';
|
||
}
|
||
return '未知路径';
|
||
}
|
||
|
||
int? _getSourceID(Map<String, dynamic> desc) {
|
||
if (desc['sourceInfo'] == false) {
|
||
// 处理删除文件的情况
|
||
return desc['parentInfo']?['sourceID'];
|
||
} else if (desc['sourceInfo'] != null) {
|
||
return desc['sourceInfo']?['sourceID'];
|
||
}
|
||
return null;
|
||
}
|
||
|
||
Future<void> _launchFileUrl(int sourceID) async {
|
||
final url = Uri.parse('${KodBoxService.baseUrl}/#explorer&sidf=$sourceID');
|
||
if (await canLaunchUrl(url)) {
|
||
await launchUrl(url);
|
||
} else {
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('无法打开文件链接')),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
Widget _buildLogItem(Map<String, dynamic> log) {
|
||
final desc = log['desc'];
|
||
final isFileOperation = desc != null &&
|
||
(desc['sourceInfo'] != null || desc['parentInfo'] != null);
|
||
|
||
final sourceID = isFileOperation ? _getSourceID(desc) : null;
|
||
|
||
return Card(
|
||
elevation: 4,
|
||
shadowColor: Colors.deepPurple.withOpacity(0.3),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(16),
|
||
side: BorderSide(color: Colors.deepPurple.withOpacity(0.1)),
|
||
),
|
||
margin: const EdgeInsets.symmetric(vertical: 5.0),
|
||
child: InkWell(
|
||
onTap: sourceID != null ? () => _launchFileUrl(sourceID) : null,
|
||
splashColor: Colors.deepPurple.withAlpha(30),
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16.0),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
CircleAvatar(
|
||
backgroundColor: Colors.deepPurple.shade200,
|
||
radius: 18,
|
||
child: Text(
|
||
log['nickName']?[0].toUpperCase() ?? '?',
|
||
style: const TextStyle(color: Colors.white),
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
log['nickName'] ?? log['name'] ?? '未知用户',
|
||
style: const TextStyle(
|
||
fontWeight: FontWeight.bold,
|
||
fontSize: 16,
|
||
),
|
||
),
|
||
if (log['title'] != null)
|
||
Text(
|
||
log['title']!,
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
color: Colors.grey,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
if (isFileOperation) ...[
|
||
const SizedBox(height: 12),
|
||
Row(
|
||
children: [
|
||
const Icon(Icons.insert_drive_file,
|
||
size: 18, color: Colors.deepPurple),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
'文件路径: ${_getFilePath(desc)}',
|
||
style: TextStyle(
|
||
color: sourceID != null ? Colors.blue : Colors.black,
|
||
fontSize: 14,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
const Icon(Icons.access_time, size: 16, color: Colors.grey),
|
||
const SizedBox(width: 6),
|
||
Text(
|
||
'时间: ${timeago.format(DateTime.fromMillisecondsSinceEpoch(int.parse(log['createTime']) * 1000), locale: 'zh_CN')}',
|
||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_fetchLogs();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Card(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(20),
|
||
side: BorderSide(color: Colors.deepPurple.withOpacity(0.15)),
|
||
),
|
||
elevation: 6,
|
||
shadowColor: Colors.deepPurple.withOpacity(0.4),
|
||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16.0),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
const Text(
|
||
'KodBox 最近操作',
|
||
style: TextStyle(
|
||
fontSize: 20,
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.deepPurple,
|
||
),
|
||
),
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.deepPurple.shade100,
|
||
borderRadius: BorderRadius.circular(20),
|
||
),
|
||
padding: const EdgeInsets.all(8),
|
||
child: IconButton(
|
||
icon: const Icon(Icons.refresh, color: Colors.deepPurple),
|
||
onPressed: _isLoading ? null : _fetchLogs,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
if (_isLoading)
|
||
const Center(
|
||
child: Padding(
|
||
padding: EdgeInsets.all(24.0),
|
||
child: CircularProgressIndicator(color: Colors.deepPurple),
|
||
),
|
||
)
|
||
else if (_error != null)
|
||
Center(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(24.0),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
const Icon(Icons.error_outline, color: Colors.red),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
_error!,
|
||
style: const TextStyle(color: Colors.red),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
)
|
||
else if (_logs.isEmpty)
|
||
const Center(
|
||
child: Padding(
|
||
padding: EdgeInsets.all(24.0),
|
||
child: Text(
|
||
'暂无操作记录',
|
||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||
),
|
||
),
|
||
)
|
||
else
|
||
Expanded(
|
||
child: ListView.builder(
|
||
itemCount: _logs.length,
|
||
itemBuilder: (context, index) {
|
||
return _buildLogItem(_logs[index]);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|