diff --git a/lib/features/v2/home/data/datasources/home_remote_datasource.dart b/lib/features/v2/home/data/datasources/home_remote_datasource.dart new file mode 100644 index 00000000..9e0a20be --- /dev/null +++ b/lib/features/v2/home/data/datasources/home_remote_datasource.dart @@ -0,0 +1,64 @@ +import 'package:maibu_satabot_v2/features/v2/home/data/models/home_data_model.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart'; + +abstract class HomeRemoteDataSource { + Future getHomeData(); +} + +class HomeRemoteDataSourceImpl implements HomeRemoteDataSource { + @override + Future getHomeData() async { + await Future.delayed(const Duration(seconds: 1)); + + return HomeDataModel( + plantName: '示例光伏电站', + realTimePower: 812.6, + todayGeneration: 4752, + equivalentHours: 3.2, + pendingWorkOrders: 12, + weather: const WeatherInfo( + condition: '多云', + tempMin: 28, + tempMax: 28, + windDirection: '', + windLevel: '', + ), + alarmCount: 8, + workOrderStats: const WorkOrderStats( + pending: 8, + processing: 4, + pendingAcceptance: 3, + ), + powerTrendData: const [ + PowerTrendPoint(time: '00:00', power: 0), + PowerTrendPoint(time: '01:00', power: 0), + PowerTrendPoint(time: '02:00', power: 0), + PowerTrendPoint(time: '03:00', power: 0), + PowerTrendPoint(time: '04:00', power: 0), + PowerTrendPoint(time: '05:00', power: 0), + PowerTrendPoint(time: '06:00', power: 50), + PowerTrendPoint(time: '07:00', power: 120), + PowerTrendPoint(time: '08:00', power: 280), + PowerTrendPoint(time: '09:00', power: 450), + PowerTrendPoint(time: '10:00', power: 620), + PowerTrendPoint(time: '11:00', power: 780), + PowerTrendPoint(time: '12:00', power: 920), + PowerTrendPoint(time: '13:00', power: 950), + PowerTrendPoint(time: '14:00', power: 880), + PowerTrendPoint(time: '15:00', power: 750), + PowerTrendPoint(time: '16:00', power: 580), + PowerTrendPoint(time: '17:00', power: 380), + PowerTrendPoint(time: '18:00', power: 180), + PowerTrendPoint(time: '19:00', power: 50), + PowerTrendPoint(time: '20:00', power: 0), + PowerTrendPoint(time: '21:00', power: 0), + PowerTrendPoint(time: '22:00', power: 0), + PowerTrendPoint(time: '23:00', power: 0), + ], + plantOverview: const PlantOverview( + installedCapacity: 1.26, + commissionDate: '2024-05-18', + ), + ); + } +} diff --git a/lib/features/v2/home/data/models/home_data_model.dart b/lib/features/v2/home/data/models/home_data_model.dart new file mode 100644 index 00000000..2a69f2e9 --- /dev/null +++ b/lib/features/v2/home/data/models/home_data_model.dart @@ -0,0 +1,119 @@ +import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart'; + +class HomeDataModel extends HomeEntity { + const HomeDataModel({ + required super.plantName, + required super.realTimePower, + required super.todayGeneration, + required super.equivalentHours, + required super.pendingWorkOrders, + required super.weather, + required super.alarmCount, + required super.workOrderStats, + required super.powerTrendData, + required super.plantOverview, + }); + + factory HomeDataModel.fromJson(Map json) { + return HomeDataModel( + plantName: json['plantName'] as String, + realTimePower: (json['realTimePower'] as num).toDouble(), + todayGeneration: (json['todayGeneration'] as num).toDouble(), + equivalentHours: (json['equivalentHours'] as num).toDouble(), + pendingWorkOrders: json['pendingWorkOrders'] as int, + weather: WeatherInfo.fromJson(json['weather']), + alarmCount: json['alarmCount'] as int, + workOrderStats: WorkOrderStats.fromJson(json['workOrderStats']), + powerTrendData: (json['powerTrendData'] as List) + .map((e) => PowerTrendPoint.fromJson(e)) + .toList(), + plantOverview: PlantOverview.fromJson(json['plantOverview']), + ); + } + + Map toJson() { + return { + 'plantName': plantName, + 'realTimePower': realTimePower, + 'todayGeneration': todayGeneration, + 'equivalentHours': equivalentHours, + 'pendingWorkOrders': pendingWorkOrders, + 'weather': weather.toJson(), + 'alarmCount': alarmCount, + 'workOrderStats': workOrderStats.toJson(), + 'powerTrendData': powerTrendData.map((e) => e.toJson()).toList(), + 'plantOverview': plantOverview.toJson(), + }; + } + + HomeEntity toEntity() { + return HomeEntity( + plantName: plantName, + realTimePower: realTimePower, + todayGeneration: todayGeneration, + equivalentHours: equivalentHours, + pendingWorkOrders: pendingWorkOrders, + weather: weather, + alarmCount: alarmCount, + workOrderStats: workOrderStats, + powerTrendData: powerTrendData, + plantOverview: plantOverview, + ); + } + + factory HomeDataModel.fromEntity(HomeEntity entity) { + return HomeDataModel( + plantName: entity.plantName, + realTimePower: entity.realTimePower, + todayGeneration: entity.todayGeneration, + equivalentHours: entity.equivalentHours, + pendingWorkOrders: entity.pendingWorkOrders, + weather: entity.weather, + alarmCount: entity.alarmCount, + workOrderStats: entity.workOrderStats, + powerTrendData: entity.powerTrendData, + plantOverview: entity.plantOverview, + ); + } +} + +extension WeatherInfoModelX on WeatherInfo { + Map toJson() { + return { + 'condition': condition, + 'tempMin': tempMin, + 'tempMax': tempMax, + 'windDirection': windDirection, + 'windLevel': windLevel, + }; + } +} + +extension WorkOrderStatsModelX on WorkOrderStats { + Map toJson() { + return { + 'pending': pending, + 'processing': processing, + 'pendingAcceptance': pendingAcceptance, + }; + } +} + +extension PlantOverviewModelX on PlantOverview { + Map toJson() { + return { + 'installedCapacity': installedCapacity, + 'commissionDate': commissionDate, + 'backgroundImage': backgroundImage, + }; + } +} + +extension PowerTrendPointModelX on PowerTrendPoint { + Map toJson() { + return { + 'time': time, + 'power': power, + }; + } +} diff --git a/lib/features/v2/home/data/repositories/home_repository_impl.dart b/lib/features/v2/home/data/repositories/home_repository_impl.dart new file mode 100644 index 00000000..adb3f918 --- /dev/null +++ b/lib/features/v2/home/data/repositories/home_repository_impl.dart @@ -0,0 +1,21 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart'; +import 'package:maibu_satabot_v2/features/v2/home/data/datasources/home_remote_datasource.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/repositories/home_repository.dart'; + +class HomeRepositoryImpl implements HomeRepository { + final HomeRemoteDataSource remoteDataSource; + + HomeRepositoryImpl(this.remoteDataSource); + + @override + Future> getHomeData() async { + try { + final result = await remoteDataSource.getHomeData(); + return Right(result.toEntity()); + } catch (e) { + return Left(Failure('获取首页数据失败:$e')); + } + } +} diff --git a/lib/features/v2/home/domain/entities/home_entity.dart b/lib/features/v2/home/domain/entities/home_entity.dart new file mode 100644 index 00000000..33f280fe --- /dev/null +++ b/lib/features/v2/home/domain/entities/home_entity.dart @@ -0,0 +1,169 @@ +import 'package:equatable/equatable.dart'; + +class HomeEntity extends Equatable { + final String plantName; + final double realTimePower; + final double todayGeneration; + final double equivalentHours; + final int pendingWorkOrders; + final WeatherInfo weather; + final int alarmCount; + final WorkOrderStats workOrderStats; + final List powerTrendData; + final PlantOverview plantOverview; + + const HomeEntity({ + required this.plantName, + required this.realTimePower, + required this.todayGeneration, + required this.equivalentHours, + required this.pendingWorkOrders, + required this.weather, + required this.alarmCount, + required this.workOrderStats, + required this.powerTrendData, + required this.plantOverview, + }); + + @override + List get props => [ + plantName, + realTimePower, + todayGeneration, + equivalentHours, + pendingWorkOrders, + weather, + alarmCount, + workOrderStats, + powerTrendData, + plantOverview, + ]; +} + +class WeatherInfo extends Equatable { + final String condition; + final int tempMin; + final int tempMax; + final String windDirection; + final String windLevel; + + const WeatherInfo({ + required this.condition, + required this.tempMin, + required this.tempMax, + required this.windDirection, + required this.windLevel, + }); + + factory WeatherInfo.fromJson(Map json) { + return WeatherInfo( + condition: json['condition'] as String, + tempMin: json['tempMin'] as int, + tempMax: json['tempMax'] as int, + windDirection: json['windDirection'] as String, + windLevel: json['windLevel'] as String, + ); + } + + Map toJson() { + return { + 'condition': condition, + 'tempMin': tempMin, + 'tempMax': tempMax, + 'windDirection': windDirection, + 'windLevel': windLevel, + }; + } + + @override + List get props => [condition, tempMin, tempMax, windDirection, windLevel]; +} + +class WorkOrderStats extends Equatable { + final int pending; + final int processing; + final int pendingAcceptance; + + const WorkOrderStats({ + required this.pending, + required this.processing, + required this.pendingAcceptance, + }); + + factory WorkOrderStats.fromJson(Map json) { + return WorkOrderStats( + pending: json['pending'] as int, + processing: json['processing'] as int, + pendingAcceptance: json['pendingAcceptance'] as int, + ); + } + + Map toJson() { + return { + 'pending': pending, + 'processing': processing, + 'pendingAcceptance': pendingAcceptance, + }; + } + + @override + List get props => [pending, processing, pendingAcceptance]; +} + +class PowerTrendPoint extends Equatable { + final String time; + final double power; + + const PowerTrendPoint({ + required this.time, + required this.power, + }); + + factory PowerTrendPoint.fromJson(Map json) { + return PowerTrendPoint( + time: json['time'] as String, + power: (json['power'] as num).toDouble(), + ); + } + + Map toJson() { + return { + 'time': time, + 'power': power, + }; + } + + @override + List get props => [time, power]; +} + +class PlantOverview extends Equatable { + final double installedCapacity; + final String commissionDate; + final String backgroundImage; + + const PlantOverview({ + required this.installedCapacity, + required this.commissionDate, + this.backgroundImage = 'assets/images/carousel1.png', + }); + + factory PlantOverview.fromJson(Map json) { + return PlantOverview( + installedCapacity: (json['installedCapacity'] as num).toDouble(), + commissionDate: json['commissionDate'] as String, + backgroundImage: json['backgroundImage'] as String? ?? 'assets/images/carousel1.png', + ); + } + + Map toJson() { + return { + 'installedCapacity': installedCapacity, + 'commissionDate': commissionDate, + 'backgroundImage': backgroundImage, + }; + } + + @override + List get props => [installedCapacity, commissionDate, backgroundImage]; +} diff --git a/lib/features/v2/home/domain/repositories/home_repository.dart b/lib/features/v2/home/domain/repositories/home_repository.dart new file mode 100644 index 00000000..7b8f6e9f --- /dev/null +++ b/lib/features/v2/home/domain/repositories/home_repository.dart @@ -0,0 +1,7 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart'; + +abstract class HomeRepository { + Future> getHomeData(); +} diff --git a/lib/features/v2/home/domain/usecases/get_home_data_usecase.dart b/lib/features/v2/home/domain/usecases/get_home_data_usecase.dart new file mode 100644 index 00000000..e59003ca --- /dev/null +++ b/lib/features/v2/home/domain/usecases/get_home_data_usecase.dart @@ -0,0 +1,24 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:maibu_satabot_v2/core/domain/usecases/base_usecase.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/repositories/home_repository.dart'; + +class GetHomeDataUseCase implements BaseUseCase { + final HomeRepository repository; + + GetHomeDataUseCase(this.repository); + + @override + Future> call(NoParams params) async { + try { + return await repository.getHomeData(); + } catch (e) { + return Left(Failure('获取首页数据失败:$e')); + } + } +} + +class NoParams { + const NoParams(); +} diff --git a/lib/features/v2/home/presentation/bloc/home_v2_bloc.dart b/lib/features/v2/home/presentation/bloc/home_v2_bloc.dart new file mode 100644 index 00000000..cc2f3e75 --- /dev/null +++ b/lib/features/v2/home/presentation/bloc/home_v2_bloc.dart @@ -0,0 +1,56 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/usecases/get_home_data_usecase.dart'; +import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_event.dart'; +import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_state.dart'; + +class HomeV2Bloc extends Bloc { + final GetHomeDataUseCase getHomeDataUseCase; + + HomeV2Bloc(this.getHomeDataUseCase) : super(const HomeV2Initial()) { + on(_onLoadData); + on(_onRefresh); + on(_onToggleTrendType); + } + + Future _onLoadData( + HomeV2LoadData event, + Emitter emit, + ) async { + emit(const HomeV2Loading()); + + final result = await getHomeDataUseCase(const NoParams()); + + result.fold( + (failure) => emit(HomeV2Error(failure.message)), + (data) => emit(HomeV2Loaded(homeData: data)), + ); + } + + Future _onRefresh( + HomeV2Refresh event, + Emitter emit, + ) async { + if (state is HomeV2Loaded) { + final result = await getHomeDataUseCase(const NoParams()); + + result.fold( + (failure) => emit(HomeV2Error(failure.message)), + (data) => emit(HomeV2Loaded( + homeData: data, + trendType: (state as HomeV2Loaded).trendType, + )), + ); + } + } + + void _onToggleTrendType( + HomeV2ToggleTrendType event, + Emitter emit, + ) { + if (state is HomeV2Loaded) { + final currentState = state as HomeV2Loaded; + emit(currentState.copyWith(trendType: event.type)); + print('切换趋势类型:${event.type}'); + } + } +} diff --git a/lib/features/v2/home/presentation/bloc/home_v2_event.dart b/lib/features/v2/home/presentation/bloc/home_v2_event.dart new file mode 100644 index 00000000..a77b0511 --- /dev/null +++ b/lib/features/v2/home/presentation/bloc/home_v2_event.dart @@ -0,0 +1,25 @@ +import 'package:equatable/equatable.dart'; + +abstract class HomeV2Event extends Equatable { + const HomeV2Event(); + + @override + List get props => []; +} + +class HomeV2LoadData extends HomeV2Event { + const HomeV2LoadData(); +} + +class HomeV2Refresh extends HomeV2Event { + const HomeV2Refresh(); +} + +class HomeV2ToggleTrendType extends HomeV2Event { + final String type; + + const HomeV2ToggleTrendType(this.type); + + @override + List get props => [type]; +} diff --git a/lib/features/v2/home/presentation/bloc/home_v2_state.dart b/lib/features/v2/home/presentation/bloc/home_v2_state.dart new file mode 100644 index 00000000..8fea8275 --- /dev/null +++ b/lib/features/v2/home/presentation/bloc/home_v2_state.dart @@ -0,0 +1,49 @@ +import 'package:equatable/equatable.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart'; + +abstract class HomeV2State extends Equatable { + const HomeV2State(); + + @override + List get props => []; +} + +class HomeV2Initial extends HomeV2State { + const HomeV2Initial(); +} + +class HomeV2Loading extends HomeV2State { + const HomeV2Loading(); +} + +class HomeV2Loaded extends HomeV2State { + final HomeEntity homeData; + final String trendType; + + const HomeV2Loaded({ + required this.homeData, + this.trendType = 'kW', + }); + + HomeV2Loaded copyWith({ + HomeEntity? homeData, + String? trendType, + }) { + return HomeV2Loaded( + homeData: homeData ?? this.homeData, + trendType: trendType ?? this.trendType, + ); + } + + @override + List get props => [homeData, trendType]; +} + +class HomeV2Error extends HomeV2State { + final String message; + + const HomeV2Error(this.message); + + @override + List get props => [message]; +} diff --git a/lib/features/v2/home/presentation/pages/home_v2_page.dart b/lib/features/v2/home/presentation/pages/home_v2_page.dart new file mode 100644 index 00000000..78200a4d --- /dev/null +++ b/lib/features/v2/home/presentation/pages/home_v2_page.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_bloc.dart'; +import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_event.dart'; +import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_state.dart'; +import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/power_card.dart'; +import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/stats_grid.dart'; +import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/work_order_card.dart'; +import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/quick_entry_card.dart'; +import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/power_trend_chart.dart'; +import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/plant_overview_card.dart'; + +class HomeV2Page extends StatefulWidget { + const HomeV2Page({super.key}); + + @override + State createState() => _HomeV2PageState(); +} + +class _HomeV2PageState extends State { + late HomeV2Bloc _bloc; + + @override + void initState() { + super.initState(); + _bloc = context.read(); + _bloc.add(const HomeV2LoadData()); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F7FA), + body: BlocBuilder( + builder: (context, state) { + if (state is HomeV2Loading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (state is HomeV2Error) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, size: 48, color: Color(0xFF86909C)), + const SizedBox(height: 16), + Text(state.message), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => _bloc.add(const HomeV2LoadData()), + child: const Text('重试'), + ), + ], + ), + ); + } + + if (state is HomeV2Loaded) { + return RefreshIndicator( + onRefresh: () async { + _bloc.add(const HomeV2Refresh()); + }, + child: CustomScrollView( + slivers: [ + SliverAppBar( + pinned: true, + backgroundColor: Colors.white, + elevation: 0, + flexibleSpace: FlexibleSpaceBar( + background: Container( + padding: const EdgeInsets.fromLTRB(16, 40, 16, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text( + '示例光伏电站', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(width: 4), + const Icon(Icons.arrow_drop_down, size: 20, color: Color(0xFF1D2129)), + const Spacer(), + IconButton( + icon: const Icon(Icons.sentiment_satisfied_alt_outlined, size: 24), + onPressed: () => print('点击表情'), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFF2F3F5), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.cloud_queue, size: 16, color: Color(0xFF165DFF)), + const SizedBox(width: 6), + const Text( + '多云 28°C', + style: TextStyle( + fontSize: 13, + color: Color(0xFF4E5969), + ), + ), + ], + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFFFF0F0), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.warning_amber_rounded, size: 16, color: Color(0xFFF53F3F)), + const SizedBox(width: 6), + const Expanded( + child: Text( + '紧急告警', + style: TextStyle( + fontSize: 13, + color: Color(0xFFF53F3F), + ), + ), + ), + const SizedBox(width: 4), + const Icon(Icons.arrow_forward_ios, size: 12, color: Color(0xFFF53F3F)), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + SliverPadding( + padding: const EdgeInsets.all(16), + sliver: SliverList( + delegate: SliverChildListDelegate([ + PowerCard(data: state.homeData), + const SizedBox(height: 12), + StatsGrid(data: state.homeData), + const SizedBox(height: 12), + PlantOverviewCard(overview: state.homeData.plantOverview), + const SizedBox(height: 12), + WorkOrderCard(stats: state.homeData.workOrderStats), + const SizedBox(height: 12), + QuickEntryCard(), + const SizedBox(height: 12), + PowerTrendChart( + data: state.homeData.powerTrendData, + trendType: state.trendType, + onToggleType: (type) => _bloc.add(HomeV2ToggleTrendType(type)), + ), + const SizedBox(height: 20), + ]), + ), + ), + ], + ), + ); + } + + return const SizedBox(); + }, + ), + ); + } +} diff --git a/lib/features/v2/home/presentation/widgets/plant_overview_card.dart b/lib/features/v2/home/presentation/widgets/plant_overview_card.dart new file mode 100644 index 00000000..04aa4970 --- /dev/null +++ b/lib/features/v2/home/presentation/widgets/plant_overview_card.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart'; + +class PlantOverviewCard extends StatelessWidget { + final PlantOverview overview; + + const PlantOverviewCard({super.key, required this.overview}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '电站概况', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 12), + Container( + height: 120, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + image: DecorationImage( + image: AssetImage(overview.backgroundImage), + fit: BoxFit.cover, + ), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withOpacity(0.6), + ], + ), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '装机容量', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + const SizedBox(height: 4), + Text( + '${overview.installedCapacity} MWp', + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const Text( + '投运日期', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + const SizedBox(height: 4), + Text( + overview.commissionDate, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/features/v2/home/presentation/widgets/power_card.dart b/lib/features/v2/home/presentation/widgets/power_card.dart new file mode 100644 index 00000000..f945a7a6 --- /dev/null +++ b/lib/features/v2/home/presentation/widgets/power_card.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart'; + +class PowerCard extends StatelessWidget { + final HomeEntity data; + + const PowerCard({super.key, required this.data}); + + @override + Widget build(BuildContext context) { + return Container( + height: 140, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF165DFF), Color(0xFF4080FF)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 12, 16), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + '实时发电功率', + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + const SizedBox(height: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Flexible( + child: Text( + data.realTimePower.toStringAsFixed(1), + style: const TextStyle( + color: Colors.white, + fontSize: 32, + fontWeight: FontWeight.bold, + height: 1.0, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 4), + const Padding( + padding: EdgeInsets.only(bottom: 4), + child: Text( + 'kW', + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + ), + ], + ), + ], + ), + ), + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + icon: const Icon(Icons.more_horiz, size: 20, color: Colors.white), + onPressed: () => print('点击更多'), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + Expanded( + child: SizedBox( + height: 70, + child: LineChart( + LineChartData( + gridData: const FlGridData(show: false), + titlesData: const FlTitlesData(show: false), + borderData: FlBorderData(show: false), + lineBarsData: [ + LineChartBarData( + spots: _generateSpots(), + isCurved: true, + color: Colors.white, + barWidth: 1.5, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + color: Colors.white.withOpacity(0.2), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + List _generateSpots() { + return data.powerTrendData + .asMap() + .entries + .map((e) => FlSpot(e.key.toDouble(), e.value.power)) + .toList(); + } +} diff --git a/lib/features/v2/home/presentation/widgets/power_trend_chart.dart b/lib/features/v2/home/presentation/widgets/power_trend_chart.dart new file mode 100644 index 00000000..1e1dcbb4 --- /dev/null +++ b/lib/features/v2/home/presentation/widgets/power_trend_chart.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart'; + +class PowerTrendChart extends StatelessWidget { + final List data; + final String trendType; + final Function(String) onToggleType; + + const PowerTrendChart({ + super.key, + required this.data, + required this.trendType, + required this.onToggleType, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '发电趋势(今日)', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + Container( + height: 32, + decoration: BoxDecoration( + color: const Color(0xFFF5F7FA), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + _buildTypeButton('kW'), + _buildTypeButton('周'), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Container( + height: 220, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: LineChart( + LineChartData( + gridData: FlGridData( + show: true, + drawVerticalLine: true, + horizontalInterval: 200, + verticalInterval: 6, + getDrawingHorizontalLine: (value) { + return FlLine( + color: const Color(0xFFE5E6EB).withOpacity(0.3), + strokeWidth: 1, + ); + }, + getDrawingVerticalLine: (value) { + return FlLine( + color: const Color(0xFFE5E6EB).withOpacity(0.3), + strokeWidth: 1, + ); + }, + ), + titlesData: FlTitlesData( + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + interval: 6, + getTitlesWidget: (value, meta) { + const titles = ['00:00', '06:00', '12:00', '18:00', '24:00']; + if (value.toInt() % 6 == 0 && value.toInt() < titles.length) { + return Text( + titles[value.toInt() ~/ 6], + style: const TextStyle( + color: Color(0xFF86909C), + fontSize: 12, + ), + ); + } + return const Text(''); + }, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + interval: 200, + getTitlesWidget: (value, meta) { + return Text( + value.toInt().toString(), + style: const TextStyle( + color: Color(0xFF86909C), + fontSize: 12, + ), + ); + }, + ), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData(show: false), + minX: 0, + maxX: 23, + minY: 0, + maxY: 1000, + lineBarsData: [ + LineChartBarData( + spots: data + .asMap() + .entries + .map((e) => FlSpot(e.key.toDouble(), e.value.power)) + .toList(), + isCurved: true, + color: const Color(0xFF165DFF), + barWidth: 2, + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 2, + color: const Color(0xFF165DFF), + ); + }, + ), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + const Color(0xFF165DFF).withOpacity(0.2), + const Color(0xFF165DFF).withOpacity(0.0), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildTypeButton(String type) { + final isSelected = trendType == type; + return GestureDetector( + onTap: () => onToggleType(type), + child: Container( + width: 50, + height: 32, + decoration: BoxDecoration( + color: isSelected ? const Color(0xFF165DFF) : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text( + type, + style: TextStyle( + color: isSelected ? Colors.white : const Color(0xFF86909C), + fontSize: 14, + ), + ), + ), + ); + } +} diff --git a/lib/features/v2/home/presentation/widgets/quick_entry_card.dart b/lib/features/v2/home/presentation/widgets/quick_entry_card.dart new file mode 100644 index 00000000..b1fadf9b --- /dev/null +++ b/lib/features/v2/home/presentation/widgets/quick_entry_card.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; + +class QuickEntryCard extends StatelessWidget { + const QuickEntryCard({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '快捷入口', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const Icon( + Icons.arrow_forward_ios, + size: 16, + color: Color(0xFF86909C), + ), + ], + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildQuickEntryItem( + icon: Icons.upload_file, + label: '设备上报', + ), + _buildQuickEntryItem( + icon: Icons.qr_code_scanner, + label: '扫一扫', + ), + _buildQuickEntryItem( + icon: Icons.assignment, + label: '工单任务', + ), + _buildQuickEntryItem( + icon: Icons.notifications_active, + label: '告警中心', + ), + ], + ), + ), + ], + ); + } + + Widget _buildQuickEntryItem({ + required IconData icon, + required String label, + }) { + return InkWell( + onTap: () => print('点击:$label'), + child: Column( + children: [ + Icon( + icon, + color: const Color(0xFF165DFF), + size: 32, + ), + const SizedBox(height: 8), + Text( + label, + style: const TextStyle( + color: Color(0xFF4E5969), + fontSize: 14, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/v2/home/presentation/widgets/stats_grid.dart b/lib/features/v2/home/presentation/widgets/stats_grid.dart new file mode 100644 index 00000000..a162f36f --- /dev/null +++ b/lib/features/v2/home/presentation/widgets/stats_grid.dart @@ -0,0 +1,218 @@ +import 'package:flutter/material.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart'; + +class StatsGrid extends StatelessWidget { + final HomeEntity data; + + const StatsGrid({super.key, required this.data}); + + @override + Widget build(BuildContext context) { + return GridView.count( + crossAxisCount: 2, + childAspectRatio: 1.5, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: 12, + crossAxisSpacing: 12, + children: [ + _buildStatCard( + title: '今日发电量', + value: data.todayGeneration.toStringAsFixed(0), + unit: 'kWh', + valueColor: const Color(0xFF00B42A), + ), + _buildStatCard( + title: '等效小时数', + value: data.equivalentHours.toStringAsFixed(1), + unit: 'h', + valueColor: const Color(0xFF165DFF), + ), + _buildAlarmCard(data.alarmCount), + _buildWorkOrderCard(data.pendingWorkOrders), + ], + ); + } + + Widget _buildStatCard({ + required String title, + required String value, + required String unit, + required Color valueColor, + }) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: const TextStyle( + color: Color(0xFF4E5969), + fontSize: 14, + ), + ), + const Icon( + Icons.arrow_forward_ios, + size: 14, + color: Color(0xFF86909C), + ), + ], + ), + const Spacer(), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + value, + style: TextStyle( + color: valueColor, + fontSize: 32, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 4), + Text( + unit, + style: const TextStyle( + color: Color(0xFF4E5969), + fontSize: 14, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildAlarmCard(int count) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFFFF0F0), + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + '告警数', + style: TextStyle( + color: Color(0xFFF53F3F), + fontSize: 14, + ), + ), + const SizedBox(height: 8), + Text( + count.toString(), + style: const TextStyle( + color: Color(0xFFF53F3F), + fontSize: 32, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + Container( + width: 48, + height: 48, + decoration: const BoxDecoration( + color: Color(0xFFFFE8E8), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.notifications_active, + size: 28, + color: Color(0xFFF53F3F), + ), + ), + ], + ), + ); + } + + Widget _buildWorkOrderCard(int count) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + '待办工单', + style: TextStyle( + color: Color(0xFF4E5969), + fontSize: 14, + ), + ), + const SizedBox(height: 8), + Text( + count.toString(), + style: const TextStyle( + color: Color(0xFF1D2129), + fontSize: 32, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + Container( + width: 48, + height: 48, + decoration: const BoxDecoration( + color: Color(0xFFE8F3FF), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.assignment, + size: 28, + color: Color(0xFF165DFF), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/v2/home/presentation/widgets/work_order_card.dart b/lib/features/v2/home/presentation/widgets/work_order_card.dart new file mode 100644 index 00000000..42555a29 --- /dev/null +++ b/lib/features/v2/home/presentation/widgets/work_order_card.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart'; + +class WorkOrderCard extends StatelessWidget { + final WorkOrderStats stats; + + const WorkOrderCard({super.key, required this.stats}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '待办工单', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const Icon( + Icons.arrow_forward_ios, + size: 16, + color: Color(0xFF86909C), + ), + ], + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: _buildWorkOrderItem( + title: '待处理', + count: stats.pending, + color: const Color(0xFFF53F3F), + bgColor: Colors.white, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildWorkOrderItem( + title: '处理中', + count: stats.processing, + color: const Color(0xFFFF7D00), + bgColor: const Color(0xFFFFF7E6), + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildWorkOrderItem( + title: '待验收', + count: stats.pendingAcceptance, + color: const Color(0xFF1D2129), + bgColor: Colors.white, + ), + ), + ], + ), + ), + ], + ); + } + + Widget _buildWorkOrderItem({ + required String title, + required int count, + required Color color, + required Color bgColor, + }) { + return Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + color: color, + fontSize: 14, + ), + ), + const SizedBox(height: 4), + Text( + count.toString(), + style: TextStyle( + color: color, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + Icon( + Icons.arrow_forward_ios, + size: 16, + color: color.withOpacity(0.5), + ), + ], + ), + ); + } +}