diff --git a/.gitignore b/.gitignore
index 0dc6e5d..af6bf99 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,3 +67,7 @@ ios
**/_old_readme/*
**/_self*/
+**/_self*
+**/mock*/
+**/bak_*/
+**/bak_*
diff --git a/README.md b/README.md
index 8544879..33fcadf 100644
--- a/README.md
+++ b/README.md
@@ -1,55 +1,213 @@
-# free_brief_accounting
+
+
+
-极度简单的支出流水账记录……
+- [AI Light Life](#ai-light-life)
+ - [项目说明](#项目说明)
+ - [版本说明](#版本说明)
+ - [创建原因](#创建原因)
+ - [功能详述](#功能详述)
+ - [Part1: 智能助手 agi\_llm\_sample](#part1-智能助手-agi_llm_sample)
+ - [Part2: 极简记账 brief\_accounting](#part2-极简记账-brief_accounting)
+ - [Part3: 随机菜品 random\_dish](#part3-随机菜品-random_dish)
+ - [使用说明](#使用说明)
+ - [导入的菜品 json 文件格式示例](#导入的菜品-json-文件格式示例)
+ - [Part4: 用户配置 user\_and\_settings](#part4-用户配置-user_and_settings)
+ - [其他说明](#其他说明)
+ - [关键文件缺失](#关键文件缺失)
+ - [开发环境](#开发环境)
+ - [仅 Android](#仅-android)
+ - [TODO](#todo)
-## Getting Started
+
-虽然名字是记账,但实际上就是一些流水账(laundry list)
+# AI Light Life
-### 核心想法
+## 项目说明
-理论上:
+**一个包含极简的记账、幸运转盘随机菜品,和简单的 AI 对话、文生图、图像理解等功能的 flutter 应用**。
-- 消费和收入的分类应该有非常专业详细的数据,在后续的报表时肯定用得上。
-- 多账本、预算、循环记账等等比较专业的东西,不会,所以没有。
-- 什么其他账单的关联(微信、支付宝什么的),就天方夜谭了。
+### 版本说明
-所以,流水账,非常简单,sqlite 的 DDL:
+- 2024-06-27 `v0.1.0-beta.1`
-```sql
-CREATE TABLE "income_list" (
- "uuid" INTEGER NOT NULL,
- "date" TEXT,
- "category" TEXT,
- "item" TEXT,
- "value" REAL,
- PRIMARY KEY("uuid" AUTOINCREMENT)
-);
+ - 基本完成了极简记账的核心功能;
+ - 基本完成了极简 AI 文本对话的核心功能;
+ - “文本对话-官方免费”部分整合了百度、腾讯、阿里几个免费使用的大模型 API;
+ - “文本对话-单个配置”需要自行配置百度、腾讯、阿里的应用 ID 和 KEY,使用自己的付费 API;
+ - “文本生图-通义万相”需要自行配置阿里云百炼平台的应用 ID 和 KEY;
+ - “图像理解-Fuyu8B”使用百度千帆平台的 Fuyu-8B 在线服务,需要自行配置百度千帆平台的应用 ID 和 KEY;
+ - 添加幸运转盘获取随机菜品的功能;
+ - 用户设置页面中可将应用内部数据完全的备份还原。
-CREATE TABLE "expand_list" (
- "uuid" INTEGER,
- "date" TEXT,
- "category" TEXT,
- "item" TEXT,
- "value" REAL,
- PRIMARY KEY("uuid" AUTOINCREMENT)
-);
+### 创建原因
+
+- 一开始只是单纯想开发一个极其简单的自用本地记账的 app。
+- **但 2024 年 5 月底国内大模型开启价格战,有了一大批免费使用的 API**,就临时继续在该项目上测试一下效果。
+- 后来发现经常问答有点帮助,就修改了项目定位,改为生活助手类,并整合了之前写好的随机菜品的功能。
+ - 随机菜品功能,可以随机生成一个菜品,并给出菜品的详细介绍。为了解决整天不知道吃什么的问题。
+- 后续可能会再添加一些自己需要的小功能,逐步形成一个完整的**可能带有 AI 功能的生活助手应用**。
+
+## 功能详述
+
+### Part1: 智能助手 agi_llm_sample
+
+仅仅简单调用百度、腾讯、阿里平台的部分大模型 API,具体功能还在思考当中。
+
+![智能助手](./_md_pics/智能助手.jpg)
+
+- 图 1:目前有考虑文本对话、文生图、图生文的试用;
+- 图 2:“文本对话-官方免费”部分整合了百度、腾讯、阿里几个免费使用的大模型 API;
+ - 3 个平台五六个模型可切换使用;
+ - 图 3:右上角“最近”会保留最近的对话记录,存入本地的 sqlite 中;
+- 图 4:“文本对话-单个配置”需要自行配置百度、腾讯、阿里的应用 ID 和 KEY(见图 8),使用自己的付费 API;
+ - 就算使用自己付费的模型,模型 API 目前我还没有兼容几个;
+- 图 5:“文本生图-通义万相”使用阿里云的通用万相绘制图片,也会留存最近的文生图记录;
+ - 文本生图需要自行配置阿里云百炼平台的应用 ID 和 KEY;
+ - 图 6:可以点击进行预览,长按进行保存;
+ - 生成的图片默认放在阿里云的 OSS,过期时间为 1 天;
+- 图 7:“图像理解-Fuyu8B”可以对传入的图片进行理解,并回复相关问题,英文更佳;
+ - 使用百度千帆平台的 Fuyu-8B 在线服务,需要自行配置百度千帆平台的应用 ID 和 KEY;
+- 图 8:在“智能助手”首页右上角可以一次性配置某个平台的应用 ID 和 KEY,需要相关平台的功能才能使用。
+
+### Part2: 极简记账 brief_accounting
+
+极度简单的支出流水账记录,虽然名字是记账,但实际上就是一些流水账,然后简单的统计图表。
+
+![极简记账](./_md_pics/极简记账.jpg)
+
+- 图 1:每天的支出收入信息。
+ - 默认是显示当前月份的数据,**上下滚动会切换月份**。
+ - 右上角“搜索”按钮,可以对所有的记录进行**关键字搜索**,可以看到相关关键字条目的记录,但不会进行相关统计。
+ - **_长按_** 主页的收支记录项次,可以对其进行**删除**。
+ - **_双击_** 主页的收支记录项次,可以对其进行**修改**。
+- 图 2:点击右上角“加号”按钮,可以**添加**一条新的支出记录。
+- 图 3 和图 4:点击右上角“条状图”按钮,可以进入统计图表页面。
+ - 目前仅支持简单的按月和按年的柱状图和分类饼图显示。
+ - 点击月份和年份下拉按钮,可以切换月份和年份。
+
+### Part3: 随机菜品 random_dish
+
+_这个其实是之前(2024-04-09)就单独开发好的 app 了,功能融合,就直接复制到这里来。_
+
+给不知道每天吃什么的选择困难症患者,指一条参考选项:随机选择一道菜。
+
+如果你关于吃什么,已经习惯了:**随便、不知道、好麻烦、你做主、看运气** 等说法,不妨试一试。
+
+当然,最后是点外卖还是自己做甚至选了依旧不吃,还是看自己的决定。
+
+#### 使用说明
+
+如下图:
+
+- 主体是一个转盘,可以选择餐次和重新生成随机菜品。
+- 点击转盘即可开始旋转,3 秒后停止,显示结果,旋转时按钮都不可点击。
+- 点击选中结果可以跳转到该菜品详情页。
+- 如果菜品详情有视频地址,可以打开对应 url;如果菜谱有上传图片(仅支持单张本地图片和使用相机拍照),可以缩放查看。
+
+![screenshot_1](./_md_pics/screenshot_1.jpg)
+
+- 当然核心还是菜品的数量,默认是文字列表显示,仅仅为了节约流量。
+- 点击上方“grid”图标(第一个)可以切换到有预览图的卡片列表,如果图片大注意流量消耗。
+- 在列表中点击某一个可以进入详情页(如上),长按可以删除指定菜品。
+- 点击上方“upload”图标(第二个)可以导入菜品 json 文件(格式见下面相关内容,其中图片时本地图片的地址则暂未考虑)。
+- 当然,也可以自行一个个手动添加菜品。
+
+![screenshot_2](./_md_pics/screenshot_2.jpg)
+
+#### 导入的菜品 json 文件格式示例
+
+```json
+[
+ {
+ "dish_name": "回锅肉",
+ "description": "此菜色味俱佳,肉鲜而香,是四川省家喻户晓的传统菜,地方风味很强。",
+ "tags": "川菜,家常菜,肉菜,麻辣鲜香",
+ "meal_categories": "午餐,晚餐,夜宵",
+ "images": [
+ "http://www.djy.gov.cn/dyjgb_rmzfwz/uploads/20191014154045sde1q1ajz3d.jpg",
+ "https://i3.meishichina.com/atta/recipe/2019/04/18/20190418155556766674398811368081.jpg?x-oss-process=style/p800"
+ ],
+ "videos": ["https://www.bilibili.com/video/BV1eA4m1L7QY/"],
+ "recipe": [
+ "原料:\n猪肉500克,蒜苗150克,化猪油40克,盐1克,郫县豆瓣50克,甜酱25克,红白酱油25克,生姜15克,葱20克,花椒10余粒。",
+ "作法:\n1. 把带皮的肥瘦相连的猪肉洗干净。",
+ "2. 锅内放开水置旺火上,下猪肉和葱、姜、花椒;将熟肉煮熟不煮𤆵;在煮肉过程中撇去汤面浮沫。蒜苗洗净切2.6厘米(约八分)长节。豆瓣剁细。",
+ "3. 将捞起的猪肉敞干水汽,在还有余热时切成约0.3厘米(约一分)厚的连皮肉片。",
+ "4. 炒锅置中火上,放入猪肉,油烧至五成热时下肉片,同事放微量盐炒均匀;炒至肉片出油时铲在锅边,相继放豆瓣、甜酱在油中炒出香味即与肉共同炒匀,然后放蒜苗合炒;蒜苗炒熟但不要炒蔫,再放酱油炒匀起锅即成。",
+ "附 注:\n1.在肉汤中加适量新鲜蔬菜同煮,可增加一样汤菜。",
+ "2.根据爱好,菜内可加豆豉炒。",
+ "3.如无红酱油可用白糖代替。"
+ ],
+ "recipe_picture": "https://demo.image.com" // 菜谱只支持单张图片
+ },
+ { …… }
+]
```
-目前设想的功能:
+后续我也会分享一下自用的菜品列表 json 文件,可以一起试用完善。
+
+### Part4: 用户配置 user_and_settings
+
+目前其实没有用户这个概念,除了调用 API 和一些网络图片,都没有需要联网的东西。
+
+这个模块目前仅有一个"备份恢复"功能。
+
+因为智能助手的对话记录、极简记账的账单条目、随机菜品的菜品列表,都是本地 sqlite 存储的,所以备份就是把 db 中的数据导出成压缩包,恢复就是把压缩包的 json 存入数据库中。
+
+哦,那个更换头像纯粹自娱自乐。
+
+## 其他说明
+
+### 关键文件缺失
+
+注意,项目中应该有一个`lib/apis/_self_keys.dart`文件,如果要自己运行项目的话注意补上。
+
+内容如下,为 BAT 平台的应用 ID 和 KEY:
+
+```dart
+// ignore_for_file: constant_identifier_names
+/// 百度的相关
+const BAIDU_API_KEY = "xxx";
+const BAIDU_SECRET_KEY = "xxx";
+/// 腾讯的相关
+const TENCENT_SECRET_ID = "xxx";
+const TENCENT_SECRET_KEY = "xxx";
+/// 阿里云的相关
+const ALIYUN_API_KEY = "xxx";
+const ALIYUN_APP_ID = "xxx";
+```
+
+### 开发环境
+
+在一个 Windows7 中使用 Visual Box 7 安装的 Ubuntu20.04 LTS 虚拟机中使用 VSCode 进行开发。
+
+2024-05-27 使用最新 flutter 版本:
+
+```sh
+$ flutter --version
+Flutter 3.22.1 • channel stable • https://github.com/flutter/flutter.git
+Framework • revision a14f74ff3a (4 天前) • 2024-05-22 11:08:21 -0500
+Engine • revision 55eae6864b
+Tools • Dart 3.4.1 • DevTools 2.34.3
+```
+
+### 仅 Android
+
+**手里没有 IOS 等其他设备,所以相关内容为 0**。
+
+虽然我日常主力机为努比亚 Z60 Ultra(Android 14),但它和我之前的 Z50 Ultra 一样,[**无法实机开发调试**](https://github.com/flutter/flutter/issues/144999),所以只有最后打包成 app 后进行使用测试。
-- 流水账(支出列表显示)的显示和记录(全手动输入的简单表单)
-- 收入/支出 item 的关键字查询
-- 报表仅仅按月、按年显示相关数据
- - (核心目的就是单纯想之前 python 编写的脚本绘制的柱状图在 app 显示罢了)
+而第二主力机是小米 6(Android 9),是 16:9 的 1080P 完整屏幕,和目前主流手记的分辨率和长 K 宽比都不一样,几乎肯定在其他的设备有一些显示上的差距,可以反馈或自行修改。
-### 页面设计
+根据模型设计的思想,可以看随手记的 [开发记录](_md_pics/development_records.md) 文档。
-就 3 个页面
+### TODO
-- 主页面:列表显示每条支出/收入的数据
- - (测试数据的工资收入默认就每月 1 号到账了)
-- 新增页面:非常简单的一个表单填写数据
-- 报表页面:几个预设的简单图表
-- 导入/导出:因为是 app 内置的 sqlite 中,所以导出备份比较重要
- - 方便导入导出,都 json 格式好了,不要什么 excel、pdf 之类的了,不好处理。
+- 因为自用,事前没有完整的项目规划,所以代码质量比较差。
+ - 许多同样逻辑的代码但写得五花八门,很多没用的组件代码块还保留,注释的代码还保留,甚至 print 都还保留。
+- 智能对话
+ - 通义千问-VL 调用 API 未符合预期
+- 一开始就只是想试一下百度的两个免费的模型 API,没有对平台和模型进行兼容性设计。
+ - 目前提供免费 token 的大模型和云平台有很多,这一块会继续完善。
+- 其实目前大模型 token 都很便宜,但是具体能用来做什么,真的不知道。
diff --git a/_md_pics/development_records.md b/_md_pics/development_records.md
new file mode 100644
index 0000000..1e4887c
--- /dev/null
+++ b/_md_pics/development_records.md
@@ -0,0 +1,424 @@
+# AI Light Life
+
+- [AI Light Life](#ai-light-life)
+ - [说明](#说明)
+ - [项目版本](#项目版本)
+ - [项目依赖](#项目依赖)
+ - [Part1: 记账部分 brief\_accounting](#part1-记账部分-brief_accounting)
+ - [核心想法](#核心想法)
+ - [页面设计](#页面设计)
+ - [Part2: AI 大模型部分 agi\_llm\_sample](#part2-ai-大模型部分-agi_llm_sample)
+ - [核心想法](#核心想法-1)
+ - [页面设计](#页面设计-1)
+ - [Part3: 随机菜品 random\_dish](#part3-随机菜品-random_dish)
+ - [使用说明](#使用说明)
+ - [设计简述](#设计简述)
+ - [导入的菜品 json 文件格式示例](#导入的菜品-json-文件格式示例)
+ - [开发过程](#开发过程)
+ - [开发记录](#开发记录)
+ - [TODO](#todo)
+
+## 说明
+
+一些我自己可能常常用的到的小功能,比如记账、菜谱、AI chat 等等
+
+一开始单纯想做个适合自己的非常简陋的记账 app,2024-05-30 添加了一个基于免费的大模型 AI 对话,应用名就不好说了:
+
+- 2024-05-30: Cost and AI Chat (AI 聊天和极简记账(智能对话和记账))
+- 2024-06-14: Light Life (简单生活?)
+- 2024-06-17: AI Light Life (智能轻生活?)
+
+## 项目版本
+
+- `0.1.x`
+ - 基本完成了极简记账的核心功能;
+- `0.2.x`
+ - 基本完成了极简 AI 对话的核心功能;
+- `0.3.x`
+ - AI 对话部分整合了百度、腾讯、阿里几个免费使用的大模型 API,可简单切换;
+ - 基本可简单使用阿里的通义万相来文本生成图片;
+ - 基本可简单使用阿里云平台的 Fuyu-8B 模型来完成图像理解功能;
+- `0.4.x`
+ - 添加幸运转盘获取随机菜品的功能;
+ - `0.4.1`
+ - 添加了可自行配置三个平台的部分付费模型(使用用户自己的 appId 和 appKey)
+ - `0.4.2`
+ - 阿里云的视觉模型测试试用(没实现,http 调用和 API 文档一样都无法使用)
+
+## 项目依赖
+
+2024-05-27 使用最新 flutter 版本:
+
+```sh
+$ flutter --version
+Flutter 3.22.1 • channel stable • https://github.com/flutter/flutter.git
+Framework • revision a14f74ff3a (4 天前) • 2024-05-22 11:08:21 -0500
+Engine • revision 55eae6864b
+Tools • Dart 3.4.1 • DevTools 2.34.3
+```
+
+依赖工具库:
+
+注意,我个人是想着尽可能多的学习使用 flutter 的各项库,所以可能这些依赖并不一定是核心功能必要的。
+
+截止 2024-06-27,使用当前可用的最新版本。
+
+```yaml
+dependencies:
+ flutter:
+ sdk: flutter
+
+ # The following adds the Cupertino Icons font to your application.
+ # Use with the CupertinoIcons class for iOS style icons.
+ cupertino_icons: ^1.0.2
+ sqflite: ^2.3.3+1 # sqlite数据库工具库
+ path_provider: ^2.1.3 # 获取主机平台文件系统上的常用位置
+ path: ^1.9.0 # 基于字符串的路径操作库
+ flutter_easyloading: ^3.0.5 # loading/toast 小部件
+ flutter_screenutil: ^5.9.3 # 适配屏幕和字体大小的插件
+ intl: ^0.19.0 # 国际化/本地化处理库
+ flutter_localizations:
+ sdk: flutter
+ # collection: ^1.19.0 #1.19和flutter_test有冲突
+ collection: ^1.18.0 # 集合相关的适用工具库
+ bottom_picker: ^2.8.0 # 简洁,但不支持仅年月
+ month_picker_dialog: ^4.0.0 # 支持仅年月,但是是弹窗,和原始组件类似
+ syncfusion_flutter_charts: ^26.1.39 # 图表库
+ flutter_form_builder: ^9.3.0 # 表单组件
+ form_builder_validators: ^10.0.1 # 表单验证
+ form_builder_file_picker: ^4.1.0 # 表单中选择文件
+ uuid: ^4.4.0 # uuid
+ flutter_markdown: ^0.7.2+1 # 使用md格式显示大模型的响应
+ dio: ^5.4.3+1 # http client # http client
+ connectivity_plus: ^6.0.3 # 用于发现可以使用的网络连接类型
+ pretty_dio_logger: ^1.3.1 # Dio 拦截器,它以漂亮、易于阅读的格式记录网络调用。
+ crypto: ^3.0.3 # Dart 的一组加密哈希函数。
+ # file_picker: ^8.0.3 # 备份恢复是选择文件路径(v8版本和上面表单中选择文件的库有冲突)
+ file_picker: ^5.5.0
+ permission_handler: ^11.3.1 # 获取设备各项权限
+ archive: ^3.6.1 # 解压缩文件
+ device_info_plus: ^10.1.0 # 获取设备信息
+ # animated_text_kit: ^4.2.2 # 动画文本特效工具,上次更新2022-06-05 但用户多
+ toggle_switch: ^2.3.0 # 第三方的切换按钮
+ cached_network_image: ^3.3.1 # 缓存网络图片
+ image_gallery_saver: ^2.0.3 # 保存图片到图库(安卓9及以下无效)
+ photo_view: ^0.15.0 # 图片预览
+ url_launcher: ^6.3.0 # 打开url
+ carousel_slider: ^4.2.1 # 轮播滑块小部件
+ multi_select_flutter: ^4.1.3 # 一个用于以多种方式创建多选小部件的包
+ image_picker: ^1.1.2 # 从设备选图片或者拍照
+ flutter_fortune_wheel: ^1.3.1 # 幸运大转盘
+ get_storage: ^2.1.1 # 简单键值对本地存储
+```
+
+
+
+## Part1: 记账部分 brief_accounting
+
+极度简单的支出流水账记录……
+
+虽然名字是记账,但实际上就是一些流水账(laundry list)
+
+### 核心想法
+
+记账,“账”虽然是重点,但也不应该忽视“记”这个动作,如果全都自动化了,岂不是"强化数字消费中的失去感"。
+
+还是觉得手动记账,一笔一笔在输入时才能体会的金钱的份量、和记账的意义。
+
+理论上:
+
+- 消费和收入的分类应该有非常专业详细的数据,在后续的报表时肯定用得上。
+- 多账本、预算、循环记账等等比较专业的东西,不会,所以没有。
+- 什么其他账单的关联(微信、支付宝什么的),就天方夜谭了。
+
+所以,流水账,非常简单,sqlite 的 DDL:
+
+```sql
+CREATE TABLE "income_list" (
+ "income_id" INTEGER NOT NULL,
+ "date" TEXT,
+ "category" TEXT,
+ "item" TEXT,
+ "value" REAL,
+ PRIMARY KEY("income_id" AUTOINCREMENT)
+);
+
+CREATE TABLE "expend_list" (
+ "expend_id" INTEGER,
+ "date" TEXT,
+ "category" TEXT,
+ "item" TEXT,
+ "value" REAL,
+ PRIMARY KEY("expend_id" AUTOINCREMENT)
+);
+```
+
+=> 再简单一点,只有一个表,用 `type` 表示收入和支出;
+再加一个 `gmt_modified` ,排序时联合 `date` 栏位共同排序避免和实际新增记录时不一致:
+
+```sql
+CREATE TABLE "bill_item_list" (
+ "bill_item_id" TEXT, -- 账单条目编号(万一多账本有bill_id呢)
+ "item_type" INTEGER, -- 0 收入;1 支出
+ "date" TEXT, -- yyyy-MM-dd 年月日即可
+ "category" TEXT, -- 支出或收入的大分类(比如支出的:通勤、医疗、饮食……)
+ "item" TEXT, -- 支出或收入的细项目(比如饮食的晚餐吃快餐) 还可以有细节就再多个detail表
+ "value" REAL, -- 支出或收入的具体数值
+ "gmt_modified" TEXT, -- 记录的创建或者修改时间
+ PRIMARY KEY("bill_item_id")
+);
+```
+
+目前设想的功能:
+
+- 流水账(支出列表显示)的显示和记录(全手动输入的简单表单)
+- 收入/支出 item 的关键字查询
+- 报表仅仅按月、按年显示相关数据
+ - (核心目的就是单纯想之前 python 编写的脚本绘制的柱状图在 app 显示罢了)
+
+### 页面设计
+
+就 3 个页面
+
+- 主页面:列表显示每条支出/收入的数据
+ - (测试数据的工资收入默认就每月 1 号到账了)
+- 新增页面:非常简单的一个表单填写数据
+- 报表页面:几个预设的简单图表
+- 导入/导出:因为是 app 内置的 sqlite 中,所以导出备份比较重要
+ - 方便导入导出,都 json 格式好了,不要什么 excel、pdf 之类的了,不好处理。
+
+## Part2: AI 大模型部分 agi_llm_sample
+
+### 核心想法
+
+其实就是一些免费使用的国内大模型 API 的简单调用,查看效果而已。
+
+用户和 AI 进行对话后,为了能查询历史记录,就把对话内容存入 sqlite,基本表结构:
+
+```sql
+-- 2024-06-01 新增AI对话留存
+-- 图像理解也有对话,所以新加一个对话类型栏位:aigc、image2text、text2image……
+-- i2t_image_path 指图像理解时被参考的图片地址(应该是应用缓存的图片地址)
+CREATE TABLE "chat_history" (
+ uuid TEXT NOT NULL,
+ title TEXT NOT NULL,
+ gmt_create TEXT NOT NULL,
+ messages TEXT NOT NULL,
+ llm_name TEXT NOT NULL,
+ yun_platform_name TEXT,
+ i2t_image_path TEXT,
+ chat_type TEXT NOT NULL,
+ PRIMARY KEY("uuid")
+);
+
+-- 2024-06-13 新增文生图简单内容流程
+CREATE TABLE "text2image_history" (
+ request_id TEXT NOT NULL,
+ prompt TEXT NOT NULL,
+ negative_prompt TEXT,
+ style TEXT NOT NULL,
+ image_urls TEXT,
+ gmt_create TEXT NOT NULL,
+ PRIMARY KEY("request_id")
+);
+```
+
+### 页面设计
+
+就两层:外层提供文生文、文生图、图生文的选项,内层显示对应的对话列表即可。
+
+## Part3: 随机菜品 random_dish
+
+_这个其实是之前(2024-04-09)就单独开发好的 app 了,功能融合,就直接复制到这里来。_
+
+给不知道每天吃什么的选择困难症患者,指一条参考选项:随机选择一道菜。
+
+如果你关于吃什么,已经习惯了:**随便、不知道、好麻烦、你做主、看运气** 等说法,不妨试一试。
+
+当然,最后是点外卖还是自己做甚至选了依旧不吃,还是看自己的决定。
+
+### 使用说明
+
+如下图:
+
+- 主体是一个转盘,可以选择餐次和重新生成随机菜品。
+- 点击转盘即可开始旋转,3 秒后停止,显示结果,旋转时按钮都不可点击。
+- 点击选中结果可以跳转到该菜品详情页。
+- 如果菜品详情有视频地址,可以打开对应 url;如果菜谱有上传图片(仅支持单张本地图片和使用相机拍照),可以缩放查看。
+
+![screenshot_1](./screenshot_1.jpg)
+
+- 当然核心还是菜品的数量,默认是文字列表显示,仅仅为了节约流量。
+- 点击上方“grid”图标(第一个)可以切换到有预览图的卡片列表,如果图片大注意流量消耗。
+- 在列表中点击某一个可以进入详情页(如上),长按可以删除指定菜品。
+- 点击上方“upload”图标(第二个)可以导入菜品 json 文件(格式见下面相关内容,其中图片时本地图片的地址则暂未考虑)。
+- 当然,也可以自行一个个手动添加菜品。
+
+![screenshot_2](./screenshot_2.jpg)
+
+### 设计简述
+
+1. 主体只有简单的一张菜品表:
+ ```sql
+ CREATE TABLE "dish" (
+ dish_id TEXT NOT NULL PRIMARY KEY,
+ dish_name TEXT NOT NULL,
+ description TEXT,
+ photos TEXT,
+ videos TEXT,
+ tags TEXT,
+ meal_categories TEXT,
+ recipe TEXT,
+ recipe_picture TEXT,
+ UNIQUE(dish_name,tags)
+ );
+ ```
+2. 把一天分成几个时间段,打开 app 时,显示该时间段的随机 10 个菜品
+ 1. 如果不满意,可以切换时间段和重新随机 10 个菜品
+3. 点击转盘开始旋转,3 秒后选中某个菜品。
+ 1. 点击该菜品,进入菜品详情,查看图片和菜谱等信息。
+4. 可以自行维护菜品列表,导入规范的 json 文件,或者自行添加菜品。
+ 1. json 文件格式参看下面,注意**导入的 tags 和 meal_categories 不在预设中,有修改后则不会显示**
+ 2. 具体 tags,比如凉菜、汤菜、煎、炒、烹、炸、焖、溜、熬、炖、汆等
+ 3. 具体 meal_categories,比如 早餐、晚餐、午餐、夜宵、甜点、主食等
+5. TODO(不一定会做):
+ 1. 为了随机的乐观性,可以多一张 random_record 随机记录表。
+ 2. 某个时间段随机过了,就不允许再随机了。
+ 3. 可以查看随机过的菜品记录。
+ 4. ……
+ ```txt random_record
+ random_record_id
+ date
+ meal_category
+ dish_id
+ ……
+ ```
+
+### 导入的菜品 json 文件格式示例
+
+```json
+[
+ {
+ "dish_name": "回锅肉",
+ "description": "此菜色味俱佳,肉鲜而香,是四川省家喻户晓的传统菜,地方风味很强。",
+ "tags": "川菜,家常菜,肉菜,麻辣鲜香",
+ "meal_categories": "午餐,晚餐,夜宵",
+ "images": [
+ "http://www.djy.gov.cn/dyjgb_rmzfwz/uploads/20191014154045sde1q1ajz3d.jpg",
+ "https://i3.meishichina.com/atta/recipe/2019/04/18/20190418155556766674398811368081.jpg?x-oss-process=style/p800"
+ ],
+ "videos": ["https://www.bilibili.com/video/BV1eA4m1L7QY/"],
+ "recipe": [
+ "原料:\n猪肉500克,蒜苗150克,化猪油40克,盐1克,郫县豆瓣50克,甜酱25克,红白酱油25克,生姜15克,葱20克,花椒10余粒。",
+ "作法:\n1. 把带皮的肥瘦相连的猪肉洗干净。",
+ "2. 锅内放开水置旺火上,下猪肉和葱、姜、花椒;将熟肉煮熟不煮𤆵;在煮肉过程中撇去汤面浮沫。蒜苗洗净切2.6厘米(约八分)长节。豆瓣剁细。",
+ "3. 将捞起的猪肉敞干水汽,在还有余热时切成约0.3厘米(约一分)厚的连皮肉片。",
+ "4. 炒锅置中火上,放入猪肉,油烧至五成热时下肉片,同事放微量盐炒均匀;炒至肉片出油时铲在锅边,相继放豆瓣、甜酱在油中炒出香味即与肉共同炒匀,然后放蒜苗合炒;蒜苗炒熟但不要炒蔫,再放酱油炒匀起锅即成。",
+ "附 注:\n1.在肉汤中加适量新鲜蔬菜同煮,可增加一样汤菜。",
+ "2.根据爱好,菜内可加豆豉炒。",
+ "3.如无红酱油可用白糖代替。"
+ ],
+ "recipe_picture": "https://demo.image.com" // 菜谱只支持单张图片
+ },
+ { …… }
+]
+```
+
+## 开发过程
+
+### 开发记录
+
+直接在 widget 上用的函数就不加下划线了,如果在其他函数中用的,就加下划线前缀。
+
+- 2024-05-24
+ - 基本完成账单列表首页的基础功能和组件占位.
+- 2024-05-26
+ - 基本完成月度年度统计基础柱状图展示和相关组件的占位
+- 2024-04-27
+ - feat: 完成账单条目的新增和修改功能; chore: 升级 flutter 为 3.22.1, 相关依赖库为当前最新.
+- 2024-05-28
+ - 账单条目关键字查询时切换到新的列表展示; feat: 完成带月统计值的列表展示在有多个月数据时滚动到某个月就加载某个月的总计.
+ - perf: 优化了图表页面的相关方法,重复度高的代码抽成了公共组件或方法。
+ - perf: 优化了账单项目列表主页面、编辑项次表单页面的相关方法和细节。
+- 2024-05-29
+ - 修正了一些细节;将新增账单项次放到账单列表页面中,调整账单项次表单页面;添加分类选择底部弹窗组件(暂未用到)。
+ - 重新调整了账单管理模块的结构,新增 AGI LLM 模块用于对话的组件框架。
+- 2024-05-30
+ - feat: 调价了基于 dio 的通用 http client 工具类;构建了 Ernie 和 huanyuan 模型的 model 以及基础查询函数。
+- 2024-05-31
+ - feat: 基本可以正常使用大模型进行对话了。
+ - fix: 优化了 AI 对话框的显示细节,完善了一些其他功能细节。
+- 2024-06-03
+ - feat: 添加了保存 AI 对话记录到本地数据库,可重新读取并继续提问。
+- 2024-06-04
+ - feat:添加了内部 sqlite 数据的备份恢复功能。
+ - feat: 账单统计部分添加了分类统计甜甜圈图。
+ - fix: 修正高版本的 Android 请求内部存储的授权问题。
+- 2024-06-05
+ - fix: 再度修正备份时内部存储管理权限的授权问题,Android14 已正常。
+ - feat: 最后一个大模型回复不满意可以点击重新生成;在 appbar 添加“新建对话”按钮;调整标题固定显示在对话正文顶部,点击修改图标可修改。
+- 2024-06-06
+ - refactor: 添加了阿里云、腾讯、百度通用的基础 aigc 的 http 请求和响应对应的 model; feat: 阿里云通义千问开源版本一个模型接口测试可用。
+- 2024-06-07
+ - refactor: 重构了通用 aigc 请求 api 和 model 的使用,整理了对应文件命名和文件夹结构等。
+ - fix: 简单调整了 llm 名称变量命令、对话主页面的入口按钮样式等。
+- 2024-06-11
+ - feat: 添加了 AI 对话的流式请求响应的处理; fix:api 还是拆回 3 个平台各自一个文件。
+ - fix: 将云平台切换、大模型切换、文生文的流式请求、最近对话统一放在了 AI 对话页面中。
+- 2024-06-12
+ - fix: AI 对话页面删除历史对话为当前对话时,返回初始对话状态;点击历史对话会替换当前对话而不是新开页面;其他显示细节。
+ - feat: 基本完成了阿里的通义万相-文本生成图像的基础使用页面。
+- 2024-06-13
+ - fix: 调整了通义万相-文本生图页面的细节,修正了循环检查任务状态的逻辑。
+ - feat: 新加通义万相-文本生图的简单历史记录留存,简单数据保持到本地数据库。
+- 2024-06-14
+ - feat: 基本完成了百度云中 Fuyu-8B 图像理解的基础使用页面。
+ - 【2024-06-21 更换了选择图片的库】**bug**:每次选择了图片用于上传做理解后,都会在本地生成一个副本图片,传一次就多一张,原因还没看。
+ - feat: 添加了 Fuyu-8B 图像理解对话记录存到本地数据库。
+ - fix: 简单调整了文本对话、文生图、图生文各自主页面的一些细节,删除部分无意义代码。
+- 2024-06-15
+ - feat: 添加了 AI 对话限时限量测试版本的基础页面。
+ - 【done】todo: 切换模型应该新建对话,因为上下文丢失了。
+ - feat: 添加了阿里云中限时限量的对话模型可选列表。
+- 2024-06-17
+ - feat: 合并了之前的 flutter_random_dish 项目的功能。
+ - todo: 数据库文件还是分开的,要何在一起吗?
+ - doc: 更新 readme 文件。
+ - rename: 重命名了整个项目名称。
+- 2024-06-20
+ - feat: 添加了支持阿里、百度、腾讯云平台部分付费模型自行配置 appId 和 appKey 的功能。
+ - refactor: 将免费对话、限量对话和自定配置对话的聊天页面统一成一个页面,根据参数进行复用。
+ - fix:简化三个平台对话接口的响应函数,并添加错误处理。
+- 2024-06-21
+ - feat: 尝试添加通义千问-VL 的示例
+ - !!!参数和 API 文档一样,但是报错:
+ ```
+ response: {
+ "code":"InvalidParameter",
+ "message":"The item of `content` should be a message of a certain modal",
+ "request_id":"03a79c8b-66dd-9859-a11a-2667f1dffcbb"
+ }
+ ```
+ - 注意:不允许本地图片地址,但测试报错,所以也没有实现上传到云端后再使用网络图片地址调用接口的逻辑。
+ - fix: 添加 baidu 的 Fuyu8B 请求错误处理,修改添加图片的函数。
+- 2024-06-22
+ - feat: 用户可以添加 3 个平台自己的通用应用 ID 和 KEY 来使用部分模型功能。
+ - todo: 对大模型的功能入口划分不够细致
+- 2024-06-24
+ - fix: 平台大模型的 api 请求加上错误处理;用户自行配置的平台应用 ID 和 KEY 与单个对话模型的配置合并,减少冗余;修正部分显示错误和细节。
+ - fix: 修正部分细节。
+- 2024-06-26
+ - fix: 随机菜品部分,加上所有预设分类和导入时存入 db 中的分类。
+- 2024-06-27
+ - fix: 修正菜品编辑时图片显示错误;修正 Android14 上获取存储权限异常问题;refactor:合并 dish 的数据库内容到唯一内嵌的 db 中。
+ - feat: 添加“用户设置”示例页面,放置全量备份恢复。fix:随机菜品下拉分类部分不仅是预设也包含数据库中导入时使用的其他分类。
+ - fix: 修复 Z60U 中文本生图中点击历史记录无法显示弹窗的问题;Z60U 在账单年度统计切换年时无法显示弹窗的问题;doc: 更新 readme 文件。
+
+### TODO
+
+- 智能对话
+ - 通义千问-VL 调用 API 未符合预期
+ - 文本对话使用 `flutter_markdown:0.7.2` 库长按会报错
+ - `0.7.2+1` 版本好像修复了
+
+flutter build apk --split-per-abi
diff --git a/_md_pics/screenshot_1.jpg b/_md_pics/screenshot_1.jpg
new file mode 100755
index 0000000..ee045c6
Binary files /dev/null and b/_md_pics/screenshot_1.jpg differ
diff --git a/_md_pics/screenshot_2.jpg b/_md_pics/screenshot_2.jpg
new file mode 100755
index 0000000..adb522e
Binary files /dev/null and b/_md_pics/screenshot_2.jpg differ
diff --git "a/_md_pics/\346\231\272\350\203\275\345\212\251\346\211\213.jpg" "b/_md_pics/\346\231\272\350\203\275\345\212\251\346\211\213.jpg"
new file mode 100755
index 0000000..f5b94b7
Binary files /dev/null and "b/_md_pics/\346\231\272\350\203\275\345\212\251\346\211\213.jpg" differ
diff --git "a/_md_pics/\346\236\201\347\256\200\350\256\260\350\264\246.jpg" "b/_md_pics/\346\236\201\347\256\200\350\256\260\350\264\246.jpg"
new file mode 100755
index 0000000..ebceb63
Binary files /dev/null and "b/_md_pics/\346\236\201\347\256\200\350\256\260\350\264\246.jpg" differ
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 50f33b3..7783899 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -23,8 +23,9 @@ if (flutterVersionName == null) {
}
android {
- namespace "com.swm.free_brief_accounting"
- compileSdkVersion flutter.compileSdkVersion
+ namespace "com.swm.ai_light_life"
+ // compileSdkVersion flutter.compileSdkVersion
+ compileSdkVersion 34
ndkVersion flutter.ndkVersion
compileOptions {
@@ -42,7 +43,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
- applicationId "com.swm.free_brief_accounting"
+ applicationId "com.swm.ai_light_life"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 27daeb6..6d6eec4 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,6 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/com/example/free_brief_accounting/MainActivity.kt b/android/app/src/main/kotlin/com/example/free_brief_accounting/MainActivity.kt
index aa7db61..e66097d 100644
--- a/android/app/src/main/kotlin/com/example/free_brief_accounting/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/example/free_brief_accounting/MainActivity.kt
@@ -1,4 +1,4 @@
-package com.swm.free_brief_accounting
+package com.swm.ai_light_life
import io.flutter.embedding.android.FlutterActivity
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
old mode 100644
new mode 100755
index db77bb4..b91ea6c
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
old mode 100644
new mode 100755
index 17987b7..b91ea6c
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
old mode 100644
new mode 100755
index 09d4391..b91ea6c
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
old mode 100644
new mode 100755
index d5f1c8d..b91ea6c
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
old mode 100644
new mode 100755
index 4d6372e..b91ea6c
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/gradle.properties b/android/gradle.properties
index 598d13f..bb37af6 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,3 +1,5 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
+android.enableR8=true
+org.gradle.java.home=/home/david/.jdks/temurin-17.0.6
\ No newline at end of file
diff --git a/assets/images/no_image.jpg b/assets/images/no_image.jpg
new file mode 100644
index 0000000..8987fac
Binary files /dev/null and b/assets/images/no_image.jpg differ
diff --git "a/assets/text2image_styles/3D\345\215\241\351\200\232.jpg" "b/assets/text2image_styles/3D\345\215\241\351\200\232.jpg"
new file mode 100755
index 0000000..2d4d1de
Binary files /dev/null and "b/assets/text2image_styles/3D\345\215\241\351\200\232.jpg" differ
diff --git "a/assets/text2image_styles/\344\270\255\345\233\275\347\224\273.jpg" "b/assets/text2image_styles/\344\270\255\345\233\275\347\224\273.jpg"
new file mode 100755
index 0000000..b1c31d5
Binary files /dev/null and "b/assets/text2image_styles/\344\270\255\345\233\275\347\224\273.jpg" differ
diff --git "a/assets/text2image_styles/\345\212\250\347\224\273.jpg" "b/assets/text2image_styles/\345\212\250\347\224\273.jpg"
new file mode 100755
index 0000000..f286a28
Binary files /dev/null and "b/assets/text2image_styles/\345\212\250\347\224\273.jpg" differ
diff --git "a/assets/text2image_styles/\346\211\201\345\271\263\346\217\222\347\224\273.jpg" "b/assets/text2image_styles/\346\211\201\345\271\263\346\217\222\347\224\273.jpg"
new file mode 100755
index 0000000..87e857e
Binary files /dev/null and "b/assets/text2image_styles/\346\211\201\345\271\263\346\217\222\347\224\273.jpg" differ
diff --git "a/assets/text2image_styles/\346\260\264\345\275\251.jpg" "b/assets/text2image_styles/\346\260\264\345\275\251.jpg"
new file mode 100755
index 0000000..4ca705e
Binary files /dev/null and "b/assets/text2image_styles/\346\260\264\345\275\251.jpg" differ
diff --git "a/assets/text2image_styles/\346\262\271\347\224\273.jpg" "b/assets/text2image_styles/\346\262\271\347\224\273.jpg"
new file mode 100755
index 0000000..8d1a792
Binary files /dev/null and "b/assets/text2image_styles/\346\262\271\347\224\273.jpg" differ
diff --git "a/assets/text2image_styles/\347\264\240\346\217\217.jpg" "b/assets/text2image_styles/\347\264\240\346\217\217.jpg"
new file mode 100755
index 0000000..db8b2e7
Binary files /dev/null and "b/assets/text2image_styles/\347\264\240\346\217\217.jpg" differ
diff --git "a/assets/text2image_styles/\351\273\230\350\256\244.jpg" "b/assets/text2image_styles/\351\273\230\350\256\244.jpg"
new file mode 100755
index 0000000..7e5f628
Binary files /dev/null and "b/assets/text2image_styles/\351\273\230\350\256\244.jpg" differ
diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 0000000..7e7e7f6
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1 @@
+extensions:
diff --git a/lib/apis/aliyun_apis.dart b/lib/apis/aliyun_apis.dart
new file mode 100644
index 0000000..21512b8
--- /dev/null
+++ b/lib/apis/aliyun_apis.dart
@@ -0,0 +1,256 @@
+// ignore_for_file: avoid_print
+
+import 'dart:convert';
+
+import '../dio_client/cus_http_client.dart';
+import '../dio_client/cus_http_request.dart';
+import '../dio_client/interceptor_error.dart';
+import '../models/ai_interface_state/aliyun_qwenvl_state.dart';
+import '../models/ai_interface_state/aliyun_text2image_state.dart';
+import '../models/common_llm_info.dart';
+import '../services/cus_get_storage.dart';
+import '_self_keys.dart';
+
+///
+/// 文生图任务提交
+///
+var aliyunText2imageUrl =
+ "https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis";
+
+Future commitAliyunText2ImgJob(
+ Input input,
+ Parameters parameters,
+) async {
+ var body = AliyunTextToImgReq(
+ model: "wanx-v1",
+ input: input,
+ parameters: parameters,
+ );
+
+ try {
+ var start = DateTime.now().millisecondsSinceEpoch;
+
+ var respData = await HttpUtils.post(
+ path: aliyunText2imageUrl,
+ method: HttpMethod.post,
+ headers: {
+ "X-DashScope-Async": "enable", // 固定的,异步方式提交作业。
+ "Content-Type": "application/json",
+ // 检查用户通用配置;再才是自己的账号key
+ "Authorization":
+ "Bearer ${MyGetStorage().getAliyunCommonAppKey() ?? ALIYUN_API_KEY}",
+ },
+ // 可能是因为头的content type设定,这里直接传类实例即可,传toJson也可
+ data: body,
+ );
+
+ print("阿里云文生图---------------------$respData");
+
+ var end = DateTime.now().millisecondsSinceEpoch;
+
+ print("2222222222xxxxxxxxxxxxxxxxx${(end - start) / 1000} 秒");
+
+ ///??? 2024-06-11 阿里云请求报错,会进入dio的错误拦截器,这里ret就是个null了
+ if (respData.runtimeType == String) {
+ respData = json.decode(respData);
+ }
+
+ // 响应是json格式
+ return AliyunTextToImgResp.fromJson(respData ?? {});
+ } on HttpException catch (e) {
+ return AliyunTextToImgResp(
+ // 这里的code和msg就不是api返回的,是自行定义的,应该抽出来
+ code: e.code.toString(),
+ message: e.msg,
+ );
+ } catch (e) {
+ print("bbbbbbbbbbbbbbbb ${e.runtimeType}---$e");
+ // API请求报错,显示报错信息
+ return AliyunTextToImgResp(
+ // 这里的code和msg就不是api返回的,是自行定义的,应该抽出来
+ code: "10001",
+ message: e.toString(),
+ );
+ }
+}
+
+///
+/// 作业任务状态查询和结果获取接口
+/// GET https://dashscope.aliyuncs.com/api/v1/tasks/{task_id}
+///
+Future getAliyunText2ImgJobResult(String taskId) async {
+ try {
+ var start = DateTime.now().millisecondsSinceEpoch;
+
+ // var taskId = "8bb11ab3-a7b2-4e37-b90f-f7d31d356279";
+
+ var respData = await HttpUtils.post(
+ path: "https://dashscope.aliyuncs.com/api/v1/tasks/$taskId",
+ method: HttpMethod.get,
+ headers: {
+ // 检查用户通用配置;再才是自己的账号key
+ "Authorization":
+ "Bearer ${MyGetStorage().getAliyunCommonAppKey() ?? ALIYUN_API_KEY}",
+ },
+ );
+
+ print("阿里云文生图结果查询---------------------$respData");
+
+ var end = DateTime.now().millisecondsSinceEpoch;
+
+ print("333333xxxxxxxxxxxxxxxxx${(end - start) / 1000} 秒");
+
+ ///??? 2024-06-11 阿里云请求报错,会进入dio的错误拦截器,这里ret就是个null了
+ if (respData.runtimeType == String) {
+ respData = json.decode(respData);
+ }
+
+ // 响应是json格式
+ return AliyunTextToImgResp.fromJson(respData ?? {});
+ } on HttpException catch (e) {
+ return AliyunTextToImgResp(
+ // 这里的code和msg就不是api返回的,是自行定义的,应该抽出来
+ code: e.code.toString(),
+ message: e.msg,
+ );
+ } catch (e) {
+ print("aaaaaas ${e.runtimeType}---$e");
+ // API请求报错,显示报错信息
+ return AliyunTextToImgResp(
+ // 这里的code和msg就不是api返回的,是自行定义的,应该抽出来
+ code: "10001",
+ message: e.toString(),
+ );
+ }
+}
+
+///
+/// 阿里的视觉大模型通义千问-VL 的请求
+/// https://help.aliyun.com/document_detail/2712587.html
+///
+
+/// 阿里平台通用多模态视觉大模型的请求地址
+var aliyunMultimodalUrl =
+ "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation";
+
+Future> getAliyunQwenVLResp(
+ List messages, {
+ String? model,
+ bool stream = false,
+ bool isUserConfig = true,
+}) async {
+ // 如果有传模型名称,就用传递的;没有就默认的
+ model = model ?? newLLMSpecs[PlatformLLM.limitedQwenVLPlus]!.model;
+
+// ?????
+// 2024-06-21
+// 明明和API文档一样了,为什么不行
+// The item of `content` should be a message of a certain modal
+ var body = AliyunQwenVlReq(
+ model: model,
+ input: QwenVLInput(messages: messages),
+ parameters:
+ stream ? QwenVLParameters(incrementalOutput: true) : QwenVLParameters(),
+ );
+
+ var start = DateTime.now().millisecondsSinceEpoch;
+
+ var header = {
+ "Content-Type": "application/json",
+ // 如果是用户自行配置,使用检查用户通用配置;否则是自己的账号key
+ "Authorization":
+ "Bearer ${isUserConfig ? MyGetStorage().getAliyunCommonAppKey() : ALIYUN_API_KEY}",
+ };
+ // 如果是流式,开启SSE
+ if (stream) {
+ header.addAll({"X-DashScope-SSE": "enable"});
+ }
+
+ print("""--------------------------------------
+getAliyunQwenVLResp 的请求体,AliyunQwenVlResp:
+${json.encode(body.toSimpleJson(stream))}
+--------------------------------------
+""");
+
+ try {
+ var respData = await HttpUtils.post(
+ path: aliyunMultimodalUrl,
+ method: HttpMethod.post,
+ headers: header,
+ // 可能是因为头的content type设定,这里直接传类实例即可,传toJson也可
+ data: json.encode(body.toSimpleJson(stream)),
+ );
+
+ var end = DateTime.now().millisecondsSinceEpoch;
+ print("阿里云qwen-vl响应耗时: ${(end - start) / 1000} 秒");
+
+ if (stream) {
+ // 使用正则表达式匹配所有以"data:{"开头的字符串
+ final regex = RegExp(r'.*data:\{".*}', multiLine: true);
+ final matches = regex.allMatches(respData);
+
+ // 提取匹配到的字符串并添加到数组中
+ List dataArray = [];
+ for (final match in matches) {
+ // 替换"data:"为空字符串(看结果data后面的冒号没有空格)
+ final replacedString = match.group(0)!.replaceAll(RegExp(r'data:'), '');
+ dataArray.add(replacedString);
+ }
+
+ List list = dataArray
+ .map((e) => AliyunQwenVlResp.fromJson(json.decode(e)))
+ .toList();
+
+ print(list);
+
+ return list;
+ } else {
+ ///??? 2024-06-11 阿里云请求报错,会进入dio的错误拦截器,这里ret就是个null了
+ if (respData.runtimeType == String) {
+ respData = json.decode(respData);
+ }
+
+ // 响应是json格式
+ return [AliyunQwenVlResp.fromJson(respData ?? {})];
+ }
+ } on HttpException catch (e) {
+ return [
+ AliyunQwenVlResp(
+ customReplyText: e.toString(),
+ // 这里的code和msg就不是api返回的,是自行定义的,应该抽出来
+ errorCode: e.code.toString(),
+ errorMsg: e.msg,
+ )
+ ];
+ } catch (e) {
+ print("vvvvvvvvvvvvvvvvvvl ${e.runtimeType}---$e");
+ // API请求报错,显示报错信息
+ return [
+ AliyunQwenVlResp(
+ customReplyText: e.toString(),
+ // 这里的code和msg就不是api返回的,是自行定义的,应该抽出来
+ errorCode: "10000",
+ errorMsg: e.toString(),
+ )
+ ];
+ }
+}
+
+var a = {
+ "model": "qwen-vl-plus",
+ "input": {
+ "messages": [
+ {
+ "role": "user",
+ "content": [
+ {
+ "text": "刚回家",
+ "image":
+ "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg"
+ }
+ ]
+ }
+ ]
+ },
+ "parameters": {}
+};
diff --git a/lib/apis/baidu_apis.dart b/lib/apis/baidu_apis.dart
new file mode 100644
index 0000000..67083af
--- /dev/null
+++ b/lib/apis/baidu_apis.dart
@@ -0,0 +1,99 @@
+// ignore_for_file: avoid_print
+
+import '../../dio_client/cus_http_client.dart';
+import '../../dio_client/cus_http_request.dart';
+import '../dio_client/interceptor_error.dart';
+import '../models/ai_interface_state/baidu_fuyu8b_state.dart';
+import '../services/cus_get_storage.dart';
+import '_self_keys.dart';
+
+/// 百度平台大模型API的前缀地址
+const baiduAigcUrl =
+ "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/";
+// 百度的token请求地址
+const baiduAigcAuthUrl = "https://aip.baidubce.com/oauth/2.0/token";
+
+// 百度平台下第三方的fuyu图像理解模型API接口
+const baiduFuyu8BUrl =
+ "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/image2text/fuyu_8b";
+
+///
+/// 注意,这里没有处理报错,请求过程中的错误在cus_client中集中处理的;
+/// 但请求结果处理的报错,就应该补上???
+///
+
+///
+///-----------------------------------------------------------------------------
+/// 百度的请求方法
+///
+/// 使用 AK,SK 生成鉴权签名(Access Token)
+Future getAccessToken() async {
+ // 这个获取的token的结果是一个_Map,不用转json直接取就得到Access Token了
+
+ try {
+ var respData = await HttpUtils.post(
+ path: baiduAigcAuthUrl,
+ method: HttpMethod.post,
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ data: {
+ "grant_type": "client_credentials",
+ // 如果是用户自行配置,使用自行配置的key;否则检查用户通用配置;最后才是自己的账号key
+ "client_id": MyGetStorage().getBaiduCommonAppId() ?? BAIDU_API_KEY,
+ "client_secret":
+ MyGetStorage().getBaiduCommonAppKey() ?? BAIDU_SECRET_KEY,
+ },
+ );
+
+ // 响应是json格式
+ return respData['access_token'];
+ } on HttpException catch (e) {
+ return e.msg;
+ } catch (e) {
+ print("bbbbbbbbbbbbbbbb ${e.runtimeType}---$e");
+ // API请求报错,显示报错信息
+ return e.toString();
+ }
+}
+
+/// 获取Fuyu-8B图像理解的响应结果
+Future getBaiduFuyu8BResp(String prompt, String image) async {
+ // 每次请求都要实时获取最小的token
+ String token = await getAccessToken();
+
+ var body = BaiduFuyu8BReq(prompt: prompt, image: image);
+
+ try {
+ var start = DateTime.now().millisecondsSinceEpoch;
+
+ var respData = await HttpUtils.post(
+ path: "$baiduFuyu8BUrl?access_token=$token",
+ method: HttpMethod.post,
+ headers: {"Content-Type": "application/json"},
+ data: body,
+ );
+
+ var end = DateTime.now().millisecondsSinceEpoch;
+
+ print("百度图生文-------耗时${(end - start) / 1000} 秒");
+ print("百度图生文-------$respData");
+
+ // 响应是json格式
+ return BaiduFuyu8BResp.fromJson(respData);
+ } on HttpException catch (e) {
+ return BaiduFuyu8BResp(
+ // 这里的code和msg就不是api返回的,是自行定义的,应该抽出来
+ errorCode: e.code.toString(),
+ errorMsg: e.msg,
+ );
+ } catch (e) {
+ print("vvvvvvvvvvvvvvvvvvl ${e.runtimeType}---$e");
+ // API请求报错,显示报错信息
+ return BaiduFuyu8BResp(
+ // 这里的code和msg就不是api返回的,是自行定义的,应该抽出来
+ errorCode: "10000",
+ errorMsg: e.toString(),
+ );
+ }
+}
diff --git a/lib/apis/common_chat_apis.dart b/lib/apis/common_chat_apis.dart
new file mode 100644
index 0000000..a66357e
--- /dev/null
+++ b/lib/apis/common_chat_apis.dart
@@ -0,0 +1,302 @@
+// ignore_for_file: avoid_print
+
+import 'dart:convert';
+
+import '../dio_client/cus_http_client.dart';
+import '../dio_client/cus_http_request.dart';
+import '../dio_client/interceptor_error.dart';
+import '../models/ai_interface_state/platform_aigc_commom_state.dart';
+import '../models/common_llm_info.dart';
+
+import '../services/cus_get_storage.dart';
+import '_self_keys.dart';
+import 'gen_access_token/tencet_hunyuan_signature_v3.dart';
+
+/// 腾讯平台大模型API的前缀地址
+const tencentAigcUrl = "https://hunyuan.tencentcloudapi.com/";
+
+///
+///-----------------------------------------------------------------------------
+/// 腾讯的请求方法
+///
+/// 获取流式和非流式的对话响应数据
+Future> getTencentAigcResp(
+ List messages, {
+ String? model,
+ bool stream = false,
+ bool isUserConfig = true,
+}) async {
+ print("-isUserConfig-----------------$isUserConfig");
+ // 如果有传模型名称,就用传递的;没有就默认的
+ model = model ?? newLLMSpecs[PlatformLLM.tencentHunyuanLiteFREE]!.model;
+
+ var body = CommonReqBody(model: model, messages: messages, stream: stream);
+
+ try {
+ var start = DateTime.now().millisecondsSinceEpoch;
+ var respData = await HttpUtils.post(
+ path: tencentAigcUrl,
+ method: HttpMethod.post,
+ headers: genHunyuanLiteSignatureHeaders(
+ commonReqBodyToJson(body, caseType: "pascal"),
+ // 如果是用户自行配置,使用用户通用配置;否则是自己的账号key
+ isUserConfig
+ ? MyGetStorage().getTencentCommonAppId() ?? ""
+ : TENCENT_SECRET_ID,
+ isUserConfig
+ ? MyGetStorage().getTencentCommonAppKey() ?? ""
+ : TENCENT_SECRET_KEY,
+ ),
+ // 可能是因为头的content type设定,这里直接传类实例即可,传toJson也可
+ data: body.toJson(caseType: "pascal"),
+ );
+
+ var end = DateTime.now().millisecondsSinceEpoch;
+ print("腾讯aigc响应耗时: ${(end - start) / 1000} 秒");
+
+ /// ??? 流式返回都是String,没有区分正常和报错返回
+ if (stream) {
+ List list = (respData as String)
+ .split("data:")
+ .where((e) => e.isNotEmpty)
+ .map((e) => CommonRespBody.fromJson(json.decode(e)))
+ .toList();
+ return list;
+ } else {
+ /// 2024-06-06 注意,这里报错的时候,响应的是String,而正常获取回复响应是_Map
+ if (respData.runtimeType == String) {
+ respData = json.decode(respData);
+ }
+
+ // 响应是json格式
+ return [CommonRespBody.fromJson(respData["Response"])];
+ }
+ } on HttpException catch (e) {
+ return [
+ CommonRespBody(
+ customReplyText: e.toString(),
+ // 这里的code和msg就不是api返回的,是自行定义的,应该抽出来
+ errorCode: e.code.toString(),
+ errorMsg: e.msg,
+ )
+ ];
+ } catch (e) {
+ print("ttttttttttttttttttttt ${e.runtimeType}---$e");
+ // API请求报错,显示报错信息
+ return [
+ CommonRespBody(
+ customReplyText: e.toString(),
+ // 这里的code和msg就不是api返回的,是自行定义的,应该抽出来
+ errorCode: "10000",
+ errorMsg: e.toString(),
+ )
+ ];
+ }
+}
+
+/// 阿里平台通用aigc的请求地址
+var aliyunAigcUrl =
+ "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation";
+
+Future> getAliyunAigcResp(
+ List messages, {
+ String? model,
+ bool stream = false,
+ bool isUserConfig = true,
+}) async {
+ // 如果有传模型名称,就用传递的;没有就默认的
+ model = model ?? newLLMSpecs[PlatformLLM.aliyunQwen1p8BChatFREE]!.model;
+
+ var body = CommonReqBody(
+ model: model,
+ input: AliyunInput(messages: messages),
+ parameters: stream
+ ? AliyunParameters(resultFormat: "message", incrementalOutput: true)
+ : AliyunParameters(resultFormat: "message"),
+ );
+
+ var start = DateTime.now().millisecondsSinceEpoch;
+
+ var header = {
+ "Content-Type": "application/json",
+ // 如果是用户自行配置,使用检查用户通用配置;否则是自己的账号key
+ "Authorization":
+ "Bearer ${isUserConfig ? MyGetStorage().getAliyunCommonAppKey() : ALIYUN_API_KEY}",
+ };
+ // 如果是流式,开启SSE
+ if (stream) {
+ header.addAll({"X-DashScope-SSE": "enable"});
+ }
+
+ try {
+ var respData = await HttpUtils.post(
+ path: aliyunAigcUrl,
+ method: HttpMethod.post,
+ headers: header,
+ // 可能是因为头的content type设定,这里直接传类实例即可,传toJson也可
+ data: body,
+ );
+
+ var end = DateTime.now().millisecondsSinceEpoch;
+ print("阿里云aigc响应耗时: ${(end - start) / 1000} 秒");
+
+ if (stream) {
+ // 使用正则表达式匹配所有以"data:{"开头的字符串
+ final regex = RegExp(r'.*data:\{".*}', multiLine: true);
+ final matches = regex.allMatches(respData);
+
+ // 提取匹配到的字符串并添加到数组中
+ List dataArray = [];
+ for (final match in matches) {
+ // 替换"data:"为空字符串(看结果data后面的冒号没有空格)
+ final replacedString = match.group(0)!.replaceAll(RegExp(r'data:'), '');
+ dataArray.add(replacedString);
+ }
+
+ List list = dataArray
+ .map((e) => CommonRespBody.fromJson(json.decode(e)))
+ .toList();
+
+ print(list);
+
+ return list;
+ } else {
+ ///??? 2024-06-11 阿里云请求报错,会进入dio的错误拦截器,这里ret就是个null了
+ if (respData.runtimeType == String) {
+ respData = json.decode(respData);
+ }
+
+ // 响应是json格式
+ return [CommonRespBody.fromJson(respData ?? {})];
+ }
+ } on HttpException catch (e) {
+ return [
+ CommonRespBody(
+ customReplyText: e.toString(),
+ // 这里的code和msg就不是api返回的,是自行定义的,应该抽出来
+ errorCode: e.code.toString(),
+ errorMsg: e.msg,
+ )
+ ];
+ } catch (e) {
+ print("aaaaaaaaaaaaaaaaaaaa ${e.runtimeType}---$e");
+ // API请求报错,显示报错信息
+ return [
+ CommonRespBody(
+ customReplyText: e.toString(),
+ // 这里的code和msg就不是api返回的,是自行定义的,应该抽出来
+ errorCode: "10000",
+ errorMsg: e.toString(),
+ )
+ ];
+ }
+}
+
+/// 百度平台大模型API的前缀地址
+const baiduAigcUrl =
+ "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/";
+// 百度的token请求地址
+const baiduAigcAuthUrl = "https://aip.baidubce.com/oauth/2.0/token";
+
+///
+///-----------------------------------------------------------------------------
+/// 百度的请求方法
+///
+/// 使用 AK,SK 生成鉴权签名(Access Token)
+Future getAccessToken({
+ bool isUserConfig = true,
+}) async {
+ try {
+ // 这个获取的token的结果是一个_Map,不用转json直接取就得到Access Token了
+ var respData = await HttpUtils.post(
+ path: baiduAigcAuthUrl,
+ method: HttpMethod.post,
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ data: {
+ "grant_type": "client_credentials",
+ // 如果是用户自行配置,使用检查用户通用配置;否则是自己的账号key
+ "client_id":
+ isUserConfig ? MyGetStorage().getBaiduCommonAppId() : BAIDU_API_KEY,
+ "client_secret": isUserConfig
+ ? MyGetStorage().getBaiduCommonAppKey()
+ : BAIDU_SECRET_KEY,
+ },
+ );
+
+ // 响应是json格式
+ return respData['access_token'];
+ } on HttpException catch (e) {
+ return e.msg;
+ } catch (e) {
+ // API请求报错,显示报错信息
+ return e.toString();
+ }
+}
+
+/// 获取流式响应数据
+Future> getBaiduAigcResp(
+ List messages, {
+ String? model,
+ bool stream = false,
+ bool isUserConfig = true,
+}) async {
+ // 如果有传模型名称,就用传递的;没有就默认的
+ // 百度免费的ernie-speed和ernie-lite 接口使用上是一致的,就是模型名称不一样
+ model = model ?? newLLMSpecs[PlatformLLM.baiduErnieSpeed128KFREE]!.model;
+
+ // 每次请求都要实时获取最小的token
+ String token = await getAccessToken(isUserConfig: isUserConfig);
+
+ var body = CommonReqBody(messages: messages, stream: stream);
+
+ var start = DateTime.now().millisecondsSinceEpoch;
+
+ try {
+ var respData = await HttpUtils.post(
+ path: "$baiduAigcUrl$model?access_token=$token",
+ method: HttpMethod.post,
+ headers: {"Content-Type": "application/json"},
+ data: body,
+ );
+
+ var end = DateTime.now().millisecondsSinceEpoch;
+ print("百度 aigc 响应耗时: ${(end - start) / 1000} 秒");
+
+ if (stream) {
+ List list = (respData as String)
+ .split("data:")
+ .where((e) => e.isNotEmpty)
+ .map((e) => CommonRespBody.fromJson(json.decode(e)))
+ .toList();
+
+ print("=======================bbbbbbbbbbbbb\n$list");
+
+ // 响应是json格式
+ return list;
+ } else {
+ return [CommonRespBody.fromJson(respData)];
+ }
+ } on HttpException catch (e) {
+ return [
+ CommonRespBody(
+ customReplyText: e.toString(),
+ // 这里的code和msg就不是api返回的,是自行定义的,应该抽出来
+ errorCode: e.code.toString(),
+ errorMsg: e.msg,
+ )
+ ];
+ } catch (e) {
+ print("bbbbbbbbbbbbbbbb ${e.runtimeType}---$e");
+ // API请求报错,显示报错信息
+ return [
+ CommonRespBody(
+ customReplyText: e.toString(),
+ // 这里的code和msg就不是api返回的,是自行定义的,应该抽出来
+ errorCode: "10000",
+ errorMsg: e.toString(),
+ )
+ ];
+ }
+}
diff --git a/lib/apis/gen_access_token/tencet_hunyuan_signature_v3.dart b/lib/apis/gen_access_token/tencet_hunyuan_signature_v3.dart
new file mode 100644
index 0000000..6e15052
--- /dev/null
+++ b/lib/apis/gen_access_token/tencet_hunyuan_signature_v3.dart
@@ -0,0 +1,118 @@
+// ignore_for_file: avoid_print
+
+import 'dart:convert';
+import 'package:crypto/crypto.dart';
+import 'package:intl/intl.dart';
+
+/// 这里是生成签名的方案
+/// 内容局限在使用hunyuan-lite大模型的时候。
+/// 参看 https://github.com/TencentCloud/signature-process-demo/blob/main/signature-v3/dart/lib/app.dart
+/// 对照此处“签名示例”进行部分内容修改
+/// https://console.cloud.tencent.com/api/explorer?Product=hunyuan&Version=2023-09-01&Action=ChatCompletions
+Map genHunyuanLiteSignatureHeaders(
+ String payloadString,
+ String id,
+ String key,
+) {
+ // 密钥参数
+ // 2024-06-16 支持用户自行输入自己的id和key
+ var secretId = id;
+ var secretKey = key;
+
+ const service = 'hunyuan';
+ const host = 'hunyuan.tencentcloudapi.com';
+ const endpoint = 'https://$host';
+ // const region = 'ap-guangzhou';
+ const action = 'ChatCompletions';
+ const version = '2023-09-01';
+ const algorithm = 'TC3-HMAC-SHA256';
+ // 获取当前时间戳
+ final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
+ // const timestamp = 1717057782;
+
+ final date = DateFormat('yyyy-MM-dd').format(
+ DateTime.fromMillisecondsSinceEpoch(timestamp * 1000, isUtc: true),
+ );
+
+ // ************* 步骤 1:拼接规范请求串 *************
+ const httpRequestMethod = 'POST';
+ const canonicalUri = '/';
+ const canonicalQuerystring = '';
+ const contentType = 'application/json; charset=utf-8';
+ var payload = payloadString;
+
+ final canonicalHeaders =
+ 'content-type:$contentType\nhost:$host\nx-tc-action:${action.toLowerCase()}\n';
+ const signedHeaders = 'content-type;host;x-tc-action';
+ final hashedRequestPayload = sha256.convert(utf8.encode(payload));
+
+ final canonicalRequest = '''
+$httpRequestMethod
+$canonicalUri
+$canonicalQuerystring
+$canonicalHeaders
+$signedHeaders
+$hashedRequestPayload''';
+
+ print("步骤 1:拼接规范请求串:==============");
+ print(canonicalRequest);
+
+ // ************* 步骤 2:拼接待签名字符串 *************
+ final credentialScope = '$date/$service/tc3_request';
+ final hashedCanonicalRequest = sha256.convert(utf8.encode(canonicalRequest));
+ final stringToSign = '''
+$algorithm
+$timestamp
+$credentialScope
+$hashedCanonicalRequest''';
+
+ print("步骤 2:拼接待签名字符串:=============");
+ print(stringToSign);
+
+ // ************* 步骤 3:计算签名 *************
+ List sign(List key, String msg) {
+ final hmacSha256 = Hmac(sha256, key);
+ return hmacSha256.convert(utf8.encode(msg)).bytes;
+ }
+
+ final secretDate = sign(utf8.encode('TC3$secretKey'), date);
+ final secretService = sign(secretDate, service);
+ final secretSigning = sign(secretService, 'tc3_request');
+ final signature =
+ Hmac(sha256, secretSigning).convert(utf8.encode(stringToSign)).toString();
+
+ print("步骤 3:计算签名:=============");
+ print(signature);
+
+ // ************* 步骤 4:拼接 Authorization *************
+ final authorization =
+ '$algorithm Credential=$secretId/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature';
+
+ print("步骤 4:拼接 Authorization:=============");
+
+ print(authorization);
+
+ print("在构建签名中的打印---------------------------");
+ print(
+ 'curl -X POST $endpoint'
+ ' -H "Authorization: $authorization"'
+ ' -H "Content-Type: $contentType"'
+ ' -H "Host: $host"'
+ ' -H "X-TC-Action: $action"'
+ ' -H "X-TC-Timestamp: $timestamp"'
+ ' -H "X-TC-Version: $version"'
+ // ' -H "X-TC-Region: $region"'
+ ' -d \'$payload\'',
+ );
+
+ // 拼接header
+ var headers = {
+ "X-TC-Action": action,
+ "X-TC-Version": version,
+ "X-TC-Timestamp": timestamp,
+ "Content-Type": contentType,
+ "Authorization": authorization,
+ };
+
+ return headers;
+}
diff --git a/lib/apis/tencent_apis.dart b/lib/apis/tencent_apis.dart
new file mode 100644
index 0000000..e69de29
diff --git a/lib/common/components/tool_widget.dart b/lib/common/components/tool_widget.dart
new file mode 100644
index 0000000..59e9540
--- /dev/null
+++ b/lib/common/components/tool_widget.dart
@@ -0,0 +1,783 @@
+// ignore_for_file: avoid_print
+
+import 'dart:convert';
+import 'dart:io';
+import 'dart:math' as math;
+
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:carousel_slider/carousel_slider.dart';
+import 'package:device_info_plus/device_info_plus.dart';
+import 'package:dio/dio.dart';
+import 'package:file_picker/file_picker.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_easyloading/flutter_easyloading.dart';
+import 'package:flutter_form_builder/flutter_form_builder.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:form_builder_file_picker/form_builder_file_picker.dart';
+import 'package:image_gallery_saver/image_gallery_saver.dart';
+import 'package:multi_select_flutter/multi_select_flutter.dart';
+import 'package:photo_view/photo_view.dart';
+import 'package:photo_view/photo_view_gallery.dart';
+
+import '../constants.dart';
+
+// 绘制转圈圈
+Widget buildLoader(bool isLoading) {
+ if (isLoading) {
+ return const Center(
+ child: CircularProgressIndicator(),
+ );
+ } else {
+ return Container();
+ }
+}
+
+commonHintDialog(BuildContext context, String title, String message) {
+ showDialog(
+ context: context,
+ builder: (context) {
+ return AlertDialog(
+ title: Text(title),
+ content: Text(message, style: TextStyle(fontSize: 12.sp)),
+ actions: [
+ TextButton(
+ onPressed: () {
+ Navigator.pop(context);
+ },
+ child: const Text("确定"),
+ ),
+ ],
+ );
+ },
+ );
+}
+
+// 显示底部提示条(默认都是出错或者提示的)
+void showSnackMessage(
+ BuildContext context,
+ String message, {
+ Color? backgroundColor = Colors.red,
+}) {
+ var snackBar = SnackBar(
+ content: Text(message),
+ duration: const Duration(seconds: 3),
+ backgroundColor: backgroundColor,
+ );
+
+ ScaffoldMessenger.of(context).showSnackBar(snackBar);
+}
+
+/// 构建文本生成的图片结果列表
+/// 点击预览,长按下载
+buildNetworkImageViewGrid(
+ String style,
+ List urls,
+ BuildContext context,
+) {
+ return GridView.count(
+ crossAxisCount: 2,
+ shrinkWrap: true,
+ mainAxisSpacing: 5.sp,
+ crossAxisSpacing: 5.sp,
+ physics: const NeverScrollableScrollPhysics(),
+ children: buildImageList(style, urls, context),
+ );
+}
+
+// 2024-06-27 在小米6中此放在上面imageViewGrid没问题,但Z60U就报错;因为无法调试,错误原因不知
+// 所以在文生图历史记录中点击某个记录时,不使用上面那个,而使用这个
+buildImageList(String style, List urls, BuildContext context) {
+ return List.generate(urls.length, (index) {
+ return GridTile(
+ child: GestureDetector(
+ // 单击预览
+ onTap: () {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return Dialog(
+ backgroundColor: Colors.transparent, // 设置背景透明
+ child: PhotoView(
+ imageProvider: NetworkImage(urls[index]),
+ // 设置图片背景为透明
+ backgroundDecoration: const BoxDecoration(
+ color: Colors.transparent,
+ ),
+ // 可以旋转
+ // enableRotation: true,
+ // 缩放的最大最小限制
+ minScale: PhotoViewComputedScale.contained * 0.8,
+ maxScale: PhotoViewComputedScale.covered * 2,
+ errorBuilder: (context, url, error) =>
+ const Icon(Icons.error),
+ ),
+ );
+ },
+ );
+ },
+ // 长按保存到相册
+ onLongPress: () async {
+ if (Platform.isAndroid) {
+ final deviceInfoPlugin = DeviceInfoPlugin();
+ final deviceInfo = await deviceInfoPlugin.androidInfo;
+ final sdkInt = deviceInfo.version.sdkInt;
+
+ // Android9对应sdk是28,<=28就不显示保存按钮
+ if (sdkInt > 28) {
+ // 点击预览或者下载
+ var response = await Dio().get(urls[index],
+ options: Options(responseType: ResponseType.bytes));
+
+ print(response.data);
+
+ // 安卓9及以下好像无法保存
+ final result = await ImageGallerySaver.saveImage(
+ Uint8List.fromList(response.data),
+ quality: 100,
+ name: "${style}_${DateTime.now().millisecondsSinceEpoch}",
+ );
+ if (result["isSuccess"] == true) {
+ EasyLoading.showToast("图片已保存到相册!");
+ } else {
+ EasyLoading.showToast("无法保存图片!");
+ }
+ } else {
+ EasyLoading.showToast("Android 9 及以下版本无法长按保存到相册!");
+ }
+ }
+ },
+ // 默认缓存展示
+ child: SizedBox(
+ height: 0.2.sw,
+ child: CachedNetworkImage(
+ imageUrl: urls[index],
+ fit: BoxFit.cover,
+ progressIndicatorBuilder: (context, url, downloadProgress) =>
+ Center(
+ child: SizedBox(
+ height: 50.sp,
+ width: 50.sp,
+ child: CircularProgressIndicator(
+ value: downloadProgress.progress,
+ ),
+ ),
+ ),
+ errorWidget: (context, url, error) => const Icon(Icons.error),
+ ),
+ ),
+ ),
+ );
+ }).toList();
+}
+
+/// 构建图片预览,可点击放大
+/// 注意限定传入的图片类型,要在这些条件之中
+Widget buildImageView(
+ dynamic image,
+ BuildContext context, {
+ // 是否是本地文件地址(暂时没使用到网络地址)
+ bool? isFileUrl = false,
+}) {
+ // 如果没有图片数据,直接返回文提示
+ if (image == null) {
+ return const Center(
+ child: Text(
+ '请选择图片',
+ style: TextStyle(color: Colors.grey),
+ ),
+ );
+ }
+
+ print("显示的图片类型---${image.runtimeType == File}-${image.runtimeType} -$image");
+
+ ImageProvider imageProvider;
+ // 只有base64的字符串或者文件格式
+ if (image.runtimeType == String && isFileUrl == false) {
+ imageProvider = MemoryImage(base64Decode(image));
+ }
+ if (image.runtimeType == String && isFileUrl == true) {
+ imageProvider = FileImage(File(image));
+ } else {
+ // 如果直接传文件,那就是文件
+ imageProvider = FileImage(image);
+ }
+
+ return GridTile(
+ child: GestureDetector(
+ // 单击预览
+ onTap: () {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return Dialog(
+ backgroundColor: Colors.transparent, // 设置背景透明
+ child: PhotoView(
+ imageProvider: imageProvider,
+ // 设置图片背景为透明
+ backgroundDecoration: const BoxDecoration(
+ color: Colors.transparent,
+ ),
+ // 可以旋转
+ // enableRotation: true,
+ // 缩放的最大最小限制
+ minScale: PhotoViewComputedScale.contained * 0.8,
+ maxScale: PhotoViewComputedScale.covered * 2,
+ errorBuilder: (context, url, error) => const Icon(Icons.error),
+ ),
+ );
+ },
+ );
+ },
+ // 默认显示文件图片
+ child: RepaintBoundary(
+ child: Center(
+ child: Image(image: imageProvider, fit: BoxFit.scaleDown),
+ ),
+ ),
+ ),
+ );
+}
+
+// 生成随机颜色
+Color genRandomColor() =>
+ Color((math.Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
+
+// 生成随机颜色带透明度
+Color genRandomColorWithOpacity({double? opacity}) =>
+ Color((math.Random().nextDouble() * 0xFFFFFF).toInt())
+ .withOpacity(opacity ?? math.Random().nextDouble());
+
+// 指定长度的随机字符串
+const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
+math.Random _rnd = math.Random();
+String getRandomString(int length) {
+ return String.fromCharCodes(
+ Iterable.generate(
+ length,
+ (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)),
+ ),
+ );
+}
+
+// 指定长度的范围的随机字符串(包含上面那个,最大最小同一个值即可)
+String generateRandomString(int minLength, int maxLength) {
+ int length = minLength + _rnd.nextInt(maxLength - minLength + 1);
+
+ return String.fromCharCodes(
+ Iterable.generate(
+ length,
+ (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)),
+ ),
+ );
+}
+
+// 异常弹窗
+commonExceptionDialog(BuildContext context, String title, String message) {
+ showDialog(
+ context: context,
+ builder: (context) {
+ return AlertDialog(
+ title: Text(title),
+ content: Text(message, style: const TextStyle(fontSize: 13)),
+ actions: [
+ TextButton(
+ onPressed: () {
+ Navigator.pop(context);
+ },
+ child: const Text("确定"),
+ ),
+ ],
+ );
+ },
+ );
+}
+
+///
+/// form builder 库中文本栏位和下拉选择框组件的二次封装
+///
+// 构建表单的文本输入框
+Widget cusFormBuilerTextField(String name,
+ {String? initialValue,
+ double? valueFontSize,
+ int? maxLines,
+ String? hintText, // 可不传提示语
+ TextStyle? hintStyle,
+ String? labelText, // 可不传栏位标签,在输入框前面有就行
+ String? Function(Object?)? validator,
+ bool? isOutline = false, // 输入框是否有线条
+ bool isReadOnly = false, // 输入框是否有线条
+ TextInputType? keyboardType,
+ void Function(String?)? onChanged,
+ List? inputFormatters}) {
+ return Padding(
+ padding: EdgeInsets.symmetric(horizontal: 10.sp),
+ child: FormBuilderTextField(
+ name: name,
+ initialValue: initialValue,
+ maxLines: maxLines,
+ readOnly: isReadOnly,
+ style: TextStyle(fontSize: valueFontSize),
+ // 2023-12-04 没有传默认使用name,原本默认的.text会弹安全键盘,可能无法输入中文
+ // 2023-12-21 enableSuggestions 设为 true后键盘类型为text就正常了。
+ // 注意:如果有最大行超过1的话,默认启用多行的键盘类型
+ enableSuggestions: true,
+ keyboardType: keyboardType ??
+ ((maxLines != null && maxLines > 1)
+ ? TextInputType.multiline
+ : TextInputType.text),
+
+ decoration: _buildInputDecoration(
+ isOutline,
+ isReadOnly,
+ labelText,
+ hintText,
+ hintStyle,
+ ),
+ validator: validator,
+ onChanged: onChanged,
+ // 输入的格式限制
+ inputFormatters: inputFormatters,
+ ),
+ );
+}
+
+/// 构建下拉多选弹窗模块栏位(主要为了样式统一)
+Widget buildModifyMultiSelectDialogField(
+ BuildContext context, {
+ required List items,
+ GlobalKey>? key,
+ List initialValue = const [],
+ String? labelText,
+ String? hintText,
+ String? Function(List?)? validator,
+ required void Function(List) onConfirm,
+}) {
+ // 把预设的基础活动选项列表转化为 MultiSelectDialogField 支持的列表
+ final formattedItems = items
+ .map>(
+ (opt) => MultiSelectItem(opt, opt.cnLabel))
+ .toList();
+
+ return Padding(
+ padding: EdgeInsets.symmetric(horizontal: 10.sp),
+ child: MultiSelectDialogField(
+ key: key,
+ items: formattedItems,
+ // ???? 好像是不带validator用了这个初始值就会报错
+ initialValue: initialValue,
+ title: Text(hintText ?? ''),
+ // selectedColor: Colors.blue,
+ decoration: BoxDecoration(
+ // color: Colors.blue.withOpacity(0.1),
+ borderRadius: BorderRadius.all(Radius.circular(5.sp)),
+ border: Border.all(
+ width: 2.sp,
+ color: Theme.of(context).disabledColor,
+ ),
+ ),
+ // buttonIcon: const Icon(Icons.fitness_center, color: Colors.blue),
+ buttonIcon: const Icon(Icons.restaurant_menu),
+ buttonText: Text(
+ labelText ?? "",
+ style: TextStyle(
+ // color: Colors.blue[800],
+ fontSize: 12.sp,
+ ),
+ ),
+ // searchable: true,
+ validator: validator,
+ onConfirm: onConfirm,
+ cancelText: const Text("取消"),
+ confirmText: const Text("确认"),
+ ),
+ );
+}
+
+// formbuilder 下拉框和文本输入框的样式等内容
+InputDecoration _buildInputDecoration(
+ bool? isOutline,
+ bool isReadOnly,
+ String? labelText,
+ String? hintText,
+ TextStyle? hintStyle,
+) {
+ final contentPadding = isOutline != null && isOutline
+ ? EdgeInsets.symmetric(horizontal: 5.sp, vertical: 15.sp)
+ : EdgeInsets.symmetric(horizontal: 5.sp, vertical: 5.sp);
+
+ return InputDecoration(
+ isDense: true,
+ labelText: labelText,
+ hintText: hintText,
+ hintStyle: hintStyle,
+ contentPadding: contentPadding,
+ border: isOutline != null && isOutline
+ ? OutlineInputBorder(
+ borderRadius: BorderRadius.circular(10.0),
+ )
+ : isReadOnly
+ ? InputBorder.none
+ : null,
+ // 设置透明底色
+ filled: true,
+ fillColor: Colors.transparent,
+ );
+}
+
+buildSmallChip(
+ String labelText, {
+ Color? bgColor,
+ double? labelTextSize,
+}) {
+ return Chip(
+ label: Text(labelText),
+ backgroundColor: bgColor,
+ labelStyle: TextStyle(fontSize: labelTextSize),
+ labelPadding: EdgeInsets.zero,
+ // 设置负数会报错,但好像看到有点效果呢
+ // labelPadding: EdgeInsets.fromLTRB(0, -6.sp, 0, -6.sp),
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ );
+}
+
+// 用一个按钮假装是一个标签,用来展示
+buildSmallButtonTag(
+ String labelText, {
+ Color? bgColor,
+ double? labelTextSize,
+}) {
+ return RawMaterialButton(
+ onPressed: () {},
+ constraints: const BoxConstraints(),
+ padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 4.0),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10.0),
+ ),
+ fillColor: bgColor ?? Colors.grey[300],
+ child: Text(
+ labelText,
+ style: TextStyle(fontSize: labelTextSize ?? 12.sp),
+ ),
+ );
+}
+
+// 一般当做标签用,比上面个还小
+// 传入的字体最好不超过10
+buildTinyButtonTag(
+ String labelText, {
+ Color? bgColor,
+ double? labelTextSize,
+}) {
+ return SizedBox(
+ // 传入大于12的字体,修正为12;不传则默认12
+ height: ((labelTextSize != null && labelTextSize > 10.sp)
+ ? 10.sp
+ : labelTextSize ?? 10.sp) +
+ 10.sp,
+ child: RawMaterialButton(
+ onPressed: () {},
+ constraints: const BoxConstraints(),
+ padding: EdgeInsets.fromLTRB(4.sp, 2.sp, 4.sp, 2.sp),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10.sp),
+ ),
+ fillColor: bgColor ?? Colors.grey[300],
+ child: Text(
+ labelText,
+ style: TextStyle(
+ // 传入大于10的字体,修正为10;不传则默认10
+ fontSize: (labelTextSize != null && labelTextSize > 10.sp)
+ ? 10.sp
+ : labelTextSize ?? 10.sp,
+ ),
+ ),
+ ),
+ );
+}
+
+// 带有横线滚动条的datatable
+buildDataTableWithHorizontalScrollbar({
+ required ScrollController scrollController,
+ required List columns,
+ required List rows,
+}) {
+ return Scrollbar(
+ thickness: 5,
+ // 设置交互模式后,滚动条和手势滚动方向才一致
+ interactive: true,
+ radius: Radius.circular(5.sp),
+ // 不设置这个,滚动条默认不显示,在滚动时才显示
+ thumbVisibility: true,
+ // trackVisibility: true,
+ // 滚动条默认在右边,要改在左边就配合Transform进行修改(此例没必要)
+ // 刻意预留一点空间给滚动条
+ controller: scrollController,
+ child: SingleChildScrollView(
+ controller: scrollController,
+ scrollDirection: Axis.horizontal,
+ child: DataTable(
+ // dataRowHeight: 10.sp,
+ dataRowMinHeight: 60.sp, // 设置行高范围
+ dataRowMaxHeight: 100.sp,
+ headingRowHeight: 25, // 设置表头行高
+ horizontalMargin: 10, // 设置水平边距
+ columnSpacing: 20.sp, // 设置列间距
+ columns: columns,
+ rows: rows,
+ ),
+ ),
+ );
+}
+
+/// ----
+///
+// 图片轮播
+buildImageCarouselSlider(
+ List imageList, {
+ bool isNoImage = false, // 是否不显示图片,默认就算无图片也显示占位图片
+ int type = 3, // 轮播图是否可以点击预览图片,预设为3(具体类型参看下方实现方法)
+}) {
+ return CarouselSlider(
+ options: CarouselOptions(
+ autoPlay: true, // 自动播放
+ enlargeCenterPage: true, // 居中图片放大
+ aspectRatio: 16 / 9, // 图片宽高比
+ viewportFraction: 1, // 图片占屏幕宽度的比例
+ // 只有一张图片时不滚动
+ enableInfiniteScroll: imageList.length > 1,
+ ),
+ // 除非指定不显示图片,否则没有图片也显示一张占位图片
+ items: isNoImage
+ ? null
+ : imageList.isEmpty
+ ? [Image.asset(placeholderImageUrl, fit: BoxFit.scaleDown)]
+ : imageList.map((imageUrl) {
+ return Builder(
+ builder: (BuildContext context) {
+ return _buildImageCarouselSliderType(
+ type,
+ context,
+ imageUrl,
+ imageList,
+ );
+ },
+ );
+ }).toList(),
+ );
+}
+
+// 2024-03-12 根据图片地址前缀来区分是否是网络图片,使用不同的方式展示图片
+Widget buildNetworkOrFileImage(String imageUrl, {BoxFit? fit}) {
+ if (imageUrl.startsWith('http') || imageUrl.startsWith('https')) {
+ return CachedNetworkImage(
+ imageUrl: imageUrl,
+ fit: fit,
+ // progressIndicatorBuilder: (context, url, progress) => Center(
+ // child: CircularProgressIndicator(
+ // value: progress.progress,
+ // ),
+ // ),
+
+ /// placeholder 和 progressIndicatorBuilder 只能2选1
+ // placeholder: (context, url) => Center(
+ // child: SizedBox(
+ // width: 50.sp,
+ // height: 50.sp,
+ // child: const CircularProgressIndicator(),
+ // ),
+ // ),
+ errorWidget: (context, url, error) => Center(
+ child: Icon(Icons.error, size: 36.sp),
+ ),
+ );
+
+// 2024-03-29 这样每次都会重新请求图片,网络图片都不小的,流量顶不住。用上面的
+ // return Image.network(
+ // imageUrl,
+ // errorBuilder: (context, error, stackTrace) {
+ // return Image.asset(placeholderImageUrl, fit: BoxFit.scaleDown);
+ // },
+ // fit: fit,
+ // );
+ } else {
+ return Image.file(
+ File(imageUrl),
+ errorBuilder: (context, error, stackTrace) {
+ return Image.asset(placeholderImageUrl, fit: BoxFit.scaleDown);
+ },
+ fit: fit,
+ );
+ }
+}
+
+// 2024-03-12 根据图片地址前缀来区分是否是网络图片
+bool isNetworkImageUrl(String imageUrl) {
+ return (imageUrl.startsWith('http') || imageUrl.startsWith('https'));
+}
+
+ImageProvider getImageProvider(String imageUrl) {
+ if (imageUrl.startsWith('http') || imageUrl.startsWith('https')) {
+ return CachedNetworkImageProvider(imageUrl);
+ // return NetworkImage(imageUrl);
+ } else {
+ return FileImage(File(imageUrl));
+ }
+}
+
+/// 2023-12-26
+/// 现在设计轮播图3种形态:
+/// 1 点击某张图片,可以弹窗显示该图片并进行缩放预览
+/// 2 点击某张图片,可以跳转新页面对该图片并进行缩放预览
+/// 3 点击某张图片,可以弹窗对该图片所在整个列表进行缩放预览(默认选项)
+/// default 单纯的轮播展示,点击图片无动作
+_buildImageCarouselSliderType(
+ int type,
+ BuildContext context,
+ String imageUrl,
+ List imageList,
+) {
+ buildCommonImageWidget(Function() onTap) =>
+ GestureDetector(onTap: onTap, child: buildNetworkOrFileImage(imageUrl));
+
+ switch (type) {
+ // 这个直接弹窗显示图片可以缩放
+ case 1:
+ return buildCommonImageWidget(() {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return Dialog(
+ backgroundColor: Colors.transparent, // 设置背景透明
+ child: PhotoView(
+ imageProvider: getImageProvider(imageUrl),
+ // 设置图片背景为透明
+ backgroundDecoration: const BoxDecoration(
+ color: Colors.transparent,
+ ),
+ // 可以旋转
+ enableRotation: true,
+ // 缩放的最大最小限制
+ minScale: PhotoViewComputedScale.contained * 0.8,
+ maxScale: PhotoViewComputedScale.covered * 2,
+ errorBuilder: (context, url, error) => const Icon(Icons.error),
+ ),
+ );
+ },
+ );
+ });
+ case 2:
+ return buildCommonImageWidget(() {
+ // 这个是跳转到新的页面去
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => PhotoView(
+ imageProvider: getImageProvider(imageUrl),
+ enableRotation: true,
+ errorBuilder: (context, url, error) => const Icon(Icons.error),
+ ),
+ ),
+ );
+ });
+ case 3:
+ return buildCommonImageWidget(() {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ // 这个弹窗默认是无法全屏的,上下左右会留点空,点击这些空隙可以关闭弹窗
+ return Dialog(
+ backgroundColor: Colors.transparent,
+ child: PhotoViewGallery.builder(
+ itemCount: imageList.length,
+ builder: (BuildContext context, int index) {
+ return PhotoViewGalleryPageOptions(
+ imageProvider: getImageProvider(imageList[index]),
+ errorBuilder: (context, url, error) =>
+ const Icon(Icons.error),
+ );
+ },
+ // enableRotation: true,
+ scrollPhysics: const BouncingScrollPhysics(),
+ backgroundDecoration: const BoxDecoration(
+ color: Colors.transparent,
+ ),
+ loadingBuilder: (BuildContext context, ImageChunkEvent? event) {
+ return const Center(child: CircularProgressIndicator());
+ },
+ ),
+ );
+ },
+ );
+ });
+ default:
+ return Container(
+ width: MediaQuery.of(context).size.width,
+ margin: const EdgeInsets.symmetric(horizontal: 5.0),
+ decoration: const BoxDecoration(color: Colors.grey),
+ child: buildNetworkOrFileImage(imageUrl),
+ );
+ }
+}
+
+// 将图片字符串,转为文件多选框中支持的图片平台文件
+// formbuilder的图片地址拼接的字符串,要转回平台文件列表
+List convertStringToPlatformFiles(String imagesString) {
+ List imageUrls = imagesString.split(','); // 拆分字符串
+ // 如果本身就是空字符串,直接返回空平台文件数组
+ if (imagesString.trim().isEmpty || imageUrls.isEmpty) {
+ return [];
+ }
+
+ List platformFiles = []; // 存储 PlatformFile 对象的列表
+
+ for (var imageUrl in imageUrls) {
+ PlatformFile file = PlatformFile(
+ name: imageUrl,
+ path: imageUrl,
+ size: 32, // 假设图片地址即为文件路径
+ );
+ platformFiles.add(file);
+ }
+
+ return platformFiles;
+}
+
+/// 显示本地路径图片,点击可弹窗显示并缩放
+buildClickImageDialog(BuildContext context, String imageUrl) {
+ return GestureDetector(
+ onTap: () {
+ // 在当前上下文中查找最近的 FocusScope 并使其失去焦点,从而收起键盘。
+ FocusScope.of(context).unfocus();
+ // 这个直接弹窗显示图片可以缩放
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return Dialog(
+ backgroundColor: Colors.transparent, // 设置背景透明
+ child: PhotoView(
+ imageProvider: FileImage(File(imageUrl)),
+ // 设置图片背景为透明
+ backgroundDecoration: const BoxDecoration(
+ color: Colors.transparent,
+ ),
+ // 可以旋转
+ // enableRotation: true,
+ // 缩放的最大最小限制
+ minScale: PhotoViewComputedScale.contained * 0.8,
+ maxScale: PhotoViewComputedScale.covered * 2,
+ errorBuilder: (context, url, error) => const Icon(Icons.error),
+ ),
+ );
+ },
+ );
+ },
+ child: Padding(
+ padding: EdgeInsets.all(20.sp),
+ child: SizedBox(
+ width: 0.8.sw,
+ child: buildNetworkOrFileImage(imageUrl),
+ ),
+ ),
+ );
+}
diff --git a/lib/common/constants.dart b/lib/common/constants.dart
new file mode 100644
index 0000000..d4962e1
--- /dev/null
+++ b/lib/common/constants.dart
@@ -0,0 +1,105 @@
+// 时间格式化字符串
+const constDatetimeFormat = "yyyy-MM-dd HH:mm:ss";
+const constDateFormat = "yyyy-MM-dd";
+const constMonthFormat = "yyyy-MM";
+const constTimeFormat = "HH:mm:ss";
+// 未知的时间字符串
+const unknownDateTimeString = '1970-01-01 00:00:00';
+const unknownDateString = '1970-01-01';
+
+const String placeholderImageUrl = 'assets/images/no_image.jpg';
+
+// 数据库分页查询数据的时候,还需要带上一个该表的总数量
+// 还可以按需补入其他属性
+class CusDataResult {
+ List data;
+ int total;
+
+ CusDataResult({
+ required this.data,
+ required this.total,
+ });
+}
+
+// 自定义标签,常用来存英文、中文、全小写带下划线的英文等。
+class CusLabel {
+ final String enLabel;
+ final String cnLabel;
+ final dynamic value;
+
+ CusLabel({
+ required this.enLabel,
+ required this.cnLabel,
+ required this.value,
+ });
+}
+
+// 菜品的分类和标签都用预设的
+// 2024-03-10 这个项目取值都直接取value,就不区别中英文了
+List dishTagOptions = [
+ CusLabel(enLabel: 'LuCuisine', cnLabel: "鲁菜", value: '鲁菜'),
+ CusLabel(enLabel: 'ChuanCuisine', cnLabel: "川菜", value: '川菜'),
+ CusLabel(enLabel: 'YueCuisine', cnLabel: "粤菜", value: '粤菜'),
+ CusLabel(enLabel: 'SuCuisine', cnLabel: "苏菜", value: '苏菜'),
+ CusLabel(enLabel: 'MinCuisine', cnLabel: "闽菜", value: '闽菜'),
+ CusLabel(enLabel: 'ZheCuisine', cnLabel: "浙菜", value: '浙菜'),
+ CusLabel(enLabel: 'XiangCuisine', cnLabel: "湘菜", value: '湘菜'),
+ CusLabel(enLabel: 'stir-fried', cnLabel: "炒", value: '炒'),
+ CusLabel(enLabel: 'Quick-fry', cnLabel: "爆", value: '爆'),
+ CusLabel(enLabel: 'sauté', cnLabel: "熘", value: '熘'),
+ CusLabel(enLabel: 'fry', cnLabel: "炸", value: '炸'),
+ CusLabel(enLabel: 'boil', cnLabel: "烹", value: '烹'),
+ CusLabel(enLabel: 'decoct', cnLabel: "煎", value: '煎'),
+ CusLabel(enLabel: 'paste', cnLabel: "贴", value: '贴'),
+ CusLabel(enLabel: 'bake', cnLabel: "烧", value: '烧'),
+ CusLabel(enLabel: 'sweat', cnLabel: "焖", value: '焖'),
+ CusLabel(enLabel: 'stew', cnLabel: "炖", value: '炖'),
+ CusLabel(enLabel: 'steam', cnLabel: "蒸", value: '蒸'),
+ CusLabel(enLabel: 'quick-boil', cnLabel: "汆", value: '汆'),
+ CusLabel(enLabel: 'boil', cnLabel: "煮", value: '煮'),
+ CusLabel(enLabel: 'braise', cnLabel: "烩", value: '烩'),
+ CusLabel(enLabel: 'Qiang', cnLabel: "炝", value: '炝'),
+ CusLabel(enLabel: 'salt', cnLabel: "腌", value: '腌'),
+ CusLabel(enLabel: 'stir-and-mix', cnLabel: "拌", value: '拌'),
+ CusLabel(enLabel: 'roast', cnLabel: "烤", value: '烤'),
+ CusLabel(enLabel: 'bittern', cnLabel: "卤", value: '卤'),
+ CusLabel(enLabel: 'freeze', cnLabel: "冻", value: '冻'),
+ CusLabel(enLabel: 'wire-drawing', cnLabel: "拔丝", value: '拔丝'),
+ CusLabel(enLabel: 'honey-sauce', cnLabel: "蜜汁", value: '蜜汁'),
+ CusLabel(enLabel: 'smoked', cnLabel: "熏", value: '熏'),
+ CusLabel(enLabel: 'roll', cnLabel: "卷", value: '卷'),
+ CusLabel(enLabel: 'other', cnLabel: "其他技法", value: '其他技法'),
+];
+
+List dishCateOptions = [
+ CusLabel(enLabel: 'Breakfast', cnLabel: "早餐", value: '早餐'),
+ CusLabel(enLabel: 'Lunch', cnLabel: "早茶", value: '早茶'),
+ CusLabel(enLabel: 'Lunch', cnLabel: "午餐", value: '午餐'),
+ CusLabel(enLabel: 'AfternoonTea', cnLabel: "下午茶", value: '下午茶'),
+ CusLabel(enLabel: 'Dinner', cnLabel: "晚餐", value: '晚餐'),
+ CusLabel(enLabel: 'MidnightSnack', cnLabel: "夜宵", value: '夜宵'),
+ CusLabel(enLabel: 'Dessert', cnLabel: "甜点", value: '甜点'),
+ CusLabel(enLabel: 'StapleFood', cnLabel: "主食", value: '主食'),
+ CusLabel(enLabel: 'Other', cnLabel: "其他", value: '其他'),
+];
+
+// 进入对话页面简单预设的一些问题
+List defaultChatQuestions = [
+ "你好,介绍一下你自己。",
+ "将“纵观世界风云,风景这边独好”这句话,翻译成英语、日语、俄语和西班牙语。",
+ "介绍一下“受害者有罪论”,并分析这个说法是否合理。",
+ "老板经常以未达到工作考核来克扣工资,经常让我无偿加班,是否已经违法?",
+ "你是一位产品文案。请设计一份PPT大纲,介绍你们公司新推出的防晒霜,要求言简意赅并且具有创意。",
+ "你是一位10w+爆款文章的编辑。请结合赛博玄学主题,如电子木鱼、机甲佛祖、星座、塔罗牌、人形锦鲤、工位装修等,用俏皮有网感的语言撰写一篇公众号文章。",
+ "我是小区物业人员,小区下周六(9.30号)下午16:00-18:00,因为电力改造施工要停电,请帮我拟一份停电通知。",
+ "一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。",
+ "使用python3编写一个快速排序算法。",
+ // "你是一个营养师。现在请帮我制定一周的健康减肥食谱。",
+ // "小明因为女朋友需要的高额彩礼费而伤心焦虑,请帮我安慰一下他。",
+ // "请为一家互联网公司写一则差旅费用管理规则。",
+ // "小王最近天天加班,压力很大,心情很糟。也想着跳槽,但是就业大环境很差,不容易找到新工作。现在他很迷茫,请帮他出出主意。",
+ // "使用python3编写一个快速排序算法。",
+ // "如果我的邻居持续发出噪音严重影响我的生活,除了民法典1032条,还有什么法条支持居民向噪音发出者维权?",
+ // "请帮我写一份通用的加薪申请模板。",
+ // "一个长方体的棱长和是144厘米,它的长、宽、高之比是4:3:2,长方体的体积是多少?",
+];
diff --git a/lib/common/db_tools/db_helper.dart b/lib/common/db_tools/db_helper.dart
new file mode 100644
index 0000000..231d4cd
--- /dev/null
+++ b/lib/common/db_tools/db_helper.dart
@@ -0,0 +1,769 @@
+// ignore_for_file: avoid_print, constant_identifier_names
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:sqflite/sqflite.dart';
+import 'package:path_provider/path_provider.dart';
+
+import '../../models/brief_accounting_state.dart';
+import '../../models/dish.dart';
+import '../../models/llm_chat_state.dart';
+import '../../models/llm_text2image_state.dart';
+import '../constants.dart';
+import 'ddl_ai_light_life.dart';
+
+// 导出表文件临时存放的文件夹
+const DB_EXPORT_DIR = "db_export";
+// 导出的表前缀
+const DB_EXPORT_TABLE_PREFIX = "all_";
+
+class DBHelper {
+ ///
+ /// 数据库初始化相关
+ ///
+
+ // 单例模式
+ static final DBHelper _dbHelper = DBHelper._createInstance();
+ // 构造函数,返回单例
+ factory DBHelper() => _dbHelper;
+ // 数据库实例
+ static Database? _database;
+
+ // 创建sqlite的db文件成功后,记录该地址,以便删除时使用。
+ var dbFilePath = "";
+
+ // 命名的构造函数用于创建DatabaseHelper的实例
+ DBHelper._createInstance();
+
+ // 获取数据库实例
+ Future get database async => _database ??= await initializeDB();
+
+ // 初始化数据库
+ Future initializeDB() async {
+ // 获取Android和iOS存储数据库的目录路径(用户看不到,在Android/data/……里看不到)。
+ // Directory directory = await getApplicationDocumentsDirectory();
+
+ // IOS不支持这个方法,所以可能取不到这个地址
+ Directory? directory2 = await getExternalStorageDirectory();
+ String path = "${directory2?.path}/${AILightLifeDdl.databaseName}";
+
+ print("初始化 DB sqlite数据库存放的地址:$path");
+
+ // 在给定路径上打开/创建数据库
+ var dietaryDb = await openDatabase(path, version: 1, onCreate: _createDb);
+ dbFilePath = path;
+ return dietaryDb;
+ }
+
+ // 创建训练数据库相关表
+ void _createDb(Database db, int newVersion) async {
+ print("开始创建表 _createDb……");
+
+ await db.transaction((txn) async {
+ // txn.execute(BriefAccountingDdl.ddlForExpend);
+ // txn.execute(BriefAccountingDdl.ddlForIncome);
+ txn.execute(AILightLifeDdl.ddlForBillItem);
+ txn.execute(AILightLifeDdl.ddlForChatHistory);
+ txn.execute(AILightLifeDdl.ddlForText2ImageHistory);
+ txn.execute(AILightLifeDdl.ddlForDish);
+ });
+ }
+
+ // 关闭数据库
+ Future closeDB() async {
+ Database db = await database;
+
+ print("db.isOpen ${db.isOpen}");
+ await db.close();
+ print("db.isOpen ${db.isOpen}");
+
+ // 删除db或者关闭db都需要重置db为null,
+ // 否则后续会保留之前的连接,以致出现类似错误:Unhandled Exception: DatabaseException(database_closed 5)
+ // https://github.com/tekartik/sqflite/issues/223
+ _database = null;
+
+ // 如果已经关闭了,返回ture
+ return !db.isOpen;
+ }
+
+ // 删除sqlite的db文件(初始化数据库操作中那个path的值)
+ Future deleteDB() async {
+ print("开始删除內嵌的 sqlite db文件,db文件地址:$dbFilePath");
+
+ // 先删除,再重置,避免仍然存在其他线程在访问数据库,从而导致删除失败
+ await deleteDatabase(dbFilePath);
+
+ // 删除db或者关闭db都需要重置db为null,
+ // 否则后续会保留之前的连接,以致出现类似错误:Unhandled Exception: DatabaseException(database_closed 5)
+ // https://stackoverflow.com/questions/60848752/delete-database-when-log-out-and-create-again-after-log-in-dart
+ _database = null;
+ }
+
+ // 显示db中已有的table,默认的和自建立的
+ void showTableNameList() async {
+ Database db = await database;
+ var tableNames = (await db.query(
+ 'sqlite_master',
+ where: 'type = ?',
+ whereArgs: ['table'],
+ ))
+ .map((row) => row['name'] as String)
+ .toList(growable: false);
+
+ print("DB中拥有的表名:------------");
+ print(tableNames);
+ }
+
+ // 导出所有数据
+ Future exportDatabase() async {
+ // 获取应用文档目录路径
+ Directory appDocDir = await getApplicationDocumentsDirectory();
+ // 创建或检索 db_export 文件夹
+ var tempDir = await Directory(
+ p.join(appDocDir.path, DB_EXPORT_DIR),
+ ).create();
+
+ // 打开数据库
+ Database db = await database;
+
+ // 获取所有表名
+ List