flutter架 构项目基础
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user