饭菜规划功能
Some checks failed
Flask 提示词大师 - CI/CD 流水线 / 代码质量检查 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 单元测试 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 集成测试 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 构建Docker镜像 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 部署到测试环境 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 部署到生产环境 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 部署监控系统 (push) Has been cancelled
Some checks failed
Flask 提示词大师 - CI/CD 流水线 / 代码质量检查 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 单元测试 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 集成测试 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 构建Docker镜像 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 部署到测试环境 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 部署到生产环境 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 部署监控系统 (push) Has been cancelled
This commit is contained in:
@@ -30,6 +30,10 @@
|
||||
android:name=".MealPlanningActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".MealPlanningHistoryActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.ruilaizi.example;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ArrayAdapter;
|
||||
@@ -16,6 +17,7 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.android.material.slider.Slider;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.ruilaizi.example.data.meal.MealPlanGenerator;
|
||||
@@ -43,11 +45,33 @@ public class MealPlanningActivity extends AppCompatActivity {
|
||||
|
||||
setupSpinners();
|
||||
binding.btnBack.setOnClickListener(v -> 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<CharSequence> 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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<MealPlanHistoryAdapter.ViewHolder> {
|
||||
|
||||
private final List<MealPlanRecord> 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<MealPlanRecord> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<List<MealPlanRecord>> 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();
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<List<MealPlanRecord>> getRecentMealPlanRecords() {
|
||||
return mealPlanRecordDao.getRecentRecords();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,12 @@
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center" />
|
||||
<View android:layout_width="48dp" android:layout_height="wrap_content" />
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_meal_history"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/btn_history" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 规划参数卡片 -->
|
||||
@@ -108,7 +113,43 @@
|
||||
|
||||
<!-- 预算 -->
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="预算范围(元)" android:textSize="12sp" android:textColor="@color/colorTextSecondary" android:layout_marginBottom="4dp" />
|
||||
<Spinner android:id="@+id/spinner_budget" android:layout_width="match_parent" android:layout_height="48dp" android:layout_marginBottom="16dp" />
|
||||
<Spinner android:id="@+id/spinner_budget" android:layout_width="match_parent" android:layout_height="48dp" android:layout_marginBottom="12dp" />
|
||||
|
||||
<!-- 生成长度:与主界面生成一致 -->
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="生成长度" android:textSize="12sp" android:textColor="@color/colorTextSecondary" android:layout_marginBottom="4dp" />
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/slider_meal_length"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="2"
|
||||
android:stepSize="1"
|
||||
android:value="1"
|
||||
app:labelBehavior="gone"
|
||||
android:layout_marginBottom="4dp" />
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_marginBottom="12dp">
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/length_short" android:textSize="12sp" android:textColor="@color/colorTextSecondary" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/length_medium" android:textSize="12sp" android:gravity="center" android:textColor="@color/colorTextSecondary" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/length_long" android:textSize="12sp" android:gravity="end" android:textColor="@color/colorTextSecondary" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 创意程度 -->
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="创意程度" android:textSize="12sp" android:textColor="@color/colorTextSecondary" android:layout_marginBottom="4dp" />
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/slider_meal_temperature"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="2"
|
||||
android:stepSize="1"
|
||||
android:value="1"
|
||||
app:labelBehavior="gone"
|
||||
android:layout_marginBottom="4dp" />
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_marginBottom="16dp">
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/temp_conservative" android:textSize="12sp" android:textColor="@color/colorTextSecondary" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/temp_balanced" android:textSize="12sp" android:gravity="center" android:textColor="@color/colorTextSecondary" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/temp_creative" android:textSize="12sp" android:gravity="end" android:textColor="@color/colorTextSecondary" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_generate"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="16dp"
|
||||
android:background="@color/colorCard">
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_back"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/history_back"
|
||||
android:textColor="@color/colorPrimary" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/meal_planning_history_title"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="12dp"
|
||||
android:clipToPadding="false"
|
||||
tools:listitem="@layout/item_meal_plan_history" />
|
||||
<TextView
|
||||
android:id="@+id/empty_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/meal_planning_history_empty"
|
||||
android:textColor="@color/colorTextSecondary"
|
||||
android:visibility="gone" />
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
67
example/app/src/main/res/layout/item_meal_plan_history.xml
Normal file
67
example/app/src/main/res/layout/item_meal_plan_history.xml
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="@color/colorCard"
|
||||
app:cardElevation="2dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_summary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/colorTextDark"
|
||||
android:textSize="14sp"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
tools:text="家乡:四川 | 午餐 | 2人 | 50-100元" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:textColor="@color/colorText"
|
||||
android:textSize="12sp"
|
||||
android:maxLines="3"
|
||||
android:ellipsize="end"
|
||||
tools:text="一、菜品推荐与搭配思路…" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:textColor="@color/colorTextSecondary"
|
||||
android:textSize="11sp"
|
||||
tools:text="2025-03-02 14:30" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="8dp">
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_use"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/history_use" />
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_copy"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/btn_copy" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -36,4 +36,6 @@
|
||||
<string name="meal_planning_btn_generate">生成饭菜规划</string>
|
||||
<string name="meal_planning_result_placeholder">填写左侧参数后点击「生成饭菜规划」</string>
|
||||
<string name="btn_meal_planning">饭菜规划</string>
|
||||
<string name="meal_planning_history_title">规划历史</string>
|
||||
<string name="meal_planning_history_empty">暂无规划记录</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user