📖 短故事模块
版本 PanGrowth
📊 短故事信息接口
提供查询短故事列表、分类、历史记录、收藏、点赞等信息的API接口。
🚀 快速开始
import 'package:pangrowth_content/pangrowth_content.dart';
// 获取短故事列表
final stories = await PangrowthContent.getStoryList(
page: 1,
count: 20,
categoryId: 1,
);
// 搜索短故事
final results = await PangrowthContent.searchStory(
'言情',
page: 1,
count: 20,
);
// 收藏短故事
await PangrowthContent.favoriteStory(
'story_123',
isFavorite: true,
);
📖 API列表
1. 获取分类列表
Future<List<Map<String, dynamic>>> getStoryCategoryList()
获取所有短故事分类信息。
返回值: 分类列表,每个分类包含分类信息的Map
示例:
final categories = await PangrowthContent.getStoryCategoryList();
for (var category in categories) {
print('分类: ${category['name']}, ID: ${category['id']}');
}
2. 获取短故事列表
Future<List<StoryInfo>> getStoryList({
int? page, // 页码,从1开始
int? count, // 每页数量
bool? order, // 排序方式: true正序/false倒序
int? categoryId, // 分类ID筛选(可选)
})
获取短故事列表,支持分页和分类筛选。
参数说明:
page
: 页码,从1开始,不传则使用默认值count
: 每页数量,不传则使用默认值(通常20)order
: 排序方式,true为正序,false为倒序categoryId
: 可选,指定分类ID进行筛选
返回值: List<StoryInfo>
短故事信息列表
示例:
// 获取第一页(默认参数)
final stories = await PangrowthContent.getStoryList();
// 获取指定分类的短故事
final categoryStories = await PangrowthContent.getStoryList(
page: 1,
count: 20,
categoryId: 5,
order: false, // 倒序
);
3. 搜索短故事
Future<List<StoryInfo>> searchStory(
String keyword, {
bool? isFuzzy, // 是否模糊搜索,默认true
int? page, // 页码,从1开始
int? count, // 每页数量
})
根据关键词搜索短故事。
参数说明:
keyword
: 搜索关键词(必填)isFuzzy
: 是否模糊搜索,默认truepage
: 页码,从1开始count
: 每页数量
返回值: List<StoryInfo>
匹配的短故事列表
示例:
// 模糊搜索
final results = await PangrowthContent.searchStory(
'爱情',
isFuzzy: true,
page: 1,
count: 20,
);
// 精确搜索
final exactResults = await PangrowthContent.searchStory(
'霸道总裁',
isFuzzy: false,
);
4. 获取阅读历史
Future<List<StoryInfo>> getStoryHistory({
int? page, // 页码,从1开始
int? count, // 每页数量
})
获取用户的短故事阅读历史记录。
参数说明:
page
: 页码,从1开始count
: 每页数量
返回值: List<StoryInfo>
历史记录列表,按阅读时间倒序
示例:
final history = await PangrowthContent.getStoryHistory(
page: 1,
count: 20,
);
for (var story in history) {
print('上次阅读: ${story.title}, 进度: ${story.progress}');
}
5. 收藏短故事
Future<bool> favoriteStory(
String storyId, {
bool isFavorite = true, // 是否收藏,默认true
})
收藏或取消收藏短故事。
参数说明:
storyId
: 短故事ID(必填,String类型)isFavorite
: true收藏,false取消收藏
返回值: bool
操作是否成功
示例:
// 收藏短故事
final success = await PangrowthContent.favoriteStory('story_123');
// 取消收藏
final removed = await PangrowthContent.favoriteStory(
'story_123',
isFavorite: false,
);
6. 获取收藏列表
Future<List<StoryInfo>> getFavoriteStories({
int? page, // 页码,从1开始
int? count, // 每页数量
})
获取用户收藏的短故事列表。
参数说明:
page
: 页码,从1开始count
: 每页数量
返回值: List<StoryInfo>
收藏的短故事列表
示例:
final favorites = await PangrowthContent.getFavoriteStories(
page: 1,
count: 20,
);
for (var story in favorites) {
print('收藏时间: ${story.favoriteTime}');
}
7. 标记为已读
Future<bool> markAsRead(String storyId)
标记短故事为已读状态。
参数说明:
storyId
: 短故事ID(必填,String类型)
返回值: bool
操作是否成功
示例:
await PangrowthContent.markAsRead('story_123');
8. 点赞短故事
Future<bool> likeStory(
String storyId, {
bool isLiked = true, // 是否点赞,默认true
})
点赞或取消点赞短故事。
参数说明:
storyId
: 短故事ID(必填,String类型)isLiked
: true点赞,false取消点赞
返回值: bool
操作是否成功
示例:
// 点赞
await PangrowthContent.likeStory('story_123');
// 取消点赞
await PangrowthContent.likeStory('story_123', isLiked: false);
9. 更新阅读进度
Future<bool> updateReadProgress(
String storyId,
int readDuration, // 阅读时长,单位毫秒
)
更新短故事的阅读进度和阅读时长。
参数说明:
storyId
: 短故事ID(必填,String类型)readDuration
: 本次阅读时长,单位毫秒
返回值: bool
操作是否成功
示例:
// 记录阅读了5分钟
await PangrowthContent.updateReadProgress('story_123', 5 * 60 * 1000);
📊 StoryInfo 数据结构
短故事信息模型,包含短故事的完整信息。
class StoryInfo {
/// 短故事ID(String类型)
final String storyId;
/// 标题
final String title;
/// 简介/描述
final String description;
/// 作者
final String author;
/// 封面图URL
final String coverUrl;
/// 封面图类型: 0封面图、1默认图、2无图
final int imageType;
/// 分类ID
final int categoryId;
/// 分类名称
final String categoryName;
/// 总章节数
final int totalChapters;
/// 创建时间(毫秒级时间戳)
final int createTime;
/// 当前阅读章节(从0开始)
final int currentIndex;
/// 当前章节阅读进度 0.0~1.0
final double progress;
/// 阅读统计次数
final int statsCount;
/// 是否已收藏
final bool isFavorited;
/// 收藏时间(秒级时间戳,0表示未收藏)
final int favoriteTime;
/// 第一章内容预览
final String contentPreview;
/// 额外透传字段
final Map<String, dynamic> extras;
}
字段说明
字段 | 类型 | 说明 |
---|---|---|
storyId | String | 短故事唯一标识(字符串形式) |
title | String | 短故事标题 |
description | String | 简介/描述 |
author | String | 作者名称 |
coverUrl | String | 封面图URL |
imageType | int | 封面类型: 0封面图/1默认图/2无图 |
categoryId | int | 所属分类ID |
categoryName | String | 所属分类名称 |
totalChapters | int | 总章节数 |
createTime | int | 创建时间(毫秒时间戳) |
currentIndex | int | 当前阅读章节索引(从0开始) |
progress | double | 当前章节阅读进度(0.0-1.0) |
statsCount | int | 阅读统计次数 |
isFavorited | bool | 是否已收藏 |
favoriteTime | int | 收藏时间(秒级时间戳,0表示未收藏) |
contentPreview | String | 第一章内容预览 |
extras | Map<String, dynamic> | 额外透传字段 |
使用示例
final story = stories.first;
print('故事ID: ${story.storyId}');
print('标题: ${story.title}');
print('作者: ${story.author}');
print('分类: ${story.categoryName}');
print('总章节: ${story.totalChapters}章');
print('当前进度: 第${story.currentIndex + 1}章, ${(story.progress * 100).toStringAsFixed(1)}%');
print('是否收藏: ${story.isFavorited ? "是" : "否"}');
print('阅读次数: ${story.statsCount}');
// 显示封面图
if (story.imageType == 0) {
Image.network(story.coverUrl);
}
💡 使用场景示例
场景1: 分类浏览
class StoryBrowsePage extends StatefulWidget {
@override
_StoryBrowsePageState createState() => _StoryBrowsePageState();
}
class _StoryBrowsePageState extends State<StoryBrowsePage> {
List<Map<String, dynamic>> _categories = [];
List<StoryInfo> _stories = [];
int? _selectedCategoryId;
@override
void initState() {
super.initState();
_loadCategories();
}
Future<void> _loadCategories() async {
final categories = await PangrowthContent.getStoryCategoryList();
setState(() => _categories = categories);
// 默认加载第一个分类
if (categories.isNotEmpty) {
_loadStoriesByCategory(categories.first['id']);
}
}
Future<void> _loadStoriesByCategory(int categoryId) async {
final stories = await PangrowthContent.getStoryList(
page: 1,
count: 20,
categoryId: categoryId,
);
setState(() {
_selectedCategoryId = categoryId;
_stories = stories;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('短故事分类')),
body: Column(
children: [
// 分类标签栏
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _categories.map((category) {
final isSelected = category['id'] == _selectedCategoryId;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: ChoiceChip(
label: Text(category['name']),
selected: isSelected,
onSelected: (_) => _loadStoriesByCategory(category['id']),
),
);
}).toList(),
),
),
// 故事列表
Expanded(
child: ListView.builder(
itemCount: _stories.length,
itemBuilder: (context, index) {
final story = _stories[index];
return ListTile(
leading: story.imageType == 0
? Image.network(story.coverUrl, width: 60)
: const Icon(Icons.book),
title: Text(story.title),
subtitle: Text('${story.author} · ${story.totalChapters}章'),
trailing: IconButton(
icon: Icon(
story.isFavorited ? Icons.favorite : Icons.favorite_border,
color: story.isFavorited ? Colors.red : null,
),
onPressed: () => _toggleFavorite(story),
),
onTap: () => _openReader(story),
);
},
),
),
],
),
);
}
Future<void> _toggleFavorite(StoryInfo story) async {
final success = await PangrowthContent.favoriteStory(
story.storyId,
isFavorite: !story.isFavorited,
);
if (success) {
// 刷新列表
_loadStoriesByCategory(_selectedCategoryId!);
}
}
void _openReader(StoryInfo story) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => StoryReaderPage(
storyId: int.parse(story.storyId),
storyName: story.title,
),
),
);
}
}
场景2: 搜索功能
class StorySearchPage extends StatefulWidget {
@override
_StorySearchPageState createState() => _StorySearchPageState();
}
class _StorySearchPageState extends State<StorySearchPage> {
final TextEditingController _searchController = TextEditingController();
List<StoryInfo> _searchResults = [];
bool _isSearching = false;
Future<void> _performSearch(String keyword) async {
if (keyword.isEmpty) return;
setState(() => _isSearching = true);
try {
final results = await PangrowthContent.searchStory(
keyword,
isFuzzy: true,
page: 1,
count: 50,
);
setState(() => _searchResults = results);
} finally {
setState(() => _isSearching = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: TextField(
controller: _searchController,
decoration: const InputDecoration(
hintText: '搜索短故事...',
border: InputBorder.none,
),
onSubmitted: _performSearch,
),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () => _performSearch(_searchController.text),
),
],
),
body: _isSearching
? const Center(child: CircularProgressIndicator())
: _searchResults.isEmpty
? const Center(child: Text('暂无搜索结果'))
: ListView.builder(
itemCount: _searchResults.length,
itemBuilder: (context, index) {
final story = _searchResults[index];
return ListTile(
leading: story.imageType == 0
? Image.network(story.coverUrl, width: 60)
: null,
title: Text(story.title),
subtitle: Text(story.description),
onTap: () {
// 跳转到阅读器
},
);
},
),
);
}
}
场景3: 阅读历史和进度管理
class StoryHistoryPage extends StatefulWidget {
@override
_StoryHistoryPageState createState() => _StoryHistoryPageState();
}
class _StoryHistoryPageState extends State<StoryHistoryPage> {
List<StoryInfo> _history = [];
@override
void initState() {
super.initState();
_loadHistory();
}
Future<void> _loadHistory() async {
final history = await PangrowthContent.getStoryHistory(
page: 1,
count: 50,
);
setState(() => _history = history);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('阅读历史')),
body: ListView.builder(
itemCount: _history.length,
itemBuilder: (context, index) {
final story = _history[index];
final progressPercent = (story.progress * 100).toStringAsFixed(0);
return Card(
margin: const EdgeInsets.all(8),
child: ListTile(
leading: story.imageType == 0
? Image.network(story.coverUrl, width: 60)
: const Icon(Icons.book),
title: Text(story.title),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('已读至: 第${story.currentIndex + 1}章'),
LinearProgressIndicator(
value: story.progress,
backgroundColor: Colors.grey[300],
),
Text('进度: $progressPercent%'),
],
),
trailing: IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: () => _continueReading(story),
),
),
);
},
),
);
}
void _continueReading(StoryInfo story) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => StoryReaderPage(
storyId: int.parse(story.storyId),
storyName: story.title,
startChapter: story.currentIndex,
),
),
);
}
}
⚠️ 注意事项
1. ID类型
- 短故事ID (
storyId
) 是 String 类型,不是 int - 分类ID (
categoryId
) 是 int 类型 - 使用时注意类型转换
2. 时间戳格式
createTime
: 毫秒级时间戳favoriteTime
: 秒级时间戳- 注意转换时的单位差异
3. 进度值范围
progress
: 0.0 ~ 1.0 之间的浮点数- 显示百分比时需要乘以100
4. 封面图类型
imageType = 0
: 有封面图,使用coverUrl
imageType = 1
: 使用默认图imageType = 2
: 无图