| 
									
										
										
										
											2025-09-17 14:38:05 +08:00
										 |  |  |  | // <your_file_name>.dart
 | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  | import 'dart:async'; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | import 'dart:io'; | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  | import 'package:device_info_plus/device_info_plus.dart'; | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  | import 'package:flutter/foundation.dart'; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | import 'package:flutter/material.dart'; | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | import 'package:flutter/services.dart'; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | import 'package:image_picker/image_picker.dart'; | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  | import 'package:permission_handler/permission_handler.dart'; | 
					
						
							|  |  |  |  | import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; | 
					
						
							| 
									
										
										
										
											2025-09-02 16:22:17 +08:00
										 |  |  |  | import 'package:qhd_prevention/customWidget/full_screen_video_page.dart'; | 
					
						
							|  |  |  |  | import 'package:qhd_prevention/customWidget/single_image_viewer.dart'; | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  | import 'package:qhd_prevention/customWidget/toast_util.dart'; | 
					
						
							| 
									
										
										
										
											2025-09-02 16:22:17 +08:00
										 |  |  |  | import 'package:qhd_prevention/tools/tools.dart'; | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | import 'package:video_compress/video_compress.dart'; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | import 'package:wechat_assets_picker/wechat_assets_picker.dart'; | 
					
						
							|  |  |  |  | import 'package:photo_manager/photo_manager.dart'; | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | import 'package:path/path.dart' as p; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | import 'ItemWidgetFactory.dart'; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  | const String kAcceptVideoSectionKey = 'accept_video'; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /// ---------------------- 全局 MediaBus (轻量事件总线) ----------------------
 | 
					
						
							|  |  |  |  | class MediaEvent { | 
					
						
							|  |  |  |  |   final String key; | 
					
						
							|  |  |  |  |   final MediaEventType type; | 
					
						
							|  |  |  |  |   final List<String>? paths; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   MediaEvent._(this.key, this.type, [this.paths]); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   factory MediaEvent.clear(String key) => MediaEvent._(key, MediaEventType.clear); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   factory MediaEvent.set(String key, List<String> paths) => | 
					
						
							|  |  |  |  |       MediaEvent._(key, MediaEventType.set, List<String>.from(paths)); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | enum MediaEventType { clear, set } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | class MediaBus { | 
					
						
							|  |  |  |  |   MediaBus._internal(); | 
					
						
							|  |  |  |  |   static final MediaBus _instance = MediaBus._internal(); | 
					
						
							|  |  |  |  |   factory MediaBus() => _instance; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   final StreamController<MediaEvent> _ctrl = StreamController<MediaEvent>.broadcast(); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   Stream<MediaEvent> get stream => _ctrl.stream; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   void emit(MediaEvent ev) { | 
					
						
							|  |  |  |  |     if (!_ctrl.isClosed) _ctrl.add(ev); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   Future<void> dispose() async { | 
					
						
							|  |  |  |  |     await _ctrl.close(); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | /// ---------------------- /MediaBus ----------------------
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | /// 媒体选择类型
 | 
					
						
							|  |  |  |  | enum MediaType { image, video } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  | /// ---------- 辅助函数(文件顶部复用) ----------
 | 
					
						
							|  |  |  |  | bool _isNetworkPath(String? p) { | 
					
						
							|  |  |  |  |   if (p == null) return false; | 
					
						
							|  |  |  |  |   final s = p.trim().toLowerCase(); | 
					
						
							|  |  |  |  |   return s.startsWith('http://') || s.startsWith('https://'); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /// 把路径列表转换为存在的本地 File 列表(过滤掉网络路径与空路径与不存在的本地文件)
 | 
					
						
							| 
									
										
										
										
											2025-09-17 14:38:05 +08:00
										 |  |  |  | /// 注意:这个函数**可能**会同步访问文件系统(existsSync),它只应在用户触发后调用,不应在 build() 中被频繁调用。
 | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  | List<File> _localFilesFromPaths(List<String>? paths) { | 
					
						
							|  |  |  |  |   if (paths == null) return <File>[]; | 
					
						
							|  |  |  |  |   return paths | 
					
						
							|  |  |  |  |       .map((e) => (e ?? '').toString().trim()) | 
					
						
							|  |  |  |  |       .where((s) => s.isNotEmpty && !_isNetworkPath(s)) | 
					
						
							| 
									
										
										
										
											2025-09-17 14:38:05 +08:00
										 |  |  |  |       .where((s) => File(s).existsSync()) | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |       .map((s) => File(s)) | 
					
						
							|  |  |  |  |       .toList(); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /// 规范化路径列表:trim + 过滤空字符串
 | 
					
						
							|  |  |  |  | List<String> _normalizePaths(List<String>? src) { | 
					
						
							|  |  |  |  |   if (src == null) return <String>[]; | 
					
						
							|  |  |  |  |   return src.map((e) => (e ?? '').toString().trim()).where((s) => s.isNotEmpty).toList(); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /// ---------- MediaPickerRow ----------
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | class MediaPickerRow extends StatefulWidget { | 
					
						
							|  |  |  |  |   final int maxCount; | 
					
						
							|  |  |  |  |   final MediaType mediaType; | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |   final List<String>? initialMediaPaths; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   final ValueChanged<List<File>> onChanged; | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |   final ValueChanged<String>? onMediaAdded; | 
					
						
							|  |  |  |  |   final ValueChanged<String>? onMediaRemoved; | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |   final ValueChanged<String>? onMediaTapped; | 
					
						
							|  |  |  |  |   final bool isEdit; | 
					
						
							|  |  |  |  |   final bool isCamera; | 
					
						
							|  |  |  |  |   /// 默认 false —— 仅在 initState 时读取 initialMediaPaths
 | 
					
						
							|  |  |  |  |   final bool followInitialUpdates; | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   const MediaPickerRow({ | 
					
						
							|  |  |  |  |     Key? key, | 
					
						
							|  |  |  |  |     this.maxCount = 4, | 
					
						
							|  |  |  |  |     this.mediaType = MediaType.image, | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |     this.initialMediaPaths, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     required this.onChanged, | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |     this.onMediaAdded, | 
					
						
							|  |  |  |  |     this.onMediaRemoved, | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |     this.onMediaTapped, | 
					
						
							|  |  |  |  |     this.isEdit = true, | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |     this.isCamera = false, | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |     this.followInitialUpdates = false, // 默认 false
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   }) : super(key: key); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  |   _MediaPickerGridState createState() => _MediaPickerGridState(); | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  | class _MediaPickerGridState extends State<MediaPickerRow> { | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   final ImagePicker _picker = ImagePicker(); | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |   late List<String> _mediaPaths; | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |   bool _isProcessing = false; | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-17 14:38:05 +08:00
										 |  |  |  |   /// 缓存每个本地路径是否存在(避免在 build 中反复同步 IO)
 | 
					
						
							|  |  |  |  |   final Map<String, bool> _localExistsCache = {}; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |   @override | 
					
						
							|  |  |  |  |   void initState() { | 
					
						
							|  |  |  |  |     super.initState(); | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |     // 初始化内部路径(保留网络路径与本地路径)
 | 
					
						
							|  |  |  |  |     _mediaPaths = _normalizePaths(widget.initialMediaPaths).take(widget.maxCount).toList(); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-17 14:38:05 +08:00
										 |  |  |  |     // 预先检查一次本地文件是否存在(只在 init 时做一次同步检查)
 | 
					
						
							|  |  |  |  |     for (final pth in _mediaPaths) { | 
					
						
							|  |  |  |  |       final t = pth.trim(); | 
					
						
							|  |  |  |  |       if (!_isNetworkPath(t)) { | 
					
						
							|  |  |  |  |         try { | 
					
						
							|  |  |  |  |           _localExistsCache[t] = File(t).existsSync(); | 
					
						
							|  |  |  |  |         } catch (_) { | 
					
						
							|  |  |  |  |           _localExistsCache[t] = false; | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  |       } else { | 
					
						
							|  |  |  |  |         _localExistsCache[pth] = false; | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |     // 仅在存在本地真实文件时才把 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(); | 
					
						
							| 
									
										
										
										
											2025-09-17 14:38:05 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |         // 更新本地存在缓存(同步检查,仅在更新时执行)
 | 
					
						
							|  |  |  |  |         for (final pth in _mediaPaths) { | 
					
						
							|  |  |  |  |           final t = pth.trim(); | 
					
						
							|  |  |  |  |           if (!_localExistsCache.containsKey(t)) { | 
					
						
							|  |  |  |  |             if (!_isNetworkPath(t)) { | 
					
						
							|  |  |  |  |               try { | 
					
						
							|  |  |  |  |                 _localExistsCache[t] = File(t).existsSync(); | 
					
						
							|  |  |  |  |               } catch (_) { | 
					
						
							|  |  |  |  |                 _localExistsCache[t] = false; | 
					
						
							|  |  |  |  |               } | 
					
						
							|  |  |  |  |             } else { | 
					
						
							|  |  |  |  |               _localExistsCache[t] = false; | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  |           } | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |         if (mounted) setState(() {}); | 
					
						
							|  |  |  |  |         WidgetsBinding.instance.addPostFrameCallback((_) { | 
					
						
							|  |  |  |  |           widget.onChanged(_localFilesFromPaths(_mediaPaths)); | 
					
						
							|  |  |  |  |         }); | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |   } | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |   Future<void> _handlePickedPath(String path) async { | 
					
						
							|  |  |  |  |     if (!mounted) return; | 
					
						
							|  |  |  |  |     if (path.isEmpty) return; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     try { | 
					
						
							|  |  |  |  |       String finalPath = path; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       if (widget.mediaType == MediaType.video) { | 
					
						
							|  |  |  |  |         final ext = p.extension(path).toLowerCase(); | 
					
						
							|  |  |  |  |         if (ext != '.mp4') { | 
					
						
							|  |  |  |  |           setState(() => _isProcessing = true); | 
					
						
							|  |  |  |  |           try { | 
					
						
							|  |  |  |  |             final info = await VideoCompress.compressVideo( | 
					
						
							|  |  |  |  |               path, | 
					
						
							|  |  |  |  |               quality: VideoQuality.MediumQuality, | 
					
						
							|  |  |  |  |               deleteOrigin: false, | 
					
						
							|  |  |  |  |             ); | 
					
						
							|  |  |  |  |             if (info != null && info.file != null) { | 
					
						
							|  |  |  |  |               finalPath = info.file!.path; | 
					
						
							|  |  |  |  |               debugPrint('✅ 转换完成: $path -> $finalPath'); | 
					
						
							|  |  |  |  |             } else { | 
					
						
							|  |  |  |  |               throw Exception("转码失败: 返回空文件"); | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  |           } catch (e) { | 
					
						
							|  |  |  |  |             debugPrint('❌ 视频转码失败: $e'); | 
					
						
							|  |  |  |  |             if (mounted) { | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |               ToastUtil.showNormal(context, '视频转码失败'); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |             } | 
					
						
							|  |  |  |  |             return; | 
					
						
							|  |  |  |  |           } finally { | 
					
						
							|  |  |  |  |             if (mounted) setState(() => _isProcessing = false); | 
					
						
							|  |  |  |  |           } | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       if (_mediaPaths.length < widget.maxCount) { | 
					
						
							|  |  |  |  |         setState(() => _mediaPaths.add(finalPath)); | 
					
						
							| 
									
										
										
										
											2025-09-17 14:38:05 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |         // 记录缓存(只在添加时检查一次文件是否真实存在)
 | 
					
						
							|  |  |  |  |         if (!_isNetworkPath(finalPath)) { | 
					
						
							|  |  |  |  |           try { | 
					
						
							|  |  |  |  |             _localExistsCache[finalPath] = File(finalPath).existsSync(); | 
					
						
							|  |  |  |  |           } catch (_) { | 
					
						
							|  |  |  |  |             _localExistsCache[finalPath] = false; | 
					
						
							|  |  |  |  |           } | 
					
						
							|  |  |  |  |         } else { | 
					
						
							|  |  |  |  |           _localExistsCache[finalPath] = false; | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |         // 回调仅包含本地真实存在的文件(网络路径不会出现在此回调中)
 | 
					
						
							|  |  |  |  |         widget.onChanged(_localFilesFromPaths(_mediaPaths)); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |         widget.onMediaAdded?.call(finalPath); | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } catch (e) { | 
					
						
							|  |  |  |  |       debugPrint('处理选中媒体失败: $e'); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   Future<void> _showPickerOptions() async { | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |     if (!widget.isEdit) return; | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     showModalBottomSheet( | 
					
						
							|  |  |  |  |       context: context, | 
					
						
							| 
									
										
										
										
											2025-08-11 17:40:03 +08:00
										 |  |  |  |       backgroundColor: Colors.white, | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |       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(); | 
					
						
							|  |  |  |  |               }, | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |             ), | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |             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(), | 
					
						
							|  |  |  |  |             ), | 
					
						
							|  |  |  |  |           ], | 
					
						
							|  |  |  |  |         ), | 
					
						
							|  |  |  |  |       ), | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     ); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   Future<void> _pickCamera() async { | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |     if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     try { | 
					
						
							|  |  |  |  |       XFile? picked; | 
					
						
							|  |  |  |  |       if (widget.mediaType == MediaType.image) { | 
					
						
							|  |  |  |  |         picked = await _picker.pickImage(source: ImageSource.camera); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |         if (picked != null) { | 
					
						
							|  |  |  |  |           final path = picked.path; | 
					
						
							|  |  |  |  |           setState(() => _mediaPaths.add(path)); | 
					
						
							| 
									
										
										
										
											2025-09-17 14:38:05 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |           // 记录存在
 | 
					
						
							|  |  |  |  |           try { | 
					
						
							|  |  |  |  |             _localExistsCache[path] = File(path).existsSync(); | 
					
						
							|  |  |  |  |           } catch (_) { | 
					
						
							|  |  |  |  |             _localExistsCache[path] = false; | 
					
						
							|  |  |  |  |           } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |           widget.onChanged(_localFilesFromPaths(_mediaPaths)); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |           widget.onMediaAdded?.call(path); | 
					
						
							|  |  |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |       } else { | 
					
						
							|  |  |  |  |         picked = await _picker.pickVideo(source: ImageSource.camera); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |         if (picked != null) { | 
					
						
							|  |  |  |  |           await _handlePickedPath(picked.path); | 
					
						
							|  |  |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |       } | 
					
						
							|  |  |  |  |     } catch (e) { | 
					
						
							| 
									
										
										
										
											2025-07-28 14:22:07 +08:00
										 |  |  |  |       debugPrint('拍摄失败: $e'); | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   Future<void> _pickGallery() async { | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |     if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |     try { | 
					
						
							|  |  |  |  |       if (Platform.isIOS) { | 
					
						
							|  |  |  |  |         final permission = await PhotoManager.requestPermissionExtend(); | 
					
						
							|  |  |  |  |         debugPrint('iOS photo permission state: $permission'); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |         if (permission != PermissionState.authorized && permission != PermissionState.limited) { | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           if (mounted) { | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |             ToastUtil.showNormal(context, '请到设置中开启相册访问权限'); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           } | 
					
						
							|  |  |  |  |           return; | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         final remaining = widget.maxCount - _mediaPaths.length; | 
					
						
							|  |  |  |  |         final List<AssetEntity>? assets = await AssetPicker.pickAssets( | 
					
						
							|  |  |  |  |           context, | 
					
						
							|  |  |  |  |           pickerConfig: AssetPickerConfig( | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |             requestType: widget.mediaType == MediaType.image ? RequestType.image : RequestType.video, | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |             maxAssets: remaining, | 
					
						
							|  |  |  |  |             gridCount: 4, | 
					
						
							|  |  |  |  |           ), | 
					
						
							|  |  |  |  |         ); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         if (assets != null) { | 
					
						
							|  |  |  |  |           for (final asset in assets) { | 
					
						
							|  |  |  |  |             if (_mediaPaths.length >= widget.maxCount) break; | 
					
						
							|  |  |  |  |             try { | 
					
						
							|  |  |  |  |               final file = await asset.file; | 
					
						
							|  |  |  |  |               if (file != null) { | 
					
						
							|  |  |  |  |                 final path = file.path; | 
					
						
							|  |  |  |  |                 await _handlePickedPath(path); | 
					
						
							|  |  |  |  |               } else { | 
					
						
							|  |  |  |  |                 debugPrint('资产获取 file 为空,asset id: ${asset.id}'); | 
					
						
							|  |  |  |  |               } | 
					
						
							|  |  |  |  |             } catch (e) { | 
					
						
							|  |  |  |  |               debugPrint('读取 asset 文件失败: $e'); | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  |           } | 
					
						
							|  |  |  |  |           if (mounted) setState(() {}); | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |           widget.onChanged(_localFilesFromPaths(_mediaPaths)); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |         } | 
					
						
							|  |  |  |  |       } else { | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |         final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); | 
					
						
							|  |  |  |  |         final androidInfo = await deviceInfo.androidInfo; | 
					
						
							|  |  |  |  |         final int sdkInt = androidInfo.version.sdkInt ?? 0; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         PermissionStatus permissionStatus = PermissionStatus.denied; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         if (sdkInt >= 33) { | 
					
						
							|  |  |  |  |           if (widget.mediaType == MediaType.image) { | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |             permissionStatus = await Permission.photos.request(); | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |           } else if (widget.mediaType == MediaType.video) { | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |             permissionStatus = await Permission.videos.request(); | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |           } else { | 
					
						
							|  |  |  |  |             final statuses = await [Permission.photos, Permission.videos].request(); | 
					
						
							|  |  |  |  |             permissionStatus = statuses[Permission.photos] ?? statuses[Permission.videos] ?? PermissionStatus.denied; | 
					
						
							|  |  |  |  |           } | 
					
						
							|  |  |  |  |         } else if (sdkInt >= 30) { | 
					
						
							|  |  |  |  |           permissionStatus = await Permission.storage.request(); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |         } else { | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |           permissionStatus = await Permission.storage.request(); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |         } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |         if (permissionStatus.isGranted) { | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           final remaining = widget.maxCount - _mediaPaths.length; | 
					
						
							|  |  |  |  |           final List<AssetEntity>? assets = await AssetPicker.pickAssets( | 
					
						
							|  |  |  |  |             context, | 
					
						
							|  |  |  |  |             pickerConfig: AssetPickerConfig( | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |               requestType: widget.mediaType == MediaType.image ? RequestType.image : RequestType.video, | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |               maxAssets: remaining, | 
					
						
							|  |  |  |  |               gridCount: 4, | 
					
						
							|  |  |  |  |             ), | 
					
						
							|  |  |  |  |           ); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |           if (assets != null) { | 
					
						
							|  |  |  |  |             for (final asset in assets) { | 
					
						
							|  |  |  |  |               if (_mediaPaths.length >= widget.maxCount) break; | 
					
						
							|  |  |  |  |               try { | 
					
						
							|  |  |  |  |                 final file = await asset.file; | 
					
						
							|  |  |  |  |                 if (file != null) { | 
					
						
							|  |  |  |  |                   final path = file.path; | 
					
						
							|  |  |  |  |                   await _handlePickedPath(path); | 
					
						
							|  |  |  |  |                 } else { | 
					
						
							|  |  |  |  |                   debugPrint('资产获取 file 为空,asset id: ${asset.id}'); | 
					
						
							|  |  |  |  |                 } | 
					
						
							|  |  |  |  |               } catch (e) { | 
					
						
							|  |  |  |  |                 debugPrint('读取 asset 文件失败: $e'); | 
					
						
							|  |  |  |  |               } | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  |             if (mounted) setState(() {}); | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |             widget.onChanged(_localFilesFromPaths(_mediaPaths)); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           } | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |         } else if (permissionStatus.isPermanentlyDenied) { | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           if (mounted) { | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |             ToastUtil.showNormal(context, '请到设置中开启相册访问权限'); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           } | 
					
						
							|  |  |  |  |           await openAppSettings(); | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |           return; | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |         } else { | 
					
						
							|  |  |  |  |           if (mounted) { | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |             ToastUtil.showNormal(context, '相册访问权限被拒绝'); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           } | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |           return; | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |         } | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } catch (e, st) { | 
					
						
							|  |  |  |  |       debugPrint('相册选择失败: $e\n$st'); | 
					
						
							|  |  |  |  |       if (mounted) { | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |         ToastUtil.showNormal(context, '相册选择失败'); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |       } | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   Future<void> _cameraAction() async { | 
					
						
							|  |  |  |  |     if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     final PermissionStatus status = await Permission.camera.request(); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     if (status != PermissionStatus.granted) { | 
					
						
							|  |  |  |  |       if (mounted) { | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |         ToastUtil.showNormal(context, '相机权限被拒绝'); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |       } | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |       return; | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |     try { | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |       if (widget.mediaType == MediaType.image) { | 
					
						
							|  |  |  |  |         XFile? picked = await _picker.pickImage(source: ImageSource.camera); | 
					
						
							|  |  |  |  |         if (picked != null) { | 
					
						
							|  |  |  |  |           final path = picked.path; | 
					
						
							|  |  |  |  |           setState(() => _mediaPaths.add(path)); | 
					
						
							| 
									
										
										
										
											2025-09-17 14:38:05 +08:00
										 |  |  |  |           try { | 
					
						
							|  |  |  |  |             _localExistsCache[path] = File(path).existsSync(); | 
					
						
							|  |  |  |  |           } catch (_) { | 
					
						
							|  |  |  |  |             _localExistsCache[path] = false; | 
					
						
							|  |  |  |  |           } | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |           widget.onChanged(_localFilesFromPaths(_mediaPaths)); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           widget.onMediaAdded?.call(path); | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  |       } else { | 
					
						
							|  |  |  |  |         XFile? picked = await _picker.pickVideo(source: ImageSource.camera); | 
					
						
							|  |  |  |  |         if (picked != null) { | 
					
						
							|  |  |  |  |           await _handlePickedPath(picked.path); | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |         } | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } catch (e) { | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |       debugPrint('拍摄失败: $e'); | 
					
						
							|  |  |  |  |       if (mounted) { | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |         ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('拍摄失败,请检查相机权限或设备状态'))); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |       } | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |   void _removeMedia(int index) async { | 
					
						
							|  |  |  |  |     final ok = await CustomAlertDialog.showConfirm( | 
					
						
							|  |  |  |  |       context, | 
					
						
							|  |  |  |  |       title: '温馨提示', | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |       content: widget.mediaType == MediaType.image ? '确定要删除这张图片吗?' : '确定要删除这个视频吗?', | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |       cancelText: '取消', | 
					
						
							|  |  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |     if (!ok) return; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     final removed = _mediaPaths[index]; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     setState(() => _mediaPaths.removeAt(index)); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-17 14:38:05 +08:00
										 |  |  |  |     // 从缓存中移除
 | 
					
						
							|  |  |  |  |     _localExistsCache.remove(removed); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |     // 始终通知 onMediaRemoved(用于父端业务逻辑)
 | 
					
						
							|  |  |  |  |     widget.onMediaRemoved?.call(removed); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // 只有当本地文件集合发生变化时才触发 onChanged(避免因为删除网络路径导致父端把列表置空)
 | 
					
						
							|  |  |  |  |     final localFiles = _localFilesFromPaths(_mediaPaths); | 
					
						
							|  |  |  |  |     widget.onChanged(localFiles); | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   Widget build(BuildContext context) { | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |     final showAddButton = widget.isEdit && _mediaPaths.length < widget.maxCount; | 
					
						
							|  |  |  |  |     final itemCount = _mediaPaths.length + (showAddButton ? 1 : 0); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-17 14:38:05 +08:00
										 |  |  |  |     // 预计算缩略图解码宽度(减少内存开销)
 | 
					
						
							|  |  |  |  |     final tileLogicalW = (MediaQuery.of(context).size.width / 4).round(); | 
					
						
							|  |  |  |  |     final cacheWidth = (tileLogicalW * MediaQuery.of(context).devicePixelRatio).round(); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     return Stack( | 
					
						
							|  |  |  |  |       children: [ | 
					
						
							|  |  |  |  |         GridView.builder( | 
					
						
							|  |  |  |  |           shrinkWrap: true, | 
					
						
							|  |  |  |  |           physics: const NeverScrollableScrollPhysics(), | 
					
						
							|  |  |  |  |           gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( | 
					
						
							|  |  |  |  |             crossAxisCount: 4, | 
					
						
							|  |  |  |  |             crossAxisSpacing: 8, | 
					
						
							|  |  |  |  |             mainAxisSpacing: 8, | 
					
						
							|  |  |  |  |             childAspectRatio: 1, | 
					
						
							|  |  |  |  |           ), | 
					
						
							|  |  |  |  |           itemCount: itemCount, | 
					
						
							|  |  |  |  |           itemBuilder: (context, index) { | 
					
						
							|  |  |  |  |             if (index < _mediaPaths.length) { | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |               final raw = (_mediaPaths[index] ?? '').toString().trim(); | 
					
						
							|  |  |  |  |               final isNetwork = _isNetworkPath(raw); | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |               return GestureDetector( | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |                 onTap: () => widget.onMediaTapped?.call(raw), | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |                 child: Stack( | 
					
						
							|  |  |  |  |                   children: [ | 
					
						
							|  |  |  |  |                     ClipRRect( | 
					
						
							|  |  |  |  |                       borderRadius: BorderRadius.circular(5), | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |                       child: SizedBox.expand( | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |                         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, | 
					
						
							| 
									
										
										
										
											2025-09-17 14:38:05 +08:00
										 |  |  |  |                           // request a scaled decode to reduce memory
 | 
					
						
							|  |  |  |  |                           width: tileLogicalW.toDouble(), | 
					
						
							|  |  |  |  |                           height: tileLogicalW.toDouble(), | 
					
						
							|  |  |  |  |                           // errorBuilder for network errors
 | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |                           errorBuilder: (_, __, ___) => Container( | 
					
						
							|  |  |  |  |                             color: Colors.grey.shade200, | 
					
						
							|  |  |  |  |                             child: const Center(child: Icon(Icons.broken_image)), | 
					
						
							|  |  |  |  |                           ), | 
					
						
							|  |  |  |  |                         ) | 
					
						
							| 
									
										
										
										
											2025-09-17 14:38:05 +08:00
										 |  |  |  |                             : Image.file( | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |                           File(raw), | 
					
						
							|  |  |  |  |                           fit: BoxFit.cover, | 
					
						
							| 
									
										
										
										
											2025-09-17 14:38:05 +08:00
										 |  |  |  |                           width: tileLogicalW.toDouble(), | 
					
						
							|  |  |  |  |                           height: tileLogicalW.toDouble(), | 
					
						
							|  |  |  |  |                           // Use cacheWidth to ask the engine to decode a smaller bitmap (reduces memory).
 | 
					
						
							|  |  |  |  |                           cacheWidth: cacheWidth, | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |                           errorBuilder: (_, __, ___) => Container( | 
					
						
							|  |  |  |  |                             color: Colors.grey.shade200, | 
					
						
							|  |  |  |  |                             child: const Center(child: Icon(Icons.broken_image)), | 
					
						
							|  |  |  |  |                           ), | 
					
						
							| 
									
										
										
										
											2025-09-17 14:38:05 +08:00
										 |  |  |  |                         )) | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |                             : Container( | 
					
						
							|  |  |  |  |                           color: Colors.black12, | 
					
						
							|  |  |  |  |                           child: const Center( | 
					
						
							|  |  |  |  |                             child: Icon(Icons.videocam, color: Colors.white70), | 
					
						
							|  |  |  |  |                           ), | 
					
						
							|  |  |  |  |                         )), | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |                       ), | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |                     ), | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |                     if (widget.isEdit) | 
					
						
							|  |  |  |  |                       Positioned( | 
					
						
							|  |  |  |  |                         top: -15, | 
					
						
							|  |  |  |  |                         right: -15, | 
					
						
							|  |  |  |  |                         child: IconButton( | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |                           icon: const Icon(Icons.cancel, size: 20, color: Colors.red), | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |                           onPressed: () => _removeMedia(index), | 
					
						
							|  |  |  |  |                         ), | 
					
						
							|  |  |  |  |                       ), | 
					
						
							|  |  |  |  |                   ], | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |                 ), | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |               ); | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |             } else if (showAddButton) { | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |               return GestureDetector( | 
					
						
							|  |  |  |  |                 onTap: widget.isCamera ? _cameraAction : _showPickerOptions, | 
					
						
							|  |  |  |  |                 child: Container( | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |                   decoration: BoxDecoration(border: Border.all(color: Colors.black12), borderRadius: BorderRadius.circular(5)), | 
					
						
							|  |  |  |  |                   child: const Center(child: Icon(Icons.camera_alt, color: Colors.black26)), | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |                 ), | 
					
						
							|  |  |  |  |               ); | 
					
						
							|  |  |  |  |             } else { | 
					
						
							|  |  |  |  |               return const SizedBox.shrink(); | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  |           }, | 
					
						
							|  |  |  |  |         ), | 
					
						
							|  |  |  |  |         if (_isProcessing) | 
					
						
							|  |  |  |  |           Positioned.fill( | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  |             child: Container( | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |               color: Colors.transparent, | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |               child: const Center(child: CircularProgressIndicator()), | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  |             ), | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |           ), | 
					
						
							|  |  |  |  |       ], | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     ); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | /// ---------- RepairedPhotoSection ----------
 | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  | class RepairedPhotoSection extends StatefulWidget { | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   final int maxCount; | 
					
						
							|  |  |  |  |   final MediaType mediaType; | 
					
						
							|  |  |  |  |   final String title; | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |   final List<String>? initialMediaPaths; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   final ValueChanged<List<File>> onChanged; | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |   final ValueChanged<String>? onMediaAdded; | 
					
						
							|  |  |  |  |   final ValueChanged<String>? onMediaRemoved; | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |   final ValueChanged<String>? onMediaTapped; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   final VoidCallback onAiIdentify; | 
					
						
							|  |  |  |  |   final bool isShowAI; | 
					
						
							|  |  |  |  |   final double horizontalPadding; | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  |   final bool isRequired; | 
					
						
							|  |  |  |  |   final bool isShowNum; | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |   final bool isEdit; | 
					
						
							|  |  |  |  |   final bool isCamera; | 
					
						
							|  |  |  |  |   final String sectionKey; | 
					
						
							|  |  |  |  |   final bool followInitialUpdates; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   const RepairedPhotoSection({ | 
					
						
							|  |  |  |  |     Key? key, | 
					
						
							|  |  |  |  |     this.maxCount = 4, | 
					
						
							|  |  |  |  |     this.mediaType = MediaType.image, | 
					
						
							|  |  |  |  |     required this.title, | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |     this.initialMediaPaths, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     this.isShowAI = false, | 
					
						
							|  |  |  |  |     required this.onChanged, | 
					
						
							|  |  |  |  |     required this.onAiIdentify, | 
					
						
							| 
									
										
										
										
											2025-08-11 17:40:03 +08:00
										 |  |  |  |     this.horizontalPadding = 5, | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |     this.onMediaAdded, | 
					
						
							|  |  |  |  |     this.onMediaRemoved, | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |     this.onMediaTapped, | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  |     this.isRequired = false, | 
					
						
							|  |  |  |  |     this.isShowNum = true, | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |     this.isEdit = true, | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |     this.isCamera = false, | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |     this.followInitialUpdates = false, // 默认 false
 | 
					
						
							|  |  |  |  |     this.sectionKey = kAcceptVideoSectionKey, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   }) : super(key: key); | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |   @override | 
					
						
							|  |  |  |  |   _RepairedPhotoSectionState createState() => _RepairedPhotoSectionState(); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | class _RepairedPhotoSectionState extends State<RepairedPhotoSection> { | 
					
						
							|  |  |  |  |   late List<String> _mediaPaths; | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |   StreamSubscription<MediaEvent>? _sub; | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   void initState() { | 
					
						
							|  |  |  |  |     super.initState(); | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |     _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 = <String>[]; | 
					
						
							|  |  |  |  |           }); | 
					
						
							|  |  |  |  |           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)); | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  |       } | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |     }); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |   @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 = <String>[]; | 
					
						
							|  |  |  |  |             }); | 
					
						
							|  |  |  |  |             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(); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   @override | 
					
						
							|  |  |  |  |   Widget build(BuildContext context) { | 
					
						
							|  |  |  |  |     return Container( | 
					
						
							|  |  |  |  |       color: Colors.white, | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |       padding: const EdgeInsets.only(left: 0, right: 10), | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |       child: Column( | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |         crossAxisAlignment: CrossAxisAlignment.start, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |         children: [ | 
					
						
							|  |  |  |  |           Padding( | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |             padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding), | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |             child: ListItemFactory.createRowSpaceBetweenItem( | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |               leftText: widget.title, | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |               rightText: widget.isShowNum ? '${_mediaPaths.length}/${widget.maxCount}' : '', | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  |               isRequired: widget.isRequired, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |             ), | 
					
						
							|  |  |  |  |           ), | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |           const SizedBox(height: 8), | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |           Padding( | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |             padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding), | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |             child: MediaPickerRow( | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |               maxCount: widget.maxCount, | 
					
						
							|  |  |  |  |               mediaType: widget.mediaType, | 
					
						
							|  |  |  |  |               initialMediaPaths: _mediaPaths, | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |               isCamera: widget.isCamera, | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |               onChanged: (files) { | 
					
						
							|  |  |  |  |                 final newPaths = files.map((f) => f.path).toList(); | 
					
						
							|  |  |  |  |                 setState(() { | 
					
						
							|  |  |  |  |                   _mediaPaths = newPaths; | 
					
						
							|  |  |  |  |                 }); | 
					
						
							|  |  |  |  |                 widget.onChanged(files); | 
					
						
							|  |  |  |  |               }, | 
					
						
							|  |  |  |  |               onMediaAdded: widget.onMediaAdded, | 
					
						
							|  |  |  |  |               onMediaRemoved: widget.onMediaRemoved, | 
					
						
							| 
									
										
										
										
											2025-09-02 16:22:17 +08:00
										 |  |  |  |               onMediaTapped: (filePath) { | 
					
						
							|  |  |  |  |                 if (widget.mediaType == MediaType.image) { | 
					
						
							|  |  |  |  |                   presentOpaque(SingleImageViewer(imageUrl: filePath), context); | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |                 } else { | 
					
						
							| 
									
										
										
										
											2025-09-02 16:22:17 +08:00
										 |  |  |  |                   showDialog( | 
					
						
							|  |  |  |  |                     context: context, | 
					
						
							|  |  |  |  |                     barrierColor: Colors.black54, | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |                     builder: (_) => VideoPlayerPopup(videoUrl: filePath), | 
					
						
							| 
									
										
										
										
											2025-09-02 16:22:17 +08:00
										 |  |  |  |                   ); | 
					
						
							|  |  |  |  |                 } | 
					
						
							|  |  |  |  |               }, | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |               isEdit: widget.isEdit, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |             ), | 
					
						
							|  |  |  |  |           ), | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           const SizedBox(height: 8), | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |           if (widget.isShowAI && widget.isEdit) | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |             Padding( | 
					
						
							| 
									
										
										
										
											2025-09-15 15:54:03 +08:00
										 |  |  |  |               padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding), | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |               child: GestureDetector( | 
					
						
							|  |  |  |  |                 onTap: widget.onAiIdentify, | 
					
						
							|  |  |  |  |                 child: Container( | 
					
						
							|  |  |  |  |                   padding: const EdgeInsets.symmetric(horizontal: 15), | 
					
						
							|  |  |  |  |                   height: 36, | 
					
						
							|  |  |  |  |                   decoration: BoxDecoration( | 
					
						
							|  |  |  |  |                     color: const Color(0xFFDFEAFF), | 
					
						
							|  |  |  |  |                     borderRadius: BorderRadius.circular(18), | 
					
						
							|  |  |  |  |                   ), | 
					
						
							|  |  |  |  |                   child: Row( | 
					
						
							|  |  |  |  |                     mainAxisSize: MainAxisSize.min, | 
					
						
							|  |  |  |  |                     children: [ | 
					
						
							|  |  |  |  |                       Image.asset('assets/images/ai_img.png', width: 20), | 
					
						
							|  |  |  |  |                       const SizedBox(width: 5), | 
					
						
							|  |  |  |  |                       const Text('AI隐患识别与处理'), | 
					
						
							|  |  |  |  |                     ], | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |                   ), | 
					
						
							|  |  |  |  |                 ), | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |               ), | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |             ), | 
					
						
							|  |  |  |  |         ], | 
					
						
							|  |  |  |  |       ), | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  |   } | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | } |