新版UI-登录页面-我的页面(简单适配)-设备的部分(无设备的+有设备)

This commit is contained in:
2026-10-24 09:08:33 +08:00
parent 81dae187ac
commit e3af8cf617
19 changed files with 4848 additions and 51 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

BIN
assets/images/logo_sata.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

BIN
assets/images/shebei_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 KiB

View File

@@ -0,0 +1,739 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/core/localization/locale_cubit.dart';
import 'package:maibu_satabot_v2/components/language_selection_dialog.dart';
import 'package:maibu_satabot_v2/components/capsule_toast.dart';
import '../../../../core/router/route_paths.dart';
import '../bloc/login_cubit.dart';
import '../bloc/login_state.dart';
/// 登录页 V2 —— 按最新设计稿实现(账号 + 密码登录)。
///
/// 说明:
/// - 视觉:背景纵向浅蓝渐变、主按钮横向蓝色渐变胶囊、顶部 logo + 语言胶囊、
/// 中部机器人 Hero 卡片、白色表单卡片内含「登录 / 注册」分段 Tab。
/// - 逻辑:完全复用现有 [LoginCubit](账号密码登录,sourceType=2),未改动数据层。
/// - 「注册」暂不支持:点击注册 Tab 仅弹出胶囊提示,不跳转。
/// - 旧登录页 login_page.dart 原样保留,路由默认指向本页。
class LoginPageV2 extends StatefulWidget {
const LoginPageV2({super.key});
@override
State<LoginPageV2> createState() => _LoginPageV2State();
}
class _LoginPageV2State extends State<LoginPageV2>
with SingleTickerProviderStateMixin {
final _userCtrl = TextEditingController();
final _pwdCtrl = TextEditingController();
bool _isAgreed = false; // 协议勾选状态
bool _obscurePwd = true; // 密码可见性
int _selectedTab = 0; // 0=登录, 1=注册(注册仅提示,不切换内容)
// 进场动画控制器
late final AnimationController _entranceController;
// ===== 设计稿配色 =====
static const _bgTop = Color(0xFFFAFCFF);
static const _bgMid = Color(0xFFF0F6FE);
// 底部蓝比设计稿基准色 #EDF6FE 加深:原稿中/底两色几乎无差,渐变看不出层次
static const _bgBottom = Color(0xFFD6E9FB);
static const _navy = Color(0xFF1A2B4A); // 主标题/标签深色
static const _grayText = Color(0xFF86909C); // 次要文字
static const _primaryBlue = Color(0xFF1C96F6);
static const _inputBorder = Color(0xFFE5EDF7);
static const _tabBg = Color(0xFFEEF3FA);
static const _tabUnselectedText = Color(0xFF4E5969);
@override
void initState() {
super.initState();
_entranceController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_maybeShowLanguageDialog();
Future.delayed(const Duration(milliseconds: 120), () {
if (mounted) _entranceController.forward();
});
});
}
/// 进入登录页:弹出语言选择框,由用户当场决定(与旧页保持一致的强制选择体验)。
void _maybeShowLanguageDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const LanguageSelectionDialog(mandatory: true),
);
}
@override
void dispose() {
_entranceController.dispose();
_userCtrl.dispose();
_pwdCtrl.dispose();
super.dispose();
}
Animation<double> _interval(double begin, double end) {
return CurvedAnimation(
parent: _entranceController,
curve: Interval(begin, end, curve: Curves.easeOut),
);
}
// 执行登录逻辑(复用现有 LoginCubit:账号 + 密码,sourceType=2)
void _handleLogin() {
context.read<LoginCubit>().login(_userCtrl.text, _pwdCtrl.text, 2);
}
void _onTabTap(int index) {
if (index == 1) {
// 注册暂不支持:仅提示,不切换内容
CapsuleToast.show('注册功能暂未开放', showCheck: false);
return;
}
setState(() => _selectedTab = 0);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _bgTop,
body: DecoratedBox(
// 底色:纵向浅蓝渐变
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [_bgTop, _bgMid, _bgBottom],
stops: [0.0, 0.4, 1.0],
),
),
child: DecoratedBox(
// 左上角蓝色晕(对应设计稿 logo 一侧的淡蓝)
decoration: const BoxDecoration(
gradient: RadialGradient(
center: Alignment(-1.1, -1.0),
radius: 1.1,
colors: [Color(0xFFD6E8FC), Color(0x00D6E8FC)],
),
),
child: DecoratedBox(
// 右上角蓝色晕(对应设计稿顶部右侧的淡蓝)
decoration: const BoxDecoration(
gradient: RadialGradient(
center: Alignment(1.1, -0.9),
radius: 1.25,
colors: [Color(0xFFC7DFF8), Color(0x00C7DFF8)],
),
),
child: SafeArea(
bottom: false,
child: BlocListener<LoginCubit, LoginState>(
listener: (context, state) {
if (state is LoginSuccess) {
context.go(RoutePaths.home);
} else if (state is LoginFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${AppLocalizations.of(context).translate('login.login_failed')},${state.message}',
),
),
);
}
},
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ConstrainedBox(
constraints:
BoxConstraints(minHeight: constraints.maxHeight),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
// 顶部:logo + 语言胶囊
_Entrance(
animation: _interval(0.00, 0.35),
child: _buildTopBar(),
),
const SizedBox(height: 12),
// 欢迎标题 + 副标题
_Entrance(
animation: _interval(0.08, 0.43),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'欢迎使用飒沓机器人',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: _navy,
height: 1.2,
),
),
const SizedBox(height: 4),
const Text(
'统一管理您的机器人设备',
style: TextStyle(
fontSize: 13,
color: _grayText,
),
),
],
),
),
const SizedBox(height: 12),
// 机器人 Hero 卡片
_Entrance(
animation: _interval(0.16, 0.51),
child: _buildHeroCard(),
),
const SizedBox(height: 12),
// 白色表单卡片
_Entrance(
animation: _interval(0.26, 0.63),
child: _buildFormCard(),
),
const SizedBox(height: 12),
// 帮助与客服
_Entrance(
animation: _interval(0.40, 0.75),
child: Center(child: _buildHelpRow()),
),
const SizedBox(height: 16),
],
),
),
);
},
),
),
),
),
),
),
);
}
// ===== 顶部栏:logo(左)+ 语言胶囊(右)=====
// 注意:本页 LayoutBuilder 的 builder 在布局回调阶段执行,
// 不能在其子树里直接调用 context.select/watch(Provider 会断言失败)。
// 因此用 BlocBuilder 包裹 LocaleCubit 的读取,让它在正常 build 阶段取值。
Widget _buildTopBar() {
return BlocBuilder<LocaleCubit, Locale>(
builder: (context, locale) {
final langName = AppLanguages.instance.nameOf(locale);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
// 中文用国内版 logo,英文用英文专版 logo(随语言切换实时更新)
locale.languageCode == 'zh'
? 'assets/images/logo_sata.png'
: 'assets/images/logo_stata-en.png',
height: 40,
fit: BoxFit.contain,
),
GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (_) =>
const LanguageSelectionDialog(mandatory: false),
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(100),
border: Border.all(color: _inputBorder),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.language, size: 16, color: _primaryBlue),
const SizedBox(width: 6),
Text(
langName,
style: const TextStyle(fontSize: 13, color: _navy),
),
const SizedBox(width: 4),
const Icon(Icons.expand_more, size: 16, color: _grayText),
],
),
),
),
],
);
},
);
}
// ===== 机器人 Hero 卡片 =====
Widget _buildHeroCard() {
return ClipRRect(
borderRadius: BorderRadius.circular(16),
child: SizedBox(
height: 130,
width: double.infinity,
child: Stack(
fit: StackFit.expand,
children: [
Image.asset(
'assets/images/robot_logo_login.png',
fit: BoxFit.cover,
),
// 底部渐变遮罩,衬托文字
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.center,
colors: [
Colors.black.withValues(alpha: 0.35),
Colors.transparent,
],
),
),
),
const Positioned(
left: 12,
bottom: 8,
child: Text(
'SMART ROBOTICS · CONNECTED',
style: TextStyle(
color: Colors.white,
fontSize: 11,
letterSpacing: 1.5,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
);
}
// ===== 白色表单卡片 =====
Widget _buildFormCard() {
return Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 16, 16, 18),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: const Color(0xFF1C96F6).withValues(alpha: 0.06),
blurRadius: 20,
offset: const Offset(0, 6),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSegmentTab(),
const SizedBox(height: 14),
// 账号
_buildFieldLabel('账号'),
const SizedBox(height: 6),
_buildTextField(
controller: _userCtrl,
icon: Icons.person_outline,
hint: AppLocalizations.of(context).translate('login.username_hint'),
),
const SizedBox(height: 12),
// 密码
_buildFieldLabel('密码'),
const SizedBox(height: 6),
_buildTextField(
controller: _pwdCtrl,
icon: Icons.lock_outline,
hint: AppLocalizations.of(context).translate('login.password_hint'),
obscureText: _obscurePwd,
suffix: IconButton(
icon: Icon(
_obscurePwd
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
size: 20,
color: _grayText,
),
onPressed: () => setState(() => _obscurePwd = !_obscurePwd),
),
),
const SizedBox(height: 12),
_buildProtocolSection(),
const SizedBox(height: 4),
const Padding(
padding: EdgeInsets.only(left: 32),
child: Text(
'新账户注册前须主动勾选确认',
style: TextStyle(fontSize: 12, color: _grayText),
),
),
const SizedBox(height: 14),
// 登录按钮
BlocBuilder<LoginCubit, LoginState>(
builder: (context, state) {
final isLoading = state is LoginLoading;
return _GradientButton(
isLoading: isLoading,
onPressed: () {
if (!_isAgreed) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)
.translate('login.agree_first')),
duration: const Duration(seconds: 1),
),
);
return;
}
_handleLogin();
},
);
},
),
],
),
);
}
// ===== 登录 / 注册 分段 Tab =====
Widget _buildSegmentTab() {
return Container(
height: 42,
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: _tabBg,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Expanded(child: _buildTabItem(0, '登录')),
Expanded(child: _buildTabItem(1, '注册')),
],
),
);
}
Widget _buildTabItem(int index, String label) {
final selected = _selectedTab == index;
return GestureDetector(
onTap: () => _onTabTap(index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: selected
? const LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [Color(0xFF2BAFFE), Color(0xFF0B7DEE)],
)
: null,
borderRadius: BorderRadius.circular(9),
),
child: Text(
label,
style: TextStyle(
fontSize: 15,
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
color: selected ? Colors.white : _tabUnselectedText,
),
),
),
);
}
Widget _buildFieldLabel(String text) {
return Text(
text,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: _navy,
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required IconData icon,
required String hint,
bool obscureText = false,
Widget? suffix,
}) {
return TextField(
controller: controller,
obscureText: obscureText,
style: const TextStyle(fontSize: 14, color: _navy),
cursorColor: _primaryBlue,
decoration: InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: _grayText, fontSize: 14),
filled: true,
fillColor: Colors.white,
prefixIcon: Icon(icon, size: 20, color: _primaryBlue),
prefixIconConstraints:
const BoxConstraints(minWidth: 44, minHeight: 48),
suffixIcon: suffix,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: _inputBorder),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: _primaryBlue, width: 1.2),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: _inputBorder),
),
),
);
}
// ===== 协议勾选 =====
Widget _buildProtocolSection() {
final loc = AppLocalizations.of(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 24,
width: 24,
child: Checkbox(
value: _isAgreed,
activeColor: _primaryBlue,
checkColor: Colors.white,
side: const BorderSide(color: _inputBorder),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onChanged: (val) => setState(() => _isAgreed = val!),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
loc.translate('login.agree_prefix'),
style: const TextStyle(fontSize: 13, color: _grayText),
),
_protocolText(loc.translate('login.user_agreement_link'),
() => _showProtocolDetail(loc.translate('login.user_agreement_link'))),
Text(
loc.translate('login.agree_and'),
style: const TextStyle(fontSize: 13, color: _grayText),
),
_protocolText(loc.translate('login.privacy_policy_link'),
() => _showProtocolDetail(loc.translate('login.privacy_policy_link'))),
],
),
),
],
);
}
Widget _protocolText(String text, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Text(
text,
style: const TextStyle(
fontSize: 13,
color: _primaryBlue,
fontWeight: FontWeight.w600,
),
),
);
}
Widget _buildHelpRow() {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.help_outline, size: 16, color: _grayText),
const SizedBox(width: 4),
GestureDetector(
onTap: () => CapsuleToast.show('帮助与客服功能开发中', showCheck: false),
child: const Text(
'帮助与客服',
style: TextStyle(fontSize: 13, color: _grayText),
),
),
],
);
}
void _showProtocolDetail(String title) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => Container(
padding: const EdgeInsets.all(24),
height: MediaQuery.of(context).size.height * 0.7,
child: Column(
children: [
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: _navy,
),
),
const Divider(height: 32, color: _inputBorder),
Expanded(
child: SingleChildScrollView(
child: Text(
(AppLocalizations.of(context)
.translate('login.protocol_detail_text')
.replaceAll('%s', title) +
'\n') *
20,
style: const TextStyle(
color: _tabUnselectedText,
height: 1.5,
),
),
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: _primaryBlue,
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(AppLocalizations.of(context)
.translate('login.understood')),
),
),
],
),
),
);
}
}
/// 主按钮:横向蓝色渐变胶囊(#2BAFFE → #1C96F6 → #0B7DEE),带前置箭头。
class _GradientButton extends StatelessWidget {
const _GradientButton({required this.onPressed, required this.isLoading});
final VoidCallback onPressed;
final bool isLoading;
@override
Widget build(BuildContext context) {
final enabled = !isLoading;
return Opacity(
opacity: enabled ? 1 : 0.7,
child: GestureDetector(
onTap: enabled ? onPressed : null,
child: Container(
width: double.infinity,
height: 46,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
gradient: const LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [Color(0xFF2BAFFE), Color(0xFF1C96F6), Color(0xFF0B7DEE)],
stops: [0.0, 0.5, 1.0],
),
boxShadow: [
BoxShadow(
color: const Color(0xFF1C96F6).withValues(alpha: 0.30),
blurRadius: 14,
offset: const Offset(0, 6),
),
],
),
child: isLoading
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.4,
color: Colors.white,
),
)
: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.arrow_forward_rounded,
size: 20, color: Colors.white),
SizedBox(width: 8),
Text(
'登录',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 1,
),
),
],
),
),
),
);
}
}
/// 进场动画包装:淡入 + 上滑。
class _Entrance extends StatelessWidget {
const _Entrance({required this.animation, required this.child});
final Animation<double> animation;
final Widget child;
@override
Widget build(BuildContext context) {
final curved =
CurvedAnimation(parent: animation, curve: Curves.easeOutCubic);
final slideAnim = Tween<Offset>(
begin: const Offset(0, 0.12),
end: Offset.zero,
).animate(curved);
return FadeTransition(
opacity: animation,
child: SlideTransition(position: slideAnim, child: child),
);
}
}

View File

@@ -1,6 +1,8 @@
import 'package:go_router/go_router.dart';
import 'package:maibu_satabot_v2/core/router/route_paths.dart';
import 'package:maibu_satabot_v2/features/auth/presentation/pages/login_page.dart';
import 'package:maibu_satabot_v2/features/auth/presentation/pages/login_page_v2.dart';
// 旧登录页保留备用:如需回退,将下方 LoginPageV2 换回 LoginPage 并恢复对应 import。
// import 'package:maibu_satabot_v2/features/auth/presentation/pages/login_page.dart';
import '../pages/register_page.dart';
import '../pages/splash_page.dart';
@@ -14,7 +16,7 @@ class AuthRoutes {
),
GoRoute(
path: RoutePaths.login,
builder: (context, state) => const LoginPage(),
builder: (context, state) => const LoginPageV2(),
),
GoRoute(
path: RoutePaths.register,

View File

@@ -6,13 +6,18 @@ import 'package:maibu_satabot_v2/features/main_container/domain/tab_config.dart'
import 'package:maibu_satabot_v2/features/home/presentation/pages/home_page.dart';
import 'package:maibu_satabot_v2/features/v2/home/presentation/pages/home_v2_page.dart';
import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_bloc.dart';
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/device_status_page.dart';
// 旧设备页保留备用:如需回退,换回 DeviceStatusPage 并恢复下方 import。
// import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/device_status_page.dart';
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/device_status_page_v2.dart';
import 'package:maibu_satabot_v2/features/ai/presentation/pages/ai_page.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/pages/alarm_center_page.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_cubit.dart';
import 'package:maibu_satabot_v2/features/my/presentation/pages/my_page.dart';
import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/my_page.dart'
as my_v2;
// 新版「我的」页(M01 设计稿);旧版 my_page.dart 保留,如需回退换回 my_v2.MyPage 即可。
import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/my_page_v2.dart'
as my_v2_new;
import 'package:maibu_satabot_v2/features/v2/workorder/presentation/pages/workorder_page.dart';
import 'package:maibu_satabot_v2/features/v2/report/presentation/pages/report_page.dart';
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart';
@@ -91,7 +96,7 @@ class _MainWrapperState extends State<MainWrapper> {
case 'home':
return const HomePage();
case 'device':
return const DeviceStatusPage();
return const DeviceStatusPageV2();
case 'ai':
return const AiPage();
case 'warning':
@@ -106,7 +111,7 @@ class _MainWrapperState extends State<MainWrapper> {
case 'my':
return const MyPage();
case 'me':
return const my_v2.MyPage();
return const my_v2_new.MyPageV2();
default:
return const HomePage();
}

View File

@@ -9,7 +9,9 @@ import 'package:maibu_satabot_v2/features/my/presentation/pages/my_page.dart';
import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart';
import 'package:maibu_satabot_v2/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart';
import 'package:maibu_satabot_v2/features/v2/home/presentation/pages/home_v2_page.dart';
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/device_status_page.dart';
// 旧设备页保留备用:如需回退,换回 DeviceStatusPage 并恢复下方 import。
// import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/device_status_page.dart';
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/device_status_page_v2.dart';
import 'package:maibu_satabot_v2/features/v2/workorder/presentation/pages/workorder_page.dart';
import 'package:maibu_satabot_v2/features/v2/report/presentation/pages/report_page.dart';
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart';
@@ -142,7 +144,7 @@ class _CustomMainContainerState extends State<CustomMainContainer>
return const HomeV2Page(key: ValueKey('home_v2'));
case 'device':
debugPrint('✅ 匹配到 device');
return const DeviceStatusPage(key: ValueKey('device'));
return const DeviceStatusPageV2(key: ValueKey('device'));
case 'ai':
debugPrint('✅ 匹配到 ai');
return const AiPage(key: ValueKey('ai'));

View File

@@ -35,6 +35,7 @@ import 'drone_station_detail_page.dart';
import '../../../../../core/network/mqtt/data/datasources/drone_osd_datasource.dart';
import 'qr_scanner_page.dart';
import 'bind_device_page.dart';
import 'no_device_page_v2.dart';
import '../../../local_camera/presentation/pages/local_camera_page.dart';
import '../../../usb_camera/presentation/pages/usb_camera_page.dart';
import '../../../../remote_control/presentation/bloc/remote_control_cubit.dart';
@@ -190,50 +191,70 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
// 无设备时整页是固定浅色渐变(与我的页 V2 同款),状态栏图标须恒为深色;
// 若跟随深色主题给白色图标,会消失在浅蓝背景上。
final isNoDevice = _isNoDeviceState(
context.watch<DeviceListBloc.DeviceStatusBloc>().state,
);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
statusBarIconBrightness: isNoDevice
? Brightness.dark
: (Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark),
),
child: Scaffold(
backgroundColor: context.appColors.pageBackground,
body: SafeArea(
child:
BlocConsumer<
DeviceListBloc.DeviceStatusBloc,
DeviceListState.DeviceStatusState
>(
listener: (context, state) {
// 🔥 监听错误状态,显示友好提示
if (state is DeviceListState.DeviceStatusError &&
state.shouldShowError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
backgroundColor: context.appColors.warning,
),
);
}
},
builder: (context, state) {
return Column(
children: [
_buildAppBar(context),
_buildSearchBar(context),
_buildTypeFilterBar(context),
Expanded(child: _buildContent(context, state)),
],
);
},
// SafeArea 下移到常规列表分支:无设备页渐变需铺进状态栏(与我的页一致)
body: BlocConsumer<
DeviceListBloc.DeviceStatusBloc,
DeviceListState.DeviceStatusState
>(
listener: (context, state) {
// 🔥 监听错误状态,显示友好提示
if (state is DeviceListState.DeviceStatusError &&
state.shouldShowError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
backgroundColor: context.appColors.warning,
),
);
}
},
builder: (context, state) {
// 无设备:该场站设备接口返回空时,整页(含顶栏)切换到
// 「01_无设备首页」设计稿页面;逻辑链保持 场站→siteId→设备列表 不变。
if (_isNoDeviceState(state)) {
return const NoDevicePageV2();
}
return SafeArea(
child: Column(
children: [
_buildAppBar(context),
_buildSearchBar(context),
_buildTypeFilterBar(context),
Expanded(child: _buildContent(context, state)),
],
),
);
},
),
),
);
}
/// 无设备判据:「全部」标签下设备接口返回空
bool _isNoDeviceState(DeviceListState.DeviceStatusState state) {
return state is DeviceListState.DeviceStatusLoaded &&
state.selectedType == 'all' &&
state.devices.isEmpty;
}
Widget _buildAppBar(BuildContext context) {
// 🔥 个人角色(roleKey == 'personal')隐藏场站选择器
final roleKey = context.watch<AppUserCubit>().state.user?.roleKey;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,575 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:maibu_satabot_v2/components/capsule_toast.dart';
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
import 'package:maibu_satabot_v2/features/main_container/domain/tab_config.dart';
import 'package:maibu_satabot_v2/features/v2/local_camera/presentation/pages/local_camera_page.dart';
import 'package:maibu_satabot_v2/features/v2/site/presentation/cubit/site_cubit.dart';
import 'package:maibu_satabot_v2/features/v2/usb_camera/presentation/pages/usb_camera_page.dart';
import '../bloc/device_status_bloc.dart';
import '../bloc/device_status_event.dart';
import 'bind_device_page.dart';
import 'qr_scanner_page.dart';
/// 无设备首页 —— 按「01_无设备首页」设计稿实现。
///
/// 触发条件(逻辑链不变):SiteCubit 取所有场站 → 第一个场站拉设备列表 →
/// DeviceStatusLoaded 且 devices 为空时,由 DeviceStatusView 整页切换到本页。
///
/// 说明:
/// - 顶栏按设计稿换新:SATA logo + 铃铛/⊕ 白底按钮;
/// - 右上角按钮下方小字「(场站名)」为切换场站入口,个人角色追加「个人账号」说明;
/// - 「添加机器人」与卡片蓝色 ⊕ 均弹出底部添加弹窗,内部各项保持原跳转;
/// - 「新手入门」三条二级内容未开发,统一 Toast 提示;
/// - 背景用设计稿地图背景切图 shebei_bg.png,车图为透明底 shebei-logo.png,
/// 车与背景分层不粘连。
class NoDevicePageV2 extends StatelessWidget {
const NoDevicePageV2({super.key});
// ===== 固定视觉配色(与登录页/我的页 V2 同系) =====
static const _bgTop = Color(0xFFFAFCFF);
static const _navy = Color(0xFF1A2B4A);
static const _grayText = Color(0xFF86909C);
static const _primaryBlue = Color(0xFF1C96F6);
static const _cardBorder = Color(0xFFE9F1FA);
static const _btnGradient = LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [Color(0xFF2BAFFE), Color(0xFF1C96F6), Color(0xFF0B7DEE)],
stops: [0.0, 0.5, 1.0],
);
static void _todoTap() {
CapsuleToast.show('功能暂未开发', showCheck: false);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _bgTop,
body: Stack(
fit: StackFit.expand,
children: [
// 背景:设计稿地图背景切图,顶对齐裁切(替代此前手绘三层渐变)
Positioned.fill(
child: Image.asset(
'assets/images/shebei_bg.png',
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
SafeArea(
bottom: false,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
_buildTopBar(context),
_buildSiteEntry(context),
const SizedBox(height: 18),
_buildHeroCard(context),
const SizedBox(height: 26),
const Center(
child: Text(
'还没有添加机器人',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: _navy,
),
),
),
const SizedBox(height: 10),
const Center(
child: Text(
'您可以先了解 APP 的功能,\n收到机器人后随时绑定使用。',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: _grayText,
height: 1.6,
),
),
),
const SizedBox(height: 20),
_buildAddButton(context),
const SizedBox(height: 30),
const Text(
'新手入门',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: _navy,
),
),
const SizedBox(height: 12),
_buildGuideCard(
icon: Icons.list_alt,
title: '产品操作指南',
subtitle: '了解机器人基础功能和使用流程',
),
const SizedBox(height: 10),
_buildGuideCard(
icon: Icons.videocam_outlined,
title: '观看产品视频',
subtitle: '快速上手遥控、图传及作业功能',
),
const SizedBox(height: 10),
_buildGuideCard(
icon: Icons.help_outline,
title: '帮助与售后服务',
subtitle: '常见问题、报修及客服支持',
),
const SizedBox(height: 24),
],
),
),
),
],
),
);
}
// ===== 顶栏:logo + 铃铛/⊕(设计稿新样式) =====
Widget _buildTopBar(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Image.asset(
'assets/images/me_left_logo.png',
height: 44,
fit: BoxFit.contain,
),
Row(
children: [
_buildTopIconButton(Icons.notifications_none, onTap: _todoTap),
const SizedBox(width: 10),
// 设计稿:右上角 ⊕ 是蓝底白号主色按钮,与铃铛的白底按钮区分
_buildTopIconButton(
Icons.add,
onTap: () => _showAddSheet(context),
primary: true,
),
],
),
],
);
}
Widget _buildTopIconButton(
IconData icon, {
required VoidCallback onTap,
bool primary = false,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: primary ? null : Colors.white,
gradient: primary ? _btnGradient : null,
borderRadius: BorderRadius.circular(14),
border: primary ? null : Border.all(color: _cardBorder),
boxShadow: [
BoxShadow(
color: primary
? _primaryBlue.withValues(alpha: 0.3)
: _primaryBlue.withValues(alpha: 0.05),
blurRadius: primary ? 10 : 8,
offset: const Offset(0, 2),
),
],
),
child: Icon(
icon,
size: 22,
color: primary ? Colors.white : _navy,
),
),
);
}
// ===== 切换场站入口:右上角按钮下方小字(场站名) =====
Widget _buildSiteEntry(BuildContext context) {
final isPersonal =
context.watch<AppUserCubit>().state.user?.roleKey ==
TabConfig.personalRoleKey;
return BlocBuilder<SiteCubit, SiteState>(
bloc: GetIt.I<SiteCubit>(),
builder: (context, siteState) {
final name = siteState.selectedSite?.siteName ?? '未选择场站';
return Align(
alignment: Alignment.centerRight,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _onSiteTap(context, siteState, isPersonal),
child: Padding(
padding: const EdgeInsets.only(top: 6, right: 4, bottom: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
isPersonal ? '($name)· 个人账号' : '($name)',
style: const TextStyle(fontSize: 12, color: _grayText),
),
if (!isPersonal)
const Icon(
Icons.keyboard_arrow_down,
size: 16,
color: _grayText,
),
],
),
),
),
);
},
);
}
void _onSiteTap(BuildContext context, SiteState siteState, bool isPersonal) {
if (isPersonal) {
CapsuleToast.show('个人账号仅绑定一个场站', showCheck: false);
return;
}
if (siteState.sites.isEmpty) {
CapsuleToast.show('暂无场站数据', showCheck: false);
return;
}
final siteCubit = GetIt.I<SiteCubit>();
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (sheetCtx) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Padding(
padding: EdgeInsets.symmetric(vertical: 14),
child: Center(
child: Text(
'选择场站',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: _navy,
),
),
),
),
const Divider(height: 1, color: Color(0xFFF0F4FA)),
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: siteState.sites.length,
itemBuilder: (_, i) {
final site = siteState.sites[i];
final selected =
siteCubit.state.selectedSite?.id == site.id;
return ListTile(
title: Text(
site.siteName,
style: TextStyle(
fontSize: 15,
fontWeight: selected
? FontWeight.w600
: FontWeight.w400,
color: selected ? _primaryBlue : _navy,
),
),
trailing: selected
? const Icon(
Icons.check,
size: 20,
color: _primaryBlue,
)
: null,
onTap: () {
// 选中后 SiteCubit 流会触发设备列表重拉;
// 若新场站有设备,本页会自动退回正常列表。
siteCubit.selectSite(site);
Navigator.pop(sheetCtx);
},
);
},
),
),
],
),
);
},
);
}
// ===== Hero 卡:透明底车图 + 右上角蓝色 ⊕ =====
Widget _buildHeroCard(BuildContext context) {
return Container(
height: 230,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.72),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: _cardBorder),
boxShadow: [
BoxShadow(
color: _primaryBlue.withValues(alpha: 0.06),
blurRadius: 16,
offset: const Offset(0, 4),
),
],
),
child: Stack(
children: [
Positioned.fill(
child: Padding(
padding: const EdgeInsets.all(20),
child: Image.asset(
'assets/images/shebei-logo.png',
fit: BoxFit.contain,
),
),
),
Positioned(
top: 14,
right: 14,
child: GestureDetector(
onTap: () => _showAddSheet(context),
child: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: _btnGradient,
),
child: const Icon(Icons.add, color: Colors.white, size: 22),
),
),
),
],
),
);
}
// ===== 添加机器人主按钮 =====
Widget _buildAddButton(BuildContext context) {
return GestureDetector(
onTap: () => _showAddSheet(context),
child: Container(
height: 48,
decoration: BoxDecoration(
gradient: _btnGradient,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: _primaryBlue.withValues(alpha: 0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add, color: Colors.white, size: 20),
SizedBox(width: 8),
Text(
'添加机器人',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
);
}
// ===== 添加机器人底部弹窗:各项保持原跳转 =====
void _showAddSheet(BuildContext context) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (sheetCtx) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 8, 4),
child: Row(
children: [
const Expanded(
child: Center(
child: Text(
'添加机器人',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: _navy,
),
),
),
),
IconButton(
icon: const Icon(
Icons.close,
size: 20,
color: _grayText,
),
onPressed: () => Navigator.pop(sheetCtx),
),
],
),
),
_sheetItem(
icon: Icons.qr_code_scanner,
label: '扫一扫',
onTap: () async {
Navigator.pop(sheetCtx);
final result = await Navigator.of(context).push<String>(
MaterialPageRoute(builder: (_) => const QrScannerPage()),
);
if (result != null && context.mounted) {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => BindDevicePage(scanResult: result),
),
);
// 绑定流程结束后重拉设备列表,有设备则自动退回正常列表页
if (context.mounted) {
context.read<DeviceStatusBloc>().add(
const DeviceStatusRefresh(),
);
}
}
},
),
_sheetDivider(),
_sheetItem(
icon: Icons.add_circle_outline,
label: '添加设备',
onTap: () {
Navigator.pop(sheetCtx);
// 原菜单该项即为 TODO,保持不跳转
_todoTap();
},
),
_sheetDivider(),
_sheetItem(
icon: Icons.videocam_outlined,
label: '本地视频',
onTap: () {
Navigator.pop(sheetCtx);
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const LocalCameraPage()),
);
},
),
_sheetDivider(),
_sheetItem(
icon: Icons.usb,
label: 'USB 摄像头',
onTap: () {
Navigator.pop(sheetCtx);
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const UsbCameraPage()),
);
},
),
const SizedBox(height: 8),
],
),
);
},
);
}
Widget _sheetItem({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
child: Row(
children: [
Icon(icon, size: 20, color: _navy),
const SizedBox(width: 12),
Text(
label,
style: const TextStyle(fontSize: 15, color: _navy),
),
],
),
),
);
}
Widget _sheetDivider() {
return const Divider(height: 1, indent: 20, color: Color(0xFFF0F4FA));
}
// ===== 新手入门卡片:二级未开发,统一提示 =====
Widget _buildGuideCard({
required IconData icon,
required String title,
required String subtitle,
}) {
return GestureDetector(
onTap: _todoTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: _cardBorder),
),
child: Row(
children: [
Icon(icon, size: 24, color: _primaryBlue),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: _navy,
),
),
const SizedBox(height: 3),
Text(
subtitle,
style: const TextStyle(fontSize: 12, color: _grayText),
),
],
),
),
const Icon(
Icons.chevron_right,
size: 20,
color: Color(0xFFC9CDD4),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,344 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:maibu_satabot_v2/components/capsule_toast.dart';
/// 设备状态 V2 —— 卡片视图设计稿组件。
///
/// 纯展示组件:点击回调、在线状态、电量均由页面传入真实数据
/// (在线态来自 onlineStatus,电量来自 battery / capacityPercent,与旧卡片一致)。
/// 仅版本 / SN 掩码等后端暂未返回的字段用设备 ID 做**稳定伪随机**占位
/// (同一设备每次渲染结果一致,不会闪变),等后端补齐字段后替换为真值即可。
/// 设备展示状态(对应设计稿徽章:作业中 / 在线 / 离线)
enum DeviceV2Status { working, online, offline }
/// 占位数据生成器:以设备唯一串为种子,保证同设备结果稳定
class DeviceV2Display {
DeviceV2Display._();
static const _editions = ['PLUS 智能版', 'STD 标准版', 'PRO 旗舰版'];
static int _seed(String key) => key.hashCode & 0x7FFFFFFF;
/// 版本后缀:PLUS 智能版 / STD 标准版 / PRO 旗舰版
static String editionOf(String key) => _editions[_seed(key) % _editions.length];
/// SN 掩码展示:前4 · · · 后2
static String maskedSn(String key) {
if (key.length <= 6) return key.toUpperCase();
return '${key.substring(0, 4)} · · · ${key.substring(key.length - 2)}'
.toUpperCase();
}
/// 底部作业态文案
static String workTextOf(DeviceV2Status status) {
return switch (status) {
DeviceV2Status.working => '自主作业中',
DeviceV2Status.online => '待机',
DeviceV2Status.offline => '暂时无法连接',
};
}
}
/// 设计稿设备卡片:缩略图 + 名称/徽章 + 型号版本 + SN + 底部作业态/电量
class DeviceCardV2 extends StatelessWidget {
const DeviceCardV2({
super.key,
required this.name,
required this.modelText,
required this.snKey,
required this.status,
required this.onTap,
this.batteryLevel,
});
/// 设备名称(别名优先,页面已处理)
final String name;
/// 型号 · 版本,如 "MC700 · PLUS 智能版"
final String modelText;
/// 用于 SN 掩码/复制与占位随机的设备唯一串
final String snKey;
final DeviceV2Status status;
/// 真实电量百分比(离线或无数据时传 null,卡片显示 --)
final int? batteryLevel;
/// 卡片点击:与旧卡片同一跳转逻辑
final VoidCallback onTap;
// ===== 固定视觉配色(与登录/我的/无设备页 V2 同系) =====
static const _navy = Color(0xFF1A2B4A);
static const _grayText = Color(0xFF86909C);
static const _primaryBlue = Color(0xFF1C96F6);
static const _cardBorder = Color(0xFFE9F1FA);
static const _workingOrange = Color(0xFFF5A623);
static const _workingBg = Color(0xFFFEF3E3);
static const _onlineGreen = Color(0xFF2FBF6B);
static const _onlineBg = Color(0xFFE5F8EC);
static const _offlineGray = Color(0xFF6B7280);
static const _offlineBg = Color(0xFFF0F2F5);
@override
Widget build(BuildContext context) {
final offline = status == DeviceV2Status.offline;
final battery = offline ? null : batteryLevel;
return GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: _cardBorder),
boxShadow: [
BoxShadow(
color: _primaryBlue.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
padding: const EdgeInsets.all(12),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildThumb(offline),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: _navy,
),
),
),
// 编辑(改名):后端能力未接,统一提示
GestureDetector(
onTap: () =>
CapsuleToast.show('功能暂未开发', showCheck: false),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 6),
child: Icon(
Icons.edit_outlined,
size: 15,
color: _grayText,
),
),
),
_buildBadge(),
],
),
const SizedBox(height: 6),
Text(
modelText,
style: const TextStyle(fontSize: 13, color: _grayText),
),
const SizedBox(height: 6),
Row(
children: [
Flexible(
child: Text(
'SN: ${DeviceV2Display.maskedSn(snKey)}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 13,
color: _grayText,
),
),
),
// 复制完整 SN
GestureDetector(
onTap: () async {
await Clipboard.setData(
ClipboardData(text: snKey),
);
CapsuleToast.show('SN 已复制到剪贴板');
},
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 6),
child: Icon(
Icons.copy_rounded,
size: 15,
color: _grayText,
),
),
),
],
),
],
),
),
Column(
children: [
// 更多操作:二级未开发
GestureDetector(
onTap: () =>
CapsuleToast.show('功能暂未开发', showCheck: false),
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: const Color(0xFFF3F6FA),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.more_horiz,
size: 18,
color: _navy,
),
),
),
const SizedBox(height: 14),
const Icon(
Icons.chevron_right,
size: 20,
color: Color(0xFFC9CDD4),
),
],
),
],
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Divider(height: 1, color: Color(0xFFF0F4FA)),
),
Row(
children: [
Icon(
offline
? Icons.link_off
: Icons.monitor_heart_outlined,
size: 18,
color: offline ? _offlineGray : _primaryBlue,
),
const SizedBox(width: 6),
Text(
DeviceV2Display.workTextOf(status),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: offline ? _offlineGray : _navy,
),
),
const Icon(
Icons.keyboard_arrow_right,
size: 16,
color: Color(0xFFC9CDD4),
),
const Spacer(),
_buildBattery(battery),
],
),
],
),
),
);
}
/// 缩略图:透明底车图,离线时置灰
Widget _buildThumb(bool offline) {
return Container(
width: 96,
height: 78,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFEAF3FE), Color(0xFFD9EAFB)],
),
),
child: () {
final car = Image.asset(
'assets/images/shebei-logo.png',
fit: BoxFit.cover,
);
// 离线置灰;在线不能套任何滤镜(transparent 滤镜会把图洗没)
if (!offline) return car;
return ColorFiltered(
colorFilter: const ColorFilter.matrix(<double>[
0.33, 0.33, 0.33, 0, 40, //
0.33, 0.33, 0.33, 0, 40, //
0.33, 0.33, 0.33, 0, 40, //
0, 0, 0, 1, 0, //
]),
child: car,
);
}(),
);
}
/// 状态徽章:作业中(橙) / 在线(绿) / 离线(灰)
Widget _buildBadge() {
final (color, bg, label) = switch (status) {
DeviceV2Status.working => (_workingOrange, _workingBg, '作业中'),
DeviceV2Status.online => (_onlineGreen, _onlineBg, '在线'),
DeviceV2Status.offline => (_offlineGray, _offlineBg, '离线'),
};
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(20)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 5,
height: 5,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: color),
),
],
),
);
}
/// 电量:离线显示空电池 + --
Widget _buildBattery(int? level) {
if (level == null) {
return const Row(
children: [
Icon(Icons.battery_unknown, size: 18, color: _offlineGray),
SizedBox(width: 4),
Text('--', style: TextStyle(fontSize: 13, color: _offlineGray)),
],
);
}
final icon = switch (level) {
>= 80 => Icons.battery_full,
>= 50 => Icons.battery_5_bar,
>= 25 => Icons.battery_3_bar,
_ => Icons.battery_2_bar,
};
return Row(
children: [
Icon(icon, size: 18, color: _onlineGreen),
const SizedBox(width: 4),
Text(
'$level%',
style: const TextStyle(fontSize: 13, color: _navy),
),
],
);
}
}

View File

@@ -145,7 +145,10 @@ class LocalCameraCubit extends Cubit<LocalCameraState>
if (!ok) {
final detail = await _describeNetworks();
final header = Platform.isIOS
? '未检测到局域网连接。请让 iPhone/iPad 与摄像头连入同一 WiFi 后重试'
? '未检测到局域网网卡。\n'
'• iPad 纯网线:插上 USB‑C 转以太网适配器并接好摄像头网线,'
'到「设置 → 以太网」把 IP 配成与摄像头同网段\n'
'• iPhone 不支持网线直连,请改用摄像头自带 WiFi 或便携路由'
: msgNoNetworkEntry;
emit(
state.copyWith(
@@ -549,18 +552,47 @@ class LocalCameraCubit extends Cubit<LocalCameraState>
}
}
/// iOS:局域网连通判据——走同一 WiFi(或以太网),不要求 USB 以太网卡存在。
/// iOS:局域网连通判据。
/// iPhone/iPad 没有安卓那种「USB-C 转以太网 + 以太网共享」,组网方式不同:
/// • 走同一 WiFi(en0);或
/// • iPad 插 USB‑C 转以太网适配器「纯网线」直连摄像头(connectivity_plus 常把它
/// 报成 other/none,所以必须再用网卡枚举复核)。
/// 判据:connectivity 报 WiFi/以太网,或存在任一「非蜂窝、非环回」的 IPv4 网卡。
Future<bool> _isLanConnectedForIOS() async {
// ① connectivity 快判
try {
final results = await Connectivity().checkConnectivity();
final bool lan = results.contains(ConnectivityResult.wifi) ||
results.contains(ConnectivityResult.ethernet);
debugPrint(
'🌐 [网络检查-iOS] ${lan ? "已连接 WiFi/以太网 LAN ✔" : "未见 LAN"}:$results',
);
return lan;
if (results.contains(ConnectivityResult.wifi) ||
results.contains(ConnectivityResult.ethernet)) {
debugPrint('🌐 [网络检查-iOS] connectivity 报 WiFi/以太网 LAN ✔ $results');
return true;
}
debugPrint('🌐 [网络检查-iOS] connectivity:$results(转网卡枚举复核)');
} catch (e) {
debugPrint('🌐 [网络检查-iOS] connectivity 失败:$e(保守放行,交给后续探测)');
debugPrint('🌐 [网络检查-iOS] connectivity 失败:$e(转网卡枚举复核)');
}
// ② 网卡枚举复核:兼容 iPad USB 以太网直连(纯网线、无 WiFi)
try {
final ifs = await NetworkInterface.list(
type: InternetAddressType.IPv4,
includeLoopback: false,
);
for (final i in ifs) {
final n = i.name.toLowerCase();
// 排除蜂窝(iPhone 数据网络),其余有 IPv4 的接口都算局域网就绪
final cellular = n.startsWith('pdp') ||
n.startsWith('wwan') ||
n.contains('cellular');
if (!cellular && i.addresses.isNotEmpty) {
final ips = i.addresses.map((a) => a.address.toString()).join(',');
debugPrint('🌐 [网络检查-iOS] 采用网卡 ${i.name}($ips) 视为局域网 ✔');
return true;
}
}
debugPrint('🌐 [网络检查-iOS] 未发现可用局域网网卡(仅蜂窝/无 IP)');
return false;
} catch (e) {
debugPrint('🌐 [网络检查-iOS] 枚举网卡失败:$e(保守放行,交给后续探测)');
return true;
}
}
@@ -738,7 +770,7 @@ class LocalCameraCubit extends Cubit<LocalCameraState>
phase: LocalCameraPhase.disconnected,
playStep: ChainStatus.failed,
message: _usesMediaKit
? '局域网连接已断开,请检查 WiFi 与摄像头是否在线'
? '局域网连接已断开,请检查网线/适配器与摄像头是否在线'
: msgDisconnected,
showReconnect: true,
),

View File

@@ -0,0 +1,610 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
import 'package:maibu_satabot_v2/core/router/route_paths.dart';
import 'package:maibu_satabot_v2/core/storage/user_storage.dart';
import 'package:maibu_satabot_v2/core/utils/image_url_util.dart';
import 'package:maibu_satabot_v2/components/capsule_toast.dart';
import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart';
import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/help_feedback_page.dart';
import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/profile_detail_page.dart';
import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/system_settings_page.dart';
/// 我的页面 V2 —— 按 M01「我的首页」设计稿实现。
///
/// 说明:
/// - 视觉:背景渐变复用登录页 V2(纵向浅蓝 + 左上/右上径向光晕);
/// 顶部 me_left_logo.png + 铃铛/设置按钮;用户卡片底图用 logo_me_bg.png 切图。
/// - 保留旧版 my_page.dart(本文件为新版,路由/Tab 已切到本页)。
/// - 「编辑个人资料」→ 现有 ProfileDetailPage;「通用设置」→ 现有 SystemSettingsPage。
/// - 数字(工单/保养/未读)暂无接口,按约定显示「未知」;未实现入口统一 Toast。
/// - 退出登录按需求放在页脚下方。
class MyPageV2 extends StatelessWidget {
const MyPageV2({super.key});
// ===== 设计稿配色(与登录页 V2 同一套固定视觉,不随主题切换) =====
static const _bgTop = Color(0xFFFAFCFF);
static const _bgMid = Color(0xFFF0F6FE);
static const _bgBottom = Color(0xFFD6E9FB);
static const _navy = Color(0xFF1A2B4A);
static const _grayText = Color(0xFF86909C);
static const _subText = Color(0xFF4E5969);
static const _primaryBlue = Color(0xFF1C96F6);
static const _orange = Color(0xFFF5A623);
static const _iconBgBlue = Color(0xFFE9F3FE);
static const _iconBgOrange = Color(0xFFFEF3E3);
static const _divider = Color(0xFFF0F4FA);
static const _noteBg = Color(0xFFEAF3FD);
// 设计稿卡片描边:白卡周围那圈淡蓝边(实测约 RGB 241,247,254)
static const _cardBorder = Color(0xFFE9F1FA);
/// 未实现入口的统一提示
static void _todoTap() {
CapsuleToast.show('功能暂未开发', showCheck: false);
}
@override
Widget build(BuildContext context) {
final user = context.watch<AppUserCubit>().state.user;
final userName = user?.nickname ?? user?.username ?? 'SATA用户';
final userContact = user?.email ?? user?.phone;
return Scaffold(
backgroundColor: _bgTop,
body: Stack(
fit: StackFit.expand,
children: [
// 背景三层:纵向渐变 + 左上/右上蓝色光晕(与登录页 V2 一致)
const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [_bgTop, _bgMid, _bgBottom],
stops: [0.0, 0.4, 1.0],
),
),
),
const DecoratedBox(
decoration: BoxDecoration(
gradient: RadialGradient(
center: Alignment(-1.1, -1.0),
radius: 1.1,
colors: [Color(0xFFD6E8FC), Color(0x00D6E8FC)],
),
),
),
const DecoratedBox(
decoration: BoxDecoration(
gradient: RadialGradient(
center: Alignment(1.1, -0.9),
radius: 1.25,
colors: [Color(0xFFC7DFF8), Color(0x00C7DFF8)],
),
),
),
SafeArea(
bottom: false,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
_buildTopBar(context),
const SizedBox(height: 16),
_buildUserCard(context, userName, userContact, user?.avatar),
const SizedBox(height: 14),
_buildStatsRow(context),
const SizedBox(height: 22),
_buildSectionTitle('服务支持'),
const SizedBox(height: 10),
_buildServiceGroup(context),
const SizedBox(height: 22),
_buildSectionTitle('账户与设置'),
const SizedBox(height: 10),
_buildAccountGroup(context),
const SizedBox(height: 16),
_buildNoteCard(),
const SizedBox(height: 14),
// 页脚灰字(设计稿)
const Center(
child: Text(
'设计原型 · 数据仅供演示 · 不连接真实设备或账号',
style: TextStyle(fontSize: 11, color: _grayText),
),
),
const SizedBox(height: 10),
// 退出登录:按需求放在设计稿内容下方
_buildLogoutButton(context),
const SizedBox(height: 24),
],
),
),
),
],
),
);
}
// ===== 顶部栏:logo + 铃铛/设置按钮 =====
Widget _buildTopBar(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Image.asset(
'assets/images/me_left_logo.png',
height: 44,
fit: BoxFit.contain,
),
Row(
children: [
_buildTopIconButton(Icons.notifications_none, onTap: _todoTap),
const SizedBox(width: 10),
_buildTopIconButton(
Icons.settings_outlined,
onTap: () => _push(context, const SystemSettingsPage()),
),
],
),
],
);
}
Widget _buildTopIconButton(IconData icon, {required VoidCallback onTap}) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: _cardBorder),
boxShadow: [
BoxShadow(
color: _primaryBlue.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Icon(icon, size: 22, color: _navy),
),
);
}
// ===== 用户卡片:底图用 logo_me_bg.png 切图 =====
Widget _buildUserCard(
BuildContext context,
String userName,
String? userContact,
String? avatarUrl,
) {
return GestureDetector(
onTap: () => _push(context, const ProfileDetailPage()),
// 切图自身已烘进圆角、细描边与透明留白:不能再套 ClipRRect+cover,
// 否则裁切会把描边线残留在卡片内部形成痕迹;fill 原样呈现即可。
child: Stack(
children: [
Positioned.fill(
child: Image.asset(
'assets/images/logo_me_bg.png',
fit: BoxFit.fill,
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
_buildAvatar(avatarUrl),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
userName,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 19,
fontWeight: FontWeight.bold,
color: _navy,
),
),
),
const Icon(
Icons.chevron_right,
size: 18,
color: _navy,
),
],
),
const SizedBox(height: 3),
Text(
'个人账号 · ${userContact ?? '未设置联系方式'}',
style: const TextStyle(
fontSize: 12.5,
color: _subText,
),
),
const SizedBox(height: 8),
const Row(
children: [
Text(
'编辑个人资料',
style: TextStyle(
fontSize: 13.5,
fontWeight: FontWeight.w600,
color: _primaryBlue,
),
),
Icon(
Icons.chevron_right,
size: 16,
color: _primaryBlue,
),
],
),
],
),
),
],
),
),
],
),
);
}
/// 头像:后端可能返回相对路径,先经 ImageUrlUtil 补全(项目规范)
Widget _buildAvatar(String? avatarUrl) {
final url = ImageUrlUtil.resolve(avatarUrl);
return Container(
width: 60,
height: 60,
clipBehavior: Clip.antiAlias,
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
child: url != null && url.isNotEmpty
? Image.network(
url,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
_buildDefaultAvatar(),
)
: _buildDefaultAvatar(),
);
}
Widget _buildDefaultAvatar() {
return const Icon(Icons.person, size: 36, color: _primaryBlue);
}
// ===== 三个数据卡 =====
Widget _buildStatsRow(BuildContext context) {
return Row(
children: [
Expanded(
child: _buildStatCard(
icon: Icons.assignment_outlined,
iconColor: _primaryBlue,
value: '未知',
label: '处理中工单',
onTap: _todoTap,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
icon: Icons.build_outlined,
iconColor: _orange,
value: '未知',
label: '保养提醒',
onTap: _todoTap,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
icon: Icons.article_outlined,
iconColor: _primaryBlue,
value: '教程',
label: '使用与帮助',
onTap: () => _push(context, const HelpFeedbackPage()),
),
),
],
);
}
Widget _buildStatCard({
required IconData icon,
required Color iconColor,
required String value,
required String label,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: _cardBorder),
boxShadow: [
BoxShadow(
color: _primaryBlue.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Icon(icon, size: 20, color: iconColor),
const SizedBox(height: 4),
// 字号与登录页统一:数值 16 ≈ 登录按钮字号
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: _navy,
),
),
const SizedBox(height: 1),
// 标签 11.5 ≈ 登录页辅助文字,略小于输入提示 14
Text(label, style: const TextStyle(fontSize: 11.5, color: _grayText)),
],
),
),
);
}
// ===== 分组卡片 =====
Widget _buildSectionTitle(String title) {
return Text(
title,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: _navy,
),
);
}
Widget _buildGroupCard(List<Widget> rows) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: _cardBorder),
boxShadow: [
BoxShadow(
color: _primaryBlue.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(children: rows),
);
}
Widget _buildServiceGroup(BuildContext context) {
return _buildGroupCard([
_buildMenuRow(
icon: Icons.headset_mic_outlined,
iconBg: _iconBgBlue,
iconColor: _primaryBlue,
title: '售后服务中心',
subtitle: '报修、工单、保修查询',
onTap: _todoTap,
),
_buildRowDivider(),
_buildMenuRow(
icon: Icons.build_outlined,
iconBg: _iconBgOrange,
iconColor: _orange,
title: '维修保养',
subtitle: '保养计划、提醒与维护记录',
onTap: _todoTap,
),
_buildRowDivider(),
_buildMenuRow(
icon: Icons.menu_book_outlined,
iconBg: _iconBgBlue,
iconColor: _primaryBlue,
title: '帮助与操作指南',
subtitle: '产品说明书、教程及常见问题',
onTap: () => _push(context, const HelpFeedbackPage()),
),
]);
}
Widget _buildAccountGroup(BuildContext context) {
return _buildGroupCard([
_buildMenuRow(
icon: Icons.verified_user_outlined,
iconBg: _iconBgBlue,
iconColor: _primaryBlue,
title: '账号与安全',
subtitle: '邮箱/手机、密码及登录设备',
onTap: _todoTap,
),
_buildRowDivider(),
_buildMenuRow(
icon: Icons.notifications_none,
iconBg: _iconBgBlue,
iconColor: _primaryBlue,
title: '消息通知',
subtitle: '设备、作业、服务及授权消息',
// 未读接口暂无,按约定显示「未知」
trailingText: '未读未知',
onTap: _todoTap,
),
_buildRowDivider(),
_buildMenuRow(
icon: Icons.settings_outlined,
iconBg: _iconBgBlue,
iconColor: _primaryBlue,
title: '通用设置',
subtitle: '语言、单位、地区及APP偏好',
onTap: () => _push(context, const SystemSettingsPage()),
),
_buildRowDivider(),
_buildMenuRow(
icon: Icons.info_outline,
iconBg: _iconBgBlue,
iconColor: _primaryBlue,
title: '关于与法律协议',
subtitle: 'APP版本、协议和隐私政策',
onTap: _todoTap,
),
]);
}
Widget _buildMenuRow({
required IconData icon,
required Color iconBg,
required Color iconColor,
required String title,
required String subtitle,
required VoidCallback onTap,
String? trailingText,
}) {
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: iconBg,
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, size: 21, color: iconColor),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: _navy,
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: const TextStyle(fontSize: 12, color: _grayText),
),
],
),
),
if (trailingText != null)
Text(
trailingText,
style: const TextStyle(fontSize: 12.5, color: _grayText),
),
const SizedBox(width: 2),
const Icon(Icons.chevron_right, size: 18, color: Color(0xFFC9CDD4)),
],
),
),
);
}
Widget _buildRowDivider() {
return const Divider(height: 1, thickness: 1, color: _divider, indent: 66);
}
// ===== 底部说明卡(设计稿原文) =====
Widget _buildNoteCard() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: _noteBg,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'设备共享、成员授权、管理员移交与解绑转让沿用「设备」模块已有流程,本模块不重复开发。',
style: TextStyle(fontSize: 12.5, color: _subText, height: 1.6),
),
);
}
// ===== 退出登录(放在页脚下方) =====
Widget _buildLogoutButton(BuildContext context) {
return GestureDetector(
onTap: () => _showLogoutDialog(context),
behavior: HitTestBehavior.opaque,
child: const SizedBox(
width: double.infinity,
height: 44,
child: Center(
child: Text(
'退出登录',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Color(0xFFF53F3F),
),
),
),
),
);
}
void _showLogoutDialog(BuildContext context) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('退出登录'),
content: const Text('确定要退出登录吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('取消'),
),
TextButton(
onPressed: () async {
Navigator.pop(dialogContext);
await GetIt.I<UserStorage>().deleteUser();
GetIt.I<AuthCubit>().logout();
if (dialogContext.mounted) {
GoRouter.of(dialogContext).go(RoutePaths.login);
}
},
style: TextButton.styleFrom(
foregroundColor: const Color(0xFFF53F3F),
),
child: const Text('确定'),
),
],
),
);
}
void _push(BuildContext context, Widget page) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => page),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:go_router/go_router.dart';
import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/my_page.dart';
// 新版「我的」页(M01 设计稿);旧版 my_page.dart 保留备用
import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/my_page_v2.dart';
/// 我的页面路由配置
class MyRoutes {
@@ -7,7 +8,7 @@ class MyRoutes {
GoRoute(
path: 'my',
name: 'my',
builder: (context, state) => const MyPage(),
builder: (context, state) => const MyPageV2(),
),
];
}