饭菜规划功能
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:
2026-03-04 23:10:03 +08:00
parent 4dc47ed186
commit 6205ac6208
15 changed files with 517 additions and 12 deletions

View File

@@ -30,6 +30,10 @@
android:name=".MealPlanningActivity" android:name=".MealPlanningActivity"
android:exported="false" android:exported="false"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity
android:name=".MealPlanningHistoryActivity"
android:exported="false"
android:screenOrientation="portrait" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"

View File

@@ -3,6 +3,7 @@ package com.ruilaizi.example;
import android.content.ClipData; import android.content.ClipData;
import android.content.ClipboardManager; import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
@@ -16,6 +17,7 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.button.MaterialButton; 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.snackbar.Snackbar;
import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputEditText;
import com.ruilaizi.example.data.meal.MealPlanGenerator; import com.ruilaizi.example.data.meal.MealPlanGenerator;
@@ -43,11 +45,33 @@ public class MealPlanningActivity extends AppCompatActivity {
setupSpinners(); setupSpinners();
binding.btnBack.setOnClickListener(v -> finish()); binding.btnBack.setOnClickListener(v -> finish());
if (binding.btnMealHistory != null) {
binding.btnMealHistory.setOnClickListener(v -> openHistory());
}
binding.btnGenerate.setOnClickListener(v -> doGenerate()); binding.btnGenerate.setOnClickListener(v -> doGenerate());
binding.btnCopy.setOnClickListener(v -> copyResult()); binding.btnCopy.setOnClickListener(v -> copyResult());
observeViewModel(); 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() { private void setupSpinners() {
ArrayAdapter<CharSequence> regionAdapter = ArrayAdapter.createFromResource(this, R.array.meal_region_types, android.R.layout.simple_spinner_item); 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); regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
@@ -69,6 +93,37 @@ public class MealPlanningActivity extends AppCompatActivity {
binding.spinnerBudget.setSelection(1); // 50-100元 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() { private void doGenerate() {
String hometown = getText(binding.etHometown); String hometown = getText(binding.etHometown);
if (hometown.isEmpty()) { if (hometown.isEmpty()) {
@@ -83,6 +138,10 @@ public class MealPlanningActivity extends AppCompatActivity {
params.setPreferences(getText(binding.etPreferences)); params.setPreferences(getText(binding.etPreferences));
params.setDietaryRestrictions(getText(binding.etDietary)); params.setDietaryRestrictions(getText(binding.etDietary));
params.setBudget(BUDGET_VALUES[binding.spinnerBudget.getSelectedItemPosition()]); 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); viewModel.generate(params);
} }

View File

@@ -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);
});
}
}

View File

@@ -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);
}
}
}

View File

@@ -42,7 +42,7 @@ public class OpenAICompletionService {
conn.setRequestMethod("POST"); conn.setRequestMethod("POST");
conn.setDoOutput(true); conn.setDoOutput(true);
conn.setConnectTimeout(30000); conn.setConnectTimeout(30000);
conn.setReadTimeout(60000); conn.setReadTimeout(120000); // 120s饭菜规划等长文生成易超时
conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Authorization", "Bearer " + apiKey); conn.setRequestProperty("Authorization", "Bearer " + apiKey);

View File

@@ -6,7 +6,7 @@ import androidx.room.Database;
import androidx.room.Room; import androidx.room.Room;
import androidx.room.RoomDatabase; 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 { public abstract class AppDatabase extends RoomDatabase {
private static volatile AppDatabase INSTANCE; private static volatile AppDatabase INSTANCE;
@@ -29,4 +29,5 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract GenerationRecordDao generationRecordDao(); public abstract GenerationRecordDao generationRecordDao();
public abstract OptimizeRecordDao optimizeRecordDao(); public abstract OptimizeRecordDao optimizeRecordDao();
public abstract DirectAnswerRecordDao directAnswerRecordDao(); public abstract DirectAnswerRecordDao directAnswerRecordDao();
public abstract MealPlanRecordDao mealPlanRecordDao();
} }

View File

@@ -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();
}
}

View File

@@ -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();
}

View File

@@ -8,8 +8,9 @@ import com.ruilaizi.example.data.api.OpenAICompletionService;
*/ */
public class MealPlanGenerator { public class MealPlanGenerator {
private static final float TEMPERATURE = 0.7f; /** 默认与 Flask 一致,可由 MealPlanParams 覆盖 */
private static final int MAX_TOKENS = 2000; private static final float DEFAULT_TEMPERATURE = 0.7f;
private static final int DEFAULT_MAX_TOKENS = 1000;
private final OpenAICompletionService completionService; private final OpenAICompletionService completionService;
private final ApiKeyProvider apiKeyProvider; private final ApiKeyProvider apiKeyProvider;
@@ -28,6 +29,7 @@ public class MealPlanGenerator {
throw new Exception("请先在设置中配置 API Key"); throw new Exception("请先在设置中配置 API Key");
} }
String regionType = params.getRegionType() != null ? params.getRegionType() : "全国"; String regionType = params.getRegionType() != null ? params.getRegionType() : "全国";
// 与 Flask meal_planning.py 完全一致的系统提示
String systemPrompt = "你是一位专业的" + regionType + "智能饭菜清单规划师和营养搭配专家。\n\n" String systemPrompt = "你是一位专业的" + regionType + "智能饭菜清单规划师和营养搭配专家。\n\n"
+ "请按以下**固定结构**输出 Markdown便于用户查阅\n\n" + "请按以下**固定结构**输出 Markdown便于用户查阅\n\n"
+ "**一、菜品推荐与搭配思路**\n" + "**一、菜品推荐与搭配思路**\n"
@@ -50,20 +52,24 @@ public class MealPlanGenerator {
if (hometown.isEmpty()) { if (hometown.isEmpty()) {
throw new Exception("请输入用餐者家乡"); throw new Exception("请输入用餐者家乡");
} }
// 默认值与 Flask generate_meal_plan_api 一致
String dinerCount = params.getDinerCount() != null ? params.getDinerCount() : "2"; String dinerCount = params.getDinerCount() != null ? params.getDinerCount() : "2";
String mealType = params.getMealType() != null ? params.getMealType() : "午餐"; String mealType = params.getMealType() != null ? params.getMealType() : "午餐";
String preferences = params.getPreferences() != null ? params.getPreferences().trim() : ""; String preferences = params.getPreferences() != null ? params.getPreferences().trim() : "";
String dietaryRestrictions = params.getDietaryRestrictions() != null ? params.getDietaryRestrictions().trim() : ""; String dietaryRestrictions = params.getDietaryRestrictions() != null ? params.getDietaryRestrictions().trim() : "";
String budget = params.getBudget() != null ? params.getBudget() : "100"; 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 String userPrompt = "" + dinerCount + "人制定" + mealType + "饭菜清单。家乡:" + hometown
+ "。喜好:" + preferences + "。禁忌:" + dietaryRestrictions + "。预算:" + budget + "。喜好:" + 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 { public static class MealPlanParams {
private String regionType; private String regionType;
private String dinerCount; private String dinerCount;
@@ -72,6 +78,10 @@ public class MealPlanGenerator {
private String preferences; private String preferences;
private String dietaryRestrictions; private String dietaryRestrictions;
private String budget; private String budget;
/** 创意程度null 时用默认 0.7 */
private Float temperature;
/** 最大 token 数null 时用默认 1000 */
private Integer maxTokens;
public String getRegionType() { return regionType; } public String getRegionType() { return regionType; }
public void setRegionType(String regionType) { this.regionType = 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 void setDietaryRestrictions(String dietaryRestrictions) { this.dietaryRestrictions = dietaryRestrictions; }
public String getBudget() { return budget; } public String getBudget() { return budget; }
public void setBudget(String budget) { this.budget = 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; }
} }
} }

View File

@@ -2,21 +2,30 @@ package com.ruilaizi.example.data.meal;
import android.content.Context; import android.content.Context;
import androidx.lifecycle.LiveData;
import com.ruilaizi.example.data.api.ApiKeyProvider; import com.ruilaizi.example.data.api.ApiKeyProvider;
import com.ruilaizi.example.data.api.OpenAICompletionService; 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 { public class MealPlanningRepository {
private final ApiKeyProvider apiKeyProvider; private final ApiKeyProvider apiKeyProvider;
private final MealPlanGenerator generator; private final MealPlanGenerator generator;
private final MealPlanRecordDao mealPlanRecordDao;
public MealPlanningRepository(Context context) { public MealPlanningRepository(Context context) {
apiKeyProvider = new ApiKeyProvider(context); apiKeyProvider = new ApiKeyProvider(context);
OpenAICompletionService completionService = new OpenAICompletionService(null); OpenAICompletionService completionService = new OpenAICompletionService(null);
generator = new MealPlanGenerator(completionService, apiKeyProvider); generator = new MealPlanGenerator(completionService, apiKeyProvider);
mealPlanRecordDao = AppDatabase.getInstance(context).mealPlanRecordDao();
} }
public String getApiKey() { public String getApiKey() {
@@ -29,4 +38,18 @@ public class MealPlanningRepository {
public String generateMealPlan(MealPlanGenerator.MealPlanParams params) throws Exception { public String generateMealPlan(MealPlanGenerator.MealPlanParams params) throws Exception {
return generator.generate(params); 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();
}
} }

View File

@@ -10,6 +10,7 @@ import androidx.lifecycle.MutableLiveData;
import com.ruilaizi.example.data.meal.MealPlanGenerator; import com.ruilaizi.example.data.meal.MealPlanGenerator;
import com.ruilaizi.example.data.meal.MealPlanningRepository; import com.ruilaizi.example.data.meal.MealPlanningRepository;
import java.net.SocketTimeoutException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -52,9 +53,17 @@ public class MealPlanningViewModel extends AndroidViewModel {
try { try {
final String plan = repository.generateMealPlan(params); final String plan = repository.generateMealPlan(params);
resultLiveData.postValue(plan); resultLiveData.postValue(plan);
if (plan != null && !plan.isEmpty()) {
repository.saveMealPlanRecord(new com.ruilaizi.example.data.db.MealPlanRecord(params, plan));
}
snackbarLiveData.postValue("饭菜规划生成完成"); snackbarLiveData.postValue("饭菜规划生成完成");
} catch (Exception e) { } 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 { } finally {
loadingLiveData.postValue(false); loadingLiveData.postValue(false);
} }

View File

@@ -35,7 +35,12 @@
android:textSize="20sp" android:textSize="20sp"
android:textStyle="bold" android:textStyle="bold"
android:gravity="center" /> 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> </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" /> <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 <com.google.android.material.button.MaterialButton
android:id="@+id/btn_generate" android:id="@+id/btn_generate"

View File

@@ -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>

View 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>

View File

@@ -36,4 +36,6 @@
<string name="meal_planning_btn_generate">生成饭菜规划</string> <string name="meal_planning_btn_generate">生成饭菜规划</string>
<string name="meal_planning_result_placeholder">填写左侧参数后点击「生成饭菜规划」</string> <string name="meal_planning_result_placeholder">填写左侧参数后点击「生成饭菜规划」</string>
<string name="btn_meal_planning">饭菜规划</string> <string name="btn_meal_planning">饭菜规划</string>
<string name="meal_planning_history_title">规划历史</string>
<string name="meal_planning_history_empty">暂无规划记录</string>
</resources> </resources>