diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index fb52aaf..c2476be 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -1,77 +1,94 @@
- import UIKit
- import Flutter
+import UIKit
+import Flutter
+import AVFoundation
- @main
- @objc class AppDelegate: FlutterAppDelegate {
- // 动态方向掩码(默认竖屏)
- static var orientationMask: UIInterfaceOrientationMask = .portrait
+@main
+@objc class AppDelegate: FlutterAppDelegate {
+ // 动态方向掩码(默认竖屏)
+ static var orientationMask: UIInterfaceOrientationMask = .portrait
- override func application(
- _ application: UIApplication,
- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
- ) -> Bool {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
- let controller = window?.rootViewController as! FlutterViewController
- let channel = FlutterMethodChannel(name: "app.orientation",
+ let controller = window?.rootViewController as! FlutterViewController
+
+ // 1) orientation channel (你已有)
+ let orientationChannel = FlutterMethodChannel(name: "app.orientation",
binaryMessenger: controller.binaryMessenger)
+ orientationChannel.setMethodCallHandler { [weak self] call, result in
+ guard let self = self else { return }
- channel.setMethodCallHandler { [weak self] call, result in
- guard let self = self else { return }
-
- if call.method == "setOrientation" {
- guard let arg = call.arguments as? String else {
- result(FlutterError(code: "BAD_ARGS", message: "need 'landscape' | 'portrait'", details: nil))
- return
- }
-
- // 先更新允许的方向掩码
- if arg == "landscape" {
- AppDelegate.orientationMask = .landscape
- } else if arg == "portrait" {
- AppDelegate.orientationMask = .portrait
- } else {
- result(FlutterError(code: "BAD_ARGS", message: "unknown arg", details: nil))
- return
- }
-
-
- // 再请求实际旋转
- if #available(iOS 16.0, *) {
- // 通知顶层 VC:其 supportedInterfaceOrientations 需要刷新
- self.window?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
-
- if let scene = self.window?.windowScene {
- let orientations: UIInterfaceOrientationMask =
- (arg == "landscape") ? .landscape : .portrait
- do {
- try scene.requestGeometryUpdate(.iOS(interfaceOrientations: orientations))
- } catch {
- result(FlutterError(code: "GEOMETRY_UPDATE_FAILED",
- message: error.localizedDescription, details: nil))
- return
- }
- }
- } else {
- let target: UIInterfaceOrientation =
- (arg == "landscape") ? .landscapeLeft : .portrait
- UIDevice.current.setValue(target.rawValue, forKey: "orientation")
- UIViewController.attemptRotationToDeviceOrientation()
- }
-
- result(true)
- } else {
- result(FlutterMethodNotImplemented)
- }
+ if call.method == "setOrientation" {
+ guard let arg = call.arguments as? String else {
+ result(FlutterError(code: "BAD_ARGS", message: "need 'landscape' | 'portrait'", details: nil))
+ return
}
- GeneratedPluginRegistrant.register(with: self)
- return super.application(application, didFinishLaunchingWithOptions: launchOptions)
- }
+ // 先更新允许的方向掩码
+ if arg == "landscape" {
+ AppDelegate.orientationMask = .landscape
+ } else if arg == "portrait" {
+ AppDelegate.orientationMask = .portrait
+ } else {
+ result(FlutterError(code: "BAD_ARGS", message: "unknown arg", details: nil))
+ return
+ }
- // 关键:把当前的掩码提供给系统
- override func application(_ application: UIApplication,
- supportedInterfaceOrientationsFor window: UIWindow?)
- -> UIInterfaceOrientationMask {
- return AppDelegate.orientationMask
+ // 再请求实际旋转
+ if #available(iOS 16.0, *) {
+ // 通知顶层 VC:其 supportedInterfaceOrientations 需要刷新
+ self.window?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
+
+ if let scene = self.window?.windowScene {
+ let orientations: UIInterfaceOrientationMask =
+ (arg == "landscape") ? .landscape : .portrait
+ do {
+ try scene.requestGeometryUpdate(.iOS(interfaceOrientations: orientations))
+ } catch {
+ result(FlutterError(code: "GEOMETRY_UPDATE_FAILED",
+ message: error.localizedDescription, details: nil))
+ return
+ }
+ }
+ } else {
+ let target: UIInterfaceOrientation =
+ (arg == "landscape") ? .landscapeLeft : .portrait
+ UIDevice.current.setValue(target.rawValue, forKey: "orientation")
+ UIViewController.attemptRotationToDeviceOrientation()
+ }
+
+ result(true)
+ } else {
+ result(FlutterMethodNotImplemented)
}
}
+
+ // 2) permissions channel (新增): 用于 iOS 原生请求相机访问权限
+ let permissionChannel = FlutterMethodChannel(name: "qhd_prevention/permissions",
+ binaryMessenger: controller.binaryMessenger)
+ permissionChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
+ if call.method == "requestCameraAccess" {
+ // AVCaptureDevice.requestAccess 会在首次调用时触发系统权限弹窗
+ AVCaptureDevice.requestAccess(for: .video) { granted in
+ DispatchQueue.main.async {
+ result(granted)
+ }
+ }
+ } else {
+ result(FlutterMethodNotImplemented)
+ }
+ }
+
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+
+ // 关键:把当前的掩码提供给系统
+ override func application(_ application: UIApplication,
+ supportedInterfaceOrientationsFor window: UIWindow?)
+ -> UIInterfaceOrientationMask {
+ return AppDelegate.orientationMask
+ }
+}
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 29fd9ff..97cc4a1 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -1,98 +1,98 @@
-
- CADisableMinimumFrameDurationOnPhone
-
- CFBundleDisplayName
- 智守安全
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- 智守安全
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- LSApplicationQueriesSchemes
-
- baidumap
-
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSRequiresIPhoneOS
-
- NFCReaderUsageDescription
- 需要NFC权限来读取和写入标签
- NSAppTransportSecurity
- NSAllowsArbitraryLoads
+ CADisableMinimumFrameDurationOnPhone
+ CFBundleDisplayName
+ 智守安全
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ 智守安全
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ LSApplicationQueriesSchemes
+
+ baidumap
+
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ NFCReaderUsageDescription
+ 需要NFC权限来读取和写入标签
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+ NSBluetoothAlwaysUsageDescription
+ app需要蓝牙权限连接设备
+ NSCameraUsageDescription
+ app需要相机权限来扫描二维码
+ NSContactsUsageDescription
+ app需要通讯录权限添加好友
+ NSHealthShareUsageDescription
+ app需要读取健康数据
+ NSHealthUpdateUsageDescription
+ app需要写入健康数据
+ NSLocalNetworkUsageDescription
+ app需要发现本地网络设备
+ NSLocationAlwaysAndWhenInUseUsageDescription
+ app需要后台定位以实现持续跟踪
+ NSLocationAlwaysUsageDescription
+ 需要位置权限以提供定位服务
+ NSLocationWhenInUseUsageDescription
+ 需要位置权限以提供定位服务
+ NSMicrophoneUsageDescription
+ app需要麦克风权限进行语音通话
+ NSMotionUsageDescription
+ app需要访问运动数据统计步数
+ NSPhotoLibraryAddUsageDescription
+ app需要保存图片到相册
+ NSPhotoLibraryUsageDescription
+ app需要访问相册以上传图片
+ NSUserNotificationsUsageDescription
+ app需要发送通知提醒重要信息
+ UIApplicationSupportsIndirectInputEvents
+
+ UIBackgroundModes
+
+ remote-notification
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UIStatusBarHidden
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ com.apple.developer.nfc.readersession.formats
+
+ NDEF
+ TAG
+
- NSBluetoothAlwaysUsageDescription
- app需要蓝牙权限连接设备
- NSCameraUsageDescription
- app需要相机权限来扫描二维码
- NSContactsUsageDescription
- app需要通讯录权限添加好友
- NSHealthShareUsageDescription
- app需要读取健康数据
- NSHealthUpdateUsageDescription
- app需要写入健康数据
- NSLocalNetworkUsageDescription
- app需要发现本地网络设备
- NSLocationAlwaysAndWhenInUseUsageDescription
- app需要后台定位以实现持续跟踪
- NSLocationAlwaysUsageDescription
- 需要位置权限以提供定位服务
- NSLocationWhenInUseUsageDescription
- 需要位置权限以提供定位服务
- NSMicrophoneUsageDescription
- app需要麦克风权限进行语音通话
- NSMotionUsageDescription
- app需要访问运动数据统计步数
- NSPhotoLibraryAddUsageDescription
- app需要保存图片到相册
- NSPhotoLibraryUsageDescription
- app需要访问相册以上传图片
- NSUserNotificationsUsageDescription
- app需要发送通知提醒重要信息
- UIApplicationSupportsIndirectInputEvents
-
- UIBackgroundModes
-
- remote-notification
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UIStatusBarHidden
-
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- com.apple.developer.nfc.readersession.formats
-
- NDEF
- TAG
-
-
diff --git a/lib/customWidget/photo_picker_row.dart b/lib/customWidget/photo_picker_row.dart
index c3c20f8..d4ae8a1 100644
--- a/lib/customWidget/photo_picker_row.dart
+++ b/lib/customWidget/photo_picker_row.dart
@@ -1,5 +1,7 @@
+import 'dart:async';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
+import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
@@ -15,11 +17,73 @@ import 'package:photo_manager/photo_manager.dart';
import 'package:path/path.dart' as p;
import 'ItemWidgetFactory.dart';
+const String kAcceptVideoSectionKey = 'accept_video';
+
+/// ---------------------- 全局 MediaBus (轻量事件总线) ----------------------
+class MediaEvent {
+ final String key;
+ final MediaEventType type;
+ final List? paths;
+
+ MediaEvent._(this.key, this.type, [this.paths]);
+
+ factory MediaEvent.clear(String key) => MediaEvent._(key, MediaEventType.clear);
+
+ factory MediaEvent.set(String key, List paths) =>
+ MediaEvent._(key, MediaEventType.set, List.from(paths));
+}
+
+enum MediaEventType { clear, set }
+
+class MediaBus {
+ MediaBus._internal();
+ static final MediaBus _instance = MediaBus._internal();
+ factory MediaBus() => _instance;
+
+ final StreamController _ctrl = StreamController.broadcast();
+
+ Stream get stream => _ctrl.stream;
+
+ void emit(MediaEvent ev) {
+ if (!_ctrl.isClosed) _ctrl.add(ev);
+ }
+
+ Future dispose() async {
+ await _ctrl.close();
+ }
+}
+/// ---------------------- /MediaBus ----------------------
+
+
/// 媒体选择类型
enum MediaType { image, video }
-/// 纵向滚动四列的媒体添加组件,支持拍摄、全屏相册多选,以及初始地址列表展示
-/// 新增 isEdit 属性控制编辑状态
+/// ---------- 辅助函数(文件顶部复用) ----------
+bool _isNetworkPath(String? p) {
+ if (p == null) return false;
+ final s = p.trim().toLowerCase();
+ return s.startsWith('http://') || s.startsWith('https://');
+}
+
+/// 把路径列表转换为存在的本地 File 列表(过滤掉网络路径与空路径与不存在的本地文件)
+List _localFilesFromPaths(List? paths) {
+ if (paths == null) return [];
+ return paths
+ .map((e) => (e ?? '').toString().trim())
+ .where((s) => s.isNotEmpty && !_isNetworkPath(s))
+ .where((s) => File(s).existsSync()) // 只回调真实存在的本地文件
+ .map((s) => File(s))
+ .toList();
+}
+
+/// 规范化路径列表:trim + 过滤空字符串
+List _normalizePaths(List? src) {
+ if (src == null) return [];
+ return src.map((e) => (e ?? '').toString().trim()).where((s) => s.isNotEmpty).toList();
+}
+
+/// ---------- MediaPickerRow ----------
+/// ---------- MediaPickerRow ----------
class MediaPickerRow extends StatefulWidget {
final int maxCount;
final MediaType mediaType;
@@ -27,9 +91,11 @@ class MediaPickerRow extends StatefulWidget {
final ValueChanged> onChanged;
final ValueChanged? onMediaAdded;
final ValueChanged? onMediaRemoved;
- final ValueChanged? onMediaTapped; // 新增:媒体点击回调
- final bool isEdit; // 新增:控制编辑状态
- final bool isCamera; // 新增:只能拍照
+ final ValueChanged? onMediaTapped;
+ final bool isEdit;
+ final bool isCamera;
+ /// 默认 false —— 仅在 initState 时读取 initialMediaPaths
+ final bool followInitialUpdates;
const MediaPickerRow({
Key? key,
@@ -39,9 +105,10 @@ class MediaPickerRow extends StatefulWidget {
required this.onChanged,
this.onMediaAdded,
this.onMediaRemoved,
- this.onMediaTapped, // 新增
- this.isEdit = true, // 默认可编辑
+ this.onMediaTapped,
+ this.isEdit = true,
this.isCamera = false,
+ this.followInitialUpdates = false, // 默认 false
}) : super(key: key);
@override
@@ -51,25 +118,41 @@ class MediaPickerRow extends StatefulWidget {
class _MediaPickerGridState extends State {
final ImagePicker _picker = ImagePicker();
late List _mediaPaths;
- bool _isProcessing = false; // 转码或处理时显示 loading
+ bool _isProcessing = false;
@override
void initState() {
super.initState();
- _mediaPaths =
- widget.initialMediaPaths != null
- ? widget.initialMediaPaths!.take(widget.maxCount).toList()
- : [];
- WidgetsBinding.instance.addPostFrameCallback((_) {
- widget.onChanged(
- _mediaPaths
- .map((p) => p.startsWith('http') ? File('') : File(p))
- .toList(),
- );
- });
+ // 初始化内部路径(保留网络路径与本地路径)
+ _mediaPaths = _normalizePaths(widget.initialMediaPaths).take(widget.maxCount).toList();
+
+ // 仅在存在本地真实文件时才把 File 列表回调给外部(避免父组件用这个回调覆盖只有网络路径的数据)
+ final initialLocalFiles = _localFilesFromPaths(_mediaPaths);
+ if (initialLocalFiles.isNotEmpty) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (!mounted) return;
+ widget.onChanged(initialLocalFiles);
+ });
+ }
+ }
+
+ @override
+ void didUpdateWidget(covariant MediaPickerRow oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (widget.followInitialUpdates) {
+ final oldList = _normalizePaths(oldWidget.initialMediaPaths);
+ final newList = _normalizePaths(widget.initialMediaPaths);
+
+ if (!listEquals(oldList, newList)) {
+ _mediaPaths = newList.take(widget.maxCount).toList();
+ if (mounted) setState(() {});
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ widget.onChanged(_localFilesFromPaths(_mediaPaths));
+ });
+ }
+ }
}
- // 公共:当得到本地媒体路径时(可能是 mov/avi 等),需要在这里统一处理(转码、入队、回调)
Future _handlePickedPath(String path) async {
if (!mounted) return;
if (path.isEmpty) return;
@@ -77,7 +160,6 @@ class _MediaPickerGridState extends State {
try {
String finalPath = path;
- // 如果是视频并且不是 mp4,则调用 video_compress 转码
if (widget.mediaType == MediaType.video) {
final ext = p.extension(path).toLowerCase();
if (ext != '.mp4') {
@@ -106,10 +188,10 @@ class _MediaPickerGridState extends State {
}
}
- // 添加到列表
if (_mediaPaths.length < widget.maxCount) {
setState(() => _mediaPaths.add(finalPath));
- widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
+ // 回调仅包含本地真实存在的文件(网络路径不会出现在此回调中)
+ widget.onChanged(_localFilesFromPaths(_mediaPaths));
widget.onMediaAdded?.call(finalPath);
}
} catch (e) {
@@ -118,54 +200,41 @@ class _MediaPickerGridState extends State {
}
Future _showPickerOptions() async {
- if (!widget.isEdit) return; // 不可编辑时直接返回
+ if (!widget.isEdit) return;
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
- builder:
- (_) => SafeArea(
- child: Wrap(
- children: [
- ListTile(
- titleAlignment: ListTileTitleAlignment.center,
- leading: Icon(
- widget.mediaType == MediaType.image
- ? Icons.camera_alt
- : Icons.videocam,
- ),
- title: Text(
- widget.mediaType == MediaType.image ? '拍照' : '拍摄视频',
- ),
- onTap: () {
- Navigator.of(context).pop();
- _pickCamera();
- },
- ),
- ListTile(
- titleAlignment: ListTileTitleAlignment.center,
- leading: Icon(
- widget.mediaType == MediaType.image
- ? Icons.photo_library
- : Icons.video_library,
- ),
- title: Text(
- widget.mediaType == MediaType.image ? '从相册选择' : '从相册选择视频',
- ),
- onTap: () {
- Navigator.of(context).pop();
- _pickGallery();
- },
- ),
- ListTile(
- titleAlignment: ListTileTitleAlignment.center,
- leading: const Icon(Icons.close),
- title: const Text('取消'),
- onTap: () => Navigator.of(context).pop(),
- ),
- ],
+ builder: (_) => SafeArea(
+ child: Wrap(
+ children: [
+ ListTile(
+ titleAlignment: ListTileTitleAlignment.center,
+ leading: Icon(widget.mediaType == MediaType.image ? Icons.camera_alt : Icons.videocam),
+ title: Text(widget.mediaType == MediaType.image ? '拍照' : '拍摄视频'),
+ onTap: () {
+ Navigator.of(context).pop();
+ _pickCamera();
+ },
),
- ),
+ ListTile(
+ titleAlignment: ListTileTitleAlignment.center,
+ leading: Icon(widget.mediaType == MediaType.image ? Icons.photo_library : Icons.video_library),
+ title: Text(widget.mediaType == MediaType.image ? '从相册选择' : '从相册选择视频'),
+ onTap: () {
+ Navigator.of(context).pop();
+ _pickGallery();
+ },
+ ),
+ ListTile(
+ titleAlignment: ListTileTitleAlignment.center,
+ leading: const Icon(Icons.close),
+ title: const Text('取消'),
+ onTap: () => Navigator.of(context).pop(),
+ ),
+ ],
+ ),
+ ),
);
}
@@ -179,7 +248,7 @@ class _MediaPickerGridState extends State {
if (picked != null) {
final path = picked.path;
setState(() => _mediaPaths.add(path));
- widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
+ widget.onChanged(_localFilesFromPaths(_mediaPaths));
widget.onMediaAdded?.call(path);
}
} else {
@@ -193,31 +262,26 @@ class _MediaPickerGridState extends State {
}
}
- // 修改 _pickGallery 方法
Future _pickGallery() async {
if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return;
try {
- // iOS: 使用 PhotoManager.requestPermissionExtend() 唤起系统权限弹窗(支持 limited)
if (Platform.isIOS) {
final permission = await PhotoManager.requestPermissionExtend();
debugPrint('iOS photo permission state: $permission');
- if (permission != PermissionState.authorized &&
- permission != PermissionState.limited) {
+ if (permission != PermissionState.authorized && permission != PermissionState.limited) {
if (mounted) {
ToastUtil.showNormal(context, '请到设置中开启相册访问权限');
}
return;
}
- // 授权或 limited:继续打开 AssetPicker
final remaining = widget.maxCount - _mediaPaths.length;
final List? assets = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
- requestType:
- widget.mediaType == MediaType.image ? RequestType.image : RequestType.video,
+ requestType: widget.mediaType == MediaType.image ? RequestType.image : RequestType.video,
maxAssets: remaining,
gridCount: 4,
),
@@ -239,11 +303,9 @@ class _MediaPickerGridState extends State {
}
}
if (mounted) setState(() {});
- widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
+ widget.onChanged(_localFilesFromPaths(_mediaPaths));
}
-
} else {
- // Android: 更可靠地请求权限(适配不同 Android 版本)
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
final androidInfo = await deviceInfo.androidInfo;
final int sdkInt = androidInfo.version.sdkInt ?? 0;
@@ -251,28 +313,21 @@ class _MediaPickerGridState extends State {
PermissionStatus permissionStatus = PermissionStatus.denied;
if (sdkInt >= 33) {
- // Android 13+: 使用更细粒度的媒体权限
- // 如果应用只选图片,request photos;只选视频则 request videos;若两者都可能同时需要,可请求两个。
if (widget.mediaType == MediaType.image) {
- permissionStatus = await Permission.photos.request(); // maps to READ_MEDIA_IMAGES on Android
+ permissionStatus = await Permission.photos.request();
} else if (widget.mediaType == MediaType.video) {
- permissionStatus = await Permission.videos.request(); // maps to READ_MEDIA_VIDEO
+ permissionStatus = await Permission.videos.request();
} else {
- // 两者皆可(同时请求会合并成单个系统对话)
final statuses = await [Permission.photos, Permission.videos].request();
permissionStatus = statuses[Permission.photos] ?? statuses[Permission.videos] ?? PermissionStatus.denied;
}
} else if (sdkInt >= 30) {
- // Android 11/12: 通常仍然使用 READ_EXTERNAL_STORAGE;若需要“全部文件访问”,要 request manageExternalStorage(慎用)
permissionStatus = await Permission.storage.request();
} else {
- // Android 10 及以下
permissionStatus = await Permission.storage.request();
}
- // 处理结果
if (permissionStatus.isGranted) {
- // 有权限:继续打开 AssetPicker
final remaining = widget.maxCount - _mediaPaths.length;
final List? assets = await AssetPicker.pickAssets(
context,
@@ -299,10 +354,9 @@ class _MediaPickerGridState extends State {
}
}
if (mounted) setState(() {});
- widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
+ widget.onChanged(_localFilesFromPaths(_mediaPaths));
}
} else if (permissionStatus.isPermanentlyDenied) {
- // 用户点了“不再询问”或系统拒绝弹窗,跳转设置页并提示
if (mounted) {
ToastUtil.showNormal(context, '请到设置中开启相册访问权限');
}
@@ -318,25 +372,19 @@ class _MediaPickerGridState extends State {
} catch (e, st) {
debugPrint('相册选择失败: $e\n$st');
if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('相册选择失败')),
- );
+ ToastUtil.showNormal(context, '相册选择失败');
}
}
}
- // 修改拍照方法,使用 permission_handler 请求相机权限
Future _cameraAction() async {
if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return;
- // 请求相机权限
final PermissionStatus status = await Permission.camera.request();
if (status != PermissionStatus.granted) {
if (mounted) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(const SnackBar(content: Text('相机权限被拒绝')));
+ ToastUtil.showNormal(context, '相机权限被拒绝');
}
return;
}
@@ -346,11 +394,10 @@ class _MediaPickerGridState extends State {
if (picked != null) {
final path = picked.path;
setState(() => _mediaPaths.add(path));
- widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
+ widget.onChanged(_localFilesFromPaths(_mediaPaths));
widget.onMediaAdded?.call(path);
}
} else {
- // video from camera
XFile? picked = await _picker.pickVideo(source: ImageSource.camera);
if (picked != null) {
await _handlePickedPath(picked.path);
@@ -358,11 +405,8 @@ class _MediaPickerGridState extends State {
}
} catch (e) {
debugPrint('拍摄失败: $e');
- // 友好提示
if (mounted) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(const SnackBar(content: Text('拍摄失败,请检查相机权限或设备状态')));
+ ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('拍摄失败,请检查相机权限或设备状态')));
}
}
}
@@ -371,16 +415,22 @@ class _MediaPickerGridState extends State {
final ok = await CustomAlertDialog.showConfirm(
context,
title: '温馨提示',
- content:
- widget.mediaType == MediaType.image ? '确定要删除这张图片吗?' : '确定要删除这个视频吗?',
+ content: widget.mediaType == MediaType.image ? '确定要删除这张图片吗?' : '确定要删除这个视频吗?',
cancelText: '取消',
);
- if (ok) {
- final removed = _mediaPaths[index];
- setState(() => _mediaPaths.removeAt(index));
- widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
- widget.onMediaRemoved?.call(removed);
- }
+ if (!ok) return;
+
+ final removed = _mediaPaths[index];
+ final wasNetwork = _isNetworkPath(removed);
+
+ setState(() => _mediaPaths.removeAt(index));
+
+ // 始终通知 onMediaRemoved(用于父端业务逻辑)
+ widget.onMediaRemoved?.call(removed);
+
+ // 只有当本地文件集合发生变化时才触发 onChanged(避免因为删除网络路径导致父端把列表置空)
+ final localFiles = _localFilesFromPaths(_mediaPaths);
+ widget.onChanged(localFiles);
}
@override
@@ -401,72 +451,71 @@ class _MediaPickerGridState extends State {
),
itemCount: itemCount,
itemBuilder: (context, index) {
- // 显示媒体项
if (index < _mediaPaths.length) {
- final path = _mediaPaths[index];
- final isNetwork = path.startsWith('http');
+ final raw = (_mediaPaths[index] ?? '').toString().trim();
+ final isNetwork = _isNetworkPath(raw);
return GestureDetector(
- onTap: () => widget.onMediaTapped?.call(path),
+ onTap: () => widget.onMediaTapped?.call(raw),
child: Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: SizedBox.expand(
- // 让内容强制填满格子
- child:
- widget.mediaType == MediaType.image
- ? (isNetwork
- ? Image.network(
- path,
- fit: BoxFit.cover, // 铺满格子并裁剪
- )
- : Image.file(
- File(path),
- fit: BoxFit.cover, // 铺满格子并裁剪
- ))
- : Container(
- color: Colors.black12,
- child: const Center(
- child: Icon(
- Icons.videocam,
- color: Colors.white70,
- ),
- ),
- ),
+ child: raw.isEmpty
+ ? Container(
+ color: Colors.grey.shade200,
+ child: const Center(child: Icon(Icons.broken_image, size: 28, color: Colors.grey)),
+ )
+ : (widget.mediaType == MediaType.image
+ ? (isNetwork
+ ? Image.network(
+ raw,
+ fit: BoxFit.cover,
+ errorBuilder: (_, __, ___) => Container(
+ color: Colors.grey.shade200,
+ child: const Center(child: Icon(Icons.broken_image)),
+ ),
+ )
+ : (File(raw).existsSync()
+ ? Image.file(
+ File(raw),
+ fit: BoxFit.cover,
+ errorBuilder: (_, __, ___) => Container(
+ color: Colors.grey.shade200,
+ child: const Center(child: Icon(Icons.broken_image)),
+ ),
+ )
+ : Container(
+ color: Colors.grey.shade200,
+ child: const Center(child: Icon(Icons.broken_image)),
+ )))
+ : Container(
+ color: Colors.black12,
+ child: const Center(
+ child: Icon(Icons.videocam, color: Colors.white70),
+ ),
+ )),
),
),
-
- // 只在可编辑状态下显示删除按钮
if (widget.isEdit)
Positioned(
top: -15,
right: -15,
child: IconButton(
- icon: const Icon(
- Icons.cancel,
- size: 20,
- color: Colors.red,
- ),
+ icon: const Icon(Icons.cancel, size: 20, color: Colors.red),
onPressed: () => _removeMedia(index),
),
),
],
),
);
- }
- // 显示添加按钮
- else if (showAddButton) {
+ } else if (showAddButton) {
return GestureDetector(
onTap: widget.isCamera ? _cameraAction : _showPickerOptions,
child: Container(
- decoration: BoxDecoration(
- border: Border.all(color: Colors.black12),
- borderRadius: BorderRadius.circular(5),
- ),
- child: const Center(
- child: Icon(Icons.camera_alt, color: Colors.black26),
- ),
+ decoration: BoxDecoration(border: Border.all(color: Colors.black12), borderRadius: BorderRadius.circular(5)),
+ child: const Center(child: Icon(Icons.camera_alt, color: Colors.black26)),
),
);
} else {
@@ -474,8 +523,6 @@ class _MediaPickerGridState extends State {
}
},
),
-
- // 转码/处理 loading 遮罩
if (_isProcessing)
Positioned.fill(
child: Container(
@@ -488,8 +535,8 @@ class _MediaPickerGridState extends State {
}
}
-/// 照片上传区域组件,使用纵向四列Grid展示
-/// 新增 isEdit 属性控制编辑状态
+
+/// ---------- RepairedPhotoSection ----------
class RepairedPhotoSection extends StatefulWidget {
final int maxCount;
final MediaType mediaType;
@@ -498,14 +545,17 @@ class RepairedPhotoSection extends StatefulWidget {
final ValueChanged> onChanged;
final ValueChanged? onMediaAdded;
final ValueChanged? onMediaRemoved;
- final ValueChanged? onMediaTapped; // 新增:媒体点击回调
+ final ValueChanged? onMediaTapped;
final VoidCallback onAiIdentify;
final bool isShowAI;
final double horizontalPadding;
final bool isRequired;
final bool isShowNum;
- final bool isEdit; // 新增:控制编辑状态
- final bool isCamera; // 新增:只能拍照
+ final bool isEdit;
+ final bool isCamera;
+ final String sectionKey;
+ final bool followInitialUpdates;
+
const RepairedPhotoSection({
Key? key,
@@ -519,11 +569,13 @@ class RepairedPhotoSection extends StatefulWidget {
this.horizontalPadding = 5,
this.onMediaAdded,
this.onMediaRemoved,
- this.onMediaTapped, // 新增
+ this.onMediaTapped,
this.isRequired = false,
this.isShowNum = true,
- this.isEdit = true, // 默认可编辑
+ this.isEdit = true,
this.isCamera = false,
+ this.followInitialUpdates = false, // 默认 false
+ this.sectionKey = kAcceptVideoSectionKey,
}) : super(key: key);
@override
@@ -532,17 +584,81 @@ class RepairedPhotoSection extends StatefulWidget {
class _RepairedPhotoSectionState extends State {
late List _mediaPaths;
+ StreamSubscription? _sub;
@override
void initState() {
super.initState();
- _mediaPaths =
- widget.initialMediaPaths?.take(widget.maxCount).toList() ?? [];
- WidgetsBinding.instance.addPostFrameCallback((_) {
- widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
+ _mediaPaths = _normalizePaths(widget.initialMediaPaths).take(widget.maxCount).toList();
+
+ // 订阅 MediaBus(如果需要)
+ _sub = MediaBus().stream.listen((ev) {
+ if (ev.key != widget.sectionKey) return;
+
+ if (ev.type == MediaEventType.clear) {
+ if (_mediaPaths.isNotEmpty) {
+ setState(() {
+ _mediaPaths = [];
+ });
+ widget.onChanged(_localFilesFromPaths(_mediaPaths));
+ }
+ } else if (ev.type == MediaEventType.set && ev.paths != null) {
+ final newList = _normalizePaths(ev.paths).take(widget.maxCount).toList();
+ if (!listEquals(newList, _mediaPaths)) {
+ setState(() {
+ _mediaPaths = newList;
+ });
+ widget.onChanged(_localFilesFromPaths(_mediaPaths));
+ }
+ }
});
}
+ @override
+ void didUpdateWidget(covariant RepairedPhotoSection oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (widget.followInitialUpdates) {
+ final oldList = _normalizePaths(oldWidget.initialMediaPaths);
+ final newList = _normalizePaths(widget.initialMediaPaths);
+
+ if (!listEquals(oldList, newList)) {
+ setState(() {
+ _mediaPaths = newList.take(widget.maxCount).toList();
+ });
+ widget.onChanged(_localFilesFromPaths(_mediaPaths));
+ }
+ }
+ if (oldWidget.sectionKey != widget.sectionKey) {
+ _sub?.cancel();
+ _sub = MediaBus().stream.listen((ev) {
+ if (ev.key != widget.sectionKey) return;
+
+ if (ev.type == MediaEventType.clear) {
+ if (_mediaPaths.isNotEmpty) {
+ setState(() {
+ _mediaPaths = [];
+ });
+ widget.onChanged(_localFilesFromPaths(_mediaPaths));
+ }
+ } else if (ev.type == MediaEventType.set && ev.paths != null) {
+ final newList = _normalizePaths(ev.paths).take(widget.maxCount).toList();
+ if (!listEquals(newList, _mediaPaths)) {
+ setState(() {
+ _mediaPaths = newList;
+ });
+ widget.onChanged(_localFilesFromPaths(_mediaPaths));
+ }
+ }
+ });
+ }
+ }
+
+ @override
+ void dispose() {
+ _sub?.cancel();
+ super.dispose();
+ }
+
@override
Widget build(BuildContext context) {
return Container(
@@ -555,10 +671,7 @@ class _RepairedPhotoSectionState extends State {
padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding),
child: ListItemFactory.createRowSpaceBetweenItem(
leftText: widget.title,
- rightText:
- widget.isShowNum
- ? '${_mediaPaths.length}/${widget.maxCount}'
- : '',
+ rightText: widget.isShowNum ? '${_mediaPaths.length}/${widget.maxCount}' : '',
isRequired: widget.isRequired,
),
),
@@ -582,24 +695,21 @@ class _RepairedPhotoSectionState extends State {
onMediaTapped: (filePath) {
if (widget.mediaType == MediaType.image) {
presentOpaque(SingleImageViewer(imageUrl: filePath), context);
- }else{
+ } else {
showDialog(
context: context,
barrierColor: Colors.black54,
- builder: (_) => VideoPlayerPopup(videoUrl:filePath),
+ builder: (_) => VideoPlayerPopup(videoUrl: filePath),
);
}
},
- // 传递点击回调
- isEdit: widget.isEdit, // 传递编辑状态
+ isEdit: widget.isEdit,
),
),
const SizedBox(height: 8),
- if (widget.isShowAI && widget.isEdit) // 只在可编辑状态下显示AI按钮
+ if (widget.isShowAI && widget.isEdit)
Padding(
- padding: EdgeInsets.symmetric(
- horizontal: widget.horizontalPadding,
- ),
+ padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding),
child: GestureDetector(
onTap: widget.onAiIdentify,
child: Container(
diff --git a/lib/customWidget/remote_file_page.dart b/lib/customWidget/remote_file_page.dart
index 3cc8803..dd637b0 100644
--- a/lib/customWidget/remote_file_page.dart
+++ b/lib/customWidget/remote_file_page.dart
@@ -39,6 +39,7 @@ class _RemoteFilePageState extends State {
_secondsRemaining = widget.countdownSeconds;
_startCountdown();
_downloadAndLoad();
+ LoadingDialogHelper.hide();
}
Future _downloadAndLoad() async {
@@ -135,7 +136,7 @@ class _RemoteFilePageState extends State {
onPressed: isButtonEnabled
? () {
// TODO: 完成回调
- Navigator.pop(context);
+ Navigator.pop(context, true);
}
: null,
),
diff --git a/lib/http/ApiService.dart b/lib/http/ApiService.dart
index 840b726..4a08048 100644
--- a/lib/http/ApiService.dart
+++ b/lib/http/ApiService.dart
@@ -22,8 +22,8 @@ class ApiService {
// static const String baseFacePath = "https://qaaqwh.qhdsafety.com/whb_stu_face";
static const String baseFacePath = "http://192.168.20.240:8500/whb_stu_face/";
/// 登录及其他管理后台接口
- // static const String basePath = "http://192.168.20.240:8500/integrated_whb";//测试服务器
// static const String basePath = "https://qaaqwh.qhdsafety.com/integrated_whb";
+ // static const String basePath = "http://192.168.20.240:8500/integrated_whb";//测试服务器
// static const String basePath = "http://192.168.20.240:8500/integrated_whb";
// static const String basePath = "http://192.168.0.25:28199";//王轩服务器
static const String basePath = "http://192.168.0.45:28199";//长久服务器
@@ -958,7 +958,8 @@ U6Hzm1ninpWeE+awIDAQAB
static Future