From 6205ac6208140f4289c5c6805655623b40d6ecd9 Mon Sep 17 00:00:00 2001
From: renjianbo <263303411@qq.com>
Date: Wed, 4 Mar 2026 23:10:03 +0800
Subject: [PATCH] =?UTF-8?q?=E9=A5=AD=E8=8F=9C=E8=A7=84=E5=88=92=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
example/app/src/main/AndroidManifest.xml | 4 +
.../example/MealPlanningActivity.java | 59 +++++++++
.../example/MealPlanningHistoryActivity.java | 54 +++++++++
.../adapter/MealPlanHistoryAdapter.java | 113 ++++++++++++++++++
.../data/api/OpenAICompletionService.java | 2 +-
.../ruilaizi/example/data/db/AppDatabase.java | 3 +-
.../example/data/db/MealPlanRecord.java | 43 +++++++
.../example/data/db/MealPlanRecordDao.java | 21 ++++
.../example/data/meal/MealPlanGenerator.java | 26 +++-
.../data/meal/MealPlanningRepository.java | 25 +++-
.../example/ui/MealPlanningViewModel.java | 11 +-
.../res/layout/activity_meal_planning.xml | 45 ++++++-
.../layout/activity_meal_planning_history.xml | 54 +++++++++
.../res/layout/item_meal_plan_history.xml | 67 +++++++++++
example/app/src/main/res/values/strings.xml | 2 +
15 files changed, 517 insertions(+), 12 deletions(-)
create mode 100644 example/app/src/main/java/com/ruilaizi/example/MealPlanningHistoryActivity.java
create mode 100644 example/app/src/main/java/com/ruilaizi/example/adapter/MealPlanHistoryAdapter.java
create mode 100644 example/app/src/main/java/com/ruilaizi/example/data/db/MealPlanRecord.java
create mode 100644 example/app/src/main/java/com/ruilaizi/example/data/db/MealPlanRecordDao.java
create mode 100644 example/app/src/main/res/layout/activity_meal_planning_history.xml
create mode 100644 example/app/src/main/res/layout/item_meal_plan_history.xml
diff --git a/example/app/src/main/AndroidManifest.xml b/example/app/src/main/AndroidManifest.xml
index ec66937..d9d1e0c 100644
--- a/example/app/src/main/AndroidManifest.xml
+++ b/example/app/src/main/AndroidManifest.xml
@@ -30,6 +30,10 @@
android:name=".MealPlanningActivity"
android:exported="false"
android:screenOrientation="portrait" />
+
finish());
+ if (binding.btnMealHistory != null) {
+ binding.btnMealHistory.setOnClickListener(v -> openHistory());
+ }
binding.btnGenerate.setOnClickListener(v -> doGenerate());
binding.btnCopy.setOnClickListener(v -> copyResult());
observeViewModel();
}
+ private static final int REQ_MEAL_HISTORY = 2001;
+
+ private void openHistory() {
+ startActivityForResult(new Intent(this, MealPlanningHistoryActivity.class), REQ_MEAL_HISTORY);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQ_MEAL_HISTORY && resultCode == RESULT_OK && data != null) {
+ String content = data.getStringExtra(MealPlanningHistoryActivity.EXTRA_MEAL_PLAN_CONTENT);
+ if (content != null && !content.isEmpty()) {
+ binding.tvResult.setText(content);
+ binding.tvResult.setTextColor(getResources().getColor(R.color.colorTextDark, getTheme()));
+ Snackbar.make(binding.getRoot(), "已填入规划结果", Snackbar.LENGTH_SHORT).show();
+ }
+ }
+ }
+
private void setupSpinners() {
ArrayAdapter regionAdapter = ArrayAdapter.createFromResource(this, R.array.meal_region_types, android.R.layout.simple_spinner_item);
regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
@@ -69,6 +93,37 @@ public class MealPlanningActivity extends AppCompatActivity {
binding.spinnerBudget.setSelection(1); // 50-100元
}
+ /** 与主界面 GenerationRepository 一致:0 短 1 中 2 长 */
+ private static int levelToMaxTokens(int level) {
+ switch (level) {
+ case 0: return 256;
+ case 1: return 512;
+ case 2: return 1024;
+ default: return 512;
+ }
+ }
+
+ /** 与主界面一致:0 保守 1 平衡 2 创意 */
+ private static float levelToTemperature(int level) {
+ switch (level) {
+ case 0: return 0.3f;
+ case 1: return 0.7f;
+ case 2: return 1.2f;
+ default: return 0.7f;
+ }
+ }
+
+ private void setupSliders() {
+ Slider lengthSlider = binding.sliderMealLength;
+ if (lengthSlider != null) {
+ lengthSlider.setValue(1f);
+ }
+ Slider tempSlider = binding.sliderMealTemperature;
+ if (tempSlider != null) {
+ tempSlider.setValue(1f);
+ }
+ }
+
private void doGenerate() {
String hometown = getText(binding.etHometown);
if (hometown.isEmpty()) {
@@ -83,6 +138,10 @@ public class MealPlanningActivity extends AppCompatActivity {
params.setPreferences(getText(binding.etPreferences));
params.setDietaryRestrictions(getText(binding.etDietary));
params.setBudget(BUDGET_VALUES[binding.spinnerBudget.getSelectedItemPosition()]);
+ int lengthLevel = binding.sliderMealLength != null ? (int) binding.sliderMealLength.getValue() : 1;
+ int tempLevel = binding.sliderMealTemperature != null ? (int) binding.sliderMealTemperature.getValue() : 1;
+ params.setMaxTokens(levelToMaxTokens(lengthLevel));
+ params.setTemperature(levelToTemperature(tempLevel));
viewModel.generate(params);
}
diff --git a/example/app/src/main/java/com/ruilaizi/example/MealPlanningHistoryActivity.java b/example/app/src/main/java/com/ruilaizi/example/MealPlanningHistoryActivity.java
new file mode 100644
index 0000000..9e5286d
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/MealPlanningHistoryActivity.java
@@ -0,0 +1,54 @@
+package com.ruilaizi.example;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.ruilaizi.example.adapter.MealPlanHistoryAdapter;
+import com.ruilaizi.example.data.meal.MealPlanningRepository;
+import com.ruilaizi.example.databinding.ActivityMealPlanningHistoryBinding;
+
+/**
+ * 饭菜规划历史列表。
+ */
+public class MealPlanningHistoryActivity extends AppCompatActivity {
+
+ /** 从历史项「使用」时,通过 Intent 带回的规划正文 */
+ public static final String EXTRA_MEAL_PLAN_CONTENT = "meal_plan_content";
+
+ private ActivityMealPlanningHistoryBinding binding;
+ private MealPlanHistoryAdapter adapter;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ActivityMealPlanningHistoryBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ adapter = new MealPlanHistoryAdapter();
+ adapter.setOnUseMealPlanListener(content -> {
+ Intent data = new Intent();
+ data.putExtra(EXTRA_MEAL_PLAN_CONTENT, content);
+ setResult(RESULT_OK, data);
+ finish();
+ });
+
+ binding.recycler.setLayoutManager(new LinearLayoutManager(this));
+ binding.recycler.setAdapter(adapter);
+
+ binding.btnBack.setOnClickListener(v -> finish());
+
+ MealPlanningRepository repo = new MealPlanningRepository(getApplication());
+ repo.getRecentMealPlanRecords().observe(this, list -> {
+ adapter.setData(list);
+ binding.emptyText.setVisibility(list == null || list.isEmpty() ? View.VISIBLE : View.GONE);
+ });
+ }
+}
diff --git a/example/app/src/main/java/com/ruilaizi/example/adapter/MealPlanHistoryAdapter.java b/example/app/src/main/java/com/ruilaizi/example/adapter/MealPlanHistoryAdapter.java
new file mode 100644
index 0000000..2eae76f
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/adapter/MealPlanHistoryAdapter.java
@@ -0,0 +1,113 @@
+package com.ruilaizi.example.adapter;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.button.MaterialButton;
+import com.ruilaizi.example.R;
+import com.ruilaizi.example.data.db.MealPlanRecord;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+public class MealPlanHistoryAdapter extends RecyclerView.Adapter {
+
+ private final List list = new ArrayList<>();
+ private OnUseMealPlanListener onUseMealPlanListener;
+
+ public interface OnUseMealPlanListener {
+ void onUseMealPlan(String mealPlanContent);
+ }
+
+ public void setOnUseMealPlanListener(OnUseMealPlanListener listener) {
+ this.onUseMealPlanListener = listener;
+ }
+
+ public void setData(List data) {
+ list.clear();
+ if (data != null) {
+ list.addAll(data);
+ }
+ notifyDataSetChanged();
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_meal_plan_history, parent, false);
+ return new ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ MealPlanRecord r = list.get(position);
+ String summary = "家乡:" + safe(r.hometown) + " | " + safe(r.mealType) + " | " + safe(r.dinerCount) + "人 | " + safe(r.budget) + "元";
+ holder.tvSummary.setText(summary);
+ holder.tvContentPreview.setText(truncate(r.mealPlanContent, 80));
+ holder.tvTime.setText(formatTime(r.createdAt));
+
+ holder.btnUse.setOnClickListener(v -> {
+ if (onUseMealPlanListener != null && r.mealPlanContent != null) {
+ onUseMealPlanListener.onUseMealPlan(r.mealPlanContent);
+ }
+ });
+
+ holder.btnCopy.setOnClickListener(v -> {
+ if (r.mealPlanContent != null && !r.mealPlanContent.isEmpty()) {
+ ClipboardManager cm = (ClipboardManager) holder.itemView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
+ if (cm != null) {
+ cm.setPrimaryClip(ClipData.newPlainText("饭菜规划", r.mealPlanContent));
+ }
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return list.size();
+ }
+
+ private static String safe(String s) {
+ return s != null ? s : "";
+ }
+
+ private static String truncate(String s, int maxLen) {
+ if (s == null) return "";
+ s = s.trim();
+ if (s.length() <= maxLen) return s;
+ return s.substring(0, maxLen) + "…";
+ }
+
+ private static String formatTime(long timestamp) {
+ try {
+ return new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).format(new Date(timestamp));
+ } catch (Exception e) {
+ return "";
+ }
+ }
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ TextView tvSummary, tvContentPreview, tvTime;
+ MaterialButton btnUse, btnCopy;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ tvSummary = itemView.findViewById(R.id.tv_summary);
+ tvContentPreview = itemView.findViewById(R.id.tv_content_preview);
+ tvTime = itemView.findViewById(R.id.tv_time);
+ btnUse = itemView.findViewById(R.id.btn_use);
+ btnCopy = itemView.findViewById(R.id.btn_copy);
+ }
+ }
+}
diff --git a/example/app/src/main/java/com/ruilaizi/example/data/api/OpenAICompletionService.java b/example/app/src/main/java/com/ruilaizi/example/data/api/OpenAICompletionService.java
index ca7f4ba..bf789e7 100644
--- a/example/app/src/main/java/com/ruilaizi/example/data/api/OpenAICompletionService.java
+++ b/example/app/src/main/java/com/ruilaizi/example/data/api/OpenAICompletionService.java
@@ -42,7 +42,7 @@ public class OpenAICompletionService {
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setConnectTimeout(30000);
- conn.setReadTimeout(60000);
+ conn.setReadTimeout(120000); // 120s,饭菜规划等长文生成易超时
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Authorization", "Bearer " + apiKey);
diff --git a/example/app/src/main/java/com/ruilaizi/example/data/db/AppDatabase.java b/example/app/src/main/java/com/ruilaizi/example/data/db/AppDatabase.java
index 34e604b..e6064a6 100644
--- a/example/app/src/main/java/com/ruilaizi/example/data/db/AppDatabase.java
+++ b/example/app/src/main/java/com/ruilaizi/example/data/db/AppDatabase.java
@@ -6,7 +6,7 @@ import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
-@Database(entities = {GenerationRecord.class, OptimizeRecord.class, DirectAnswerRecord.class}, version = 3, exportSchema = false)
+@Database(entities = {GenerationRecord.class, OptimizeRecord.class, DirectAnswerRecord.class, MealPlanRecord.class}, version = 4, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
private static volatile AppDatabase INSTANCE;
@@ -29,4 +29,5 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract GenerationRecordDao generationRecordDao();
public abstract OptimizeRecordDao optimizeRecordDao();
public abstract DirectAnswerRecordDao directAnswerRecordDao();
+ public abstract MealPlanRecordDao mealPlanRecordDao();
}
diff --git a/example/app/src/main/java/com/ruilaizi/example/data/db/MealPlanRecord.java b/example/app/src/main/java/com/ruilaizi/example/data/db/MealPlanRecord.java
new file mode 100644
index 0000000..7c517c0
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/data/db/MealPlanRecord.java
@@ -0,0 +1,43 @@
+package com.ruilaizi.example.data.db;
+
+import androidx.room.Entity;
+import androidx.room.Ignore;
+import androidx.room.PrimaryKey;
+
+import com.ruilaizi.example.data.meal.MealPlanGenerator;
+
+/**
+ * 饭菜规划历史记录。
+ */
+@Entity(tableName = "meal_plan_records")
+public class MealPlanRecord {
+
+ @PrimaryKey(autoGenerate = true)
+ public long id;
+
+ public String regionType;
+ public String dinerCount;
+ public String mealType;
+ public String hometown;
+ public String preferences;
+ public String dietaryRestrictions;
+ public String budget;
+ /** 生成的规划正文(Markdown) */
+ public String mealPlanContent;
+ public long createdAt;
+
+ public MealPlanRecord() {}
+
+ @Ignore
+ public MealPlanRecord(MealPlanGenerator.MealPlanParams params, String mealPlanContent) {
+ this.regionType = params.getRegionType();
+ this.dinerCount = params.getDinerCount();
+ this.mealType = params.getMealType();
+ this.hometown = params.getHometown();
+ this.preferences = params.getPreferences();
+ this.dietaryRestrictions = params.getDietaryRestrictions();
+ this.budget = params.getBudget();
+ this.mealPlanContent = mealPlanContent;
+ this.createdAt = System.currentTimeMillis();
+ }
+}
diff --git a/example/app/src/main/java/com/ruilaizi/example/data/db/MealPlanRecordDao.java b/example/app/src/main/java/com/ruilaizi/example/data/db/MealPlanRecordDao.java
new file mode 100644
index 0000000..b17c5ae
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/data/db/MealPlanRecordDao.java
@@ -0,0 +1,21 @@
+package com.ruilaizi.example.data.db;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.Query;
+
+import java.util.List;
+
+@Dao
+public interface MealPlanRecordDao {
+
+ @Insert
+ long insert(MealPlanRecord record);
+
+ @Query("SELECT * FROM meal_plan_records ORDER BY createdAt DESC LIMIT 50")
+ LiveData> getRecentRecords();
+
+ @Query("DELETE FROM meal_plan_records WHERE id NOT IN (SELECT id FROM meal_plan_records ORDER BY createdAt DESC LIMIT 50)")
+ void keepOnlyRecent();
+}
diff --git a/example/app/src/main/java/com/ruilaizi/example/data/meal/MealPlanGenerator.java b/example/app/src/main/java/com/ruilaizi/example/data/meal/MealPlanGenerator.java
index ae833de..044ed1d 100644
--- a/example/app/src/main/java/com/ruilaizi/example/data/meal/MealPlanGenerator.java
+++ b/example/app/src/main/java/com/ruilaizi/example/data/meal/MealPlanGenerator.java
@@ -8,8 +8,9 @@ import com.ruilaizi.example.data.api.OpenAICompletionService;
*/
public class MealPlanGenerator {
- private static final float TEMPERATURE = 0.7f;
- private static final int MAX_TOKENS = 2000;
+ /** 默认与 Flask 一致,可由 MealPlanParams 覆盖 */
+ private static final float DEFAULT_TEMPERATURE = 0.7f;
+ private static final int DEFAULT_MAX_TOKENS = 1000;
private final OpenAICompletionService completionService;
private final ApiKeyProvider apiKeyProvider;
@@ -28,6 +29,7 @@ public class MealPlanGenerator {
throw new Exception("请先在设置中配置 API Key");
}
String regionType = params.getRegionType() != null ? params.getRegionType() : "全国";
+ // 与 Flask meal_planning.py 完全一致的系统提示
String systemPrompt = "你是一位专业的" + regionType + "智能饭菜清单规划师和营养搭配专家。\n\n"
+ "请按以下**固定结构**输出 Markdown,便于用户查阅:\n\n"
+ "**一、菜品推荐与搭配思路**\n"
@@ -50,20 +52,24 @@ public class MealPlanGenerator {
if (hometown.isEmpty()) {
throw new Exception("请输入用餐者家乡");
}
+ // 默认值与 Flask generate_meal_plan_api 一致
String dinerCount = params.getDinerCount() != null ? params.getDinerCount() : "2";
String mealType = params.getMealType() != null ? params.getMealType() : "午餐";
- String preferences = params.getPreferences() != null ? params.getPreferences().trim() : "无";
- String dietaryRestrictions = params.getDietaryRestrictions() != null ? params.getDietaryRestrictions().trim() : "无";
+ String preferences = params.getPreferences() != null ? params.getPreferences().trim() : "";
+ String dietaryRestrictions = params.getDietaryRestrictions() != null ? params.getDietaryRestrictions().trim() : "";
String budget = params.getBudget() != null ? params.getBudget() : "100";
+ // 与 Flask user_prompt 逐字一致:为{diner_count}人制定{meal_type}饭菜清单。家乡:{hometown}。喜好:{preferences}。禁忌:{dietary_restrictions}。预算:{budget}元。请按「一、二、三」结构输出...
String userPrompt = "为" + dinerCount + "人制定" + mealType + "饭菜清单。家乡:" + hometown
+ "。喜好:" + preferences + "。禁忌:" + dietaryRestrictions + "。预算:" + budget
+ "元。请按「一、二、三」结构输出,每道菜包含菜品特色、食材清单、制作步骤、营养信息、预算估算。";
- return completionService.chat(systemPrompt, userPrompt, TEMPERATURE, MAX_TOKENS, apiKey);
+ float temperature = params.getTemperature() != null ? params.getTemperature() : DEFAULT_TEMPERATURE;
+ int maxTokens = params.getMaxTokens() != null ? params.getMaxTokens() : DEFAULT_MAX_TOKENS;
+ return completionService.chat(systemPrompt, userPrompt, temperature, maxTokens, apiKey);
}
- /** 饭菜规划参数,与 Web 端一致 */
+ /** 饭菜规划参数,与 Web 端一致;temperature / maxTokens 与优化提示词一样可配置。 */
public static class MealPlanParams {
private String regionType;
private String dinerCount;
@@ -72,6 +78,10 @@ public class MealPlanGenerator {
private String preferences;
private String dietaryRestrictions;
private String budget;
+ /** 创意程度,null 时用默认 0.7 */
+ private Float temperature;
+ /** 最大 token 数,null 时用默认 1000 */
+ private Integer maxTokens;
public String getRegionType() { return regionType; }
public void setRegionType(String regionType) { this.regionType = regionType; }
@@ -87,5 +97,9 @@ public class MealPlanGenerator {
public void setDietaryRestrictions(String dietaryRestrictions) { this.dietaryRestrictions = dietaryRestrictions; }
public String getBudget() { return budget; }
public void setBudget(String budget) { this.budget = budget; }
+ public Float getTemperature() { return temperature; }
+ public void setTemperature(Float temperature) { this.temperature = temperature; }
+ public Integer getMaxTokens() { return maxTokens; }
+ public void setMaxTokens(Integer maxTokens) { this.maxTokens = maxTokens; }
}
}
diff --git a/example/app/src/main/java/com/ruilaizi/example/data/meal/MealPlanningRepository.java b/example/app/src/main/java/com/ruilaizi/example/data/meal/MealPlanningRepository.java
index 2943a58..16a8956 100644
--- a/example/app/src/main/java/com/ruilaizi/example/data/meal/MealPlanningRepository.java
+++ b/example/app/src/main/java/com/ruilaizi/example/data/meal/MealPlanningRepository.java
@@ -2,21 +2,30 @@ package com.ruilaizi.example.data.meal;
import android.content.Context;
+import androidx.lifecycle.LiveData;
+
import com.ruilaizi.example.data.api.ApiKeyProvider;
import com.ruilaizi.example.data.api.OpenAICompletionService;
+import com.ruilaizi.example.data.db.AppDatabase;
+import com.ruilaizi.example.data.db.MealPlanRecord;
+import com.ruilaizi.example.data.db.MealPlanRecordDao;
+
+import java.util.List;
/**
- * 饭菜规划仓库:本地调用 DeepSeek 生成饭菜清单,与 example 项目提示词优化/生成同构。
+ * 饭菜规划仓库:本地调用 DeepSeek 生成饭菜清单,与 example 项目提示词优化/生成同构;支持历史记录。
*/
public class MealPlanningRepository {
private final ApiKeyProvider apiKeyProvider;
private final MealPlanGenerator generator;
+ private final MealPlanRecordDao mealPlanRecordDao;
public MealPlanningRepository(Context context) {
apiKeyProvider = new ApiKeyProvider(context);
OpenAICompletionService completionService = new OpenAICompletionService(null);
generator = new MealPlanGenerator(completionService, apiKeyProvider);
+ mealPlanRecordDao = AppDatabase.getInstance(context).mealPlanRecordDao();
}
public String getApiKey() {
@@ -29,4 +38,18 @@ public class MealPlanningRepository {
public String generateMealPlan(MealPlanGenerator.MealPlanParams params) throws Exception {
return generator.generate(params);
}
+
+ /**
+ * 保存一条规划历史。需在后台线程调用。
+ */
+ public void saveMealPlanRecord(MealPlanRecord record) {
+ new Thread(() -> {
+ mealPlanRecordDao.insert(record);
+ mealPlanRecordDao.keepOnlyRecent();
+ }).start();
+ }
+
+ public LiveData> getRecentMealPlanRecords() {
+ return mealPlanRecordDao.getRecentRecords();
+ }
}
diff --git a/example/app/src/main/java/com/ruilaizi/example/ui/MealPlanningViewModel.java b/example/app/src/main/java/com/ruilaizi/example/ui/MealPlanningViewModel.java
index 10d2f4d..3fcb463 100644
--- a/example/app/src/main/java/com/ruilaizi/example/ui/MealPlanningViewModel.java
+++ b/example/app/src/main/java/com/ruilaizi/example/ui/MealPlanningViewModel.java
@@ -10,6 +10,7 @@ import androidx.lifecycle.MutableLiveData;
import com.ruilaizi.example.data.meal.MealPlanGenerator;
import com.ruilaizi.example.data.meal.MealPlanningRepository;
+import java.net.SocketTimeoutException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -52,9 +53,17 @@ public class MealPlanningViewModel extends AndroidViewModel {
try {
final String plan = repository.generateMealPlan(params);
resultLiveData.postValue(plan);
+ if (plan != null && !plan.isEmpty()) {
+ repository.saveMealPlanRecord(new com.ruilaizi.example.data.db.MealPlanRecord(params, plan));
+ }
snackbarLiveData.postValue("饭菜规划生成完成");
} catch (Exception e) {
- errorLiveData.postValue(e != null ? e.getMessage() : "生成失败");
+ String raw = e != null ? e.getMessage() : "";
+ boolean isTimeout = e instanceof SocketTimeoutException
+ || (e.getCause() != null && e.getCause() instanceof SocketTimeoutException)
+ || (raw != null && (raw.toLowerCase().contains("timeout") || raw.contains("timed out")));
+ String msg = isTimeout ? "请求超时,请检查网络或稍后重试" : (raw != null && !raw.isEmpty() ? raw : "生成失败");
+ errorLiveData.postValue(msg);
} finally {
loadingLiveData.postValue(false);
}
diff --git a/example/app/src/main/res/layout/activity_meal_planning.xml b/example/app/src/main/res/layout/activity_meal_planning.xml
index e3c49a7..03b1ee1 100644
--- a/example/app/src/main/res/layout/activity_meal_planning.xml
+++ b/example/app/src/main/res/layout/activity_meal_planning.xml
@@ -35,7 +35,12 @@
android:textSize="20sp"
android:textStyle="bold"
android:gravity="center" />
-
+
@@ -108,7 +113,43 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/app/src/main/res/layout/item_meal_plan_history.xml b/example/app/src/main/res/layout/item_meal_plan_history.xml
new file mode 100644
index 0000000..fbbbd8a
--- /dev/null
+++ b/example/app/src/main/res/layout/item_meal_plan_history.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/app/src/main/res/values/strings.xml b/example/app/src/main/res/values/strings.xml
index c3b404f..5c34973 100644
--- a/example/app/src/main/res/values/strings.xml
+++ b/example/app/src/main/res/values/strings.xml
@@ -36,4 +36,6 @@
生成饭菜规划
填写左侧参数后点击「生成饭菜规划」
饭菜规划
+ 规划历史
+ 暂无规划记录