flutter架 构项目基础

This commit is contained in:
崔宝新
2026-09-04 15:22:20 +08:00
parent 70967229d0
commit 0c5aee59c0
265 changed files with 35484 additions and 0 deletions
@@ -0,0 +1,578 @@
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/user_circle_avatar.dart';
import 'package:new_learn/core/utils/img_url.dart';
/// ============================================================
/// 【班级动态组件】由 class_update_view.dart 抽离的展示组件
///
/// PostCard(动态卡片,按 type 分发内容 + 相册 + 点赞评论栏)
/// CardHeader / SubjectTag / ResolveProblemBox / AlbumDesc / AlbumGrid
/// 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,
});
final UserPostDto post;
final VoidCallback onLike;
final ValueChanged<String> onComment;
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) {
return Container(
margin: EdgeInsets.only(bottom: 10.h),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15.r),
),
child: Stack(
children: [
Positioned(
top: 0,
left: 0,
right: 0,
height: 85.h,
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomLeft,
colors: [
const Color(0xFFCEE9FF).withValues(alpha: 0.9),
const Color(0xFFF6FBFF).withValues(alpha: 0.0),
],
),
),
),
),
),
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),
LikeCommentBar(
post: post,
onLike: onLike,
onComment: onComment,
),
],
),
),
],
),
);
}
}
/// 卡片标题:头像 + 姓名 + 学科标签 + 时间
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)),
),
],
),
),
],
);
}
}
/// 学科标签(对标 SubjectTag)
class SubjectTag extends StatelessWidget {
const SubjectTag({super.key, required this.subject});
final String subject;
static const Map<String, _SubjectColor> _map = {
'数学': _SubjectColor(0xFF3D7DF5, 0xFFE5F0FF),
'物理': _SubjectColor(0xFFA45CFF, 0xFFF5ECFF),
'英语': _SubjectColor(0xFFE5A301, 0xFFFFF6E3),
'化学': _SubjectColor(0xFF2DB985, 0xFFE5FAF3),
};
@override
Widget build(BuildContext context) {
final t = subject.trim();
if (t.isEmpty) return const SizedBox.shrink();
final c = _map[t] ?? const _SubjectColor(0xFF3D7DF5, 0xFFE5F0FF);
return Container(
padding: EdgeInsets.symmetric(horizontal: 7.w, vertical: 2.h),
decoration: BoxDecoration(
color: Color(c.bg),
borderRadius: BorderRadius.circular(6.r),
),
child: Text(t, style: TextStyle(fontSize: 11, color: Color(c.fg))),
);
}
}
/// 攻克任务卡(对标 ResolveProblem)
class ResolveProblemBox extends StatelessWidget {
const ResolveProblemBox({super.key, required this.desc});
final String desc;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: EdgeInsets.fromLTRB(19.w, 25.h, 19.w, 11.h),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFEAF4FF), Color(0xFFFFFFFF)],
),
borderRadius: BorderRadius.circular(10.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'刚刚攻克了',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w900,
color: Color(0xFF1D2B47),
),
),
SizedBox(height: 3.h),
Text.rich(
TextSpan(
children: [
TextSpan(
text: '「$desc」',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Color(0xFF1D2B47),
),
),
const TextSpan(
text: ' 的所有高难考点!',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Color(0xFF45597E),
),
),
],
),
),
],
),
);
}
}
/// 纯文字笔记(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<String> 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++)
_gridItem(displayed[i], i, urls.length),
],
);
}
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,
});
final UserPostDto post;
final VoidCallback onLike;
final ValueChanged<String> onComment;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
MetaButton(
icon: post.userLikeStatus
? Icons.favorite
: Icons.favorite_border,
count: post.likeCount,
active: post.userLikeStatus,
onTap: onLike,
),
SizedBox(width: 8.w),
MetaButton(icon: Icons.chat_bubble_outline, count: post.replyCount),
],
),
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 IconData 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(icon, size: 13.w, color: color),
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<String> onSubmit;
@override
State<CommentInput> createState() => _CommentInputState();
}
class _CommentInputState extends State<CommentInput> {
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(
children: [
Expanded(
child: Container(
height: 30.h,
padding: EdgeInsets.symmetric(horizontal: 12.w),
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: const Color(0xFFE3ECFF)),
borderRadius: BorderRadius.circular(10.r),
),
child: TextField(
controller: _controller,
style: const TextStyle(fontSize: 12, color: Color(0xFF1D2B47)),
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),
),
),
),
],
);
}
}
class _SubjectColor {
const _SubjectColor(this.fg, this.bg);
final int fg;
final int bg;
}