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 @@ 暂无生成记录 暂无直答记录 使用 + 智能饭菜规划 + 生成饭菜规划 + 填写左侧参数后点击「生成饭菜规划」 + 饭菜规划