目录树重构

This commit is contained in:
Developer
2026-04-19 04:17:54 +08:00
parent ceb11d9aac
commit 23c939ef64
112 changed files with 5880 additions and 4619 deletions

View File

@@ -19,6 +19,7 @@ import 'package:mom_kitchen/src/controllers/recipe/search_controller.dart';
import 'package:mom_kitchen/src/controllers/data/shopping_list_controller.dart';
import 'package:mom_kitchen/src/controllers/data/cooking_note_controller.dart';
import 'package:mom_kitchen/src/controllers/data/browse_history_controller.dart';
import 'package:mom_kitchen/src/controllers/data/rating_records_controller.dart';
import 'package:mom_kitchen/src/controllers/tools/tools_controller.dart';
import 'package:mom_kitchen/src/controllers/tools/what_to_eat_controller.dart';
import 'package:mom_kitchen/src/controllers/data/weekly_menu_controller.dart';
@@ -84,6 +85,7 @@ class AppBinding extends Bindings {
Get.lazyPut(() => WeeklyMenuController(), fenix: true);
Get.lazyPut(() => BedtimeReminderController(), fenix: true);
Get.lazyPut(() => MealRecordController(), fenix: true);
Get.lazyPut(() => RatingRecordsController(), fenix: true);
}
}

View File

@@ -17,7 +17,7 @@ class AppConfig {
static const String baseUrl = 'https://eat.wktyl.com';
// 超时时间(秒)
static const int timeoutSeconds = 10;
static const int timeoutSeconds = 8;
// 缓存时间(天)
static const int cacheDays = 7;

View File

@@ -52,7 +52,9 @@ import 'package:mom_kitchen/src/pages/profile/references_page.dart';
import 'package:mom_kitchen/src/pages/profile/privacy_policy_page.dart';
import 'package:mom_kitchen/src/pages/profile/guide_page.dart';
import 'package:mom_kitchen/src/pages/profile/learn_us_page.dart';
import 'package:mom_kitchen/src/pages/profile/rating_records_page.dart';
import 'package:mom_kitchen/src/pages/profile/social/email_history_page.dart';
import 'package:mom_kitchen/src/pages/profile/social/share_records_page.dart';
import 'package:mom_kitchen/src/pages/tools/cooking/date_calculator_page.dart';
import 'package:mom_kitchen/src/pages/tools/cooking/food_copy_generator_page.dart';
import 'package:mom_kitchen/src/pages/tools/health/safe_period_calculator_page.dart';
@@ -62,6 +64,7 @@ import 'package:mom_kitchen/src/pages/tools/ranking/dish_ranking_page.dart';
import 'package:mom_kitchen/src/pages/tools/ingredient_manage_page.dart';
import 'package:mom_kitchen/src/pages/tools/tool_detail_page.dart';
import 'package:mom_kitchen/src/pages/tools/cooking/order_assistant_page.dart';
import 'package:mom_kitchen/src/pages/tools/cooking/decision_maker_page.dart';
import 'package:mom_kitchen/src/pages/tools/farm/farm_game_page.dart';
import 'package:mom_kitchen/src/pages/tools/farm/farm_shop_page.dart';
import 'package:mom_kitchen/src/pages/tools/farm/farm_inventory_page.dart';
@@ -123,6 +126,7 @@ class AppRoutes {
static const String nutritionRecipeList = '/nutrition-recipe-list';
static const String miniCard = '/mini-card';
static const String emailHistory = '/email-history';
static const String shareRecords = '/share-records';
static const String hot = '/hot';
static const String dateCalculator = '/tools/date-calculator';
static const String foodCopyGenerator = '/tools/food-copy';
@@ -132,7 +136,9 @@ class AppRoutes {
static const String toolsIngredientManage = '/tools/ingredient-manage';
static const String toolDetail = '/tool-detail';
static const String toolsOrderAssistant = '/tools/order-assistant';
static const String toolsDecisionMaker = '/tools/decision-maker';
static const String dataExport = '/data-export';
static const String ratingRecords = '/rating-records';
// 农场游戏路由
static const String farmGame = '/farm-game';
@@ -196,6 +202,11 @@ class AppRoutes {
page: () => const DataExportPage(),
middlewares: [PageStandardsMiddleware()],
),
GetPage(
name: ratingRecords,
page: () => const RatingRecordsPage(),
middlewares: [PageStandardsMiddleware()],
),
// 农场游戏路由
GetPage(
name: farmGame,
@@ -515,6 +526,11 @@ class AppRoutes {
page: () => const EmailHistoryPage(),
middlewares: [PageStandardsMiddleware()],
),
GetPage(
name: shareRecords,
page: () => const ShareRecordsPage(),
middlewares: [PageStandardsMiddleware()],
),
GetPage(
name: dateCalculator,
page: () => const DateCalculatorPage(),
@@ -556,6 +572,11 @@ class AppRoutes {
page: () => const OrderAssistantPage(),
middlewares: [PageStandardsMiddleware()],
),
GetPage(
name: toolsDecisionMaker,
page: () => const DecisionMakerPage(),
middlewares: [PageStandardsMiddleware()],
),
];
static void registerAllPages() {
@@ -1183,6 +1204,18 @@ class AppRoutes {
],
builder: () => const EmailHistoryPage(),
),
PageInfo(
route: shareRecords,
name: 'Share Records Page',
description: '分享记录页面',
requiredStandards: const [
StandardCheck.themeColors,
StandardCheck.textColors,
StandardCheck.fontSize,
StandardCheck.darkMode,
],
builder: () => const ShareRecordsPage(),
),
PageInfo(
route: dateCalculator,
name: 'Date Calculator Page',

View File

@@ -5,10 +5,12 @@ import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:mom_kitchen/src/controllers/base_controller.dart';
import 'package:mom_kitchen/src/controllers/data/browse_history_controller.dart';
import 'package:mom_kitchen/src/controllers/data/share_record_controller.dart';
import 'package:mom_kitchen/src/controllers/feed/action_controller.dart';
import 'package:mom_kitchen/src/controllers/data/favorites_controller.dart';
import 'package:mom_kitchen/src/controllers/data/shopping_list_controller.dart';
import 'package:mom_kitchen/src/models/data/browse_history_model.dart';
import 'package:mom_kitchen/src/models/data/share_record_model.dart';
import 'package:mom_kitchen/src/models/feed_item_model.dart';
import 'package:mom_kitchen/src/models/recipe/recipe_model.dart';
import 'package:mom_kitchen/src/models/data/shopping_item_model.dart';
@@ -323,5 +325,34 @@ class RecipeDetailController extends BaseController {
shareText.write('— 来自 小妈厨房 App 🍳');
AppUtils.shareContent(shareText.toString(), subject: '🍳 ${r.title}');
_recordShare(r);
}
void _recordShare(RecipeModel r) {
try {
if (!Get.isRegistered<ShareRecordController>()) {
Get.put(ShareRecordController());
}
final controller = Get.find<ShareRecordController>();
final record = ShareRecordModel(
id: DateTime.now().millisecondsSinceEpoch.toString(),
recipeId: r.id.toString(),
recipeTitle: r.title,
recipeCode: r.code,
categoryName: r.categoryName,
shareType: r.hasCode ? ShareType.link : ShareType.text,
shareUrl: r.hasCode
? 'https://eat.wktyl.com/recipe/${r.code}'
: null,
ratingScore: r.rating?.score,
viewCount: viewCount.value > 0 ? viewCount.value : null,
likeCount: likeCount.value > 0 ? likeCount.value : null,
sharedAt: DateTime.now().toIso8601String(),
);
controller.addRecord(record);
} catch (e) {
debugPrint('记录分享历史失败: $e');
}
}
}

View File

@@ -0,0 +1,308 @@
// 2026-04-19 | RatingRecordsController | 评分记录控制器 | 管理用户评分记录支持Hive持久化
// 2026-04-19 | 初始创建:评分记录增删查改、排序、搜索、批量删除、统计
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:mom_kitchen/src/controllers/base_controller.dart';
import 'package:mom_kitchen/src/controllers/feed/action_controller.dart';
import 'package:mom_kitchen/src/models/data/rating_record_model.dart';
import 'package:mom_kitchen/src/services/data/hive_service.dart';
import 'package:mom_kitchen/src/services/ui/toast_service.dart';
class RatingRecordsController extends BaseController {
final RxMap<int, RatingRecordModel> _records = <int, RatingRecordModel>{}.obs;
final Rx<RatingSortMode> sortMode = RatingSortMode.newest.obs;
final RxSet<int> selectedIds = <int>{}.obs;
final RxBool isEditMode = false.obs;
final RxString searchQuery = ''.obs;
final RxInt filterScore = 0.obs;
List<RatingRecordModel> get records => _getSortedRecords();
List<RatingRecordModel> get allRecords => _records.values.toList();
int get count => _records.length;
int get selectedCount => selectedIds.length;
bool get hasSelection => selectedIds.isNotEmpty;
bool get isSearching => searchQuery.value.isNotEmpty;
bool get isFiltering => filterScore.value > 0;
@override
void onInit() {
super.onInit();
_loadFromHive();
_syncWithActionController();
}
void _loadFromHive() {
try {
final hive = HiveService();
if (!hive.isInitialized) return;
final allData = hive.getAllRatingRecords();
for (final data in allData) {
final recipeId = data['recipeId'] as int?;
if (recipeId != null) {
_records[recipeId] = RatingRecordModel.fromJson(data);
}
}
} catch (e) {
debugPrint('RatingRecordsController: _loadFromHive failed: $e');
}
}
void _syncWithActionController() {
try {
if (!Get.isRegistered<ActionController>()) return;
final actionController = Get.find<ActionController>();
final ratedItems = actionController.ratedItems;
for (final entry in ratedItems.entries) {
final recipeId = entry.key;
final score = entry.value;
if (!_records.containsKey(recipeId)) {
addRecord(
recipeId: recipeId,
recipeTitle: '菜谱 #$recipeId',
score: score,
);
}
}
} catch (e) {
debugPrint('RatingRecordsController: _syncWithActionController failed: $e');
}
}
void addRecord({
required int recipeId,
required String recipeTitle,
String? coverImage,
String? categoryName,
required int score,
String? type,
}) {
final now = DateTime.now().toIso8601String();
final record = RatingRecordModel(
id: 'rating_${recipeId}_$now',
recipeId: recipeId,
recipeTitle: recipeTitle,
coverImage: coverImage,
categoryName: categoryName,
score: score,
ratedAt: now,
type: type ?? 'recipe',
);
_records[recipeId] = record;
_saveToHive(record);
}
void updateRecordScore(int recipeId, int newScore) {
final record = _records[recipeId];
if (record == null) return;
final updated = record.copyWith(
score: newScore,
ratedAt: DateTime.now().toIso8601String(),
);
_records[recipeId] = updated;
_saveToHive(updated);
try {
if (Get.isRegistered<ActionController>()) {
final actionController = Get.find<ActionController>();
actionController.ratedItems[recipeId] = newScore;
}
} catch (_) {}
ToastService.show(message: '评分已更新为 $newScore');
}
void removeRecord(int recipeId) {
if (_records.containsKey(recipeId)) {
_records.remove(recipeId);
_removeFromHive(recipeId);
try {
if (Get.isRegistered<ActionController>()) {
final actionController = Get.find<ActionController>();
actionController.ratedItems.remove(recipeId);
}
} catch (_) {}
ToastService.show(message: '已删除评分记录 🗑️');
}
}
void clearAll() {
_records.clear();
_clearHive();
ToastService.show(message: '已清空所有评分记录');
}
RatingRecordModel? getRecord(int recipeId) => _records[recipeId];
bool hasRecord(int recipeId) => _records.containsKey(recipeId);
List<RatingRecordModel> _getSortedRecords() {
var items = _records.values.toList();
if (searchQuery.value.isNotEmpty) {
final query = searchQuery.value.toLowerCase();
items = items.where((e) {
final title = e.recipeTitle.toLowerCase();
final category = (e.categoryName ?? '').toLowerCase();
return title.contains(query) || category.contains(query);
}).toList();
}
if (filterScore.value > 0) {
items = items.where((e) => e.score == filterScore.value).toList();
}
switch (sortMode.value) {
case RatingSortMode.newest:
items.sort((a, b) => b.ratedAt.compareTo(a.ratedAt));
break;
case RatingSortMode.oldest:
items.sort((a, b) => a.ratedAt.compareTo(b.ratedAt));
break;
case RatingSortMode.scoreHigh:
items.sort((a, b) => b.score.compareTo(a.score));
break;
case RatingSortMode.scoreLow:
items.sort((a, b) => a.score.compareTo(b.score));
break;
}
return items;
}
void setSearchQuery(String query) {
searchQuery.value = query;
_records.refresh();
}
void clearSearch() {
searchQuery.value = '';
_records.refresh();
}
void setFilterScore(int score) {
filterScore.value = score;
_records.refresh();
}
void clearFilter() {
filterScore.value = 0;
_records.refresh();
}
void setSortMode(RatingSortMode mode) {
sortMode.value = mode;
_records.refresh();
}
Map<String, dynamic> get statistics {
final stats = <String, dynamic>{
'total': _records.length,
'avgScore': 0.0,
'5': 0,
'4': 0,
'3': 0,
'2': 0,
'1': 0,
};
if (_records.isEmpty) return stats;
var totalScore = 0;
for (final record in _records.values) {
totalScore += record.score;
stats['${record.score}'] = (stats['${record.score}'] ?? 0) + 1;
}
stats['avgScore'] = (totalScore / _records.length).toStringAsFixed(1);
return stats;
}
String exportToJson() {
final data = _records.values.map((e) => e.toJson()).toList();
return const JsonEncoder.withIndent(' ').convert(data);
}
String exportToCsv() {
final buffer = StringBuffer();
buffer.writeln('菜谱ID,菜谱名称,分类,评分,评分等级,评分时间');
for (final item in _records.values) {
buffer.writeln(
[
item.recipeId,
'"${item.recipeTitle.replaceAll('"', '""')}"',
item.categoryName ?? '',
item.score,
item.scoreLabel,
item.ratedAt,
].join(','),
);
}
return buffer.toString();
}
void toggleEditMode() {
isEditMode.value = !isEditMode.value;
if (!isEditMode.value) {
selectedIds.clear();
}
}
void toggleSelection(int recipeId) {
if (selectedIds.contains(recipeId)) {
selectedIds.remove(recipeId);
} else {
selectedIds.add(recipeId);
}
}
void selectAll() {
selectedIds.clear();
selectedIds.addAll(_records.keys);
}
void deselectAll() {
selectedIds.clear();
}
void deleteSelected() {
final count = selectedIds.length;
for (final id in selectedIds.toList()) {
_records.remove(id);
_removeFromHive(id);
}
selectedIds.clear();
isEditMode.value = false;
ToastService.show(message: '已删除 $count 条评分记录 🗑️');
}
void _saveToHive(RatingRecordModel record) {
try {
final hive = HiveService();
if (!hive.isInitialized) return;
hive.addRatingRecord(record.recipeId, record.toJson());
} catch (_) {}
}
void _removeFromHive(int recipeId) {
try {
final hive = HiveService();
if (!hive.isInitialized) return;
hive.removeRatingRecord(recipeId);
} catch (_) {}
}
void _clearHive() {
try {
final hive = HiveService();
if (!hive.isInitialized) return;
hive.clearAllRatingRecords();
} catch (_) {}
}
}

View File

@@ -0,0 +1,150 @@
// 2026-04-19 | share_record_controller.dart | 分享记录控制器 | 管理菜谱分享历史
// 2026-04-19 | 初始创建,使用 SharedPreferences JSON 持久化
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import '../../models/data/share_record_model.dart';
class ShareRecordController extends GetxController {
static ShareRecordController get to => Get.find();
final RxList<ShareRecordModel> _records = <ShareRecordModel>[].obs;
static const String _storageKey = 'share_records';
static const int _maxRecordCount = 200;
final RxString _searchQuery = ''.obs;
final Rx<ShareType?> _filterType = Rx<ShareType?>(null);
List<ShareRecordModel> get records => _records;
String get searchQuery => _searchQuery.value;
ShareType? get filterType => _filterType.value;
int get count => _records.length;
int get textShareCount =>
_records.where((r) => r.shareType == ShareType.text).length;
int get linkShareCount =>
_records.where((r) => r.shareType == ShareType.link).length;
int get qrShareCount =>
_records.where((r) => r.shareType == ShareType.qr).length;
int get emailShareCount =>
_records.where((r) => r.shareType == ShareType.email).length;
List<ShareRecordModel> get filteredRecords {
var result = _records.toList();
if (_filterType.value != null) {
result = result.where((r) => r.shareType == _filterType.value).toList();
}
if (_searchQuery.value.isNotEmpty) {
final query = _searchQuery.value.toLowerCase();
result = result.where((r) {
return r.recipeTitle.toLowerCase().contains(query) ||
(r.categoryName?.toLowerCase().contains(query) ?? false) ||
(r.note?.toLowerCase().contains(query) ?? false);
}).toList();
}
return result;
}
SharedPreferences? _prefs;
@override
void onInit() {
super.onInit();
_initPrefs();
}
Future<void> _initPrefs() async {
try {
_prefs = await SharedPreferences.getInstance();
await loadRecords();
debugPrint('分享记录初始化完成,共 ${_records.length} 条记录');
} catch (e) {
debugPrint('初始化SharedPreferences失败: $e');
}
}
Future<void> loadRecords() async {
try {
_prefs ??= await SharedPreferences.getInstance();
final String? data = _prefs!.getString(_storageKey);
if (data != null && data.isNotEmpty) {
final List<dynamic> jsonList = json.decode(data);
final loaded = jsonList
.map((json) => ShareRecordModel.fromJson(json))
.toList();
_records.assignAll(loaded);
debugPrint('从本地加载 ${loaded.length} 条分享记录');
}
} catch (e) {
debugPrint('加载分享记录失败: $e');
}
}
Future<void> addRecord(ShareRecordModel record) async {
try {
_records.insert(0, record);
if (_records.length > _maxRecordCount) {
_records.removeRange(_maxRecordCount, _records.length);
}
await _saveRecords();
debugPrint('添加分享记录: ${record.recipeTitle}');
} catch (e) {
debugPrint('添加分享记录失败: $e');
}
}
Future<void> removeRecord(String id) async {
try {
_records.removeWhere((r) => r.id == id);
await _saveRecords();
} catch (e) {
debugPrint('删除分享记录失败: $e');
}
}
Future<void> clearRecords() async {
try {
_records.clear();
await _saveRecords();
} catch (e) {
debugPrint('清空分享记录失败: $e');
}
}
void setSearchQuery(String query) {
_searchQuery.value = query;
}
void clearSearch() {
_searchQuery.value = '';
}
void setFilterType(ShareType? type) {
_filterType.value = type;
}
Future<void> _saveRecords() async {
try {
_prefs ??= await SharedPreferences.getInstance();
final data = json.encode(_records.map((r) => r.toJson()).toList());
await _prefs!.setString(_storageKey, data);
} catch (e) {
debugPrint('保存分享记录失败: $e');
}
}
List<ShareRecordModel> getRecordsByDate(String date) {
return _records.where((r) => r.sharedAt.startsWith(date)).toList();
}
List<ShareRecordModel> getRecordsByType(ShareType type) {
return _records.where((r) => r.shareType == type).toList();
}
bool hasSharedRecipe(String recipeId) {
return _records.any((r) => r.recipeId == recipeId);
}
}

View File

@@ -8,6 +8,7 @@
* : 2026-04-15
* : 2026-04-15
* : 2026-04-17 waterfallSlot homeCardTools getter
* : 2026-04-19
*/
import 'package:mom_kitchen/src/models/waterfall_slot.dart';
@@ -334,6 +335,16 @@ class ToolRegistry {
description: '点餐推单,二维码分享',
waterfallSlot: WaterfallSlotConfig(show: true, priority: 9),
),
ToolItem(
id: 'decision_maker',
name: '帮我做决定',
icon: '🎯',
needsNetwork: false,
category: 'cooking',
route: '/tools/decision-maker',
description: '转盘随机决策,选择困难终结者',
waterfallSlot: WaterfallSlotConfig(show: true, priority: 2, badge: 'NEW'),
),
ToolItem(
id: 'farm_game',
name: '小妈菜园',

View File

@@ -0,0 +1,184 @@
// 2026-04-19 | RatingRecordModel | 评分记录模型 | 记录用户评分历史支持Hive持久化
// 2026-04-19 | 初始创建:支持菜谱评分记录的本地持久化存储
import 'package:hive_ce/hive.dart';
class RatingRecordAdapter extends TypeAdapter<RatingRecordModel> {
@override
final int typeId = 5;
@override
RatingRecordModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (var i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RatingRecordModel(
id: fields[0] as String,
recipeId: fields[1] as int,
recipeTitle: fields[2] as String,
coverImage: fields[3] as String?,
categoryName: fields[4] as String?,
score: fields[5] as int,
ratedAt: fields[6] as String,
type: fields[7] as String? ?? 'recipe',
);
}
@override
void write(BinaryWriter writer, RatingRecordModel obj) {
writer
..writeByte(8)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.recipeId)
..writeByte(2)
..write(obj.recipeTitle)
..writeByte(3)
..write(obj.coverImage)
..writeByte(4)
..write(obj.categoryName)
..writeByte(5)
..write(obj.score)
..writeByte(6)
..write(obj.ratedAt)
..writeByte(7)
..write(obj.type);
}
}
enum RatingSortMode { newest, oldest, scoreHigh, scoreLow }
class RatingRecordModel {
final String id;
final int recipeId;
final String recipeTitle;
final String? coverImage;
final String? categoryName;
final int score;
final String ratedAt;
final String type;
const RatingRecordModel({
required this.id,
required this.recipeId,
required this.recipeTitle,
this.coverImage,
this.categoryName,
required this.score,
required this.ratedAt,
this.type = 'recipe',
});
bool get isHighScore => score >= 4;
bool get isMediumScore => score == 3;
bool get isLowScore => score <= 2;
String get scoreLabel {
switch (score) {
case 5:
return '完美';
case 4:
return '推荐';
case 3:
return '一般';
case 2:
return '较差';
case 1:
return '不推荐';
default:
return '';
}
}
String get scoreEmoji {
switch (score) {
case 5:
return '🌟';
case 4:
return '';
case 3:
return '👍';
case 2:
return '😐';
case 1:
return '👎';
default:
return '';
}
}
String get displayDate {
if (ratedAt.isEmpty) return '';
try {
final dt = DateTime.parse(ratedAt);
final now = DateTime.now();
final diff = now.difference(dt);
if (diff.inDays == 0) {
if (diff.inHours == 0) {
if (diff.inMinutes == 0) return '刚刚';
return '${diff.inMinutes}分钟前';
}
return '${diff.inHours}小时前';
} else if (diff.inDays == 1) {
return '昨天';
} else if (diff.inDays < 7) {
return '${diff.inDays}天前';
} else {
return '${dt.month}${dt.day}';
}
} catch (_) {
return ratedAt;
}
}
RatingRecordModel copyWith({
String? id,
int? recipeId,
String? recipeTitle,
String? coverImage,
String? categoryName,
int? score,
String? ratedAt,
String? type,
}) {
return RatingRecordModel(
id: id ?? this.id,
recipeId: recipeId ?? this.recipeId,
recipeTitle: recipeTitle ?? this.recipeTitle,
coverImage: coverImage ?? this.coverImage,
categoryName: categoryName ?? this.categoryName,
score: score ?? this.score,
ratedAt: ratedAt ?? this.ratedAt,
type: type ?? this.type,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'recipeId': recipeId,
'recipeTitle': recipeTitle,
'coverImage': coverImage,
'categoryName': categoryName,
'score': score,
'ratedAt': ratedAt,
'type': type,
};
}
factory RatingRecordModel.fromJson(Map<String, dynamic> json) {
return RatingRecordModel(
id: json['id'] as String? ?? '',
recipeId: json['recipeId'] as int? ?? 0,
recipeTitle: json['recipeTitle'] as String? ?? '',
coverImage: json['coverImage'] as String?,
categoryName: json['categoryName'] as String?,
score: json['score'] as int? ?? 0,
ratedAt: json['ratedAt'] as String? ?? '',
type: json['type'] as String? ?? 'recipe',
);
}
}

View File

@@ -0,0 +1,154 @@
// 2026-04-19 | share_record_model.dart | 分享记录模型 | 记录菜谱分享历史
// 2026-04-19 | 初始创建,支持 SharedPreferences JSON 持久化
enum ShareType {
text,
link,
qr,
email,
}
class ShareRecordModel {
final String id;
final String recipeId;
final String recipeTitle;
final String? recipeCode;
final String? categoryName;
final ShareType shareType;
final String? shareUrl;
final double? ratingScore;
final int? viewCount;
final int? likeCount;
final String sharedAt;
final String? note;
const ShareRecordModel({
required this.id,
required this.recipeId,
required this.recipeTitle,
this.recipeCode,
this.categoryName,
required this.shareType,
this.shareUrl,
this.ratingScore,
this.viewCount,
this.likeCount,
required this.sharedAt,
this.note,
});
String get displayDate {
if (sharedAt.isEmpty) return '';
try {
final dt = DateTime.parse(sharedAt);
final now = DateTime.now();
final diff = now.difference(dt);
if (diff.inDays == 0) {
if (diff.inHours == 0) {
if (diff.inMinutes == 0) return '刚刚';
return '${diff.inMinutes}分钟前';
}
return '${diff.inHours}小时前';
} else if (diff.inDays == 1) {
return '昨天';
} else if (diff.inDays < 7) {
return '${diff.inDays}天前';
} else {
return '${dt.month}${dt.day}';
}
} catch (_) {
return sharedAt;
}
}
String get typeIcon {
switch (shareType) {
case ShareType.text:
return '📝';
case ShareType.link:
return '🔗';
case ShareType.qr:
return '📱';
case ShareType.email:
return '📧';
}
}
String get typeLabel {
switch (shareType) {
case ShareType.text:
return '文本分享';
case ShareType.link:
return '链接分享';
case ShareType.qr:
return '二维码分享';
case ShareType.email:
return '邮件分享';
}
}
ShareRecordModel copyWith({
String? id,
String? recipeId,
String? recipeTitle,
String? recipeCode,
String? categoryName,
ShareType? shareType,
String? shareUrl,
double? ratingScore,
int? viewCount,
int? likeCount,
String? sharedAt,
String? note,
}) {
return ShareRecordModel(
id: id ?? this.id,
recipeId: recipeId ?? this.recipeId,
recipeTitle: recipeTitle ?? this.recipeTitle,
recipeCode: recipeCode ?? this.recipeCode,
categoryName: categoryName ?? this.categoryName,
shareType: shareType ?? this.shareType,
shareUrl: shareUrl ?? this.shareUrl,
ratingScore: ratingScore ?? this.ratingScore,
viewCount: viewCount ?? this.viewCount,
likeCount: likeCount ?? this.likeCount,
sharedAt: sharedAt ?? this.sharedAt,
note: note ?? this.note,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'recipeId': recipeId,
'recipeTitle': recipeTitle,
'recipeCode': recipeCode,
'categoryName': categoryName,
'shareType': shareType.index,
'shareUrl': shareUrl,
'ratingScore': ratingScore,
'viewCount': viewCount,
'likeCount': likeCount,
'sharedAt': sharedAt,
'note': note,
};
}
factory ShareRecordModel.fromJson(Map<String, dynamic> json) {
return ShareRecordModel(
id: json['id'] as String? ?? '',
recipeId: json['recipeId'] as String? ?? '',
recipeTitle: json['recipeTitle'] as String? ?? '',
recipeCode: json['recipeCode'] as String?,
categoryName: json['categoryName'] as String?,
shareType: ShareType.values[json['shareType'] as int? ?? 0],
shareUrl: json['shareUrl'] as String?,
ratingScore: (json['ratingScore'] as num?)?.toDouble(),
viewCount: json['viewCount'] as int?,
likeCount: json['likeCount'] as int?,
sharedAt: json['sharedAt'] as String? ?? '',
note: json['note'] as String?,
);
}
}

View File

@@ -156,12 +156,8 @@ class DiscoverSectionsWidget extends StatelessWidget {
Get.toNamed('/recipe-detail', arguments: '${recipe.id}');
return false;
} else {
return true;
}
},
onDismissed: (direction) {
if (direction == DismissDirection.startToEnd) {
_showQuickActions(context, recipe);
return false;
}
},
background: Container(

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
* :
* :
* : 2026-04-18
* : 2026-04-18 鸿unknown
* : 2026-04-19 鸿unknowndefaultTargetPlatform兜底
*/
import 'dart:ui';
@@ -636,12 +636,12 @@ class _AppInfoPageState extends State<AppInfoPage> {
String platformName = platform.operatingSystemName;
String deviceType = '未知设备';
if (platform.isMobile) {
if (platform.isWeb) {
deviceType = '🌐 Web 浏览器';
} else if (platform.isMobile) {
deviceType = '📱 移动设备';
} else if (platform.isDesktop) {
deviceType = '💻 桌面设备';
} else if (platform.isWeb) {
deviceType = '🌐 Web 浏览器';
}
final size = MediaQuery.of(context).size;

View File

@@ -1,4 +1,4 @@
/*
/*
* 文件: profile_home.dart
* 名称: 个人中心首页标签
* 作用: iOS 26 风格的用户信息展示、功能入口和消息预览
@@ -8,6 +8,7 @@
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:mom_kitchen/src/config/app_config.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
import 'package:mom_kitchen/src/controllers/user/profile_controller.dart';
import 'package:mom_kitchen/src/pages/profile/settings/personalization_page.dart';
@@ -279,16 +280,16 @@ class ProfileHomeTab extends StatelessWidget {
AppRoutes.miniCard,
),
_FeatureItem(
CupertinoIcons.cart,
CupertinoIcons.share,
'分享记录',
DesignTokens.green,
AppRoutes.shoppingList,
AppRoutes.shareRecords,
),
_FeatureItem(
CupertinoIcons.bookmark,
'评分记录',
DesignTokens.secondary,
AppRoutes.favorites,
AppRoutes.ratingRecords,
),
];
@@ -459,7 +460,7 @@ class ProfileHomeTab extends StatelessWidget {
),
SizedBox(height: DesignTokens.space2),
Text(
'v0.88.5 · 2026 Liquid Glass',
'v${AppConfig.appVersion} · 2026 Liquid Glass',
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,

View File

@@ -10,6 +10,7 @@
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:mom_kitchen/src/config/app_config.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
import 'package:mom_kitchen/src/controllers/user/profile_controller.dart';
import 'package:mom_kitchen/src/pages/profile/settings/personalization_page.dart';
@@ -269,7 +270,7 @@ class ProfileSettingsTab extends StatelessWidget {
),
SizedBox(height: DesignTokens.space2),
Text(
'v0.88.5 · iOS 26 Liquid Glass',
'v${AppConfig.appVersion} · iOS 26 Liquid Glass',
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,

View File

@@ -0,0 +1,879 @@
/*
* 文件: share_records_page.dart
* 名称: 分享记录页面
* 作用: iOS 26 Liquid Glass 风格的分享记录管理页面
* 创建: 2026-04-19
* 更新: 2026-04-19 初始创建,支持搜索/筛选/删除/重新分享/查看菜谱
*/
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart' show Colors;
import 'package:get/get.dart';
import 'package:mom_kitchen/src/config/app_routes.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
import 'package:mom_kitchen/src/controllers/data/share_record_controller.dart';
import 'package:mom_kitchen/src/models/data/share_record_model.dart';
import 'package:mom_kitchen/src/services/ui/toast_service.dart';
import 'package:mom_kitchen/src/utils/app_utils.dart';
import 'package:mom_kitchen/src/widgets/glass/glass_container.dart';
class ShareRecordsPage extends StatefulWidget {
const ShareRecordsPage({super.key});
@override
State<ShareRecordsPage> createState() => _ShareRecordsPageState();
}
class _ShareRecordsPageState extends State<ShareRecordsPage> {
ShareRecordController? _controller;
bool _isInitialized = false;
final TextEditingController _searchController = TextEditingController();
final FocusNode _searchFocusNode = FocusNode();
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
}
@override
void dispose() {
_searchController.dispose();
_searchFocusNode.dispose();
_scrollController.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_isInitialized) {
_isInitialized = true;
_initController();
}
}
void _initController() {
try {
if (Get.isRegistered<ShareRecordController>()) {
_controller = Get.find<ShareRecordController>();
} else {
_controller = Get.put(ShareRecordController());
}
} catch (e) {
debugPrint('ShareRecordsPage: Controller init error: $e');
}
}
ShareRecordController get _safeController {
if (_controller == null) {
throw StateError('ShareRecordController is not initialized');
}
return _controller!;
}
@override
Widget build(BuildContext context) {
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
if (_controller == null) {
return CupertinoPageScaffold(
backgroundColor:
isDark ? DarkDesignTokens.background : DesignTokens.background,
child: const Center(child: CupertinoActivityIndicator()),
);
}
final controller = _safeController;
return CupertinoPageScaffold(
backgroundColor:
isDark ? DarkDesignTokens.background : DesignTokens.background,
navigationBar: CupertinoNavigationBar(
middle: Text(
'🔗 分享记录',
style: TextStyle(
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
fontSize: DesignTokens.fontMd,
fontWeight: FontWeight.w600,
),
),
leading: CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () => Get.back(),
child: Icon(
CupertinoIcons.back,
color: DesignTokens.dynamicPrimary,
),
),
trailing: Obx(
() => controller.records.isNotEmpty
? CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () =>
_showClearDialog(context, controller, isDark),
child: Text(
'清空',
style: TextStyle(
color: DesignTokens.red,
fontSize: DesignTokens.fontMd,
),
),
)
: const SizedBox.shrink(),
),
backgroundColor: isDark
? DarkDesignTokens.background.withValues(alpha: 0.9)
: DesignTokens.background.withValues(alpha: 0.9),
border: null,
),
child: SafeArea(
child: Obx(() => _buildBody(context, controller, isDark)),
),
);
}
Widget _buildBody(
BuildContext context,
ShareRecordController controller,
bool isDark,
) {
final records = controller.records;
if (records.isEmpty) {
return _buildEmptyState(isDark);
}
return Column(
children: [
_buildStatsHeader(controller, isDark),
_buildSearchBar(isDark, controller),
_buildFilterTabs(isDark, controller),
Expanded(
child: Obx(() {
final filtered = controller.filteredRecords;
if (filtered.isEmpty) {
return _buildNoResultsState(isDark);
}
return ListView.separated(
controller: _scrollController,
padding: const EdgeInsets.all(DesignTokens.space4),
itemCount: filtered.length,
separatorBuilder: (_, _) =>
const SizedBox(height: DesignTokens.space3),
itemBuilder: (context, index) =>
_buildRecordCard(context, controller, filtered[index], isDark),
);
}),
),
],
);
}
Widget _buildEmptyState(bool isDark) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: DesignTokens.primaryLight,
borderRadius: DesignTokens.borderRadiusXl,
),
child: Icon(
CupertinoIcons.share,
size: 48,
color: DesignTokens.dynamicPrimary,
),
),
const SizedBox(height: DesignTokens.space4),
Text(
'暂无分享记录',
style: TextStyle(
fontSize: DesignTokens.fontLg,
fontWeight: FontWeight.w600,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
),
const SizedBox(height: DesignTokens.space2),
Text(
'在菜谱详情页点击分享即可记录',
style: TextStyle(
fontSize: DesignTokens.fontMd,
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
),
),
const SizedBox(height: DesignTokens.space4),
SizedBox(
width: 180,
child: CupertinoButton.filled(
borderRadius: DesignTokens.borderRadiusLg,
onPressed: () => Get.toNamed('/discover'),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(CupertinoIcons.compass, size: 18),
SizedBox(width: DesignTokens.space2),
Text('去发现'),
],
),
),
),
],
),
);
}
Widget _buildNoResultsState(bool isDark) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('🔍', style: TextStyle(fontSize: 48)),
const SizedBox(height: DesignTokens.space3),
Text(
'没有找到匹配的记录',
style: TextStyle(
fontSize: DesignTokens.fontMd,
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
),
),
const SizedBox(height: DesignTokens.space2),
CupertinoButton(
onPressed: () {
_searchController.clear();
_safeController.clearSearch();
_safeController.setFilterType(null);
},
child: Text(
'清除筛选',
style: TextStyle(color: DesignTokens.dynamicPrimary),
),
),
],
),
);
}
Widget _buildStatsHeader(ShareRecordController controller, bool isDark) {
return Obx(() {
final total = controller.count;
return Container(
margin: const EdgeInsets.fromLTRB(
DesignTokens.space4,
DesignTokens.space4,
DesignTokens.space4,
0,
),
padding: const EdgeInsets.all(DesignTokens.space4),
decoration: BoxDecoration(
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
borderRadius: DesignTokens.borderRadiusMd,
border: Border.all(
color: isDark
? DarkDesignTokens.glassBorder
: DesignTokens.text3.withValues(alpha: 0.15),
),
),
child: Row(
children: [
const Text('📊', style: TextStyle(fontSize: 20)),
const SizedBox(width: DesignTokens.space2),
Expanded(
child: Text(
'$total 条记录',
style: TextStyle(
fontSize: DesignTokens.fontMd,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
),
),
if (controller.textShareCount > 0)
_buildStatBadge(
'📝 ${controller.textShareCount}',
DesignTokens.dynamicPrimary,
isDark,
),
if (controller.linkShareCount > 0) ...[
const SizedBox(width: DesignTokens.space1),
_buildStatBadge(
'🔗 ${controller.linkShareCount}',
DesignTokens.green,
isDark,
),
],
if (controller.qrShareCount > 0) ...[
const SizedBox(width: DesignTokens.space1),
_buildStatBadge(
'📱 ${controller.qrShareCount}',
DesignTokens.purple,
isDark,
),
],
if (controller.emailShareCount > 0) ...[
const SizedBox(width: DesignTokens.space1),
_buildStatBadge(
'📧 ${controller.emailShareCount}',
DesignTokens.secondary,
isDark,
),
],
],
),
);
});
}
Widget _buildStatBadge(String text, Color color, bool isDark) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space2,
vertical: 2,
),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: DesignTokens.borderRadiusSm,
),
child: Text(
text,
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: color,
),
),
);
}
Widget _buildSearchBar(bool isDark, ShareRecordController controller) {
return Obx(() {
final isSearching = controller.searchQuery.isNotEmpty;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space4,
vertical: DesignTokens.space2,
),
child: GlassContainer(
padding:
const EdgeInsets.symmetric(horizontal: DesignTokens.space3),
borderRadius: DesignTokens.radiusMd,
opacity: isDark ? 0.6 : 0.75,
child: Row(
children: [
Icon(
CupertinoIcons.search,
size: 18,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
const SizedBox(width: DesignTokens.space2),
Expanded(
child: CupertinoTextField(
controller: _searchController,
focusNode: _searchFocusNode,
onChanged: (value) => controller.setSearchQuery(value),
style: TextStyle(
fontSize: DesignTokens.fontMd,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
placeholder: '搜索分享记录...',
placeholderStyle: TextStyle(
color:
isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
decoration: const BoxDecoration(),
padding: EdgeInsets.zero,
),
),
if (isSearching)
GestureDetector(
onTap: () {
_searchController.clear();
controller.clearSearch();
},
child: Icon(
CupertinoIcons.clear_circled_solid,
size: 18,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
),
],
),
),
);
});
}
Widget _buildFilterTabs(bool isDark, ShareRecordController controller) {
return Obx(() {
final currentType = controller.filterType;
final types = ShareType.values;
return Container(
height: 40,
margin: const EdgeInsets.symmetric(horizontal: DesignTokens.space4),
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: types.length + 1,
separatorBuilder: (_, _) =>
const SizedBox(width: DesignTokens.space2),
itemBuilder: (context, index) {
if (index == 0) {
final isSelected = currentType == null;
return _buildFilterChip(
'📋',
'全部',
isSelected,
isDark,
() => controller.setFilterType(null),
);
}
final type = types[index - 1];
final isSelected = type == currentType;
return _buildFilterChip(
_getTypeIcon(type),
_getTypeLabel(type),
isSelected,
isDark,
() => controller.setFilterType(type),
);
},
),
);
});
}
String _getTypeIcon(ShareType type) {
switch (type) {
case ShareType.text:
return '📝';
case ShareType.link:
return '🔗';
case ShareType.qr:
return '📱';
case ShareType.email:
return '📧';
}
}
String _getTypeLabel(ShareType type) {
switch (type) {
case ShareType.text:
return '文本';
case ShareType.link:
return '链接';
case ShareType.qr:
return '二维码';
case ShareType.email:
return '邮件';
}
}
Widget _buildFilterChip(
String emoji,
String label,
bool isSelected,
bool isDark,
VoidCallback onTap,
) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space3,
vertical: DesignTokens.space2,
),
decoration: BoxDecoration(
color: isSelected
? DesignTokens.dynamicPrimary.withValues(alpha: 0.12)
: (isDark
? DarkDesignTokens.glass
: DesignTokens.card.withValues(alpha: 0.6)),
borderRadius: DesignTokens.borderRadiusFull,
border: Border.all(
color: isSelected
? DesignTokens.dynamicPrimary.withValues(alpha: 0.4)
: Colors.transparent,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(emoji, style: const TextStyle(fontSize: 14)),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: DesignTokens.fontSm,
color: isSelected
? DesignTokens.dynamicPrimary
: (isDark ? DarkDesignTokens.text1 : DesignTokens.text1),
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
),
),
],
),
),
);
}
Widget _buildRecordCard(
BuildContext context,
ShareRecordController controller,
ShareRecordModel record,
bool isDark,
) {
return Dismissible(
key: ValueKey(record.id),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: DesignTokens.space5),
decoration: BoxDecoration(
color: DesignTokens.red,
borderRadius: DesignTokens.borderRadiusMd,
),
child: const Icon(CupertinoIcons.delete, color: CupertinoColors.white),
),
confirmDismiss: (_) => _confirmDelete(context, record, isDark),
onDismissed: (_) {
controller.removeRecord(record.id);
ToastService.show(message: '记录已删除 🗑️');
},
child: GestureDetector(
onTap: () => _showRecordDetail(context, record, isDark),
child: Container(
padding: const EdgeInsets.all(DesignTokens.space3),
decoration: BoxDecoration(
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
borderRadius: DesignTokens.borderRadiusMd,
border: Border.all(
color: isDark
? DarkDesignTokens.glassBorder
: DesignTokens.text3.withValues(alpha: 0.15),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'🍳 ${record.recipeTitle}',
style: TextStyle(
fontSize: DesignTokens.fontMd,
fontWeight: FontWeight.w500,
color: isDark
? DarkDesignTokens.text1
: DesignTokens.text1,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space1,
vertical: 2,
),
decoration: BoxDecoration(
color: _getTypeColor(record.shareType)
.withValues(alpha: 0.1),
borderRadius: DesignTokens.borderRadiusSm,
),
child: Text(
'${record.typeIcon} ${record.typeLabel}',
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: _getTypeColor(record.shareType),
),
),
),
],
),
const SizedBox(height: DesignTokens.space2),
Row(
children: [
if (record.categoryName != null &&
record.categoryName!.isNotEmpty) ...[
Icon(
CupertinoIcons.folder,
size: 12,
color:
isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
const SizedBox(width: DesignTokens.space1),
Text(
record.categoryName!,
style: TextStyle(
fontSize: DesignTokens.fontSm,
color: isDark
? DarkDesignTokens.text2
: DesignTokens.text2,
),
),
const SizedBox(width: DesignTokens.space2),
],
if (record.ratingScore != null) ...[
const Text('', style: TextStyle(fontSize: 12)),
const SizedBox(width: 2),
Text(
'${record.ratingScore}',
style: TextStyle(
fontSize: DesignTokens.fontSm,
color: isDark
? DarkDesignTokens.text2
: DesignTokens.text2,
),
),
const SizedBox(width: DesignTokens.space2),
],
if (record.viewCount != null && record.viewCount! > 0) ...[
const Text('👁️', style: TextStyle(fontSize: 12)),
const SizedBox(width: 2),
Text(
'${record.viewCount}',
style: TextStyle(
fontSize: DesignTokens.fontSm,
color: isDark
? DarkDesignTokens.text2
: DesignTokens.text2,
),
),
const SizedBox(width: DesignTokens.space2),
],
if (record.likeCount != null && record.likeCount! > 0) ...[
const Text('❤️', style: TextStyle(fontSize: 12)),
const SizedBox(width: 2),
Text(
'${record.likeCount}',
style: TextStyle(
fontSize: DesignTokens.fontSm,
color: isDark
? DarkDesignTokens.text2
: DesignTokens.text2,
),
),
],
],
),
const SizedBox(height: DesignTokens.space1),
Row(
children: [
Icon(
CupertinoIcons.clock,
size: 12,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
const SizedBox(width: DesignTokens.space1),
Text(
record.displayDate,
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: isDark
? DarkDesignTokens.text3
: DesignTokens.text3,
),
),
if (record.shareUrl != null) ...[
const SizedBox(width: DesignTokens.space2),
Icon(
CupertinoIcons.link,
size: 12,
color:
isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
],
],
),
],
),
),
),
);
}
Color _getTypeColor(ShareType type) {
switch (type) {
case ShareType.text:
return DesignTokens.dynamicPrimary;
case ShareType.link:
return DesignTokens.green;
case ShareType.qr:
return DesignTokens.purple;
case ShareType.email:
return DesignTokens.secondary;
}
}
void _showRecordDetail(
BuildContext context,
ShareRecordModel record,
bool isDark,
) {
showCupertinoDialog(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Text('${record.typeIcon} ${record.recipeTitle}'),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: DesignTokens.space3),
_detailRow('📤 类型', record.typeLabel, isDark),
if (record.categoryName != null)
_detailRow('📂 分类', record.categoryName!, isDark),
if (record.ratingScore != null)
_detailRow('⭐ 评分', '${record.ratingScore}', isDark),
if (record.viewCount != null)
_detailRow('👁️ 浏览', '${record.viewCount}', isDark),
if (record.likeCount != null)
_detailRow('❤️ 点赞', '${record.likeCount}', isDark),
if (record.shareUrl != null)
_detailRow('🔗 链接', record.shareUrl!, isDark),
_detailRow('🕐 时间', record.displayDate, isDark),
if (record.note != null && record.note!.isNotEmpty)
_detailRow('📝 备注', record.note!, isDark),
],
),
actions: [
CupertinoDialogAction(
onPressed: () {
Navigator.pop(ctx);
_reshareRecord(record);
},
child: Text(
'重新分享',
style: TextStyle(color: DesignTokens.dynamicPrimary),
),
),
CupertinoDialogAction(
onPressed: () {
Navigator.pop(ctx);
Get.toNamed(
AppRoutes.recipeDetail,
arguments: record.recipeId,
);
},
child: Text(
'查看菜谱',
style: TextStyle(color: DesignTokens.dynamicPrimary),
),
),
CupertinoDialogAction(
onPressed: () => Navigator.pop(ctx),
child: const Text('关闭'),
),
],
),
);
}
void _reshareRecord(ShareRecordModel record) {
final shareText = StringBuffer();
shareText.writeln('🍳 ${record.recipeTitle}');
if (record.categoryName != null) {
shareText.writeln('📂 ${record.categoryName}');
}
if (record.ratingScore != null) {
shareText.writeln('${record.ratingScore}');
}
if (record.shareUrl != null) {
shareText.writeln('🔗 ${record.shareUrl}');
}
shareText.writeln('');
shareText.write('— 来自 小妈厨房 App 🍳');
AppUtils.shareContent(shareText.toString(), subject: '🍳 ${record.recipeTitle}');
}
Widget _detailRow(String label, String value, bool isDark) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: DesignTokens.space1),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 72,
child: Text(
label,
style: TextStyle(
fontSize: DesignTokens.fontSm,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: DesignTokens.fontSm,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
fontWeight: FontWeight.w500,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
Future<bool> _confirmDelete(
BuildContext context,
ShareRecordModel record,
bool isDark,
) async {
return await showCupertinoDialog<bool>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: const Text('删除记录'),
content: Text('确定要删除「${record.recipeTitle}」的分享记录吗?'),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('取消'),
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () => Navigator.pop(ctx, true),
child: const Text('删除'),
),
],
),
) ??
false;
}
void _showClearDialog(
BuildContext context,
ShareRecordController controller,
bool isDark,
) {
showCupertinoDialog(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: const Text('清空分享记录'),
content: const Text('确定要清空所有分享记录吗?此操作不可撤销。'),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.pop(ctx),
child: const Text('取消'),
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () {
controller.clearRecords();
Navigator.pop(ctx);
ToastService.show(message: '分享记录已清空 🗑️');
},
child: const Text('清空'),
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -247,23 +247,23 @@ class _CookingTipDetailPageState extends State<CookingTipDetailPage> {
: DesignTokens.text3.withValues(alpha: 0.1),
),
const SizedBox(height: DesignTokens.space2),
Text(
'📖 数据来源于 HowToCook 开源项目',
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
textAlign: TextAlign.center,
),
const SizedBox(height: DesignTokens.space1),
Text(
'github.com/Anduin2017/HowToCook',
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: DesignTokens.dynamicPrimary,
),
textAlign: TextAlign.center,
),
// Text(
// '📖 数据来源于 HowToCook 开源项目',
// style: TextStyle(
// fontSize: DesignTokens.fontXs,
// color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
// ),
// textAlign: TextAlign.center,
// ),
// const SizedBox(height: DesignTokens.space1),
// Text(
// 'github.com/Anduin2017/HowToCook',
// style: TextStyle(
// fontSize: DesignTokens.fontXs,
// color: DesignTokens.dynamicPrimary,
// ),
// textAlign: TextAlign.center,
// ),
],
),
);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,317 @@
/*
* 文件: tool_item_detail_page.dart
* 名称: 工具项详情页
* 作用: 从工具中心进入,展示 ToolItem 详细信息和功能说明
* 创建: 2026-04-19 从 tools_center_page.dart 拆分
* 更新: 2026-04-19 初始拆分
*/
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
import 'package:mom_kitchen/src/models/tool_item_model.dart';
class ToolItemDetailPage extends StatelessWidget {
final ToolItem tool;
const ToolItemDetailPage({super.key, required this.tool});
@override
Widget build(BuildContext context) {
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
return CupertinoPageScaffold(
backgroundColor: isDark
? DarkDesignTokens.background
: DesignTokens.background,
child: SafeArea(
child: Column(
children: [
_buildHeader(context, isDark),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(DesignTokens.space4),
child: Column(
children: [
_buildHeroCard(isDark),
const SizedBox(height: DesignTokens.space4),
_buildInfoCards(isDark),
],
),
),
),
_buildBottomButton(isDark),
],
),
),
);
}
Widget _buildHeader(BuildContext context, bool isDark) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space4,
vertical: DesignTokens.space2,
),
child: Row(
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isDark
? DarkDesignTokens.glass
: DesignTokens.text3.withValues(alpha: 0.08),
borderRadius: DesignTokens.borderRadiusMd,
),
child: Icon(
CupertinoIcons.back,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
),
),
const SizedBox(width: DesignTokens.space3),
Expanded(
child: Text(
tool.name,
style: TextStyle(
fontSize: DesignTokens.fontLg,
fontWeight: FontWeight.w600,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
),
),
],
),
);
}
Widget _buildHeroCard(bool isDark) {
return Container(
width: double.infinity,
padding: EdgeInsets.all(DesignTokens.space5),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
DesignTokens.dynamicPrimary.withValues(alpha: 0.15),
DesignTokens.secondary.withValues(alpha: 0.08),
],
),
borderRadius: DesignTokens.borderRadiusXl,
border: Border.all(
color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1),
),
),
child: Column(
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: DesignTokens.dynamicPrimary.withValues(alpha: 0.15),
borderRadius: DesignTokens.borderRadiusXl,
),
child: Center(
child: Text(tool.icon, style: const TextStyle(fontSize: 40)),
),
),
const SizedBox(height: DesignTokens.space4),
Text(
tool.name,
style: TextStyle(
fontSize: DesignTokens.fontXxl,
fontWeight: FontWeight.w700,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
),
if (tool.description != null) ...[
const SizedBox(height: DesignTokens.space2),
Text(
tool.description!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: DesignTokens.fontMd,
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
height: 1.5,
),
),
],
SizedBox(height: DesignTokens.space4),
Container(
padding: EdgeInsets.symmetric(
horizontal: DesignTokens.space3,
vertical: DesignTokens.space2,
),
decoration: BoxDecoration(
color: tool.needsNetwork
? DesignTokens.green.withValues(alpha: 0.15)
: DesignTokens.dynamicPrimary.withValues(alpha: 0.15),
borderRadius: DesignTokens.borderRadiusLg,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
tool.needsNetwork
? CupertinoIcons.wifi
: CupertinoIcons.device_phone_portrait,
size: 14,
color: tool.needsNetwork
? DesignTokens.green
: DesignTokens.dynamicPrimary,
),
SizedBox(width: 6),
Text(
tool.needsNetwork ? '需要网络连接' : '本地运行',
style: TextStyle(
fontSize: DesignTokens.fontSm,
fontWeight: FontWeight.w500,
color: tool.needsNetwork
? DesignTokens.green
: DesignTokens.dynamicPrimary,
),
),
],
),
),
],
),
);
}
Widget _buildInfoCards(bool isDark) {
return Column(
children: [
_buildInfoCard(
icon: CupertinoIcons.chart_bar,
title: '使用统计',
value: '已使用 ${tool.usageCount}',
isDark: isDark,
),
const SizedBox(height: DesignTokens.space2),
_buildInfoCard(
icon: CupertinoIcons.folder,
title: '所属分类',
value: _getCategoryName(tool.category),
isDark: isDark,
),
],
);
}
Widget _buildInfoCard({
required IconData icon,
required String title,
required String value,
required bool isDark,
}) {
return Container(
padding: const EdgeInsets.all(DesignTokens.space4),
decoration: BoxDecoration(
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
borderRadius: DesignTokens.borderRadiusMd,
border: Border.all(
color: isDark
? DarkDesignTokens.glassBorder
: DesignTokens.text3.withValues(alpha: 0.08),
),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1),
borderRadius: DesignTokens.borderRadiusMd,
),
child: Icon(icon, size: 20, color: DesignTokens.dynamicPrimary),
),
const SizedBox(width: DesignTokens.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
),
Text(
value,
style: TextStyle(
fontSize: DesignTokens.fontMd,
fontWeight: FontWeight.w600,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
),
],
),
),
],
),
);
}
Widget _buildBottomButton(bool isDark) {
return Container(
padding: const EdgeInsets.all(DesignTokens.space4),
decoration: BoxDecoration(
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
border: Border(
top: BorderSide(
color: isDark
? DarkDesignTokens.glassBorder
: DesignTokens.text3.withValues(alpha: 0.08),
),
),
),
child: SizedBox(
width: double.infinity,
child: CupertinoButton(
padding: EdgeInsets.symmetric(vertical: DesignTokens.space3),
borderRadius: DesignTokens.borderRadiusMd,
color: DesignTokens.dynamicPrimary,
onPressed: () {
Get.toNamed(tool.route);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
CupertinoIcons.arrow_right_circle,
size: 18,
color: CupertinoColors.white,
),
const SizedBox(width: 8),
Text(
'进入工具',
style: TextStyle(
fontSize: DesignTokens.fontMd,
fontWeight: FontWeight.w600,
color: CupertinoColors.white,
),
),
],
),
),
),
);
}
String _getCategoryName(String categoryId) {
const categoryNames = {
'cooking': '烹饪助手',
'health': '健康营养',
'data': '数据查询',
'planning': '规划管理',
};
return categoryNames[categoryId] ?? categoryId;
}
}

View File

@@ -0,0 +1,397 @@
/*
* 文件: tools_category_widgets.dart
* 名称: 工具中心分类组件
* 作用: 分类组、分类头部、工具卡片等组件
* 创建: 2026-04-19 从 tools_center_page.dart 拆分
* 更新: 2026-04-19 初始拆分
*/
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
import 'package:mom_kitchen/src/models/tool_item_model.dart';
class ToolsCategoryGroup extends StatelessWidget {
final String category;
final List<ToolItem> tools;
final Map<String, dynamic> categoryInfo;
final bool isDark;
final ValueChanged<ToolItem> onOpenTool;
final ValueChanged<ToolItem> onUseTool;
const ToolsCategoryGroup({
super.key,
required this.category,
required this.tools,
required this.categoryInfo,
required this.isDark,
required this.onOpenTool,
required this.onUseTool,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
borderRadius: DesignTokens.borderRadiusLg,
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.05),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ToolsCategoryHeader(
categoryInfo: categoryInfo,
count: tools.length,
isDark: isDark,
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: LayoutBuilder(
builder: (context, constraints) {
final crossCount = _calcCrossAxisCount(constraints.maxWidth);
final itemWidth =
(constraints.maxWidth - 12 * (crossCount - 1)) / crossCount;
return Wrap(
spacing: 12,
runSpacing: 12,
children: tools.map((tool) {
return SizedBox(
width: itemWidth,
child: ToolsCategoryChip(
tool: tool,
categoryInfo: categoryInfo,
isDark: isDark,
onOpenTool: onOpenTool,
onUseTool: onUseTool,
),
);
}).toList(),
);
},
),
),
],
),
);
}
int _calcCrossAxisCount(double width) {
if (width >= 1200) return 4;
if (width >= 900) return 3;
if (width >= 600) return 2;
return 2;
}
}
class ToolsCategoryHeader extends StatelessWidget {
final Map<String, dynamic> categoryInfo;
final int count;
final bool isDark;
const ToolsCategoryHeader({
super.key,
required this.categoryInfo,
required this.count,
required this.isDark,
});
@override
Widget build(BuildContext context) {
final gradientColors = categoryInfo['gradient'] as List<Color>;
return Container(
padding: const EdgeInsets.all(DesignTokens.space4),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
gradientColors[0].withValues(alpha: 0.15),
gradientColors[1].withValues(alpha: 0.08),
],
),
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: gradientColors,
),
borderRadius: DesignTokens.borderRadiusMd,
boxShadow: [
BoxShadow(
color: gradientColors[0].withValues(alpha: 0.4),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Center(
child: Text(
categoryInfo['icon'] as String,
style: const TextStyle(fontSize: 20),
),
),
),
const SizedBox(width: DesignTokens.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
categoryInfo['name'] as String,
style: TextStyle(
fontSize: DesignTokens.fontLg,
fontWeight: FontWeight.w600,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
),
Text(
'$count 个工具',
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
),
],
),
),
],
),
);
}
}
class ToolsCategoryChip extends StatelessWidget {
final ToolItem tool;
final Map<String, dynamic> categoryInfo;
final bool isDark;
final ValueChanged<ToolItem> onOpenTool;
final ValueChanged<ToolItem> onUseTool;
const ToolsCategoryChip({
super.key,
required this.tool,
required this.categoryInfo,
required this.isDark,
required this.onOpenTool,
required this.onUseTool,
});
@override
Widget build(BuildContext context) {
final gradientColors = categoryInfo['gradient'] as List<Color>;
return Container(
padding: const EdgeInsets.all(DesignTokens.space3),
decoration: BoxDecoration(
color: isDark
? DarkDesignTokens.glass
: DesignTokens.text3.withValues(alpha: 0.04),
borderRadius: DesignTokens.borderRadiusMd,
border: Border.all(
color: isDark
? DarkDesignTokens.glassBorder
: DesignTokens.text3.withValues(alpha: 0.08),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
gradientColors[0].withValues(alpha: 0.2),
gradientColors[1].withValues(alpha: 0.1),
],
),
borderRadius: DesignTokens.borderRadiusMd,
),
child: Center(
child: Text(tool.icon, style: const TextStyle(fontSize: 22)),
),
),
const SizedBox(width: DesignTokens.space2),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tool.name,
style: TextStyle(
fontSize: DesignTokens.fontSm,
fontWeight: FontWeight.w600,
color: isDark
? DarkDesignTokens.text1
: DesignTokens.text1,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2),
Row(
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: tool.needsNetwork
? DesignTokens.green
: DesignTokens.dynamicPrimary,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
Text(
tool.needsNetwork ? '联网' : '本地',
style: TextStyle(
fontSize: 10,
color: isDark
? DarkDesignTokens.text3
: DesignTokens.text3,
),
),
],
),
],
),
),
],
),
const SizedBox(height: DesignTokens.space2),
GestureDetector(
onTap: () => onOpenTool(tool),
child: Container(
width: double.infinity,
height: 32,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: gradientColors,
),
borderRadius: DesignTokens.borderRadiusMd,
boxShadow: [
BoxShadow(
color: gradientColors[0].withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
CupertinoIcons.arrow_right_circle,
size: 16,
color: CupertinoColors.white,
),
const SizedBox(width: 6),
Text(
'工具详情',
style: TextStyle(
fontSize: DesignTokens.fontSm,
fontWeight: FontWeight.w600,
color: CupertinoColors.white,
),
),
],
),
),
),
const SizedBox(height: DesignTokens.space2),
GestureDetector(
onTap: () => onUseTool(tool),
child: Container(
width: double.infinity,
height: 32,
decoration: BoxDecoration(
color: isDark
? DarkDesignTokens.glass
: DesignTokens.card.withValues(alpha: 0.9),
borderRadius: DesignTokens.borderRadiusMd,
border: Border.all(
color: gradientColors[0].withValues(alpha: 0.3),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
CupertinoIcons.play_circle,
size: 16,
color: gradientColors[0],
),
const SizedBox(width: 6),
Text(
'使用工具',
style: TextStyle(
fontSize: DesignTokens.fontSm,
fontWeight: FontWeight.w600,
color: gradientColors[0],
),
),
],
),
),
),
],
),
);
}
}
Map<String, dynamic> getToolsCategoryInfo(String categoryId) {
final categoryMap = {
'cooking': {
'name': '烹饪助手',
'icon': '🍳',
'gradient': [const Color(0xFFFF6B35), const Color(0xFFFF9F1C)],
},
'health': {
'name': '健康营养',
'icon': '💊',
'gradient': [const Color(0xFF2ECC71), const Color(0xFF27AE60)],
},
'data': {
'name': '数据查询',
'icon': '📊',
'gradient': [DesignTokens.blue, const Color(0xFF2980B9)],
},
'planning': {
'name': '规划管理',
'icon': '📅',
'gradient': [const Color(0xFF9B59B6), const Color(0xFF8E44AD)],
},
};
return categoryMap[categoryId] ??
{
'name': categoryId,
'icon': '📦',
'gradient': [DesignTokens.dynamicPrimary, DesignTokens.secondary],
};
}

View File

@@ -3,14 +3,18 @@
* 名称: 工具中心页面
* 作用: 展示所有工具,支持分类筛选和搜索
* 更新: 2026-04-14 新增embedded模式(嵌入面板时隐藏导航栏); 布局改为一行2列响应式
* 更新: 2026-04-19 新增精选推荐/最近使用区域,扩展页面内容
* 更新: 2026-04-19 拆分组件到独立文件tools_featured_section/tools_category_widgets/tool_item_detail_page
*/
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
import 'package:mom_kitchen/src/controllers/tools/tools_controller.dart';
import 'package:mom_kitchen/src/models/tool_item_model.dart';
import 'package:mom_kitchen/src/pages/tools/tools_category_widgets.dart';
import 'package:mom_kitchen/src/pages/tools/tools_featured_section.dart';
import 'package:mom_kitchen/src/pages/tools/tool_item_detail_page.dart';
class ToolsCenterPage extends StatefulWidget {
final bool embedded;
@@ -49,7 +53,6 @@ class _ToolsCenterPageState extends State<ToolsCenterPage>
if (!_isInitialized) {
_isInitialized = true;
_initController();
_animationController.forward();
}
}
@@ -101,16 +104,102 @@ class _ToolsCenterPageState extends State<ToolsCenterPage>
final filteredTools = _controller!.filteredTools;
final groups = _groupToolsByCategory(filteredTools);
final categories = groups.keys.toList();
final allTools = _controller!.tools;
final isSearching = _searchQuery.isNotEmpty;
return CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
if (!isSearching && filteredTools.isNotEmpty) ...[
SliverToBoxAdapter(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
final slideAnimation =
Tween<Offset>(
begin: const Offset(0, 0.2),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.4, curve: Curves.easeOut),
),
);
return SlideTransition(
position: slideAnimation,
child: FadeTransition(
opacity: CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.4, curve: Curves.easeOut),
),
child: child,
),
);
},
child: Padding(
padding: const EdgeInsets.fromLTRB(
DesignTokens.space4,
DesignTokens.space3,
DesignTokens.space4,
DesignTokens.space2,
),
child: ToolsFeaturedSection(
allTools: allTools,
onToolTap: _useTool,
),
),
),
),
SliverToBoxAdapter(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
final slideAnimation =
Tween<Offset>(
begin: const Offset(0, 0.2),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(
0.05,
0.45,
curve: Curves.easeOut,
),
),
);
return SlideTransition(
position: slideAnimation,
child: FadeTransition(
opacity: CurvedAnimation(
parent: _animationController,
curve: const Interval(0.05, 0.45, curve: Curves.easeOut),
),
child: child,
),
);
},
child: Padding(
padding: const EdgeInsets.fromLTRB(
DesignTokens.space4,
0,
DesignTokens.space4,
DesignTokens.space2,
),
child: ToolsRecentSection(
allTools: allTools,
onToolTap: _useTool,
),
),
),
),
],
if (filteredTools.isNotEmpty)
...categories.asMap().entries.map((entry) {
final groupIndex = entry.key;
final category = entry.value;
final tools = groups[category]!;
final categoryInfo = _getCategoryInfo(category);
final categoryInfo = getToolsCategoryInfo(category);
return SliverToBoxAdapter(
child: AnimatedBuilder(
animation: _animationController,
@@ -141,11 +230,13 @@ class _ToolsCenterPageState extends State<ToolsCenterPage>
right: DesignTokens.space4,
bottom: DesignTokens.space3,
),
child: _buildCategoryGroup(
category,
tools,
categoryInfo,
isDark,
child: ToolsCategoryGroup(
category: category,
tools: tools,
categoryInfo: categoryInfo,
isDark: isDark,
onOpenTool: _openTool,
onUseTool: _useTool,
),
),
),
@@ -315,342 +406,6 @@ class _ToolsCenterPageState extends State<ToolsCenterPage>
return groups;
}
Map<String, dynamic> _getCategoryInfo(String categoryId) {
final categoryMap = {
'cooking': {
'name': '烹饪助手',
'icon': '🍳',
'gradient': [const Color(0xFFFF6B35), const Color(0xFFFF9F1C)],
},
'health': {
'name': '健康营养',
'icon': '💊',
'gradient': [const Color(0xFF2ECC71), const Color(0xFF27AE60)],
},
'data': {
'name': '数据查询',
'icon': '📊',
'gradient': [DesignTokens.blue, Color(0xFF2980B9)],
},
'planning': {
'name': '规划管理',
'icon': '📅',
'gradient': [Color(0xFF9B59B6), Color(0xFF8E44AD)],
},
};
return categoryMap[categoryId] ??
{
'name': categoryId,
'icon': '📦',
'gradient': [DesignTokens.dynamicPrimary, DesignTokens.secondary],
};
}
Widget _buildCategoryGroup(
String category,
List<ToolItem> tools,
Map<String, dynamic> categoryInfo,
bool isDark,
) {
return Container(
decoration: BoxDecoration(
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
borderRadius: DesignTokens.borderRadiusLg,
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.05),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildCategoryHeader(categoryInfo, tools.length, isDark),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: LayoutBuilder(
builder: (context, constraints) {
final crossCount = _calcCrossAxisCount(constraints.maxWidth);
final itemWidth =
(constraints.maxWidth - 12 * (crossCount - 1)) / crossCount;
return Wrap(
spacing: 12,
runSpacing: 12,
children: tools.map((tool) {
return SizedBox(
width: itemWidth,
child: _buildToolChip(tool, categoryInfo, isDark),
);
}).toList(),
);
},
),
),
],
),
);
}
int _calcCrossAxisCount(double width) {
if (width >= 1200) return 4;
if (width >= 900) return 3;
if (width >= 600) return 2;
return 2;
}
Widget _buildCategoryHeader(
Map<String, dynamic> categoryInfo,
int count,
bool isDark,
) {
final gradientColors = categoryInfo['gradient'] as List<Color>;
return Container(
padding: const EdgeInsets.all(DesignTokens.space4),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
gradientColors[0].withValues(alpha: 0.15),
gradientColors[1].withValues(alpha: 0.08),
],
),
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: gradientColors,
),
borderRadius: DesignTokens.borderRadiusMd,
boxShadow: [
BoxShadow(
color: gradientColors[0].withValues(alpha: 0.4),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Center(
child: Text(
categoryInfo['icon'] as String,
style: const TextStyle(fontSize: 20),
),
),
),
const SizedBox(width: DesignTokens.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
categoryInfo['name'] as String,
style: TextStyle(
fontSize: DesignTokens.fontLg,
fontWeight: FontWeight.w600,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
),
Text(
'$count 个工具',
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
),
],
),
),
],
),
);
}
Widget _buildToolChip(
ToolItem tool,
Map<String, dynamic> categoryInfo,
bool isDark,
) {
final gradientColors = categoryInfo['gradient'] as List<Color>;
return Container(
padding: const EdgeInsets.all(DesignTokens.space3),
decoration: BoxDecoration(
color: isDark
? DarkDesignTokens.glass
: DesignTokens.text3.withValues(alpha: 0.04),
borderRadius: DesignTokens.borderRadiusMd,
border: Border.all(
color: isDark
? DarkDesignTokens.glassBorder
: DesignTokens.text3.withValues(alpha: 0.08),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
gradientColors[0].withValues(alpha: 0.2),
gradientColors[1].withValues(alpha: 0.1),
],
),
borderRadius: DesignTokens.borderRadiusMd,
),
child: Center(
child: Text(tool.icon, style: const TextStyle(fontSize: 22)),
),
),
const SizedBox(width: DesignTokens.space2),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tool.name,
style: TextStyle(
fontSize: DesignTokens.fontSm,
fontWeight: FontWeight.w600,
color: isDark
? DarkDesignTokens.text1
: DesignTokens.text1,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2),
Row(
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: tool.needsNetwork
? DesignTokens.green
: DesignTokens.dynamicPrimary,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
Text(
tool.needsNetwork ? '联网' : '本地',
style: TextStyle(
fontSize: 10,
color: isDark
? DarkDesignTokens.text3
: DesignTokens.text3,
),
),
],
),
],
),
),
],
),
const SizedBox(height: DesignTokens.space2),
GestureDetector(
onTap: () => _openTool(tool),
child: Container(
width: double.infinity,
height: 32,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: gradientColors,
),
borderRadius: DesignTokens.borderRadiusMd,
boxShadow: [
BoxShadow(
color: gradientColors[0].withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
CupertinoIcons.arrow_right_circle,
size: 16,
color: CupertinoColors.white,
),
const SizedBox(width: 6),
Text(
'工具详情',
style: TextStyle(
fontSize: DesignTokens.fontSm,
fontWeight: FontWeight.w600,
color: CupertinoColors.white,
),
),
],
),
),
),
const SizedBox(height: DesignTokens.space2),
GestureDetector(
onTap: () => _useTool(tool),
child: Container(
width: double.infinity,
height: 32,
decoration: BoxDecoration(
color: isDark
? DarkDesignTokens.glass
: DesignTokens.card.withValues(alpha: 0.9),
borderRadius: DesignTokens.borderRadiusMd,
border: Border.all(
color: gradientColors[0].withValues(alpha: 0.3),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
CupertinoIcons.play_circle,
size: 16,
color: gradientColors[0],
),
const SizedBox(width: 6),
Text(
'使用工具',
style: TextStyle(
fontSize: DesignTokens.fontSm,
fontWeight: FontWeight.w600,
color: gradientColors[0],
),
),
],
),
),
),
],
),
);
}
Widget _buildEmptyState(bool isDark) {
return Center(
child: Column(
@@ -692,7 +447,7 @@ class _ToolsCenterPageState extends State<ToolsCenterPage>
void _openTool(ToolItem tool) {
_controller?.recordUsage(tool.id);
Navigator.of(context).push(
CupertinoPageRoute(builder: (context) => _ToolDetailPage(tool: tool)),
CupertinoPageRoute(builder: (context) => ToolItemDetailPage(tool: tool)),
);
}
@@ -702,313 +457,10 @@ class _ToolsCenterPageState extends State<ToolsCenterPage>
Get.toNamed(tool.route);
} else {
Navigator.of(context).push(
CupertinoPageRoute(builder: (context) => _ToolDetailPage(tool: tool)),
CupertinoPageRoute(
builder: (context) => ToolItemDetailPage(tool: tool),
),
);
}
}
}
class _ToolDetailPage extends StatelessWidget {
final ToolItem tool;
const _ToolDetailPage({required this.tool});
@override
Widget build(BuildContext context) {
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
return CupertinoPageScaffold(
backgroundColor: isDark
? DarkDesignTokens.background
: DesignTokens.background,
child: SafeArea(
child: Column(
children: [
_buildHeader(context, isDark),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(DesignTokens.space4),
child: Column(
children: [
_buildHeroCard(isDark),
const SizedBox(height: DesignTokens.space4),
_buildInfoCards(isDark),
],
),
),
),
_buildBottomButton(isDark),
],
),
),
);
}
Widget _buildHeader(BuildContext context, bool isDark) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space4,
vertical: DesignTokens.space2,
),
child: Row(
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isDark
? DarkDesignTokens.glass
: DesignTokens.text3.withValues(alpha: 0.08),
borderRadius: DesignTokens.borderRadiusMd,
),
child: Icon(
CupertinoIcons.back,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
),
),
const SizedBox(width: DesignTokens.space3),
Expanded(
child: Text(
tool.name,
style: TextStyle(
fontSize: DesignTokens.fontLg,
fontWeight: FontWeight.w600,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
),
),
],
),
);
}
Widget _buildHeroCard(bool isDark) {
return Container(
width: double.infinity,
padding: EdgeInsets.all(DesignTokens.space5),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
DesignTokens.dynamicPrimary.withValues(alpha: 0.15),
DesignTokens.secondary.withValues(alpha: 0.08),
],
),
borderRadius: DesignTokens.borderRadiusXl,
border: Border.all(
color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1),
),
),
child: Column(
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: DesignTokens.dynamicPrimary.withValues(alpha: 0.15),
borderRadius: DesignTokens.borderRadiusXl,
),
child: Center(
child: Text(tool.icon, style: const TextStyle(fontSize: 40)),
),
),
const SizedBox(height: DesignTokens.space4),
Text(
tool.name,
style: TextStyle(
fontSize: DesignTokens.fontXxl,
fontWeight: FontWeight.w700,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
),
if (tool.description != null) ...[
const SizedBox(height: DesignTokens.space2),
Text(
tool.description!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: DesignTokens.fontMd,
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
height: 1.5,
),
),
],
SizedBox(height: DesignTokens.space4),
Container(
padding: EdgeInsets.symmetric(
horizontal: DesignTokens.space3,
vertical: DesignTokens.space2,
),
decoration: BoxDecoration(
color: tool.needsNetwork
? DesignTokens.green.withValues(alpha: 0.15)
: DesignTokens.dynamicPrimary.withValues(alpha: 0.15),
borderRadius: DesignTokens.borderRadiusLg,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
tool.needsNetwork
? CupertinoIcons.wifi
: CupertinoIcons.device_phone_portrait,
size: 14,
color: tool.needsNetwork
? DesignTokens.green
: DesignTokens.dynamicPrimary,
),
SizedBox(width: 6),
Text(
tool.needsNetwork ? '需要网络连接' : '本地运行',
style: TextStyle(
fontSize: DesignTokens.fontSm,
fontWeight: FontWeight.w500,
color: tool.needsNetwork
? DesignTokens.green
: DesignTokens.dynamicPrimary,
),
),
],
),
),
],
),
);
}
Widget _buildInfoCards(bool isDark) {
return Column(
children: [
_buildInfoCard(
icon: CupertinoIcons.chart_bar,
title: '使用统计',
value: '已使用 ${tool.usageCount}',
isDark: isDark,
),
const SizedBox(height: DesignTokens.space2),
_buildInfoCard(
icon: CupertinoIcons.folder,
title: '所属分类',
value: _getCategoryName(tool.category),
isDark: isDark,
),
],
);
}
Widget _buildInfoCard({
required IconData icon,
required String title,
required String value,
required bool isDark,
}) {
return Container(
padding: const EdgeInsets.all(DesignTokens.space4),
decoration: BoxDecoration(
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
borderRadius: DesignTokens.borderRadiusMd,
border: Border.all(
color: isDark
? DarkDesignTokens.glassBorder
: DesignTokens.text3.withValues(alpha: 0.08),
),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1),
borderRadius: DesignTokens.borderRadiusMd,
),
child: Icon(icon, size: 20, color: DesignTokens.dynamicPrimary),
),
const SizedBox(width: DesignTokens.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
),
Text(
value,
style: TextStyle(
fontSize: DesignTokens.fontMd,
fontWeight: FontWeight.w600,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
),
],
),
),
],
),
);
}
Widget _buildBottomButton(bool isDark) {
return Container(
padding: const EdgeInsets.all(DesignTokens.space4),
decoration: BoxDecoration(
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
border: Border(
top: BorderSide(
color: isDark
? DarkDesignTokens.glassBorder
: DesignTokens.text3.withValues(alpha: 0.08),
),
),
),
child: SizedBox(
width: double.infinity,
child: CupertinoButton(
padding: EdgeInsets.symmetric(vertical: DesignTokens.space3),
borderRadius: DesignTokens.borderRadiusMd,
color: DesignTokens.dynamicPrimary,
onPressed: () {
Get.toNamed(tool.route);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
CupertinoIcons.arrow_right_circle,
size: 18,
color: CupertinoColors.white,
),
const SizedBox(width: 8),
Text(
'进入工具',
style: TextStyle(
fontSize: DesignTokens.fontMd,
fontWeight: FontWeight.w600,
color: CupertinoColors.white,
),
),
],
),
),
),
);
}
String _getCategoryName(String categoryId) {
const categoryNames = {
'cooking': '烹饪助手',
'health': '健康营养',
'data': '数据查询',
'planning': '规划管理',
};
return categoryNames[categoryId] ?? categoryId;
}
}

View File

@@ -0,0 +1,306 @@
/*
* 文件: tools_featured_section.dart
* 名称: 工具中心精选推荐与最近使用组件
* 作用: 展示精选推荐横向卡片和最近使用工具网格
* 创建: 2026-04-19 从 tools_center_page.dart 拆分
* 更新: 2026-04-19 初始拆分
*/
import 'package:flutter/cupertino.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
import 'package:mom_kitchen/src/models/tool_item_model.dart';
class ToolsFeaturedSection extends StatelessWidget {
final List<ToolItem> allTools;
final ValueChanged<ToolItem> onToolTap;
const ToolsFeaturedSection({
super.key,
required this.allTools,
required this.onToolTap,
});
static const List<String> _featuredIds = [
'decision_maker',
'farm_game',
'cooking_timer',
'order_assistant',
'meal_planner',
];
@override
Widget build(BuildContext context) {
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
final featuredTools = _featuredIds
.map((id) => allTools.where((t) => t.id == id).firstOrNull)
.whereType<ToolItem>()
.toList();
if (featuredTools.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text('', style: TextStyle(fontSize: 16)),
const SizedBox(width: DesignTokens.space2),
Text(
'精选推荐',
style: TextStyle(
fontSize: DesignTokens.fontLg,
fontWeight: FontWeight.w700,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
),
const Spacer(),
Text(
'滑动查看 →',
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
),
],
),
const SizedBox(height: DesignTokens.space3),
SizedBox(
height: 120,
child: ListView.separated(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
itemCount: featuredTools.length,
separatorBuilder: (_, _) =>
const SizedBox(width: DesignTokens.space3),
itemBuilder: (context, index) {
return _FeaturedCard(
tool: featuredTools[index],
isDark: isDark,
onTap: onToolTap,
);
},
),
),
],
);
}
}
class _FeaturedCard extends StatelessWidget {
final ToolItem tool;
final bool isDark;
final ValueChanged<ToolItem> onTap;
const _FeaturedCard({
required this.tool,
required this.isDark,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final gradientColors = DesignTokens.toolGradients;
final colorIndex = tool.id.hashCode % gradientColors.length;
final gradientColor = gradientColors[colorIndex];
return GestureDetector(
onTap: () => onTap(tool),
child: Container(
width: 150,
padding: const EdgeInsets.all(DesignTokens.space3),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
gradientColor.withValues(alpha: 0.15),
gradientColor.withValues(alpha: 0.05),
],
),
borderRadius: DesignTokens.borderRadiusLg,
border: Border.all(color: gradientColor.withValues(alpha: 0.2)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: gradientColor.withValues(alpha: 0.15),
borderRadius: DesignTokens.borderRadiusMd,
),
child: Center(
child: Text(
tool.icon,
style: const TextStyle(fontSize: 18),
),
),
),
const SizedBox(width: DesignTokens.space2),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tool.name,
style: TextStyle(
fontSize: DesignTokens.fontSm,
fontWeight: FontWeight.w600,
color: isDark
? DarkDesignTokens.text1
: DesignTokens.text1,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (tool.waterfallSlot.badge != null)
Container(
margin: const EdgeInsets.only(top: 2),
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 1,
),
decoration: BoxDecoration(
color: DesignTokens.red.withValues(alpha: 0.15),
borderRadius: DesignTokens.borderRadiusSm,
),
child: Text(
tool.waterfallSlot.badge!,
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w700,
color: DesignTokens.red,
),
),
),
],
),
),
],
),
const Spacer(),
Text(
tool.description ?? '',
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
}
class ToolsRecentSection extends StatelessWidget {
final List<ToolItem> allTools;
final ValueChanged<ToolItem> onToolTap;
const ToolsRecentSection({
super.key,
required this.allTools,
required this.onToolTap,
});
@override
Widget build(BuildContext context) {
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
final recentTools = List<ToolItem>.from(allTools)
..sort((a, b) => b.usageCount.compareTo(a.usageCount));
final topRecent = recentTools
.where((t) => t.usageCount > 0)
.take(4)
.toList();
if (topRecent.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text('🕐', style: TextStyle(fontSize: 16)),
const SizedBox(width: DesignTokens.space2),
Text(
'最近使用',
style: TextStyle(
fontSize: DesignTokens.fontLg,
fontWeight: FontWeight.w700,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
),
const Spacer(),
Text(
'使用 ${topRecent.length}',
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
),
],
),
const SizedBox(height: DesignTokens.space3),
Row(
children: topRecent.map((tool) {
return Expanded(
child: GestureDetector(
onTap: () => onToolTap(tool),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 3),
padding: const EdgeInsets.symmetric(
vertical: DesignTokens.space3,
horizontal: DesignTokens.space2,
),
decoration: BoxDecoration(
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
borderRadius: DesignTokens.borderRadiusMd,
border: Border.all(
color: isDark
? DarkDesignTokens.glassBorder
: DesignTokens.text3.withValues(alpha: 0.08),
),
),
child: Column(
children: [
Text(tool.icon, style: const TextStyle(fontSize: 24)),
const SizedBox(height: DesignTokens.space1),
Text(
tool.name,
style: TextStyle(
fontSize: DesignTokens.fontXs,
fontWeight: FontWeight.w500,
color: isDark
? DarkDesignTokens.text1
: DesignTokens.text1,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
'${tool.usageCount}',
style: TextStyle(
fontSize: 9,
color: isDark
? DarkDesignTokens.text3
: DesignTokens.text3,
),
),
],
),
),
),
);
}).toList(),
),
],
);
}
}

View File

@@ -17,8 +17,7 @@ class AppInfoService {
String get appName => _packageInfo?.appName ?? 'Mom\'s Kitchen';
// 获取包名
String get packageName =>
_packageInfo?.packageName ?? 'com.example.mom_kitchen';
String get packageName => _packageInfo?.packageName ?? 'cute.major.kitchen';
// 获取版本号 (如 1.0.0)
String get version => _packageInfo?.version ?? '1.0.0';

View File

@@ -4,6 +4,7 @@
* : recipe_share.php
* : 2026-04-18
* : 2026-04-18 使 toJson()
* : 2026-04-18 RESTful DELETE
*/
import 'package:dio/dio.dart';
@@ -76,12 +77,12 @@ class RecipeShareService {
return false;
}
///
/// (RESTful DELETE)
Future<bool> deleteRecipeShare(int recipeId) async {
try {
final response = await _dio.get(
final response = await _dio.delete(
'${ApiConfig.baseUrl}/kitchen/recipe_share.php',
queryParameters: {'act': 'delete', 'id': recipeId},
queryParameters: {'id': recipeId},
);
if (response.statusCode == 200) {

View File

@@ -23,6 +23,7 @@ class HiveService {
static const String _cookingNoteBox = 'cookingNoteBox';
static const String _favoriteBox = 'favoriteBox';
static const String _searchHistoryBox = 'searchHistoryBox';
static const String _ratingRecordBox = 'ratingRecordBox';
static const int _currentSchemaVersion = 1;
@@ -35,6 +36,7 @@ class HiveService {
Box<CookingNoteModel>? _cookingNotes;
Box<Map>? _favorites;
Box<List>? _searchHistory;
Box<Map>? _ratingRecords;
Box? _versionBoxInstance;
// Box
@@ -116,6 +118,7 @@ class HiveService {
_cookingNotes = await _openBoxSafe<CookingNoteModel>(_cookingNoteBox);
_favorites = await _openBoxSafe<Map>(_favoriteBox);
_searchHistory = await _openBoxSafe<List>(_searchHistoryBox);
_ratingRecords = await _openBoxSafe<Map>(_ratingRecordBox);
// Boxes
_farmPlayer = await _openBoxSafe<FarmPlayer>('farmPlayerBox');
@@ -177,7 +180,9 @@ class HiveService {
}
await _versionBoxInstance?.put(_versionKey, _currentSchemaVersion);
LoggerService().info('Hive migration completed to v$_currentSchemaVersion');
LoggerService().info(
'Hive migration completed to v$_currentSchemaVersion',
);
}
//
@@ -515,6 +520,42 @@ class HiveService {
await _favorites!.delete(id.toString());
}
// === Rating Records CRUD ===
Future<void> addRatingRecord(int recipeId, Map<String, dynamic> data) async {
if (!_initialized || _ratingRecords == null) return;
await _ratingRecords!.put(recipeId.toString(), data);
}
Map<String, dynamic>? getRatingRecord(int recipeId) {
if (!_initialized || _ratingRecords == null) return null;
final raw = _ratingRecords!.get(recipeId.toString());
if (raw == null) return null;
return Map<String, dynamic>.from(raw);
}
bool isRated(int recipeId) {
if (!_initialized || _ratingRecords == null) return false;
return _ratingRecords!.containsKey(recipeId.toString());
}
List<Map<String, dynamic>> getAllRatingRecords() {
if (!_initialized || _ratingRecords == null) return [];
return _ratingRecords!.values
.map((m) => Map<String, dynamic>.from(m))
.toList();
}
Future<void> removeRatingRecord(int recipeId) async {
if (!_initialized || _ratingRecords == null) return;
await _ratingRecords!.delete(recipeId.toString());
}
Future<void> clearAllRatingRecords() async {
if (!_initialized || _ratingRecords == null) return;
await _ratingRecords!.clear();
}
// === Search History ===
List<String> getSearchHistory() {
@@ -546,7 +587,9 @@ class HiveService {
}
// box null init
LoggerService().warning('Box $boxName is not opened. Please initialize it in init() method.');
LoggerService().warning(
'Box $boxName is not opened. Please initialize it in init() method.',
);
return null;
}
@@ -575,7 +618,9 @@ class HiveService {
try {
final box = _getOrOpenBox(boxName);
if (box == null) {
LoggerService().warning('Box $boxName is not available for put operation');
LoggerService().warning(
'Box $boxName is not available for put operation',
);
return;
}
box.put(key, value);
@@ -593,7 +638,9 @@ class HiveService {
try {
final box = _getOrOpenBox(boxName);
if (box == null) {
LoggerService().warning('Box $boxName is not available for delete operation');
LoggerService().warning(
'Box $boxName is not available for delete operation',
);
return;
}
box.delete(key);

View File

@@ -1,8 +1,9 @@
// 2026-04-09 | PlatformUtils | 平台工具类 | 判断运行平台兼容Web
// 2026-04-09 | 修复Web平台Platform API崩溃问题
// 2026-04-18 | 修复鸿蒙端检测:使用动态检测避免非鸿蒙平台崩溃
// 2026-04-19 | 修复鸿蒙端operatingSystemName返回Unknown增加defaultTargetPlatform兜底
import 'dart:io' if (dart.library.html) 'platform_web_stub.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform, TargetPlatform;
class PlatformUtils {
static final PlatformUtils _instance = PlatformUtils._internal();
@@ -45,14 +46,31 @@ class PlatformUtils {
String get operatingSystemName {
if (kIsWeb) return 'Web';
if (_checkIsHarmonyOS()) return 'HarmonyOS';
if (Platform.isIOS) return 'iOS';
if (Platform.isAndroid) return 'Android';
if (Platform.isWindows) return 'Windows';
if (Platform.isMacOS) return 'macOS';
if (Platform.isLinux) return 'Linux';
if (Platform.isFuchsia) return 'Fuchsia';
return 'Unknown';
try {
if (_checkIsHarmonyOS()) return 'HarmonyOS';
if (Platform.isIOS) return 'iOS';
if (Platform.isAndroid) return 'Android';
if (Platform.isWindows) return 'Windows';
if (Platform.isMacOS) return 'macOS';
if (Platform.isLinux) return 'Linux';
if (Platform.isFuchsia) return 'Fuchsia';
} catch (_) {}
switch (defaultTargetPlatform) {
case TargetPlatform.ohos:
return 'HarmonyOS';
case TargetPlatform.iOS:
return 'iOS';
case TargetPlatform.android:
return 'Android';
case TargetPlatform.macOS:
return 'macOS';
case TargetPlatform.windows:
return 'Windows';
case TargetPlatform.linux:
return 'Linux';
case TargetPlatform.fuchsia:
return 'Fuchsia';
}
}
String get operatingSystemVersion =>
@@ -77,7 +95,8 @@ class PlatformUtils {
return 'Platform: $operatingSystemName, Version: $operatingSystemVersion, Dart: $dartVersion';
}
bool get isMobile => isIOS || isAndroid || isHarmonyOS;
bool get isMobile =>
isIOS || isAndroid || isHarmonyOS || defaultTargetPlatform == TargetPlatform.ohos;
bool get isDesktop => isWindows || isMacOS || isLinux;

View File

@@ -11,6 +11,7 @@
import 'package:flutter/cupertino.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
enum RecipeImageMode { thumbnail, full }
@@ -280,6 +281,8 @@ class _RecipeImageState extends State<RecipeImage> {
class RecipeImageCache {
static Future<void> clearCache() async {
await CachedNetworkImage.evictFromCache('');
await DefaultCacheManager().emptyCache();
PaintingBinding.instance.imageCache.clear();
PaintingBinding.instance.imageCache.clearLiveImages();
}
}