v2版本登录

This commit is contained in:
2026-05-19 08:46:35 +08:00
parent 07b6b19b34
commit b9f747de05
16 changed files with 1599 additions and 0 deletions

View File

@@ -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<HomeDataModel> getHomeData();
}
class HomeRemoteDataSourceImpl implements HomeRemoteDataSource {
@override
Future<HomeDataModel> 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',
),
);
}
}

View File

@@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() {
return {
'condition': condition,
'tempMin': tempMin,
'tempMax': tempMax,
'windDirection': windDirection,
'windLevel': windLevel,
};
}
}
extension WorkOrderStatsModelX on WorkOrderStats {
Map<String, dynamic> toJson() {
return {
'pending': pending,
'processing': processing,
'pendingAcceptance': pendingAcceptance,
};
}
}
extension PlantOverviewModelX on PlantOverview {
Map<String, dynamic> toJson() {
return {
'installedCapacity': installedCapacity,
'commissionDate': commissionDate,
'backgroundImage': backgroundImage,
};
}
}
extension PowerTrendPointModelX on PowerTrendPoint {
Map<String, dynamic> toJson() {
return {
'time': time,
'power': power,
};
}
}

View File

@@ -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<Either<Failure, HomeEntity>> getHomeData() async {
try {
final result = await remoteDataSource.getHomeData();
return Right(result.toEntity());
} catch (e) {
return Left(Failure('获取首页数据失败:$e'));
}
}
}

View File

@@ -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<PowerTrendPoint> 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<Object?> 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<String, dynamic> 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<String, dynamic> toJson() {
return {
'condition': condition,
'tempMin': tempMin,
'tempMax': tempMax,
'windDirection': windDirection,
'windLevel': windLevel,
};
}
@override
List<Object?> 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<String, dynamic> json) {
return WorkOrderStats(
pending: json['pending'] as int,
processing: json['processing'] as int,
pendingAcceptance: json['pendingAcceptance'] as int,
);
}
Map<String, dynamic> toJson() {
return {
'pending': pending,
'processing': processing,
'pendingAcceptance': pendingAcceptance,
};
}
@override
List<Object?> 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<String, dynamic> json) {
return PowerTrendPoint(
time: json['time'] as String,
power: (json['power'] as num).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'time': time,
'power': power,
};
}
@override
List<Object?> 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<String, dynamic> json) {
return PlantOverview(
installedCapacity: (json['installedCapacity'] as num).toDouble(),
commissionDate: json['commissionDate'] as String,
backgroundImage: json['backgroundImage'] as String? ?? 'assets/images/carousel1.png',
);
}
Map<String, dynamic> toJson() {
return {
'installedCapacity': installedCapacity,
'commissionDate': commissionDate,
'backgroundImage': backgroundImage,
};
}
@override
List<Object?> get props => [installedCapacity, commissionDate, backgroundImage];
}

View File

@@ -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<Either<Failure, HomeEntity>> getHomeData();
}

View File

@@ -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<HomeEntity, NoParams> {
final HomeRepository repository;
GetHomeDataUseCase(this.repository);
@override
Future<Either<Failure, HomeEntity>> call(NoParams params) async {
try {
return await repository.getHomeData();
} catch (e) {
return Left(Failure('获取首页数据失败:$e'));
}
}
}
class NoParams {
const NoParams();
}

View File

@@ -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<HomeV2Event, HomeV2State> {
final GetHomeDataUseCase getHomeDataUseCase;
HomeV2Bloc(this.getHomeDataUseCase) : super(const HomeV2Initial()) {
on<HomeV2LoadData>(_onLoadData);
on<HomeV2Refresh>(_onRefresh);
on<HomeV2ToggleTrendType>(_onToggleTrendType);
}
Future<void> _onLoadData(
HomeV2LoadData event,
Emitter<HomeV2State> emit,
) async {
emit(const HomeV2Loading());
final result = await getHomeDataUseCase(const NoParams());
result.fold(
(failure) => emit(HomeV2Error(failure.message)),
(data) => emit(HomeV2Loaded(homeData: data)),
);
}
Future<void> _onRefresh(
HomeV2Refresh event,
Emitter<HomeV2State> 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<HomeV2State> emit,
) {
if (state is HomeV2Loaded) {
final currentState = state as HomeV2Loaded;
emit(currentState.copyWith(trendType: event.type));
print('切换趋势类型:${event.type}');
}
}
}

View File

@@ -0,0 +1,25 @@
import 'package:equatable/equatable.dart';
abstract class HomeV2Event extends Equatable {
const HomeV2Event();
@override
List<Object?> 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<Object?> get props => [type];
}

View File

@@ -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<Object?> 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<Object?> get props => [homeData, trendType];
}
class HomeV2Error extends HomeV2State {
final String message;
const HomeV2Error(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -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<HomeV2Page> createState() => _HomeV2PageState();
}
class _HomeV2PageState extends State<HomeV2Page> {
late HomeV2Bloc _bloc;
@override
void initState() {
super.initState();
_bloc = context.read<HomeV2Bloc>();
_bloc.add(const HomeV2LoadData());
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F7FA),
body: BlocBuilder<HomeV2Bloc, HomeV2State>(
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();
},
),
);
}
}

View File

@@ -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,
),
),
],
),
],
),
],
),
),
),
),
],
);
}
}

View File

@@ -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<FlSpot> _generateSpots() {
return data.powerTrendData
.asMap()
.entries
.map((e) => FlSpot(e.key.toDouble(), e.value.power))
.toList();
}
}

View File

@@ -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<PowerTrendPoint> 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,
),
),
),
);
}
}

View File

@@ -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,
),
),
],
),
);
}
}

View File

@@ -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),
),
),
],
),
);
}
}

View File

@@ -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),
),
],
),
);
}
}