add first version

This commit is contained in:
2026-03-02 22:57:35 +08:00
parent d517195df7
commit d9631fbb80
17 changed files with 1631 additions and 93 deletions

View File

@@ -0,0 +1,79 @@
import 'dart:typed_data';
import 'package:dio/dio.dart';
import '../utils/app_config.dart';
class ApiService {
late final Dio _dio;
ApiService() {
_dio = Dio(BaseOptions(
baseUrl: AppConfig.apiBaseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {
'Content-Type': 'application/json',
},
));
// 添加日志拦截器
_dio.interceptors.add(LogInterceptor(
requestBody: false, // 不打印请求体(图片太大)
responseBody: true,
error: true,
));
}
/// 上传图像到后端
Future<Map<String, dynamic>> uploadImage(Uint8List imageBytes) async {
try {
final formData = FormData.fromMap({
'file': MultipartFile.fromBytes(
imageBytes,
filename: 'image_${DateTime.now().millisecondsSinceEpoch}.jpg',
),
});
final response = await _dio.post(
'/api/v1/images/upload',
data: formData,
);
return response.data;
} catch (e) {
print('上传图像失败: $e');
rethrow;
}
}
/// 请求 AI 分析
Future<Map<String, dynamic>> analyzeImage(String imageUrl) async {
try {
final response = await _dio.post(
'/api/v1/analysis/analyze',
data: {'image_url': imageUrl},
);
return response.data;
} catch (e) {
print('分析图像失败: $e');
rethrow;
}
}
/// 一步到位:上传并分析
Future<Map<String, dynamic>> uploadAndAnalyze(Uint8List imageBytes) async {
try {
// 1. 上传图像
final uploadResult = await uploadImage(imageBytes);
final imageUrl = uploadResult['url'];
// 2. 请求分析
final analysisResult = await analyzeImage(imageUrl);
return analysisResult;
} catch (e) {
print('上传并分析失败: $e');
rethrow;
}
}
}

View File

@@ -0,0 +1,158 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:image/image.dart' as img;
import '../utils/app_config.dart';
class CameraService extends ChangeNotifier {
CameraController? _controller;
List<CameraDescription> _cameras = [];
bool _isInitialized = false;
bool _isStreaming = false;
Timer? _captureTimer;
CameraController? get controller => _controller;
bool get isInitialized => _isInitialized;
bool get isStreaming => _isStreaming;
/// 初始化相机
Future<void> initialize() async {
try {
_cameras = await availableCameras();
if (_cameras.isEmpty) {
print('没有可用的相机');
return;
}
// 使用后置摄像头
final camera = _cameras.firstWhere(
(camera) => camera.lensDirection == CameraLensDirection.back,
orElse: () => _cameras.first,
);
_controller = CameraController(
camera,
ResolutionPreset.medium, // 中等分辨率,平衡质量和性能
enableAudio: false,
imageFormatGroup: ImageFormatGroup.jpeg,
);
await _controller!.initialize();
_isInitialized = true;
notifyListeners();
print('相机初始化成功');
} catch (e) {
print('相机初始化失败: $e');
_isInitialized = false;
}
}
/// 拍摄单张照片
Future<Uint8List?> takePicture() async {
if (!_isInitialized || _controller == null) return null;
try {
final XFile image = await _controller!.takePicture();
final bytes = await image.readAsBytes();
// 压缩图像
final compressedBytes = await _compressImage(bytes);
return compressedBytes;
} catch (e) {
print('拍照失败: $e');
return null;
}
}
/// 开始实时捕获(定时拍照)
void startStreaming(Function(Uint8List) onImageCaptured) {
if (_isStreaming) return;
_isStreaming = true;
notifyListeners();
_captureTimer = Timer.periodic(AppConfig.captureInterval, (timer) async {
if (!_isStreaming) {
timer.cancel();
return;
}
final imageBytes = await takePicture();
if (imageBytes != null) {
onImageCaptured(imageBytes);
}
});
print('开始实时捕获,间隔: ${AppConfig.captureInterval.inMilliseconds}ms');
}
/// 停止实时捕获
void stopStreaming() {
_isStreaming = false;
_captureTimer?.cancel();
_captureTimer = null;
notifyListeners();
print('停止实时捕获');
}
/// 压缩图像
Future<Uint8List> _compressImage(Uint8List bytes) async {
return await compute(_compressImageIsolate, bytes);
}
/// 在独立 Isolate 中压缩图像(避免阻塞 UI
static Uint8List _compressImageIsolate(Uint8List bytes) {
// 解码图像
img.Image? image = img.decodeImage(bytes);
if (image == null) return bytes;
// 调整大小
if (image.width > AppConfig.imageMaxWidth ||
image.height > AppConfig.imageMaxHeight) {
image = img.copyResize(
image,
width: AppConfig.imageMaxWidth,
height: AppConfig.imageMaxHeight,
);
}
// 压缩为 JPEG
final compressed = img.encodeJpg(image, quality: AppConfig.imageQuality);
print('图像压缩: ${bytes.length} bytes -> ${compressed.length} bytes');
return Uint8List.fromList(compressed);
}
/// 切换摄像头
Future<void> switchCamera() async {
if (_cameras.length < 2) return;
final currentLens = _controller?.description.lensDirection;
final newCamera = _cameras.firstWhere(
(camera) => camera.lensDirection != currentLens,
orElse: () => _cameras.first,
);
await _controller?.dispose();
_controller = CameraController(
newCamera,
ResolutionPreset.medium,
enableAudio: false,
imageFormatGroup: ImageFormatGroup.jpeg,
);
await _controller!.initialize();
notifyListeners();
}
/// 释放资源
@override
void dispose() {
stopStreaming();
_controller?.dispose();
super.dispose();
}
}