feat:增加博客卡片内容

This commit is contained in:
2025-11-14 23:22:01 +08:00
parent 94849be69c
commit 3b167508dd
18 changed files with 815 additions and 339 deletions

31
lib/apis/blog.dart Normal file
View File

@@ -0,0 +1,31 @@
import 'package:blog_app/models/blog.dart';
import 'package:blog_app/models/common.dart';
import 'package:blog_app/utils/http_utils.dart';
import 'package:blog_app/utils/index.dart';
Future<PageResult<Blog>> queryBlogByPageApi(int currentPage, int pageSize) {
return HttpUtil().get<PageResult<Blog>>(
"/blog/page",
queryParameters: {"currentPage": currentPage, "pageSize": pageSize},
converter: (data) => convertPageResponse(data, Blog.fromJson),
);
}
Future<List<Blog>> queryBlogByConditionApi(
String category,
String title,
int year,
) {
return HttpUtil().get<List<Blog>>(
"/blog/condition",
queryParameters: {"category": category, "title": title, "year": year},
converter: (data) => convertListResponse<Blog>(data, Blog.fromJson),
);
}
Future<Blog> queryBlogByIdApi(int id) {
return HttpUtil().get<Blog>(
"blog/$id/content",
converter: (data) => Blog.fromJson(data),
);
}

View File

@@ -3,6 +3,6 @@ class AppConfig {
// 网络配置
// http://192.168.1.3:8100
// http://14.103.235.151:81/food-service
static const String baseApiUrl = "https://cxx0822.iepose.cn/blog-api/blog";
static const String baseApiUrl = "https://cxx0822.iepose.cn/blog-api";
// static const String baseApiUrl = "http://172.29.101.108:8100";
}

View File

@@ -1,6 +1,10 @@
import 'package:blog_app/pages/home_page.dart';
import 'package:blog_app/utils/sp_utils.dart';
import 'package:flutter/material.dart';
void main() {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await SPUtil.init();
runApp(MyApp());
}
@@ -8,193 +12,12 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('博客卡片Demo'),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.blue.shade50, Colors.purple.shade50],
),
),
child: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
children: [
BlogCard(
title: 'Flutter开发实战',
description: '本文详细介绍了如何使用Flutter开发高性能的跨平台应用包含最佳实践和性能优化技巧。通过实际案例演示了状态管理、路由导航、网络请求等核心功能的实现方式帮助开发者快速掌握Flutter开发精髓。',
labels: [
BlogLabel('精品', Icons.star, Colors.amber),
BlogLabel('置顶', Icons.push_pin, Colors.red),
BlogLabel('技术', Icons.category, Colors.blue),
BlogLabel('2024-01-15', Icons.calendar_today, Colors.green),
BlogLabel('2024-01-20', Icons.update, Colors.orange),
BlogLabel('2.3k', Icons.remove_red_eye, Colors.purple),
BlogLabel('3.2k字', Icons.text_fields, Colors.indigo),
BlogLabel('8分钟', Icons.access_time, Colors.brown),
BlogLabel('24', Icons.comment, Colors.teal),
],
),
SizedBox(height: 20),
BlogCard(
title: 'Dart语言特性深入解析',
description: '深入探讨Dart语言的现代特性包括空安全、异步编程、扩展方法等高级功能。结合实例讲解如何利用Dart的特性编写更安全、更高效的代码提升开发效率和代码质量。',
labels: [
BlogLabel('推荐', Icons.thumb_up, Colors.pink),
BlogLabel('编程', Icons.code, Colors.blue),
BlogLabel('2024-01-10', Icons.calendar_today, Colors.green),
BlogLabel('2024-01-18', Icons.update, Colors.orange),
BlogLabel('1.5k', Icons.remove_red_eye, Colors.purple),
BlogLabel('2.1k字', Icons.text_fields, Colors.indigo),
BlogLabel('6分钟', Icons.access_time, Colors.brown),
BlogLabel('18', Icons.comment, Colors.teal),
],
),
],
),
),
)
),
home: HomePage(),
debugShowCheckedModeBanner: false,
theme: ThemeData(
fontFamily: 'CustomFont'
scaffoldBackgroundColor: Color(0xFFF5F5F5),
fontFamily: 'CustomFont',
),
);
}
}
class BlogLabel {
final String text;
final IconData icon;
final Color color;
BlogLabel(this.text, this.icon, this.color);
}
class BlogCard extends StatelessWidget {
final String title;
final String description;
final List<BlogLabel> labels;
const BlogCard({
super.key,
required this.title,
required this.description,
required this.labels,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
width: double.infinity, // 确保宽度对齐
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.white, Colors.grey.shade50],
),
),
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 标题 - 居中
Text(
title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 16),
// 单行标签 - 居中,更小的尺寸
Container(
width: double.infinity,
child: Wrap(
spacing: 6,
runSpacing: 6,
alignment: WrapAlignment.center,
children: labels.map((label) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: label.color.withAlpha(30),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: label.color.withAlpha(20),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(label.icon, size: 12, color: label.color),
SizedBox(width: 4),
Text(
label.text,
style: TextStyle(
fontSize: 10,
color: label.color,
fontWeight: FontWeight.w500,
),
),
],
),
);
}).toList(),
),
),
SizedBox(height: 16),
// 分隔线
Container(
height: 1,
width: 80,
color: Colors.blue.shade200,
),
SizedBox(height: 16),
// 描述 - 放在下面大约100字
Container(
width: double.infinity,
child: Text(
description,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade700,
height: 1.6,
),
textAlign: TextAlign.justify,
maxLines: 4,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
);
}
}

62
lib/models/blog.dart Normal file
View File

@@ -0,0 +1,62 @@
import 'package:json_annotation/json_annotation.dart';
// flutter pub run build_runner build
part 'blog.g.dart';
@JsonSerializable()
class Blog {
@JsonKey(name: 'id')
final int id;
@JsonKey(name: 'title')
final String title;
@JsonKey(name: 'topValue')
final int topValue;
@JsonKey(name: 'isGreat')
final bool isGreat;
@JsonKey(name: 'category')
final String category;
@JsonKey(name: 'summary')
final String summary;
@JsonKey(name: 'content')
final String? content;
@JsonKey(name: 'wordCount')
final int wordCount;
@JsonKey(name: 'readDuration')
final int readDuration;
@JsonKey(name: 'visitCount')
final int visitCount;
@JsonKey(name: 'createTime')
final String createTime;
@JsonKey(name: 'updateTime')
final String updateTime;
Blog({
required this.id,
required this.title,
required this.topValue,
required this.isGreat,
required this.category,
required this.summary,
this.content,
required this.wordCount,
required this.readDuration,
required this.visitCount,
required this.createTime,
required this.updateTime,
});
factory Blog.fromJson(Map<String, dynamic> json) => _$BlogFromJson(json);
Map<String, dynamic> toJson() => _$BlogToJson(this);
}

37
lib/models/blog.g.dart Normal file
View File

@@ -0,0 +1,37 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'blog.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Blog _$BlogFromJson(Map<String, dynamic> json) => Blog(
id: (json['id'] as num).toInt(),
title: json['title'] as String,
topValue: (json['topValue'] as num).toInt(),
isGreat: json['isGreat'] as bool,
category: json['category'] as String,
summary: json['summary'] as String,
content: json['content'] as String?,
wordCount: (json['wordCount'] as num).toInt(),
readDuration: (json['readDuration'] as num).toInt(),
visitCount: (json['visitCount'] as num).toInt(),
createTime: json['createTime'] as String,
updateTime: json['updateTime'] as String,
);
Map<String, dynamic> _$BlogToJson(Blog instance) => <String, dynamic>{
'id': instance.id,
'title': instance.title,
'topValue': instance.topValue,
'isGreat': instance.isGreat,
'category': instance.category,
'summary': instance.summary,
'content': instance.content,
'wordCount': instance.wordCount,
'readDuration': instance.readDuration,
'visitCount': instance.visitCount,
'createTime': instance.createTime,
'updateTime': instance.updateTime,
};

37
lib/models/common.dart Normal file
View File

@@ -0,0 +1,37 @@
import 'package:json_annotation/json_annotation.dart';
part 'common.g.dart';
@JsonSerializable(genericArgumentFactories: true)
class PageResult<T> {
@JsonKey(name: 'records')
final List<T> records;
@JsonKey(name: 'total')
final int total;
@JsonKey(name: 'size')
final int size;
@JsonKey(name: 'current')
final int current;
@JsonKey(name: 'pages')
final int pages;
const PageResult({
required this.records,
required this.total,
required this.size,
required this.current,
required this.pages,
});
factory PageResult.fromJson(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) => _$PageResultFromJson(json, fromJsonT);
Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$PageResultToJson(this, toJsonT);
}

29
lib/models/common.g.dart Normal file
View File

@@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'common.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PageResult<T> _$PageResultFromJson<T>(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) => PageResult<T>(
records: (json['records'] as List<dynamic>).map(fromJsonT).toList(),
total: (json['total'] as num).toInt(),
size: (json['size'] as num).toInt(),
current: (json['current'] as num).toInt(),
pages: (json['pages'] as num).toInt(),
);
Map<String, dynamic> _$PageResultToJson<T>(
PageResult<T> instance,
Object? Function(T value) toJsonT,
) => <String, dynamic>{
'records': instance.records.map(toJsonT).toList(),
'total': instance.total,
'size': instance.size,
'current': instance.current,
'pages': instance.pages,
};

38
lib/pages/blog_page.dart Normal file
View File

@@ -0,0 +1,38 @@
import 'package:blog_app/apis/blog.dart';
import 'package:blog_app/models/blog.dart';
import 'package:blog_app/widget/blog.dart';
import 'package:flutter/material.dart';
class BlogPage extends StatefulWidget {
const BlogPage({super.key});
@override
State<BlogPage> createState() => _BlogPageState();
}
class _BlogPageState extends State<BlogPage> {
late List<Blog> blogList = [];
@override
void initState() {
super.initState();
// 初始加载数据
_loadData();
}
Future<void> _loadData() async {
final result = await queryBlogByPageApi(1, 10);
setState(() {
blogList = result.records;
});
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: blogList.length,
itemBuilder: (context, index) => BlogCard(blog: blogList[index]),
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:blog_app/pages/about_page.dart';
import 'package:blog_app/pages/blog_page.dart';
import 'package:blog_app/pages/message_page.dart';
import 'package:blog_app/pages/profile_page.dart';
import 'package:blog_app/pages/settings_page.dart';
@@ -14,16 +15,11 @@ class _HomePageState extends State<HomePage> {
late PageController _pageController;
// 页面标题列表
final List<String> _pageTitles = [
'个人资料',
'消息中心',
'设置',
'关于我们',
];
final List<String> _pageTitles = ['博客', '消息中心', '设置', '关于我们'];
// 页面图标列表
final List<IconData> _pageIcons = [
Icons.home,
Icons.article,
Icons.person,
Icons.message,
Icons.settings,
@@ -42,18 +38,34 @@ class _HomePageState extends State<HomePage> {
super.dispose();
}
Widget _buildPageView() {
return PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentPageIndex = index;
});
},
children: [BlogPage(), MessagePage(), SettingsPage(), AboutPage()],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_pageTitles[_currentPageIndex]),
backgroundColor: Colors.blue,
title: Text(
_pageTitles[_currentPageIndex],
style: TextStyle(color: Colors.white),
),
backgroundColor: Theme.of(context).colorScheme.primary,
elevation: 0,
leading: Builder(
builder: (context) => IconButton(
icon: Icon(Icons.menu, color: Colors.white),
onPressed: () => Scaffold.of(context).openDrawer(),
),
builder:
(context) => IconButton(
icon: Icon(Icons.menu, color: Colors.white),
onPressed: () => Scaffold.of(context).openDrawer(),
),
),
actions: [
IconButton(
@@ -63,20 +75,7 @@ class _HomePageState extends State<HomePage> {
],
),
drawer: _buildDrawer(),
body: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentPageIndex = index;
});
},
children: [
ProfilePage(),
MessagePage(),
SettingsPage(),
AboutPage(),
],
),
body: Padding(padding: EdgeInsets.all(10), child: _buildPageView()),
);
}
@@ -94,12 +93,13 @@ class _HomePageState extends State<HomePage> {
child: ListView(
padding: EdgeInsets.zero,
children: [
...List.generate(_pageTitles.length, (index) =>
_buildDrawerItem(
icon: _pageIcons[index],
title: _pageTitles[index],
index: index,
)
...List.generate(
_pageTitles.length,
(index) => _buildDrawerItem(
icon: _pageIcons[index],
title: _pageTitles[index],
index: index,
),
),
],
),
@@ -128,11 +128,7 @@ class _HomePageState extends State<HomePage> {
CircleAvatar(
radius: 40,
backgroundColor: Colors.white.withOpacity(0.3),
child: Icon(
Icons.person,
size: 50,
color: Colors.white,
),
child: Icon(Icons.person, size: 50, color: Colors.white),
),
SizedBox(height: 16),
Text(
@@ -146,10 +142,7 @@ class _HomePageState extends State<HomePage> {
SizedBox(height: 4),
Text(
'user@example.com',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
style: TextStyle(color: Colors.white70, fontSize: 14),
),
],
),
@@ -174,24 +167,31 @@ class _HomePageState extends State<HomePage> {
child: ListTile(
leading: Icon(
icon,
color: isSelected ? Colors.blue :
isSpecialItem ? Colors.grey[600] : Colors.grey[700],
color:
isSelected
? Colors.blue
: isSpecialItem
? Colors.grey[600]
: Colors.grey[700],
size: 24,
),
title: Text(
title,
style: TextStyle(
color: isSelected ? Colors.blue :
isSpecialItem ? Colors.grey[600] : Colors.grey[800],
color:
isSelected
? Colors.blue
: isSpecialItem
? Colors.grey[600]
: Colors.grey[800],
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 16,
),
),
trailing: isSelected ? Icon(
Icons.arrow_forward_ios,
size: 16,
color: Colors.blue,
) : null,
trailing:
isSelected
? Icon(Icons.arrow_forward_ios, size: 16, color: Colors.blue)
: null,
onTap: () {
if (!isSpecialItem) {
// 正常页面切换
@@ -228,101 +228,3 @@ class _HomePageState extends State<HomePage> {
}
}
}
// 各个页面组件
class HomeContentPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 欢迎卡片
Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: EdgeInsets.all(20),
child: Row(
children: [
Icon(Icons.waving_hand, size: 40, color: Colors.amber),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'欢迎回来!',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
'今天也是美好的一天',
style: TextStyle(color: Colors.grey),
),
],
),
),
],
),
),
),
SizedBox(height: 20),
// 功能网格
GridView.count(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
_buildFeatureCard('数据统计', Icons.analytics, Colors.blue),
_buildFeatureCard('文件管理', Icons.folder, Colors.green),
_buildFeatureCard('消息通知', Icons.notifications, Colors.orange),
_buildFeatureCard('系统设置', Icons.settings, Colors.purple),
],
),
],
),
);
}
Widget _buildFeatureCard(String title, IconData icon, Color color) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {},
child: Container(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 40, color: color),
SizedBox(height: 8),
Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
}

View File

@@ -1,4 +1,4 @@
import 'package:blog_app/utils/http_util.dart';
import 'package:blog_app/utils/http_utils.dart';
import 'package:flutter/material.dart';
import 'package:markdown_widget/config/toc.dart';
import 'package:markdown_widget/widget/markdown.dart';

13
lib/utils/date_utils.dart Normal file
View File

@@ -0,0 +1,13 @@
import 'package:intl/intl.dart';
String formatDate(DateTime date) {
return DateFormat('yyyy-MM-dd').format(date);
}
String formatDateString(String date) {
return DateFormat('yyyy-MM-dd').format(DateTime.parse(date));
}
String formatTime(DateTime datetime) {
return DateFormat('yyyy-MM-dd HH:mm:ss').format(datetime);
}

View File

@@ -1,8 +1,8 @@
import 'package:blog_app/config/app_config.dart';
import 'package:blog_app/utils/sp_util.dart';
import 'package:blog_app/utils/sp_utils.dart';
import 'package:dio/dio.dart';
import 'log_util.dart';
import 'log_utils.dart';
class HttpUtil {
static final HttpUtil _instance = HttpUtil._internal();

28
lib/utils/index.dart Normal file
View File

@@ -0,0 +1,28 @@
import 'package:blog_app/models/common.dart';
List<T> convertListResponse<T>(
dynamic data,
T Function(Map<String, dynamic>) fromJson,
) {
if (data is List) {
return data.map((item) => fromJson(item as Map<String, dynamic>)).toList();
}
throw FormatException(
'Expected a list of items for conversion, but got ${data.runtimeType}',
);
}
PageResult<T> convertPageResponse<T>(
dynamic data,
T Function(Map<String, dynamic>) fromJson,
) {
if (data is Map<String, dynamic>) {
return PageResult<T>.fromJson(
data,
(json) => fromJson(json as Map<String, dynamic>),
);
}
throw FormatException(
'Expected a Map for page response conversion, but got ${data.runtimeType}',
);
}

144
lib/widget/blog.dart Normal file
View File

@@ -0,0 +1,144 @@
import 'package:blog_app/models/blog.dart';
import 'package:blog_app/utils/date_utils.dart';
import 'package:flutter/material.dart';
class BlogLabel {
final String text;
final IconData icon;
final Color color;
BlogLabel(this.text, this.icon, this.color);
}
class BlogCard extends StatelessWidget {
final Blog blog;
const BlogCard({super.key, required this.blog});
Widget _buildBlogLabelItem(String text, IconData icon, Color color) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withAlpha(30),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withAlpha(20), width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 12, color: color),
SizedBox(width: 4),
Text(
text,
style: TextStyle(
fontSize: 10,
color: color,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildBlogLabel() {
return SizedBox(
width: double.infinity,
child: Wrap(
spacing: 6,
runSpacing: 6,
alignment: WrapAlignment.center,
children: [
if (blog.isGreat)
_buildBlogLabelItem('精品', Icons.workspace_premium, Colors.amber),
if (blog.topValue > 1)
_buildBlogLabelItem('置顶', Icons.push_pin, Colors.red),
_buildBlogLabelItem(blog.category, Icons.folder, Colors.blue),
_buildBlogLabelItem(
formatDateString(blog.createTime),
Icons.calendar_today,
Colors.green,
),
_buildBlogLabelItem(
formatDateString(blog.updateTime),
Icons.calendar_month,
Colors.orange,
),
_buildBlogLabelItem(
'${blog.visitCount}',
Icons.remove_red_eye,
Colors.purple,
),
_buildBlogLabelItem(
'${blog.wordCount}',
Icons.text_fields,
Colors.indigo,
),
_buildBlogLabelItem(
'${blog.readDuration} 分钟',
Icons.access_time,
Colors.brown,
),
],
),
);
}
Widget _buildBlogTitle(BuildContext context) {
return Text(
blog.title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
}
Widget _buildBlogSummary() {
return SizedBox(
width: double.infinity,
child: Text(
blog.summary,
style: TextStyle(color: Colors.grey.shade700, height: 1.6),
overflow: TextOverflow.ellipsis,
),
);
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Card(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: colors.outline.withAlpha(50), width: 1),
),
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildBlogTitle(context),
SizedBox(height: 16),
_buildBlogLabel(),
SizedBox(height: 16),
Container(height: 1, width: 80, color: Theme.of(context).colorScheme.primary),
SizedBox(height: 16),
_buildBlogSummary(),
],
),
),
),
);
}
}