新版UI-登录页面-我的页面(简单适配)-设备的部分(无设备的+有设备)
BIN
assets/images/login-logo.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/images/logo_me_bg.png
Normal file
|
After Width: | Height: | Size: 279 KiB |
BIN
assets/images/logo_sata.png
Normal file
|
After Width: | Height: | Size: 677 KiB |
BIN
assets/images/logo_stata-en.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
assets/images/me_left_logo.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
assets/images/robot_logo_login.png
Normal file
|
After Width: | Height: | Size: 524 KiB |
BIN
assets/images/shebei-logo.png
Normal file
|
After Width: | Height: | Size: 774 KiB |
BIN
assets/images/shebei_bg.png
Normal file
|
After Width: | Height: | Size: 807 KiB |
739
lib/features/auth/presentation/pages/login_page_v2.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
610
lib/features/v2/my/presentation/pages/my_page_v2.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||