From 4dc47ed18655d94026e4b6a6dfa0129267cf17c3 Mon Sep 17 00:00:00 2001
From: rjb <263303411@qq.com>
Date: Wed, 4 Mar 2026 22:42:44 +0800
Subject: [PATCH] =?UTF-8?q?=E8=8F=9C=E5=93=81=E8=A7=84=E5=88=92?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
example/app/src/main/AndroidManifest.xml | 4 +
.../com/ruilaizi/example/MainActivity.java | 3 +
.../example/MealPlanningActivity.java | 141 +++++++++++++++
.../example/data/meal/MealPlanGenerator.java | 91 ++++++++++
.../data/meal/MealPlanningRepository.java | 32 ++++
.../example/ui/MealPlanningViewModel.java | 77 ++++++++
.../app/src/main/res/layout/activity_main.xml | 7 +
.../res/layout/activity_meal_planning.xml | 165 ++++++++++++++++++
example/app/src/main/res/values/arrays.xml | 19 ++
example/app/src/main/res/values/strings.xml | 4 +
10 files changed, 543 insertions(+)
create mode 100644 example/app/src/main/java/com/ruilaizi/example/MealPlanningActivity.java
create mode 100644 example/app/src/main/java/com/ruilaizi/example/data/meal/MealPlanGenerator.java
create mode 100644 example/app/src/main/java/com/ruilaizi/example/data/meal/MealPlanningRepository.java
create mode 100644 example/app/src/main/java/com/ruilaizi/example/ui/MealPlanningViewModel.java
create mode 100644 example/app/src/main/res/layout/activity_meal_planning.xml
diff --git a/example/app/src/main/AndroidManifest.xml b/example/app/src/main/AndroidManifest.xml
index 7e59d07..ec66937 100644
--- a/example/app/src/main/AndroidManifest.xml
+++ b/example/app/src/main/AndroidManifest.xml
@@ -26,6 +26,10 @@
android:name=".HistoryActivity"
android:exported="false"
android:screenOrientation="portrait" />
+
openHistory());
}
+ if (binding.btnMealPlanning != null) {
+ binding.btnMealPlanning.setOnClickListener(v -> startActivity(new Intent(this, MealPlanningActivity.class)));
+ }
observeViewModel();
showApiKeyHintIfNeeded();
}
diff --git a/example/app/src/main/java/com/ruilaizi/example/MealPlanningActivity.java b/example/app/src/main/java/com/ruilaizi/example/MealPlanningActivity.java
new file mode 100644
index 0000000..6474747
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/MealPlanningActivity.java
@@ -0,0 +1,141 @@
+package com.ruilaizi.example;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ProgressBar;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.google.android.material.button.MaterialButton;
+import com.google.android.material.snackbar.Snackbar;
+import com.google.android.material.textfield.TextInputEditText;
+import com.ruilaizi.example.data.meal.MealPlanGenerator;
+import com.ruilaizi.example.databinding.ActivityMealPlanningBinding;
+import com.ruilaizi.example.ui.MealPlanningViewModel;
+
+/**
+ * 智能饭菜规划:本地调用 DeepSeek 生成饭菜清单,与 example 提示词优化同构。
+ */
+public class MealPlanningActivity extends AppCompatActivity {
+
+ private ActivityMealPlanningBinding binding;
+ private MealPlanningViewModel viewModel;
+
+ private static final String[] BUDGET_VALUES = {"50", "100", "150", "200", "300", "500", "1000"};
+ private static final String[] DINER_VALUES = {"1", "2", "3", "4", "5", "6", "8", "10"};
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ActivityMealPlanningBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ viewModel = new ViewModelProvider(this, new ViewModelProvider.AndroidViewModelFactory(getApplication())).get(MealPlanningViewModel.class);
+
+ setupSpinners();
+ binding.btnBack.setOnClickListener(v -> finish());
+ binding.btnGenerate.setOnClickListener(v -> doGenerate());
+ binding.btnCopy.setOnClickListener(v -> copyResult());
+ observeViewModel();
+ }
+
+ 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);
+ binding.spinnerRegion.setAdapter(regionAdapter);
+
+ ArrayAdapter dinerAdapter = ArrayAdapter.createFromResource(this, R.array.meal_diner_counts, android.R.layout.simple_spinner_item);
+ dinerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ binding.spinnerDiner.setAdapter(dinerAdapter);
+ binding.spinnerDiner.setSelection(1); // 2人
+
+ ArrayAdapter mealTypeAdapter = ArrayAdapter.createFromResource(this, R.array.meal_types, android.R.layout.simple_spinner_item);
+ mealTypeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ binding.spinnerMealType.setAdapter(mealTypeAdapter);
+ binding.spinnerMealType.setSelection(1); // 午餐
+
+ ArrayAdapter budgetAdapter = ArrayAdapter.createFromResource(this, R.array.meal_budgets, android.R.layout.simple_spinner_item);
+ budgetAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ binding.spinnerBudget.setAdapter(budgetAdapter);
+ binding.spinnerBudget.setSelection(1); // 50-100元
+ }
+
+ private void doGenerate() {
+ String hometown = getText(binding.etHometown);
+ if (hometown.isEmpty()) {
+ Toast.makeText(this, "请输入用餐者家乡", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ MealPlanGenerator.MealPlanParams params = new MealPlanGenerator.MealPlanParams();
+ params.setRegionType(getSpinnerValue(binding.spinnerRegion, getResources().getStringArray(R.array.meal_region_types)));
+ params.setDinerCount(DINER_VALUES[binding.spinnerDiner.getSelectedItemPosition()]);
+ params.setMealType(getSpinnerValue(binding.spinnerMealType, getResources().getStringArray(R.array.meal_types)));
+ params.setHometown(hometown);
+ params.setPreferences(getText(binding.etPreferences));
+ params.setDietaryRestrictions(getText(binding.etDietary));
+ params.setBudget(BUDGET_VALUES[binding.spinnerBudget.getSelectedItemPosition()]);
+
+ viewModel.generate(params);
+ }
+
+ private static String getSpinnerValue(Spinner spinner, String[] displayItems) {
+ int pos = spinner.getSelectedItemPosition();
+ if (pos >= 0 && pos < displayItems.length) {
+ return displayItems[pos];
+ }
+ return "";
+ }
+
+ private String getText(TextInputEditText et) {
+ return et.getText() != null ? et.getText().toString().trim() : "";
+ }
+
+ private void copyResult() {
+ String text = binding.tvResult.getText() != null ? binding.tvResult.getText().toString() : "";
+ if (text.isEmpty() || getString(R.string.meal_planning_result_placeholder).equals(text)) {
+ Snackbar.make(binding.getRoot(), "暂无内容可复制", Snackbar.LENGTH_SHORT).show();
+ return;
+ }
+ ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ if (cm != null) {
+ cm.setPrimaryClip(ClipData.newPlainText("饭菜规划", text));
+ Snackbar.make(binding.getRoot(), "已复制到剪贴板", Snackbar.LENGTH_SHORT).show();
+ }
+ }
+
+ private void observeViewModel() {
+ viewModel.getResultLiveData().observe(this, text -> {
+ if (text != null) {
+ binding.tvResult.setText(text);
+ binding.tvResult.setTextColor(getResources().getColor(R.color.colorTextDark, getTheme()));
+ }
+ });
+ viewModel.getLoadingLiveData().observe(this, loading -> {
+ binding.progressBar.setVisibility(loading != null && loading ? View.VISIBLE : View.GONE);
+ binding.btnGenerate.setEnabled(loading == null || !loading);
+ });
+ viewModel.getErrorLiveData().observe(this, error -> {
+ if (error != null && !error.isEmpty()) {
+ new AlertDialog.Builder(this)
+ .setMessage(error)
+ .setPositiveButton(android.R.string.ok, (d, w) -> viewModel.clearError())
+ .show();
+ }
+ });
+ viewModel.getSnackbarLiveData().observe(this, msg -> {
+ if (msg != null && !msg.isEmpty()) {
+ Snackbar.make(binding.getRoot(), msg, Snackbar.LENGTH_SHORT).show();
+ viewModel.clearSnackbar();
+ }
+ });
+ }
+}
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
new file mode 100644
index 0000000..ae833de
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/data/meal/MealPlanGenerator.java
@@ -0,0 +1,91 @@
+package com.ruilaizi.example.data.meal;
+
+import com.ruilaizi.example.data.api.ApiKeyProvider;
+import com.ruilaizi.example.data.api.OpenAICompletionService;
+
+/**
+ * 智能饭菜规划生成器。本地调用 DeepSeek,与 Flask 端 meal_planning 逻辑一致。
+ */
+public class MealPlanGenerator {
+
+ private static final float TEMPERATURE = 0.7f;
+ private static final int MAX_TOKENS = 2000;
+
+ private final OpenAICompletionService completionService;
+ private final ApiKeyProvider apiKeyProvider;
+
+ public MealPlanGenerator(OpenAICompletionService completionService, ApiKeyProvider apiKeyProvider) {
+ this.completionService = completionService;
+ this.apiKeyProvider = apiKeyProvider;
+ }
+
+ /**
+ * 生成饭菜规划。需在后台线程调用。
+ */
+ public String generate(MealPlanParams params) throws Exception {
+ String apiKey = apiKeyProvider.getApiKey();
+ if (apiKey == null || apiKey.isEmpty()) {
+ throw new Exception("请先在设置中配置 API Key");
+ }
+ String regionType = params.getRegionType() != null ? params.getRegionType() : "全国";
+ String systemPrompt = "你是一位专业的" + regionType + "智能饭菜清单规划师和营养搭配专家。\n\n"
+ + "请按以下**固定结构**输出 Markdown,便于用户查阅:\n\n"
+ + "**一、菜品推荐与搭配思路**\n"
+ + "用一段话说明本餐的搭配思路(结合家乡风味、人数、预算等)。\n\n"
+ + "**二、推荐菜品详情**\n"
+ + "每道菜单独成块,格式如下:\n"
+ + "- 用 **加粗标题** 写菜品名(如:**1. 肉夹馍(经典主食)**)\n"
+ + "- **菜品特色**:一句话或要点\n"
+ + "- **食材清单**:列表写出食材与用量、预估价格(如:五花肉 300g (18元))\n"
+ + "- **制作步骤**:编号步骤\n"
+ + "- **营养信息**:简要说明\n"
+ + "- **预算估算**:单道菜金额(如:29元)\n\n"
+ + "**三、总预算与营养总结**\n"
+ + "- **总预算**:总金额与简要说明\n"
+ + "- **碳水化合物/蛋白质/维生素与纤维/脂肪**:各一句话\n"
+ + "- **过敏信息**:如有需提示\n\n"
+ + "约束:营养均衡、符合个人喜好与禁忌、控制预算、体现家乡特色。";
+
+ String hometown = params.getHometown() != null ? params.getHometown().trim() : "";
+ if (hometown.isEmpty()) {
+ throw new Exception("请输入用餐者家乡");
+ }
+ 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 budget = params.getBudget() != null ? params.getBudget() : "100";
+
+ String userPrompt = "为" + dinerCount + "人制定" + mealType + "饭菜清单。家乡:" + hometown
+ + "。喜好:" + preferences + "。禁忌:" + dietaryRestrictions + "。预算:" + budget
+ + "元。请按「一、二、三」结构输出,每道菜包含菜品特色、食材清单、制作步骤、营养信息、预算估算。";
+
+ return completionService.chat(systemPrompt, userPrompt, TEMPERATURE, MAX_TOKENS, apiKey);
+ }
+
+ /** 饭菜规划参数,与 Web 端一致 */
+ public static class MealPlanParams {
+ private String regionType;
+ private String dinerCount;
+ private String mealType;
+ private String hometown;
+ private String preferences;
+ private String dietaryRestrictions;
+ private String budget;
+
+ public String getRegionType() { return regionType; }
+ public void setRegionType(String regionType) { this.regionType = regionType; }
+ public String getDinerCount() { return dinerCount; }
+ public void setDinerCount(String dinerCount) { this.dinerCount = dinerCount; }
+ public String getMealType() { return mealType; }
+ public void setMealType(String mealType) { this.mealType = mealType; }
+ public String getHometown() { return hometown; }
+ public void setHometown(String hometown) { this.hometown = hometown; }
+ public String getPreferences() { return preferences; }
+ public void setPreferences(String preferences) { this.preferences = preferences; }
+ public String getDietaryRestrictions() { return dietaryRestrictions; }
+ public void setDietaryRestrictions(String dietaryRestrictions) { this.dietaryRestrictions = dietaryRestrictions; }
+ public String getBudget() { return budget; }
+ public void setBudget(String budget) { this.budget = budget; }
+ }
+}
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
new file mode 100644
index 0000000..2943a58
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/data/meal/MealPlanningRepository.java
@@ -0,0 +1,32 @@
+package com.ruilaizi.example.data.meal;
+
+import android.content.Context;
+
+import com.ruilaizi.example.data.api.ApiKeyProvider;
+import com.ruilaizi.example.data.api.OpenAICompletionService;
+
+/**
+ * 饭菜规划仓库:本地调用 DeepSeek 生成饭菜清单,与 example 项目提示词优化/生成同构。
+ */
+public class MealPlanningRepository {
+
+ private final ApiKeyProvider apiKeyProvider;
+ private final MealPlanGenerator generator;
+
+ public MealPlanningRepository(Context context) {
+ apiKeyProvider = new ApiKeyProvider(context);
+ OpenAICompletionService completionService = new OpenAICompletionService(null);
+ generator = new MealPlanGenerator(completionService, apiKeyProvider);
+ }
+
+ public String getApiKey() {
+ return apiKeyProvider.getApiKey();
+ }
+
+ /**
+ * 生成饭菜规划。需在后台线程调用。
+ */
+ public String generateMealPlan(MealPlanGenerator.MealPlanParams params) throws Exception {
+ return generator.generate(params);
+ }
+}
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
new file mode 100644
index 0000000..10d2f4d
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/ui/MealPlanningViewModel.java
@@ -0,0 +1,77 @@
+package com.ruilaizi.example.ui;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.ruilaizi.example.data.meal.MealPlanGenerator;
+import com.ruilaizi.example.data.meal.MealPlanningRepository;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * 饭菜规划页 ViewModel:参数、生成、结果、加载与错误。
+ */
+public class MealPlanningViewModel extends AndroidViewModel {
+
+ private final MealPlanningRepository repository;
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ private final MutableLiveData resultLiveData = new MutableLiveData<>("");
+ private final MutableLiveData loadingLiveData = new MutableLiveData<>(false);
+ private final MutableLiveData errorLiveData = new MutableLiveData<>();
+ private final MutableLiveData snackbarLiveData = new MutableLiveData<>();
+
+ public MealPlanningViewModel(@NonNull Application application) {
+ super(application);
+ repository = new MealPlanningRepository(application);
+ }
+
+ public LiveData getResultLiveData() { return resultLiveData; }
+ public LiveData getLoadingLiveData() { return loadingLiveData; }
+ public LiveData getErrorLiveData() { return errorLiveData; }
+ public LiveData getSnackbarLiveData() { return snackbarLiveData; }
+
+ /**
+ * 生成饭菜规划。参数由调用方从界面读取并传入。
+ */
+ public void generate(MealPlanGenerator.MealPlanParams params) {
+ if (repository.getApiKey() == null || repository.getApiKey().isEmpty()) {
+ errorLiveData.setValue("请先在主界面设置中配置 API Key");
+ return;
+ }
+ loadingLiveData.setValue(true);
+ resultLiveData.setValue("");
+ errorLiveData.setValue(null);
+
+ executor.execute(() -> {
+ try {
+ final String plan = repository.generateMealPlan(params);
+ resultLiveData.postValue(plan);
+ snackbarLiveData.postValue("饭菜规划生成完成");
+ } catch (Exception e) {
+ errorLiveData.postValue(e != null ? e.getMessage() : "生成失败");
+ } finally {
+ loadingLiveData.postValue(false);
+ }
+ });
+ }
+
+ public void clearError() {
+ errorLiveData.setValue(null);
+ }
+
+ public void clearSnackbar() {
+ snackbarLiveData.setValue(null);
+ }
+
+ @Override
+ protected void onCleared() {
+ executor.shutdown();
+ super.onCleared();
+ }
+}
diff --git a/example/app/src/main/res/layout/activity_main.xml b/example/app/src/main/res/layout/activity_main.xml
index 0bc1317..badeec5 100644
--- a/example/app/src/main/res/layout/activity_main.xml
+++ b/example/app/src/main/res/layout/activity_main.xml
@@ -28,6 +28,13 @@
android:textColor="@color/colorPrimary"
android:textSize="24sp"
android:textStyle="bold" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/app/src/main/res/values/arrays.xml b/example/app/src/main/res/values/arrays.xml
index 7ea209f..e680f5b 100644
--- a/example/app/src/main/res/values/arrays.xml
+++ b/example/app/src/main/res/values/arrays.xml
@@ -1,5 +1,24 @@
+
+
+ - 全国
- 北方
- 南方
- 川菜
- 粤菜
+ - 鲁菜
- 苏菜
- 浙菜
- 闽菜
- 湘菜
- 徽菜
+
+
+
+ - 1人
- 2人
- 3人
- 4人
- 5人
+ - 6人
- 8人
- 10人
+
+
+
+ - 早餐
- 午餐
- 晚餐
- 全天
+
+
+
+ - 50元以下
- 50-100元
- 100-150元
- 150-200元
+ - 200-300元
- 300-500元
- 500元以上
+
diff --git a/example/app/src/main/res/values/strings.xml b/example/app/src/main/res/values/strings.xml
index bfb0d78..c3b404f 100644
--- a/example/app/src/main/res/values/strings.xml
+++ b/example/app/src/main/res/values/strings.xml
@@ -32,4 +32,8 @@
暂无生成记录
暂无直答记录
使用
+ 智能饭菜规划
+ 生成饭菜规划
+ 填写左侧参数后点击「生成饭菜规划」
+ 饭菜规划