1
0
Fork 0

Compare commits

...

197 Commits

Author SHA1 Message Date
vine_liutk 5fde7566f4 添加后台区域标记大屏位置功能 2023-08-01 19:33:36 +08:00
vine_liutk bdc88db320 取消首页调试 2023-08-01 14:34:41 +08:00
vine_liutk 909eee158c 完善通风控制 2023-07-31 17:22:36 +08:00
vine_liutk f719f38677 暂存修改 2023-07-31 17:01:11 +08:00
panliang 09a24aaa83 monitor history 2023-07-28 12:22:17 +08:00
panliang b01e8e9aac Merge branch 'develop' of gitea.hmily.club:liutk/internet-everythings-agricultural into develop 2023-07-28 12:14:14 +08:00
panliang b0d9dbe3f6 monitor history 2023-07-28 12:14:09 +08:00
李静 8da20ed9ad Update 2023-07-27 15:52:32 +08:00
李静 151f7877e4 Update 2023-07-27 15:47:40 +08:00
李静 f11d16251d Update 2023-07-24 14:13:20 +08:00
vine_liutk de943fda5a 完善喷雾设置 2023-07-18 18:22:35 +08:00
李静 132c36e9ef 通风设备数据部同步 2023-07-17 13:50:17 +08:00
李静 aec3b6a338 Update 2023-07-17 13:35:40 +08:00
李静 1414cfae3d 设备数据下行 2023-07-17 12:53:17 +08:00
vine_liutk eab2a29438 调试 2023-07-17 12:07:08 +08:00
李静 ff775ec9d5 Fix 2023-07-17 12:00:06 +08:00
vine_liutk 101bb2c632 调整喷雾设备后台自动设置 2023-07-17 11:58:20 +08:00
vine_liutk de53ecbb16 保存喷雾管理 2023-07-11 18:35:01 +08:00
vine_liutk 4c9d067325 调整报错 2023-07-03 16:16:56 +08:00
vine_liutk 461761d4ae 新增监控点位通过实验田ID查询 2023-06-29 15:17:19 +08:00
vine_liutk 45d9aa724d 调整监控数据接口 2023-06-29 15:05:45 +08:00
vine_liutk 8ba5c0e822 实现大屏基础项设置配置 2023-06-27 10:42:17 +08:00
vine_liutk 0e10085b6e 添加警报数获取,警报记录获取 2023-06-21 15:58:43 +08:00
vine_liutk 4816a77d7f 添加农机设备开机提醒 2023-06-21 15:41:49 +08:00
vine_liutk cb40ea6383 添加基础信息接口 2023-06-21 15:38:29 +08:00
vine_liutk 3cd7f6c206 添加大屏设备数据接口 2023-06-21 15:20:14 +08:00
vine_liutk ba69244e2d 添加部分接口 2023-06-21 12:19:13 +08:00
vine_liutk 9f8dec1843 添加登录和退出登录接口 2023-06-20 10:36:36 +08:00
panliang bbbf09f8ab component editor 2023-05-29 15:23:55 +08:00
panliang 2c2436aa8e component editor 2023-05-29 15:19:05 +08:00
vine_liutk 007f5bb08d 处理冲突 2023-05-29 14:42:43 +08:00
vine_liutk b1af62011e 调整标签搜索 2023-05-29 14:42:01 +08:00
panliang fc8d414a19 filer 2023-05-29 14:33:03 +08:00
panliang f61ebd0606 auth 2023-05-29 13:05:54 +08:00
panliang 58c83b5f7d bug monitor_modes 2023-05-29 12:50:08 +08:00
vine_liutk 9cd82c64bc Merge branch 'develop' of https://gitea.hmily.club/liutk/internet-everythings-agricultural into develop 2023-05-29 10:56:54 +08:00
vine_liutk 692151b5b3 添加保存提示 2023-05-29 10:56:50 +08:00
panliang 9eca0a82ca auth 2023-05-27 09:57:32 +08:00
panliang 372f19fc0c region 2023-05-26 23:30:38 +08:00
vine_liutk ae12a7aceb 添加预警消息点位显示 2023-05-26 19:15:37 +08:00
vine_liutk da6634fd4e 添加预警消息点位显示 2023-05-26 19:15:10 +08:00
vine_liutk 8840e75eb2 处理重复报警问题 2023-05-26 18:44:43 +08:00
vine_liutk cf78e26df7 调整菜单问题 2023-05-26 17:26:31 +08:00
vine_liutk cfb53e9096 调整 2023-05-26 17:22:54 +08:00
vine_liutk c96c4b5c85 禁止添加 2023-05-26 17:17:58 +08:00
vine_liutk 187086b50e 禁止添加 2023-05-26 17:15:23 +08:00
vine_liutk 01668c19a5 调整 2023-05-26 17:08:59 +08:00
vine_liutk ff572e40de 调整基地详情弹窗样式 2023-05-26 11:01:26 +08:00
vine_liutk fb22e5f045 调整样式 2023-05-26 10:44:55 +08:00
vine_liutk 9173252975 Merge branch 'develop' of https://gitea.hmily.club/liutk/internet-everythings-agricultural into develop 2023-05-25 21:23:06 +08:00
panliang 5eab5450d8 region 2023-05-25 19:09:53 +08:00
panliang 0d735f631a 1 2023-05-25 16:41:21 +08:00
panliang 5811fdfb0e 1 2023-05-25 16:32:45 +08:00
panliang b8b4762539 1 2023-05-25 16:16:50 +08:00
panliang 95855a2ba7 resources 2023-05-25 16:12:58 +08:00
vine_liutk 172c6fe895 调整气象图 2023-05-24 12:27:21 +08:00
vine_liutk c5f6578377 调整 2023-05-23 15:55:34 +08:00
vine_liutk fc5213a00b 添加尺寸比例裁剪 2023-05-22 12:43:23 +08:00
vine_liutk 8dea83e32a 处理未设置规则时候的情况 2023-05-22 12:19:00 +08:00
vine_liutk d77eb42256 补充预警添加记录 2023-05-22 12:17:14 +08:00
vine_liutk e5ddd219f4 调整 2023-05-19 16:34:16 +08:00
vine_liutk e15ea3e8c7 完善预警设置保存 2023-05-19 10:56:31 +08:00
vine_liutk b99678f96e 调整默认预警设置选中或 2023-05-18 20:48:39 +08:00
vine_liutk 7853032f81 改变预警设置表单 2023-05-18 19:14:22 +08:00
vine_liutk 1a251a3625 处理bug 2023-05-17 12:12:19 +08:00
vine_liutk 3a9cdc6d9b 完善图 2023-05-17 12:10:45 +08:00
李静 996a0dba68 Update 2023-05-16 12:01:27 +08:00
李静 d4f90262e8 Update 2023-05-16 11:59:00 +08:00
李静 5b69d6758a 生成土壤监控报告 2023-05-16 11:53:48 +08:00
李静 6654d00ee3 优化同步 linkos 历史流水 2023-05-16 11:32:20 +08:00
panliang 2660bd86bc Merge branch 'develop' of gitea.hmily.club:liutk/internet-everythings-agricultural into develop 2023-05-16 11:27:51 +08:00
panliang d97be45086 种植记录id 2023-05-16 11:27:38 +08:00
李静 d701d25b06 优化创建 linkos 设备监控报告流程 2023-05-15 22:36:48 +08:00
vine_liutk 96bb016325 Merge branch 'develop' of https://gitea.hmily.club/liutk/internet-everythings-agricultural into develop 2023-05-12 15:38:10 +08:00
vine_liutk cf8f326515 调整数据结构 2023-05-12 15:38:04 +08:00
李静 418e227866 Fix 2023-05-12 14:15:21 +08:00
李静 a33d981dcc Fix 2023-05-12 13:32:25 +08:00
李静 4b467fa7b0 优化 linkos 设备报告 2023-05-12 13:04:58 +08:00
vine_liutk c01420471d Merge branch 'develop' of https://gitea.hmily.club/liutk/internet-everythings-agricultural into develop 2023-05-12 12:26:38 +08:00
vine_liutk 073f509618 完善监测点位设置设备 2023-05-12 12:26:35 +08:00
李静 91c74b29c3 linkos 气象设备、水质设备每日报告 2023-05-12 12:01:20 +08:00
vine_liutk d07a31741a 处理编辑区域报错问题 2023-05-11 16:05:05 +08:00
vine_liutk 96c4744bd7 调整设备信息查看 2023-05-11 11:38:46 +08:00
vine_liutk 7ffb4048b2 调整标签 2023-05-11 11:16:53 +08:00
vine_liutk f0312e34f0 调整报错 2023-05-10 21:40:15 +08:00
vine_liutk ce8ee04df9 处理冲突 2023-05-10 21:37:55 +08:00
vine_liutk d1a48b3f45 完善样式 2023-05-10 21:36:51 +08:00
vine_liutk f5e1a5698e 暂存 2023-05-10 19:06:01 +08:00
panliang 7281a51b22 timestamp null 2023-05-09 20:24:22 +08:00
panliang 4745c1dff4 region pagination 2023-05-09 20:16:14 +08:00
panliang cbf27302e4 api region harvest 2023-05-09 19:38:52 +08:00
panliang d1b8d88a5e merge app/Models/Region.php 2023-05-09 19:08:59 +08:00
panliang 041a588023 api region 2023-05-09 19:07:47 +08:00
vine_liutk 3968270cf3 补充实验田点击打开 2023-05-09 19:05:04 +08:00
vine_liutk 51dd0f4e1f 大致封装echart-config 2023-05-09 18:19:31 +08:00
vine_liutk 4f92f3017c 完善种植详情 2023-05-09 16:23:10 +08:00
panliang 0fd8af5a24 update index.html 2023-05-08 22:20:38 +08:00
panliang 7d120b8443 com 2023-05-08 22:15:44 +08:00
panliang 892bc642e0 ModelNotFoundException 2023-05-08 21:43:02 +08:00
panliang d40af05598 editor 2023-05-08 21:34:27 +08:00
panliang c4c8832a72 editor 2023-05-08 21:32:03 +08:00
vine_liutk 08202b451f 调整文件名称后报错 2023-05-08 18:32:37 +08:00
vine_liutk 4c3904f38f Merge branch 'develop' of https://gitea.hmily.club/liutk/internet-everythings-agricultural into develop 2023-05-08 18:23:04 +08:00
vine_liutk 94883ec937 调整收获记录 2023-05-08 18:22:59 +08:00
李静 bd6fdcecdb 优化设备报监控告生成脚本 2023-05-08 17:28:58 +08:00
vine_liutk 9eac19eafe 调整文件命名 2023-05-08 17:15:47 +08:00
vine_liutk 8dc26a1b30 Merge branch 'develop' of https://gitea.hmily.club/liutk/internet-everythings-agricultural into develop 2023-05-08 17:06:49 +08:00
vine_liutk e0f73aff5d 完善种植记录添加删除修改 2023-05-08 17:06:40 +08:00
李静 d589404415 Update 2023-05-08 13:39:04 +08:00
李静 c4ea661dcc 优化设备流水同步 2023-05-08 12:11:23 +08:00
vine_liutk 2ed2ce6968 Merge branch 'develop' of https://gitea.hmily.club/liutk/internet-everythings-agricultural into develop 2023-05-08 10:25:10 +08:00
李静 015e10682f 设备监控报告 2023-05-06 18:02:02 +08:00
李静 3d07fde008 同步 linkos 设备历史数据时,区分土壤监控数据和气象监控数据 2023-05-06 16:16:21 +08:00
李静 3fcbe33ddc 优化设备数据同步命令 2023-05-06 14:18:52 +08:00
李静 c551bcafeb 修复 SQL 查询 N+1 2023-05-06 10:16:38 +08:00
李静 4646f1fd16 优化设备数据同步 2023-05-05 17:21:05 +08:00
李静 3f43e9953f 设备数据同步命令 2023-05-05 16:28:15 +08:00
panliang 180f2e070d 视频监控 2023-05-05 15:12:15 +08:00
panliang d1a14556f2 setting 2023-05-05 11:58:10 +08:00
vine_liutk fa9d5456f2 注释友情链接菜单 2023-05-05 11:37:45 +08:00
panliang 2ab9c49655 banner seeder 2023-05-05 11:32:53 +08:00
vine_liutk 60fa9f32f2 调整菜单seeder 2023-05-04 18:19:12 +08:00
vine_liutk 1dfee8c501 调整 2023-05-04 18:13:18 +08:00
vine_liutk c8831a3c66 调整pannel-txt的颜色 2023-05-04 17:44:39 +08:00
vine_liutk 0d48e59c15 Merge branch 'develop' of https://gitea.hmily.club/liutk/internet-everythings-agricultural into develop 2023-05-04 17:18:35 +08:00
vine_liutk 0d177eb289 完善设备和试验田管理 2023-05-04 17:18:26 +08:00
李静 9ec299c490 Linkos 回调日志 2023-05-04 13:48:59 +08:00
李静 177b2cee24 修复 Linkos 设备上线、离线通知 2023-05-04 11:30:40 +08:00
vine_liutk 8f8d5ff8ef 完善设备管理 2023-05-04 10:50:16 +08:00
panliang cfbd28918d Exceptions/Handler 2023-04-29 17:06:34 +08:00
panliang 22d9248cd3 Merge branch 'develop' of gitea.hmily.club:liutk/internet-everythings-agricultural into develop 2023-04-29 16:51:50 +08:00
panliang cf11df2bf4 1 2023-04-29 16:51:34 +08:00
李静 7b641539cd linkos 设备离线、上线通知通知 2023-04-28 17:40:20 +08:00
李静 0eef056851 设备日志 2023-04-28 16:56:38 +08:00
李静 f2a1be3adc Update linkos http client 2023-04-28 14:06:35 +08:00
李静 235fbeafe6 Update linkos http client 2023-04-28 14:02:52 +08:00
panliang d226abe231 admin-notice 2023-04-28 13:03:42 +08:00
李静 3d1d58ac61 LinkOS Http 客户端 2023-04-28 11:16:54 +08:00
vine_liutk 80ca23c2ed Merge branch 'develop' of https://gitea.hmily.club/liutk/internet-everythings-agricultural into develop 2023-04-27 16:41:11 +08:00
vine_liutk a61e625f10 更新版本,调整时间选择器英文问题 2023-04-27 16:41:03 +08:00
panliang 9fbf167a0b dialog 2023-04-27 16:04:57 +08:00
panliang df2f45a26c add ArticleFactory 2023-04-27 15:49:51 +08:00
panliang f8389d8d99 api 2023-04-27 15:27:12 +08:00
panliang 7baf63523d banner 2023-04-27 13:09:25 +08:00
panliang b67e8f04af banner-place 2023-04-27 12:31:28 +08:00
panliang d675db43a3 Merge branch 'develop' of gitea.hmily.club:liutk/internet-everythings-agricultural into develop 2023-04-27 11:38:26 +08:00
panliang 0fae071c1b admin-notice 2023-04-27 11:38:11 +08:00
vine_liutk 87b76c3311 Merge branch 'develop' of https://gitea.hmily.club/liutk/internet-everythings-agricultural into develop 2023-04-27 10:50:36 +08:00
vine_liutk ce68167927 添加农机设备跳转 2023-04-27 10:50:27 +08:00
panliang 994e0c7895 article 2023-04-27 09:55:13 +08:00
vine_liutk 0ce6575106 简单封装enable 2023-04-26 20:35:21 +08:00
panliang 8c38cfd497 article filter 2023-04-26 18:54:40 +08:00
panliang 4995c1c367 article todo 2023-04-26 17:53:50 +08:00
panliang 21cd0da1a7 Merge branch 'develop' of gitea.hmily.club:liutk/internet-everythings-agricultural into develop 2023-04-26 16:19:40 +08:00
panliang 24a19ccfd7 article-caetgory 2023-04-26 16:19:23 +08:00
vine_liutk e7ab16e694 去掉status状态字段 2023-04-26 12:33:16 +08:00
vine_liutk aea7d202e6 处理冲突 2023-04-26 12:31:43 +08:00
vine_liutk 48337e440f 更新样式以及实验田报错 2023-04-26 12:31:00 +08:00
panliang 26a9d2458c merge 2023-04-26 11:16:25 +08:00
panliang 64ed17e654 article caetgory 2023-04-26 11:15:27 +08:00
vine_liutk 854f53e1d2 调整service 2023-04-26 11:14:58 +08:00
vine_liutk 1a1550ad56 添加父级选项 2023-04-26 11:00:57 +08:00
panliang 0e0fb98bd2 Merge branch 'develop' of gitea.hmily.club:liutk/internet-everythings-agricultural into develop 2023-04-26 10:36:44 +08:00
panliang e3c3ca0fed article category 2023-04-26 10:36:41 +08:00
vine_liutk 37e119b8e2 添加行内修改示例 2023-04-26 10:36:26 +08:00
panliang 9704ac8520 Merge branch 'develop' of gitea.hmily.club:liutk/internet-everythings-agricultural into develop 2023-04-26 10:28:51 +08:00
panliang f7a908c593 article 2023-04-26 10:28:42 +08:00
vine_liutk 8abbca243c Merge branch 'develop' of https://gitea.hmily.club/liutk/internet-everythings-agricultural into develop 2023-04-26 10:14:03 +08:00
vine_liutk 412c97bd7a 添加搜索示例 2023-04-26 10:13:56 +08:00
panliang 16443e49e0 add AdminSeeder 2023-04-25 11:41:06 +08:00
panliang 15ed5a1a77 merge 2023-04-25 11:40:40 +08:00
panliang 91cf0dfcc8 admin init 2023-04-25 11:39:37 +08:00
vine_liutk 16aae8cfa2 调整 2023-04-25 09:31:11 +08:00
vine_liutk 27cb9a7d52 调整bug 2023-04-17 17:13:22 +08:00
vine_liutk 4904ef811e 调整样式 2023-04-17 17:09:09 +08:00
vine_liutk 980a3e8d57 更新样式 2023-04-13 22:56:17 +08:00
vine_liutk 2dc87694db 完善农作物种植记录样式补全 2023-04-12 11:36:30 +08:00
vine_liutk 65c2edc5a2 处理监控显示 2023-03-31 15:44:16 +08:00
vine_liutk ac066bacf9 调整种植记录 2023-03-28 18:56:05 +08:00
vine_liutk ec9f9acad9 调整设备设置表单 2023-03-28 14:55:11 +08:00
vine_liutk ed5591710d 调整 2023-03-27 17:18:30 +08:00
vine_liutk f83d869e57 调整报错 2023-03-27 17:01:37 +08:00
vine_liutk 36262e6295 调整报错 2023-03-27 16:59:48 +08:00
vine_liutk 84cdc61cda 调整 2023-03-27 16:57:08 +08:00
vine_liutk c7d2d8fcd7 添加喷灌配置 2023-03-27 16:55:57 +08:00
vine_liutk b8ab64b5e4 添加通风设备设置 2023-03-27 16:30:43 +08:00
vine_liutk 75adf7f1c8 处理其他设备数据显示 2023-03-24 20:12:53 +08:00
vine_liutk 7ef12f611e 调整稻田和育秧的菜单 2023-03-22 18:44:50 +08:00
vine_liutk 278a41e60f 添加基础信息和直播画面详情 2023-03-22 17:10:30 +08:00
vine_liutk 96fbe56898 添加负责人字段 2023-03-21 18:07:03 +08:00
vine_liutk 001e78a111 补充实验田基础模块样式 2023-03-21 17:46:30 +08:00
vine_liutk 4b2d6f95ac 添加设备样式处理 2023-03-20 17:38:55 +08:00
vine_liutk 9489c3ea3a 添加友情链接样式 2023-03-20 16:19:28 +08:00
vine_liutk c7c3c9cd39 添加图片管理样式 2023-03-20 15:59:08 +08:00
vine_liutk 7adb8316e1 添加文章管理样式 2023-03-20 15:35:19 +08:00
vine_liutk dddff27e78 添加格式化时间 2023-03-20 11:40:30 +08:00
vine_liutk b8fbc8394a 添加公告基础样式 2023-03-20 11:36:30 +08:00
1851 changed files with 34451 additions and 3767534 deletions

View File

@ -4,7 +4,7 @@ APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_CHANNEL=daily
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
@ -17,7 +17,7 @@ DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
FILESYSTEM_DISK=public
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
@ -56,3 +56,5 @@ VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
NONG_JI_HOST = https://jxsam-beta.fjdynamics.com

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ yarn-error.log
/.fleet
/.idea
/.vscode
project.md

View File

@ -1,66 +1,6 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 2000 video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the Laravel [Patreon page](https://patreon.com/taylorotwell).
### Premium Partners
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Cubet Techno Labs](https://cubettech.com)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[Many](https://www.many.co.uk)**
- **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)**
- **[DevSquad](https://devsquad.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[OP.GG](https://op.gg)**
- **[WebReinvent](https://webreinvent.com/?utm_source=laravel&utm_medium=github&utm_campaign=patreon-sponsors)**
- **[Lendio](https://lendio.com)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
- `cp .env.example .env`
- `php artisan key:generate`
- `php artisan storage:link`
- 创建数据库
- 修改 .env 文件中的配置
- `php artisan migrate --seed`

View File

@ -2,17 +2,217 @@
namespace App\Admin;
use App\Models\Keyword;
use Slowlyo\OwlAdmin\Renderers\BaseRenderer;
use Slowlyo\OwlAdmin\Renderers\WangEditor;
class Components extends BaseRenderer {
public function parentControl($apiUrl = '', $name ='parent_id', $labelField = 'name', $valueField = 'id')
/**
* 父级选择器
*/
public function parentControl($apiUrl = '', $name ='parent_id', $label ='父级', $labelField = 'name', $valueField = 'id')
{
return amisMake()->TreeSelectControl()
->name('parent_id')->label('父级')
->name($name)->label($label)
->showIcon(false)
->labelField($labelField)
->valueField($valueField)
->value(0)->source($apiUrl);
->valueField($valueField)->source($apiUrl);
}
/**
* 排序字段
*/
public function sortControl($name ='sort', $label = '排序'){
return amisMake()->NumberControl()
->name($name)->label($label)
->displayMode('enhance')
->value(0)
->min(0);
}
/**
* 2位小数输入框
*/
public function decimalControl($name ='decimal', $label = '数值'){
return amisMake()->NumberControl()
->name($name)->label($label)
->kilobitSeparator(true)
->percision(2)
->step(0.01)
->value(0.00)
->min(0);
}
/**
* 富文本编辑器
*/
public function fuEditorControl($name ='content', $label = '内容')
{
return WangEditor::make()
->name($name)
->label($label)
->height('auto')
->excludeKeys(['group-video']);
}
public function enableControl($name = 'is_enable', $label= '状态', $mode = 'horizontal'){
return amisMake()->SwitchControl()
->name($name)->label($label)
->mode($mode)
->onText(__('admin.extensions.status_map.enabled'))->offText(__('admin.extensions.status_map.disabled'));
}
public function keywordsTagControl($name = 'tags', $label= '标签', $typeKey = ''){
return amisMake()->TagControl()
->name($name)->label($label)->maxTagLength(0)
->options(Keyword::getByParentKey($typeKey)->pluck('name', 'id')->toArray());
}
public function keywordsTag($label = '标签'){
return amisMake()->Tag()->label($label)->displayMode('rounded')->color('inactive');
}
/**
* 生成统计图config
* 折线图或者柱状图
*/
public function chartLineBarConfig($title = '', array $x , array $y){
$yAxisData = [];
$seriesData = [];
$color = [];
if(!isset($y[0])){
$_y = $y;
$y = [0=>$_y];
}
$i = 0;
$tips = '{b0}';
foreach($y as $item) {
//调色盘
$color[] = $item['color'];
//tips
$tips.= '<br/> {a'.$i.'}: {c'.$i.'}'.($item['unit'] ?? '');
//纵坐标
$yAxisData[] = [
'name'=>($item['unit'] ?? ''),
'type' =>'value',
'axisTick' => true,
'alignTicks' => true,
'axisLine' => [
'show' => true,
'lineStyle' => [
'color' => $item['color'] ?? ''
]
],
'axisLabel'=> [
'formatter'=>'{value} '
]
];
//数据
$_series = [
'name' => $item['name'] ?? '',
'data' => $item['data'] ?? [],
'type' => $item['type'] ?? 'line',
'yAxisIndex' => $i,
];
switch($item['type']){
case 'line':
$_series = array_merge($_series, [
'smooth'=> true,
'symbol'=> 'none',
'lineStyle' => [
'color' => $item['color'] ?? ''
],
'areaStyle' => [
'color' => $item['color'] ?? ''
],
]);
break;
case 'bar':
$_series = array_merge($_series, [
]);
break;
}
$seriesData[] = $_series;
$i++;
}
return [
'color' => $color,
'title' => [
'text' => $title,
],
"tooltip" => [//提示
'trigger'=>'axis',//坐标轴触发
'axisPointer' => [
'type' => 'cross'
],
// 'formatter' => $tips
],
'grid' => [
'left' => '8%',
'right' => '8%',
],
'xAxis' => [
'type' => 'category',
'data' => $x,
],
'yAxis' => $yAxisData,
'series' => $seriesData
];
}
/**
* 散点图
*/
public function chartScatterConfig($title = '', array $x , array $y, array $yData = null){
$yAxisData = [];
$seriesData = [];
$color = [];
if($yData){
$yAxisData = [
'type' =>'category',
// 'splitLine'=>[
// 'show'=>true,
// 'lineStyle'=>[
// 'type'=>'dashed'
// ]
// ],
'axisTick' => [
'alignWithLabel'=>true
],
'data'=> $yData
];
}
$seriesData = $y;
return [
'color' => $color,
'title' => [
'text' => $title,
],
"tooltip" => [//提示
'trigger'=>'axis',//坐标轴触发
'axisPointer' => [
'type' => 'cross'
],
],
'xAxis' => [
'type' => 'category',
'data' => $x,
],
'yAxis' => $yAxisData,
'series' => $seriesData
];
}
/**
* 生成饼状图config
* -todo
*/
public function chartPieConfig(){
return ;
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Renderers\{Form, Page};
use Slowlyo\OwlAdmin\Renderers\{Component, Button, TableColumn, TextControl, SelectControl, DateTimeControl, SwitchControl, Tabs, Tab, Status, Html};
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\AdminNoticeService;
use App\Admin\Components;
class AdminNoticeController extends AdminController
{
protected string $serviceName = AdminNoticeService::class;
public function list(): Page
{
$crud = $this->baseCRUD()
->filterTogglable(false)
->headerToolbar([
$this->createButton(true, 'lg'),
...$this->baseHeaderToolBar(),
])
->filter($this->baseFilter()->actions([])->body([
TextControl::make()->name('title')->label(__('admin-notice.title'))->size('md'),
Button::make()->label(__('admin.reset'))->actionType('clear-and-submit'),
Component::make()->setType('submit')->label(__('admin.search'))->level('primary'),
]))
->quickSaveItemApi(admin_url('quick-edit/admin-notices/$id'))
->columns([
TableColumn::make()->name('id')->label(__('admin-notice.id')),
TableColumn::make()->name('title')->label(__('admin-notice.title')),
TableColumn::make()->name('article.title')->label(__('admin-notice.article_id')),
TableColumn::make()->name('is_enable')->type('switch')->label(__('admin-notice.is_enable'))->quickEdit(SwitchControl::make()->saveImmediately(true)->mode('inline')),
TableColumn::make()->name('sort')->label(__('admin-notice.sort'))->align('center')->quickEdit(Components::make()->sortControl('sort', __('admin-notice.sort'))->saveImmediately(true)),
TableColumn::make()->name('published_at')->label(__('admin-notice.published_at')),
$this->rowActions(true, 'lg'),
]);
return $this->baseList($crud);
}
public function form(): Form
{
return $this->baseForm()->title('')->body([
TextControl::make()->name('title')->label(__('admin-notice.title'))->required(true),
SelectControl::make()->name('article_id')->label(__('admin-notice.article_id'))->source(admin_url('api/articles/options')),
Components::make()->sortControl('sort', __('admin-notice.sort')),
DateTimeControl::make()->name('published_at')->label(__('admin-notice.published_at'))->value(now())->format('YYYY-MM-DD HH:mm:ss')->description('*不填写则默认为创建时间'),
SwitchControl::make()->name('is_enable')->label(__('admin-notice.is_enable'))->value(true),
Components::make()->fuEditorControl('content', __('admin-notice.content'))
]);
}
public function detail(): Form
{
return $this->baseDetail()->title('')->body([
TextControl::make()->static(true)->name('id')->label(__('admin-notice.id')),
TextControl::make()->static(true)->name('title')->label(__('admin-notice.title')),
TextControl::make()->static(true)->name('article.title')->label(__('admin-notice.article_id')),
TextControl::make()->static(true)->name('sort')->label(__('admin-notice.sort')),
TextControl::make()->name('is_enable')->label(__('admin-notice.is_enable'))->static(true)->staticSchema(Status::make()->source([
['label' => '不显示', 'icon' => 'fa fa-close', 'color' => '#cc292e'],
['label' => '显示', 'icon' => 'fa fa-check', 'color' => '#30bf13'],
])),
TextControl::make()->static(true)->name('published_at')->label(__('admin-notice.published_at')),
TextControl::make()->static(true)->name('created_at')->label(__('admin-notice.created_at')),
Components::make()->fuEditorControl('content', __('admin-notice.content'))->static(true),
]);
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Renderers\Page;
use Slowlyo\OwlAdmin\Renderers\Form;
use Slowlyo\OwlAdmin\Renderers\Operation;
use Slowlyo\OwlAdmin\Services\AdminUserService;
use Slowlyo\OwlAdmin\Services\AdminRoleService;
use Slowlyo\OwlAdmin\Controllers\AdminController;
/**
* @property AdminUserService $service
*/
class AdminUserController extends AdminController
{
protected string $serviceName = AdminUserService::class;
public function list(): Page
{
$crud = $this->baseCRUD()
->headerToolbar([
$this->createButton(true),
...$this->baseHeaderToolBar(),
])
->filter($this->baseFilter()->body(
amisMake()->TextControl('keyword', __('admin.keyword'))
->size('md')
->placeholder(__('admin.admin_user.search_username'))
))
->columns([
amisMake()->TableColumn('id', 'ID')->sortable(),
amisMake()->TableColumn('avatar', __('admin.admin_user.avatar'))->type('avatar')->src('${avatar}'),
amisMake()->TableColumn('username', __('admin.username')),
amisMake()->TableColumn('name', __('admin.admin_user.name')),
amisMake()->TableColumn('roles', __('admin.admin_user.roles'))->type('each')->items(
amisMake()->Tag()->label('${name}')->className('my-1')
),
amisMake()->TableColumn('created_at', __('admin.created_at'))->type('datetime')->sortable(true),
Operation::make()->label(__('admin.actions'))->buttons([
$this->rowEditButton(true),
$this->rowDeleteButton()->visibleOn('${id != 1}'),
]),
]);
return $this->baseList($crud);
}
public function form(): Form
{
return $this->baseForm()->body([
amisMake()->ImageControl('avatar', __('admin.admin_user.avatar'))->receiver($this->uploadImagePath()),
amisMake()->TextControl('username', __('admin.username'))->required(),
amisMake()->TextControl('name', __('admin.admin_user.name'))->required(),
amisMake()->TextControl('password', __('admin.password'))->type('input-password')->required()->validations(['minLength' => 6]),
amisMake()->TextControl('confirm_password', __('admin.confirm_password'))->type('input-password')->required()->validations(['minLength' => 6]),
amisMake()->SelectControl('roles', __('admin.admin_user.roles'))
->searchable()
->multiple()
->labelField('name')
->valueField('id')
->joinValues(false)
->extractValue()
->options(AdminRoleService::make()->query()->get(['id', 'name'])),
]);
}
public function detail(): Form
{
return $this->baseDetail()->body([]);
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Admin\Controllers;
use Illuminate\Http\Request;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\AirLogService;
class AirLogController extends AdminController
{
protected string $serviceName = AirLogService::class;
}

View File

@ -0,0 +1,90 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Renderers\{Form, Page};
use Slowlyo\OwlAdmin\Renderers\{Component, TableColumn, TextControl, Button, Image, Status, NumberControl, SwitchControl, ImageControl};
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\ArticleCategoryService;
use App\Admin\Components;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
class ArticleCategoryController extends AdminController
{
protected string $serviceName = ArticleCategoryService::class;
public function list(): Page
{
$crud = $this->baseCRUD()
->filterTogglable(false)
->loadDataOnce(true)
->footerToolbar([])
->headerToolbar([
$this->createButton(true),
...$this->baseHeaderToolBar(),
])
->filter($this->baseFilter()->actions([])->body([
TextControl::make()->name('name')->label(__('article-category.name'))->size('md'),
Components::make()->parentControl(admin_url('api/article-categories/tree-list'), 'parent_path')->size('lg'),
Button::make()->label(__('admin.reset'))->actionType('clear-and-submit'),
Component::make()->setType('submit')->label(__('admin.search'))->level('primary'),
]))
->quickSaveItemApi(admin_url('quick-edit/article-categories/$id'))
->columns([
TableColumn::make()->name('id')->label(__('article-category.id')),
TableColumn::make()->name('name')->label(__('article-category.name')),
TableColumn::make()->name('icon')->label(__('article-category.icon'))->type('image')->width(60),
TableColumn::make()->name('sort')->label(__('article-category.sort'))->align('center')
->quickEdit(
Components::make()->sortControl('sort', __('article-category.sort'))->saveImmediately(true)),
TableColumn::make()->name('is_enable')->label(__('article-category.is_enable'))->type('switch')
->quickEdit(
Components::make()->enableControl('is_enable', '', 'inline')->saveImmediately(true)
),
$this->rowActions(true),
]);
return $this->baseList($crud);
}
public function form(): Form
{
return $this->baseForm()->title('')->body([
TextControl::make()->name('name')->label(__('article-category.name'))->required(true),
ImageControl::make()->name('icon')->label(__('article-category.name'))->autoUpload(true),
Components::make()->parentControl(admin_url('api/article-categories/tree-list')),
Components::make()->sortControl('sort', __('article-category.sort')),
Components::make()->enableControl()->value(true),
]);
}
public function detail(): Form
{
return $this->baseDetail()->body([
TextControl::make()->name('id')->label(__('article-category.id'))->static(true),
TextControl::make()->name('name')->label(__('article-category.name'))->static(true),
TextControl::make()->name('icon')->label(__('article-category.icon'))->static(true)->staticSchema(Image::make()),
TextControl::make()->name('sort')->label(__('article-category.sort'))->static(true),
TextControl::make()->name('is_enable')->label(__('article-category.is_enable'))->static(true)->staticSchema(Status::make()->source([
['label' => __('admin.extensions.status_map.disabled'), 'icon' => 'fa fa-close', 'color' => '#cc292e'],
['label' => __('admin.extensions.status_map.enabled'), 'icon' => 'fa fa-check', 'color' => '#30bf13'],
])),
TextControl::make()->name('created_at')->label(__('article-category.created_at'))->static(true),
]);
}
public function getTreeList(Request $request)
{
return $this->service->getTree();
}
public function multipleUpdate(Request $request)
{
$diff = $request->input('rowsDiff');
foreach ($diff as $item) {
$this->service->update(data_get($item, 'id'), Arr::except($item, ['id']));
}
return $this->response()->success();
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Renderers\{Form, Page};
use Slowlyo\OwlAdmin\Renderers\{Component, Button, TableColumn, TextControl, Image, ImageControl, DateTimeControl, SwitchControl, Tabs, Tab, Status, Html};
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\ArticleService;
use App\Admin\Components;
class ArticleController extends AdminController
{
protected string $serviceName = ArticleService::class;
public function list(): Page
{
$crud = $this->baseCRUD()
->filterTogglable(false)
->headerToolbar([
$this->createButton(true, 'lg'),
...$this->baseHeaderToolBar(),
])
->filter($this->baseFilter()->actions([])->body([
TextControl::make()->name('title')->label(__('article.title'))->size('md'),
Components::make()->parentControl(admin_url('api/article-categories/tree-list'), 'category_path', __('article.category_id'))->size('lg'),
Button::make()->label(__('admin.reset'))->actionType('clear-and-submit'),
Component::make()->setType('submit')->label(__('admin.search'))->level('primary'),
]))
->quickSaveItemApi(admin_url('quick-edit/article/$id'))
->columns([
TableColumn::make()->name('id')->label(__('article.id'))->sortable(true),
TableColumn::make()->name('title')->label(__('article.title')),
TableColumn::make()->name('category.name')->label(__('article.category_id'))->className('text-primary'),
Image::make()->name('cover')->label(__('article.cover'))->width(100),
TableColumn::make()->name('sub_title')->label(__('article.sub_title')),
TableColumn::make()->name('sort')->label(__('article.sort'))->align('center')->quickEdit(Components::make()->sortControl('sort', __('article-category.sort'))->saveImmediately(true)),
TableColumn::make()->name('is_enable')->type('switch')->label(__('article.is_enable'))->quickEdit(SwitchControl::make()->saveImmediately(true)->mode('inline')),
TableColumn::make()->name('published_at')->label(__('article.published_at')),
$this->rowActions(true, 'lg'),
]);
return $this->baseList($crud);
}
public function form(): Form
{
return $this->baseForm()->title('')->body([
TextControl::make()->name('title')->label(__('article.title'))->required(true),
Components::make()->parentControl(admin_url('api/article-categories/tree-list'), 'category_id', __('article.category_id')),
TextControl::make()->name('sub_title')->label(__('article.sub_title')),
ImageControl::make()->name('cover')->label(__('article.cover'))->autoUpload(true),
Components::make()->sortControl('sort', __('article.sort')),
DateTimeControl::make()->name('published_at')->label(__('article.published_at'))->value(now())->format('YYYY-MM-DD HH:mm:ss')->description('*不填写则默认为创建时间'),
SwitchControl::make()->name('is_enable')->label(__('article.is_enable'))->value(true),
Components::make()->fuEditorControl('content', __('article.content')),
]);
}
public function detail(): Form
{
return $this->baseDetail()->title('')->body([
TextControl::make()->static(true)->name('id')->label(__('article.id')),
TextControl::make()->static(true)->name('title')->label(__('article.title')),
TextControl::make()->static(true)->name('category.name')->label(__('article.category_id')),
TextControl::make()->static(true)->name('sub_title')->label(__('article.sub_title')),
TextControl::make()->name('cover')->label(__('article.cover'))->static(true)->staticSchema(Image::make()),
TextControl::make()->static(true)->name('sort')->label(__('article.sort')),
TextControl::make()->name('is_enable')->label(__('article.is_enable'))->static(true)->staticSchema(Status::make()->source([
['label' => '不显示', 'icon' => 'fa fa-close', 'color' => '#cc292e'],
['label' => '显示', 'icon' => 'fa fa-check', 'color' => '#30bf13'],
])),
TextControl::make()->static(true)->name('published_at')->label(__('article.published_at')),
TextControl::make()->static(true)->name('created_at')->label(__('article.created_at')),
Components::make()->fuEditorControl('content', __('article.content'))->static(true),
]);
}
public function options()
{
$list = $this->service->listQuery()->select(['id as value', 'title as label'])->without('category')->get();
return $this->response()->success($list);
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Admin\Controllers;
use Illuminate\Http\Request;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\AtomizingLogService;
class AtomizingLogController extends AdminController
{
protected string $serviceName = AtomizingLogService::class;
}

View File

@ -2,9 +2,77 @@
namespace App\Admin\Controllers;
use Illuminate\Support\Arr;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Hash;
use Slowlyo\OwlAdmin\Controllers\AuthController as AdminAuthController;
use Slowlyo\OwlAdmin\Renderers\{Page, Form, TextControl, ImageControl};
class AuthController extends AdminAuthController
{
public function userSetting(): \Illuminate\Http\JsonResponse|\Illuminate\Http\Resources\Json\JsonResource
{
$user = $this->user();
$form = Form::make()
->title()
->panelClassName('px-48 m:px-0')
->mode('horizontal')
->data(Arr::only($user->toArray(), ['avatar', 'name']))
->api('put:' . admin_url('/user_setting'))
// ->persistDataKeys(['avatar', 'name'])
// ->resetAfterSubmit()
->body([
ImageControl::make()
->label(__('admin.admin_user.avatar'))
->name('avatar')
->receiver($this->uploadImagePath()),
TextControl::make()
->label(__('admin.admin_user.name'))
->name('name')
->required(),
TextControl::make()
->type('input-password')
->label(__('admin.old_password'))
->name('old_password'),
TextControl::make()
->type('input-password')
->label('新密码')
->name('password')
->validations(['minLength' => 6])
->requiredOn('!!this.old_password'),
TextControl::make()
->type('input-password')
->label(__('admin.confirm_password'))
->name('confirm_password')
->validations(['minLength' => 6])
->requiredOn('!!this.old_password'),
]);
return $this->response()->success(Page::make()->body($form));
}
public function saveUserSetting(): \Illuminate\Http\JsonResponse|\Illuminate\Http\Resources\Json\JsonResource
{
$request = request();
$request->validate([
'old_password' => ['nullable', 'current_password:sanctum', Rule::requiredIf($request->filled(['password', 'confirm_password']))],
'password' => Rule::requiredIf($request->filled('old_password')),
'confirm_password' => [Rule::requiredIf($request->filled('old_password')), 'same:password'],
], [
'old_password.required' => '原密码必填',
'password.required' => '新密码必填',
'confirm_password.required' => '确认密码必填',
'old_password.current_password' => __('admin.admin_user.old_password_error'),
'confirm_password.same' => __('admin.admin_user.password_confirmation'),
]);
$user = $this->user();
$attributes = $request->only(['avatar', 'name']);
if ($request->filled('old_password')) {
$attributes['password']
= Hash::make($request->input('password'));
}
$user->update($attributes);
return $this->response()->successMessage(__('admin.action_success'));
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Renderers\{Page, Form};
use Slowlyo\OwlAdmin\Renderers\{Component, TableColumn, TextControl, Image, Button, ImageControl, SelectControl, SwitchControl, DateTimeControl, InputKV, Status, Json};
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\BannerService;
use App\Admin\Components;
class BannerController extends AdminController
{
protected string $serviceName = BannerService::class;
public function list(): Page
{
$crud = $this->baseCRUD()
->filterTogglable(false)
->headerToolbar([
$this->createButton(true, 'lg'),
...$this->baseHeaderToolBar(),
])
->quickSaveItemApi(admin_url('quick-edit/banners/$id'))
->filter($this->baseFilter()->actions([])->body([
SelectControl::make()->name('place_id')->label(__('banner.place_id'))->source(admin_url('api/banner-places/options'))->size('md'),
TextControl::make()->name('name')->label(__('banner.name'))->size('md'),
Button::make()->label(__('admin.reset'))->actionType('clear-and-submit'),
Component::make()->setType('submit')->label(__('admin.search'))->level('primary'),
]))
->columns([
TableColumn::make()->name('place.name')->label(__('banner.place_id')),
TableColumn::make()->name('name')->label(__('banner.name')),
Image::make()->name('picture')->label(__('banner.picture'))->width(100),
TableColumn::make()->name('sort')->label(__('banner.sort'))->align('center')->quickEdit(Components::make()->sortControl('sort', __('article-category.sort'))->saveImmediately(true)),
TableColumn::make()->name('is_enable')->label(__('banner.is_enable'))->type('switch')->quickEdit(SwitchControl::make()->mode('inline')->saveImmediately(true)),
$this->rowActions(true, 'lg'),
]);
return $this->baseList($crud);
}
public function form(): Form
{
return $this->baseForm()->title('')->body([
SelectControl::make()->name('place_id')->label(__('banner.place_id'))->source(admin_url('api/banner-places/options'))->required(true),
TextControl::make()->name('name')->label(__('banner.name'))->required(true),
ImageControl::make()->name('picture')->label(__('banner.picture'))->autoUpload(true)->required(true),
InputKV::make()->name('link_config')->label(__('banner.link_config'))->draggable(false),
Components::make()->sortControl('sort', __('banner.sort')),
DateTimeControl::make()->name('published_at')->label(__('banner.published_at'))->value(now())->format('YYYY-MM-DD HH:mm:ss')->description('*不填写则默认为创建时间'),
SwitchControl::make()->name('is_enable')->label(__('banner.is_enable'))->value(true),
]);
}
public function detail(): Form
{
return $this->baseDetail()->title('')->body([
TextControl::make()->name('place.name')->label(__('banner.place_id'))->static(true),
TextControl::make()->name('name')->label(__('banner.name'))->static(true),
TextControl::make()->name('picture')->label(__('banner.picture'))->static(true)->staticSchema(Image::make()),
TextControl::make()->name('link_config')->label(__('banner.link_config'))->static(true)->staticSchema(Json::make()),
TextControl::make()->name('sort')->label(__('banner.sort'))->static(true),
TextControl::make()->name('published_at')->label(__('banner.published_at'))->static(true),
TextControl::make()->name('is_enable')->label(__('banner.is_enable'))->static(true)->staticSchema(Status::make()->source([
['label' => '不显示', 'icon' => 'fa fa-close', 'color' => '#cc292e'],
['label' => '显示', 'icon' => 'fa fa-check', 'color' => '#30bf13'],
])),
TextControl::make()->name('created_at')->label(__('banner.created_at'))->static(true),
]);
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Renderers\{Form, Page};
use Slowlyo\OwlAdmin\Renderers\{TableColumn, TextControl, SwitchControl, NumberControl, Status, Button, Component};
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\BannerPlaceService;
class BannerPlaceController extends AdminController
{
protected string $serviceName = BannerPlaceService::class;
protected string $pageTitle = '图片位置';
public function list(): Page
{
$crud = $this->baseCRUD()
->filterTogglable(false)
->headerToolbar([
$this->createButton(true),
...$this->baseHeaderToolBar(),
])
->quickSaveItemApi(admin_url('quick-edit/banner-places/$id'))
->filter($this->baseFilter()->actions([])->body([
TextControl::make()->name('key')->label(__('banner-place.key'))->size('md'),
TextControl::make()->name('name')->label(__('banner-place.name'))->size('md'),
Button::make()->label(__('admin.reset'))->actionType('clear-and-submit'),
Component::make()->setType('submit')->label(__('admin.search'))->level('primary'),
]))
->columns([
TableColumn::make()->name('id')->label(__('banner-place.id')),
TableColumn::make()->name('key')->label(__('banner-place.key')),
TableColumn::make()->name('name')->label(__('banner-place.name')),
TableColumn::make()->name('is_enable')->type('switch')->label(__('banner-place.is_enable'))->quickEdit(SwitchControl::make()->saveImmediately(true)->mode('inline')),
TableColumn::make()->name('remark')->label(__('banner-place.remark'))->quickEdit(TextControl::make()->saveImmediately(true)),
$this->rowActions(true),
]);
return $this->baseList($crud);
}
public function form(): Form
{
return $this->baseForm()->title('')->body([
TextControl::make()->name('key')->label('KEY')->required(true),
TextControl::make()->name('name')->label('名称')->required(true),
NumberControl::make()->name('width')->label(__('banner-place.width'))->step(1)->min(0),
NumberControl::make()->name('height')->label(__('banner-place.height'))->step(1)->min(0),
SwitchControl::make()->name('is_enable')->label(__('banner-place.is_enable'))->value(true),
TextControl::make()->name('remark')->label('备注'),
]);
}
public function detail(): Form
{
return $this->baseDetail()->body([
TextControl::make()->static(true)->name('id')->label(__('banner-place.id')),
TextControl::make()->static(true)->name('name')->label(__('banner-place.name')),
TextControl::make()->static(true)->name('width')->label(__('banner-place.width')),
TextControl::make()->static(true)->name('height')->label(__('banner-place.height')),
TextControl::make()->name('is_enable')->label(__('banner-place.is_enable'))->static(true)->staticSchema(Status::make()->source([
['label' => '不显示', 'icon' => 'fa fa-close', 'color' => '#cc292e'],
['label' => '显示', 'icon' => 'fa fa-check', 'color' => '#30bf13'],
])),
TextControl::make()->static(true)->name('remark')->label(__('banner-place.remark')),
TextControl::make()->static(true)->name('created_at')->label(__('banner-place.created_at')),
]);
}
public function options()
{
$list = $this->service->listQuery()->select(['id as value', 'name as label'])->get();
return $this->response()->success($list);
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Admin\Controllers;
use Illuminate\Http\Request;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\CropHarvestService;
class CropHarvestController extends AdminController
{
protected string $serviceName = CropHarvestService::class;
}

View File

@ -0,0 +1,170 @@
<?php
namespace App\Admin\Controllers;
use Illuminate\Http\Request;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use Slowlyo\OwlAdmin\Renderers\DialogAction;
use Slowlyo\OwlAdmin\Renderers\Dialog;
use App\Services\Admin\CropPlantService;
use App\Admin\Components;
use App\Models\RegionPlantLog;
class CropPlantController extends AdminController
{
protected string $serviceName = CropPlantService::class;
public function plantDetail(Request $request)
{
$id = $request->id;
$plant = RegionPlantLog::find($id);
$page = $this->basePage()->body([
amisMake()->Grid()->columns([
amisMake()->Wrapper()->sm(6)->body([
amisMake()->Panel()->title('种植详情')
->subFormMode('horizontal')
->labelWidth(100)
->body([
\amisMake()->TextControl()->static(true)->name('plant_name')->label('作物名称')->value($plant->plant_name),
\amisMake()->TextControl()->static(true)->name('director')->label('负责人')->value($plant->director),
\amisMake()->TextControl()->static(true)->name('area')->label('种植面积m²')->value($plant->area),
\amisMake()->TextControl()->static(true)->name('start_at')->label('开始时间')->value($plant->start_at),
\amisMake()->TextControl()->static(true)->name('end_at')->label('结束时间')->value($plant->end_at),
]),
amisMake()->Panel()->title('收获记录')
->body([
DialogAction::make()->className('absolute top-1 right-4')->label(__('admin.create'))->dialog(
Dialog::make()->title('收获记录')->body($this->harvestCreateForm($plant))->reload('plantDetail')
)->reload('plant_detail'),
\amisMake()->CRUDTable()
->api(admin_url('crop-harvestes').'?_action=getData&plant_id='.$plant->id)
->title('')
->columns([
amisMake()->TableColumn()->name('director')->label('负责人'),
amisMake()->TableColumn()->name('area')->label('收获面积m²'),
amisMake()->TableColumn()->name('output')->label('收获产量kg'),
amisMake()->TableColumn()->name('harvest_at')->label('收获时间'),
amisMake()->Operation()->label(__('admin.actions'))->buttons([
\amisMake()
->DialogAction()
->label('编辑')
->level('link')
->dialog(Dialog::make()->title('编辑种植记录')->body($this->harvestEditForm())),
\amisMake()
->AjaxAction()
->label('删除')
->level('link')
->actionType('ajax')
->confirmText(__('admin.confirm_delete'))
->api([
'method' => 'delete',
'url' => admin_url('crop-harvestes/${id}')
])
]),
])
]),
]),
amisMake()->Wrapper()->sm(6)->body([
\amisMake()->grid()->columns([
\amisMake()->Form()->title('搜索条件')->mode('inline')->body([
amisMake()->DateControl()->name('start_at')->format('YYYY-MM-DD')->label('开始时间'),
amisMake()->DateControl()->name('end_at')->format('YYYY-MM-DD')->label('结束时间'),
amis('submit')->label(__('admin.search'))->level('primary'),
])->target('plant_harvest_chart'),
]),
amisMake()->Card()->className('m-r')->body(
amisMake()->Chart()->name('plant_harvest_chart')->api(admin_url('crop-plant-harveste-chart?plant_id=${id}&start_at=${start_at}&end_at=${end_at}'))->debug(true),
),
]),
])
]);
return $this->response()->success($page);
}
public function plantHarvestChart(Request $request)
{
$data = [];
$plant = RegionPlantLog::find($request->plant_id);
$startAt = $request->start_at;
$endAt = $request->end_at;
if($plant){
$query = $plant->harvestes();
if($startAt){
$query->where('harvest_at', '>=', $startAt);
}
if($endAt){
$query->where('harvest_at', '<=', $endAt);
}
$harvestes = $query->get()->sortBy('harvest_at');
$times = $harvestes->pluck('harvest_at')->map(function($item, $key){
return $item->format('Y-m-d');
})->toArray();
$areas = $harvestes->pluck('area')->toArray();
$outputs = $harvestes->pluck('output')->toArray();
$data = $this->plantHarvestChartConfig($times, $areas, $outputs);
}
return $this->response()->success($data);
}
private function harvestCreateForm(RegionPlantLog $plant = null) {
return amisMake()->Form()
->api([
'method'=>'post',
'url'=>admin_url('crop-harvestes'),
"data" => [
'plant_id'=>'${plant_id}',
'director'=>'${harbest_director}',
'area'=>'${harbest_area}',
'output'=>'${output}',
'harvest_at'=>'${harvest_at}',
'is_last'=>'${is_last}',
],
])
->body([
\amisMake()->TextControl()->name('plant_id')->label('计划ID')->hidden(true)->value($plant?->id ?? 0),
\amisMake()->TextControl()->name('harbest_director')->label('负责人')->required(true),
Components::make()->decimalControl('harbest_area','收获面积m²')->required(true),
Components::make()->decimalControl('output','收获产量kg')->required(true),
\amisMake()->DateControl()->name('harvest_at')->format('YYYY-MM-DD')->label('收获时间')->required(true),
amisMake()->SwitchControl()->name('is_last')->label('最后一次收获')->description('该次收获是否是此次种植最后一次?')
]);
}
private function harvestEditForm() {
return amisMake()->Form()
->api([
'method'=>'put',
'url'=>admin_url('crop-harvestes/${id}'),
])->initApi(admin_url('crop-harvestes/${id}/edit').'?_action=getData')
->body([
\amisMake()->TextControl()->name('plant_id')->label('计划ID')->hidden(true),
\amisMake()->TextControl()->name('director')->label('负责人')->required(true),
Components::make()->decimalControl('area','收获面积m²')->required(true),
Components::make()->decimalControl('output','收获产量kg')->required(true),
\amisMake()->DateControl()->name('harvest_at')->format('YYYY-MM-DD')->label('收获时间')->required(true),
]);
}
private function plantHarvestChartConfig(array $times, array $areas, array $outputs)
{
return Components::make()->chartLineBarConfig('种植情况', $times, [
[
'name'=> '种植面积',
'type' => 'line',
'data' => $areas,
'color' => '#91CC75',
'unit' => 'm²'
],
[
'name'=> '产量',
'type'=>'bar',
'data'=> $outputs,
'color' => '#5470C6',
'unit' => 'kg'
]
]);
}
}

View File

@ -0,0 +1,199 @@
<?php
namespace App\Admin\Controllers;
use Illuminate\Http\Request;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Admin\Components;
use App\Models\Region;
use Slowlyo\OwlAdmin\Renderers\DialogAction;
use Slowlyo\OwlAdmin\Renderers\Dialog;
use Slowlyo\OwlAdmin\Renderers\Image;
use Slowlyo\OwlAdmin\Renderers\TextControl;
class CustomRegionController extends AdminController
{
protected string $queryPath = 'custom-region';
public function regionIndex($type)
{
switch($type){
case 'yuyang':
// $this->pageTitle = '育秧列表';
$categoryId = 1;
break;
case 'daotian':
// $this->pageTitle = '稻田列表';
$categoryId = 2;
break;
}
$page = $this->basePage()->body([
\amisMake()->GridNav()->options(
$this->regionList($categoryId)
)
]);
return $this->response()->success($page);
}
private function regionList($categoryId)
{
$regionList = Region::with('monitorModes')->where('category_id', $categoryId)->get();
$resList = [];
foreach($regionList as $region){
$tabs = Region::regionTabConfig($region);
$resList[] = [
"icon"=> $region->cover ?: url('default.svg'),
"text"=> $region->name,
'clickAction' => DialogAction::make()->dialog(
Dialog::make()->title($region['name'])->body([
\amisMake()->Tabs()->tabsMode('simple')->tabs($tabs),
])->size('full')->actions([])
),
];
}
return $resList;
}
public function getRegionTabs(Request $request){
$region = Region::find($request->id);
$tabs = Region::regionTabConfig($region);
$page = \amisMake()->Tabs()->tabsMode('simple')->tabs($tabs);
return $this->response()->success($page);
}
public function regionDetail(Request $request)
{
$id = $request->id;
$region = Region::find($id);
$page = $this->basePage()->body([
amisMake()->Grid()->columns([
amisMake()->Wrapper()->sm(4)->body([
amisMake()->Panel()->title('基础详情')
->subFormMode('horizontal')
->labelWidth(100)
->body([
\amisMake()->TextControl()->static(true)->name('name')->label('名称')->value($region?->name),
TextControl::make()->name('cover')->label(__('region.cover'))->static(true)->staticSchema(Image::make()->src($region?->cover)),
\amisMake()->TextControl()->static(true)->name('director')->label('负责人')->value($region->director),
\amisMake()->TextControl()->static(true)->name('category_name')->label('分类')->value($region->category?->name ?? ''),
\amisMake()->TextControl()->static(true)->name('area')->label('面积m²')->value($region?->area ?? ''),
\amisMake()->TextControl()->static(true)->name('sort')->label('排序')->value($region?->sort ?? '0'),
\amisMake()->SwitchControl()->static(true)->label('显示')->value(!!$region->is_enable),
]),
amisMake()->Panel()->title('基础介绍')
->body([
amisMake()->Wrapper()->className('overflow-x-auto')->size('none')->body(
Components::make()->fuEditorControl('content', '')->value($region?->description)->static(true)
),
])
]),
amisMake()->Wrapper()->sm(8)->body([
amisMake()->Panel()->title('种植记录')
->body([
DialogAction::make()->className('absolute top-1 right-4')->label(__('admin.create'))->dialog(
Dialog::make()->title('新增种植记录')->body($this->plantCreateForm($region))
),
\amisMake()->CRUDTable()->affixHeader(false)
->title('')
->api(admin_url('crop-plants').'?_action=getData&region_id='.$region->id)
->columns([
amisMake()->TableColumn()->name('plant_name')->label('名称'),
amisMake()->TableColumn()->name('director')->label('负责人'),
amisMake()->TableColumn()->name('area')->label('种植面积m²'),
amisMake()->TableColumn()->name('start_at')->label('种植时间'),
amisMake()->TableColumn()->name('plant_state')->type('status')->source([
1 => ['label' => '种植中','icon' => 'fa fa-warning','color' => '#ff9326'],
2 => ['label' => '已结束','icon' => 'fa fa-check-circle','color' => '#ffb6b3']
])->label('种植状态'),
amisMake()->Operation()->label(__('admin.actions'))->buttons([
amisMake()
->DialogAction()
->label('编辑')
->level('link')
->dialog(Dialog::make()->title('编辑种植记录')->body($this->plantEditForm())),
amisMake()->AjaxAction()->label('删除')->level('link')
->actionType('ajax')
->confirmText(__('admin.confirm_delete'))
->api([
'method' => 'delete',
'url' => admin_url('crop-plants/${id}')
])
]),
])
->itemAction([
'type'=>'button',
'actionType'=>'dialog',
'dialog'=> \amisMake()->Dialog()->title('${plant_name}')
->size('full')->actions([])->body([
\amisMake()->Tabs()->tabsMode('simple')->name('detailTab')->tabs([
[
'title' => '种植详情',
'value' => 'detail',
'tab'=>\amisMake()->Service()->name('plant_detail')->schemaApi(admin_url('crop-plant-detail?id=${id}')),//传id
'unmountOnExit' => true//每次切换tab都要销毁
],
// [
// 'title' => '报警记录',
// 'value' => 'warning',
// // 'tab'=>\amisMake()->Service()->schemaApi(admin_url('custom-region-detail?id='.$region['id'])),
// 'unmountOnExit' => true//每次切换tab都要销毁
// ],
[
'title' => '虫情记录',
'value' => 'warning',
// 'tab'=>\amisMake()->Service()->schemaApi(admin_url('custom-region-detail?id='.$region['id'])),
'unmountOnExit' => true//每次切换tab都要销毁
],
])
])
])
])
]),
])
]);
return $this->response()->success($page);
}
private function plantCreateForm(Region $region = null){
return amisMake()->Form()
->api([
'method'=>'post',
'url'=>admin_url('crop-plants'),
"data" => [
'region_id'=>'${region_id}',
'plant_name'=>'${plant_name}',
'director'=>'${plant_director}',
'area'=>'${plant_area}',
'start_at'=>'${start_at}',
],
])
->body([
\amisMake()->TextControl()->name('region_id')->label('区域ID')->hidden(true)->value($region?->id ?? 0),
\amisMake()->TextControl()->name('plant_name')->label('作物名称')->required(true),
\amisMake()->TextControl()->name('plant_director')->label('负责人')->required(true),
Components::make()->decimalControl('plant_area','种植面积')->required(true),
\amisMake()->DateControl()->name('start_at')->format('YYYY-MM-DD')->label('种植时间')->required(true),
]);
}
private function plantEditForm(){
return amisMake()->Form()
->api([
'method'=>'put',
'url'=>admin_url('crop-plants/${id}'),
])->initApi(admin_url('crop-plants/${id}/edit').'?_action=getData')
->body([
\amisMake()->TextControl()->name('id')->hidden(true),
\amisMake()->TextControl()->name('region_id')->label('区域ID')->hidden(true),
\amisMake()->TextControl()->name('plant_name')->label('作物名称')->required(true),
\amisMake()->TextControl()->name('director')->label('负责人')->required(true),
Components::make()->decimalControl('area','种植面积')->required(true),
\amisMake()->DateControl()->name('start_at')->format('YYYY-MM-DD')->label('种植时间')->required(true),
\amisMake()->DateControl()->name('end_at')->format('YYYY-MM-DD')->label('结束时间'),
]);
}
}

View File

@ -0,0 +1,489 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Renderers\{Button, Form, Page, TableColumn, TextControl, Json, Component, CRUDTable, Card, Video, InputDatetimeRange, DateTimeControl, Mapping, SelectControl};
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\DeviceService;
use App\Admin\Components;
use App\Models\Device;
use App\Models\Keyword;
use App\Models\MonitorMode;
use App\Models\RegionMonitor;
use App\Models\Region;
use Illuminate\Http\Request;
use App\Services\MqttService;
class DeviceController extends AdminController
{
protected string $serviceName = DeviceService::class;
protected string $pageTitle = '设备管理';
public function options(){
$list = $this->service->listQuery()->select(['id as value', 'name as label'])->get();
return $this->response()->success($list);
}
public function list(): Page
{
$crud = $this->baseCRUD()
->filterTogglable(false)
->headerToolbar([
$this->createButton(true, 'lg'),
...$this->baseHeaderToolBar(),
])
->filter($this->baseFilter()->actions([])->body([
TextControl::make()->name('name')->label('名称')->size('md'),
amisMake()->SelectControl()->name('factory')->label('厂家')->options(Keyword::getByParentKey('device-factory')->pluck('name', 'id')->toArray())->size('md'),
amisMake()->SelectControl()->name('type')->label('类型')->options(Device::typeMap())->size('md'),
Button::make()->label(__('admin.reset'))->actionType('clear-and-submit'),
amis('submit')->label(__('admin.search'))->level('primary'),
]))
->columns([
TableColumn::make()->name('id')->label('ID')->sortable(true),
TableColumn::make()->name('name')->label('名称'),
TableColumn::make()->name('factory.name')->label('厂家'),
TableColumn::make()->name('sn')->label('编号'),
TableColumn::make()->name('type')->type('mapping')->map(Device::typeMap())->label('类型'),
TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true),
TableColumn::make()->name('updated_at')->label('更新时间')->type('datetime')->sortable(true),
$this->rowActions(true, 'lg'),
])->debug(true);
return $this->baseList($crud);
}
public function form(): Form
{
return $this->baseForm()->body([
TextControl::make()->name('name')->label('名称')->required(true),
TextControl::make()->name('sn')->label('设备编号')->required(true),
\amisMake()->SelectControl()->name('powered_by')->label('厂家')->options(Keyword::getByParentKey('device-factory')->pluck('name', 'id')->toArray())->required(true),
TextControl::make()->name('model_sn')->label('型号'),
\amisMake()->RadiosControl()->name('type')->label('类型')->options(Device::typeMap())->required(true),
// 监控设备-额外参数
// rtsp://admin:lcdx12345@172.16.40.2:554/Streaming/Channels/5201
TextControl::make()->name('extends.rtsp_url')->hiddenOn('data.type != '.Device::TYPE_MONITOR)->label(__('device.rtsp_url')),
// rtsp://admin:lcdx12345@172.16.40.2:554/Streaming/tracks/5201
TextControl::make()->name('extends.rtsp_history')->hiddenOn('data.type != '.Device::TYPE_MONITOR)->label(__('device.rtsp_history')),
]);
}
public function detail(): Form
{
return $this->baseDetail()->body([
TextControl::make()->static(true)->name('id')->label('ID'),
TextControl::make()->static(true)->name('name')->label('名称'),
TextControl::make()->static(true)->name('sn')->label('编号'),
TextControl::make()->static(true)->name('factory.name')->label('厂家'),
TextControl::make()->static(true)->name('model_sn')->label('型号'),
TextControl::make()->static(true)->name('type')->label('类型')->staticSchema(
Mapping::make()->map(Device::typeMap())),
TextControl::make()->static(true)->name('extends')->label('扩展信息')->staticSchema(
Json::make()),
TextControl::make()->static(true)->name('created_at')->label('创建时间'),
TextControl::make()->static(true)->name('updated_at')->label('更新时间')
]);
}
/**
* 监控设备列表
*/
public function monitorList()
{
if ($this->actionOfGetData()) {
return $this->response()->success($this->service->list());
}
$regionId = request()->input('region_id', 0);
if($regionId){
$region = Region::find($regionId);
$query = $region->monitorModes()->where('type', MonitorMode::TYPE_MONITOR)->pluck('name','monitor_id');
}else{
$query = MonitorMode::where('type', MonitorMode::TYPE_MONITOR)->pluck('name','id');
}
return CRUDTable::make()
->mode('cards')
->hideCheckToggler()
->columnsCount(3)
->perPage(6)
->affixHeader(false)
->filterTogglable(true)
->set('primaryField', $this->service->primaryKey())
->api(admin_url($this->queryPath . '?_action=getData&_type=' . Device::TYPE_MONITOR))
->footerToolbar(['statistics', 'pagination'])
->headerToolbar([])
->filter($this->baseFilter()->actions([])->body([
amisMake()->SelectControl('monitor_mode', '点位名称')->size('md')->options($query->toArray())->selectFirst(true),
Button::make()->label(__('admin.reset'))->actionType('clear-and-submit'),
Component::make()->setType('submit')->label(__('admin.search'))->level('primary'),
]))
->actions([])
->itemClassName('col-sm-4')
->card(Card::make()->header(['title' => '$name'])->body(Video::make()->videoType('video/x-flv')->muted(true)->autoPlay(true)->src('${src}')));
}
/**
* 监控历史视频
*/
public function monitorVideoList()
{
if ($this->actionOfGetData()) {
return $this->response()->success($this->service->list());
}
$regionId = request()->input('region_id', 0);
if($regionId){
$region = Region::find($regionId);
$query = $region->monitorModes()->where('type', MonitorMode::TYPE_MONITOR)->pluck('name','monitor_id');
}else{
$query = MonitorMode::where('type', MonitorMode::TYPE_MONITOR)->pluck('name','id');
}
return CRUDTable::make()
->mode('cards')
->hideCheckToggler()
->columnsCount(3)
->perPage(6)
->affixHeader(false)
->filterTogglable(true)
->set('primaryField', $this->service->primaryKey())
->api(admin_url($this->queryPath . '?_action=getData&_type=' . Device::TYPE_MONITOR . '&_mode=history'))
->footerToolbar(['statistics', 'pagination'])
->headerToolbar([])
->filter($this->baseFilter()->actions([])->body([
amisMake()->SelectControl('monitor_mode', '点位名称')->size('md')->options($query->toArray())->selectFirst(true),
InputDatetimeRange::make()->name('date')->label('日期')->maxDate('now')->size('md'),
Button::make()->label(__('admin.reset'))->actionType('clear-and-submit'),
Component::make()->setType('submit')->label(__('admin.search'))->level('primary'),
]))
->actions([])
->itemClassName('col-sm-4')
->card(Card::make()->header(['title' => '$name'])->body(Video::make()->videoType('video/x-flv')->muted(true)->autoPlay(true)->src('${src}')));
}
/**
* 气象图表
*/
public function meteorologicalChart(){
$regionId = request()->input('region_id', 0);
if($regionId){
$region = Region::find($regionId);
$options = $region->monitorModes()->where('type', MonitorMode::TYPE_METEOROLOGICAL)->pluck('name','monitor_id')->toArray();
}else{
$options = MonitorMode::where('type', MonitorMode::TYPE_METEOROLOGICAL)->pluck('name','id')->toArray();
}
return $this->basePage()->title('')->body([
\amisMake()->grid()->columns([
\amisMake()->Form()->title('搜索条件')->mode('inline')->body([
amisMake()->SelectControl('monitor_mode', '监测点位')->size('md')->options($options)->selectFirst(true)->required(),
\amisMake()->DateRangeControl('date_range', '时间范围')->clearable(false)->value('today,today')->required(),
amis('submit')->label(__('admin.search'))->level('primary'),
])->target('meteorological_chart'),
]),
amisMake()->Service()->name('meteorological_chart')
->data([
'monitor_mode'=>array_key_first($options),
'date_range'=>mktime(0,0,0,date('m'),date('d'),date('Y')).','.mktime(0,0,0,date('m'),date('d'),date('Y'))
])->schemaApi(
admin_url('device-chart?monitor_mode=${monitor_mode}&date_range=${date_range}')
),
]);
}
public function deviceChart(Request $request)
{
$dateRange = $request->input('date_range');
if (!$dateRange) {
$startTime = now()->startOfDay()->timestamp;
$endTime = now()->endOfDay()->timestamp;
} else {
list($startTime, $endTime) = explode(',', $dateRange);
}
$startTime = date('Y-m-d', $startTime);
$endTime = date('Y-m-d', $endTime);
$monitorMode = $request->monitor_mode ?? 0;
$monitorMode = MonitorMode::find($monitorMode);
$data = $monitorMode ? $this->service->getMonitorModeDeviceChartConfig($monitorMode, $startTime.' 00:00:00', $endTime.' 23:59:59', 2):[];
return $this->response()->success($data);
}
/**
* 水质图表
*/
public function waterChart(){
$regionId = request()->input('region_id', 0);
if($regionId){
$region = Region::find($regionId);
$options = $region->monitorModes()->where('type', MonitorMode::TYPE_WATER_QUALITY)->pluck('name','monitor_id')->toArray();
}else{
$options = MonitorMode::where('type', MonitorMode::TYPE_WATER_QUALITY)->pluck('name','id')->toArray();
}
return $this->basePage()->title('')->body([
\amisMake()->grid()->columns([
\amisMake()->Form()->title('搜索条件')->mode('inline')->body([
amisMake()->SelectControl('monitor_mode', '监测点位')->size('md')->options($options)->selectFirst(true),
\amisMake()->DateRangeControl('date_range', '时间范围')->value('today,today'),
amis('submit')->label(__('admin.search'))->level('primary'),
])->target('water_chart'),
]),
amisMake()->Service()->name('water_chart')
->data([
'monitor_mode'=>array_key_first($options),
'date_range'=>mktime(0,0,0,date('m'),date('d'),date('Y')).','.mktime(0,0,0,date('m'),date('d'),date('Y'))
])->schemaApi(
admin_url('device-chart?monitor_mode=${monitor_mode}&date_range=${date_range}')
),
]);
}
/**
* 土壤图表
*/
public function soilChart(){
$regionId = request()->input('region_id', 0);
if($regionId){
$region = Region::find($regionId);
$options = $region->monitorModes()->where('type', MonitorMode::TYPE_SOIL)->pluck('name','monitor_id')->toArray();
}else{
$options = MonitorMode::where('type', MonitorMode::TYPE_SOIL)->pluck('name','id')->toArray();
}
return $this->basePage()->title('')->body([
\amisMake()->grid()->columns([
\amisMake()->Form()->title('搜索条件')->mode('inline')->body([
amisMake()->SelectControl('monitor_mode', '监测点位')->size('md')->options($options)->selectFirst(true),
\amisMake()->DateRangeControl('date_range', '时间范围')->value('today,today'),
amis('submit')->label(__('admin.search'))->level('primary'),
])->target('soil_chart'),
]),
amisMake()->Service()->name('soil_chart')
->data([
'monitor_mode'=>array_key_first($options),
'date_range'=>mktime(0,0,0,date('m'),date('d'),date('Y')).','.mktime(0,0,0,date('m'),date('d'),date('Y'))
])->schemaApi(
admin_url('device-chart?monitor_mode=${monitor_mode}&date_range=${date_range}')
),
]);
}
/**
* 空气条件设置
*/
public function airDetail(){
$regionId = request()->input('region_id', 0);
$config = null;
$device = null;
if($regionId){
$region = Region::find($regionId);
if($region){
$monitorMode = $region->monitorModes()->where('type', MonitorMode::TYPE_AIR)->first();
$device = $monitorMode->devices()->first();
$config = $device?->extends ?? null;
}
}
return amisMake()->Grid()->columns([
amisMake()->Wrapper()->sm(6)->body([
amisMake()->Panel()->title('智能开关设置')
->labelWidth(100)
->body([
\amisMake()->Form()->title('')->mode('horizontal')
->api([
'method'=>'post',
'url'=>admin_url('save-region-config').'/'.$regionId.'?type='.MonitorMode::TYPE_AIR,
'data'=>[
'open_is_enable' => '${open_is_enable}',
'open_config' => '${open_config}',
'close_is_enable' => '${close_is_enable}',
'close_config' => '${close_config}',
]
])
->data($config ? $config : [])
->body([
amisMake()->FieldSetControl()->title('智能开启')->body([
\amisMake()->SwitchControl()->name('open_is_enable')->label('开关'),
\amisMake()->ConditionBuilderControl()->name('open_config')->label('条件')->fields([
[
'label' => '温度', 'type'=>'number','step'=>'0.01','name'=>'box_temperature',
'operators'=> [
'less', 'less_or_equal', 'greater', 'greater_or_equal'
// 'is_empty', 'is_not_empty'
]
],
[
'label' => '湿度', 'type'=>'number','step'=>'0.01','name'=>'box_humidity',
'operators'=> [
'less', 'less_or_equal', 'greater', 'greater_or_equal'
// 'is_empty', 'is_not_empty'
]
],
]),
]),
amisMake()->FieldSetControl()->title('智能关闭')->body([
\amisMake()->SwitchControl()->name('close_is_enable')->label('开关'),
\amisMake()->ConditionBuilderControl()->name('close_config')->label('条件')->fields([
[
'label' => '温度', 'type'=>'number','step'=>'0.01','name'=>'box_temperature',
'operators'=> [
'less', 'less_or_equal', 'greater', 'greater_or_equal',
// 'is_empty', 'is_not_empty'
]
],
[
'label' => '湿度', 'type'=>'number','step'=>'0.01','name'=>'box_humidity',
'operators'=> [
'less', 'less_or_equal', 'greater', 'greater_or_equal'
// 'is_empty', 'is_not_empty'
]
],
]),
]),
]),
])
]),
amisMake()->Wrapper()->sm(6)->body([
amisMake()->Panel()->title('开关记录')
->body([
\amisMake()->CRUDTable()
->api(admin_url('air-logs').'?_action=getData&device_id='.($device?->id ?? 0))
->title('')
->columns([
amisMake()->TableColumn()->name('content')->label('触发条件'),
amisMake()->TableColumn()->name('type')->type('mapping')->map([
"0"=>"<span class='label label-info'>未知</span>",
"1"=>"<span class='label label-success'>开启</span>",
"2"=>"<span class='label label-danger'>关闭</span>",
"*"=> '其他:${type}'
])->label('动作'),
amisMake()->TableColumn()->name('created_at')->type('datetime')->label('执行时间'),
])
])
]),
]);
}
/**
* 喷雾
*/
public function atomizingDetail(){
$regionId = request()->input('region_id', 0);
$config = null;
$statusStr = '未知';
$device = null;
if($regionId){
$region = Region::find($regionId);
if($region){
$monitorMode = $region->monitorModes()->where('type', MonitorMode::TYPE_ATOMIZING)->first();
$device = $monitorMode->devices()->first();
$config = $device?->extends ?? null;
if($monitorMode){
//判断设备状态是否离线;
$device = $monitorMode->devices()->first();
if($device->state == Device::STATE_ONLINE){
$res = (new MqttService())->getStatus();
if($res['error']){
switch($res['error']){
case 1:
$statusStr = '急停';
break;
case 2:
$statusStr = '低水位报警';
break;
}
}else{
if($res['is_running']){
if($res['yv1']){
$statusStr = '区域A运行中喷雾量'.$res['speed1'].'%';
}elseif(['yv2']){
$statusStr = '区域B运行中喷雾量'.$res['speed2'].'%';
}
}else{
$statusStr = '未运行';
}
}
}else{
$statusStr = Device::stateMap()[$device->state];
}
}
}
}
return amisMake()->Grid()->columns([
amisMake()->Wrapper()->sm(6)->body([
amisMake()->Panel()->title('智能喷灌设置')
->subFormMode('horizontal')
->labelWidth(80)
->body([
\amisMake()->Form()->title('')->mode('horizontal')
->api([
'method'=>'post',
'url'=>admin_url('save-region-config').'/'.$regionId.'?type='.MonitorMode::TYPE_ATOMIZING,
'data'=>[
'is_enable' => '${is_enable}',
'config' => '${config}'
]
])
->data($config ? $config : [])
->body([
amisMake()->FieldSetControl()->title('当前状态')->body([
amisMake()->TextControl('status', '状态')->value($statusStr),
])->static(),
amisMake()->FieldSetControl()->className('mt-10')->title('定时喷灌')->body([
\amisMake()->SwitchControl()->name('is_enable')->label('开关'),
\amisMake()->ArrayControl()->name('config')->label('定时')->items([
amisMake()->ComboControl()->items([
amisMake()->SelectControl('value')->options([
'a'=>'区域A','b'=>'区域B'
]),
\amisMake()->InputTimeRange()->name('time_zone'),
Components::make()->decimalControl('input', '喷雾量%')->value(70)->percision(0)->step(1)->max(100)
]),
]),
]),
]),
])
]),
amisMake()->Wrapper()->sm(6)->body([
amisMake()->Panel()->title('开关记录')
->body([
\amisMake()->CRUDTable()
->api(admin_url('atomizing-logs').'?_action=getData&device_id='.($device?->id ?? 0))
->title('')
->columns([
amisMake()->TableColumn()->name('type')->type('mapping')->map([
"0"=>"<span class='label label-info'>未知</span>",
"1"=>"<span class='label label-success'>开启</span>",
"2"=>"<span class='label label-danger'>关闭</span>",
"*"=> '其他:${type}'
])->label('动作'),
amisMake()->TableColumn()->name('content')->label('描述内容'),
amisMake()->TableColumn()->name('created_at')->type('datetime')->label('操作时间'),
])
])
]),
]);
}
public function saveRegionConfig($id, Request $request)
{
if($id){
$region = Region::find($id);
if($region){
$type = $request->input('type', 0);
$monitorMode = $region->monitorModes()->where('type', $type)->first();
$config = $request->input() ?? null;
unset($config['type']);
$res = $monitorMode->devices()->update([
'extends' => $config,
]);
}
}
return $this->autoResponse($res, __('admin.save'));
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Renderers\Page;
use Slowlyo\OwlAdmin\Renderers\Form;
use Slowlyo\OwlAdmin\Renderers\TableColumn;
use Slowlyo\OwlAdmin\Renderers\TextControl;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\FriendLinkService;
use App\Admin\Components;
class FriendLinkController extends AdminController
{
protected string $serviceName = FriendLinkService::class;
protected string $pageTitle = '友情链接';//待完善-todo
public function list(): Page
{
$crud = $this->baseCRUD()
->filterTogglable(false)
->headerToolbar([
$this->createButton(true),
...$this->baseHeaderToolBar(),
])
->columns([
TableColumn::make()->name('id')->label('ID')->sortable(true),
TableColumn::make()->name('name')->label('名称'),
TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true),
TableColumn::make()->name('updated_at')->label('更新时间')->type('datetime')->sortable(true),
$this->rowActions(true),
]);
return $this->baseList($crud);
}
public function form(): Form
{
return $this->baseForm()->body([
TextControl::make()->name('name')->label('名称'),
TextControl::make()->name('path')->label('链接')->required(true),
amisMake()->ImageControl()->name('icon')->label('icon')->autoUpload(true),
Components::make()->sortControl(),
amisMake()->SwitchControl()->name('is_recommend')->label('推荐'),
amisMake()->SwitchControl()->name('is_enable')->label('显示'),
]);
}
public function detail(): Form
{
return $this->baseDetail()->body([
TextControl::make()->static(true)->name('id')->label('ID'),
TextControl::make()->static(true)->name('name')->label('名称'),
TextControl::make()->static(true)->name('created_at')->label('创建时间'),
TextControl::make()->static(true)->name('updated_at')->label('更新时间')
]);
}
}

View File

@ -17,32 +17,71 @@ use Slowlyo\OwlAdmin\Controllers\AdminController;
class HomeController extends AdminController
{
protected string $queryPath = 'dashboard';
protected string $pageTitle = '首页';
public function index(): JsonResponse|JsonResource
{
$page = $this->basePage()->css($this->css())->body([
Grid::make()->columns([
// $this->frameworkInfo()->md(5),
$this->frameworkInfo()->md(8),
Flex::make()->items([
$this->pieChart(),
$this->cube(),
]),
]),
Grid::make()->columns([
$this->lineChart()->md(8),
Flex::make()->className('h-full')->items([
$this->clock(),
// $this->giteeWidget(),
$this->hitokoto(),
])->direction('column'),
]),
// Grid::make()->columns([
// $this->lineChart()->md(8),
// Flex::make()->className('h-full')->items([
// $this->clock(),
// // $this->giteeWidget(),
// ])->direction('column'),
// ]),
]);
return $this->response()->success($page);
}
/**
* 一言
*/
public function hitokoto()
{
return Card::make()
->className('h-full clear-card-mb')
->body(
Custom::make()->html(<<<HTML
<div class="h-full flex flex-col mt-5 py-8 px-7">
<div></div>
<div class="flex flex-1 items-center w-full justify-center" id="hitokoto">
<a class="text-dark" href="#" id="hitokoto_text" target="_blank"></a>
</div>
<div class="flex justify-end"></div>
</div>
<div class="flex justify-end mt-3">
——&nbsp;
<span id="hitokoto_from_who"></span>
<span></span>
<span id="hitokoto_from"></span>
<span></span>
</div>
HTML
)->onMount(<<<JS
fetch('https://v1.hitokoto.cn?c=i')
.then(response => response.json())
.then(data => {
const hitokoto = document.querySelector('#hitokoto_text')
hitokoto.href = `https://hitokoto.cn/?uuid=\${data.uuid}`
hitokoto.innerText = data.hitokoto
document.querySelector('#hitokoto_from_who').innerText = data.from_who
document.querySelector('#hitokoto_from').innerText = data.from
})
.catch(console.error)
JS
)
);
}
/**
* gitee widget
*/
@ -125,25 +164,25 @@ JS
Image::make()->src(url(config('admin.logo'))),
Wrapper::make()->className('text-3xl mt-9')->body(config('admin.name')),
Flex::make()->className('w-64 mt-5')->justify('space-around')->items([
Action::make()
->level('link')
->label('Gitee')
->blank(true)
->actionType('url')
->blank(true)
->link('https://gitee.com/slowlyo/owl-admin'),
Action::make()
->level('link')
->label('OwlAdmin 文档')
->blank(true)
->actionType('url')
->link('https://slowlyo.gitee.io/owl-admin-doc/'),
Action::make()
->level('link')
->label('Amis 文档')
->blank(true)
->actionType('url')
->link('https://aisuda.bce.baidu.com/amis/zh-CN/docs/index'),
// Action::make()
// ->level('link')
// ->label('Gitee')
// ->blank(true)
// ->actionType('url')
// ->blank(true)
// ->link('https://gitee.com/slowlyo/owl-admin'),
// Action::make()
// ->level('link')
// ->label('OwlAdmin 文档')
// ->blank(true)
// ->actionType('url')
// ->link('https://slowlyo.gitee.io/owl-admin-doc/'),
// Action::make()
// ->level('link')
// ->label('Amis 文档')
// ->blank(true)
// ->actionType('url')
// ->link('https://aisuda.bce.baidu.com/amis/zh-CN/docs/index'),
]),
]),
])

View File

@ -0,0 +1,49 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Controllers\IndexController as BaseController;
use Slowlyo\OwlAdmin\Admin;
use Illuminate\Http\JsonResponse;
use Slowlyo\OwlAdmin\Models\Extension;
use Illuminate\Http\Resources\Json\JsonResource;
class IndexController extends BaseController
{
public function settings(): JsonResponse|JsonResource
{
return $this->response()->success([
'assets' => Admin::getAssets(),
'app_name' => config('admin.name'),
'locale' => config('app.locale'),
'layout' => config('admin.layout'),
'logo' => url(config('admin.logo')),
'login_captcha' => config('admin.auth.login_captcha'),
'show_development_tools' => config('admin.show_development_tools'),
'system_theme_setting' => array_merge(
Admin::setting()->get('system_theme_setting', []),
[
"animateInDuration"=> 550,
"animateInType"=> "alpha",
"animateOutDuration" => 450,
"animateOutType"=> "alpha",
"breadcrumb"=> true,
"breadcrumbIcon" => false,
"footer"=> false,
"keepAlive"=> false,
"layoutMode"=> "default",
"loginTemplate"=> "default",
"menuWidth"=> 220,
"siderTheme"=>"light",
"theme"=>"light",
"themeColor"=>"#4080FF",
"topTheme"=> "light",
'enableTab' => config('admin.layout.tab_enable'),
'tabIcon' => config('admin.layout.tab_icon'),
]
),
'enabled_extensions' => Extension::query()->where('is_enabled', 1)->pluck('name')?->toArray(),
]);
}
}

View File

@ -0,0 +1,216 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Renderers\Page;
use Slowlyo\OwlAdmin\Renderers\Form;
use Slowlyo\OwlAdmin\Renderers\TableColumn;
use Slowlyo\OwlAdmin\Renderers\TextControl;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\MonitorModeService;
use App\Admin\Components;
use App\Models\MonitorMode;
use App\Models\Device;
use App\Models\Keyword;
/**
* @property MonitorModeService $service
*/
class MonitorModeController extends AdminController
{
protected string $serviceName = MonitorModeService::class;
public function list(): Page
{
$crud = $this->baseCRUD()
->filterTogglable(false)
->headerToolbar([
$this->createButton(true, 'lg'),
...$this->baseHeaderToolBar(),
])
->filter($this->baseFilter()->actions([])->body([
TextControl::make()->name('name')->label('名称')->size('md'),
amisMake()->SelectControl()->name('type')->label('类型')->options(MonitorMode::typeMap())->size('md'),
Components::make()->keywordsTagControl('group_tags', '分组', 'monitor-mode-group')->size('md'),
amis('button')->label(__('admin.reset'))->actionType('clear-and-submit'),
amis('submit')->label(__('admin.search'))->level('primary'),
]))
->columns([
TableColumn::make()->name('id')->label('ID')->sortable(true),
TableColumn::make()->name('name')->label('名称'),
TableColumn::make()->name('type')->type('mapping')->map(MonitorMode::typeMap())->label('类型'),
TableColumn::make()->name('tags')->type('mapping')->map(Keyword::tagsMap('monitor-mode-group'))->label('分组'),
TableColumn::make()->name('created_at')->label(__('admin.created_at'))->type('datetime')->sortable(true),
amisMake()->Operation()->label(__('admin.actions'))->buttons([
$this->setAboutDevice(),
$this->rowEditButton(true, 'lg'),
$this->rowDeleteButton()
]),
]);
return $this->baseList($crud);
}
public function form(): Form
{
return $this->baseForm()->body([
TextControl::make()->name('name')->label('名称'),
\amisMake()->RadiosControl()->name('type')->label('类型')->options(MonitorMode::typeMap())->required(true)->disabledOn('data.id > 0'),
Components::make()->keywordsTagControl('group_tags', '分组', 'monitor-mode-group'),
// Components::make()->sortControl('sort', __('admin.order')),
// TextControl::make()->name('is_enable')->type('switch')->default(1)->label('显示'),
// TextControl::make()->name('is_recommend')->type('switch')->default(0)->label('推荐'),
]);
}
private function setAboutDevice(){
return amisMake()->DrawerAction()->label('设置监测设备')->icon('fa-solid fa-gear')->level('link')->drawer(
amisMake()->Drawer()->title('设置监测设备')->resizable(true)->closeOnOutside(true)->closeOnEsc(true)->body([
amisMake()
->Form()
->api(admin_url('monitor-mode-save-devices'))
->initApi($this->getEditGetDataPath())
->mode('normal')
->data(['id' => '${id}'])
->body([
amisMake()->RadiosControl()->name('type')->label('类型')->options(MonitorMode::typeMap())->disabled(true),
amisMake()->PickerControl('picker_devices', '监控设备')->visibleOn('data.type == '.MonitorMode::TYPE_MONITOR)
->valueField('id')
->labelField('name')
->multiple(true)->joinValues(false)->extractValue(true)
->size('lg')
->source([
'method' => 'get',
'url' => admin_url('devices?_action=getData&type='.Device::TYPE_MONITOR),
'data' => [
'name'=>'${device_name}',
]
])
->pickerSchema(
[
'mode' => 'table',
'name' => 'monitor_list',
'headerToolbar' => amisMake()->form()
->wrapWithPanel(false)
->className('text-right')
->target('monitor_list')
->mode('inline')
->body([
amisMake()->TextControl('device_name', '名称')->addOn(
amis('submit')->label(__('admin.search'))->level('primary')
)
]),
'columns' => [
TableColumn::make()->name('id')->label('ID')->sortable(true),
TableColumn::make()->name('name')->label('名称'),
TableColumn::make()->name('sn')->label('编号'),
TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true),
TableColumn::make()->name('updated_at')->label('更新时间')->type('datetime')->sortable(true),
]
]
),
amisMake()->ArrayControl('array_devices', '土壤监测')->visibleOn('data.type == '.MonitorMode::TYPE_SOIL)
->items(
amisMake()->ComboControl()->items([
\amisMake()->CheckboxesControl('device_fields', '监测字段')->checkAll(true)->options(MonitorMode::fieldMap(MonitorMode::TYPE_SOIL)),
\amisMake()->SelectControl('device_id', '监测设备')->options(Device::where('type', Device::TYPE_SOIL)->get()->pluck('name', 'id')),
]),
),
amisMake()->ArrayControl('array_devices', '水质监测')->visibleOn('data.type == '.MonitorMode::TYPE_WATER_QUALITY)
->items(
amisMake()->ComboControl()->items([
\amisMake()->CheckboxesControl('device_fields', '监测字段')->checkAll(true)->options(MonitorMode::fieldMap(MonitorMode::TYPE_WATER_QUALITY)),
\amisMake()->SelectControl('device_id', '监测设备')->options(Device::where('type', Device::TYPE_WATER_QUALITY)->get()->pluck('name', 'id')),
]),
),
amisMake()->ArrayControl('array_devices', '气象监测')->visibleOn('data.type == '.MonitorMode::TYPE_METEOROLOGICAL)
->items(
amisMake()->ComboControl()->items([
\amisMake()->CheckboxesControl('device_fields', '监测字段')->checkAll(true)->options(MonitorMode::fieldMap(MonitorMode::TYPE_METEOROLOGICAL)),
\amisMake()->SelectControl('device_id', '监测设备')->options(Device::where('type', Device::TYPE_METEOROLOGICAL)->get()->pluck('name', 'id')),
]),
),
amisMake()->PickerControl('picker_devices', '通风设备')->visibleOn('data.type == '.MonitorMode::TYPE_AIR)
->valueField('id')
->labelField('name')
->multiple(true)
->size('lg')
->source([
'method' => 'get',
'url' => admin_url('devices?_action=getData&type='.Device::TYPE_AIR),
'data' => [
'name'=>'${device_name}',
]
])
->pickerSchema(
[
'mode' => 'table',
'name' => 'monitor_list',
'headerToolbar' => amisMake()->form()
->wrapWithPanel(false)
->className('text-right')
->target('monitor_list')
->mode('inline')
->body([
amisMake()->TextControl('device_name', '名称')->addOn(
amis('submit')->label(__('admin.search'))->level('primary')
)
]),
'columns' => [
TableColumn::make()->name('id')->label('ID')->sortable(true),
TableColumn::make()->name('name')->label('名称'),
TableColumn::make()->name('sn')->label('编号'),
TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true),
TableColumn::make()->name('updated_at')->label('更新时间')->type('datetime')->sortable(true),
]
]
),
amisMake()->PickerControl('picker_devices', '喷雾设备')->visibleOn('data.type == '.MonitorMode::TYPE_ATOMIZING)
->valueField('id')
->labelField('name')
->multiple(false)//喷雾设备只能选择一个
->size('lg')
->source([
'method' => 'get',
'url' => admin_url('devices?_action=getData&type='.Device::TYPE_ATOMIZING),
'data' => [
'name'=>'${device_name}',
]
])
->pickerSchema(
[
'mode' => 'table',
'name' => 'monitor_list',
'headerToolbar' => amisMake()->form()
->wrapWithPanel(false)
->className('text-right')
->target('monitor_list')
->mode('inline')
->body([
amisMake()->TextControl('device_name', '名称')->addOn(
amis('submit')->label(__('admin.search'))->level('primary')
)
]),
'columns' => [
TableColumn::make()->name('id')->label('ID')->sortable(true),
TableColumn::make()->name('name')->label('名称'),
TableColumn::make()->name('sn')->label('编号'),
TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true),
TableColumn::make()->name('updated_at')->label('更新时间')->type('datetime')->sortable(true),
]
]
),
])
])->footer([
amisMake()->Button()->label('保存')->type('submit')->level('primary'),
])
);
}
public function saveDevices()
{
$result = $this->service->saveDevices(request('id'), request());
return $this->autoResponse($result, __('admin.save'));
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Renderers\Page;
use Slowlyo\OwlAdmin\Renderers\Form;
use Slowlyo\OwlAdmin\Renderers\TableColumn;
use Slowlyo\OwlAdmin\Renderers\TextControl;
use Slowlyo\OwlAdmin\Renderers\Image;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use Slowlyo\OwlAdmin\Renderers\Button;
use App\Services\Admin\RegionCategoryService;
use App\Admin\Components;
use Illuminate\Http\Request;
class RegionCategoryController extends AdminController
{
protected string $serviceName = RegionCategoryService::class;
protected string $pageTitle = '区域分类';
public function list(): Page
{
// dd(Components::make()->parentControl('region-categories/tree-list'));
$crud = $this->baseCRUD()
//关闭查询
->filterTogglable(false)
//去掉分页-start
->loadDataOnce(true)
->footerToolbar([])
//去掉分页-end
->headerToolbar([
$this->createButton(true),
...$this->baseHeaderToolBar(),
])
->quickSaveItemApi(admin_url('quick-edit/region-categories/$id'))
->filter(
$this->baseFilter()->body([
amisMake()->TextControl()->make('name')->label('分类名称')->name('name')->size('md'),
Components::make()->parentControl(admin_url('api/region-categories/tree-list'))->size('md'),
Button::make()->label(__('admin.reset'))->actionType('clear-and-submit'),
amis('submit')->label(__('admin.search'))->level('primary'),
])->actions([])
)
->columns([
TableColumn::make()->name('id')->label('ID')->sortable(true),
TableColumn::make()->name('name')->label('名称'),
Image::make()->name('icon')->label(__('region-category.icon'))->width(100),
// TableColumn::make()->name('parent.name')->label('父级ID'),
TableColumn::make()->name('sort')->label('排序'),
TableColumn::make()->name('is_enable')->type('switch')->label('显示')->quickEdit(
Components::make()->enableControl('is_enable', '', 'inline')->saveImmediately(true)
),
TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true),
$this->rowActions(true),
]);
return $this->baseList($crud);
}
public function form(): Form
{
return $this->baseForm()->body([
amisMake()->TextControl()->name('name')->label('名称')->required(true),
amisMake()->ImageControl()->name('icon')->label('封面图')->autoUpload(true),
amisMake()->TextControl()->name('description')->label('描述'),
Components::make()->parentControl(admin_url('api/region-categories/tree-list')),
Components::make()->sortControl(),
Components::make()->enableControl()->value(1),
]);
}
public function detail(): Form
{
return $this->baseDetail()->body([
TextControl::make()->static(true)->name('id')->label('ID'),
TextControl::make()->static(true)->name('name')->label('名称'),
TextControl::make()->name('icon')->label(__('region-category.icon'))->static(true)->staticSchema(Image::make()),
TextControl::make()->static(true)->name('description')->label('描述'),
TextControl::make()->static(true)->name('created_at')->label('创建时间'),
TextControl::make()->static(true)->name('updated_at')->label('更新时间')
]);
}
public function getTreeList(Request $request){
return $this->service->getTree();
}
}

View File

@ -0,0 +1,127 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Renderers\Page;
use Slowlyo\OwlAdmin\Renderers\Form;
use Slowlyo\OwlAdmin\Renderers\Image;
use Slowlyo\OwlAdmin\Renderers\TableColumn;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\RegionService;
use Slowlyo\OwlAdmin\Renderers\Button;
use App\Admin\Components;
use App\Models\MonitorMode;
use App\Models\Region;
use Illuminate\Http\Request;
class RegionController extends AdminController
{
protected string $serviceName = RegionService::class;
protected string $pageTitle = '实验田';
public function list(): Page
{
$crud = $this->baseCRUD()
->filterTogglable(false)
->headerToolbar([
$this->createButton(true, 'lg'),
...$this->baseHeaderToolBar(),
])
->filter(
$this->baseFilter()->body([
amisMake()->TextControl()->make('name')->label('名称')->name('name')->size('md'),
Components::make()->parentControl(admin_url('api/region-categories/tree-list'), 'category_id', '分类')->size('md'),
Button::make()->label(__('admin.reset'))->actionType('clear-and-submit'),
amis('submit')->label(__('admin.search'))->level('primary'),
])->actions([])
)
->quickSaveItemApi(admin_url('quick-edit/regions/$id'))
->columns([
TableColumn::make()->name('id')->label('ID')->sortable(true),
TableColumn::make()->name('name')->label('名称'),
Image::make()->name('cover')->label(__('region.cover'))->width(100),
TableColumn::make()->name('category.name')->label('分类')->className('text-primary'),
TableColumn::make()->name('director')->label('负责人'),
TableColumn::make()->name('area')->label('面积m²'),
TableColumn::make()->name('is_enable')->type('switch')->label('显示')->quickEdit(Components::make()->enableControl('is_enable', '', 'inline')->saveImmediately(true)),
TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true),
// TableColumn::make()->name('updated_at')->label('更新时间')->type('datetime')->sortable(true),
amisMake()->Operation()->label(__('admin.actions'))->buttons([
$this->showRegion(),
$this->rowEditButton(true, 'lg'),
$this->rowDeleteButton(),
amisMake()->Button()->label('设置大屏位置')->level('link')->actionType('url')->blank(true)
->url(url('/regions-position').'/'.'${id}')
]),
]);
return $this->baseList($crud);
}
public function form(): Form
{
$monitorModeType = [];
$monitorModes = MonitorMode::get()->groupBy('type');
foreach(MonitorMode::typeMap() as $key => $item) {;
$monitorModeType[] = [
'label' => $item,
'children' => $monitorModes->get($key)?->pluck('name', 'id')->toArray(),
];
}
return $this->baseForm()->body([
\amisMake()->TextControl()->name('name')->label('名称')->required(true),
\amisMake()->TextControl()->name('director')->label('负责人')->required(true),
Components::make()->parentControl(admin_url('api/region-categories/tree-list'), 'category_id', '分类')->required(),
\amisMake()->ImageControl()->name('cover')->label('封面')->crop([
'aspectRatio'=> 1.1,
'scalable' => true,
])->autoUpload(true),
Components::make()->fuEditorControl('description', '介绍'),
Components::make()->decimalControl('area', '面积m²'),
Components::make()->sortControl(),
\amisMake()->SwitchControl()->name('is_enable')->value(1)->label('显示'),
\amisMake()->TransferControl()->name('monitorModes')->label(__('region.monitor'))
->selectMode('chained')->searchable(true)
->joinValues(false)->extractValue(true)
->options($monitorModeType),
]);
}
private function showRegion(){
return amisMake()->DialogAction()->icon('fa-regular fa-eye')->label('查看')->level('link')->dialog(
amisMake()->Dialog()->title('查看详情')->body([
\amisMake()->Service()->schemaApi(admin_url('custom-region-tabs?id=${id}'))
])->size('full')->actions([])
);
}
public function setRegionPosition($id, Request $request){
$region = Region::find($id);
$x = $region?->position_x ?? 0;
$y = $region?->position_y ?? 0;
return view('region-position', compact('id', 'x', 'y'));
}
public function saveRegionPosition(Request $request)
{
$id = $request->input('id', 0);
$positionX = $request->input('x', 0);
$positionY = $request->input('y', 0);
$region = Region::find($id);
if(!$region){
return response()->json(['code'=>1, 'msg'=> '非法请求']);
}
if($positionX <= 0 || $positionY <= 0){
return response()->json(['code'=>2, 'msg'=>'请点击地图,标记位置']);
}
$region->update([
'position_x'=> $positionX,
'position_y'=> $positionY
]);
//直接关闭窗口
echo '<script>window.opener=null;window.close();</script>';
return ;
}
}

View File

@ -5,10 +5,9 @@ namespace App\Admin\Controllers;
use Illuminate\Http\Request;
use Slowlyo\OwlAdmin\Renderers\Tab;
use Slowlyo\OwlAdmin\Renderers\Tabs;
use Slowlyo\OwlAdmin\Renderers\Alert;
use Slowlyo\OwlAdmin\Renderers\InputKV;
use Slowlyo\OwlAdmin\Renderers\TextControl;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Admin\Components;
class SettingController extends AdminController
{
@ -18,10 +17,7 @@ class SettingController extends AdminController
public function index()
{
$page = $this->basePage()->body([
Alert::make()->showIcon(true)->body("此处内容仅供演示, 设置项无实际意义,实际开发中请根据实际情况进行修改。"),
$this->form(),
]);
$page = $this->basePage()->body($this->form());
return $this->response()->success($page);
}
@ -32,15 +28,19 @@ class SettingController extends AdminController
->redirect('')
->api($this->getStorePath())
->data(settings()->all())
->panelClassName('')
->title('')
->body(
Tabs::make()->tabs([
Tab::make()->title('基本设置')->body([
TextControl::make()->label('网站名称')->name('site_name'),
InputKV::make()->label('附加配置')->name('addition_config'),
Tab::make()->title('基地信息设置')->body([
amisMake()->ComboControl('region_base', '基础信息')->multiple(true)->items([
amisMake()->TextControl('name', '名称'),
Components::make()->decimalControl('value', '值'),
amisMake()->TextControl('unit', '单位'),
])->description('以上明细前6项会展示在大屏中间底部。'),
]),
Tab::make()->title('上传设置')->body([
TextControl::make()->label('上传域名')->name('upload_domain'),
TextControl::make()->label('上传路径')->name('upload_path'),
Tab::make()->title('系统设置')->body([
TextControl::make()->label('RTSP 转流服务')->name('rtsp_url'),
]),
])
);
@ -49,10 +49,8 @@ class SettingController extends AdminController
public function store(Request $request)
{
$data = $request->only([
'site_name',
'addition_config',
'upload_domain',
'upload_path',
'rtsp_url',
'region_base'
]);
return settings()->adminSetMany($data);

View File

@ -0,0 +1,55 @@
<?php
namespace App\Admin\Controllers;
use App\Models\MonitorMode;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use Slowlyo\OwlAdmin\Renderers\Form;
use Slowlyo\OwlAdmin\Renderers\Page;
use Slowlyo\OwlAdmin\Renderers\TableColumn;
use App\Services\Admin\WarningNoticeService;
use App\Models\WarningNotice;
class WarningNoticeController extends AdminController
{
protected string $serviceName = WarningNoticeService::class;
protected string $pageTitle = '报警记录';
public function list(): Page
{
$crud = $this->baseCRUD()
->filterTogglable(false)
->headerToolbar([
...$this->baseHeaderToolBar(),
])
->columns([
TableColumn::make()->name('id')->label('ID')->sortable(true),
TableColumn::make()->name('device.modes')->type('each')->items([
'type' => 'tpl',
'tpl' => "<span class='label label-info m-l-sm'><%= data.name %></span>",
])->label('监测点位'),
TableColumn::make()->name('device.name')->label('设备名称'),
TableColumn::make()->name('lv')->type('mapping')->map(WarningNotice::lvMap())->label('报警等级')->className('text-primary'),
TableColumn::make()->name('content')->label('报警内容'),
// TableColumn::make()->name('status')->label('状态'),//可以忽略
TableColumn::make()->name('created_at')->label('报警时间')->type('datetime')->sortable(true),
]);
return $this->baseList($crud);
}
public function form(): Form
{
return $this->baseForm()->body([
]);
}
public function detail(): Form
{
return $this->baseDetail()->body([
]);
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\Admin\Controllers;
use App\Models\Device;
use App\Models\MonitorMode;
use App\Models\WarningNotice;
use Illuminate\Http\Request;
use Slowlyo\OwlAdmin\Controllers\AdminController;
class WarningSettingController extends AdminController
{
public function settingIndex(){
$page = $this->basePage()->body([
amisMake()->Card()->className('border-0')->body([
$this->settingForm()
]),
]);
return $this->response()->success($page);
}
private function settingForm(){
return \amisMake()->Tabs()->name('detailTab')->tabs([
[
'title' => '气象预警',
'value' => 'meteorological',
'tab'=>$this->modeForm(Device::TYPE_METEOROLOGICAL),
'unmountOnExit' => true//每次切换tab都要销毁
],
// [
// 'title' => '水质预警',
// 'value' => 'detail',
// 'tab'=>'',
// 'unmountOnExit' => true//每次切换tab都要销毁
// ],
[
'title' => '土壤预警',
'value' => 'detail',
'tab'=>$this->modeForm(Device::TYPE_SOIL),
'unmountOnExit' => true//每次切换tab都要销毁
],
]);
}
private function modeForm($type)
{
$formData = $formBody = $fields = [];
//获取当前已设置警报内容
$data = settings()->get('mode_warning_'.$type);
$data && $formData = $data;
foreach(MonitorMode::fieldMap($type) as $field => $fieldName){
$fields[] = [
'label' => $fieldName, 'type'=>'number','step'=>'0.01','name'=>$field,
'operators'=> ['between']
];
}
foreach(WarningNotice::lvMap() as $lv => $lvName){
if(!isset($formData[str($lv).'.conjunction'] )){
$formData[str($lv).'.conjunction'] = 'or';
}
$formBody[] = amisMake()->ConditionBuilderControl(str($lv), $lvName)->builderMode('simple')->showANDOR(true)->fields( $fields);
}
return \amisMake()->Form()
->data($formData)
->api(admin_url('warning-setting-save'))
->title('')->body(array_merge($formBody, [
amisMake()->TextControl('type', '类别')->value('mode_warning_'.$type)->hidden(true),
amis('submit')->label(__('admin.save'))->level('primary')
])
);
}
public function saveSettingConfig(Request $request){
$res = false;
$data = $request->input();
$key = $data['type'] ?? '';
if($key){
unset($data['type']);
$res = settings()->set($key, $data);
}
return $this->autoResponse($res, __('admin.save'));
}
}

View File

@ -1 +1,8 @@
<?php
use Slowlyo\OwlAdmin\Admin;
Admin::styles([
'.cxd-Panel-heading { border-width: 0; background: #4080ffc7 !important; }',
// '.cxd-Panel {box-shadow: none; }',
'.cxd-Panel-title { color:white;font-size: 16px; }'
]);

View File

@ -8,17 +8,101 @@ Route::group([
'prefix' => config('admin.route.prefix'),
'middleware' => config('admin.route.middleware'),
], function (Router $router) {
$router->get('/_settings', '\App\Admin\Controllers\IndexController@settings');
$router->resource('system/admin_users', App\Admin\Controllers\AdminUserController::class);
// 用户设置
$router->get('user_setting', [App\Admin\Controllers\AuthController::class, 'userSetting']);
$router->put('user_setting', [App\Admin\Controllers\AuthController::class, 'saveUserSetting']);
$router->group([
'prefix' => 'api',
], function (Router $router) {
$router->get('keywords/tree-list', '\App\Admin\Controllers\KeywordController@getTreeList')->name('api.keywords.tree-list');
$router->get('article-categories/tree-list', [\App\Admin\Controllers\ArticleCategoryController::class, 'getTreeList'])->name('api.article-categories.tree-list');
$router->get('articles/options', [\App\Admin\Controllers\ArticleController::class, 'options'])->name('api.articles.options');
$router->get('banner-places/options', [\App\Admin\Controllers\BannerPlaceController::class, 'options'])->name('api.banner-places.options');
$router->get('region-categories/tree-list', '\App\Admin\Controllers\RegionCategoryController@getTreeList')->name('api.region-categories.tree-list');
$router->get('devices/options', [\App\Admin\Controllers\DeviceController::class, 'options'])->name('api.devices.options');
});
$router->resource('dashboard', \App\Admin\Controllers\HomeController::class);
$router->get('dashboard', '\App\Admin\Controllers\HomeController@index');
//公告管理
$router->resource('admin-notices', \App\Admin\Controllers\AdminNoticeController::class);
$router->post('quick-edit/admin-notices/{admin_notice}',[ \App\Admin\Controllers\AdminNoticeController::class, 'update']);
// 文章分类
$router->resource('article-categories', \App\Admin\Controllers\ArticleCategoryController::class);
$router->post('quick-edit/article-categories/{article_category}', [\App\Admin\Controllers\ArticleCategoryController::class, 'update']);
$router->post('quick-edit/article-categories', [\App\Admin\Controllers\ArticleCategoryController::class, 'multipleUpdate']);
// 文章管理
$router->resource('articles', \App\Admin\Controllers\ArticleController::class);
$router->post('quick-edit/article/{article}', [\App\Admin\Controllers\ArticleController::class, 'update']);
// 图片位置
$router->resource('banner-places', \App\Admin\Controllers\BannerPlaceController::class);
$router->post('quick-edit/banner-places/{banner_place}',[\App\Admin\Controllers\BannerPlaceController::class, 'update']);
// 图片管理
$router->resource('banners', \App\Admin\Controllers\BannerController::class);
$router->post('quick-edit/banners/{banner}',[\App\Admin\Controllers\BannerController::class, 'update']);
//友情链接
$router->resource('friend-links', \App\Admin\Controllers\FriendLinkController::class);
//字典表
$router->resource('keywords', \App\Admin\Controllers\KeywordController::class);
//设备管理
$router->resource('devices', \App\Admin\Controllers\DeviceController::class);
$router->post('device-chart', '\App\Admin\Controllers\DeviceController@deviceChart');
//设备预警
$router->get('warning-setting', '\App\Admin\Controllers\WarningSettingController@settingIndex');
$router->post('warning-setting-save', '\App\Admin\Controllers\WarningSettingController@saveSettingConfig');
$router->get('warning-notice', '\App\Admin\Controllers\WarningNoticeController@index');
// $router->resource('system/re-roles', \App\Admin\Controllers\AdminRoleController::class);
//监测点位
$router->resource('monitor-modes', \App\Admin\Controllers\MonitorModeController::class);
$router->post('monitor-mode-save-devices', '\App\Admin\Controllers\MonitorModeController@saveDevices');
//区域分类
$router->resource('region-categories', \App\Admin\Controllers\RegionCategoryController::class);
$router->post('quick-edit/region-categories/{region_category}', '\App\Admin\Controllers\RegionCategoryController@update');
//区域列表
$router->resource('regions', \App\Admin\Controllers\RegionController::class);
$router->post('quick-edit/regions/{region}',[\App\Admin\Controllers\RegionController::class, 'update']);
//特殊菜单
$router->get('custom-region/{type}', '\App\Admin\Controllers\CustomRegionController@regionIndex');
$router->post('custom-region-tabs', '\App\Admin\Controllers\CustomRegionController@getRegionTabs');
$router->post('custom-region-detail', '\App\Admin\Controllers\CustomRegionController@regionDetail');
$router->get('custom-region-monitor', '\App\Admin\Controllers\DeviceController@monitorList');
$router->post('custom-region-monitor', '\App\Admin\Controllers\DeviceController@monitorList');
$router->get('custom-region-monitor-video', '\App\Admin\Controllers\DeviceController@monitorVideoList');
$router->get('custom-region-meteorological', '\App\Admin\Controllers\DeviceController@meteorologicalChart');
$router->post('custom-region-meteorological', '\App\Admin\Controllers\DeviceController@meteorologicalChart');
$router->get('custom-region-water', '\App\Admin\Controllers\DeviceController@waterChart');
$router->post('custom-region-water', '\App\Admin\Controllers\DeviceController@waterChart');
$router->get('custom-region-soil', '\App\Admin\Controllers\DeviceController@soilChart');
$router->post('custom-region-soil', '\App\Admin\Controllers\DeviceController@soilChart');
$router->post('custom-region-air', '\App\Admin\Controllers\DeviceController@airDetail');
$router->post('custom-region-atomizing', '\App\Admin\Controllers\DeviceController@atomizingDetail');
$router->post('save-region-config/{id}', '\App\Admin\Controllers\DeviceController@saveRegionConfig');
//种植记录
$router->resource('crop-plants', \App\Admin\Controllers\CropPlantController::class)->only(['index','store', 'edit', 'update', 'destroy']);
$router->resource('crop-harvestes', \App\Admin\Controllers\CropHarvestController::class)->only(['index','store', 'edit', 'update', 'destroy']);
$router->get('crop-plant-harveste-chart', '\App\Admin\Controllers\CropPlantController@plantHarvestChart');
$router->post('crop-plant-detail', '\App\Admin\Controllers\CropPlantController@plantDetail');
$router->resource('system/settings', \App\Admin\Controllers\SettingController::class);
//喷雾自动开启/关闭日志
$router->resource('atomizing-logs', \App\Admin\Controllers\AtomizingLogController::class)->only(['index']);
//通风自动开启/关闭日志
$router->resource('air-logs', \App\Admin\Controllers\AirLogController::class)->only(['index']);
});

View File

@ -0,0 +1,51 @@
<?php
namespace App\Casts;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage as StorageFacades;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
/**
* 转换文件存储路径
*
* get: 返回全路径
* set: 原样保存
*/
class Storage implements CastsAttributes
{
protected $disk;
function __construct($disk = 'public')
{
$this->disk = $disk;
}
/**
* Cast the given value.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return array
*/
public function get($model, $key, $value, $attributes)
{
return $value ? (Str::startsWith($value, ['http://', 'https://']) ? $value : StorageFacades::disk($this->disk)->url($value)) : '';
}
/**
* Prepare the given value for storage.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param array $value
* @param array $attributes
* @return string
*/
public function set($model, $key, $value, $attributes)
{
return $value;
}
}

View File

@ -0,0 +1,141 @@
<?php
namespace App\Console\Commands;
use App\Models\Device;
use App\Models\MeteorologicalDailyReport;
use App\Models\MeteorologicalReport;
use App\Models\SoilDailyReport;
use App\Models\SoilReport;
use App\Models\WaterQualityDailyReport;
use App\Models\WaterQualityReport;
use App\Services\DeviceLogService;
use Illuminate\Console\Command;
class DeviceLogDailyReportCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'device-log:daily-report
{factory}
{--sleep=300 : 监控报告生产后的休眠时间(秒)}';
/**
* The console command description.
*
* @var string
*/
protected $description = '按设备厂商生成监控报告';
/**
* @var \App\Services\DeviceLogService
*/
protected $deviceLogService;
/**
* Execute the console command.
*/
public function handle(DeviceLogService $deviceLogService)
{
$this->deviceLogService = $deviceLogService;
$factory = $this->argument('factory');
$sleep = (int) value(fn ($sleep) => is_numeric($sleep) ? $sleep : 300, $this->option('sleep'));
while (true) {
/** @var \Illuminate\Database\Eloquent\Collection */
$devices = Device::with(['factory'])->poweredBy($factory)->get();
foreach ($devices as $device) {
switch ($device->factory?->key) {
case 'link-os':
$this->createReportToLinkosDevice($device);
break;
}
}
sleep($sleep);
};
}
/**
* 创建 linkos 设备报告
*/
protected function createReportToLinkosDevice(Device $device): void
{
[$lastReportedAt, $latestReportedAt] = value(function (Device $device) {
$lastReportedAt = null;
$latestReportedAt = null;
switch ($device->type) {
case Device::TYPE_WATER_QUALITY:
$lastReportedAt = WaterQualityDailyReport::where('device_id', $device->id)
->latest('reported_at')
->value('reported_at');
$lastReportedAt ??= WaterQualityReport::where('device_id', $device->id)
->oldest('reported_at')
->value('reported_at');
if ($lastReportedAt) {
$latestReportedAt = WaterQualityReport::where('device_id', $device->id)
->latest('reported_at')
->value('reported_at');
}
break;
case Device::TYPE_METEOROLOGICAL:
$lastReportedAt = MeteorologicalDailyReport::where('device_id', $device->id)
->latest('reported_at')
->value('reported_at');
$lastReportedAt ??= MeteorologicalReport::where('device_id', $device->id)
->oldest('reported_at')
->value('reported_at');
if ($lastReportedAt) {
$latestReportedAt = MeteorologicalReport::where('device_id', $device->id)
->latest('reported_at')
->value('reported_at');
}
break;
case Device::TYPE_SOIL:
$lastReportedAt = SoilDailyReport::where('device_id', $device->id)
->latest('reported_at')
->value('reported_at');
$lastReportedAt ??= SoilReport::where('device_id', $device->id)
->oldest('reported_at')
->value('reported_at');
if ($lastReportedAt) {
$latestReportedAt = SoilReport::where('device_id', $device->id)
->latest('reported_at')
->value('reported_at');
}
break;
}
return [$lastReportedAt, $latestReportedAt];
}, $device);
if ($lastReportedAt === null || $latestReportedAt === null) {
return;
}
/** @var \Carbon\Carbon */
$startAt = $lastReportedAt->copy()->startOfDay();
do {
$this->deviceLogService->createDailyReportToLinkosDevice($device, $startAt->copy());
$startAt->addDay();
} while ($latestReportedAt->gte($startAt));
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace App\Console\Commands;
use App\Models\Device;
use App\Models\MeteorologicalReport;
use App\Models\SoilReport;
use App\Models\WaterQualityReport;
use App\Services\DeviceLogService;
use Illuminate\Console\Command;
class DeviceLogReportCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'device-log:report
{factory}
{--sleep=300 : 监控报告生产后的休眠时间(秒)}';
/**
* The console command description.
*
* @var string
*/
protected $description = '按设备厂商生成监控报告';
/**
* @var \App\Services\DeviceLogService
*/
protected $deviceLogService;
/**
* Execute the console command.
*/
public function handle(DeviceLogService $deviceLogService)
{
$this->deviceLogService = $deviceLogService;
$factory = $this->argument('factory');
$sleep = (int) value(fn ($sleep) => is_numeric($sleep) ? $sleep : 300, $this->option('sleep'));
while (true) {
/** @var \Illuminate\Database\Eloquent\Collection */
$devices = Device::with(['factory'])->poweredBy($factory)->get();
foreach ($devices as $device) {
switch ($device->factory?->key) {
case 'link-os':
$this->createReportToLinkosDevice($device);
break;
}
}
sleep($sleep);
};
}
/**
* 创建 linkos 设备报告
*/
protected function createReportToLinkosDevice(Device $device): void
{
$lastReportedAt = match ($device->type) {
Device::TYPE_SOIL => SoilReport::where('device_id', $device->id)->latest('reported_at')->value('reported_at'),
Device::TYPE_WATER_QUALITY => WaterQualityReport::where('device_id', $device->id)->latest('reported_at')->value('reported_at'),
Device::TYPE_METEOROLOGICAL => MeteorologicalReport::where('device_id', $device->id)->latest('reported_at')->value('reported_at'),
default => null,
};
if (is_null($lastReportedAt ??= $device->logs()->oldest('reported_at')->value('reported_at'))) {
return;
}
if (is_null($latestReportedAt = $device->logs()->latest('reported_at')->value('reported_at'))) {
return;
}
/** @var \Carbon\Carbon */
$startAt = $lastReportedAt->copy()->startOfHour();
do {
$this->deviceLogService->createReportToLinkosDevice($device, $startAt->copy());
$startAt->addHour();
} while ($latestReportedAt->gte($startAt));
}
}

View File

@ -0,0 +1,100 @@
<?php
namespace App\Console\Commands;
use App\Models\Device;
use App\Services\DeviceLogService;
use Illuminate\Console\Command;
use Throwable;
class DeviceLogSyncCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'device-log:sync
{factory}
{--sleep=60 : 设备数据同步完成后的休眠时间(秒)}';
/**
* The console command description.
*
* @var string
*/
protected $description = '按设备厂商同步数据';
/**
* @var \App\Services\DeviceLogService
*/
protected $deviceLogService;
/**
* Execute the console command.
*/
public function handle(DeviceLogService $deviceLogService)
{
$this->deviceLogService = $deviceLogService;
$factory = $this->argument('factory');
$sleep = (int) value(fn ($sleep) => is_numeric($sleep) ? $sleep : 60, $this->option('sleep'));
while (true) {
$this->runSync($factory);
sleep($sleep);
};
}
/**
* 执行同步
*/
protected function runSync(string $factory): void
{
$end = now();
$start = $end->copy()->subHours(1);
$this->info('------------------------------------------');
$this->info('开始时间: '. $start);
$this->info('结束时间: '. $end);
/** @var \Illuminate\Database\Eloquent\Collection */
$devices = Device::with(['factory'])->poweredBy($factory)->get();
/** @var \App\Models\Device */
foreach ($devices as $device) {
// 忽略通风设备
if ($device->isTypeAir()) {
continue;
}
$this->info('==========================================');
$this->info('设备编号: ' . $device->sn);
$this->info('设备名称: ' . $device->name);
$this->info('设备类型: ' . match ($device->type) {
Device::TYPE_SOIL => '土壤设备',
Device::TYPE_WATER_QUALITY => '水质设备',
Device::TYPE_METEOROLOGICAL => '气象设备',
Device::TYPE_AIR => '通风设备',
default => '其它',
});
try {
$this->deviceLogService->sync($device, $start, $end);
$this->info('同步成功!');
} catch (Throwable $e) {
report($e);
$this->error('同步失败: '. $e->getMessage());
}
$this->info('==========================================');
}
$this->info('------------------------------------------');
$this->newLine();
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Schema;
class ModelFillable extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'model:fillable {table} {--connection}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'get model fillable ';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
*/
public function handle()
{
$table = $this->argument('table');
$ignore = ['id', 'created_at', 'updated_at', 'deleted_at'];
$connection = $this->option('connection');
if (!$connection) {
$connection = config('database.default');
}
if (Schema::connection($connection)->hasTable($table)) {
$list = Schema::connection($connection)->getColumnListing($table);
$list = array_filter($list, function ($value) use ($ignore) {
return !in_array($value, $ignore);
});
$this->info("protected \$fillable = ['".implode('\', \'', $list)."'];");
}
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace App\Console\Commands;
use App\Models\Device;
use App\Models\AtomizingLog;
use App\Services\MqttService;
use Illuminate\Console\Command;
class MqttPenwuPlan extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'mqtt:penwu-plan';
/**
* The console command description.
*
* @var string
*/
protected $description = 'MQTT 喷雾控制';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
//首先获取当前状态
$service = new MqttService();
$status = $service->getStatus();
//判断是否离线,或者状态异常;
if($status && $status['error'] == 0 && $status['status'] == 1){
//获取所有喷雾监控点,对应的自动喷雾配置
$deviceList = Device::where('type', Device::TYPE_ATOMIZING)->get();
$time = now()->format('H:i');//获取当前时间(时,分)
$time = '01:00';
foreach($deviceList as $device){
$_config = $device->extends ?? [];
if($_config && $_config['is_enable']){//判断该配置是否开启
foreach($_config['config'] as $item){
list($start, $end) = explode(',', $item['time_zone']);
//决定开启,关闭,
if($time == $start){//相等
// 如果当前对应位置已开启,则不作为
if(
($item['value'] == 'a' && $status['yv1'] == 0)
||($item['value'] == 'b' && $status['yv2'] == 0)
){
$service->open($item['value'], $item['input']);
//记录触发日志
AtomizingLog::create([
'device_id' => $device->id,
'type' => 1,
'content' => '自动开启【区域'.strtoupper($item['value']).'】,喷雾量'.$item['input'].'%',
]);
}
}elseif($time == $end){
if($status['is_running']){
if(
($item['value'] == 'a' && $status['yv1'] == 1)
||($item['value'] == 'b' && $status['yv2'] == 1)
){
$service->close();
//记录触发日志
AtomizingLog::create([
'device_id' => $device->id,
'type' => 2,
'content' => '自动关闭【区域'.strtoupper($item['value']).'】',
]);
}
}
}
}
}
}
}
return Command::SUCCESS;
}
}

View File

@ -16,6 +16,7 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')->hourly();
$schedule->command('mqtt:penwu-plan')->everyMinute();//每分钟执行
}
/**

View File

@ -0,0 +1,42 @@
<?php
namespace App\Exceptions;
use Exception;
class BizException extends Exception
{
public function __construct(string $message, int $code = 400)
{
parent::__construct($message, $code);
}
/**
* 用于响应的 HTTP 状态代码
*
* @var int
*/
public $status = 200;
/**
* 设置用于响应的 HTTP 状态代码
*
* @param int $status
* @return $this
*/
public function status(int $status)
{
$this->status = $status;
return $this;
}
/**
* 报告异常
*
* @return mixed
*/
public function report()
{
}
}

View File

@ -2,33 +2,30 @@
namespace App\Exceptions;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of exception types with their corresponding custom log levels.
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [
//
];
/**
* A list of the exception types that are not reported.
*
* @var array<int, class-string<\Throwable>>
* @var string[]
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed to the session on validation exceptions.
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array<int, string>
* @var string[]
*/
protected $dontFlash = [
'current_password',
@ -36,6 +33,28 @@ class Handler extends ExceptionHandler
'password_confirmation',
];
/**
* Get the default context variables for logging.
*
* @return array
*/
protected function context()
{
try {
return array_merge(parent::context(), [
'url' => request()->url(),
'params' => request()->except($this->dontFlash),
'accept' => request()->header('accept'),
'client_app' => request()->header('client-app'),
'client_version' => request()->header('client-version'),
'client_platform' => request()->header('client-platform'),
'client_os' => request()->header('client-os'),
]);
} catch (Throwable $e) {
return [];
}
}
/**
* Register the exception handling callbacks for the application.
*
@ -43,8 +62,98 @@ class Handler extends ExceptionHandler
*/
public function register()
{
$this->reportable(function (Throwable $e) {
//
$this->map(ModelNotFoundException::class, function ($e) {
$resource = __($key = 'models.'.$e->getModel());
if ($key === $resource) {
$resource = str_ireplace(['App\\Models\\Admin\\', 'App\\Models\\'], '', $e->getModel());
}
return new HttpException(404, __(':resource not found', ['resource' => $resource]), $e);
});
$this->renderable(function (BizException $e, $request) {
if ($this->shouldReturnJson($request, $e)) {
return response()->json($this->convertExceptionToArray($e), $e->status);
}
if (! config('app.debug')) {
$e = new HttpException($e->status, $e->getMessage(), $e);
}
return $this->prepareResponse($request, $e);
});
$this->renderable(function (ThrottleRequestsException $e, Request $request) {
if ($request->acceptsJson()) {
return response()->json(['code' => 429, 'message' => $e->getMessage()]);
}
return $this->prepareResponse($request, $e);
});
}
/**
* {@inheritdoc}
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
return $this->shouldReturnJson($request, $exception)
? response()->json([
'code' => 401,
'message' => $exception->getMessage(),
], 401)
: redirect()->guest($exception->redirectTo() ?? route('login'));
}
/**
* {@inheritdoc}
*/
protected function invalidJson($request, ValidationException $exception)
{
return response()->json([
'code' => 422,
'message' => $exception->getMessage(),
'errors' => $exception->errors(),
], 200);
}
/**
* {@inheritdoc}
*/
protected function convertExceptionToArray(Throwable $e)
{
$data = [
'code' => 500,
'message' => config('app.debug') ? $e->getMessage() : '服务器繁忙,请稍后再试',
];
if ($this->isBizException($e)) {
$data = [
'code' => $e->getCode(),
'message' => $e->getMessage(),
'data' => null,
];
return $data;
} elseif ($this->isHttpException($e)) {
$data = [
'code' => $e->getStatusCode(),
'message' => $e->getMessage(),
'data' => null,
];
}
return array_merge(parent::convertExceptionToArray($e), $data);
}
/**
* 确认给定的异常是否是自定义业务异常
*
* @param Throwable $e
* @return bool
*/
protected function isBizException(Throwable $e): bool
{
return $e instanceof BizException;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Filters\Admin;
use EloquentFilter\ModelFilter;
class AirLogFilter extends ModelFilter
{
/**
* 所属种植计划
*/
public function device($deviceId)
{
return $this->where('device_id', $deviceId);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Filters\Admin;
use EloquentFilter\ModelFilter;
class AtomizingLogFilter extends ModelFilter
{
/**
* 所属种植计划
*/
public function device($deviceId)
{
return $this->where('device_id', $deviceId);
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Filters\Admin;
use EloquentFilter\ModelFilter;
use App\Models\Device;
use App\Models\MonitorMode;
class DeviceFilter extends ModelFilter
{
/**
* 名称
*/
public function name($name){
return $this->where('name', 'like', '%'.$name.'%');
}
/**
* 厂家
*/
public function factory($factory){
return $this->where('powered_by', $factory);
}
/**
* 类型
*/
public function type($type){
return $this->where('type', $type);
}
public function monitorMode($monitorMode){
if($monitorMode){
$deviceIds = MonitorMode::find($monitorMode)?->devices()->get()->pluck('id')->toArray();
}
return $this->whereIn('id', $deviceIds);
}
/**
* 类型
*/
public function typeName($typeName){
$type = 0;
foreach(Device::typeMap() as $key => $item){
if($item == $typeName){
$type = $key;
break;
}
};
return $this->where('type', $type);
}
/**
* 分组
*/
public function groupTags($groupTags){
return $this->whereRaw("FIND_IN_SET(group_tags,'$groupTags')");
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Filters\Admin;
use EloquentFilter\ModelFilter;
use App\Models\Device;
class MonitorModeFilter extends ModelFilter
{
/**
* 名称
*/
public function name($name){
return $this->where('name', 'like', '%'.$name.'%');
}
/**
* 类型
*/
public function type($type){
return $this->where('type', $type);
}
/**
* 类型
*/
public function typeName($typeName){
$type = 0;
foreach(Device::typeMap() as $key => $item){
if($item == $typeName){
$type = $key;
break;
}
};
return $this->where('type', $type);
}
/**
* 分组
*/
public function groupTags($groupTags){
if(strpos($groupTags, ',')){
$this->where(function($q) use ($groupTags) {
foreach(explode(',', $groupTags) as $tag){
$q->whereRaw("FIND_IN_SET('".$tag."',group_tags)");
}
return $q;
});
}else{
return $this->whereRaw("FIND_IN_SET('".$groupTags."',group_tags)");
}
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Filters\Admin;
use EloquentFilter\ModelFilter;
class PlantHarvestLogFilter extends ModelFilter
{
/**
* 所属种植计划
*/
public function plant($plantId)
{
return $this->where('plant_id', $plantId);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Filters\Admin;
use EloquentFilter\ModelFilter;
class RegionCategoryFilter extends ModelFilter
{
/**
* 名称
*/
public function name($name){
return $this->where('name', 'like', '%'.$name.'%');
}
/**
* 父级分类
*/
public function parent($parentId){
return $this->where('parent_id', $parentId);
}
/**
* 是否开启
*/
public function isEnable($isEnable){
return $this->where('is_enbale', $isEnable);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Filters\Admin;
use EloquentFilter\ModelFilter;
class RegionFilter extends ModelFilter
{
/**
* 名称
*/
public function name($name){
return $this->where('name', 'like', '%'.$name.'%');
}
/**
* 父级分类
*/
public function category($categoryId){
return $this->where('category_id', $categoryId);
}
/**
* 是否开启
*/
public function isEnable($isEnable){
return $this->where('is_enbale', $isEnable);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Filters\Admin;
use EloquentFilter\ModelFilter;
class RegionPlantLogFilter extends ModelFilter
{
/**
* 名称
*/
public function plantName($name){
return $this->where('plant_name', 'like', '%'.$name.'%');
}
/**
* 所属区域
*/
public function region($regionId){
return $this->where('region_id', $regionId);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Filters;
use EloquentFilter\ModelFilter;
class AdminNoticeFilter extends ModelFilter
{
/**
* Related Models that have ModelFilters as well as the method on the ModelFilter
* As [relationMethod => [input_key1, input_key2]].
*
* @var array
*/
public $relations = [];
public function title($v)
{
$this->whereLike('title', $v);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Filters;
use EloquentFilter\ModelFilter;
class ArticleCategoryFilter extends ModelFilter
{
protected $drop_id = false;
/**
* Related Models that have ModelFilters as well as the method on the ModelFilter
* As [relationMethod => [input_key1, input_key2]].
*
* @var array
*/
public $relations = [];
public function name($v)
{
$this->whereLike('name', $v);
}
public function parentId($v)
{
$this->where('parent_id', $v);
}
public function parentPath($v)
{
$this->where('path', 'like', '%-'.$v.'-%');
}
public function level($v)
{
$this->where('level', $v);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Filters;
use EloquentFilter\ModelFilter;
class ArticleFilter extends ModelFilter
{
protected $drop_id = false;
/**
* Related Models that have ModelFilters as well as the method on the ModelFilter
* As [relationMethod => [input_key1, input_key2]].
*
* @var array
*/
public $relations = [];
public function title($v)
{
$this->whereLike('title', $v);
}
public function categoryId($v)
{
$this->where('category_id', $v);
}
public function categoryPath($v)
{
$this->where('category_path', 'like', '%-'.$v.'-%');
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Filters;
use EloquentFilter\ModelFilter;
class BannerFilter extends ModelFilter
{
/**
* Related Models that have ModelFilters as well as the method on the ModelFilter
* As [relationMethod => [input_key1, input_key2]].
*
* @var array
*/
public $relations = [];
public function place($id)
{
$this->where('place_id', $id);
}
public function key($v)
{
$this->whereHas('place', fn ($q) => $q->enable()->whereIn('key', explode(',', $v)));
}
public function name($v)
{
$this->whereLike('name', $v);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Filters;
use EloquentFilter\ModelFilter;
class BannerPlaceFilter extends ModelFilter
{
/**
* Related Models that have ModelFilters as well as the method on the ModelFilter
* As [relationMethod => [input_key1, input_key2]].
*
* @var array
*/
public $relations = [];
public function key($v)
{
$this->whereLike('key', $v);
}
public function name($v)
{
$this->whereLike('name', $v);
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Filters;
use EloquentFilter\ModelFilter;
use App\Models\Device;
class MonitorModeFilter extends ModelFilter
{
public function region($region){
return $this->whereHas('region', function($q)use($region){
return $q->where('regions.id', $region);
});
}
/**
* 类型
*/
public function type($type){
return $this->where('type', $type);
}
/**
* 类型
*/
public function typeName($typeName){
$type = 0;
foreach(Device::typeMap() as $key => $item){
if($item == $typeName){
$type = $key;
break;
}
};
return $this->where('type', $type);
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Filters;
use EloquentFilter\ModelFilter;
class WarningNoticeFilter extends ModelFilter
{
public function lv($lv){
return $this->where('lv', $lv);
}
public function status($status)
{
return $this->where('status', $status);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Helpers;
class Paginator
{
/**
* 解析每页显示的条数
*
* @param string $perPageName
* @param int $default
* @param int|null $max
* @return int
*/
public static function resolvePerPage(string $perPageName = 'per_page', int $default = 20, ?int $max = null): int
{
$perPage = (int) request()->input($perPageName);
if ($perPage >= 1) {
if ($max !== null && $max >= 1 && $perPage >= $max) {
return $max;
}
return $perPage;
}
return $default;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\AdminNotice;
use App\Http\Resources\AdminNoticeResource;
class AdminNoticeController extends Controller
{
public function index(Request $request)
{
$query = AdminNotice::filter($request->all())->sort()->enable();
$list = $query->get();
return $this->json(AdminNoticeResource::collection($list));
}
public function show($id)
{
$info = AdminNotice::enable()->findOrFail($id);
return $this->json(AdminNoticeResource::make($info));
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\{Article, ArticleCategory};
use App\Http\Resources\{ArticleResource, ArticleCategoryResource};
class ArticleController extends Controller
{
public function category(Request $request)
{
$query = ArticleCategory::filter($request->all())->enable()->sort();
$list = $query->get();
return $this->json(ArticleCategoryResource::collection($list));
}
public function tree(Request $request)
{
$pid = $request->input('parent_id');
$list = ArticleCategory::filter(['parent_path' => $pid])->enable()->sort()->select(['id', 'name', 'icon', 'parent_id'])->get()->toArray();
return $this->json(array2tree($list, $pid ?? 0));
}
public function index(Request $request)
{
$query = Article::filter($request->all())->enable()->sort();
$list = $query->paginate($request->input('per_page'));
return $this->json(ArticleResource::collection($list));
}
public function show($id)
{
$info = Article::enable()->findOrFail($id);
return $this->json(ArticleResource::make($info));
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Slowlyo\OwlAdmin\Models\AdminUser;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
public function login(Request $request)
{
$request->validate([
'username' => 'required',
'password' => 'required',
]);
$user = AdminUser::where(['username' => $request->input('username')])->first();
if (! $user) {
return $this->error('用户名或密码错误');
}
if (! Hash::check($request->input('password'), $user->password)) {
return $this->error('用户名或密码错误');
}
// if ($user->is_enable !== 1) {
// return $this->error('用户状态异常请联系管理员');
// }
return $this->attemptUser($user);
}
protected function attemptUser(AdminUser $user, $name = 'api')
{
$token = $user->createToken($name)->plainTextToken;
return $this->json(['token' => $token]);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Banner;
use App\Http\Resources\BannerResource;
class BannerController extends Controller
{
public function index(Request $request)
{
$request->validate([
'key' => 'required',
]);
$query = Banner::with(['place'])->filter($request->all())->sort()->enable();
$list = $query->get();
return $this->json(BannerResource::collection($list));
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\{Device, MonitorDevice, RegionMonitor};
use DB;
class DeviceController extends Controller
{
/**
* 统计某个实验田下所有设备状态数量
*/
public function typeStateNum(Request $request)
{
$regionId = $request->input('region_id', 0);
$monitorId = $request->input('monitor_id', 0);
$query = Device::query();
if($monitorId){
$deviceIds = MonitorDevice::where('monitor_id', $monitorId)->pluck('id')->toArray();
$query->whereIn('id', $deviceIds);
}elseif($regionId){
$monitorIds = RegionMonitor::where('region_id', $regionId)->pluck('id')->toArray();
if(count($monitorIds) > 0){
$deviceIds = MonitorDevice::whereIn('monitor_id', $monitorIds)->pluck('id')->toArray();
$query->whereIn('id', $deviceIds);
}
}
$query->groupBy('type')->groupBy('state');
$list = $query->select(DB::raw('type, state, count(1) as num '))->get();
$resData = [];
foreach ($list as $item) {
$resData[$item->type][$item->state] = $item->num;
}
//初始化数据;
$data = [];
foreach (Device::typeMap() as $typeKey => $typeName) {
foreach (Device::stateMap() as $statusKey => $statusName) {
$data[$typeKey][$statusKey] = $resData[$typeKey][$statusKey] ?? 0;
}
}
return $this->json($data);
}
/**
* 农机短信提醒-todo
*/
public function deviceNotice(){
return $this->success('操作成功');
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Http\Controllers\Api;
use App\Filters\MonitorModeFilter;
use App\Http\Controllers\Controller;
use App\Http\Resources\DeviceResource;
use App\Http\Resources\MonitorModeResource;
use App\Models\MeteorologicalDailyReport;
use App\Models\MonitorMode;
use App\Services\Admin\DeviceService;
use Illuminate\Http\Request;
class MonitorModeController extends Controller
{
/**
* 获取监控点列表;
*/
public function getMonitorMode(Request $request)
{
$monitors = MonitorMode::filter($request->all(), MonitorModeFilter::class)->get();//有推荐,排序字段,不过目前没开放;
return $this->json(MonitorModeResource::collection($monitors));
}
/**
* 获取监控点位数据-可指定字段
*/
public function getMonitorDeviceData(Request $request)
{
$monitorId = $request->input('monitor_id');
$column = $request->input('column'); //指定字段
//监控类型气象土壤为折线图数据虫情数据待定监控设备rtsp流其他类型无数据
$data = [];
$monitor = MonitorMode::find($monitorId);
if($monitor){
switch($monitor->type){
case MonitorMode::TYPE_MONITOR://监控视频
$data = DeviceResource::collection($monitor->devices);
break;
case MonitorMode::TYPE_SOIL://土壤设备--只拿最近6小时数据
$startTime = now()->subHours(6)->format('Y-m-d H:i:s');
$endTime = now()->format('Y-m-d H:i:s');
$data = (new DeviceService())->getMonitorModeDeviceData($monitor, $startTime, $endTime);
if($column){
$data = $data[$column] ?? [];
}
break;
case MonitorMode::TYPE_METEOROLOGICAL://气象设备--只拿最新的一个值
$fieldNameMap = MonitorMode::fieldMap(MonitorMode::TYPE_METEOROLOGICAL);
$fieldUnitMap = MonitorMode::fieldUnitMap(MonitorMode::TYPE_METEOROLOGICAL);
foreach($monitor->devices as $device){
$_fields = explode(',', $device->pivot->fields);
$reportData = MeteorologicalDailyReport::where('device_id', $device->id)->orderBy('reported_at', 'desc')->first()?->toArray() ?? [];
foreach($_fields as $field){
$data[$field] = [
'name' => $fieldNameMap[$field],
'unit' => $fieldUnitMap[$field],
'value' => $reportData[$field] ?? 0,
];
if($field == 'wind_direction'){//单独处理风向
$formatData = ['北风','东北风','东风','东南风','南风','西南风','西风','西北风'];
$data[$field]['value'] = $formatData[$reportData[$field]]??'未知';
}
}
}
break;
case MonitorMode::TYPE_INSECT://虫情设备-todo
break;
default:
break;
}
}
return $this->json($data);
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\{Region, RegionCategory};
use App\Http\Resources\{RegionResource, RegionCategoryResource, RegionPlantResource, PlantHarvestResource};
use App\Filters\Admin\RegionFilter;
class RegionController extends Controller
{
public function category(Request $request)
{
$list = RegionCategory::query()->show()->sort()->get();
return $this->json(RegionCategoryResource::collection($list));
}
public function index(Request $request)
{
$query = Region::with(['currentPlant'])->filter($request->all(), RegionFilter::class)->sort()->show();
$list = $query->paginate($request->input('per_page'));
return $this->json(RegionResource::collection($list));
}
public function show($id)
{
$info = Region::with(['currentPlant'])->show()->findOrFail($id);
return $this->json(RegionResource::make($info));
}
public function plants($id, Request $request)
{
$info = Region::show()->findOrFail($id);
$query = $info->plants();
$per = $request->input('per_page');
if ($per == -1) {
$list = $query->get();
} else {
$list = $query->paginate($per);
}
return $this->json(RegionPlantResource::collection($list));
}
public function harvests($id, Request $request)
{
$info = Region::show()->findOrFail($id);
$query = $info->harvests()->filter($request->all());
$per = $request->input('per_page');
if ($per == -1) {
$list = $query->get();
} else {
$list = $query->paginate($per);
}
return $this->json(PlantHarvestResource::collection($list));
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class SettingController extends Controller
{
/**
* 基础数据信息-最多返回6项
*/
public function staticBaseData()
{
$baseData = settings()->get('region_base') ?? [];
$data = [];
foreach ($baseData as $index => $item) {
if($index <= 6){
$data[] = [
'name' => $item['name'],
'value' => number_format($item['value']),
'unit' => $item['unit'],
];
}else{
break;//最多放6个
}
}
return $this->json($data);
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
class UserController extends Controller
{
public function logout()
{
$user = auth()->user();
// $user->tokens()->delete();
$user->currentAccessToken()->delete();
return $this->success('退出成功');
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\Paginator;
use App\Http\Controllers\Controller;
use App\Models\WarningNotice;
use Illuminate\Http\Request;
use App\Filters\WarningNoticeFilter;
use App\Http\Resources\WarningNoticeResource;
use DB;
class WarningNoticeController extends Controller
{
/**
* 获取警报数量
*/
public function warningLogNum(Request $request){
$list = WarningNotice::filter($request->input(), WarningNoticeFilter::class)
->select(DB::raw('lv, count(1) as num'))
->groupBy('lv')
->get()
->pluck('num', 'lv')->toArray();
for($i = 1; $i <= 4; $i++) {
$data[$i] = $list[$i] ?? 0;
}
return $this->json($data);
}
/**
* 获取警报记录
*
* @return void
*/
public function warningLog(Request $request)
{
$query = WarningNotice::with(['device.modes'])->filter($request->input(), WarningNoticeFilter::class)->orderBy('created_at', 'desc');
$list = $query->simplePaginate(Paginator::resolvePerPage('per_page', 20, 50));
return $this->json(WarningNoticeResource::collection($list));
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Callback;
use App\Http\Controllers\Controller;
use App\Models\Device;
use Illuminate\Http\Request;
class LinkosCallbackController extends Controller
{
public function __invoke(Request $request)
{
logger()->debug('linkos callback parameters -------->', $request->input());
if ($request->filled('notify_type')) {
switch ($request->notify_type) {
case 'online_state_change':
$this->handleDeviceStateNotify($request);
break;
}
}
return response()->json(['code' => 0, 'msg' => 'ok']);
}
/**
* 设备状态通知
*/
protected function handleDeviceStateNotify(Request $request): void
{
foreach ($request->input('data', []) as $item) {
if (! isset($item['device_id'])) {
continue;
}
$device = Device::where('sn', $item['device_id'])->first();
if ($device === null) {
continue;
}
if (isset($item['online_state'])) {
switch ($item['online_state']) {
case 0:
$device->forceFill([
'state' => Device::STATE_OFFLINE,
])->save();
break;
case 1:
$device->forceFill([
'state' => Device::STATE_ONLINE,
])->save();
break;
}
}
}
}
}

View File

@ -6,8 +6,30 @@ use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Support\Arr;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
public function json($data, $code = 0, $message = '')
{
if ($data instanceof ResourceCollection) {
$data = $data->resource;
}
$result = ['data' => $data ?: '', 'status' => $code, 'msg' => $message];
return response()->json($result);
}
public function success($message = '', $data = null)
{
return $this->json($data, 0, $message);
}
public function error($message = '', $data = null)
{
return $this->json($data, 1, $message);
}
}

View File

@ -12,6 +12,6 @@ class VerifyCsrfToken extends Middleware
* @var array<int, string>
*/
protected $except = [
//
'callback/*'
];
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class AdminNoticeResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => $this->content,
'article_id' => $this->article_id,
'sort' => $this->sort,
'published_at' => $this->published_at ? $this->published_at->timestamp : '',
'created_at' => $this->created_at->timestamp
];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ArticleCategoryResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'icon' => $this->icon,
'name' => $this->name,
'parent_id' => $this->parent_id,
'level' => $this->level,
'path' => $this->path,
'sort' => $this->sort,
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ArticleResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'sub_title' => $this->sub_title,
'category_id' => $this->category_id,
'category' => ArticleCategoryResource::make($this->whenLoaded('category')),
'content' => $this->content,
'cover' => $this->cover,
'published_at' => $this->published_at ? $this->published_at->timestamp : '',
'created_at' => $this->created_at->timestamp
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class BannerResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'place_id' => $this->place_id,
'key' => data_get($this->whenLoaded('place'), 'key'),
'name' => $this->name,
'picture' => $this->picture,
'link_config' => $this->link_config,
'sort' => $this->sort,
'published_at' => $this->published_at ? $this->published_at->timestamp : '',
'created_at' => $this->created_at->timestamp,
];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Resources;
use App\Models\Device;
use Illuminate\Http\Resources\Json\JsonResource;
class DeviceResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'type' => $this->type,
'type_name' => Device::typeMap()[$this->type] ?? '未知',
'state' => $this->state,
'state_name' => Device::stateMap()[$this->state] ?? '未知',
'extends' => $this->extends ?? [],
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class MonitorModeResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'type' => $this->type,
'name' => $this->name
];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class PlantHarvestResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'plant_id' => $this->plant_id,
'region_id' => $this->region_id,
'director' => $this->director,
'area' => $this->area,
'output' => $this->output,
'harvest_at' => $this->harvest_at ? $this->harvest_at->timestamp : null,
];
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class RegionCategoryResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'icon' => $this->icon,
'description' => $this->description,
];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class RegionPlantResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'plant_name' => $this->plant_name,
'director' => $this->director,
'area' => $this->area,
'start_at' => $this->start_at ? $this->start_at->timestamp : null,
'end_at' => $this->end_at ? $this->end_at->timestamp : null,
'plant_state' => $this->plant_state,
];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class RegionResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'cover' => $this->cover,
'director' => $this->director,
'area' => $this->area,
'description' => $this->description,
'category_id' => $this->category_id,
'category' => RegionCategoryResource::make($this->whenLoaded('category')),
'current_plant' => RegionPlantResource::make($this->whenLoaded('currentPlant')),
'position_x'=> $this->position_x,
'position_y'=> $this->position_y,
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class WarningNoticeResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'lv' => $this->lv,
'monitor_modes' => MonitorModeResource::collection($this->whenLoaded('device')->modes),
'status' => $this->status,
'content' => $this->content,
'created_at' => strtotime($this->created_at) ?? 0, //录入时间
];
}
}

View File

@ -0,0 +1,160 @@
<?php
namespace App\Iot\Linkos;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class HttpClient
{
public const ENDPOINT_URL = 'http://service.easylinkin.com';
public function __construct(
protected readonly string $apiKey,
protected readonly string $apiSecret,
) {
}
/**
* 获取设备的历史数据
*
* @param string $deviceId
* @param \Illuminate\Support\Carbon $start
* @param \Illuminate\Support\Carbon $end
* @param int $page
* @param int $perPage
* @return array
*/
public function deviceFlowList(string $deviceId, Carbon $start, Carbon $end, int $page = 1, int $perPage = 50): array
{
$result = $this->post('/api/deviceFlow/v1/list', [
'device_id' => $deviceId,
'start_time' => $start->unix() * 1000,
'end_time' => $end->unix() * 1000,
'pageable' => [
'page' => $page - 1,
'size' => $perPage,
],
]);
if (data_get($result, 'success') !== true) {
throw new RuntimeException(data_get($result, 'msg', '出错啦!'));
}
return $result['data'];
}
/**
* 设备数据下行
*
* @param string $deviceId
* @param string $service
* @param array $data
* @param boolean $confirm
* @param boolean $clear
* @param boolean $schedule
* @return array
*/
public function deviceDataDownlink(string $deviceId, string $service, array $data = [], bool $confirm = true, bool $clear = true, bool $schedule = false): array
{
return $this->post('/api/down', [
'device_id' => $deviceId,
'service_id' => $service,
'parameter' => $data,
'clear' => (int) $clear,
'schedule' => (int) $schedule,
'confirm' => (int) $confirm,
]);
}
/**
* 获取设备最新属性数据
*/
public function getDeviceStatus(string $deviceId, array $props): array
{
$result = $this->get('/api/deviceStatus/v1/getDeviceStatus', [
'deviceCode' => $deviceId,
'prop' => implode(",", $props),
]);
if (data_get($result, 'success') !== true) {
throw new RuntimeException(data_get($result, 'msg', '出错啦!'));
}
return $result['data'];
}
public function get(string $url, array $query = []): array
{
return $this->request('GET', $url, [
'query' => $query,
]);
}
/**
* @param string $url
* @param array $data
* @return array
*/
public function post(string $url, array $data = []): array
{
return $this->request('POST', $url, [
'json' => $data,
]);
}
/**
* @param string $method
* @param string $url
* @param array $options
* @return array
*
* @throws \Illuminate\Http\Client\RequestException
* @throws \RuntimeException
*/
public function request(string $method, string $url, array $options = []): array
{
$nonce = $this->nonce();
$timestamp = now()->getTimestampMs();
/** @var \Illuminate\Http\Client\Response */
$response = Http::withHeaders([
'Content-Type' => 'application/json',
'api-key' => $this->apiKey,
'Nonce' => $nonce,
'Timestamp' => $timestamp,
'Signature' => $this->sign(compact('nonce', 'timestamp')),
])->baseUrl(self::ENDPOINT_URL)->send($method, $url, $options);
return $response->throw()->json();
}
/**
* @param array $data
* @return string
*/
protected function sign(array $data): string
{
return sha1(
sprintf(
'%s%s%s',
$data['nonce'] ?? '',
$data['timestamp'] ?? '',
$this->apiSecret
)
);
}
protected function nonce(): string
{
$nonce = '';
for ($i = 0; $i < 8; $i++) {
$nonce .= mt_rand(0, 9);
}
return $nonce;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use EloquentFilter\Filterable;
/**
* 公告
*/
class AdminNotice extends Model
{
use Filterable;
protected $fillable = ['title', 'content', 'article_id', 'remark', 'is_enable', 'published_at' ,'sort'];
protected $casts = [
'published_at' => 'datetime:Y-m-d H:i:s',
'created_at' => 'datetime:Y-m-d H:i:s',
'updated_at' => 'datetime:Y-m-d H:i:s',
'is_enable' => 'boolean'
];
public function article()
{
return $this->belongsTo(Article::class, 'article_id');
}
public function scopeSort($q)
{
return $q->orderBy('sort', 'desc')->orderBy('published_at', 'desc');
}
public function scopeEnable($q)
{
return $q->where('is_enable', 1)->where('published_at', '<=', now());
}
public function modelFilter()
{
return \App\Filters\AdminNoticeFilter::class;
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use EloquentFilter\Filterable;
class AirLog extends Model
{
use Filterable;
protected $fillable = ['device_id', 'type', 'content', 'created_at', 'updated_at'];
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Models;
use App\Casts\Storage;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Model;
/**
* 文章
*/
class Article extends Model
{
use Filterable;
protected $fillable = ['author', 'category_id', 'category_path', 'content', 'cover', 'is_enable', 'is_recommend', 'published_at', 'sort', 'sub_title', 'title'];
protected $casts = [
'created_at' => 'datetime:Y-m-d H:i:s',
'updated_at' => 'datetime:Y-m-d H:i:s',
'published_at' => 'datetime:Y-m-d H:i:s',
'is_enable' => 'boolean',
'is_recommend' => 'boolean',
'cover' => Storage::class,
];
public function category()
{
return $this->belongsTo(ArticleCategory::class, 'category_id');
}
public function scopeSort($q)
{
return $q->orderBy('sort', 'desc')->orderBy('published_at', 'desc');
}
public function scopeEnable($q)
{
return $q->where('is_enable', 1)->where('published_at', '<=', now());
}
public function modelFilter()
{
return \App\Filters\ArticleFilter::class;
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* 文章分类
*/
class ArticleCategory extends Model
{
use Filterable;
protected $casts = [
'created_at' => 'datetime:Y-m-d H:i:s',
'updated_at' => 'datetime:Y-m-d H:i:s',
'is_enable' => 'boolean',
];
protected $fillable = ['icon', 'is_enable', 'level', 'name', 'parent_id', 'path', 'sort'];
protected function icon(): Attribute
{
return Attribute::make(
get: fn($value) => $value ? (Str::startsWith($value, ['http://', 'https://']) ? $value : Storage::url($value)) : '',
);
}
public function parent()
{
return $this->belongsTo(static::class, 'parent_id');
}
public function children()
{
return $this->hasMany(static::class, 'parent_id');
}
public function articles()
{
return $this->hasMany(Article::class, 'category_id');
}
public function scopeSort($q)
{
return $q->orderBy('sort', 'desc');
}
public function scopeEnable($q)
{
return $q->where('is_enable', 1);
}
public function modelFilter()
{
return \App\Filters\ArticleCategoryFilter::class;
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use EloquentFilter\Filterable;
class AtomizingLog extends Model
{
use Filterable;
protected $fillable = ['device_id', 'type', 'content', 'created_at', 'updated_at'];
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Models;
use App\Casts\Storage;
use Illuminate\Support\Str;
use App\Filters\BannerFilter;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Model;
/**
* 广告图
*/
class Banner extends Model
{
use Filterable;
protected $fillable = ['is_enable', 'link_config', 'name', 'picture', 'place_id', 'published_at', 'sort'];
protected $casts = [
'created_at' => 'datetime:Y-m-d H:i:s',
'updated_at' => 'datetime:Y-m-d H:i:s',
'published_at' => 'datetime:Y-m-d H:i:s',
'is_enable' => 'boolean',
'link_config' => 'json',
'picture' => Storage::class,
];
public function place()
{
return $this->belongsTo(BannerPlace::class, 'place_id');
}
public function scopeSort($q)
{
return $q->orderBy('sort', 'desc');
}
public function scopeEnable($q)
{
return $q->where('is_enable', 1)->where('published_at', '<=', now());
}
public function modelFilter()
{
return BannerFilter::class;
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use EloquentFilter\Filterable;
/**
* 广告位
*/
class BannerPlace extends Model
{
use Filterable;
protected $fillable = ['height', 'is_enable', 'key', 'name', 'remark', 'width'];
protected $casts = [
'created_at' => 'datetime:Y-m-d H:i:s',
'updated_at' => 'datetime:Y-m-d H:i:s',
'is_enable' => 'boolean',
];
public function banners()
{
return $this->hasMany(Banner::class, 'place_id');
}
public function scopeEnable($q)
{
return $q->where('is_enable', 1);
}
public function scopeSort($q)
{
return $q->orderBy('id', 'desc');
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Device extends Model
{
use Filterable;
public const TYPE_MONITOR = 1; //监控设备
public const TYPE_SOIL = 2; //土壤设备
public const TYPE_WATER_QUALITY = 3; //水质设备
public const TYPE_METEOROLOGICAL = 4; //气象设备
public const TYPE_AIR = 5; //通风设备
public const TYPE_ATOMIZING = 6; //喷雾设备
public const TYPE_INSECT = 7; //虫情监测
public const STATE_DISABLED = 0;
public const STATE_ONLINE = 1;
public const STATE_OFFLINE = 2;
public const STATE_FAULT = 3;
protected $casts = [
'extends' => 'json'
];
protected $fillable = [
'name', 'sn', 'powered_by', 'type', 'model_sn', 'state', 'extends'
];
public function scopePoweredBy(Builder $query, string $factory): void
{
$query->whereHas('factory', fn ($query) => $query->where('key', $factory));
}
protected function serializeDate(\DateTimeInterface $date){
return $date->format('Y-m-d H:i:s');
}
public static function typeMap()
{
return [
self::TYPE_MONITOR => '监控设备',
self::TYPE_SOIL => '土壤设备',
// self::TYPE_WATER_QUALITY => '水质设备',
self::TYPE_METEOROLOGICAL => '气象设备',
self::TYPE_AIR => '通风设备',
self::TYPE_ATOMIZING => '喷雾设备',
self::TYPE_INSECT => '虫情监测',
];
}
public static function stateMap(){
return [
self::STATE_DISABLED => '禁用',
self::STATE_ONLINE => '在线',
self::STATE_OFFLINE => '离线',
self::STATE_FAULT => '故障',
];
}
public function modes(){
return $this->belongsToMany(MonitorMode::class, MonitorDevice::class, 'device_id', 'monitor_id')->withPivot('fields');
}
public function logs(): HasMany
{
return $this->hasMany(DeviceLog::class);
}
public function factory()
{
return $this->belongsTo(Keyword::class, 'powered_by');
}
public function isTypeSoil(): bool
{
return $this->type === static::TYPE_SOIL;
}
public function isTypeMeteorological(): bool
{
return $this->type === static::TYPE_METEOROLOGICAL;
}
public function isTypeAir(): bool
{
return $this->type === static::TYPE_AIR;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class DeviceLog extends Model
{
use HasFactory;
protected $casts = [
'data' => 'json',
'reported_at' => 'datetime',
];
protected $fillable = [
'device_id',
'data',
'reported_at',
];
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class FriendLink extends Model
{
use HasFactory;
}

View File

@ -5,6 +5,8 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Builder;
use App\Admin\Components;
class Keyword extends Model
{
@ -12,6 +14,10 @@ class Keyword extends Model
use Filterable;
protected $fillable = ['name', 'key', 'value', 'parent_id', 'type_key', 'path', 'sort', 'level'];
protected function serializeDate(\DateTimeInterface $date){
return $date->format('Y-m-d H:i:s');
}
protected static function boot()
{
@ -44,4 +50,15 @@ class Keyword extends Model
return $this->hasMany(static::class, 'parent_id');
}
public static function getByParentKey(String $key)
{
return self::query()->where('type_key', $key)->get();
}
public static function tagsMap(String $key){
$list = self::query()->where('type_key', $key)->get()->pluck('name', 'id')->toArray();
return array_map(function($item){
return Components::make()->keywordsTag($item);
}, $list);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class MeteorologicalDailyReport extends Model
{
use HasFactory;
const WIND_DIRECTION_NORTH = 0;
const WIND_DIRECTION_NORTHEAST = 1;
const WIND_DIRECTION_EAST = 2;
const WIND_DIRECTION_SOUTHEAST = 3;
const WIND_DIRECTION_SOUTH = 4;
const WIND_DIRECTION_SOUTHWEST = 5;
const WIND_DIRECTION_WEST = 6;
const WIND_DIRECTION_NORTHWEST = 7;
protected $casts = [
'reported_at' => 'date'
];
protected $fillable = [
'device_id',
'today_rainfall',
'yesterday_rainfall',
'accumulate_rainfall',
'moment_rainfall',
'pm10',
'pm25',
'box_illumination',
'box_pressure',
'box_co2',
'box_temperature',
'box_humidity',
'box_noise',
'wind_degree',
'wind_direction',
'wind_power',
'wind_speed',
'reported_at',
];
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class MeteorologicalReport extends Model
{
use HasFactory;
protected $casts = [
'reported_at' => 'datetime'
];
protected $fillable = [
'device_id',
'today_rainfall',
'yesterday_rainfall',
'accumulate_rainfall',
'moment_rainfall',
'pm10',
'pm25',
'box_illumination',
'box_pressure',
'box_co2',
'box_temperature',
'box_humidity',
'box_noise',
'wind_degree',
'wind_direction',
'wind_power',
'wind_speed',
'reported_at',
];
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class MonitorDevice extends Model
{
use HasFactory;
public $timestamps = false;
}

View File

@ -0,0 +1,132 @@
<?php
namespace App\Models;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
class MonitorMode extends Model
{
use Filterable;
protected $fillable = [
'name', 'type',
'is_enable', 'sort', 'is_recommend',
'group_tags'
];
protected $appends = ['tags'];
public const TYPE_MONITOR = 1; //视频监控
public const TYPE_SOIL = 2; //土壤监测
public const TYPE_WATER_QUALITY = 3; //水质监测
public const TYPE_METEOROLOGICAL = 4; //气象监测
public const TYPE_AIR = 5; //通风控制
public const TYPE_ATOMIZING = 6; //喷雾控制
public const TYPE_INSECT = 7; //虫情监测
public static function typeMap()
{
return [
self::TYPE_MONITOR => '视频监控',
self::TYPE_SOIL => '土壤监测',
// self::TYPE_WATER_QUALITY => '水质监测',
self::TYPE_METEOROLOGICAL => '气象监测',
self::TYPE_AIR => '通风控制',
self::TYPE_ATOMIZING => '喷雾控制',
self::TYPE_INSECT => '虫情监测',
];
}
public static function fieldMap($type)
{
$arr = [];
switch ($type) {
case self::TYPE_SOIL:
$arr = [
'conductivity'=>'导电率',
'humidity'=>'湿度',
'temperature'=>'温度',
'n'=>'氮',
'p'=>'磷',
'k'=>'钾'
];
break;
case self::TYPE_WATER_QUALITY:
$arr = [
];
break;
case self::TYPE_METEOROLOGICAL:
$arr = [
'box_temperature' => '温度',
'box_humidity' => '湿度',
'box_illumination' => '光照强度',
'moment_rainfall' => '降雨量',
'wind_speed' => '风速',
'wind_direction' => '风向',
'box_noise' => '噪音',
'pm10' => 'PM10',
'pm25' => 'PM25',
'box_co2' => 'CO2'
];
break;
}
return $arr;
}
public static function fieldUnitMap($type)
{
$arr = [];
switch ($type) {
case self::TYPE_SOIL:
$arr = [
'conductivity'=>'us/cm',
'humidity'=>'%RH',
'temperature'=>'℃',
'n'=>'n',
'p'=>'p',
'k'=>'k'
];
break;
case self::TYPE_WATER_QUALITY:
$arr = [
];
break;
case self::TYPE_METEOROLOGICAL:
$arr = [
'box_temperature' => '℃',
'box_humidity' => '%RH',
'box_illumination' => 'Lux',
'moment_rainfall' => 'mm',
'wind_speed' => 'm/s',
'wind_direction' => '',
'box_noise' => 'db',
'pm10' => 'ug/m3',
'pm25' => 'ug/m3',
'box_co2' => 'ppm'
];
break;
}
return $arr;
}
protected function serializeDate(\DateTimeInterface $date){
return $date->format('Y-m-d H:i:s');
}
protected function tags():Attribute
{
return Attribute::make(
get: fn($value) => $this->group_tags ? explode(',', $this->group_tags) : [],
);
}
public function region(){
return $this->belongsToMany(Region::class, RegionMonitor::class, 'monitor_id', 'region_id')->withTimestamps();
}
public function devices(){
return $this->belongsToMany(Device::class, MonitorDevice::class, 'monitor_id', 'device_id')->withPivot('fields');
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Models;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Model;
/**
* 收获记录
*/
class PlantHarvestLog extends Model
{
use Filterable;
protected $casts = [
'harvest_at' => 'datetime:Y-m-d',
];
public function plant()
{
return $this->belongsTo(RegionPlantLog::class, 'plant_id');
}
public function modelFilter()
{
return \App\Filters\Admin\PlantHarvestLogFilter::class;
}
}

View File

@ -0,0 +1,144 @@
<?php
namespace App\Models;
use App\Casts\Storage;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
/**
* 实验田
*/
class Region extends Model
{
use Filterable;
protected $fillable = [
'name', 'cover', 'director', 'area', 'description', 'category_id',
'sort', 'is_recommend','is_enable',
'position_x', 'position_y'
];
protected $casts = [
'cover' => Storage::class,
];
protected function serializeDate(\DateTimeInterface $date){
return $date->format('Y-m-d H:i:s');
}
public function scopeShow($q)
{
return $q->where('is_enable', 1);
}
public function scopeSort($q)
{
return $q->orderBy('sort', 'desc')->latest('id');
}
// 试验田分类
public function category()
{
return $this->belongsTo(RegionCategory::class, 'category_id');
}
public function monitorModes(){
return $this->belongsToMany(MonitorMode::class, RegionMonitor::class, 'region_id', 'monitor_id')->withTimestamps()->withPivot('config');
}
// 种植记录
public function plants()
{
return $this->hasMany(RegionPlantLog::class, 'region_id')->orderBy('start_at', 'desc');
}
// 当前种植
public function currentPlant()
{
return $this->hasOne(RegionPlantLog::class, 'region_id')->whereNull('end_at')->orderBy('start_at', 'desc');
}
// 收货记录
public function harvests()
{
return $this->hasMany(PlantHarvestLog::class, 'region_id')->orderBy('harvest_at', 'desc');
}
public static function regionTabConfig($region = null){
$tabs = [
[
'title' => '基地详情',
'value' => 'detail',
'tab'=>\amisMake()->Service()->schemaApi(admin_url('custom-region-detail?id='.($region?->id ?? '${id}'))),
'unmountOnExit' => true,//每次切换tab都要销毁
'sort' => 0,
],
];
if($region?->monitorModes){
foreach($region->monitorModes as $monitorMode){
switch($monitorMode->type)
{
case MonitorMode::TYPE_MONITOR:
$tabs[] = [//有监控设备才有
'title' => '监控视频',
'value' => 'monitor',
'tab'=>\amisMake()->Service()->schemaApi(admin_url('custom-region-monitor?region_id='.$region->id)),
'unmountOnExit' => true,//每次切换tab都要销毁
'sort' => 1,
];
break;
case MonitorMode::TYPE_SOIL:
$tabs[] = [//有土壤设备才有
'title' => '土壤数据',
'value' => 'soil',
'tab'=>\amisMake()->Service()->schemaApi(admin_url('custom-region-soil?region_id='.$region->id)),
'unmountOnExit' => true,//每次切换tab都要销毁
'sort' => 2,
];
break;
case MonitorMode::TYPE_WATER_QUALITY:
$tabs[] = [//有水质设备才有
'title' => '水质数据',
'value' => 'quality',
'tab'=>\amisMake()->Service()->schemaApi(admin_url('custom-region-water?region_id='.$region->id)),
'unmountOnExit' => true,//每次切换tab都要销毁
'sort' => 3,
];
break;
case MonitorMode::TYPE_METEOROLOGICAL:
$tabs[] = [//有气象设备才有
'title' => '气象数据',
'value' => 'meteorological',
'tab'=>\amisMake()->Service()->schemaApi(admin_url('custom-region-meteorological?region_id='.$region->id)),
'unmountOnExit' => true,//每次切换tab都要销毁
'sort' => 4,
];
break;
case MonitorMode::TYPE_AIR:
$tabs[] = [//有通风设备才有
'title' => '通风设备',
'value' => 'air',
'tab'=>\amisMake()->Service()->schemaApi(admin_url('custom-region-air?region_id='.$region->id)),
'unmountOnExit' => true,//每次切换tab都要销毁
'sort' => 5,
];
break;
case MonitorMode::TYPE_ATOMIZING:
$tabs[] = [//有喷雾设备才有
'title' => '喷雾设备',
'value' => 'atomizing',
'tab'=>\amisMake()->Service()->schemaApi(admin_url('custom-region-atomizing?region_id='.$region->id)),
'unmountOnExit' => true,//每次切换tab都要销毁
'sort' => 6,
];
break;
}
continue;
}
}
$tabs = array_merge(collect($tabs)->sortBy('sort')->toArray(), []);
return $tabs;
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use EloquentFilter\Filterable;
use App\Casts\Storage;
/**
* 区域分类
*/
class RegionCategory extends Model
{
use HasFactory;
use Filterable;
protected $fillable = ['name', 'icon', 'description', 'parent_id', 'level', 'sort', 'path', 'is_enable'];
protected $casts = [
'icon' => Storage::class,
];
protected function serializeDate(\DateTimeInterface $date){
return $date->format('Y-m-d H:i:s');
}
protected static function boot()
{
parent::boot();
// 监听 Category 的创建事件,用于初始化 path 和 level 字段值
static::creating(function ($category) {
// 如果创建的是一个根类目
if (! $category->parent_id) {
// 将层级设为 1
$category->level = 1;
// 将 path 设为 -
$category->path = '-';
} else {
// 将层级设为父类目的层级 + 1
$category->level = $category->parent->level + 1;
// 将 path 值设为父类目的 path 追加父类目 ID 以及最后跟上一个 - 分隔符
$category->path = $category->parent->path.$category->parent_id.'-';
}
});
}
public function parent()
{
return $this->belongsTo(self::class, 'parent_id');
}
public function scopeShow($q)
{
return $q->where('is_enable', 1);
}
public function scopeSort($q)
{
return $q->orderBy('sort', 'desc')->latest('id');
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class RegionMonitor extends Model
{
use HasFactory;
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Models;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
/**
* 种植记录
*/
class RegionPlantLog extends Model
{
use Filterable;
protected $appends = ['plant_state'];
protected $casts = [
'start_at' => 'datetime:Y-m-d',
'end_at' => 'datetime:Y-m-d',
];
//1进行中2已结束
protected function plantState():Attribute
{
return Attribute::make(
get: fn($value) => $this->end_at ? 2 : 1,
);
}
// 收获记录
public function harvestes()
{
return $this->hasMany(PlantHarvestLog::class, 'plant_id');
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class SoilDailyReport extends Model
{
use HasFactory;
protected $casts = [
'reported_at' => 'date',
];
protected $fillable = [
'device_id',
'temperature',
'humidity',
'n',
'p',
'k',
'conductivity',
'moisture',
'reported_at',
];
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class SoilReport extends Model
{
use HasFactory;
protected $casts = [
'reported_at' => 'datetime',
];
protected $fillable = [
'device_id',
'temperature',
'humidity',
'n',
'p',
'k',
'conductivity',
'moisture',
'reported_at',
];
}

Some files were not shown because too many files have changed in this diff Show More