feat(client): 商品详情页支持拍照添加照片(Android/iOS)

- 移动端加图改用 image_picker,底部弹出「拍照 / 从相册选择」二选一;
  桌面/Web 保持 file_picker 文件选择
- 用 defaultTargetPlatform 判断移动端,避免在参与 Web 编译的文件引入 dart:io
- iOS:Info.plist 增加 NSCameraUsageDescription / NSPhotoLibraryUsageDescription
- Android:拍照走系统相机、相册走 Photo Picker,均无需声明权限

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-07 14:47:08 +08:00
parent 5278bea940
commit 211fafc7e1
7 changed files with 165 additions and 4 deletions
@@ -1,6 +1,7 @@
import '../../core/utils/dialog_util.dart';
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:image_picker/image_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -91,7 +92,56 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
}
}
// 移动端(Android/iOS)支持拍照,用 image_picker;桌面/Web 用 file_picker 选文件。
// 用 defaultTargetPlatform 而非 dart:io 的 Platform,避免在参与 Web 编译的文件里引入 dart:io。
bool get _isMobile =>
!kIsWeb &&
(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS);
Future<void> _pickAndUpload() async {
if (_isMobile) {
await _pickFromCameraOrGallery();
} else {
await _pickFromFiles();
}
}
/// 移动端:底部弹出「拍照 / 从相册选择」
Future<void> _pickFromCameraOrGallery() async {
final source = await showModalBottomSheet<ImageSource>(
context: context,
builder: (ctx) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.camera_alt_outlined),
title: const Text('拍照'),
onTap: () => Navigator.pop(ctx, ImageSource.camera),
),
ListTile(
leading: const Icon(Icons.photo_library_outlined),
title: const Text('从相册选择'),
onTap: () => Navigator.pop(ctx, ImageSource.gallery),
),
],
),
),
);
if (source == null) return;
final XFile? xfile = await ImagePicker().pickImage(
source: source,
maxWidth: 2000,
imageQuality: 85,
);
if (xfile == null) return;
await _uploadImage(filePath: xfile.path, fileName: xfile.name);
}
/// 桌面 / Web:文件选择器
Future<void> _pickFromFiles() async {
final result = await FilePicker.platform.pickFiles(
type: FileType.image,
allowMultiple: false,
@@ -102,13 +152,17 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
final String? filePath = kIsWeb ? null : file.path;
final fileBytes = file.bytes;
if (filePath == null && fileBytes == null) return;
await _uploadImage(
filePath: filePath, bytes: fileBytes, fileName: file.name);
}
Future<void> _uploadImage(
{String? filePath, Uint8List? bytes, required String fileName}) async {
setState(() => _uploading = true);
try {
final img = await ref
.read(productRepositoryProvider)
.uploadImage(_product!.id, filePath,
bytes: fileBytes, fileName: file.name);
final img = await ref.read(productRepositoryProvider).uploadImage(
_product!.id, filePath,
bytes: bytes, fileName: fileName);
_updateImages([..._product!.images, img]);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(