ai应用
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:
rjb
2026-03-04 23:24:28 +08:00
parent 6205ac6208
commit 5efd79ddfc
24 changed files with 1180 additions and 4 deletions

View File

@@ -34,6 +34,26 @@
android:name=".MealPlanningHistoryActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".AIServicesActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".PlaceholderActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".TravelPlanningActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".WeeklyReportActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".MeetingMinutesActivity"
android:exported="false"
android:screenOrientation="portrait" />
<provider
android:name="androidx.core.content.FileProvider"

View File

@@ -0,0 +1,111 @@
package com.ruilaizi.example;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.ruilaizi.example.databinding.ActivityAiServicesBinding;
import com.ruilaizi.example.databinding.ItemAiServiceBinding;
import java.util.ArrayList;
import java.util.List;
/**
* AI 智能应用列表:有参考实现的跳转对应页,无的跳转占位页。
*/
public class AIServicesActivity extends AppCompatActivity {
private static final class Entry {
final String title;
final String subtitle;
final String badge;
@Nullable final Class<?> activityClass;
final boolean placeholder;
Entry(String title, String subtitle, String badge, @Nullable Class<?> activityClass) {
this.title = title;
this.subtitle = subtitle;
this.badge = badge;
this.activityClass = activityClass;
this.placeholder = (activityClass == null);
}
}
private final List<Entry> entries = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityAiServicesBinding binding = ActivityAiServicesBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
buildEntries();
binding.btnBack.setOnClickListener(v -> finish());
LinearLayout container = binding.containerAiList;
LayoutInflater inflater = getLayoutInflater();
for (int i = 0; i < entries.size(); i++) {
Entry e = entries.get(i);
ItemAiServiceBinding itemBinding = ItemAiServiceBinding.inflate(inflater, container, false);
itemBinding.itemTitle.setText(e.title);
itemBinding.itemSubtitle.setText(e.subtitle);
itemBinding.itemBadge.setText(e.badge);
itemBinding.itemBadge.setVisibility(View.VISIBLE);
final int index = i;
itemBinding.getRoot().setOnClickListener(v -> onItemClick(entries.get(index)));
container.addView(itemBinding.getRoot());
}
}
private void buildEntries() {
entries.clear();
// 有参考实现的:跳转对应 Activity
entries.add(new Entry("智能饭菜规划", "AI智能推荐营养搭配让每一餐都健康美味", "热门", MealPlanningActivity.class));
entries.add(new Entry("提示词优化", "两阶段专家生成专业提示词", "通用", null)); // 主界面已有,可跳回主界面
entries.add(new Entry("旅行攻略规划", "根据目的地与天数AI 生成行程与实用建议", "出行", TravelPlanningActivity.class));
entries.add(new Entry("智能周报生成", "输入工作要点,一键生成规范周报/日报", "办公", WeeklyReportActivity.class));
entries.add(new Entry("会议纪要整理", "粘贴会议转写或要点,一键生成结构化会议纪要", "办公", MeetingMinutesActivity.class));
// 以下为占位
entries.add(new Entry("古诗词解析", "深度解析古诗词意境,感受中华文化之美", "文化", null));
entries.add(new Entry("古诗词收藏", "收藏喜爱的古诗词,建立个人文化宝库", "收藏", null));
entries.add(new Entry("简历/求职信优化", "润色简历或根据岗位生成针对性求职信", "求职", null));
entries.add(new Entry("智能提示词优化2号专家", "需求分析 + 领域专家生成高质量提示词", "专家", null));
entries.add(new Entry("智能提示词优化3号专家", "需求分析 + 领域专家生成,含历史记录", "专家", null));
entries.add(new Entry("读书笔记/摘要", "生成摘要、金句、思维导图式大纲或读后感", "文化", null));
entries.add(new Entry("育儿/教育助手", "育儿建议、睡前故事、简单科普或习题讲解", "教育", null));
entries.add(new Entry("健身/运动计划", "根据目标与场地生成一周训练计划", "健康", null));
entries.add(new Entry("合同/条款解读", "人话版要点、风险提示与建议关注条款", "法律", null));
entries.add(new Entry("小红书/短视频脚本", "生成标题、分镜文案、口播稿", "创作", null));
entries.add(new Entry("面试模拟/问答准备", "按岗位类型出题并给参考答案与点评", "求职", null));
entries.add(new Entry("节日/祝福语生成", "祝福语、贺卡文案、红包配文", "生活", null));
entries.add(new Entry("个人复盘/周复盘", "结构化复盘与下周重点", "成长", null));
entries.add(new Entry("待办/日程整理", "零散待办生成按优先级与日期的日程清单", "效率", null));
entries.add(new Entry("邮件润色/回复建议", "草稿或要点生成得体邮件回复", "办公", null));
entries.add(new Entry("演讲稿/汇报稿", "根据主题与场合生成演讲稿或汇报大纲", "办公", null));
entries.add(new Entry("取名/品牌名", "公司名、产品名或宝宝取名建议", "创意", null));
entries.add(new Entry("菜谱/做法", "根据食材或菜名生成详细做法", "生活", null));
entries.add(new Entry("学习计划/备考规划", "生成学习计划与复习建议", "教育", null));
}
private void onItemClick(Entry e) {
if (e.activityClass != null) {
startActivity(new Intent(this, e.activityClass));
} else if ("提示词优化".equals(e.title)) {
// 主界面已有优化提示词,返回主界面
startActivity(new Intent(this, MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
finish();
} else {
Intent intent = new Intent(this, PlaceholderActivity.class);
intent.putExtra(PlaceholderActivity.EXTRA_TITLE, e.title);
intent.putExtra(PlaceholderActivity.EXTRA_SUBTITLE, e.subtitle);
startActivity(intent);
}
}
}

View File

@@ -48,8 +48,8 @@ public class MainActivity extends AppCompatActivity {
if (binding.btnHistory != null) {
binding.btnHistory.setOnClickListener(v -> openHistory());
}
if (binding.btnMealPlanning != null) {
binding.btnMealPlanning.setOnClickListener(v -> startActivity(new Intent(this, MealPlanningActivity.class)));
if (binding.btnAiServices != null) {
binding.btnAiServices.setOnClickListener(v -> startActivity(new Intent(this, AIServicesActivity.class)));
}
observeViewModel();
showApiKeyHintIfNeeded();

View File

@@ -0,0 +1,87 @@
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.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.textfield.TextInputEditText;
import com.ruilaizi.example.databinding.ActivityMeetingMinutesBinding;
import com.ruilaizi.example.ui.MeetingMinutesViewModel;
public class MeetingMinutesActivity extends AppCompatActivity {
private ActivityMeetingMinutesBinding binding;
private MeetingMinutesViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMeetingMinutesBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
viewModel = new ViewModelProvider(this, new ViewModelProvider.AndroidViewModelFactory(getApplication())).get(MeetingMinutesViewModel.class);
binding.btnBack.setOnClickListener(v -> finish());
binding.btnGenerate.setOnClickListener(v -> doGenerate());
binding.btnCopy.setOnClickListener(v -> copyResult());
observeViewModel();
}
private void doGenerate() {
String raw = getText(binding.etRawContent);
if (raw.isEmpty()) {
Toast.makeText(this, "请粘贴或输入会议内容", Toast.LENGTH_SHORT).show();
return;
}
String title = getText(binding.etTitle);
viewModel.generate(raw, title);
}
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()) {
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();
}
});
}
}

View File

@@ -0,0 +1,34 @@
package com.ruilaizi.example;
import android.os.Bundle;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.ruilaizi.example.databinding.ActivityPlaceholderBinding;
/**
* 占位页:无参考实现的功能仅显示标题与「敬请期待」。
*/
public class PlaceholderActivity extends AppCompatActivity {
public static final String EXTRA_TITLE = "title";
public static final String EXTRA_SUBTITLE = "subtitle";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityPlaceholderBinding binding = ActivityPlaceholderBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
String title = getIntent() != null && getIntent().hasExtra(EXTRA_TITLE)
? getIntent().getStringExtra(EXTRA_TITLE) : "AI 应用";
String subtitle = getIntent() != null && getIntent().hasExtra(EXTRA_SUBTITLE)
? getIntent().getStringExtra(EXTRA_SUBTITLE) : "";
binding.placeholderTitle.setText(title);
binding.placeholderSubtitle.setText(subtitle != null ? subtitle : "");
binding.placeholderHint.setText(R.string.placeholder_coming_soon);
binding.btnBack.setOnClickListener(v -> finish());
}
}

View File

@@ -0,0 +1,84 @@
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 androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.textfield.TextInputEditText;
import com.ruilaizi.example.databinding.ActivityTravelPlanningBinding;
import com.ruilaizi.example.ui.TravelPlanningViewModel;
public class TravelPlanningActivity extends AppCompatActivity {
private ActivityTravelPlanningBinding binding;
private TravelPlanningViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityTravelPlanningBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
viewModel = new ViewModelProvider(this, new ViewModelProvider.AndroidViewModelFactory(getApplication())).get(TravelPlanningViewModel.class);
binding.btnBack.setOnClickListener(v -> finish());
binding.btnGenerate.setOnClickListener(v -> doGenerate());
binding.btnCopy.setOnClickListener(v -> copyResult());
observeViewModel();
}
private void doGenerate() {
String dest = getText(binding.etDestination);
if (dest.isEmpty()) {
android.widget.Toast.makeText(this, "请填写目的地", android.widget.Toast.LENGTH_SHORT).show();
return;
}
String days = getText(binding.etDays);
String people = getText(binding.etPeople);
String prefs = getText(binding.etPreferences);
viewModel.generate(dest, days.isEmpty() ? "3" : days, people.isEmpty() ? "2" : people, prefs, "适中");
}
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()) {
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();
}
});
}
}

View File

@@ -0,0 +1,86 @@
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.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.textfield.TextInputEditText;
import com.ruilaizi.example.databinding.ActivityWeeklyReportBinding;
import com.ruilaizi.example.ui.WeeklyReportViewModel;
public class WeeklyReportActivity extends AppCompatActivity {
private ActivityWeeklyReportBinding binding;
private WeeklyReportViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityWeeklyReportBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
viewModel = new ViewModelProvider(this, new ViewModelProvider.AndroidViewModelFactory(getApplication())).get(WeeklyReportViewModel.class);
binding.btnBack.setOnClickListener(v -> finish());
binding.btnGenerate.setOnClickListener(v -> doGenerate());
binding.btnCopy.setOnClickListener(v -> copyResult());
observeViewModel();
}
private void doGenerate() {
String content = getText(binding.etContent);
if (content.isEmpty()) {
Toast.makeText(this, "请填写工作要点或内容", Toast.LENGTH_SHORT).show();
return;
}
viewModel.generate("weekly", content);
}
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()) {
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();
}
});
}
}

View File

@@ -0,0 +1,31 @@
package com.ruilaizi.example.data.meeting;
import com.ruilaizi.example.data.api.ApiKeyProvider;
import com.ruilaizi.example.data.api.OpenAICompletionService;
/**
* 会议纪要整理生成器,与 Flask meeting_minutes 逻辑一致。
*/
public class MeetingMinutesGenerator {
private static final String SYSTEM_PROMPT = "你是一位专业的会议记录助手,擅长将零散的会议转写或要点整理成结构清晰的会议纪要。\n\n要求\n"
+ "1. 从用户输入中提炼:会议主题、时间/参与人(如有)、讨论要点、结论与决议、待办事项(含责任人/截止时间若可推断)。\n"
+ "2. 使用 Markdown 格式:一级标题为「一、会议概要」「二、讨论要点」「三、结论与决议」「四、待办事项」等,二级标题和列表项简明扼要。\n"
+ "3. 语气正式、条理清楚,便于归档和分享。若信息不足可合理归纳,不要编造具体人名与数据。";
private final OpenAICompletionService completionService;
private final ApiKeyProvider apiKeyProvider;
public MeetingMinutesGenerator(OpenAICompletionService completionService, ApiKeyProvider apiKeyProvider) {
this.completionService = completionService;
this.apiKeyProvider = apiKeyProvider;
}
public String generate(String rawContent, String title) throws Exception {
String apiKey = apiKeyProvider.getApiKey();
if (apiKey == null || apiKey.isEmpty()) throw new Exception("请先在设置中配置 API Key");
if (rawContent == null || rawContent.trim().isEmpty()) throw new Exception("请粘贴或输入会议内容");
String userPrompt = "请将以下会议内容整理成会议纪要:\n\n" + (title != null && !title.isEmpty() ? "会议主题:" + title + "\n\n" : "") + rawContent.trim();
return completionService.chat(SYSTEM_PROMPT, userPrompt, 0.4f, 2000, apiKey);
}
}

View File

@@ -0,0 +1,20 @@
package com.ruilaizi.example.data.meeting;
import android.content.Context;
import com.ruilaizi.example.data.api.ApiKeyProvider;
import com.ruilaizi.example.data.api.OpenAICompletionService;
public class MeetingMinutesRepository {
private final MeetingMinutesGenerator generator;
public MeetingMinutesRepository(Context context) {
ApiKeyProvider apiKeyProvider = new ApiKeyProvider(context);
generator = new MeetingMinutesGenerator(new OpenAICompletionService(null), apiKeyProvider);
}
public String generate(String rawContent, String title) throws Exception {
return generator.generate(rawContent, title);
}
}

View File

@@ -0,0 +1,30 @@
package com.ruilaizi.example.data.travel;
import com.ruilaizi.example.data.api.ApiKeyProvider;
import com.ruilaizi.example.data.api.OpenAICompletionService;
/** 旅行攻略生成器,与 Flask travel_planning 逻辑一致。 */
public class TravelPlanGenerator {
private static final String SYSTEM_PROMPT = "你是一位专业的旅行规划师,擅长根据目的地、天数和偏好生成实用、可执行的旅行攻略。\n\n"
+ "请按以下 **Markdown 结构** 输出:\n**一、行程概览**\n**二、每日行程**(按天,上午/下午/晚上)\n"
+ "**三、实用信息**(交通、住宿、美食、预算、注意事项)。要求:具体可执行、符合预算与偏好。";
private final OpenAICompletionService completionService;
private final ApiKeyProvider apiKeyProvider;
public TravelPlanGenerator(OpenAICompletionService completionService, ApiKeyProvider apiKeyProvider) {
this.completionService = completionService;
this.apiKeyProvider = apiKeyProvider;
}
public String generate(String destination, String days, String people, String preferences, String budget) throws Exception {
String apiKey = apiKeyProvider.getApiKey();
if (apiKey == null || apiKey.isEmpty()) throw new Exception("请先在设置中配置 API Key");
if (destination == null || destination.trim().isEmpty()) throw new Exception("请填写目的地");
String user = "目的地:" + destination.trim() + "。出行天数:" + (days != null ? days : "3")
+ "。人数:" + (people != null ? people : "2") + "。偏好:" + (preferences != null ? preferences : "")
+ "。预算:" + (budget != null ? budget : "适中") + "。请生成旅行攻略。";
return completionService.chat(SYSTEM_PROMPT, user, 0.6f, 2000, apiKey);
}
}

View File

@@ -0,0 +1,20 @@
package com.ruilaizi.example.data.travel;
import android.content.Context;
import com.ruilaizi.example.data.api.ApiKeyProvider;
import com.ruilaizi.example.data.api.OpenAICompletionService;
public class TravelPlanningRepository {
private final TravelPlanGenerator generator;
public TravelPlanningRepository(Context context) {
ApiKeyProvider apiKeyProvider = new ApiKeyProvider(context);
generator = new TravelPlanGenerator(new OpenAICompletionService(null), apiKeyProvider);
}
public String generate(String destination, String days, String people, String preferences, String budget) throws Exception {
return generator.generate(destination, days, people, preferences, budget);
}
}

View File

@@ -0,0 +1,32 @@
package com.ruilaizi.example.data.weekly;
import com.ruilaizi.example.data.api.ApiKeyProvider;
import com.ruilaizi.example.data.api.OpenAICompletionService;
/**
* 智能周报/日报生成器,与 Flask weekly_report 逻辑一致。
*/
public class WeeklyReportGenerator {
private final OpenAICompletionService completionService;
private final ApiKeyProvider apiKeyProvider;
public WeeklyReportGenerator(OpenAICompletionService completionService, ApiKeyProvider apiKeyProvider) {
this.completionService = completionService;
this.apiKeyProvider = apiKeyProvider;
}
public String generate(String reportType, String content) throws Exception {
String apiKey = apiKeyProvider.getApiKey();
if (apiKey == null || apiKey.isEmpty()) throw new Exception("请先在设置中配置 API Key");
if (content == null || content.trim().isEmpty()) throw new Exception("请填写工作要点或内容");
String typeName = "weekly".equals(reportType) ? "周报" : "日报";
String systemPrompt = "你是一位专业的职场写作助手,擅长将零散的工作要点整理成规范、简洁的" + typeName + "\n\n要求\n"
+ "1. 根据用户输入的工作要点或流水记录,归纳成结构清晰的" + typeName + "\n"
+ "2. 使用 Markdown 格式:一级标题为「一、本周/今日工作完成情况」等,二级标题区分模块,列表项简明扼要。\n"
+ "3. 可适当补充「二、下周/明日计划」「三、问题与风险」等小节(若用户未提供则简要生成)。\n"
+ "4. 语气正式、条理清楚,便于直接复制到飞书/钉钉/邮件。";
String userPrompt = "请将以下内容整理成一份" + typeName + "\n\n" + content.trim();
return completionService.chat(systemPrompt, userPrompt, 0.5f, 1500, apiKey);
}
}

View File

@@ -0,0 +1,20 @@
package com.ruilaizi.example.data.weekly;
import android.content.Context;
import com.ruilaizi.example.data.api.ApiKeyProvider;
import com.ruilaizi.example.data.api.OpenAICompletionService;
public class WeeklyReportRepository {
private final WeeklyReportGenerator generator;
public WeeklyReportRepository(Context context) {
ApiKeyProvider apiKeyProvider = new ApiKeyProvider(context);
generator = new WeeklyReportGenerator(new OpenAICompletionService(null), apiKeyProvider);
}
public String generate(String reportType, String content) throws Exception {
return generator.generate(reportType, content);
}
}

View File

@@ -0,0 +1,59 @@
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.meeting.MeetingMinutesRepository;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MeetingMinutesViewModel extends AndroidViewModel {
private final MeetingMinutesRepository repository;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final MutableLiveData<String> resultLiveData = new MutableLiveData<>("");
private final MutableLiveData<Boolean> loadingLiveData = new MutableLiveData<>(false);
private final MutableLiveData<String> errorLiveData = new MutableLiveData<>();
private final MutableLiveData<String> snackbarLiveData = new MutableLiveData<>();
public MeetingMinutesViewModel(@NonNull Application app) {
super(app);
repository = new MeetingMinutesRepository(app);
}
public LiveData<String> getResultLiveData() { return resultLiveData; }
public LiveData<Boolean> getLoadingLiveData() { return loadingLiveData; }
public LiveData<String> getErrorLiveData() { return errorLiveData; }
public LiveData<String> getSnackbarLiveData() { return snackbarLiveData; }
public void generate(String rawContent, String title) {
loadingLiveData.setValue(true);
resultLiveData.setValue("");
errorLiveData.setValue(null);
executor.execute(() -> {
try {
String summary = repository.generate(rawContent, title);
resultLiveData.postValue(summary);
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();
}
}

View File

@@ -0,0 +1,59 @@
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.travel.TravelPlanningRepository;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TravelPlanningViewModel extends AndroidViewModel {
private final TravelPlanningRepository repository;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final MutableLiveData<String> resultLiveData = new MutableLiveData<>("");
private final MutableLiveData<Boolean> loadingLiveData = new MutableLiveData<>(false);
private final MutableLiveData<String> errorLiveData = new MutableLiveData<>();
private final MutableLiveData<String> snackbarLiveData = new MutableLiveData<>();
public TravelPlanningViewModel(@NonNull Application app) {
super(app);
repository = new TravelPlanningRepository(app);
}
public LiveData<String> getResultLiveData() { return resultLiveData; }
public LiveData<Boolean> getLoadingLiveData() { return loadingLiveData; }
public LiveData<String> getErrorLiveData() { return errorLiveData; }
public LiveData<String> getSnackbarLiveData() { return snackbarLiveData; }
public void generate(String destination, String days, String people, String preferences, String budget) {
loadingLiveData.setValue(true);
resultLiveData.setValue("");
errorLiveData.setValue(null);
executor.execute(() -> {
try {
String plan = repository.generate(destination, days, people, preferences, budget);
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();
}
}

View File

@@ -0,0 +1,59 @@
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.weekly.WeeklyReportRepository;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class WeeklyReportViewModel extends AndroidViewModel {
private final WeeklyReportRepository repository;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final MutableLiveData<String> resultLiveData = new MutableLiveData<>("");
private final MutableLiveData<Boolean> loadingLiveData = new MutableLiveData<>(false);
private final MutableLiveData<String> errorLiveData = new MutableLiveData<>();
private final MutableLiveData<String> snackbarLiveData = new MutableLiveData<>();
public WeeklyReportViewModel(@NonNull Application app) {
super(app);
repository = new WeeklyReportRepository(app);
}
public LiveData<String> getResultLiveData() { return resultLiveData; }
public LiveData<Boolean> getLoadingLiveData() { return loadingLiveData; }
public LiveData<String> getErrorLiveData() { return errorLiveData; }
public LiveData<String> getSnackbarLiveData() { return snackbarLiveData; }
public void generate(String reportType, String content) {
loadingLiveData.setValue(true);
resultLiveData.setValue("");
errorLiveData.setValue(null);
executor.execute(() -> {
try {
String report = repository.generate(reportType, content);
resultLiveData.postValue(report);
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();
}
}

View File

@@ -0,0 +1,56 @@
<?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"
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">
<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="返回" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/ai_services_title"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/colorPrimary"
android:gravity="center" />
<View android:layout_width="48dp" android:layout_height="wrap_content" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp"
android:text="@string/ai_services_subtitle"
android:textSize="13sp"
android:textColor="@color/colorTextSecondary" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="24dp">
<LinearLayout
android:id="@+id/container_ai_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</ScrollView>
</LinearLayout>

View File

@@ -29,11 +29,11 @@
android:textSize="24sp"
android:textStyle="bold" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_meal_planning"
android:id="@+id/btn_ai_services"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_meal_planning"
android:text="@string/btn_ai_services"
android:layout_marginEnd="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_history"

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorSurface"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<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="返回" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="会议纪要整理"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/colorPrimary"
android:gravity="center" />
<View android:layout_width="48dp" android:layout_height="wrap_content" />
</LinearLayout>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/colorCard"
app:cardElevation="2dp"
app:cardCornerRadius="12dp"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<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.textfield.TextInputEditText
android:id="@+id/et_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="如:产品需求评审"
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.textfield.TextInputEditText
android:id="@+id/et_raw_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minLines="6"
android:hint="粘贴会议转写或要点…"
android:inputType="textMultiLine"
android:layout_marginBottom="16dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_generate"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="生成会议纪要"
app:backgroundTint="@color/colorPrimary"
android:textColor="@color/colorWhite" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<ProgressBar android:id="@+id/progress_bar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:visibility="gone" 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="8dp" />
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/colorCard"
app:cardElevation="2dp"
app:cardCornerRadius="12dp"
android:minHeight="80dp"
android:layout_marginBottom="12dp">
<ScrollView android:layout_width="match_parent" android:layout_height="wrap_content" android:fillViewport="true" android:padding="16dp">
<TextView android:id="@+id/tv_result" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="14sp" android:lineSpacingMultiplier="1.2" android:textColor="@color/colorText" />
</ScrollView>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButton android:id="@+id/btn_copy" style="@style/Widget.MaterialComponents.Button.OutlinedButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/btn_copy" />
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,43 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp"
android:background="@color/colorSurface"
android:gravity="center">
<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:layout_gravity="start"
android:text="返回" />
<TextView
android:id="@+id/placeholder_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:textSize="22sp"
android:textStyle="bold"
android:textColor="@color/colorPrimary" />
<TextView
android:id="@+id/placeholder_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="14sp"
android:textColor="@color/colorTextSecondary" />
<TextView
android:id="@+id/placeholder_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:textSize="16sp"
android:textColor="@color/colorText" />
</LinearLayout>

View File

@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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:background="@color/colorSurface"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- 标题栏:返回 + 标题 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<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="返回" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="旅行攻略规划"
android:textColor="@color/colorPrimary"
android:textSize="20sp"
android:textStyle="bold"
android:gravity="center" />
<View android:layout_width="48dp" android:layout_height="wrap_content" />
</LinearLayout>
<!-- 旅行参数卡片 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/colorCard"
app:cardElevation="2dp"
app:cardCornerRadius="12dp"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<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.textfield.TextInputEditText android:id="@+id/et_destination" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="如:北京" 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.textfield.TextInputEditText android:id="@+id/et_days" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="3" android:inputType="number" 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.textfield.TextInputEditText android:id="@+id/et_people" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="2" android:inputType="number" 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.textfield.TextInputEditText android:id="@+id/et_preferences" android:layout_width="match_parent" android:layout_height="wrap_content" android:minLines="2" android:hint="如:喜欢人文,预算适中" android:inputType="textMultiLine" android:layout_marginBottom="16dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_generate"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="生成旅行攻略"
app:backgroundTint="@color/colorPrimary"
android:textColor="@color/colorWhite" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
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="8dp" />
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/colorCard"
app:cardElevation="2dp"
app:cardCornerRadius="12dp"
android:minHeight="100dp"
android:layout_marginBottom="12dp">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true"
android:padding="16dp">
<TextView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/colorText"
android:textSize="14sp"
android:lineSpacingMultiplier="1.2" />
</ScrollView>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_copy"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/btn_copy" />
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorSurface"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<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="返回" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="智能周报生成"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/colorPrimary"
android:gravity="center" />
<View android:layout_width="48dp" android:layout_height="wrap_content" />
</LinearLayout>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/colorCard"
app:cardElevation="2dp"
app:cardCornerRadius="12dp"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<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.textfield.TextInputEditText android:id="@+id/et_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:minLines="5" android:hint="输入本周或当日工作要点、流水记录…" android:inputType="textMultiLine" android:layout_marginBottom="16dp" />
<com.google.android.material.button.MaterialButton android:id="@+id/btn_generate" android:layout_width="match_parent" android:layout_height="48dp" android:text="生成周报/日报" app:backgroundTint="@color/colorPrimary" android:textColor="@color/colorWhite" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<ProgressBar android:id="@+id/progress_bar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:visibility="gone" 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="8dp" />
<com.google.android.material.card.MaterialCardView android:layout_width="match_parent" android:layout_height="wrap_content" app:cardBackgroundColor="@color/colorCard" app:cardElevation="2dp" app:cardCornerRadius="12dp" android:minHeight="80dp" android:layout_marginBottom="12dp">
<ScrollView android:layout_width="match_parent" android:layout_height="wrap_content" android:fillViewport="true" android:padding="16dp">
<TextView android:id="@+id/tv_result" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="14sp" android:lineSpacingMultiplier="1.2" android:textColor="@color/colorText" />
</ScrollView>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButton android:id="@+id/btn_copy" style="@style/Widget.MaterialComponents.Button.OutlinedButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/btn_copy" />
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,52 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/colorCard"
app:cardElevation="2dp"
app:cardCornerRadius="12dp"
android:clickable="true"
android:focusable="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/item_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/colorTextDark" />
<TextView
android:id="@+id/item_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="11sp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:background="@color/colorPrimary"
android:textColor="@color/colorWhite" />
</LinearLayout>
<TextView
android:id="@+id/item_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="13sp"
android:textColor="@color/colorTextSecondary" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -36,6 +36,10 @@
<string name="meal_planning_btn_generate">生成饭菜规划</string>
<string name="meal_planning_result_placeholder">填写左侧参数后点击「生成饭菜规划」</string>
<string name="btn_meal_planning">饭菜规划</string>
<string name="btn_ai_services">AI应用</string>
<string name="meal_planning_history_title">规划历史</string>
<string name="meal_planning_history_empty">暂无规划记录</string>
<string name="placeholder_coming_soon">敬请期待</string>
<string name="ai_services_title">AI 智能应用</string>
<string name="ai_services_subtitle">探索更多 AI 应用</string>
</resources>