Files
wushu/lib/views/profile/components/entire_page.dart
2026-04-02 07:06:55 +08:00

1086 lines
30 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.
/// 时间: 2026-04-01
/// 功能: 全站统计页面
/// 介绍: 展示网站统计数据,包括收录数量、热度统计、热门内容等
/// 最新变化: 新建页面iOS风格设计
library;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../constants/app_constants.dart';
import '../../../utils/http/http_client.dart';
import '../../../services/network_listener_service.dart';
import '../../../services/get/theme_controller.dart';
import 'server_info_dialog.dart';
class EntirePage extends StatefulWidget {
const EntirePage({super.key});
@override
State<EntirePage> createState() => _EntirePageState();
}
class _EntirePageState extends State<EntirePage>
with NetworkListenerMixin, SingleTickerProviderStateMixin {
final ThemeController _themeController = Get.find<ThemeController>();
Map<String, dynamic>? _statsData;
String? _errorMessage;
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
);
_animationController.forward();
_loadStatsData();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
Future<void> _loadStatsData() async {
if (!mounted) return;
setState(() {
_errorMessage = null;
});
startNetworkLoading('stats');
try {
final response = await HttpClient.get('app/stats.php');
if (!mounted) return;
if (response.isSuccess && response.jsonData['ok'] == true) {
setState(() {
_statsData = response.jsonData['data'] as Map<String, dynamic>;
});
sendRefreshEvent();
} else {
setState(() {
_errorMessage = '加载失败:${response.message}';
});
}
} catch (e) {
if (!mounted) return;
setState(() {
_errorMessage = '网络错误:$e';
});
} finally {
endNetworkLoading('stats');
}
}
Future<void> _showServerInfo() async {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const AlertDialog(
content: Row(
children: [
CircularProgressIndicator(),
SizedBox(width: 16),
Text('正在检测网络状态...'),
],
),
);
},
);
try {
final response = await HttpClient.get('poe/load.php');
if (!mounted) return;
Navigator.of(context).pop();
if (response.isSuccess) {
final data = response.jsonData;
if (data['status'] == 'success') {
ServerInfoDialog.show(context, data: data as Map<String, dynamic>?);
} else {
ServerInfoDialog.show(context);
}
} else {
ServerInfoDialog.show(context);
}
} catch (e) {
if (!mounted) return;
Navigator.of(context).pop();
ServerInfoDialog.show(context);
}
}
@override
Widget build(BuildContext context) {
return Obx(() {
final isDark = _themeController.isDarkMode;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: isDark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark,
child: Scaffold(
backgroundColor: isDark
? const Color(0xFF1A1A1A)
: const Color(0xFFF2F2F7),
appBar: _buildAppBar(isDark),
body: _buildBody(isDark),
),
);
});
}
PreferredSizeWidget _buildAppBar(bool isDark) {
return AppBar(
backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDark ? Colors.white : AppConstants.primaryColor,
),
onPressed: () => Navigator.pop(context),
),
title: Text(
'全站统计',
style: TextStyle(
color: isDark ? Colors.white : Colors.black,
fontSize: 17,
fontWeight: FontWeight.w600,
),
),
centerTitle: true,
actions: [
IconButton(
icon: Icon(
Icons.info_outline,
color: isDark ? Colors.white : AppConstants.primaryColor,
),
onPressed: _showServerInfo,
tooltip: '服务器信息',
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(0.5),
child: Container(
height: 0.5,
color: isDark ? Colors.grey[700] : const Color(0xFFE5E5EA),
),
),
);
}
Widget _buildBody(bool isDark) {
if (_errorMessage != null) {
return _buildErrorView(isDark);
}
if (_statsData == null) {
return _buildSkeletonView(isDark);
}
return _buildStatsContent(isDark);
}
Widget _buildSkeletonBox({
double width = double.infinity,
double height = 16,
double radius = 8,
bool isDark = false,
}) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
color: isDark ? Colors.grey[700] : const Color(0xFFE5E5EA),
borderRadius: BorderRadius.circular(radius),
),
);
}
Widget _buildSkeletonView(bool isDark) {
return FadeTransition(
opacity: _fadeAnimation,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildSkeletonHeaderCard(isDark),
const SizedBox(height: 16),
_buildSkeletonSection(isDark),
const SizedBox(height: 16),
_buildSkeletonSection(isDark),
const SizedBox(height: 16),
_buildSkeletonSection(isDark),
const SizedBox(height: 16),
_buildSkeletonBuildTimeCard(isDark),
const SizedBox(height: 32),
],
),
);
}
Widget _buildSkeletonHeaderCard(bool isDark) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark ? Colors.grey[700] : const Color(0xFFE5E5EA),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
_buildSkeletonBox(
width: 28,
height: 28,
radius: 14,
isDark: isDark,
),
const SizedBox(width: 12),
_buildSkeletonBox(width: 100, height: 22, isDark: isDark),
],
),
const SizedBox(height: 12),
_buildSkeletonBox(width: 200, height: 14, isDark: isDark),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildSkeletonBox(width: 60, height: 40, isDark: isDark),
_buildSkeletonBox(width: 60, height: 40, isDark: isDark),
_buildSkeletonBox(width: 60, height: 40, isDark: isDark),
],
),
],
),
);
}
Widget _buildSkeletonSection(bool isDark) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
_buildSkeletonBox(
width: 20,
height: 20,
radius: 10,
isDark: isDark,
),
const SizedBox(width: 8),
_buildSkeletonBox(width: 80, height: 16, isDark: isDark),
],
),
const SizedBox(height: 16),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
childAspectRatio: 1.0,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
children: List.generate(
9,
(index) => _buildSkeletonCountItem(isDark),
),
),
],
),
);
}
Widget _buildSkeletonCountItem(bool isDark) {
return Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF333333) : const Color(0xFFF2F2F7),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Expanded(
child: _buildSkeletonBox(
width: double.infinity,
height: 28,
radius: 8,
isDark: isDark,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildSkeletonBox(
width: double.infinity,
height: 22,
isDark: isDark,
),
),
],
),
),
const SizedBox(height: 4),
Expanded(
flex: 1,
child: _buildSkeletonBox(width: 50, height: 12, isDark: isDark),
),
],
),
);
}
Widget _buildSkeletonBuildTimeCard(bool isDark) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
_buildSkeletonBox(width: 40, height: 40, radius: 10, isDark: isDark),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSkeletonBox(width: 60, height: 14, isDark: isDark),
const SizedBox(height: 4),
_buildSkeletonBox(width: 150, height: 16, isDark: isDark),
],
),
),
],
),
);
}
Widget _buildErrorView(bool isDark) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: const Color(0xFFFF3B30).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(32),
),
child: const Icon(
Icons.error_outline,
color: Color(0xFFFF3B30),
size: 32,
),
),
const SizedBox(height: 16),
Text(
_errorMessage ?? '加载失败',
style: TextStyle(
color: isDark ? Colors.grey[400] : const Color(0xFF8E8E93),
fontSize: 14,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _loadStatsData,
style: ElevatedButton.styleFrom(
backgroundColor: AppConstants.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
child: const Text('重试'),
),
],
),
),
);
}
Widget _buildStatsContent(bool isDark) {
return FadeTransition(
opacity: _fadeAnimation,
child: RefreshIndicator(
color: AppConstants.primaryColor,
onRefresh: _loadStatsData,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildHeaderCard(),
const SizedBox(height: 16),
_buildHotSection(isDark),
const SizedBox(height: 16),
_buildCountSection(isDark),
const SizedBox(height: 16),
_buildTopContentSection(isDark),
const SizedBox(height: 16),
_buildBuildTimeCard(isDark),
const SizedBox(height: 32),
],
),
),
);
}
Widget _buildHeaderCard() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppConstants.primaryColor,
AppConstants.primaryColor.withValues(alpha: 0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppConstants.primaryColor.withValues(alpha: 0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.analytics, color: Colors.white, size: 28),
const SizedBox(width: 12),
const Text(
'情景诗词',
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.refresh, color: Colors.white, size: 22),
onPressed: _loadStatsData,
tooltip: '刷新数据',
),
],
),
const SizedBox(height: 12),
Text(
'诗意生活,触手可及',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 14,
),
),
const SizedBox(height: 16),
Row(
children: [
_buildHeaderStat(
'收录诗句',
_statsData?['count_site']?.toString() ?? '0',
),
Container(
width: 1,
height: 40,
color: Colors.white.withValues(alpha: 0.3),
margin: const EdgeInsets.symmetric(horizontal: 20),
),
_buildHeaderStat(
'累计热度',
_statsData?['cumulative_hits']?.toString() ?? '0',
),
Container(
width: 1,
height: 40,
color: Colors.white.withValues(alpha: 0.3),
margin: const EdgeInsets.symmetric(horizontal: 20),
),
_buildHeaderStat(
'累计点赞',
_statsData?['cumulative_likes']?.toString() ?? '0',
),
],
),
],
),
);
}
Widget _buildHeaderStat(String label, String value) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: 12,
),
),
],
),
);
}
Widget _buildCountSection(bool isDark) {
return _buildSection('数量统计', Icons.format_list_numbered, [
_buildCountGrid(isDark),
], isDark);
}
Widget _buildCountGrid(bool isDark) {
final counts = [
{
'label': '项目',
'value': _statsData?['count_category'] ?? 0,
'icon': Icons.category,
'color': const Color(0xFF007AFF),
'showIcon': true,
},
{
'label': '收录诗句',
'value': _statsData?['count_site'] ?? 0,
'icon': Icons.article,
'color': const Color(0xFF34C759),
'showIcon': false,
},
{
'label': '审核中',
'value': _statsData?['count_apply'] ?? 0,
'icon': Icons.pending,
'color': const Color(0xFFFF9500),
'showIcon': true,
},
{
'label': '已拒审',
'value': _statsData?['count_apply_reject'] ?? 0,
'icon': Icons.block,
'color': const Color(0xFFFF3B30),
'showIcon': true,
},
{
'label': '每日一句',
'value': _statsData?['count_article'] ?? 0,
'icon': Icons.wb_sunny,
'color': const Color(0xFF5856D6),
'showIcon': true,
},
{
'label': '文章分类',
'value': _statsData?['count_article_category'] ?? 0,
'icon': Icons.folder,
'color': const Color(0xFFAF52DE),
'showIcon': true,
},
{
'label': '推送',
'value': _statsData?['count_notice'] ?? 0,
'icon': Icons.campaign,
'color': const Color(0xFF32ADE6),
'showIcon': true,
},
{
'label': '开发者',
'value': _statsData?['count_link'] ?? 0,
'icon': Icons.people,
'color': const Color(0xFFFF2D55),
'showIcon': true,
},
{
'label': '分类标签',
'value': _statsData?['count_tags'] ?? 0,
'icon': Icons.label,
'color': const Color(0xFF64D2FF),
'showIcon': false,
},
];
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.0,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: counts.length,
itemBuilder: (context, index) {
final item = counts[index];
return _buildCountItem(
item['label'] as String,
item['value'].toString(),
item['icon'] as IconData,
item['color'] as Color,
item['showIcon'] as bool,
isDark,
);
},
);
}
Widget _buildCountItem(
String label,
String value,
IconData icon,
Color color,
bool showIcon,
bool isDark,
) {
return Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
// 上行icon和数据比例2:1有icon时1:1无icon时数据占满
Expanded(
flex: 2,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (showIcon) ...[
Expanded(
child: Container(
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 24),
),
),
const SizedBox(width: 8),
],
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black,
),
textAlign: TextAlign.center,
),
),
],
),
),
const SizedBox(height: 4),
// 下行:描述
Expanded(
flex: 1,
child: Center(
child: Text(
label,
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.grey[400] : const Color(0xFF3C3C43),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
),
],
),
);
}
Widget _buildHotSection(bool isDark) {
return _buildSection('热度统计', Icons.trending_up, [
_buildHotItem(
'累计热度',
_statsData?['cumulative_hits']?.toString() ?? '0',
Icons.local_fire_department,
const Color(0xFFFF9500),
isDark,
),
const SizedBox(height: 12),
_buildHotItem(
'累计点赞',
_statsData?['cumulative_likes']?.toString() ?? '0',
Icons.favorite,
const Color(0xFFFF2D55),
isDark,
),
], isDark);
}
Widget _buildHotItem(
String label,
String value,
IconData icon,
Color color,
bool isDark,
) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 24),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
color: isDark ? Colors.grey[400] : const Color(0xFF8E8E93),
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black,
),
),
],
),
),
],
),
);
}
Widget _buildTopContentSection(bool isDark) {
return _buildSection('热门内容', Icons.star, [
_buildTopContentItem(
'今日热门',
_statsData?['top_hits_day'],
Icons.today,
const Color(0xFFFF9500),
isDark,
),
const SizedBox(height: 12),
_buildTopContentItem(
'本月热门',
_statsData?['top_hits_month'],
Icons.calendar_month,
const Color(0xFF007AFF),
isDark,
),
const SizedBox(height: 12),
_buildTopContentItem(
'历史最热',
_statsData?['top_hits_total'],
Icons.history,
const Color(0xFF5856D6),
isDark,
),
const SizedBox(height: 12),
_buildTopContentItem(
'最高点赞',
_statsData?['top_like'],
Icons.thumb_up,
const Color(0xFF34C759),
isDark,
),
], isDark);
}
Widget _buildTopContentItem(
String label,
dynamic data,
IconData icon,
Color color,
bool isDark,
) {
final hasData = data != null && data is Map<String, dynamic>;
final content = hasData ? data['name']?.toString() ?? '暂无数据' : '暂无数据';
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black,
),
),
const SizedBox(height: 8),
Text(
content,
style: TextStyle(
fontSize: 13,
color: hasData
? (isDark ? Colors.grey[400] : const Color(0xFF3C3C43))
: (isDark ? Colors.grey[500] : const Color(0xFF8E8E93)),
height: 1.5,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
}
Widget _buildBuildTimeCard(bool isDark) {
final buildTime = _statsData?['build_time']?.toString() ?? '未知';
int days = 0;
try {
final buildDate = DateTime.parse(buildTime);
final now = DateTime.now();
days = now.difference(buildDate).inDays;
if (days < 0) days = 0;
} catch (e) {
days = 0;
}
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppConstants.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.cake,
color: AppConstants.primaryColor,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'建站时间',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black,
),
),
const SizedBox(height: 4),
Row(
children: [
Text(
buildTime,
style: TextStyle(
fontSize: 16,
color: isDark
? Colors.grey[400]
: const Color(0xFF3C3C43),
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: AppConstants.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'已运行 $days',
style: const TextStyle(
fontSize: 12,
color: AppConstants.primaryColor,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
),
),
],
),
);
}
Widget _buildSection(
String title,
IconData icon,
List<Widget> children,
bool isDark,
) {
return Container(
decoration: BoxDecoration(
color: isDark ? const Color(0xFF2A2A2A) : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(icon, color: AppConstants.primaryColor, size: 20),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black,
),
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
),
],
),
);
}
}