import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; /// 金值统一的网络图片入口。 /// /// 图片会自动使用 [CachedNetworkImage] 的磁盘缓存;业务页面不应直接创建 /// `Image.network`,以保证加载和失败状态的视觉及缓存策略一致。 class AppNetworkImage extends StatelessWidget { const AppNetworkImage({ super.key, required this.imageUrl, this.width, this.height, this.fit = BoxFit.cover, this.alignment = Alignment.center, this.borderRadius, this.cacheKey, this.httpHeaders, this.placeholder, this.errorWidget, this.semanticLabel, }); final String imageUrl; final double? width; final double? height; final BoxFit fit; final Alignment alignment; final BorderRadius? borderRadius; final String? cacheKey; final Map? httpHeaders; final Widget? placeholder; final Widget? errorWidget; final String? semanticLabel; @override Widget build(BuildContext context) { final child = imageUrl.trim().isEmpty ? _ImageFallback(errorWidget: errorWidget) : CachedNetworkImage( imageUrl: imageUrl, cacheKey: cacheKey, httpHeaders: httpHeaders, width: width, height: height, fit: fit, alignment: alignment, fadeInDuration: const Duration(milliseconds: 150), placeholder: (_, _) => placeholder ?? const _ImageLoadingPlaceholder(), errorWidget: (_, _, _) => _ImageFallback(errorWidget: errorWidget), ); final clipped = borderRadius == null ? child : ClipRRect(borderRadius: borderRadius!, child: child); if (semanticLabel == null) return clipped; return Semantics(image: true, label: semanticLabel, child: clipped); } } class _ImageLoadingPlaceholder extends StatelessWidget { const _ImageLoadingPlaceholder(); @override Widget build(BuildContext context) { final colors = ShadTheme.of(context).colorScheme; return ColoredBox( color: colors.muted, child: Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: colors.mutedForeground, ), ), ), ); } } class _ImageFallback extends StatelessWidget { const _ImageFallback({required this.errorWidget}); final Widget? errorWidget; @override Widget build(BuildContext context) { if (errorWidget != null) return errorWidget!; final colors = ShadTheme.of(context).colorScheme; return ColoredBox( color: colors.muted, child: Center( child: Icon( Icons.image_not_supported_outlined, color: colors.mutedForeground, ), ), ); } }