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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user