Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
S
smart_hotel_app
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
张宏
smart_hotel_app
Commits
1a461d59
Commit
1a461d59
authored
Jun 10, 2026
by
张宏
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
2
parent
8e8f1b15
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
1321 additions
and
303 deletions
+1321
-303
dashboard_bo.dart
lib/models/bo/dashboard_bo.dart
+0
-0
device_list_bo.dart
lib/models/bo/device_list_bo.dart
+186
-0
linkage_rule_bo.dart
lib/models/bo/linkage_rule_bo.dart
+67
-0
dashboard_repository.dart
lib/repositories/dashboard_repository.dart
+25
-0
device_list_repository.dart
lib/repositories/device_list_repository.dart
+28
-0
linkage_rule_repository.dart
lib/repositories/linkage_rule_repository.dart
+33
-0
dashboard_service.dart
lib/services/dashboard_service.dart
+27
-0
device_list_service.dart
lib/services/device_list_service.dart
+25
-0
linkage_rule_service.dart
lib/services/linkage_rule_service.dart
+33
-0
device_list_cubit.dart
lib/views/report/device/cubit/device_list_cubit.dart
+55
-78
device_list_state.dart
lib/views/report/device/cubit/device_list_state.dart
+78
-39
device_list_view.dart
lib/views/report/device/device_list_view.dart
+169
-47
energy_cubit.dart
lib/views/report/energy/cubit/energy_cubit.dart
+68
-3
energy_state.dart
lib/views/report/energy/cubit/energy_state.dart
+16
-3
energy_detail_view.dart
lib/views/report/energy/energy_detail_view.dart
+33
-9
report_index_cubit.dart
lib/views/report/index/cubit/report_index_cubit.dart
+91
-38
report_index_state.dart
lib/views/report/index/cubit/report_index_state.dart
+12
-2
index_view.dart
lib/views/report/index/index_view.dart
+38
-4
rule_management_cubit.dart
lib/views/report/rule/cubit/rule_management_cubit.dart
+83
-59
rule_management_state.dart
lib/views/report/rule/cubit/rule_management_state.dart
+39
-18
对接方案.md
对接方案.md
+215
-3
No files found.
lib/models/bo/dashboard_bo.dart
0 → 100644
View file @
1a461d59
This diff is collapsed.
Click to expand it.
lib/models/bo/device_list_bo.dart
0 → 100644
View file @
1a461d59
import
'package:equatable/equatable.dart'
;
int
_parseInt
(
dynamic
value
,
{
int
fallback
=
0
})
{
if
(
value
==
null
)
return
fallback
;
if
(
value
is
int
)
return
value
;
if
(
value
is
double
)
return
value
.
toInt
();
if
(
value
is
String
)
{
final
parsed
=
int
.
tryParse
(
value
.
trim
());
return
parsed
??
fallback
;
}
return
fallback
;
}
String
_parseString
(
dynamic
value
,
{
String
fallback
=
''
})
{
if
(
value
==
null
)
return
fallback
;
if
(
value
is
String
)
return
value
;
return
value
.
toString
();
}
class
DeviceSubtitleBO
extends
Equatable
{
final
String
text
;
final
String
voltage
;
final
String
powerDisplay
;
final
String
temperature
;
final
String
statusText
;
final
String
wifiSignal
;
const
DeviceSubtitleBO
({
required
this
.
text
,
required
this
.
voltage
,
required
this
.
powerDisplay
,
required
this
.
temperature
,
required
this
.
statusText
,
required
this
.
wifiSignal
,
});
factory
DeviceSubtitleBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
DeviceSubtitleBO
(
text:
_parseString
(
json
[
'text'
]),
voltage:
_parseString
(
json
[
'voltage'
]),
powerDisplay:
_parseString
(
json
[
'powerDisplay'
]),
temperature:
_parseString
(
json
[
'temperature'
]),
statusText:
_parseString
(
json
[
'statusText'
]),
wifiSignal:
_parseString
(
json
[
'wifiSignal'
]),
);
}
@override
List
<
Object
?>
get
props
=>
[
text
,
voltage
,
powerDisplay
,
temperature
,
statusText
,
wifiSignal
];
}
class
DeviceStatusTagBO
extends
Equatable
{
final
String
text
;
final
String
color
;
const
DeviceStatusTagBO
({
required
this
.
text
,
required
this
.
color
});
factory
DeviceStatusTagBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
DeviceStatusTagBO
(
text:
_parseString
(
json
[
'text'
]),
color:
_parseString
(
json
[
'color'
]),
);
}
@override
List
<
Object
?>
get
props
=>
[
text
,
color
];
}
class
DeviceRowBO
extends
Equatable
{
final
int
deviceId
;
final
String
deviceName
;
final
String
deviceCode
;
final
String
deviceLocation
;
final
String
deviceTypeIcon
;
final
String
deviceTypeName
;
final
DeviceSubtitleBO
subtitle
;
final
DeviceStatusTagBO
statusTag
;
final
String
onlineStatus
;
final
String
runStatus
;
final
int
roomId
;
final
String
roomNumber
;
const
DeviceRowBO
({
required
this
.
deviceId
,
required
this
.
deviceName
,
required
this
.
deviceCode
,
required
this
.
deviceLocation
,
required
this
.
deviceTypeIcon
,
required
this
.
deviceTypeName
,
required
this
.
subtitle
,
required
this
.
statusTag
,
required
this
.
onlineStatus
,
required
this
.
runStatus
,
required
this
.
roomId
,
required
this
.
roomNumber
,
});
factory
DeviceRowBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
DeviceRowBO
(
deviceId:
_parseInt
(
json
[
'deviceId'
]),
deviceName:
_parseString
(
json
[
'deviceName'
]),
deviceCode:
_parseString
(
json
[
'deviceCode'
]),
deviceLocation:
_parseString
(
json
[
'deviceLocation'
]),
deviceTypeIcon:
_parseString
(
json
[
'deviceTypeIcon'
]),
deviceTypeName:
_parseString
(
json
[
'deviceTypeName'
]),
subtitle:
DeviceSubtitleBO
.
fromJson
(
json
[
'subtitle'
]
as
Map
<
String
,
dynamic
>?
??
{}),
statusTag:
DeviceStatusTagBO
.
fromJson
(
json
[
'statusTag'
]
as
Map
<
String
,
dynamic
>?
??
{}),
onlineStatus:
_parseString
(
json
[
'onlineStatus'
]),
runStatus:
_parseString
(
json
[
'runStatus'
]),
roomId:
_parseInt
(
json
[
'roomId'
]),
roomNumber:
_parseString
(
json
[
'roomNumber'
]),
);
}
@override
List
<
Object
?>
get
props
=>
[
deviceId
,
deviceName
,
deviceCode
,
deviceLocation
,
deviceTypeIcon
,
deviceTypeName
,
subtitle
,
statusTag
,
onlineStatus
,
runStatus
,
roomId
,
roomNumber
,
];
}
class
DeviceListTabCountBO
extends
Equatable
{
final
int
all
;
final
int
airSwitch
;
final
int
guestControl
;
final
int
network
;
const
DeviceListTabCountBO
({
required
this
.
all
,
required
this
.
airSwitch
,
required
this
.
guestControl
,
required
this
.
network
,
});
factory
DeviceListTabCountBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
DeviceListTabCountBO
(
all:
_parseInt
(
json
[
'all'
]),
airSwitch:
_parseInt
(
json
[
'airSwitch'
]),
guestControl:
_parseInt
(
json
[
'guestControl'
]),
network:
_parseInt
(
json
[
'network'
]),
);
}
@override
List
<
Object
?>
get
props
=>
[
all
,
airSwitch
,
guestControl
,
network
];
}
class
DeviceListBO
extends
Equatable
{
final
int
total
;
final
DeviceListTabCountBO
tabCount
;
final
List
<
DeviceRowBO
>
rows
;
const
DeviceListBO
({
required
this
.
total
,
required
this
.
tabCount
,
required
this
.
rows
,
});
factory
DeviceListBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
DeviceListBO
(
total:
_parseInt
(
json
[
'total'
]),
tabCount:
DeviceListTabCountBO
.
fromJson
(
json
[
'tabCount'
]
as
Map
<
String
,
dynamic
>?
??
{}),
rows:
(
json
[
'rows'
]
as
List
<
dynamic
>?)
?.
map
((
e
)
=>
DeviceRowBO
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
()
??
[],
);
}
@override
List
<
Object
?>
get
props
=>
[
total
,
tabCount
,
rows
];
}
lib/models/bo/linkage_rule_bo.dart
0 → 100644
View file @
1a461d59
import
'package:equatable/equatable.dart'
;
int
_parseInt
(
dynamic
value
,
{
int
fallback
=
0
})
{
if
(
value
==
null
)
return
fallback
;
if
(
value
is
int
)
return
value
;
if
(
value
is
double
)
return
value
.
toInt
();
if
(
value
is
String
)
{
final
parsed
=
int
.
tryParse
(
value
.
trim
());
return
parsed
??
fallback
;
}
return
fallback
;
}
String
_parseString
(
dynamic
value
,
{
String
fallback
=
''
})
{
if
(
value
==
null
)
return
fallback
;
if
(
value
is
String
)
return
value
;
return
value
.
toString
();
}
/// GET /app/linkage/rules 响应中的单条规则
class
LinkageRuleBO
extends
Equatable
{
final
int
ruleId
;
final
String
ruleName
;
final
String
description
;
final
String
enabled
;
// "true"/"false" 或 "1"/"0"
const
LinkageRuleBO
({
required
this
.
ruleId
,
required
this
.
ruleName
,
required
this
.
description
,
required
this
.
enabled
,
});
factory
LinkageRuleBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
LinkageRuleBO
(
ruleId:
_parseInt
(
json
[
'ruleId'
]),
ruleName:
_parseString
(
json
[
'ruleName'
]),
description:
_parseString
(
json
[
'description'
]),
enabled:
_parseString
(
json
[
'enabled'
]),
);
}
bool
get
isEnabled
{
return
enabled
==
'true'
||
enabled
==
'1'
;
}
@override
List
<
Object
?>
get
props
=>
[
ruleId
,
ruleName
,
description
,
enabled
];
}
/// PUT /app/linkage/rules/toggle 响应
class
LinkageRuleToggleBO
extends
Equatable
{
final
int
ruleId
;
final
String
enabled
;
const
LinkageRuleToggleBO
({
required
this
.
ruleId
,
required
this
.
enabled
});
factory
LinkageRuleToggleBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
LinkageRuleToggleBO
(
ruleId:
_parseInt
(
json
[
'ruleId'
]),
enabled:
_parseString
(
json
[
'enabled'
]),
);
}
@override
List
<
Object
?>
get
props
=>
[
ruleId
,
enabled
];
}
lib/repositories/dashboard_repository.dart
0 → 100644
View file @
1a461d59
import
'../utils/http/response_model.dart'
;
import
'../utils/http/dio_request.dart'
;
import
'../models/bo/dashboard_bo.dart'
;
class
DashboardRepository
{
/// 获取经营概览看板数据
/// GET /app/dashboard/overview
Future
<
ResponseModel
<
DashboardOverviewBO
>>
getOverview
()
{
return
DioRequest
.
instance
.
get
<
DashboardOverviewBO
>(
'/app/dashboard/overview'
,
fromJsonT:
(
data
)
=>
DashboardOverviewBO
.
fromJson
(
data
as
Map
<
String
,
dynamic
>),
);
}
/// 获取能耗分析数据
/// GET /app/dashboard/energy-analysis
Future
<
ResponseModel
<
EnergyAnalysisBO
>>
getEnergyAnalysis
()
{
return
DioRequest
.
instance
.
get
<
EnergyAnalysisBO
>(
'/app/dashboard/energy-analysis'
,
fromJsonT:
(
data
)
=>
EnergyAnalysisBO
.
fromJson
(
data
as
Map
<
String
,
dynamic
>),
);
}
}
lib/repositories/device_list_repository.dart
0 → 100644
View file @
1a461d59
import
'../utils/http/response_model.dart'
;
import
'../utils/http/dio_request.dart'
;
import
'../models/bo/device_list_bo.dart'
;
class
DeviceListRepository
{
/// 获取全部设备列表(分页)
/// GET /app/device/list
Future
<
ResponseModel
<
DeviceListBO
>>
getList
({
required
int
pageSize
,
required
int
pageNum
,
int
?
deviceTypeId
,
})
{
final
params
=
<
String
,
dynamic
>{
'pageSize'
:
pageSize
,
'pageNum'
:
pageNum
,
};
if
(
deviceTypeId
!=
null
)
{
params
[
'deviceTypeId'
]
=
deviceTypeId
;
}
return
DioRequest
.
instance
.
get
<
DeviceListBO
>(
'/app/device/list'
,
queryParameters:
params
,
fromJsonT:
(
data
)
=>
DeviceListBO
.
fromJson
(
data
as
Map
<
String
,
dynamic
>),
);
}
}
lib/repositories/linkage_rule_repository.dart
0 → 100644
View file @
1a461d59
import
'../utils/http/response_model.dart'
;
import
'../utils/http/dio_request.dart'
;
import
'../models/bo/linkage_rule_bo.dart'
;
class
LinkageRuleRepository
{
/// 获取联动规则列表
/// GET /app/linkage/rules
Future
<
ResponseModel
<
List
<
LinkageRuleBO
>>>
getRules
()
{
return
DioRequest
.
instance
.
get
<
List
<
LinkageRuleBO
>>(
'/app/linkage/rules'
,
fromJsonT:
(
data
)
{
final
list
=
data
as
List
<
dynamic
>;
return
list
.
map
((
e
)
=>
LinkageRuleBO
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
();
},
);
}
/// 启用/禁用联动规则
/// PUT /app/linkage/rules/toggle
Future
<
ResponseModel
<
LinkageRuleToggleBO
>>
toggleRule
({
required
int
ruleId
,
required
String
enabled
,
})
{
return
DioRequest
.
instance
.
put
<
LinkageRuleToggleBO
>(
'/app/linkage/rules/toggle'
,
data:
{
'ruleId'
:
ruleId
,
'enabled'
:
enabled
},
fromJsonT:
(
data
)
=>
LinkageRuleToggleBO
.
fromJson
(
data
as
Map
<
String
,
dynamic
>),
);
}
}
lib/services/dashboard_service.dart
0 → 100644
View file @
1a461d59
import
'../models/bo/dashboard_bo.dart'
;
import
'../repositories/dashboard_repository.dart'
;
class
DashboardService
{
final
DashboardRepository
_repository
;
DashboardService
({
required
DashboardRepository
repository
})
:
_repository
=
repository
;
/// 获取经营概览看板数据
Future
<
DashboardOverviewBO
>
getOverview
()
async
{
final
result
=
await
_repository
.
getOverview
();
if
(
result
.
success
&&
result
.
data
!=
null
)
{
return
result
.
data
!;
}
throw
Exception
(
result
.
msg
);
}
/// 获取能耗分析数据
Future
<
EnergyAnalysisBO
>
getEnergyAnalysis
()
async
{
final
result
=
await
_repository
.
getEnergyAnalysis
();
if
(
result
.
success
&&
result
.
data
!=
null
)
{
return
result
.
data
!;
}
throw
Exception
(
result
.
msg
);
}
}
lib/services/device_list_service.dart
0 → 100644
View file @
1a461d59
import
'../models/bo/device_list_bo.dart'
;
import
'../repositories/device_list_repository.dart'
;
class
DeviceListService
{
final
DeviceListRepository
_repository
;
DeviceListService
({
required
DeviceListRepository
repository
})
:
_repository
=
repository
;
Future
<
DeviceListBO
>
getList
({
required
int
pageSize
,
required
int
pageNum
,
int
?
deviceTypeId
,
})
async
{
final
result
=
await
_repository
.
getList
(
pageSize:
pageSize
,
pageNum:
pageNum
,
deviceTypeId:
deviceTypeId
,
);
if
(
result
.
success
&&
result
.
data
!=
null
)
{
return
result
.
data
!;
}
throw
Exception
(
result
.
msg
);
}
}
lib/services/linkage_rule_service.dart
0 → 100644
View file @
1a461d59
import
'../models/bo/linkage_rule_bo.dart'
;
import
'../repositories/linkage_rule_repository.dart'
;
class
LinkageRuleService
{
final
LinkageRuleRepository
_repository
;
LinkageRuleService
({
required
LinkageRuleRepository
repository
})
:
_repository
=
repository
;
/// 获取联动规则列表
Future
<
List
<
LinkageRuleBO
>>
getRules
()
async
{
final
result
=
await
_repository
.
getRules
();
if
(
result
.
success
&&
result
.
data
!=
null
)
{
return
result
.
data
!;
}
throw
Exception
(
result
.
msg
);
}
/// 启用/禁用联动规则
Future
<
LinkageRuleToggleBO
>
toggleRule
({
required
int
ruleId
,
required
String
enabled
,
})
async
{
final
result
=
await
_repository
.
toggleRule
(
ruleId:
ruleId
,
enabled:
enabled
,
);
if
(
result
.
success
&&
result
.
data
!=
null
)
{
return
result
.
data
!;
}
throw
Exception
(
result
.
msg
);
}
}
lib/views/report/device/cubit/device_list_cubit.dart
View file @
1a461d59
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'
;
import
'package:smart_hotel_app/repositories/device_list_repository.dart'
;
import
'package:smart_hotel_app/services/device_list_service.dart'
;
import
'package:smart_hotel_app/views/report/device/cubit/device_list_state.dart'
;
class
DeviceListCubit
extends
Cubit
<
DeviceListState
>
{
DeviceListCubit
()
:
super
(
const
DeviceListState
())
{
_initData
();
static
const
int
pageSize
=
20
;
final
DeviceListService
_service
;
PagingController
<
int
,
DeviceInfo
>?
_pagingController
;
DeviceListCubit
({
DeviceListService
?
service
})
:
_service
=
service
??
DeviceListService
(
repository:
DeviceListRepository
()),
super
(
const
DeviceListState
());
/// 绑定 PagingController,在 View 的 initState 中调用
void
bindPagingController
(
PagingController
<
int
,
DeviceInfo
>
controller
)
{
_pagingController
=
controller
;
}
void
_initData
()
{
final
devices
=
[
const
DeviceInfo
(
id:
'1'
,
name:
'智能空开 1F-01'
,
room:
'101'
,
type:
DeviceTypeEnum
.
smartBreaker
,
online:
true
,
status:
'正常'
,
lastUpdate:
'10:30'
,
),
const
DeviceInfo
(
id:
'2'
,
name:
'客控设备 1F-01'
,
room:
'101'
,
type:
DeviceTypeEnum
.
guestControl
,
online:
true
,
status:
'正常'
,
lastUpdate:
'10:32'
,
),
const
DeviceInfo
(
id:
'3'
,
name:
'网络设备 1F'
,
room:
'一楼'
,
type:
DeviceTypeEnum
.
network
,
online:
true
,
status:
'正常'
,
lastUpdate:
'10:28'
,
),
const
DeviceInfo
(
id:
'4'
,
name:
'智能空开 2F-01'
,
room:
'201'
,
type:
DeviceTypeEnum
.
smartBreaker
,
online:
true
,
status:
'正常'
,
lastUpdate:
'10:25'
,
),
const
DeviceInfo
(
id:
'5'
,
name:
'客控设备 2F-01'
,
room:
'201'
,
type:
DeviceTypeEnum
.
guestControl
,
online:
false
,
status:
'离线'
,
lastUpdate:
'09:15'
,
),
const
DeviceInfo
(
id:
'6'
,
name:
'智能空开 3F-01'
,
room:
'301'
,
type:
DeviceTypeEnum
.
smartBreaker
,
online:
true
,
status:
'正常'
,
lastUpdate:
'10:30'
,
),
const
DeviceInfo
(
id:
'7'
,
name:
'客控设备 3F-01'
,
room:
'301'
,
type:
DeviceTypeEnum
.
guestControl
,
online:
true
,
status:
'正常'
,
lastUpdate:
'10:30'
,
),
const
DeviceInfo
(
id:
'8'
,
name:
'网络设备 2F'
,
room:
'二楼'
,
type:
DeviceTypeEnum
.
network
,
online:
true
,
status:
'正常'
,
lastUpdate:
'10:30'
,
),
];
/// 解绑 PagingController,在 View 的 dispose 中调用
void
unbindPagingController
()
{
_pagingController
=
null
;
}
/// 供 PagingController.fetchPage 回调使用
Future
<
List
<
DeviceInfo
>>
fetchPage
(
int
pageKey
)
async
{
final
typeId
=
deviceTypeIdFromEnum
(
state
.
selectedType
);
final
result
=
await
_service
.
getList
(
pageSize:
pageSize
,
pageNum:
pageKey
,
deviceTypeId:
typeId
,
);
// 首页时更新 tab 计数和总数
if
(
pageKey
==
1
)
{
emit
(
state
.
copyWith
(
tabAll:
result
.
tabCount
.
all
,
tabAirSwitch:
result
.
tabCount
.
airSwitch
,
tabGuestControl:
result
.
tabCount
.
guestControl
,
tabNetwork:
result
.
tabCount
.
network
,
totalCount:
result
.
total
,
));
}
emit
(
state
.
copyWith
(
devices:
devices
));
return
result
.
rows
.
map
((
row
)
{
return
DeviceInfo
(
deviceId:
row
.
deviceId
,
name:
row
.
deviceName
,
room:
row
.
roomNumber
.
isNotEmpty
?
row
.
roomNumber
:
row
.
deviceLocation
,
type:
deviceTypeFromName
(
row
.
deviceTypeName
),
online:
row
.
onlineStatus
==
'1'
,
status:
row
.
statusTag
.
text
.
isNotEmpty
?
row
.
statusTag
.
text
:
(
row
.
onlineStatus
==
'1'
?
'正常'
:
'离线'
),
lastUpdate:
row
.
subtitle
.
text
,
);
}).
toList
();
}
/// 切换筛选类型
void
selectType
(
DeviceTypeEnum
type
)
{
if
(
state
.
selectedType
==
type
)
return
;
emit
(
state
.
copyWith
(
selectedType:
type
));
_pagingController
?.
refresh
();
}
}
lib/views/report/device/cubit/device_list_state.dart
View file @
1a461d59
...
...
@@ -7,8 +7,37 @@ enum DeviceTypeEnum {
network
,
}
class
DeviceInfo
{
final
String
id
;
/// deviceTypeId 映射: all→null, smartBreaker→1, guestControl→2, network→3
int
?
deviceTypeIdFromEnum
(
DeviceTypeEnum
type
)
{
switch
(
type
)
{
case
DeviceTypeEnum
.
all
:
return
null
;
case
DeviceTypeEnum
.
smartBreaker
:
return
1
;
case
DeviceTypeEnum
.
guestControl
:
return
2
;
case
DeviceTypeEnum
.
network
:
return
3
;
}
}
/// 根据 API 的 deviceTypeName 匹配 DeviceTypeEnum
DeviceTypeEnum
deviceTypeFromName
(
String
typeName
)
{
final
name
=
typeName
.
toLowerCase
();
if
(
name
.
contains
(
'空开'
)
||
name
.
contains
(
'airswitch'
)
||
name
.
contains
(
'breaker'
))
{
return
DeviceTypeEnum
.
smartBreaker
;
}
if
(
name
.
contains
(
'客控'
)
||
name
.
contains
(
'guest'
))
{
return
DeviceTypeEnum
.
guestControl
;
}
if
(
name
.
contains
(
'网络'
)
||
name
.
contains
(
'network'
))
{
return
DeviceTypeEnum
.
network
;
}
return
DeviceTypeEnum
.
smartBreaker
;
// fallback
}
class
DeviceInfo
extends
Equatable
{
final
int
deviceId
;
final
String
name
;
final
String
room
;
final
DeviceTypeEnum
type
;
...
...
@@ -17,62 +46,72 @@ class DeviceInfo {
final
String
lastUpdate
;
const
DeviceInfo
({
required
this
.
id
,
required
this
.
name
,
required
this
.
room
,
this
.
deviceId
=
0
,
this
.
name
=
''
,
this
.
room
=
''
,
required
this
.
type
,
required
this
.
onlin
e
,
required
this
.
status
,
required
this
.
lastUpdate
,
this
.
online
=
fals
e
,
this
.
status
=
''
,
this
.
lastUpdate
=
''
,
});
DeviceInfo
copyWith
({
String
?
id
,
String
?
name
,
String
?
room
,
DeviceTypeEnum
?
type
,
bool
?
online
,
String
?
status
,
String
?
lastUpdate
,
})
{
return
DeviceInfo
(
id:
id
??
this
.
id
,
name:
name
??
this
.
name
,
room:
room
??
this
.
room
,
type:
type
??
this
.
type
,
online:
online
??
this
.
online
,
status:
status
??
this
.
status
,
lastUpdate:
lastUpdate
??
this
.
lastUpdate
,
);
}
@override
List
<
Object
?>
get
props
=>
[
deviceId
,
name
,
room
,
type
,
online
,
status
,
lastUpdate
];
}
class
DeviceListState
extends
Equatable
{
final
List
<
DeviceInfo
>
devices
;
final
bool
isLoading
;
final
String
?
error
;
final
int
tabAll
;
final
int
tabAirSwitch
;
final
int
tabGuestControl
;
final
int
tabNetwork
;
final
int
totalCount
;
final
DeviceTypeEnum
selectedType
;
const
DeviceListState
({
this
.
devices
=
const
[],
this
.
isLoading
=
false
,
this
.
error
,
this
.
tabAll
=
0
,
this
.
tabAirSwitch
=
0
,
this
.
tabGuestControl
=
0
,
this
.
tabNetwork
=
0
,
this
.
totalCount
=
0
,
this
.
selectedType
=
DeviceTypeEnum
.
all
,
});
DeviceListState
copyWith
({
List
<
DeviceInfo
>?
devices
,
bool
?
isLoading
,
String
?
error
,
int
?
tabAll
,
int
?
tabAirSwitch
,
int
?
tabGuestControl
,
int
?
tabNetwork
,
int
?
totalCount
,
DeviceTypeEnum
?
selectedType
,
bool
clearError
=
false
,
})
{
return
DeviceListState
(
devices:
devices
??
this
.
devices
,
isLoading:
isLoading
??
this
.
isLoading
,
error:
clearError
?
null
:
(
error
??
this
.
error
),
tabAll:
tabAll
??
this
.
tabAll
,
tabAirSwitch:
tabAirSwitch
??
this
.
tabAirSwitch
,
tabGuestControl:
tabGuestControl
??
this
.
tabGuestControl
,
tabNetwork:
tabNetwork
??
this
.
tabNetwork
,
totalCount:
totalCount
??
this
.
totalCount
,
selectedType:
selectedType
??
this
.
selectedType
,
);
}
List
<
DeviceInfo
>
get
filteredDevices
{
if
(
selectedType
==
DeviceTypeEnum
.
all
)
{
return
devices
;
}
return
devices
.
where
((
device
)
=>
device
.
type
==
selectedType
).
toList
();
}
@override
List
<
Object
?>
get
props
=>
[
devices
,
selectedType
];
List
<
Object
?>
get
props
=>
[
isLoading
,
error
,
tabAll
,
tabAirSwitch
,
tabGuestControl
,
tabNetwork
,
totalCount
,
selectedType
,
];
}
lib/views/report/device/device_list_view.dart
View file @
1a461d59
...
...
@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:flutter_screenutil/flutter_screenutil.dart'
;
import
'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'
;
import
'package:smart_hotel_app/views/report/device/cubit/device_list_cubit.dart'
;
import
'package:smart_hotel_app/views/report/device/cubit/device_list_state.dart'
;
import
'package:smart_hotel_app/views/report/device/widget/device_card.dart'
;
...
...
@@ -15,56 +16,177 @@ class ReportDeviceListView extends StatelessWidget {
Widget
build
(
BuildContext
context
)
{
return
BlocProvider
(
create:
(
_
)
=>
DeviceListCubit
(),
child:
BlocBuilder
<
DeviceListCubit
,
DeviceListState
>(
builder:
(
context
,
state
)
{
return
Scaffold
(
backgroundColor:
const
Color
.
fromRGBO
(
242
,
243
,
245
,
1
),
appBar:
AppBar
(
backgroundColor:
Colors
.
white
,
elevation:
0
,
leading:
IconButton
(
icon:
const
Icon
(
Icons
.
arrow_back
,
color:
Colors
.
black
),
onPressed:
()
{
context
.
popRoute
();
},
),
title:
Text
(
'全部设备'
,
style:
TextStyle
(
color:
Colors
.
black
,
fontSize:
18
.
sp
,
fontWeight:
FontWeight
.
bold
,
),
),
centerTitle:
true
,
child:
const
_ReportDeviceListBody
(),
);
}
}
class
_ReportDeviceListBody
extends
StatefulWidget
{
const
_ReportDeviceListBody
();
@override
State
<
_ReportDeviceListBody
>
createState
()
=>
_ReportDeviceListBodyState
();
}
class
_ReportDeviceListBodyState
extends
State
<
_ReportDeviceListBody
>
{
late
final
PagingController
<
int
,
DeviceInfo
>
_pagingController
;
late
final
DeviceListCubit
_cubit
;
@override
void
initState
()
{
super
.
initState
();
_cubit
=
context
.
read
<
DeviceListCubit
>();
_pagingController
=
PagingController
<
int
,
DeviceInfo
>(
getNextPageKey:
(
state
)
{
if
(
state
.
keys
==
null
||
state
.
keys
!.
isEmpty
)
return
1
;
final
total
=
_cubit
.
state
.
totalCount
;
final
nextKey
=
state
.
nextIntPageKey
;
if
(
nextKey
==
null
)
return
null
;
if
((
nextKey
-
1
)
*
DeviceListCubit
.
pageSize
>=
total
)
{
return
null
;
}
return
nextKey
;
},
fetchPage:
(
pageKey
)
=>
_cubit
.
fetchPage
(
pageKey
),
);
_cubit
.
bindPagingController
(
_pagingController
);
}
@override
void
dispose
()
{
_pagingController
.
dispose
();
_cubit
.
unbindPagingController
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
BlocBuilder
<
DeviceListCubit
,
DeviceListState
>(
builder:
(
context
,
state
)
{
return
Scaffold
(
backgroundColor:
const
Color
.
fromRGBO
(
242
,
243
,
245
,
1
),
appBar:
AppBar
(
backgroundColor:
Colors
.
white
,
elevation:
0
,
leading:
IconButton
(
icon:
const
Icon
(
Icons
.
arrow_back
,
color:
Colors
.
black
),
onPressed:
()
{
context
.
popRoute
();
},
),
body:
SingleChildScrollView
(
child:
Padding
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
28
.
w
),
child:
Column
(
children:
[
SizedBox
(
height:
10
.
h
),
DeviceFilterTab
(
selectedType:
state
.
selectedType
,
onSelect:
(
type
)
{
context
.
read
<
DeviceListCubit
>().
selectType
(
type
);
},
),
SizedBox
(
height:
10
.
h
),
...
state
.
filteredDevices
.
map
((
device
)
{
return
Padding
(
padding:
EdgeInsets
.
only
(
bottom:
10
.
h
),
child:
DeviceCard
(
device:
device
),
);
}).
toList
(),
SizedBox
(
height:
10
.
h
),
],
),
title:
Text
(
'全部设备'
,
style:
TextStyle
(
color:
Colors
.
black
,
fontSize:
32
.
sp
,
fontWeight:
FontWeight
.
bold
,
),
),
);
},
),
centerTitle:
true
,
),
body:
PagingListener
<
int
,
DeviceInfo
>(
controller:
_pagingController
,
builder:
(
context
,
pagingState
,
fetchNextPage
)
{
return
CustomScrollView
(
slivers:
[
SliverToBoxAdapter
(
child:
Padding
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
28
.
w
),
child:
Column
(
children:
[
SizedBox
(
height:
10
.
h
),
DeviceFilterTab
(
selectedType:
state
.
selectedType
,
onSelect:
(
type
)
{
context
.
read
<
DeviceListCubit
>()
.
selectType
(
type
);
},
),
SizedBox
(
height:
10
.
h
),
],
),
),
),
SliverPadding
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
28
.
w
),
sliver:
PagedSliverList
<
int
,
DeviceInfo
>(
state:
pagingState
,
fetchNextPage:
fetchNextPage
,
builderDelegate:
PagedChildBuilderDelegate
<
DeviceInfo
>(
firstPageErrorIndicatorBuilder:
(
_
)
=>
Center
(
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
SizedBox
(
height:
60
.
h
),
Text
(
'加载失败:
${pagingState.error}
'
,
style:
TextStyle
(
fontSize:
28
.
sp
,
color:
const
Color
.
fromRGBO
(
255
,
100
,
101
,
1
),
),
textAlign:
TextAlign
.
center
,
),
SizedBox
(
height:
24
.
h
),
ElevatedButton
(
onPressed:
fetchNextPage
,
child:
Text
(
'重新加载'
,
style:
TextStyle
(
fontSize:
28
.
sp
)),
),
],
),
),
noItemsFoundIndicatorBuilder:
(
_
)
=>
Padding
(
padding:
EdgeInsets
.
only
(
top:
60
.
h
),
child:
Text
(
'暂无设备'
,
style:
TextStyle
(
fontSize:
28
.
sp
,
color:
const
Color
.
fromRGBO
(
100
,
116
,
139
,
1
),
),
),
),
newPageErrorIndicatorBuilder:
(
_
)
=>
Center
(
child:
Padding
(
padding:
EdgeInsets
.
symmetric
(
vertical:
20
.
h
),
child:
Column
(
children:
[
Text
(
'加载失败:
${pagingState.error}
'
,
style:
TextStyle
(
fontSize:
24
.
sp
,
color:
const
Color
.
fromRGBO
(
255
,
100
,
101
,
1
),
),
),
SizedBox
(
height:
12
.
h
),
TextButton
(
onPressed:
fetchNextPage
,
child:
Text
(
'重试'
,
style:
TextStyle
(
fontSize:
24
.
sp
)),
),
],
),
),
),
itemBuilder:
(
_
,
item
,
__
)
{
return
Padding
(
padding:
EdgeInsets
.
only
(
bottom:
10
.
h
),
child:
DeviceCard
(
device:
item
),
);
},
),
),
),
SliverToBoxAdapter
(
child:
SizedBox
(
height:
20
.
h
),
),
],
);
},
),
);
},
);
}
}
lib/views/report/energy/cubit/energy_cubit.dart
View file @
1a461d59
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:smart_hotel_app/models/bo/dashboard_bo.dart'
;
import
'package:smart_hotel_app/repositories/dashboard_repository.dart'
;
import
'package:smart_hotel_app/services/dashboard_service.dart'
;
import
'package:smart_hotel_app/views/report/energy/cubit/energy_state.dart'
;
class
EnergyCubit
extends
Cubit
<
EnergyState
>
{
EnergyCubit
()
:
super
(
const
EnergyState
());
}
\ No newline at end of file
final
DashboardService
_service
;
EnergyCubit
({
DashboardService
?
service
})
:
_service
=
service
??
DashboardService
(
repository:
DashboardRepository
()),
super
(
const
EnergyState
())
{
loadData
();
}
Future
<
void
>
loadData
()
async
{
emit
(
state
.
copyWith
(
isLoading:
true
,
clearError:
true
));
try
{
final
data
=
await
_service
.
getEnergyAnalysis
();
emit
(
_mapAnalysisToState
(
data
));
}
catch
(
e
)
{
emit
(
state
.
copyWith
(
isLoading:
false
,
error:
e
.
toString
()));
}
}
EnergyState
_mapAnalysisToState
(
EnergyAnalysisBO
data
)
{
// 计算今日变化百分比
double
todayChange
=
0
;
if
(
data
.
yesterdayElectricity
>
0
)
{
todayChange
=
((
data
.
todayElectricity
-
data
.
yesterdayElectricity
)
/
data
.
yesterdayElectricity
)
*
100
;
}
// 计算本月变化百分比
double
monthChange
=
0
;
if
(
data
.
lastMonthElectricity
>
0
)
{
monthChange
=
((
data
.
monthElectricity
-
data
.
lastMonthElectricity
)
/
data
.
lastMonthElectricity
)
*
100
;
}
// 将逐小时数据点提取为 List<double>
final
hourlyData
=
data
.
hourlyUsageToday
.
hourlyPoints
.
map
((
p
)
=>
p
.
value
)
.
toList
();
// 将分区数据映射为 ZoneData 列表
final
totalToday
=
data
.
zoneUsageRatio
.
totalToday
;
final
zoneData
=
data
.
zoneUsageRatio
.
zones
.
map
((
z
)
{
final
percent
=
totalToday
>
0
?
(
z
.
todayKwh
/
totalToday
*
100
)
:
0.0
;
return
ZoneData
(
name:
z
.
zoneLabel
,
percentage:
percent
,
energy:
z
.
todayKwh
,
);
}).
toList
();
return
EnergyState
(
isLoading:
false
,
todayEnergy:
data
.
todayElectricity
,
todayEnergyChange:
todayChange
,
monthEnergy:
data
.
monthElectricity
,
monthEnergyChange:
monthChange
,
monthBill:
data
.
monthCostEstimate
,
hourlyData:
hourlyData
,
zoneData:
zoneData
,
);
}
}
lib/views/report/energy/cubit/energy_state.dart
View file @
1a461d59
import
'package:equatable/equatable.dart'
;
class
ZoneData
{
class
ZoneData
extends
Equatable
{
final
String
name
;
final
double
percentage
;
final
double
energy
;
...
...
@@ -10,9 +10,14 @@ class ZoneData {
required
this
.
percentage
,
required
this
.
energy
,
});
@override
List
<
Object
?>
get
props
=>
[
name
,
percentage
,
energy
];
}
class
EnergyState
extends
Equatable
{
final
bool
isLoading
;
final
String
?
error
;
final
double
todayEnergy
;
final
double
todayEnergyChange
;
final
double
monthEnergy
;
...
...
@@ -22,6 +27,8 @@ class EnergyState extends Equatable {
final
List
<
ZoneData
>
zoneData
;
const
EnergyState
({
this
.
isLoading
=
false
,
this
.
error
,
this
.
todayEnergy
=
1284
,
this
.
todayEnergyChange
=
-
8
,
this
.
monthEnergy
=
38420
,
...
...
@@ -40,6 +47,8 @@ class EnergyState extends Equatable {
});
EnergyState
copyWith
({
bool
?
isLoading
,
String
?
error
,
double
?
todayEnergy
,
double
?
todayEnergyChange
,
double
?
monthEnergy
,
...
...
@@ -47,8 +56,11 @@ class EnergyState extends Equatable {
double
?
monthBill
,
List
<
double
>?
hourlyData
,
List
<
ZoneData
>?
zoneData
,
bool
clearError
=
false
,
})
{
return
EnergyState
(
isLoading:
isLoading
??
this
.
isLoading
,
error:
clearError
?
null
:
(
error
??
this
.
error
),
todayEnergy:
todayEnergy
??
this
.
todayEnergy
,
todayEnergyChange:
todayEnergyChange
??
this
.
todayEnergyChange
,
monthEnergy:
monthEnergy
??
this
.
monthEnergy
,
...
...
@@ -61,6 +73,8 @@ class EnergyState extends Equatable {
@override
List
<
Object
?>
get
props
=>
[
isLoading
,
error
,
todayEnergy
,
todayEnergyChange
,
monthEnergy
,
...
...
@@ -69,4 +83,4 @@ class EnergyState extends Equatable {
hourlyData
,
zoneData
,
];
}
\ No newline at end of file
}
lib/views/report/energy/energy_detail_view.dart
View file @
1a461d59
...
...
@@ -37,10 +37,35 @@ class ReportEnergyDetailView extends StatelessWidget {
),
centerTitle:
true
,
),
body:
SingleChildScrollView
(
child:
BlocBuilder
<
EnergyCubit
,
EnergyState
>(
builder:
(
context
,
state
)
{
return
Column
(
body:
BlocBuilder
<
EnergyCubit
,
EnergyState
>(
builder:
(
context
,
state
)
{
if
(
state
.
isLoading
)
{
return
const
Center
(
child:
CircularProgressIndicator
());
}
if
(
state
.
error
!=
null
)
{
return
Center
(
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Text
(
'加载失败:
${state.error}
'
,
style:
TextStyle
(
fontSize:
28
.
sp
,
color:
const
Color
.
fromRGBO
(
100
,
116
,
139
,
1
),
),
),
SizedBox
(
height:
16
.
h
),
ElevatedButton
(
onPressed:
()
=>
context
.
read
<
EnergyCubit
>().
loadData
(),
child:
const
Text
(
'重试'
),
),
],
),
);
}
return
SingleChildScrollView
(
child:
Column
(
children:
[
SizedBox
(
height:
16
.
h
),
EnergyStatsCardBlock
(
...
...
@@ -56,11 +81,11 @@ class ReportEnergyDetailView extends StatelessWidget {
ZoneChartBlock
(
zoneData:
state
.
zoneData
),
SizedBox
(
height:
20
.
h
),
],
)
;
},
)
,
)
,
);
}
,
),
),
);
}
}
\ No newline at end of file
}
lib/views/report/index/cubit/report_index_cubit.dart
View file @
1a461d59
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:smart_hotel_app/models/bo/dashboard_bo.dart'
;
import
'package:smart_hotel_app/repositories/dashboard_repository.dart'
;
import
'package:smart_hotel_app/services/dashboard_service.dart'
;
import
'package:smart_hotel_app/views/report/index/cubit/report_index_state.dart'
;
class
ReportIndexCubit
extends
Cubit
<
ReportIndexState
>
{
ReportIndexCubit
()
:
super
(
const
ReportIndexState
())
{
initData
();
final
DashboardService
_service
;
ReportIndexCubit
({
DashboardService
?
service
})
:
_service
=
service
??
DashboardService
(
repository:
DashboardRepository
()),
super
(
const
ReportIndexState
())
{
loadData
();
}
Future
<
void
>
loadData
()
async
{
emit
(
state
.
copyWith
(
isLoading:
true
,
clearError:
true
));
try
{
final
overview
=
await
_service
.
getOverview
();
emit
(
_mapOverviewToState
(
overview
));
}
catch
(
e
)
{
emit
(
state
.
copyWith
(
isLoading:
false
,
error:
e
.
toString
()));
}
}
void
initData
()
{
emit
(
state
.
copyWith
(
ReportIndexState
_mapOverviewToState
(
DashboardOverviewBO
overview
)
{
// 今日用电指标
final
elec
=
overview
.
electricityToday
;
final
yesterdayVal
=
elec
.
yesterdayValue
;
final
todayVal
=
elec
.
value
;
double
changePercent
=
0
;
if
(
yesterdayVal
>
0
)
{
changePercent
=
((
todayVal
-
yesterdayVal
)
/
yesterdayVal
*
100
);
}
final
isDown
=
changePercent
<=
0
;
final
changeAbs
=
changePercent
.
abs
().
round
();
return
state
.
copyWith
(
isLoading:
false
,
reportHeader:
{
'title'
:
'经营概览看板'
,
'subtitle'
:
'管理层视角 · 关键指标一览'
,
},
reportMetrics:
{
'powerUsage'
:
{
'value'
:
1284
,
'value'
:
todayVal
.
toInt
()
,
'unit'
:
'kWh'
,
'change'
:
-
8
,
'changeText'
:
'↓ 8
%'
,
'progress'
:
0.6
,
'change'
:
isDown
?
-
changeAbs
:
changeAbs
,
'changeText'
:
isDown
?
'↓
$changeAbs
%'
:
'↑
$changeAbs
%'
,
'progress'
:
(
todayVal
/
2000
).
clamp
(
0.0
,
1.0
)
,
},
'pendingAlerts'
:
{
'value'
:
3
,
'value'
:
overview
.
alertPending
.
count
,
'unit'
:
'条'
,
'change'
:
null
,
'changeText'
:
'需关注
'
,
'progress'
:
0.3
,
'changeText'
:
overview
.
alertPending
.
count
>
0
?
'需关注'
:
'无告警
'
,
'progress'
:
(
overview
.
alertPending
.
count
/
10
).
clamp
(
0.0
,
1.0
)
,
},
},
weeklyPowerUsage:
{
'title'
:
'本周用电趋势'
,
'unit'
:
'kWh'
,
'dateRange'
:
'5.7 - 5.13'
,
'data'
:
[
{
'day'
:
'一'
,
'value'
:
120
},
{
'day'
:
'二'
,
'value'
:
180
},
{
'day'
:
'三'
,
'value'
:
150
},
{
'day'
:
'四'
,
'value'
:
200
},
{
'day'
:
'五'
,
'value'
:
250
},
{
'day'
:
'六'
,
'value'
:
220
},
{
'day'
:
'日'
,
'value'
:
164
},
],
'dateRange'
:
overview
.
electricityTrend
.
dateRange
,
'data'
:
_mapWeekDays
(
overview
.
electricityTrend
.
points
),
},
roomStatusDistribution:
{
'title'
:
'客房状态分布'
,
'totalRooms'
:
52
,
'statuses'
:
[
{
'name'
:
'入住中'
,
'count'
:
45
,
'color'
:
const
Color
.
fromRGBO
(
59
,
130
,
246
,
1
)},
{
'name'
:
'空闲'
,
'count'
:
7
,
'color'
:
const
Color
.
fromRGBO
(
148
,
163
,
184
,
1
)},
{
'name'
:
'已预订'
,
'count'
:
5
,
'color'
:
const
Color
.
fromRGBO
(
96
,
165
,
250
,
1
)},
{
'name'
:
'设备告警'
,
'count'
:
3
,
'color'
:
const
Color
.
fromRGBO
(
249
,
115
,
22
,
1
)},
],
'totalRooms'
:
overview
.
roomStatusDistribution
.
total
,
'statuses'
:
_mapRoomStatuses
(
overview
.
roomStatusDistribution
.
items
),
},
deviceOnlineRate:
{
'title'
:
'设备在线率'
,
'rate'
:
1.0
,
'rateText'
:
'100%'
,
'devices'
:
[
{
'name'
:
'智能空开'
,
'count'
:
16
},
{
'name'
:
'客控设备'
,
'count'
:
24
},
{
'name'
:
'网络设备'
,
'count'
:
8
},
],
'rateText'
:
'
${overview.deviceTypeDistribution.totalDeviceCount}
台'
,
'devices'
:
_mapDeviceTypes
(
overview
.
deviceTypeDistribution
.
items
),
},
quickActions:
{
'title'
:
'快捷操作'
,
...
...
@@ -70,6 +82,48 @@ class ReportIndexCubit extends Cubit<ReportIndexState> {
{
'name'
:
'联动规则'
,
'icon'
:
Icons
.
tune
},
],
},
));
);
}
/// 将趋势点映射为周几标签
List
<
Map
<
String
,
dynamic
>>
_mapWeekDays
(
List
<
DashboardTrendPointBO
>
points
)
{
const
dayLabels
=
[
'一'
,
'二'
,
'三'
,
'四'
,
'五'
,
'六'
,
'日'
];
return
points
.
asMap
().
entries
.
map
((
entry
)
{
final
index
=
entry
.
key
;
final
point
=
entry
.
value
;
return
{
'day'
:
index
<
dayLabels
.
length
?
dayLabels
[
index
]
:
'
${index + 1}
'
,
'value'
:
point
.
value
.
toInt
(),
};
}).
toList
();
}
/// 映射客房状态并分配颜色
List
<
Map
<
String
,
dynamic
>>
_mapRoomStatuses
(
List
<
RoomStatusItemBO
>
items
)
{
const
statusColors
=
[
Color
.
fromRGBO
(
59
,
130
,
246
,
1
),
Color
.
fromRGBO
(
148
,
163
,
184
,
1
),
Color
.
fromRGBO
(
96
,
165
,
250
,
1
),
Color
.
fromRGBO
(
249
,
115
,
22
,
1
),
];
return
items
.
asMap
().
entries
.
map
((
entry
)
{
final
index
=
entry
.
key
;
final
item
=
entry
.
value
;
return
{
'name'
:
item
.
statusLabel
,
'count'
:
item
.
count
,
'color'
:
statusColors
[
index
%
statusColors
.
length
],
};
}).
toList
();
}
/// 映射设备类型分布
List
<
Map
<
String
,
dynamic
>>
_mapDeviceTypes
(
List
<
DeviceTypeItemBO
>
items
)
{
return
items
.
map
((
item
)
{
return
{
'name'
:
item
.
typeLabel
,
'count'
:
item
.
count
,
};
}).
toList
();
}
}
\ No newline at end of file
}
lib/views/report/index/cubit/report_index_state.dart
View file @
1a461d59
import
'package:equatable/equatable.dart'
;
class
ReportIndexState
extends
Equatable
{
final
bool
isLoading
;
final
String
?
error
;
final
Map
<
String
,
dynamic
>
reportHeader
;
final
Map
<
String
,
dynamic
>
reportMetrics
;
final
Map
<
String
,
dynamic
>
weeklyPowerUsage
;
...
...
@@ -9,6 +11,8 @@ class ReportIndexState extends Equatable {
final
Map
<
String
,
dynamic
>
quickActions
;
const
ReportIndexState
({
this
.
isLoading
=
false
,
this
.
error
,
this
.
reportHeader
=
const
{},
this
.
reportMetrics
=
const
{},
this
.
weeklyPowerUsage
=
const
{},
...
...
@@ -18,14 +22,19 @@ class ReportIndexState extends Equatable {
});
ReportIndexState
copyWith
({
bool
?
isLoading
,
String
?
error
,
Map
<
String
,
dynamic
>?
reportHeader
,
Map
<
String
,
dynamic
>?
reportMetrics
,
Map
<
String
,
dynamic
>?
weeklyPowerUsage
,
Map
<
String
,
dynamic
>?
roomStatusDistribution
,
Map
<
String
,
dynamic
>?
deviceOnlineRate
,
Map
<
String
,
dynamic
>?
quickActions
,
bool
clearError
=
false
,
})
{
return
ReportIndexState
(
isLoading:
isLoading
??
this
.
isLoading
,
error:
clearError
?
null
:
(
error
??
this
.
error
),
reportHeader:
reportHeader
??
this
.
reportHeader
,
reportMetrics:
reportMetrics
??
this
.
reportMetrics
,
weeklyPowerUsage:
weeklyPowerUsage
??
this
.
weeklyPowerUsage
,
...
...
@@ -37,6 +46,8 @@ class ReportIndexState extends Equatable {
@override
List
<
Object
?>
get
props
=>
[
isLoading
,
error
,
reportHeader
,
reportMetrics
,
weeklyPowerUsage
,
...
...
@@ -44,4 +55,4 @@ class ReportIndexState extends Equatable {
deviceOnlineRate
,
quickActions
,
];
}
\ No newline at end of file
}
lib/views/report/index/index_view.dart
View file @
1a461d59
...
...
@@ -21,6 +21,37 @@ class ReportIndexView extends StatelessWidget {
create:
(
_
)
=>
ReportIndexCubit
(),
child:
BlocBuilder
<
ReportIndexCubit
,
ReportIndexState
>(
builder:
(
context
,
state
)
{
if
(
state
.
isLoading
)
{
return
Container
(
color:
const
Color
.
fromRGBO
(
242
,
243
,
245
,
1
),
child:
const
Center
(
child:
CircularProgressIndicator
()),
);
}
if
(
state
.
error
!=
null
)
{
return
Container
(
color:
const
Color
.
fromRGBO
(
242
,
243
,
245
,
1
),
child:
Center
(
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Text
(
'加载失败:
${state.error}
'
,
style:
TextStyle
(
fontSize:
28
.
sp
,
color:
const
Color
.
fromRGBO
(
100
,
116
,
139
,
1
),
),
),
SizedBox
(
height:
16
.
h
),
ElevatedButton
(
onPressed:
()
=>
context
.
read
<
ReportIndexCubit
>().
loadData
(),
child:
const
Text
(
'重试'
),
),
],
),
),
);
}
return
LayoutBuilder
(
builder:
(
context
,
constraints
)
{
return
SingleChildScrollView
(
...
...
@@ -31,7 +62,10 @@ class ReportIndexView extends StatelessWidget {
child:
Container
(
width:
double
.
infinity
,
color:
const
Color
.
fromRGBO
(
242
,
243
,
245
,
1
),
padding:
EdgeInsets
.
only
(
left:
28
.
w
,
right:
28
.
w
,
top:
ScreenUtil
().
statusBarHeight
),
padding:
EdgeInsets
.
only
(
left:
28
.
w
,
right:
28
.
w
,
top:
ScreenUtil
().
statusBarHeight
),
child:
Column
(
children:
[
ReportHeaderBlock
(
...
...
@@ -50,7 +84,8 @@ class ReportIndexView extends StatelessWidget {
DeviceOnlineRate
(
deviceOnlineRate:
state
.
deviceOnlineRate
),
SizedBox
(
height:
10
.
h
),
QuickActionsBlock
(
quickActions:
state
.
quickActions
),
QuickActionsBlock
(
quickActions:
state
.
quickActions
),
SizedBox
(
height:
10
.
h
),
],
),
...
...
@@ -63,4 +98,4 @@ class ReportIndexView extends StatelessWidget {
),
);
}
}
\ No newline at end of file
}
lib/views/report/rule/cubit/rule_management_cubit.dart
View file @
1a461d59
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:smart_hotel_app/models/bo/linkage_rule_bo.dart'
;
import
'package:smart_hotel_app/repositories/linkage_rule_repository.dart'
;
import
'package:smart_hotel_app/services/linkage_rule_service.dart'
;
import
'package:smart_hotel_app/views/report/rule/cubit/rule_management_state.dart'
;
class
RuleManagementCubit
extends
Cubit
<
RuleManagementState
>
{
RuleManagementCubit
()
:
super
(
const
RuleManagementState
())
{
_initData
();
final
LinkageRuleService
_service
;
RuleManagementCubit
({
LinkageRuleService
?
service
})
:
_service
=
service
??
LinkageRuleService
(
repository:
LinkageRuleRepository
()),
super
(
const
RuleManagementState
())
{
loadRules
();
}
Future
<
void
>
loadRules
()
async
{
emit
(
state
.
copyWith
(
isLoading:
true
,
clearError:
true
));
try
{
final
rules
=
await
_service
.
getRules
();
emit
(
state
.
copyWith
(
isLoading:
false
,
rules:
rules
.
map
(
_toRuleInfo
).
toList
()));
}
catch
(
e
)
{
emit
(
state
.
copyWith
(
isLoading:
false
,
error:
e
.
toString
()));
}
}
void
_initData
()
{
final
rules
=
[
const
RuleInfo
(
id:
'1'
,
name:
'退房自动断电'
,
description:
'酒管系统—酒度状态'
,
enabled:
true
,
triggerCondition:
'酒管系统—酒度状态'
,
action:
'断电'
,
icon:
Icons
.
logout_outlined
,
),
const
RuleInfo
(
id:
'2'
,
name:
'清洁中请自动开灯'
,
description:
'服务彩忧消洁中调'
,
enabled:
true
,
triggerCondition:
'服务彩忧消洁中调'
,
action:
'开灯'
,
icon:
Icons
.
cleaning_services_outlined
,
),
const
RuleInfo
(
id:
'3'
,
name:
'温度过高自动保护'
,
description:
'温度·80°C—断电+告管'
,
enabled:
true
,
triggerCondition:
'温度·80°C'
,
action:
'断电+告管'
,
icon:
Icons
.
thermostat_outlined
,
),
const
RuleInfo
(
id:
'4'
,
name:
'深夜低功事休眠'
,
description:
'23:00-06:00 平<50W'
,
enabled:
false
,
triggerCondition:
'23:00-06:00 平<50W'
,
action:
'休眠'
,
icon:
Icons
.
nightlight_outlined
,
),
const
RuleInfo
(
id:
'5'
,
name:
'定时全楼巡检提醒'
,
description:
'国日 09:00/15.00'
,
enabled:
true
,
triggerCondition:
'国日 09:00/15.00'
,
action:
'提醒'
,
icon:
Icons
.
access_time_outlined
,
),
];
/// 根据规则名称映射图标
IconData
_mapIcon
(
String
ruleName
)
{
final
name
=
ruleName
.
toLowerCase
();
if
(
name
.
contains
(
'退房'
)
||
name
.
contains
(
'logout'
))
{
return
Icons
.
logout_outlined
;
}
if
(
name
.
contains
(
'清洁'
)
||
name
.
contains
(
'灯'
)
||
name
.
contains
(
'clean'
))
{
return
Icons
.
cleaning_services_outlined
;
}
if
(
name
.
contains
(
'温度'
)
||
name
.
contains
(
'thermostat'
))
{
return
Icons
.
thermostat_outlined
;
}
if
(
name
.
contains
(
'深夜'
)
||
name
.
contains
(
'night'
))
{
return
Icons
.
nightlight_outlined
;
}
if
(
name
.
contains
(
'定时'
)
||
name
.
contains
(
'巡检'
)
||
name
.
contains
(
'time'
))
{
return
Icons
.
access_time_outlined
;
}
return
Icons
.
tune
;
}
emit
(
state
.
copyWith
(
rules:
rules
));
RuleInfo
_toRuleInfo
(
LinkageRuleBO
bo
)
{
return
RuleInfo
(
ruleId:
bo
.
ruleId
,
name:
bo
.
ruleName
,
description:
bo
.
description
,
enabled:
bo
.
isEnabled
,
icon:
_mapIcon
(
bo
.
ruleName
),
);
}
void
toggleRule
(
String
id
)
{
final
updatedRules
=
state
.
rules
.
map
((
rule
)
{
if
(
rule
.
id
==
id
)
{
return
rule
.
copyWith
(
enabled:
!
rule
.
enabled
);
}
return
rule
;
}).
toList
();
/// 启用/禁用联动规则(View 层传入 String id)
Future
<
void
>
toggleRule
(
String
id
)
async
{
final
ruleId
=
int
.
tryParse
(
id
)
??
0
;
if
(
ruleId
==
0
)
return
;
if
(
state
.
isToggling
)
return
;
// 防重复提交
final
previousRules
=
List
<
RuleInfo
>.
from
(
state
.
rules
);
final
index
=
state
.
rules
.
indexWhere
((
r
)
=>
r
.
ruleId
==
ruleId
);
if
(
index
==
-
1
)
return
;
final
currentEnabled
=
state
.
rules
[
index
].
enabled
;
final
newEnabled
=
!
currentEnabled
;
// 乐观更新 UI
final
updatedRules
=
List
<
RuleInfo
>.
from
(
state
.
rules
);
updatedRules
[
index
]
=
updatedRules
[
index
].
copyWith
(
enabled:
newEnabled
);
emit
(
state
.
copyWith
(
rules:
updatedRules
,
isToggling:
true
,
togglingRuleId:
ruleId
,
));
emit
(
state
.
copyWith
(
rules:
updatedRules
));
try
{
await
_service
.
toggleRule
(
ruleId:
ruleId
,
enabled:
newEnabled
?
'1'
:
'0'
,
);
emit
(
state
.
copyWith
(
isToggling:
false
));
}
catch
(
e
)
{
// 失败回滚
emit
(
state
.
copyWith
(
rules:
previousRules
,
isToggling:
false
,
error:
e
.
toString
(),
));
}
}
}
lib/views/report/rule/cubit/rule_management_state.dart
View file @
1a461d59
import
'package:flutter/material.dart'
;
import
'package:equatable/equatable.dart'
;
class
RuleInfo
{
final
String
i
d
;
class
RuleInfo
extends
Equatable
{
final
int
ruleI
d
;
final
String
name
;
final
String
description
;
final
bool
enabled
;
final
String
triggerCondition
;
final
String
action
;
final
IconData
icon
;
const
RuleInfo
({
required
this
.
id
,
required
this
.
name
,
required
this
.
description
,
required
this
.
enabled
,
required
this
.
triggerCondition
,
required
this
.
action
,
required
this
.
icon
,
this
.
ruleId
=
0
,
this
.
name
=
''
,
this
.
description
=
''
,
this
.
enabled
=
false
,
this
.
icon
=
Icons
.
tune
,
});
RuleInfo
copyWith
({
String
?
i
d
,
int
?
ruleI
d
,
String
?
name
,
String
?
description
,
bool
?
enabled
,
String
?
triggerCondition
,
String
?
action
,
IconData
?
icon
,
})
{
return
RuleInfo
(
id:
id
??
this
.
i
d
,
ruleId:
ruleId
??
this
.
ruleI
d
,
name:
name
??
this
.
name
,
description:
description
??
this
.
description
,
enabled:
enabled
??
this
.
enabled
,
triggerCondition:
triggerCondition
??
this
.
triggerCondition
,
action:
action
??
this
.
action
,
icon:
icon
??
this
.
icon
,
);
}
/// 兼容 View 层 rule.id 引用,返回 ruleId 的字符串形式
String
get
id
=>
ruleId
.
toString
();
@override
List
<
Object
?>
get
props
=>
[
ruleId
,
name
,
description
,
enabled
,
icon
];
}
class
RuleManagementState
extends
Equatable
{
final
bool
isLoading
;
final
String
?
error
;
final
bool
isToggling
;
// 某个开关操作进行中
final
int
togglingRuleId
;
// 正在操作的规则ID
final
List
<
RuleInfo
>
rules
;
const
RuleManagementState
({
this
.
isLoading
=
false
,
this
.
error
,
this
.
isToggling
=
false
,
this
.
togglingRuleId
=
0
,
this
.
rules
=
const
[],
});
RuleManagementState
copyWith
({
bool
?
isLoading
,
String
?
error
,
bool
?
isToggling
,
int
?
togglingRuleId
,
List
<
RuleInfo
>?
rules
,
bool
clearError
=
false
,
})
{
return
RuleManagementState
(
isLoading:
isLoading
??
this
.
isLoading
,
error:
clearError
?
null
:
(
error
??
this
.
error
),
isToggling:
isToggling
??
this
.
isToggling
,
togglingRuleId:
togglingRuleId
??
this
.
togglingRuleId
,
rules:
rules
??
this
.
rules
,
);
}
@override
List
<
Object
?>
get
props
=>
[
rules
];
List
<
Object
?>
get
props
=>
[
isLoading
,
error
,
isToggling
,
togglingRuleId
,
rules
,
];
}
对接方案.md
View file @
1a461d59
# 智慧酒
店 App 接口对接方案
# 智慧酒
店 App 接口对接方案
...
...
@@ -222,10 +222,18 @@ class xxxBO extends Equatable {
|
客房详情-房间总电源-switch开 |POST | /app/room/power/on | 已对接 | 客房详情-房间总电源-switch开 | |
|
客房详情-房间总电源-switch关 |POST | /app/room/power/off | 已对接 | 客房详情-房间总电源-switch关 | |
|
空调控制 |GET | /app/device/ac/status |
未
对接 | 空调控制 | |
|
空调控制-相关操作 |POST | /app/device/ac/control |
未
对接 | 空调控制-相关操作 | |
|
空调控制 |GET | /app/device/ac/status |
已
对接 | 空调控制 | |
|
空调控制-相关操作 |POST | /app/device/ac/control |
已
对接 | 空调控制-相关操作 | |
|
经营概览看板 |GET | /app/dashboard/overview | 已对接 | 经营概览看板 | |
|
能耗分析 |GET | /app/dashboard/energy-analysis | 已对接 | 能耗分析 | |
|
全部设备 |GET | /app/device/list | 已对接 | 全部设备 | |
|
联动规则管理 |GET | /app/linkage/rules | 未对接 | 联动规则管理 | |
|
联动规则管理-启用/禁用 |PUT | /app/linkage/rules/toggle | 未对接 | 联动规则管理-启用/禁用 | |
...
...
@@ -233,6 +241,210 @@ class xxxBO extends Equatable {
#
## 3.2 接口详细定义
#
### 联动规则管理-启用/禁用
*
*PUT /app/linkage/rules/toggle**
`
``
请
求体:
{
"ruleId": 0,
"enabled": "string"
}
响
应 data:
{
"code": 0,
"msg": "string",
"data": {
"ruleId": 0,
"enabled": "string"
}
}
`
``
#
### 联动规则管理
*
*GET /app/linkage/rules**
`
``
请
求体:
{
}
响
应 data:
{
"code": 0,
"msg": "string",
"data": [
{
"ruleId": 0,
"ruleName": "string",
"description": "string",
"enabled": "string" // 启用/禁用
}
]
}
`
``
#
### 全部设备
*
*GET /app/device/list**
`
``
请
求体:
{
"pageSize": 20,
"pageNum": 1,
"deviceTypeId":1 // 设备类型id 可选
}
响
应 data:
{
"code": 0,
"msg": "string",
"data": {
"total": 0, // 总记录数,满足条件的设备总数,用于前端分页计算
"tabCount": {
"all": 0, // 全部设备数量
"airSwitch": 0, // 空开类设备数
"guestControl": 0, // 客控类设备数
"network": 0 // 网络类设备数
},
"rows": [ // 设备行记录列表
{
"deviceId": 0,
"deviceName": "string",
"deviceCode": "string",
"deviceLocation": "string",
"deviceTypeIcon": "string",
"deviceTypeName": "string",
"subtitle": {
"text": "string",
"voltage": "string",
"powerDisplay": "string",
"temperature": "string",
"statusText": "string",
"wifiSignal": "string"
},
"statusTag": { // 右侧状态标签,颜色+文字组合,来源于 hotel_device.run_status 字段判断:normal→绿色'正常'/绿色'在线' warning→黄色'注意' fault→红色'故障' offline→灰色'离线',对应卡片右侧的彩色标签(告警黄/正常绿/在线绿/注意黄/故障红)
"text": "string", // 状态标签文字:'正常'/'在线'/'告警'/'注意'/'故障'/'离线',根据 run_status + online_status 综合判断
"color": "string" // 状态标签颜色类型:success=绿色(green) warning=黄色(orange/yellow) danger=红色(red) info=灰色(gray),用于前端渲染标签背景色
},
"onlineStatus": "string", // 在线状态码:0=离线 1=在线
"runStatus": "string", // 运行状态码:normal=正常 warning=预警/注意 fault=异常/故障 offline=离线
"roomId": 0,
"roomNumber": "string"
}
]
}
}
`
``
#
### 能耗分析
*
*GET /app/dashboard/energy-analysis**
`
``
请
求体:
{
}
响
应 data:
{
"code": 0,
"msg": "string",
"data": {
"todayElectricity": 0, // 今日累计用电量(kWh)
"yesterdayElectricity": 0,
"monthElectricity": 0,
"lastMonthElectricity": 0,
"monthCostEstimate": 0,
"hourlyUsageToday": { // 今日逐时用电柱状图
"maxValue": 0, // 【计算字段 | 来源: 24个 hourlyPoints 的 value 值 | 取值方式: MAX(hourlyPoints[].value),即24个小时中最大的那个 power_consumption 累加值 | 说明: 当天24小时中单小时最大用电量值(kWh),用于设定 Y 轴上限(向上取整到十位整数如 80/90/100),避免柱子贴顶】
"hourlyPoints": [ // 逐小时数据点列表
{
"time": "string",
"value": 0
}
]
},
"zoneUsageRatio": { // 分区用电占比(横向条形图)
"totalToday": 0,
"totalYesterday": 0,
"zones": [ // 各区域用电明细列表
{
"zoneLabel": "string",
"todayKwh": 0,
"yesterdayKwh": 0
}
]
}
}
}
`
``
#
### 经营概览看板
*
*GET /app/dashboard/overview**
`
``
请
求体:
{
}
响
应 data:
{
"code": 0,
"msg": "string",
"data": {
"electricityToday": { // 今日用电统计数据
"value": 0,
"yesterdayValue": 0
},
"alertPending": { // 待处理告警卡片数据
"count": 0
},
"electricityTrend": { // 本周用电趋势折线图
"dateRange": "string",
"maxValue": 0,
"points": [
{
"time": "string",
"value": 0
}
]
},
"roomStatusDistribution": { // 客房状态分布(横向条形图)
"total": 0,
"items": [
{
"statusLabel": "string",
"count": 0,
"percent": 0
}
]
},
"deviceTypeDistribution": { // 设备类型分布统计(圆环图)
"totalDeviceCount": 0,
"items": [
{
"typeLabel": "string",
"count": 0
}
]
}
}
}
`
``
#
### 空调控制-相关操作
*
*POST /app/device/ac/control**
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment