Compare commits

..

2 Commits

Author SHA1 Message Date
zyc
3f7b38a59b Merge pull request 'fix: auto repair bugs #53' (#2) from fix/auto-20260228-143427 into main
Merge PR #2 (approved via Log Center)
2026-02-28 14:47:24 +08:00
repair-agent
2fabae8738 fix: auto repair bugs #53 2026-02-28 14:35:50 +08:00
11 changed files with 159 additions and 123 deletions

View File

@ -20,7 +20,5 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@ -2,12 +2,15 @@ import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@ -24,6 +26,42 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限来搜索和连接您的设备</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限来搜索和连接您的设备</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要位置权限以扫描附近的蓝牙设备</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchScreen</key>
<dict>
<key>UIColorName</key>
<string>LaunchBackgroundColor</string>
<key>UIImageName</key>
<string>LaunchImage</string>
</dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@ -41,22 +79,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限来搜索和连接您的设备</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限来搜索和连接您的设备</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要位置权限以扫描附近的蓝牙设备</string>
<key>UILaunchScreen</key>
<dict>
<key>UIColorName</key>
<string>LaunchBackgroundColor</string>
<key>UIImageName</key>
<string>LaunchImage</string>
</dict>
</dict>
</plist>

View File

@ -5,6 +5,7 @@ import '../../features/auth/presentation/pages/login_page.dart';
import '../../pages/bluetooth_page.dart';
import '../../pages/device_control_page.dart';
import '../../pages/home_page.dart';
import '../../pages/product_selection_page.dart';
import '../../pages/profile/profile_page.dart';
import '../../pages/webview_page.dart';
import '../../pages/wifi_config_page.dart';
@ -48,6 +49,10 @@ GoRouter goRouter(Ref ref) {
extra: state.extra as Map<String, dynamic>?,
),
),
GoRoute(
path: '/product-selection',
builder: (context, state) => const ProductSelectionPage(),
),
GoRoute(
path: '/device-control',
builder: (context, state) => const DeviceControlPage(),

View File

@ -48,4 +48,4 @@ final class GoRouterProvider
}
}
String _$goRouterHash() => r'8e620e452bb81f2c6ed87b136283a9e508dca2e9';
String _$goRouterHash() => r'9f77a00bcbc90890c4b6594a9709288e5206c7d8';

View File

@ -30,12 +30,12 @@ Map<String, dynamic> _$DeviceTypeToJson(_DeviceType instance) =>
_DeviceInfo _$DeviceInfoFromJson(Map<String, dynamic> json) => _DeviceInfo(
id: (json['id'] as num).toInt(),
sn: json['sn'] as String,
deviceType: (json['device_type'] is Map<String, dynamic>)
? DeviceType.fromJson(json['device_type'] as Map<String, dynamic>)
: null,
deviceTypeInfo: (json['device_type_info'] is Map<String, dynamic>)
? DeviceType.fromJson(json['device_type_info'] as Map<String, dynamic>)
: null,
deviceType: json['device_type'] == null
? null
: DeviceType.fromJson(json['device_type'] as Map<String, dynamic>),
deviceTypeInfo: json['device_type_info'] == null
? null
: DeviceType.fromJson(json['device_type_info'] as Map<String, dynamic>),
macAddress: json['mac_address'] as String?,
name: json['name'] as String? ?? '',
status: json['status'] as String? ?? 'in_stock',

View File

@ -21,6 +21,7 @@ class DeviceController extends _$DeviceController {
Future<bool> bindDevice(String sn, {int? spiritId}) async {
final repository = ref.read(deviceRepositoryProvider);
final result = await repository.bindDevice(sn, spiritId: spiritId);
if (!ref.mounted) return false;
return result.fold(
(failure) => false,
(bindingId) {
@ -33,6 +34,7 @@ class DeviceController extends _$DeviceController {
Future<bool> unbindDevice(int userDeviceId) async {
final repository = ref.read(deviceRepositoryProvider);
final result = await repository.unbindDevice(userDeviceId);
if (!ref.mounted) return false;
return result.fold(
(failure) => false,
(_) {
@ -48,6 +50,7 @@ class DeviceController extends _$DeviceController {
Future<bool> updateSpirit(int userDeviceId, int spiritId) async {
final repository = ref.read(deviceRepositoryProvider);
final result = await repository.updateSpirit(userDeviceId, spiritId);
if (!ref.mounted) return false;
return result.fold(
(failure) => false,
(updated) {
@ -78,6 +81,7 @@ class DeviceDetailController extends _$DeviceDetailController {
Future<bool> updateSettings(Map<String, dynamic> settings) async {
final repository = ref.read(deviceRepositoryProvider);
final result = await repository.updateSettings(userDeviceId, settings);
if (!ref.mounted) return false;
return result.fold(
(failure) => false,
(_) {
@ -90,6 +94,7 @@ class DeviceDetailController extends _$DeviceDetailController {
Future<bool> configWifi(String ssid) async {
final repository = ref.read(deviceRepositoryProvider);
final result = await repository.configWifi(userDeviceId, ssid);
if (!ref.mounted) return false;
return result.fold(
(failure) => false,
(_) {

View File

@ -36,7 +36,7 @@ final class DeviceControllerProvider
DeviceController create() => DeviceController();
}
String _$deviceControllerHash() => r'9b39117bd54964ba0035aad0eca10250454efaa7';
String _$deviceControllerHash() => r'3f73a13c7f93fecb9fe781efc4ee305b6186639e';
///

View File

@ -1,17 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../widgets/animated_gradient_background.dart';
import '../widgets/ios_toast.dart';
import '../features/device/presentation/controllers/device_controller.dart';
import '../features/device/domain/entities/device.dart';
class ProductSelectionPage extends StatefulWidget {
class ProductSelectionPage extends ConsumerStatefulWidget {
const ProductSelectionPage({super.key});
@override
State<ProductSelectionPage> createState() => _ProductSelectionPageState();
ConsumerState<ProductSelectionPage> createState() => _ProductSelectionPageState();
}
class _ProductSelectionPageState extends State<ProductSelectionPage> {
class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
final ScrollController _scrollController = ScrollController();
@override
@ -28,12 +31,30 @@ class _ProductSelectionPageState extends State<ProductSelectionPage> {
super.dispose();
}
/// ID product_code
static const Map<String, List<String>> _productCodeMap = {
'capybara': ['KPBL-ON'],
'badge-ai': ['DZBJ-ON'],
'badge-basic': ['DZBJ-OFF'],
};
///
UserDevice? _findBoundDevice(String productId, List<UserDevice> devices) {
final codes = _productCodeMap[productId];
if (codes == null || codes.isEmpty) return null;
for (final device in devices) {
final dt = device.device.deviceType ?? device.device.deviceTypeInfo;
if (dt != null && codes.contains(dt.productCode)) {
return device;
}
}
return null;
}
static final List<Map<String, dynamic>> _products = [
{
'id': 'capybara',
'name': '毛绒机芯',
'status': '已连接',
'statusColor': const Color(0xFF10B981),
'icon': 'assets/www/Capybara.png',
'isPng': true,
'hasTag': true,
@ -45,13 +66,10 @@ class _ProductSelectionPageState extends State<ProductSelectionPage> {
end: Alignment.centerRight,
),
'shadowColor': const Color(0xFFC9A07A),
'selected': true,
},
{
'id': 'badge-ai',
'name': '电子吧唧 AI',
'status': '离线',
'statusColor': const Color(0xFFE5E7EB),
'icon': 'assets/www/icons/icon-product-badge.svg',
'isPng': false,
'hasTag': true,
@ -63,13 +81,10 @@ class _ProductSelectionPageState extends State<ProductSelectionPage> {
end: Alignment.centerRight,
),
'shadowColor': const Color(0xFF6366F1),
'selected': false,
},
{
'id': 'badge-basic',
'name': '普通吧唧',
'status': '未配对',
'statusColor': const Color(0xFFE5E7EB),
'icon': 'assets/www/icons/icon-product-badge.svg',
'isPng': false,
'hasTag': false,
@ -80,13 +95,10 @@ class _ProductSelectionPageState extends State<ProductSelectionPage> {
end: Alignment.centerRight,
),
'shadowColor': const Color(0xFFA78BFA),
'selected': false,
},
{
'id': 'bracelet',
'name': 'AI 手链',
'status': '点击扫描',
'statusColor': const Color(0xFFE5E7EB),
'icon': 'assets/www/icons/icon-product-badge.svg',
'isPng': false,
'hasTag': true,
@ -98,13 +110,10 @@ class _ProductSelectionPageState extends State<ProductSelectionPage> {
end: Alignment.centerRight,
),
'shadowColor': const Color(0xFFE07B54),
'selected': false,
},
{
'id': 'vsinger',
'name': '洛天依',
'status': '去下载专属 APP →',
'statusColor': Colors.transparent,
'icon': 'assets/www/icons/icon-product-luo.svg',
'isPng': false,
'hasTag': false,
@ -115,13 +124,10 @@ class _ProductSelectionPageState extends State<ProductSelectionPage> {
end: Alignment.centerRight,
),
'shadowColor': const Color(0xFF2DD4BF),
'selected': false,
},
{
'id': 'nightlight',
'name': 'AI 星空夜灯',
'status': '未配对',
'statusColor': const Color(0xFFE5E7EB),
'icon': 'assets/www/icons/icon-product-badge.svg',
'isPng': false,
'hasTag': true,
@ -133,13 +139,10 @@ class _ProductSelectionPageState extends State<ProductSelectionPage> {
end: Alignment.centerRight,
),
'shadowColor': const Color(0xFF7C3AED),
'selected': false,
},
{
'id': 'feeder',
'name': '智能喂食器',
'status': '点击扫描',
'statusColor': const Color(0xFFE5E7EB),
'icon': 'assets/www/icons/icon-product-badge.svg',
'isPng': false,
'hasTag': false,
@ -150,24 +153,24 @@ class _ProductSelectionPageState extends State<ProductSelectionPage> {
end: Alignment.centerRight,
),
'shadowColor': const Color(0xFFE11D48),
'selected': false,
},
];
@override
Widget build(BuildContext context) {
final safeTop = MediaQuery.of(context).padding.top;
// = safeArea + padding + + padding
final headerHeight = safeTop + 12 + 40 + 12;
//
final devicesAsync = ref.watch(deviceControllerProvider);
final devices = devicesAsync.value ?? [];
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
// 1.
const AnimatedGradientBackground(),
// 2.
ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
scrollbars: false,
@ -178,17 +181,28 @@ class _ProductSelectionPageState extends State<ProductSelectionPage> {
itemCount: _products.length,
separatorBuilder: (_, __) => const SizedBox(height: 16),
itemBuilder: (context, index) {
final product = _products[index];
final boundDevice = _findBoundDevice(product['id'] as String, devices);
return _FadeOnScrollCard(
key: ValueKey(_products[index]['id']),
product: _products[index],
key: ValueKey(product['id']),
product: product,
isBound: boundDevice != null,
fadeStartY: headerHeight + 16,
fadeEndY: safeTop,
onTap: () {
if (boundDevice != null) {
//
context.go('/device-control');
} else {
//
context.go('/bluetooth');
}
},
);
},
),
),
// 3. +
Positioned(
top: 0,
left: 0,
@ -198,7 +212,6 @@ class _ProductSelectionPageState extends State<ProductSelectionPage> {
child: Stack(
alignment: Alignment.center,
children: [
//
Text(
'选择产品',
style: GoogleFonts.outfit(
@ -207,7 +220,6 @@ class _ProductSelectionPageState extends State<ProductSelectionPage> {
color: const Color(0xFF1F2937),
),
),
//
Align(
alignment: Alignment.centerLeft,
child: GestureDetector(
@ -238,17 +250,21 @@ class _ProductSelectionPageState extends State<ProductSelectionPage> {
}
}
/// Opacity ShaderMask widget
///
class _FadeOnScrollCard extends StatefulWidget {
final Map<String, dynamic> product;
final double fadeStartY; //
final double fadeEndY; //
final bool isBound;
final double fadeStartY;
final double fadeEndY;
final VoidCallback onTap;
const _FadeOnScrollCard({
super.key,
required this.product,
required this.isBound,
required this.fadeStartY,
required this.fadeEndY,
required this.onTap,
});
@override
@ -279,7 +295,11 @@ class _FadeOnScrollCardState extends State<_FadeOnScrollCard> {
return Opacity(
key: _posKey,
opacity: opacity,
child: _ProductCard(product: widget.product),
child: _ProductCard(
product: widget.product,
isBound: widget.isBound,
onTap: widget.onTap,
),
);
}
}
@ -287,22 +307,20 @@ class _FadeOnScrollCardState extends State<_FadeOnScrollCard> {
///
class _ProductCard extends StatelessWidget {
final Map<String, dynamic> product;
final bool isBound;
final VoidCallback onTap;
const _ProductCard({super.key, required this.product});
const _ProductCard({
super.key,
required this.product,
required this.isBound,
required this.onTap,
});
@override
Widget build(BuildContext context) {
bool isSelected = product['selected'] == true;
return GestureDetector(
onTap: () {
if (isSelected) {
Navigator.of(context).pop();
} else {
AppToast.show(
context, '${product['name']} 离线或未配对', isError: true);
}
},
onTap: onTap,
child: Container(
height: 120,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
@ -343,18 +361,16 @@ class _ProductCard extends StatelessWidget {
const SizedBox(height: 6),
Row(
children: [
if ((product['statusColor'] as Color) !=
Colors.transparent)
Container(
width: 7,
height: 7,
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
color: product['id'] == 'capybara'
color: isBound
? const Color(0xFF34D399)
: Colors.white.withOpacity(0.5),
shape: BoxShape.circle,
boxShadow: product['id'] == 'capybara'
boxShadow: isBound
? [
BoxShadow(
color: const Color(0xFF34D399)
@ -365,7 +381,7 @@ class _ProductCard extends StatelessWidget {
),
),
Text(
product['status'],
isBound ? '已连接' : '点击配对',
style: TextStyle(
fontSize: 13,
color: Colors.white.withOpacity(0.85)),

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'product_selection_page.dart';
import '../widgets/glass_dialog.dart';
import '../widgets/animated_gradient_background.dart';
import '../widgets/ios_toast.dart';
@ -494,10 +493,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
.unbindDevice(_userDeviceId!);
if (mounted) {
if (success) {
Navigator.pop(context); // close settings
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const ProductSelectionPage()),
);
context.go('/product-selection');
} else {
AppToast.show(context, '解绑失败', isError: true);
}

View File

@ -132,10 +132,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
@ -644,14 +644,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
json_annotation:
dependency: "direct main"
description:
@ -736,18 +728,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
@ -1205,26 +1197,26 @@ packages:
dependency: transitive
description:
name: test
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a"
url: "https://pub.dev"
source: hosted
version: "1.26.3"
version: "1.29.0"
test_api:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev"
source: hosted
version: "0.7.7"
version: "0.7.9"
test_core:
dependency: transitive
description:
name: test_core
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943"
url: "https://pub.dev"
source: hosted
version: "0.6.12"
version: "0.6.15"
typed_data:
dependency: transitive
description: