import 'dart:async'; import 'package:flutter/material.dart'; import '../../core/config/app_config.dart'; /// 3D 化身查看组件 /// /// 服务端预渲染帧序列(36 帧绕 Y 轴旋转 PNG)轮播模拟 3D; /// framesUrl 为空时降级为静态占位。 class AvatarViewer extends StatefulWidget { final String? glbUrl; final String? framesUrl; final String title; final String subtitle; final double height; const AvatarViewer({ super.key, this.glbUrl, this.framesUrl, required this.title, required this.subtitle, this.height = 360, }); @override State createState() => _AvatarViewerState(); } class _AvatarViewerState extends State { static const int _frameCount = 36; Timer? _timer; int _frame = 0; @override void initState() { super.initState(); if (widget.framesUrl != null && widget.framesUrl!.isNotEmpty) { _timer = Timer.periodic(const Duration(milliseconds: 120), (_) { setState(() => _frame = (_frame + 1) % _frameCount); }); } } @override void dispose() { _timer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { final framesUrl = widget.framesUrl; if (framesUrl != null && framesUrl.isNotEmpty) { final frameUrl = AppConfig.resolveUrl( '$framesUrl/frame_${_frame.toString().padLeft(3, '0')}.png'); final scheme = Theme.of(context).colorScheme; return Container( height: widget.height, width: double.infinity, clipBehavior: Clip.antiAlias, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ scheme.primaryContainer, scheme.primary.withValues(alpha: 0.3), ], ), borderRadius: BorderRadius.circular(16), ), child: Stack( alignment: Alignment.bottomCenter, children: [ Positioned.fill( child: Image.network( frameUrl, fit: BoxFit.contain, errorBuilder: (_, _, _) => _placeholder(context), ), ), Padding( padding: const EdgeInsets.only(bottom: 10), child: Text( widget.subtitle, style: const TextStyle( color: Colors.white, fontSize: 12, shadows: [ Shadow(color: Colors.black45, blurRadius: 4), ]), ), ), ], ), ); } return _placeholder(context); } Widget _placeholder(BuildContext context) { final scheme = Theme.of(context).colorScheme; return Container( height: widget.height, width: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ scheme.primaryContainer, scheme.primary.withValues(alpha: 0.3), ], ), borderRadius: BorderRadius.circular(16), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.accessibility_new, size: 72, color: scheme.primary), const SizedBox(height: 12), Text(widget.title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 4), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Text( widget.subtitle.isEmpty ? '3D 渲染服务未就绪' : widget.subtitle, textAlign: TextAlign.center, style: const TextStyle(color: Colors.grey, fontSize: 12), ), ), ], ), ); } }