目录树重构
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
308
lib/src/controllers/data/rating_records_controller.dart
Normal file
308
lib/src/controllers/data/rating_records_controller.dart
Normal 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 (_) {}
|
||||
}
|
||||
}
|
||||
150
lib/src/controllers/data/share_record_controller.dart
Normal file
150
lib/src/controllers/data/share_record_controller.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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: '小妈菜园',
|
||||
184
lib/src/models/data/record/rating_record_model.dart
Normal file
184
lib/src/models/data/record/rating_record_model.dart
Normal 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',
|
||||
);
|
||||
}
|
||||
}
|
||||
154
lib/src/models/data/record/share_record_model.dart
Normal file
154
lib/src/models/data/record/share_record_model.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -3,7 +3,7 @@
|
||||
* 名称: 软件信息页面
|
||||
* 作用: 展示应用版本、技术栈、构建信息、设备信息等
|
||||
* 创建: 2026-04-18
|
||||
* 更新: 2026-04-18 修复鸿蒙端设备信息显示unknown,渲染引擎动态检测
|
||||
* 更新: 2026-04-19 修复鸿蒙端操作系统显示unknown、设备类型显示未知,增加defaultTargetPlatform兜底
|
||||
*/
|
||||
|
||||
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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
879
lib/src/pages/profile/social/share_records_page.dart
Normal file
879
lib/src/pages/profile/social/share_records_page.dart
Normal 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('清空'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1344
lib/src/pages/profile/tools/rating_records_page.dart
Normal file
1344
lib/src/pages/profile/tools/rating_records_page.dart
Normal file
File diff suppressed because it is too large
Load Diff
1073
lib/src/pages/tools/cooking/decision_maker_page.dart
Normal file
1073
lib/src/pages/tools/cooking/decision_maker_page.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
317
lib/src/pages/tools/tool_item_detail_page.dart
Normal file
317
lib/src/pages/tools/tool_item_detail_page.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
397
lib/src/pages/tools/tools_category_widgets.dart
Normal file
397
lib/src/pages/tools/tools_category_widgets.dart
Normal 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],
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
306
lib/src/pages/tools/tools_featured_section.dart
Normal file
306
lib/src/pages/tools/tools_featured_section.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user