import 'dart:ui' as ui; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:new_learn/core/api/class/model.dart'; import 'package:new_learn/core/components/image_preview_viewer.dart'; import 'package:new_learn/core/components/resolve_problem_box.dart'; import 'package:new_learn/core/components/subject_tag.dart'; import 'package:new_learn/core/components/user_circle_avatar.dart'; import 'package:new_learn/core/utils/img_url.dart'; /// ============================================================ /// 【班级动态组件】由 class_update_view.dart 抽离的展示组件 /// /// PostCard(动态卡片,按 type 分发内容 + 相册 + 点赞评论栏) /// CardHeader / SubjectTag / AlbumDesc / AlbumGrid / LikeCommentBar / ... /// ResolveProblemBox 已抽离到 core/components/resolve_problem_box.dart /// LikeCommentBar / MetaButton / CommentPreview / CommentInput /// /// 原为 class_update_view.dart 内的私有组件,重命名去下划线后放到本文件。 /// 头像复用 core/components/user_circle_avatar.dart。 /// ============================================================ /// 动态卡片(对标 SubjectCard + ResolveProblem/文字 + Album + LikeAndComment) class PostCard extends StatelessWidget { const PostCard({ super.key, required this.post, required this.onLike, required this.onComment, this.showCommentComposer = true, this.hasCardBackground = true, this.showLikeCommentBar = true, }); final UserPostDto post; final VoidCallback onLike; final ValueChanged onComment; /// 是否显示卡片内嵌的评论预览 + 输入框(列表页 true;详情页由全量留言接管) final bool showCommentComposer; /// 是否显示白色卡片背景(列表页 true;详情页与留言区共用一张大卡片 → false) final bool hasCardBackground; /// 是否显示点赞/评论标记栏(列表页 true;详情页不需要 → false) final bool showLikeCommentBar; String get _avatar { final url = ImgUrl.build(post.avatar); return url.isEmpty ? ImgUrl.build('/StaticResource/2026-04-03/fake-avatar.png') : url; } String get _name => post.userName.isEmpty ? '默认姓名' : post.userName; String get _subject => post.subjectName.isEmpty ? '数学' : post.subjectName; String get _time { final t = post.createTime; if (t.isEmpty) return ''; final s = t.replaceFirst('T', ' ').replaceFirst('Z', ''); return s.length > 16 ? s.substring(0, 16) : s; } @override Widget build(BuildContext context) { final card = Stack( children: [ Positioned( top: 0, left: 0, right: 0, height: 85.h, child: IgnorePointer( // 卡片顶部右上角光晕,对标 Vue SubjectCard::before 的双层 radial-gradient child: CustomPaint(painter: _CardTopGlowPainter()), ), ), Padding( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 21.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CardHeader( avatar: _avatar, name: _name, subject: _subject, time: _time, ), if (post.type == 1) ...[ SizedBox(height: 5.h), ResolveProblemBox(desc: post.content), ], if (post.type == 2 || post.type == 3) ...[ SizedBox(height: 15.h), AlbumDesc(text: post.content), ], if (post.urls.isNotEmpty) ...[ SizedBox(height: 15.h), AlbumGrid(urls: post.urls.map(ImgUrl.build).toList()), ], SizedBox(height: 10.h), if (showLikeCommentBar) LikeCommentBar( post: post, onLike: onLike, onComment: onComment, showComposer: showCommentComposer, ), ], ), ), ], ); if (!hasCardBackground) return card; return Container( margin: EdgeInsets.only(bottom: 10.h), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15.r), ), child: card, ); } } /// 卡片顶部光晕:对标 Vue `SubjectCard::before` 的两层 radial-gradient /// (椭圆均以右上角为圆心,CSS 先写的在上层): /// radial-gradient(120% 85% at 100% 0%, /// rgba(206,233,255,1) 0%, rgba(206,233,255,.26) 10%, rgba(255,255,255,0) 58%) /// radial-gradient(75% 55% at 100% 0%, /// rgba(196,226,255,.88) 0%, rgba(255,255,255,1) 64%) class _CardTopGlowPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { // 先画下层(更小、偏白的光晕),再画上层(更大的浅蓝光晕) _paintGlow( canvas, size, rx: size.width * 0.75, ry: size.height * 0.55, colors: [const Color(0xFFC4E2FF).withValues(alpha: 0.88), Colors.white], stops: const [0, 0.64], ); _paintGlow( canvas, size, rx: size.width * 1.20, ry: size.height * 0.85, colors: [ const Color(0xFFCEE9FF), const Color(0xFFCEE9FF).withValues(alpha: 0.26), Colors.white.withValues(alpha: 0.0), ], stops: const [0, 0.10, 0.58], ); } /// 用 canvas.scale 把圆形径向渐变拉成 CSS 的椭圆(圆心 = 右上角) void _paintGlow( Canvas canvas, Size size, { required double rx, required double ry, required List colors, required List stops, }) { canvas.save(); canvas.translate(size.width, 0); canvas.scale(rx, ry); final Paint paint = Paint() ..shader = ui.Gradient.radial(Offset.zero, 1.0, colors, stops); canvas.drawRect(const Rect.fromLTWH(-1, 0, 1, 1), paint); canvas.restore(); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } /// 卡片标题:头像 + 姓名 + 学科标签 + 时间 class CardHeader extends StatelessWidget { const CardHeader({ super.key, required this.avatar, required this.name, required this.subject, required this.time, }); final String avatar; final String name; final String subject; final String time; @override Widget build(BuildContext context) { return Row( children: [ UserCircleAvatar(url: avatar, radius: 20.w), SizedBox(width: 12.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Flexible( child: Text( name, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1D2B47), ), ), ), SizedBox(width: 8.w), SubjectTag(subject: subject), ], ), SizedBox(height: 2.h), Text( time, style: const TextStyle(fontSize: 12, color: Color(0xFF495A7A)), ), ], ), ), ], ); } } /// 纯文字笔记(album-desc,最多 2 行) class AlbumDesc extends StatelessWidget { const AlbumDesc({super.key, required this.text}); final String text; @override Widget build(BuildContext context) { return Text( text, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w400, color: Color(0xFF495A7A), height: 42 / 30, ), ); } } /// 九宫格相册(最多展示 9 张,第 9 张叠加 +N) class AlbumGrid extends StatelessWidget { const AlbumGrid({super.key, required this.urls}); final List urls; @override Widget build(BuildContext context) { final displayed = urls.take(9).toList(); return Wrap( spacing: 9.w, runSpacing: 9.h, children: [ for (int i = 0; i < displayed.length; i++) GestureDetector( onTap: () => _preview(context, urls, i), child: _gridItem(displayed[i], i, urls.length), ), ], ); } /// 点击相册图:进入整组大图预览,定位到点中的那张 void _preview(BuildContext context, List allUrls, int index) { ImagePreviewViewer.show( context, items: [ for (final u in allUrls) PreviewImageItem(imageProvider: CachedNetworkImageProvider(u)), ], initialIndex: index, ); } Widget _gridItem(String url, int index, int total) { final more = total > 9 && index == 8 ? total - 9 : 0; return ClipRRect( borderRadius: BorderRadius.circular(8.r), child: SizedBox( width: 99.w, height: 99.h, child: Stack( fit: StackFit.expand, children: [ CachedNetworkImage( imageUrl: url, fit: BoxFit.cover, errorWidget: (_, _, _) => const ColoredBox(color: Color(0xFFF0F2F5)), ), if (more > 0) ColoredBox( color: const Color(0x66000000), child: Center( child: Text( '+$more', style: const TextStyle( fontSize: 25, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ], ), ), ); } } /// 点赞 / 评论栏(对标 LikeAndComment) class LikeCommentBar extends StatelessWidget { const LikeCommentBar({ super.key, required this.post, required this.onLike, required this.onComment, this.showComposer = true, }); final UserPostDto post; final VoidCallback onLike; final ValueChanged onComment; /// 是否显示评论预览 + 输入框(列表页 true;详情页 false,由全量留言列表接管) final bool showComposer; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.end, children: [ MetaButton( icon: Image.asset( post.userLikeStatus ? 'assets/img/class/like-check-icon.png' : 'assets/img/class/like-icon.png', width: 13.w, height: 13.w, ), count: post.likeCount, active: post.userLikeStatus, onTap: onLike, ), SizedBox(width: 8.w), MetaButton( icon: Image.asset( 'assets/img/class/comment-icon.png', width: 13.w, height: 13.w, ), count: post.replyCount, ), ], ), if (showComposer) ...[ if (post.commons.isNotEmpty) ...[ SizedBox(height: 12.h), CommentPreview(post: post), ], SizedBox(height: 12.h), CommentInput(onSubmit: onComment), ], ], ); } } class MetaButton extends StatelessWidget { const MetaButton({ super.key, required this.icon, required this.count, this.active = false, this.onTap, }); final Widget icon; final int count; final bool active; final VoidCallback? onTap; @override Widget build(BuildContext context) { final color = active ? const Color(0xFF3D7DF5) : const Color(0xFFB4C2E0); return GestureDetector( onTap: onTap, child: Container( width: 51.w, height: 22.h, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20.r), border: Border.all( color: const Color(0xFFB3C2E0).withValues(alpha: 0.3), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ icon, SizedBox(width: 4.w), Text('$count', style: TextStyle(fontSize: 13, color: color)), ], ), ), ); } } /// 评论预览框(前 3 条 + 共 N 条) class CommentPreview extends StatelessWidget { const CommentPreview({super.key, required this.post}); final UserPostDto post; @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 19.w, vertical: 12.h), decoration: BoxDecoration( color: const Color(0xFFF5F8FA), borderRadius: BorderRadius.circular(10.r), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ for (final c in post.commons.take(3)) Padding( padding: EdgeInsets.only(bottom: 8.h), child: Text.rich( TextSpan( children: [ TextSpan( text: '${c.userName}:', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w800, color: Color(0xFF1D2B47), ), ), TextSpan( text: c.content, style: const TextStyle( fontSize: 13, color: Color(0xFF495A7A), ), ), ], ), ), ), if (post.replyCount > 3) Row( children: [ Text( '共${post.replyCount}条评论', style: const TextStyle( fontSize: 12, color: Color(0xFF687FAD), ), ), const Icon( Icons.chevron_right, size: 14, color: Color(0xFF687FAD), ), ], ), ], ), ); } } /// 评论输入行 class CommentInput extends StatefulWidget { const CommentInput({super.key, required this.onSubmit}); final ValueChanged onSubmit; @override State createState() => _CommentInputState(); } class _CommentInputState extends State { final TextEditingController _controller = TextEditingController(); @override void dispose() { _controller.dispose(); super.dispose(); } void _send() { final text = _controller.text.trim(); if (text.isEmpty) return; widget.onSubmit(text); _controller.clear(); } @override Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: Container( constraints: BoxConstraints(minHeight: 30.h), padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.h), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: const Color(0xFFE3ECFF)), borderRadius: BorderRadius.circular(10.r), ), // multiline:键盘右下角为「换行」,可换行写多条评论 child: TextField( controller: _controller, style: const TextStyle(fontSize: 12, color: Color(0xFF1D2B47)), keyboardType: TextInputType.multiline, textInputAction: TextInputAction.newline, minLines: 1, maxLines: 4, decoration: const InputDecoration( isDense: true, border: InputBorder.none, hintText: '写下你的评论', hintStyle: TextStyle(fontSize: 12, color: Color(0xFFB4C2E0)), ), onSubmitted: (_) => _send(), ), ), ), SizedBox(width: 10.w), GestureDetector( onTap: _send, child: Container( width: 51.w, height: 30.h, alignment: Alignment.center, decoration: BoxDecoration( gradient: const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Color(0xFF8AD1FD), Color(0xFF3C84FF), Color(0xFF3D7DF5), ], ), borderRadius: BorderRadius.circular(6.r), ), child: const Text( '发送', style: TextStyle(fontSize: 13, color: Colors.white), ), ), ), ], ); } }