1. 项目概述:剧本杀店铺模块的核心价值
剧本杀作为一种新兴的线下社交娱乐方式,近年来在国内迅速流行。作为连接玩家与线下店铺的桥梁,一个功能完善的店铺模块至关重要。这个模块需要解决三个核心问题:
- 发现效率:帮助玩家快速找到符合需求的店铺
- 决策支持:提供足够的信息辅助玩家做出选择
- 转化路径:简化从浏览到预订的流程
我们的实现方案基于Flutter框架,充分利用其跨平台特性,确保在OpenHarmony系统上也能提供流畅的用户体验。整个模块采用紫色作为主题色,既符合剧本杀行业的神秘感,又能建立鲜明的品牌识别度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据模型设计与业务逻辑解析
2.1 店铺核心数据模型
店铺模型(Store类)是整个模块的基础,其字段设计考虑了实际业务需求:
dart复制class Store {
final String id; // 唯一标识
final String name;
final String address;
final String phone;
final double distance; // 单位:公里
final double rating; // 1-5分
final int reviewCount;
final int scriptCount;
final String priceRange; // 如"68-128"
final List<String> tags; // 如["热门","新店"]
final String? imageUrl;
final double latitude;
final double longitude;
// 构造函数...
}
设计要点解析:
distance采用double类型存储公里数,便于后续排序和显示rating使用1-5分的标准评分体系,与主流平台保持一致priceRange用字符串形式存储,可以灵活表示不同价格区间tags列表支持动态标记,方便运营人员突出特色店铺
2.2 评分等级转换方法
dart复制String getRatingLevel() {
if (rating >= 4.8) return '优秀';
if (rating >= 4.5) return '很好';
if (rating >= 4.0) return '好';
return '一般';
}
业务逻辑考量:
- 4.8分以上为"优秀",代表顶尖店铺
- 4.5-4.8分为"很好",属于优质选择
- 4.0-4.5分为"好",满足基本要求
- 低于4.0分为"一般",需要谨慎选择
这种分级方式既直观又符合用户认知习惯,比单纯显示数字评分更有参考价值。
2.3 关联模型设计
店铺模块还包含两个重要关联模型:
dart复制// 剧本模型
class StoreScript {
final String id;
final String name;
final String type; // 如"情感本"、"恐怖本"
final int players; // 适合人数
final int duration; // 分钟
final double price;
final double rating;
final String difficulty; // "简单"/"中等"/"困难"
// ...
}
// 评价模型
class StoreReview {
final String id;
final String userId;
final String userName;
final String userAvatar;
final double rating;
final String content;
final DateTime createdTime;
final int likes;
// ...
}
模型关系说明:
- 一个店铺(Store)包含多个剧本(StoreScript)
- 一个店铺(Store)拥有多条评价(StoreReview)
- 评价(StoreReview)与用户(User)关联(通过userId)
3. 店铺列表页面实现详解
3.1 页面结构与布局方案
店铺列表采用经典的"顶部操作栏+内容列表"布局:
dart复制Scaffold(
appBar: AppBar(
title: const Text('附近店铺'),
actions: [IconButton(icon: Icon(Icons.map), onPressed: _showMapView)],
),
body: Column(
children: [
_buildSortBar(), // 排序选项栏
Expanded(
child: ListView.builder(
itemCount: _stores.length,
itemBuilder: (context, index) => _buildStoreCard(_stores[index]),
),
),
],
),
)
布局选择原因:
Column+Expanded确保排序栏固定,列表可滚动ListView.builder优化内存使用,只渲染可见项- 地图按钮放在AppBar右侧,符合用户习惯
3.2 动态排序功能实现
排序栏使用简单的文本按钮实现交互:
dart复制Widget _buildSortBar() {
return Container(
height: 50,
child: Row(
children: _sortOptions.map((option) => GestureDetector(
onTap: () => setState(() => _sortBy = option),
child: Text(
option,
style: TextStyle(
color: _sortBy == option ? _primaryColor : Colors.grey,
fontWeight: _sortBy == option ? FontWeight.bold : FontWeight.normal,
),
),
)).toList(),
),
);
}
排序逻辑处理(实际项目应放在后端):
dart复制List<Store> get sortedStores {
switch(_sortBy) {
case '距离':
return [..._stores]..sort((a,b) => a.distance.compareTo(b.distance));
case '评分':
return [..._stores]..sort((a,b) => b.rating.compareTo(a.rating));
case '人气':
return [..._stores]..sort((a,b) => b.reviewCount.compareTo(a.reviewCount));
default:
return _stores;
}
}
3.3 店铺卡片UI设计
店铺卡片采用左图标右信息的布局:
dart复制Widget _buildStoreCard(Store store) {
return Card(
elevation: 2,
child: Padding(
padding: EdgeInsets.all(12),
child: Row(
children: [
// 左侧图标区域
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: _primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.store, color: _primaryColor),
),
// 右侧信息区域
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitleRow(store),
_buildAddressText(store),
SizedBox(height: 8),
_buildMetaInfoRow(store),
],
),
),
],
),
),
);
}
视觉设计要点:
- 使用
Card组件自带阴影效果,提升层次感 - 左侧图标区域使用主题色浅色背景,强化品牌识别
- 信息区域采用紧凑布局,确保关键信息一目了然
- 重要数据(如价格)使用主题色突出显示
4. 店铺详情页深度实现
4.1 页面结构与Tab设计
详情页采用"顶部信息+Tab内容"的结构:
dart复制Scaffold(
appBar: AppBar(title: Text('店铺详情')),
body: SingleChildScrollView(
child: Column(
children: [
_buildStoreHeader(), // 店铺头图和信息
_buildStoreInfo(), // 详细联系信息
_buildTabBar(), // 剧本/评价Tab
_buildTabContent(), // Tab对应内容
],
),
),
)
Tab控制器初始化:
dart复制late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
4.2 店铺头图与基础信息
头图区域展示店铺核心信息:
dart复制Widget _buildStoreHeader() {
return Stack(
children: [
Container(
height: 180,
color: Colors.grey[200],
child: store.imageUrl != null
? Image.network(store.imageUrl!, fit: BoxFit.cover)
: Icon(Icons.store, size: 60, color: Colors.grey),
),
Positioned(
bottom: 16,
left: 16,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(store.name, style: TextStyle(fontSize: 24, color: Colors.white)),
SizedBox(height: 8),
Row(
children: [
Icon(Icons.star, color: Colors.amber),
Text(' ${store.rating} (${store.reviewCount})'),
],
),
],
),
),
],
);
}
设计考虑:
- 使用Stack实现图片与文字叠加效果
- 无图片时显示默认店铺图标
- 关键信息使用白色文字确保可读性
- 评分和评价数量并排显示,增强可信度
4.3 Tab内容实现
剧本列表采用ListView展示:
dart复制Widget _buildScriptList() {
return ListView.builder(
itemCount: _scripts.length,
itemBuilder: (context, index) {
final script = _scripts[index];
return ListTile(
leading: Icon(Icons.menu_book, color: _primaryColor),
title: Text(script.name),
subtitle: Text('${script.type} · ${script.players}人 · ${script.duration}分钟'),
trailing: Text('¥${script.price}'),
onTap: () => _bookScript(script),
);
},
);
}
评价列表则更注重用户生成内容:
dart复制Widget _buildReviewList() {
return ListView.builder(
itemCount: _reviews.length,
itemBuilder: (context, index) {
final review = _reviews[index];
return Card(
child: Padding(
padding: EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(backgroundImage: NetworkImage(review.userAvatar)),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(review.userName),
StarRating(rating: review.rating),
],
),
],
),
SizedBox(height: 8),
Text(review.content),
SizedBox(height: 8),
Text(
'${review.createdTime.year}-${review.createdTime.month}-${review.createdTime.day}',
style: TextStyle(color: Colors.grey),
),
],
),
),
);
},
);
}
5. 性能优化与实用技巧
5.1 列表性能优化方案
- 分页加载:
dart复制ListView.builder(
itemCount: _stores.length + 1,
itemBuilder: (context, index) {
if (index == _stores.length) {
if (_hasMore) {
_loadMoreStores();
return Center(child: CircularProgressIndicator());
}
return SizedBox();
}
return _buildStoreCard(_stores[index]);
},
)
- 图片缓存:
dart复制CachedNetworkImage(
imageUrl: store.imageUrl,
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
)
5.2 状态管理建议
对于更复杂的交互,推荐使用状态管理方案:
dart复制class StoreController extends GetxController {
var stores = <Store>[].obs;
var isLoading = false.obs;
void loadStores() async {
isLoading.value = true;
stores.value = await StoreApi.fetchStores();
isLoading.value = false;
}
}
// 在页面中使用
final controller = Get.put(StoreController());
Obx(() => ListView.builder(
itemCount: controller.stores.length,
itemBuilder: (context, index) => StoreCard(controller.stores[index]),
))
5.3 实际项目中的增强功能
- 地图集成:
dart复制void _showMapView() {
Get.to(MapPage(
initialLocation: LatLng(store.latitude, store.longitude),
markers: [
Marker(
markerId: MarkerId(store.id),
position: LatLng(store.latitude, store.longitude),
infoWindow: InfoWindow(title: store.name),
),
],
));
}
- 搜索筛选功能:
dart复制Widget _buildSearchBar() {
return Padding(
padding: EdgeInsets.all(12),
child: TextField(
decoration: InputDecoration(
hintText: '搜索店铺名称',
prefixIcon: Icon(Icons.search),
),
onChanged: (value) => _filterStores(value),
),
);
}
- 收藏功能实现:
dart复制IconButton(
icon: Obx(() => Icon(
controller.isFavorite.value ? Icons.favorite : Icons.favorite_border,
color: Colors.red,
)),
onPressed: () => controller.toggleFavorite(),
)
6. 常见问题与解决方案
6.1 数据加载问题排查
问题现象:列表加载缓慢或卡顿
排查步骤:
- 检查网络请求是否使用了正确的API端点
- 确认后端返回的数据格式与模型定义匹配
- 添加加载状态指示器,确认数据加载过程
- 使用Flutter DevTools分析性能瓶颈
示例代码:
dart复制FutureBuilder<List<Store>>(
future: _loadStores(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('加载失败: ${snapshot.error}'));
}
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) => StoreCard(snapshot.data![index]),
);
},
)
6.2 UI渲染异常处理
问题现象:图片加载失败导致布局错乱
解决方案:
dart复制Widget _buildStoreImage(Store store) {
return store.imageUrl != null
? Image.network(
store.imageUrl!,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _buildPlaceholder(),
)
: _buildPlaceholder();
}
Widget _buildPlaceholder() {
return Container(
color: Colors.grey[200],
child: Center(child: Icon(Icons.store, size: 48)),
);
}
6.3 跨平台适配问题
OpenHarmony特定问题:
- 字体渲染差异:在pubspec.yaml中明确指定字体
yaml复制flutter:
fonts:
- family: HarmonySans
fonts:
- asset: assets/fonts/HarmonySans-Regular.ttf
- 平台视图集成:使用platformViewRegistry注册原生视图
dart复制void registerMapView() {
if (Platform.isOpenHarmony) {
platformViewRegistry.registerViewFactory(
'mapView',
(id) => createNativeMapView(),
);
}
}
- 权限处理:适配OpenHarmony的权限系统
dart复制Future<void> requestLocationPermission() async {
if (Platform.isOpenHarmony) {
final status = await OpenHarmonyPermissions.request(
OpenHarmonyPermission.location,
);
if (!status.isGranted) {
showPermissionDeniedDialog();
}
} else {
// 其他平台处理
}
}
7. 项目扩展与进阶方向
7.1 后端API设计建议
完整的店铺模块需要后端支持以下API:
- 店铺列表API:
code复制GET /api/stores
参数:
- lat, lng: 用户当前位置
- sort: 排序方式(distance/rating/popularity)
- page: 分页页码
- search: 搜索关键词
响应:
{
"data": [Store],
"total": 100,
"page": 1
}
- 店铺详情API:
code复制GET /api/stores/{id}
响应: {
"store": Store,
"scripts": [StoreScript],
"reviews": [StoreReview]
}
7.2 数据分析与运营功能
- 用户行为追踪:
dart复制void _trackStoreView(String storeId) {
analytics.logEvent(
name: 'store_view',
parameters: {'store_id': storeId},
);
}
@override
void initState() {
super.initState();
_trackStoreView(widget.store.id);
}
- 热门店铺算法:
dart复制double calculatePopularityScore(Store store) {
// 基础权重
double score = store.rating * 20;
// 近期评价加成
final recentReviews = store.reviews
.where((r) => r.createdTime.isAfter(DateTime.now().subtract(Duration(days: 30))))
.length;
score += recentReviews * 0.5;
// 转化率加成
final conversionRate = store.bookingCount / store.viewCount;
score += conversionRate * 100;
return score;
}
7.3 微交互增强用户体验
- 卡片点击效果:
dart复制InkWell(
onTap: () => _navigateToDetail(store),
borderRadius: BorderRadius.circular(12),
splashColor: _primaryColor.withOpacity(0.2),
highlightColor: Colors.transparent,
child: _buildStoreCardContent(store),
)
- 加载骨架屏:
dart复制Widget _buildLoadingSkeleton() {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: ListView.builder(
itemCount: 5,
itemBuilder: (_, __) => Container(
margin: EdgeInsets.all(12),
height: 120,
color: Colors.white,
),
),
);
}
- 下拉刷新体验:
dart复制RefreshIndicator(
onRefresh: _refreshStores,
color: _primaryColor,
child: ListView.builder(...),
)
在实现这些功能时,我发现几个关键点值得特别注意:首先,店铺卡片的布局密度需要精确控制,信息过多会导致视觉混乱,过少则降低决策效率;其次,评分显示建议结合星级和文字描述,比单纯数字更直观;最后,地图集成要考虑不同平台的API差异,提前做好抽象封装。
