/// 网络照片缩略图(UI-01 §4.1):固定宽高比预留 + 占位 + 失败替代图。 /// /// 布局稳定性优先:占位不是转圈,而是同一宽高比的中性色块(网格/卡片 /// 不因加载状态跳动);加载完成前显示占位,失败显示中性色块 + 小图标 /// (不显示破图)。[url] 为 null(无封面/已删除)时常驻占位。使用 /// Image.network + errorBuilder(项目未引入 cached_network_image,保持 /// 零新增依赖)。 library; import 'package:flutter/material.dart'; /// 网络缩略图:`AspectRatio` 包裹,cover 裁切。 class PhotoThumbnail extends StatelessWidget { const PhotoThumbnail({ required this.url, required this.aspectRatio, this.fit = BoxFit.cover, super.key, }); /// 图片 URL;null 显示常驻占位(无封面/无预览语义)。 final String? url; /// 宽高比(相册封面 16:9、网格 1:1 等)——占位与图片共用,尺寸恒定。 final double aspectRatio; /// 图片适配(默认 cover 裁切;大图查看用 contain)。 final BoxFit fit; @override Widget build(BuildContext context) { if (url == null) { return AspectRatio( aspectRatio: aspectRatio, child: _placeholder(context), ); } return AspectRatio( aspectRatio: aspectRatio, child: Image.network( url!, fit: fit, // 帧 ready 前显示中性占位(不是 spinner):布局稳定优先。 frameBuilder: (context, child, frame, wasSynchronouslyLoaded) => wasSynchronouslyLoaded || frame != null ? child : _placeholder(context), errorBuilder: (_, _, _) => _placeholder(context, icon: Icons.broken_image_outlined), ), ); } /// 中性占位色块 + 小图标(加载中 / 无封面 / 加载失败共用)。 Widget _placeholder( BuildContext context, { IconData icon = Icons.image_outlined, }) { final theme = Theme.of(context); return ColoredBox( color: theme.colorScheme.surfaceContainerHighest, child: Center( child: Icon(icon, size: 24, color: theme.colorScheme.onSurfaceVariant), ), ); } }