git init
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
---
|
||||
description:
|
||||
alwaysApply: true
|
||||
enabled: true
|
||||
updatedAt: 2026-04-30T03:57:02.715Z
|
||||
provider:
|
||||
---
|
||||
|
||||
代码输出要符合PHP Webman规则
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
description:
|
||||
alwaysApply: true
|
||||
enabled: true
|
||||
updatedAt: 2026-05-09T09:17:43.130Z
|
||||
provider:
|
||||
---
|
||||
|
||||
涉及html前端,优先使用Vant4版本的组件,整体美观页面风格一致
|
||||
@@ -0,0 +1,8 @@
|
||||
/runtime
|
||||
/.idea
|
||||
/.vscode
|
||||
/vendor
|
||||
*.log
|
||||
.env
|
||||
/tests/tmp
|
||||
/tests/.phpunit.result.cache
|
||||
@@ -0,0 +1,339 @@
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e3]:
|
||||
- generic [ref=e5] [cursor=pointer]: 搜索护工姓名/技能...
|
||||
- generic [ref=e6]:
|
||||
- generic [ref=e7] [cursor=pointer]: 技能类型
|
||||
- generic [ref=e8] [cursor=pointer]: 价格排序
|
||||
- generic [ref=e9] [cursor=pointer]: 经验不限
|
||||
- generic [ref=e10] [cursor=pointer]: 时间范围
|
||||
- generic [ref=e11] [cursor=pointer]: 评分最高
|
||||
- generic [ref=e12]:
|
||||
- generic [ref=e13]:
|
||||
- generic [ref=e14]: "10"
|
||||
- generic [ref=e15]: 位护工
|
||||
- generic [ref=e17]:
|
||||
- generic [ref=e18]: 98.6%
|
||||
- generic [ref=e19]: 好评率
|
||||
- generic [ref=e21]:
|
||||
- generic [ref=e22]: 24h
|
||||
- generic [ref=e23]: 快速响应
|
||||
- feed [ref=e28]:
|
||||
- generic [ref=e29] [cursor=pointer]:
|
||||
- generic [ref=e30]:
|
||||
- generic [ref=e33]:
|
||||
- generic [ref=e35]:
|
||||
- generic [ref=e36]:
|
||||
- generic [ref=e37]: 张秀芳
|
||||
- generic [ref=e38]: 热门
|
||||
- generic [ref=e39]: 认证
|
||||
- radiogroup [ref=e41]:
|
||||
- radio "" [ref=e42]:
|
||||
- generic [ref=e43]:
|
||||
- radio "" [ref=e44]:
|
||||
- generic [ref=e45]:
|
||||
- radio "" [ref=e46]:
|
||||
- generic [ref=e47]:
|
||||
- radio "" [ref=e48]:
|
||||
- generic [ref=e49]:
|
||||
- radio "" [ref=e50]:
|
||||
- generic [ref=e51]:
|
||||
- generic [ref=e52]:
|
||||
- generic [ref=e53]: 术后护理
|
||||
- generic [ref=e54]: 老年照护
|
||||
- generic [ref=e55]: 康复训练
|
||||
- generic [ref=e56]:
|
||||
- generic [ref=e57]:
|
||||
- generic [ref=e58]: ¥
|
||||
- generic [ref=e59]: "68"
|
||||
- generic [ref=e60]: /时
|
||||
- button "预约" [ref=e61]:
|
||||
- generic [ref=e63]: 预约
|
||||
- generic [ref=e65]: 护理证 · 健康证
|
||||
- generic [ref=e66] [cursor=pointer]:
|
||||
- generic [ref=e67]:
|
||||
- generic [ref=e70]:
|
||||
- generic [ref=e71]:
|
||||
- generic [ref=e72]:
|
||||
- generic [ref=e73]: 王美玲
|
||||
- generic [ref=e74]: 认证
|
||||
- radiogroup [ref=e76]:
|
||||
- radio "" [ref=e77]:
|
||||
- generic [ref=e78]:
|
||||
- radio "" [ref=e79]:
|
||||
- generic [ref=e80]:
|
||||
- radio "" [ref=e81]:
|
||||
- generic [ref=e82]:
|
||||
- radio "" [ref=e83]:
|
||||
- generic [ref=e84]:
|
||||
- radio "" [ref=e85]:
|
||||
- generic [ref=e86]:
|
||||
- generic [ref=e87]:
|
||||
- generic [ref=e88]: 母婴护理
|
||||
- generic [ref=e89]: 月子照护
|
||||
- generic [ref=e90]: 营养配餐
|
||||
- generic [ref=e91]:
|
||||
- generic [ref=e92]:
|
||||
- generic [ref=e93]: ¥
|
||||
- generic [ref=e94]: "62"
|
||||
- generic [ref=e95]: /时
|
||||
- button "预约" [ref=e96]:
|
||||
- generic [ref=e98]: 预约
|
||||
- generic [ref=e100]: 护理证 · 育婴师
|
||||
- generic [ref=e101] [cursor=pointer]:
|
||||
- generic [ref=e102]:
|
||||
- generic [ref=e105]:
|
||||
- generic [ref=e107]:
|
||||
- generic [ref=e108]:
|
||||
- generic [ref=e109]: 吴秀英
|
||||
- generic [ref=e110]: 热门
|
||||
- generic [ref=e111]: 认证
|
||||
- radiogroup [ref=e113]:
|
||||
- radio "" [ref=e114]:
|
||||
- generic [ref=e115]:
|
||||
- radio "" [ref=e116]:
|
||||
- generic [ref=e117]:
|
||||
- radio "" [ref=e118]:
|
||||
- generic [ref=e119]:
|
||||
- radio "" [ref=e120]:
|
||||
- generic [ref=e121]:
|
||||
- radio "" [ref=e122]:
|
||||
- generic [ref=e123]:
|
||||
- generic [ref=e124]:
|
||||
- generic [ref=e125]: 母婴护理
|
||||
- generic [ref=e126]: 月子照护
|
||||
- generic [ref=e127]: 营养配餐
|
||||
- generic [ref=e128]: 产后恢复
|
||||
- generic [ref=e129]:
|
||||
- generic [ref=e130]:
|
||||
- generic [ref=e131]: ¥
|
||||
- generic [ref=e132]: "72"
|
||||
- generic [ref=e133]: /时
|
||||
- button "预约" [ref=e134]:
|
||||
- generic [ref=e136]: 预约
|
||||
- generic [ref=e138]: 护理证 · 育婴师 · 营养师证
|
||||
- generic [ref=e139] [cursor=pointer]:
|
||||
- generic [ref=e140]:
|
||||
- generic [ref=e143]:
|
||||
- generic [ref=e145]:
|
||||
- generic [ref=e146]:
|
||||
- generic [ref=e147]: 李国华
|
||||
- generic [ref=e148]: 热门
|
||||
- generic [ref=e149]: 认证
|
||||
- radiogroup [ref=e151]:
|
||||
- radio "" [ref=e152]:
|
||||
- generic [ref=e153]:
|
||||
- radio "" [ref=e154]:
|
||||
- generic [ref=e155]:
|
||||
- radio "" [ref=e156]:
|
||||
- generic [ref=e157]:
|
||||
- radio "" [ref=e158]:
|
||||
- generic [ref=e159]:
|
||||
- radio "" [ref=e160]:
|
||||
- generic [ref=e161]:
|
||||
- generic [ref=e162]:
|
||||
- generic [ref=e163]: 重症监护
|
||||
- generic [ref=e164]: 输液换药
|
||||
- generic [ref=e165]: 心理疏导
|
||||
- generic [ref=e166]:
|
||||
- generic [ref=e167]:
|
||||
- generic [ref=e168]: ¥
|
||||
- generic [ref=e169]: "75"
|
||||
- generic [ref=e170]: /时
|
||||
- button "预约" [ref=e171]:
|
||||
- generic [ref=e173]: 预约
|
||||
- generic [ref=e175]: 护士资格证 · 急救证
|
||||
- generic [ref=e176] [cursor=pointer]:
|
||||
- generic [ref=e177]:
|
||||
- generic [ref=e180]:
|
||||
- generic [ref=e181]:
|
||||
- generic [ref=e182]:
|
||||
- generic [ref=e183]: 赵文斌
|
||||
- generic [ref=e184]: 热门
|
||||
- generic [ref=e185]: 认证
|
||||
- radiogroup [ref=e187]:
|
||||
- radio "" [ref=e188]:
|
||||
- generic [ref=e189]:
|
||||
- radio "" [ref=e190]:
|
||||
- generic [ref=e191]:
|
||||
- radio "" [ref=e192]:
|
||||
- generic [ref=e193]:
|
||||
- radio "" [ref=e194]:
|
||||
- generic [ref=e195]:
|
||||
- radio "" [ref=e196]:
|
||||
- generic [ref=e197]:
|
||||
- generic [ref=e198]:
|
||||
- generic [ref=e199]: 重症监护
|
||||
- generic [ref=e200]: 术后护理
|
||||
- generic [ref=e201]: 康复训练
|
||||
- generic [ref=e202]: 心理疏导
|
||||
- generic [ref=e203]:
|
||||
- generic [ref=e204]:
|
||||
- generic [ref=e205]: ¥
|
||||
- generic [ref=e206]: "80"
|
||||
- generic [ref=e207]: /时
|
||||
- button "预约" [ref=e208]:
|
||||
- generic [ref=e210]: 预约
|
||||
- generic [ref=e212]: 护士资格证 · 急救证 · 康复师
|
||||
- generic [ref=e213] [cursor=pointer]:
|
||||
- generic [ref=e214]:
|
||||
- generic [ref=e217]:
|
||||
- generic [ref=e219]:
|
||||
- generic [ref=e220]:
|
||||
- generic [ref=e221]: 陈建国
|
||||
- generic [ref=e222]: 认证
|
||||
- radiogroup [ref=e224]:
|
||||
- radio "" [ref=e225]:
|
||||
- generic [ref=e226]:
|
||||
- radio "" [ref=e227]:
|
||||
- generic [ref=e228]:
|
||||
- radio "" [ref=e229]:
|
||||
- generic [ref=e230]:
|
||||
- radio "" [ref=e231]:
|
||||
- generic [ref=e232]:
|
||||
- radio "" [ref=e233]:
|
||||
- generic [ref=e234]:
|
||||
- generic [ref=e235]:
|
||||
- generic [ref=e236]: 行动辅助
|
||||
- generic [ref=e237]: 日常起居
|
||||
- generic [ref=e238]: 陪同就医
|
||||
- generic [ref=e239]:
|
||||
- generic [ref=e240]:
|
||||
- generic [ref=e241]: ¥
|
||||
- generic [ref=e242]: "58"
|
||||
- generic [ref=e243]: /时
|
||||
- button "预约" [ref=e244]:
|
||||
- generic [ref=e246]: 预约
|
||||
- generic [ref=e248]: 护理证 · 康复师
|
||||
- generic [ref=e249] [cursor=pointer]:
|
||||
- generic [ref=e250]:
|
||||
- generic [ref=e253]:
|
||||
- generic [ref=e255]:
|
||||
- generic [ref=e256]:
|
||||
- generic [ref=e257]: 周明辉
|
||||
- generic [ref=e258]: 认证
|
||||
- radiogroup [ref=e260]:
|
||||
- radio "" [ref=e261]:
|
||||
- generic [ref=e262]:
|
||||
- radio "" [ref=e263]:
|
||||
- generic [ref=e264]:
|
||||
- radio "" [ref=e265]:
|
||||
- generic [ref=e266]:
|
||||
- radio "" [ref=e267]:
|
||||
- generic [ref=e268]:
|
||||
- radio "" [ref=e269]:
|
||||
- generic [ref=e270]:
|
||||
- generic [ref=e271]:
|
||||
- generic [ref=e272]: 老年照护
|
||||
- generic [ref=e273]: 慢性病管理
|
||||
- generic [ref=e274]: 用药提醒
|
||||
- generic [ref=e275]:
|
||||
- generic [ref=e276]:
|
||||
- generic [ref=e277]: ¥
|
||||
- generic [ref=e278]: "65"
|
||||
- generic [ref=e279]: /时
|
||||
- button "预约" [ref=e280]:
|
||||
- generic [ref=e282]: 预约
|
||||
- generic [ref=e284]: 健康证 · 急救证
|
||||
- generic [ref=e285] [cursor=pointer]:
|
||||
- generic [ref=e286]:
|
||||
- generic [ref=e289]:
|
||||
- generic [ref=e291]:
|
||||
- generic [ref=e293]: 刘晓红
|
||||
- radiogroup [ref=e295]:
|
||||
- radio "" [ref=e296]:
|
||||
- generic [ref=e297]:
|
||||
- radio "" [ref=e298]:
|
||||
- generic [ref=e299]:
|
||||
- radio "" [ref=e300]:
|
||||
- generic [ref=e301]:
|
||||
- radio "" [ref=e302]:
|
||||
- generic [ref=e303]:
|
||||
- radio "" [ref=e304]:
|
||||
- generic [ref=e305]:
|
||||
- generic [ref=e306]:
|
||||
- generic [ref=e307]: 基础护理
|
||||
- generic [ref=e308]: 饮食照料
|
||||
- generic [ref=e309]: 卫生清洁
|
||||
- generic [ref=e310]:
|
||||
- generic [ref=e311]:
|
||||
- generic [ref=e312]: ¥
|
||||
- generic [ref=e313]: "55"
|
||||
- generic [ref=e314]: /时
|
||||
- button "预约" [ref=e315]:
|
||||
- generic [ref=e317]: 预约
|
||||
- generic [ref=e319]: 护理证
|
||||
- generic [ref=e320] [cursor=pointer]:
|
||||
- generic [ref=e321]:
|
||||
- generic [ref=e324]:
|
||||
- generic [ref=e325]:
|
||||
- generic [ref=e326]:
|
||||
- generic [ref=e327]: 郑海涛
|
||||
- generic [ref=e328]: 认证
|
||||
- radiogroup [ref=e330]:
|
||||
- radio "" [ref=e331]:
|
||||
- generic [ref=e332]:
|
||||
- radio "" [ref=e333]:
|
||||
- generic [ref=e334]:
|
||||
- radio "" [ref=e335]:
|
||||
- generic [ref=e336]:
|
||||
- radio "" [ref=e337]:
|
||||
- generic [ref=e338]:
|
||||
- radio "" [ref=e339]:
|
||||
- generic [ref=e340]:
|
||||
- generic [ref=e341]:
|
||||
- generic [ref=e342]: 康复训练
|
||||
- generic [ref=e343]: 行动辅助
|
||||
- generic [ref=e344]: 按摩理疗
|
||||
- generic [ref=e345]:
|
||||
- generic [ref=e346]:
|
||||
- generic [ref=e347]: ¥
|
||||
- generic [ref=e348]: "60"
|
||||
- generic [ref=e349]: /时
|
||||
- button "预约" [ref=e350]:
|
||||
- generic [ref=e352]: 预约
|
||||
- generic [ref=e354]: 护理证 · 康复师
|
||||
- generic [ref=e355] [cursor=pointer]:
|
||||
- generic [ref=e356]:
|
||||
- generic [ref=e359]:
|
||||
- generic [ref=e361]:
|
||||
- generic [ref=e363]: 孙丽萍
|
||||
- radiogroup [ref=e365]:
|
||||
- radio "" [ref=e366]:
|
||||
- generic [ref=e367]:
|
||||
- radio "" [ref=e368]:
|
||||
- generic [ref=e369]:
|
||||
- radio "" [ref=e370]:
|
||||
- generic [ref=e371]:
|
||||
- radio "" [ref=e372]:
|
||||
- generic [ref=e373]:
|
||||
- radio "" [ref=e374]:
|
||||
- generic [ref=e375]:
|
||||
- generic [ref=e376]:
|
||||
- generic [ref=e377]: 生活照料
|
||||
- generic [ref=e378]: 陪伴聊天
|
||||
- generic [ref=e379]: 协助活动
|
||||
- generic [ref=e380]:
|
||||
- generic [ref=e381]:
|
||||
- generic [ref=e382]: ¥
|
||||
- generic [ref=e383]: "48"
|
||||
- generic [ref=e384]: /时
|
||||
- button "预约" [ref=e385]:
|
||||
- generic [ref=e387]: 预约
|
||||
- generic [ref=e389]: 护理员培训证
|
||||
- generic [ref=e390]: 没有更多了
|
||||
- tablist [ref=e393]:
|
||||
- tab " 首页" [ref=e394] [cursor=pointer]:
|
||||
- generic [ref=e396]:
|
||||
- generic [ref=e397]: 首页
|
||||
- tab " 找护工" [selected] [ref=e398] [cursor=pointer]:
|
||||
- generic [ref=e400]:
|
||||
- generic [ref=e401]: 找护工
|
||||
- tab " 发布订单" [ref=e402] [cursor=pointer]:
|
||||
- generic [ref=e404]:
|
||||
- generic [ref=e406]: 发布订单
|
||||
- tab " 订单" [ref=e407] [cursor=pointer]:
|
||||
- generic [ref=e409]:
|
||||
- generic [ref=e410]: 订单
|
||||
- tab " 我的" [ref=e411] [cursor=pointer]:
|
||||
- generic [ref=e413]:
|
||||
- generic [ref=e414]: 我的
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
+18
@@ -0,0 +1,18 @@
|
||||
FROM php:8.3.22-cli-alpine
|
||||
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||
&& apk update --no-cache \
|
||||
&& docker-php-source extract
|
||||
|
||||
# install extensions
|
||||
RUN docker-php-ext-install pdo pdo_mysql -j$(nproc) pcntl
|
||||
|
||||
# enable opcache and pcntl
|
||||
RUN docker-php-ext-enable opcache pcntl
|
||||
RUN docker-php-source delete \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 walkor<walkor@workerman.net> and contributors (see https://github.com/walkor/webman/contributors)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,70 @@
|
||||
<div style="padding:18px;max-width: 1024px;margin:0 auto;background-color:#fff;color:#333">
|
||||
<h1>webman</h1>
|
||||
|
||||
基于<a href="https://www.workerman.net" target="__blank">workerman</a>开发的超高性能PHP框架
|
||||
|
||||
|
||||
<h1>学习</h1>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://www.workerman.net/webman" target="__blank">主页 / Home page</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://webman.workerman.net" target="__blank">文档 / Document</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.workerman.net/doc/webman/install.html" target="__blank">安装 / Install</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.workerman.net/questions" target="__blank">问答 / Questions</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.workerman.net/apps" target="__blank">市场 / Apps</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.workerman.net/sponsor" target="__blank">赞助 / Sponsors</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.workerman.net/doc/webman/thanks.html" target="__blank">致谢 / Thanks</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div style="float:left;padding-bottom:30px;">
|
||||
|
||||
<h1>赞助商</h1>
|
||||
|
||||
<h4>特别赞助</h4>
|
||||
<a href="https://www.crmeb.com/?form=workerman" target="__blank">
|
||||
<img src="https://www.workerman.net/img/sponsors/6429/20230719111500.svg" width="200">
|
||||
</a>
|
||||
|
||||
<h4>铂金赞助</h4>
|
||||
<a href="https://www.fadetask.com/?from=workerman" target="__blank"><img src="https://www.workerman.net/img/sponsors/1/20230719084316.png" width="200"></a>
|
||||
<a href="https://www.yilianyun.net/?from=workerman" target="__blank" style="margin-left:20px;"><img src="https://www.workerman.net/img/sponsors/6218/20230720114049.png" width="200"></a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div style="float:left;padding-bottom:30px;clear:both">
|
||||
|
||||
<h1>请作者喝咖啡</h1>
|
||||
|
||||
<img src="https://www.workerman.net/img/wx_donate.png" width="200">
|
||||
<img src="https://www.workerman.net/img/ali_donate.png" width="200">
|
||||
<br>
|
||||
<b>如果您觉得webman对您有所帮助,欢迎捐赠。</b>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div style="clear: both">
|
||||
<h1>LICENSE</h1>
|
||||
The webman is open-sourced software licensed under the MIT.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller;
|
||||
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* 首页控制器
|
||||
* @Author: Lwei<1750433663@qq.com>
|
||||
* @Date: 2026-04-30
|
||||
* @Time: 15:05:39
|
||||
*/
|
||||
class IndexController
|
||||
{
|
||||
//全局总入口
|
||||
public function index(Request $request)
|
||||
{
|
||||
// return view('index/view', ['name' => 'webman']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller\PatientApp;
|
||||
|
||||
use app\controller\PatientAPP\PageController;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
|
||||
/**
|
||||
* 患者端应用主入口
|
||||
* @Author: Lwei<1750433663@qq.com>
|
||||
* @Date: 2026-05-06
|
||||
* @Time: 21:51:32
|
||||
*/
|
||||
class IndexController extends PageController
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
return view('PatientAPP/index/index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller\PatientAPP;
|
||||
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use support\View;
|
||||
|
||||
|
||||
class MineController extends PageController
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
return view('PatientAPP/Mine/index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller\PatientAPP;
|
||||
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use support\View;
|
||||
|
||||
|
||||
class NursingController extends PageController
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
return view('PatientAPP/Nursing/index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller\PatientAPP;
|
||||
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use support\View;
|
||||
|
||||
|
||||
class OrderController extends PageController
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
return view('PatientAPP/Order/index');
|
||||
}
|
||||
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
return view('PatientAPP/Order/create');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller\PatientAPP;
|
||||
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use support\View;
|
||||
|
||||
|
||||
/**
|
||||
* 基类控制器
|
||||
* @Author: Lwei<1750433663@qq.com>
|
||||
* @Date: 2026-05-07
|
||||
* @Time: 20:08:26
|
||||
*/
|
||||
class PageController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
//赋值全局JS变量
|
||||
$GlobalJS = think_view(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'view' . DIRECTORY_SEPARATOR . 'PatientAPP' . DIRECTORY_SEPARATOR . 'GlobalJS.js');
|
||||
View::assign('GlobalJS',$GlobalJS->rawBody());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller\StaffApp;
|
||||
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
|
||||
class IndexController
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
return response('Hello, IndexController');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* Here is your custom functions.
|
||||
*/
|
||||
|
||||
if (!function_exists('dd')) {
|
||||
/**
|
||||
* Dump the passed variables and end the script.
|
||||
*
|
||||
* @param mixed ...$args
|
||||
* @return void
|
||||
*/
|
||||
function dd(...$args): void
|
||||
{
|
||||
if (!in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && !headers_sent()) {
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
}
|
||||
|
||||
foreach ($args as $arg) {
|
||||
\var_dump($arg);
|
||||
}
|
||||
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('dump')) {
|
||||
/**
|
||||
* Dump the passed variables without ending the script.
|
||||
*
|
||||
* @param mixed ...$args
|
||||
* @return void
|
||||
*/
|
||||
function dump(...$args): void
|
||||
{
|
||||
if (!in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && !headers_sent()) {
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
}
|
||||
|
||||
foreach ($args as $arg) {
|
||||
\var_dump($arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace app\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Response;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* Class StaticFile
|
||||
* @package app\middleware
|
||||
*/
|
||||
class StaticFile implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
// Access to files beginning with. Is prohibited
|
||||
if (strpos($request->path(), '/.') !== false) {
|
||||
return response('<h1>403 forbidden</h1>', 403);
|
||||
}
|
||||
/** @var Response $response */
|
||||
$response = $handler($request);
|
||||
// Add cross domain HTTP header
|
||||
/*$response->withHeaders([
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
]);*/
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class Test extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'test';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use Webman\App;
|
||||
|
||||
class Http extends App
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use FilesystemIterator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Worker;
|
||||
|
||||
/**
|
||||
* Class FileMonitor
|
||||
* @package process
|
||||
*/
|
||||
class Monitor
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $paths = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $extensions = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $loadedFiles = [];
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected int $ppid = 0;
|
||||
|
||||
/**
|
||||
* Pause monitor
|
||||
* @return void
|
||||
*/
|
||||
public static function pause(): void
|
||||
{
|
||||
file_put_contents(static::lockFile(), time());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume monitor
|
||||
* @return void
|
||||
*/
|
||||
public static function resume(): void
|
||||
{
|
||||
clearstatcache();
|
||||
if (is_file(static::lockFile())) {
|
||||
unlink(static::lockFile());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether monitor is paused
|
||||
* @return bool
|
||||
*/
|
||||
public static function isPaused(): bool
|
||||
{
|
||||
clearstatcache();
|
||||
return file_exists(static::lockFile());
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock file
|
||||
* @return string
|
||||
*/
|
||||
protected static function lockFile(): string
|
||||
{
|
||||
return runtime_path('monitor.lock');
|
||||
}
|
||||
|
||||
/**
|
||||
* FileMonitor constructor.
|
||||
* @param $monitorDir
|
||||
* @param $monitorExtensions
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct($monitorDir, $monitorExtensions, array $options = [])
|
||||
{
|
||||
$this->ppid = function_exists('posix_getppid') ? posix_getppid() : 0;
|
||||
static::resume();
|
||||
$this->paths = (array)$monitorDir;
|
||||
$this->extensions = $monitorExtensions;
|
||||
foreach (get_included_files() as $index => $file) {
|
||||
$this->loadedFiles[$file] = $index;
|
||||
if (strpos($file, 'webman-framework/src/support/App.php')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!Worker::getAllWorkers()) {
|
||||
return;
|
||||
}
|
||||
$disableFunctions = explode(',', ini_get('disable_functions'));
|
||||
if (in_array('exec', $disableFunctions, true)) {
|
||||
echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n";
|
||||
} else {
|
||||
if ($options['enable_file_monitor'] ?? true) {
|
||||
Timer::add(1, function () {
|
||||
$this->checkAllFilesChange();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$memoryLimit = $this->getMemoryLimit($options['memory_limit'] ?? null);
|
||||
if ($memoryLimit && ($options['enable_memory_monitor'] ?? true)) {
|
||||
Timer::add(60, [$this, 'checkMemory'], [$memoryLimit]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $monitorDir
|
||||
* @return bool
|
||||
*/
|
||||
public function checkFilesChange($monitorDir): bool
|
||||
{
|
||||
static $lastMtime, $tooManyFilesCheck;
|
||||
if (!$lastMtime) {
|
||||
$lastMtime = time();
|
||||
}
|
||||
clearstatcache();
|
||||
if (!is_dir($monitorDir)) {
|
||||
if (!is_file($monitorDir)) {
|
||||
return false;
|
||||
}
|
||||
$iterator = [new SplFileInfo($monitorDir)];
|
||||
} else {
|
||||
// recursive traversal directory
|
||||
$dirIterator = new RecursiveDirectoryIterator($monitorDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS);
|
||||
$iterator = new RecursiveIteratorIterator($dirIterator);
|
||||
}
|
||||
$count = 0;
|
||||
foreach ($iterator as $file) {
|
||||
$count ++;
|
||||
/** @var SplFileInfo $file */
|
||||
if (is_dir($file->getRealPath())) {
|
||||
continue;
|
||||
}
|
||||
// check mtime
|
||||
if (in_array($file->getExtension(), $this->extensions, true) && $lastMtime < $file->getMTime()) {
|
||||
$lastMtime = $file->getMTime();
|
||||
if (DIRECTORY_SEPARATOR === '/' && isset($this->loadedFiles[$file->getRealPath()])) {
|
||||
echo "$file updated but cannot be reloaded because only auto-loaded files support reload.\n";
|
||||
continue;
|
||||
}
|
||||
$var = 0;
|
||||
exec('"'.PHP_BINARY . '" -l ' . $file, $out, $var);
|
||||
if ($var) {
|
||||
continue;
|
||||
}
|
||||
// send SIGUSR1 signal to master process for reload
|
||||
if (DIRECTORY_SEPARATOR === '/') {
|
||||
if ($masterPid = $this->getMasterPid()) {
|
||||
echo $file . " updated and reload\n";
|
||||
posix_kill($masterPid, SIGUSR1);
|
||||
} else {
|
||||
echo "Master process has gone away and can not reload\n";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
echo $file . " updated and reload\n";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!$tooManyFilesCheck && $count > 1000) {
|
||||
echo "Monitor: There are too many files ($count files) in $monitorDir which makes file monitoring very slow\n";
|
||||
$tooManyFilesCheck = 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getMasterPid(): int
|
||||
{
|
||||
if ($this->ppid === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (function_exists('posix_kill') && !posix_kill($this->ppid, 0)) {
|
||||
echo "Master process has gone away\n";
|
||||
return $this->ppid = 0;
|
||||
}
|
||||
if (PHP_OS_FAMILY !== 'Linux') {
|
||||
return $this->ppid;
|
||||
}
|
||||
$cmdline = "/proc/$this->ppid/cmdline";
|
||||
if (!is_readable($cmdline) || !($content = file_get_contents($cmdline)) || (!str_contains($content, 'WorkerMan') && !str_contains($content, 'php'))) {
|
||||
// Process not exist
|
||||
$this->ppid = 0;
|
||||
}
|
||||
return $this->ppid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function checkAllFilesChange(): bool
|
||||
{
|
||||
if (static::isPaused()) {
|
||||
return false;
|
||||
}
|
||||
foreach ($this->paths as $path) {
|
||||
if ($this->checkFilesChange($path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $memoryLimit
|
||||
* @return void
|
||||
*/
|
||||
public function checkMemory($memoryLimit): void
|
||||
{
|
||||
if (static::isPaused() || $memoryLimit <= 0) {
|
||||
return;
|
||||
}
|
||||
$masterPid = $this->getMasterPid();
|
||||
if ($masterPid <= 0) {
|
||||
echo "Master process has gone away\n";
|
||||
return;
|
||||
}
|
||||
|
||||
$childrenFile = "/proc/$masterPid/task/$masterPid/children";
|
||||
if (!is_file($childrenFile) || !($children = file_get_contents($childrenFile))) {
|
||||
return;
|
||||
}
|
||||
foreach (explode(' ', $children) as $pid) {
|
||||
$pid = (int)$pid;
|
||||
$statusFile = "/proc/$pid/status";
|
||||
if (!is_file($statusFile) || !($status = file_get_contents($statusFile))) {
|
||||
continue;
|
||||
}
|
||||
$mem = 0;
|
||||
if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) {
|
||||
$mem = $match[1];
|
||||
}
|
||||
$mem = (int)($mem / 1024);
|
||||
if ($mem >= $memoryLimit) {
|
||||
posix_kill($pid, SIGINT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory limit
|
||||
* @param $memoryLimit
|
||||
* @return int
|
||||
*/
|
||||
protected function getMemoryLimit($memoryLimit): int
|
||||
{
|
||||
if ($memoryLimit === 0) {
|
||||
return 0;
|
||||
}
|
||||
$usePhpIni = false;
|
||||
if (!$memoryLimit) {
|
||||
$memoryLimit = ini_get('memory_limit');
|
||||
$usePhpIni = true;
|
||||
}
|
||||
|
||||
if ($memoryLimit == -1) {
|
||||
return 0;
|
||||
}
|
||||
$unit = strtolower($memoryLimit[strlen($memoryLimit) - 1]);
|
||||
$memoryLimit = (int)$memoryLimit;
|
||||
if ($unit === 'g') {
|
||||
$memoryLimit = 1024 * $memoryLimit;
|
||||
} else if ($unit === 'k') {
|
||||
$memoryLimit = ($memoryLimit / 1024);
|
||||
} else if ($unit === 'm') {
|
||||
$memoryLimit = (int)($memoryLimit);
|
||||
} else if ($unit === 't') {
|
||||
$memoryLimit = (1024 * 1024 * $memoryLimit);
|
||||
} else {
|
||||
$memoryLimit = ($memoryLimit / (1024 * 1024));
|
||||
}
|
||||
if ($memoryLimit < 50) {
|
||||
$memoryLimit = 50;
|
||||
}
|
||||
if ($usePhpIni) {
|
||||
$memoryLimit = (0.8 * $memoryLimit);
|
||||
}
|
||||
return (int)$memoryLimit;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<!-- 底部占位 -->
|
||||
<div class="bottom-safe"></div>
|
||||
|
||||
<!-- 底部导航 -->
|
||||
<van-tabbar v-model="activeTab" active-color="#058397" fixed placeholder safe-area-inset-bottom>
|
||||
<van-tabbar-item icon="home-o" name="home">首页</van-tabbar-item>
|
||||
<van-tabbar-item icon="search" name="nurses">找护工</van-tabbar-item>
|
||||
<van-tabbar-item icon="plus" name="publish" dot>发布订单</van-tabbar-item>
|
||||
<van-tabbar-item icon="notes-o" name="order">订单</van-tabbar-item>
|
||||
<van-tabbar-item icon="user-o" name="mine">我的</van-tabbar-item>
|
||||
</van-tabbar>
|
||||
@@ -0,0 +1,67 @@
|
||||
// var apiUrl = "$apiUrl";
|
||||
// var token = "$token";
|
||||
|
||||
// var apiRequest = function (url, data = {}, method = "GET") {
|
||||
// let _RES = null;
|
||||
// $.ajax({
|
||||
// url: apiUrl + url,
|
||||
// beforeSend: function (request) {
|
||||
// request.setRequestHeader("Authorization", token);
|
||||
// },
|
||||
// method: method,
|
||||
// data: data,
|
||||
// async: false,
|
||||
// success: function (res) {
|
||||
// _RES = res
|
||||
// }
|
||||
// });
|
||||
// return _RES;
|
||||
// }
|
||||
|
||||
// //延时异步请求
|
||||
// var apiSyncRequest = function (url, data = {}, method = "GET") {
|
||||
// var defer = $.Deferred();
|
||||
// $.ajax({
|
||||
// url: apiUrl + url,
|
||||
// beforeSend: function (request) {
|
||||
// request.setRequestHeader("Authorization", token);
|
||||
// },
|
||||
// method: method,
|
||||
// data: data,
|
||||
// async: false,
|
||||
// success: function (res) {
|
||||
// defer.resolve(res);
|
||||
// }
|
||||
// });
|
||||
// return defer.promise();
|
||||
// }
|
||||
|
||||
// var getQueryVal = function (name) {
|
||||
// return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.href) || [, ""])[1].replace(/\+/g, '%20')) || null
|
||||
// };
|
||||
|
||||
// var wxconfig = function () {
|
||||
// let rs = apiRequest("Wechat/JSSDK");
|
||||
// wx.config(rs.data);
|
||||
// wx.error(function (res) {
|
||||
// console.log(res)
|
||||
// });
|
||||
// }
|
||||
|
||||
// 底部导航路由配置
|
||||
const TabBarRoutes = {
|
||||
home: '/PatientApp/Index/index',
|
||||
nurses: '/PatientApp/Nursing/index',
|
||||
publish: '/PatientApp/Order/create',
|
||||
order: '/PatientApp/Order/index',
|
||||
mine: '/PatientApp/Mine/index'
|
||||
};
|
||||
|
||||
// 通用跳转方法
|
||||
function onTabBarChange(name) {
|
||||
if (TabBarRoutes[name]) {
|
||||
window.location.href = TabBarRoutes[name];
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
@@ -0,0 +1,218 @@
|
||||
{extend name="PatientAPP/layout" /}
|
||||
{block name="css"}
|
||||
<link rel="stylesheet" href="/PatientAPP/Index/index.css" />
|
||||
{/block}
|
||||
{block name="app"}
|
||||
<div id="app" v-cloak>
|
||||
<!-- 头部区域 -->
|
||||
<div class="header-section">
|
||||
<div class="header-top">
|
||||
<div class="user-info">
|
||||
<div class="user-avatar">
|
||||
<van-icon name="user-o" size="24" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="user-name">您好,患者用户</div>
|
||||
<div class="user-desc">祝您早日康复</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="icon-btn" @click="onNotice">
|
||||
<van-icon name="bell" size="17" />
|
||||
</div>
|
||||
<div class="icon-btn" @click="onSetting">
|
||||
<van-icon name="setting-o" size="17" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-wrapper">
|
||||
<van-search placeholder="搜索护工姓名/技能" shape="round" background="transparent" @search="onSearch" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 快捷功能区(专业版) -->
|
||||
<div class="quick-actions">
|
||||
<div class="qa-header">
|
||||
<div class="qa-title"><span class="qa-title-dot"></span> 服务导航</div>
|
||||
<div class="qa-more">更多服务 <van-icon name="arrow" size="11" /></div>
|
||||
</div>
|
||||
<div class="qa-body">
|
||||
<div class="qa-grid">
|
||||
<div class="qa-item" @click="goPublish('hospital')">
|
||||
<div class="qa-icon-wrap primary">
|
||||
<van-icon class="qa-icon-primary" name="edit" size="23" />
|
||||
</div>
|
||||
<span class="qa-label">发布订单</span>
|
||||
</div>
|
||||
<div class="qa-item" @click="goNurses()">
|
||||
<div class="qa-icon-wrap info">
|
||||
<van-icon class="qa-icon-info" name="friends-o" size="23" />
|
||||
</div>
|
||||
<span class="qa-label">选择护工</span>
|
||||
</div>
|
||||
<div class="qa-item" @click="goOrders()">
|
||||
<div class="qa-icon-wrap cyan">
|
||||
<van-icon class="qa-icon-cyan" name="notes-o" size="23" />
|
||||
</div>
|
||||
<span class="qa-label">我的订单</span>
|
||||
</div>
|
||||
<div class="qa-item" @click="goService()">
|
||||
<div class="qa-icon-wrap warning">
|
||||
<van-icon class="qa-icon-warning" name="service-o" size="23" />
|
||||
</div>
|
||||
<span class="qa-label">在线客服</span>
|
||||
</div>
|
||||
<div class="qa-item" @click="goPublish('home')">
|
||||
<div class="qa-icon-wrap success">
|
||||
<van-icon name="hotel-o" size="23" color="#1a7a32" />
|
||||
</div>
|
||||
<span class="qa-label">住院陪护</span>
|
||||
</div>
|
||||
<div class="qa-item" @click="goPublish('accompany')">
|
||||
<div class="qa-icon-wrap purple">
|
||||
<van-icon class="qa-icon-purple" name="guide-o" size="23" />
|
||||
</div>
|
||||
<span class="qa-label">门诊陪护</span>
|
||||
</div>
|
||||
<div class="qa-item" @click="goService()">
|
||||
<div class="qa-icon-wrap gold">
|
||||
<van-icon class="qa-icon-gold" name="certificate" size="23" />
|
||||
</div>
|
||||
<span class="qa-label">资质认证</span>
|
||||
</div>
|
||||
<div class="qa-item" @click="onNotice()">
|
||||
<div class="qa-icon-wrap danger">
|
||||
<van-icon class="qa-icon-danger" name="info-o" size="23" />
|
||||
</div>
|
||||
<span class="qa-label">帮助中心</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 数据统计条 -->
|
||||
<div class="qa-stats">
|
||||
<div class="qa-stat">
|
||||
<div class="qa-stat-num">1,280+</div>
|
||||
<div class="qa-stat-lbl">专业护工</div>
|
||||
</div>
|
||||
<div class="qa-stat">
|
||||
<div class="qa-stat-num">98.6%</div>
|
||||
<div class="qa-stat-lbl">好评率</div>
|
||||
</div>
|
||||
<div class="qa-stat">
|
||||
<div class="qa-stat-num">52,000+</div>
|
||||
<div class="qa-stat-lbl">服务次数</div>
|
||||
</div>
|
||||
<div class="qa-stat">
|
||||
<div class="qa-stat-num">24h</div>
|
||||
<div class="qa-stat-lbl">快速响应</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 公告栏 -->
|
||||
<div class="notice-section">
|
||||
<van-notice-bar left-icon="volume-o" :scrollable="true" text="系统公告:智能陪护系统已全新升级,提供更优质的陪护服务体验!" />
|
||||
</div>
|
||||
<!-- 快速下单入口 -->
|
||||
<div class="order-entry">
|
||||
<div class="order-entry-header">
|
||||
<div class="order-entry-left">
|
||||
<div class="order-entry-title">需要专业陪护服务?</div>
|
||||
<div class="order-entry-desc">一键发布需求,专业持证护工快速响应</div>
|
||||
<div class="order-entry-btns">
|
||||
<van-button type="primary" round size="small" @click="goPublish('hospital')">就诊陪护</van-button>
|
||||
<van-button type="primary" round plain size="small" @click="goPublish('home')">住院陪护</van-button>
|
||||
<van-button type="primary" round plain size="small"
|
||||
@click="goPublish('accompany')">居家陪护</van-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-entry-right">
|
||||
<van-icon name="medal-o" size="30" color="rgba(255,255,255,0.85)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 推荐护工 -->
|
||||
<div class="nurse-section">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<van-icon name="medal-o" color="#018fa5" size="18" /> <span style="font-weight:600"> 推荐护工</span>
|
||||
</div>
|
||||
<div class="section-more" @click="goNurses">
|
||||
查看更多 <van-icon name="arrow" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="nurse-list">
|
||||
<div class="nurse-card" v-for="(nurse, index) in nurseList" :key="index" @click="viewNurse(nurse)">
|
||||
<div class="nurse-avatar">
|
||||
<van-icon name="contact" />
|
||||
</div>
|
||||
<div class="nurse-info">
|
||||
<div class="nurse-name-row">
|
||||
<span class="nurse-name">{{ nurse.name }}</span>
|
||||
<span class="nurse-rating">
|
||||
<van-icon name="star" /> {{ nurse.rating }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="nurse-tags">
|
||||
<span class="nurse-tag green" v-for="(tag, i) in nurse.certs.slice(0, 2)" :key="'c'+i">{{ tag
|
||||
}}</span>
|
||||
<span class="nurse-tag orange" v-if="nurse.isHot">热门</span>
|
||||
</div>
|
||||
<div class="nurse-tags">
|
||||
<span class="nurse-tag teal" v-for="(skill, i) in nurse.skills.slice(0, 3)" :key="'s'+i">{{
|
||||
skill }}</span>
|
||||
</div>
|
||||
<div class="nurse-bottom">
|
||||
<span class="nurse-price"><span>¥</span>{{ nurse.price }}<span>/小时</span></span>
|
||||
<span class="nurse-exp">{{ nurse.exp }}年经验 | {{ nurse.orders }}次服务</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部栏 -->
|
||||
{include file="PatientAPP/Component/van-tabbar" /}
|
||||
</div>
|
||||
{/block}
|
||||
{block name="js"}
|
||||
<script>
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'home',
|
||||
nurseList: [
|
||||
{ id: 1, name: '张秀芳', rating: 4.9, price: 68, exp: 8, orders: 320, isHot: true, certs: ['护理证', '健康证'], skills: ['术后护理', '老年照护', '康复训练'] },
|
||||
{ id: 2, name: '李国华', rating: 4.8, price: 75, exp: 10, orders: 456, isHot: true, certs: ['护士资格证', '急救证'], skills: ['重症监护', '输液换药', '心理疏导'] },
|
||||
{ id: 3, name: '王美玲', rating: 4.9, price: 62, exp: 5, orders: 218, isHot: false, certs: ['护理证', '育婴师'], skills: ['母婴护理', '月子照护', '营养配餐'] },
|
||||
{ id: 4, name: '陈建国', rating: 4.7, price: 58, exp: 6, orders: 167, isHot: false, certs: ['护理证', '康复师'], skills: ['行动辅助', '日常起居', '陪同就医'] },
|
||||
],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onSearch(val) { console.log('搜索:', val); },
|
||||
onNotice() { vant.showToast({ message: '暂无新消息', position: 'top' }); },
|
||||
onSetting() { vant.showToast('设置'); },
|
||||
goPublish(type) {
|
||||
vant.dialog.alert({
|
||||
title: '发布陪护订单',
|
||||
message: type ? `正在进入${type === 'hospital' ? '院内' : type === 'home' ? '居家' : '陪同'}陪护订单发布页面...` : '正在进入订单发布页面...',
|
||||
});
|
||||
},
|
||||
goNurses() { vant.showToast('跳转到护工列表'); },
|
||||
goOrders() { vant.showToast('跳转到我的订单'); },
|
||||
goService() { vant.showToast('联系在线客服'); },
|
||||
viewNurse(nurse) {
|
||||
vant.showDialog({ title: nurse.name, message: `评分: ${nurse.rating}分\n价格: ¥${nurse.price}/小时\n经验: ${nurse.exp}年\n服务: ${nurse.orders}次` });
|
||||
//vant.dialog.alert({ title: nurse.name, message: `评分: ${nurse.rating}分\n价格: ¥${nurse.price}/小时\n经验: ${nurse.exp}年\n服务: ${nurse.orders}次` });
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeTab(val) { if (val !== 'home') {
|
||||
onTabBarChange(val);
|
||||
}},
|
||||
},
|
||||
});
|
||||
app.use(vant);
|
||||
app.use(vant.Lazyload);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
{/block}
|
||||
@@ -0,0 +1,332 @@
|
||||
{extend name="PatientAPP/layout" /}
|
||||
{block name="css"}
|
||||
<style>
|
||||
.mine-container { background: #f5f6fa; min-height: 100vh; padding-bottom: 70px; }
|
||||
.mine-header {
|
||||
background: linear-gradient(135deg, #018fa5 0%, #027a8c 50%, #016577 100%);
|
||||
padding: 40px 20px 30px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mine-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -20%;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
border-radius: 50%;
|
||||
}
|
||||
.mine-header::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -30%;
|
||||
left: -10%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 50%;
|
||||
}
|
||||
.user-profile { display: flex; align-items: center; position: relative; z-index: 1; }
|
||||
.user-avatar-box {
|
||||
width: 65px; height: 65px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 15px;
|
||||
border: 2px solid rgba(255,255,255,0.35);
|
||||
}
|
||||
.user-avatar-box .van-icon { color: #fff; font-size: 32px; }
|
||||
.user-text { color: #fff; flex: 1; }
|
||||
.user-nickname { font-size: 19px; font-weight: 600; margin-bottom: 4px; }
|
||||
.user-phone { font-size: 13px; opacity: 0.8; }
|
||||
.user-arrow { color: rgba(255,255,255,0.7); }
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-card {
|
||||
margin: -18px 15px 12px;
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
padding: 18px 10px;
|
||||
box-shadow: 0 3px 12px rgba(1,143,165,0.1);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
}
|
||||
.stat-item { flex: 1; text-align: center; cursor: pointer; }
|
||||
.stat-num { font-size: 21px; font-weight: 700; color: #333; }
|
||||
.stat-label { font-size: 12px; color: #999; margin-top: 4px; }
|
||||
.stat-item:nth-child(1) .stat-num { color: #018fa5; }
|
||||
.stat-item:nth-child(2) .stat-num { color: #e6a23c; }
|
||||
.stat-item:nth-child(3) .stat-num { color: #67c23a; }
|
||||
|
||||
/* 订单快捷入口 */
|
||||
.order-section {
|
||||
margin: 0 15px 12px;
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
padding: 16px 0;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
}
|
||||
.section-header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 16px 14px;
|
||||
}
|
||||
.section-title-text { font-size: 15px; font-weight: 600; color: #333; }
|
||||
.section-more-link { font-size: 12px; color: #999; display: flex; align-items: center; }
|
||||
.order-grid { display: flex; padding: 0 10px; }
|
||||
.order-item { flex: 1; text-align: center; cursor: pointer; padding: 8px 0; }
|
||||
.order-icon-wrap {
|
||||
width: 44px; height: 44px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 8px;
|
||||
position: relative;
|
||||
}
|
||||
.order-icon-wrap .van-badge { position: absolute; top: -2px; right: -2px; }
|
||||
.order-label { font-size: 12px; color: #666; }
|
||||
|
||||
/* 功能菜单 */
|
||||
.menu-section {
|
||||
margin: 0 15px 12px;
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
}
|
||||
.menu-section .van-cell { padding: 14px 16px; }
|
||||
.menu-section .van-cell__left-icon { color: #018fa5; margin-right: 10px; font-size: 20px; }
|
||||
.menu-icon-wrap {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 7px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.menu-icon-wrap .van-icon { font-size: 16px; color: #fff; }
|
||||
</style>
|
||||
{/block}
|
||||
|
||||
{block name="app"}
|
||||
<div id="app" v-cloak>
|
||||
<div class="mine-container">
|
||||
<!-- 头部用户信息 -->
|
||||
<div class="mine-header">
|
||||
<div class="user-profile">
|
||||
<div class="user-avatar-box">
|
||||
<van-icon name="user-o" />
|
||||
</div>
|
||||
<div class="user-text">
|
||||
<div class="user-nickname">{{ userInfo.nickname }}</div>
|
||||
<div class="user-phone">{{ userInfo.phone }}</div>
|
||||
</div>
|
||||
<van-icon name="arrow" class="user-arrow" size="16" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单统计 -->
|
||||
<div class="stats-card">
|
||||
<div class="stat-item" @click="goOrderList('all')">
|
||||
<div class="stat-num">{{ stats.totalOrders }}</div>
|
||||
<div class="stat-label">全部订单</div>
|
||||
</div>
|
||||
<div class="stat-item" @click="goOrderList('pending')">
|
||||
<div class="stat-num">{{ stats.pending }}</div>
|
||||
<div class="stat-label">待服务</div>
|
||||
</div>
|
||||
<div class="stat-item" @click="goOrderList('completed')">
|
||||
<div class="stat-num">{{ stats.completed }}</div>
|
||||
<div class="stat-label">已完成</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 我的订单 -->
|
||||
<div class="order-section">
|
||||
<div class="section-header-bar">
|
||||
<span class="section-title-text">我的订单</span>
|
||||
<div class="section-more-link" @click="goOrderList('all')">
|
||||
全部订单 <van-icon name="arrow" size="11" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-grid">
|
||||
<div class="order-item" @click="goOrderList('pending_payment')">
|
||||
<div class="order-icon-wrap" style="background:#fff4e5;">
|
||||
<van-icon name="clock-o" color="#e6a23c" size="20" />
|
||||
<van-badge :content="orderBadge.pendingPayment" v-if="orderBadge.pendingPayment > 0" />
|
||||
</div>
|
||||
<span class="order-label">待付款</span>
|
||||
</div>
|
||||
<div class="order-item" @click="goOrderList('pending_service')">
|
||||
<div class="order-icon-wrap" style="background:#e8f4fd;">
|
||||
<van-icon name="todo-list-o" color="#1989fa" size="20" />
|
||||
<van-badge :content="orderBadge.pendingService" v-if="orderBadge.pendingService > 0" />
|
||||
</div>
|
||||
<span class="order-label">待服务</span>
|
||||
</div>
|
||||
<div class="order-item" @click="goOrderList('in_progress')">
|
||||
<div class="order-icon-wrap" style="background:#eaf9f1;">
|
||||
<van-icon name="guide-o" color="#07c160" size="20" />
|
||||
<van-badge :content="orderBadge.inProgress" v-if="orderBadge.inProgress > 0" />
|
||||
</div>
|
||||
<span class="order-label">进行中</span>
|
||||
</div>
|
||||
<div class="order-item" @click="goOrderList('completed')">
|
||||
<div class="order-icon-wrap" style="background:#f0f2f5;">
|
||||
<van-icon name="certificate" color="#969799" size="20" />
|
||||
</div>
|
||||
<span class="order-label">已完成</span>
|
||||
</div>
|
||||
<div class="order-item" @click="goOrderList('review')">
|
||||
<div class="order-icon-wrap" style="background:#fef0f0;">
|
||||
<van-icon name="star-o" color="#ee0a24" size="20" />
|
||||
<van-badge :content="orderBadge.review" v-if="orderBadge.review > 0" />
|
||||
</div>
|
||||
<span class="order-label">待评价</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具服务 -->
|
||||
<div class="menu-section">
|
||||
<van-cell title="我的收藏" is-link @click="onMenuClick('collect')">
|
||||
<template #icon>
|
||||
<div class="menu-icon-wrap" style="background:linear-gradient(135deg,#ff6b6b,#ff8e53);">
|
||||
<van-icon name="like-o" />
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell title="常用护工" is-link @click="onMenuClick('favorite_nurse')">
|
||||
<template #icon>
|
||||
<div class="menu-icon-wrap" style="background:linear-gradient(135deg,#018fa5,#02b5d1);">
|
||||
<van-icon name="contact" />
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell title="地址管理" is-link @click="onMenuClick('address')">
|
||||
<template #icon>
|
||||
<div class="menu-icon-wrap" style="background:linear-gradient(135deg,#67c23a,#85ce61);">
|
||||
<van-icon name="location-o" />
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell title="在线客服" is-link @click="onMenuClick('service')">
|
||||
<template #icon>
|
||||
<div class="menu-icon-wrap" style="background:linear-gradient(135deg,#e6a23c,#f0c78a);">
|
||||
<van-icon name="service-o" />
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
</div>
|
||||
|
||||
<!-- 设置相关 -->
|
||||
<div class="menu-section">
|
||||
<van-cell title="账号设置" is-link @click="onMenuClick('setting')">
|
||||
<template #icon>
|
||||
<div class="menu-icon-wrap" style="background:linear-gradient(135deg,#909399,#b1b3b8);">
|
||||
<van-icon name="setting-o" />
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell title="关于我们" is-link @click="onMenuClick('about')">
|
||||
<template #icon>
|
||||
<div class="menu-icon-wrap" style="background:linear-gradient(135deg,#7232dd,#9b69e6);">
|
||||
<van-icon name="info-o" />
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
</div>
|
||||
|
||||
<!-- 退出按钮 -->
|
||||
<div style="padding: 20px 15px;">
|
||||
<van-button type="danger" block round plain @click="onLogout">退出登录</van-button>
|
||||
</div>
|
||||
|
||||
<!-- 底部栏 -->
|
||||
{include file="PatientAPP/Component/van-tabbar" /}
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
|
||||
{block name="js"}
|
||||
<script>
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'mine',
|
||||
userInfo: {
|
||||
nickname: '患者用户',
|
||||
phone: '138****8888',
|
||||
avatar: ''
|
||||
},
|
||||
stats: {
|
||||
totalOrders: 12,
|
||||
pending: 2,
|
||||
completed: 10
|
||||
},
|
||||
orderBadge: {
|
||||
pendingPayment: 0,
|
||||
pendingService: 1,
|
||||
inProgress: 1,
|
||||
review: 0
|
||||
},
|
||||
menuList: [
|
||||
{ icon: 'like-o', title: '我的收藏', color: '#ff6b6b', action: 'collect' },
|
||||
{ icon: 'contact', title: '常用护工', color: '#018fa5', action: 'favorite_nurse' },
|
||||
{ icon: 'location-o', title: '地址管理', color: '#67c23a', action: 'address' },
|
||||
{ icon: 'service-o', title: '在线客服', color: '#e6a23c', action: 'service' },
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
goOrderList(type) {
|
||||
const map = {
|
||||
all: '全部订单',
|
||||
pending_payment: '待付款',
|
||||
pending_service: '待服务',
|
||||
pending: '待服务',
|
||||
in_progress: '进行中',
|
||||
completed: '已完成',
|
||||
review: '待评价'
|
||||
};
|
||||
vant.showToast({ message: `查看${map[type] || '订单列表'}`, position: 'top' });
|
||||
},
|
||||
onMenuClick(action) {
|
||||
const map = {
|
||||
collect: '我的收藏',
|
||||
favorite_nurse: '常用护工',
|
||||
address: '地址管理',
|
||||
service: '在线客服',
|
||||
setting: '账号设置',
|
||||
about: '关于我们'
|
||||
};
|
||||
vant.showToast({ message: map[action] || action, position: 'top' });
|
||||
},
|
||||
onLogout() {
|
||||
vant.showDialog({
|
||||
title: '提示',
|
||||
message: '确定要退出登录吗?'
|
||||
}).then(() => {
|
||||
vant.showToast({ type: 'success', message: '已退出登录' });
|
||||
}).catch(() => {});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeTab(val) {
|
||||
if (val !== 'mine') onTabBarChange(val);
|
||||
}
|
||||
}
|
||||
});
|
||||
app.use(vant);
|
||||
app.use(vant.Lazyload);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
{/block}
|
||||
@@ -0,0 +1,318 @@
|
||||
{extend name="PatientAPP/layout" /}
|
||||
|
||||
{block name="css"}
|
||||
<link rel="stylesheet" href="index.css" />
|
||||
{/block}
|
||||
|
||||
{block name="app"}
|
||||
<div id="app" v-cloak>
|
||||
<!-- 头部区域 -->
|
||||
<div class="header-section">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="header-search" @click="showSearch = true">
|
||||
<van-icon name="search" size="16" color="#999" />
|
||||
<span>搜索护工姓名/技能...</span>
|
||||
</div>
|
||||
|
||||
<!-- 筛选标签栏 -->
|
||||
<div class="filter-bar">
|
||||
<div class="filter-tag" :class="{ active: filterSkill }" @click="toggleFilter('skill')">{{ filterSkill || '技能类型' }}</div>
|
||||
<div class="filter-tag" :class="{ active: filterPrice }" @click="toggleFilter('price')">{{ currentPriceLabel }}</div>
|
||||
<div class="filter-tag" :class="{ active: filterExp }" @click="toggleFilter('exp')">{{ currentExpLabel }}</div>
|
||||
<div class="filter-tag" :class="{ active: filterDateRange.length > 0 }" @click="showDateCalendar = true">{{ currentDateLabel }}</div>
|
||||
<div class="filter-tag" :class="{ active: sortBy !== 'rating' }" @click="toggleFilter('sort')">{{ currentSortLabel }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选面板(内联展开) -->
|
||||
<transition name="slide">
|
||||
<div class="filter-panel" v-if="showFilterPanel" @click.stop>
|
||||
<div class="filter-panel-list">
|
||||
<span v-for="item in popupOptions" :key="item.value"
|
||||
class="filter-panel-item" :class="{ selected: isPopupSelected(item.value) }"
|
||||
@click="onPopupSelect(item.value)">{{ item.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="stats-bar">
|
||||
<div class="stats-item">
|
||||
<span class="stats-num">{{ filteredList.length }}</span>
|
||||
<span class="stats-label">位护工</span>
|
||||
</div>
|
||||
<div class="stats-divider"></div>
|
||||
<div class="stats-item">
|
||||
<span class="stats-num">98.6%</span>
|
||||
<span class="stats-label">好评率</span>
|
||||
</div>
|
||||
<div class="stats-divider"></div>
|
||||
<div class="stats-item">
|
||||
<span class="stats-num">24h</span>
|
||||
<span class="stats-label">快速响应</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 护工列表 -->
|
||||
<div class="nurse-list-wrapper">
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<van-list
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<div class="nurse-card-wrap" v-for="(nurse, index) in filteredList" :key="nurse.id" @click="viewNurse(nurse)">
|
||||
<div class="nurse-card-main">
|
||||
<!-- 左侧头像 -->
|
||||
<div class="nurse-avatar-box">
|
||||
<div class="nurse-avatar-inner" :class="{ 'avatar-online': nurse.online }">
|
||||
<van-icon name="contact" size="36" color="#fff" />
|
||||
</div>
|
||||
<span class="online-dot" v-if="nurse.online"></span>
|
||||
</div>
|
||||
<!-- 中间信息 -->
|
||||
<div class="nurse-detail">
|
||||
<div class="nurse-name-row">
|
||||
<span class="nurse-name">{{ nurse.name }}</span>
|
||||
<van-tag type="primary" size="small" plain v-if="nurse.isHot">热门</van-tag>
|
||||
<van-tag type="success" size="small" plain v-if="nurse.certified">认证</van-tag>
|
||||
</div>
|
||||
<div class="nurse-meta-row">
|
||||
<van-rate :value="nurse.rating" readonly size="12" color="#ff976a" void-icon="star" void-color="#eee" />
|
||||
<span class="nurse-score">{{ nurse.rating }}</span>
|
||||
<span class="nurse-sep">|</span>
|
||||
<span class="nurse-exp-text">{{ nurse.exp }}年经验</span>
|
||||
<span class="nurse-sep">|</span>
|
||||
<span class="nurse-orders-text">服务{{ nurse.orders }}次</span>
|
||||
</div>
|
||||
<div class="nurse-skill-tags">
|
||||
<van-tag type="primary" plain size="medium" v-for="(skill, i) in nurse.skills.slice(0, 4)" :key="'s'+i">{{ skill }}</van-tag>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧价格 -->
|
||||
<div class="nurse-price-col">
|
||||
<div class="nurse-price-row">
|
||||
<span class="price-symbol">¥</span>
|
||||
<span class="price-num">{{ nurse.price }}</span>
|
||||
<span class="price-unit">/时</span>
|
||||
</div>
|
||||
<van-button type="primary" size="small" round class="book-btn" @click.stop="bookNurse(nurse)">预约</van-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部认证信息 -->
|
||||
<div class="nurse-cert-row" v-if="nurse.certs && nurse.certs.length">
|
||||
<van-icon name="shield-o" size="14" color="#07c160" />
|
||||
<span class="cert-text">{{ nurse.certs.join(' · ') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
</div>
|
||||
|
||||
<!-- 时间范围选择弹窗 -->
|
||||
<van-popup v-model:show="showDateCalendar" round :style="{ width: '85%', padding: '20px' }">
|
||||
<div style="font-size: 16px; font-weight: 600; margin-bottom: 16px; text-align: center;">选择时间范围</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 12px;">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="font-size: 14px; color: #666; width: 36px;">开始</span>
|
||||
<input type="date" v-model="dateStart" style="flex:1; border:1px solid #e0e0e0; border-radius:8px; padding:10px 12px; font-size:14px; outline:none;" />
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="font-size: 14px; color: #666; width: 36px;">结束</span>
|
||||
<input type="date" v-model="dateEnd" style="flex:1; border:1px solid #e0e0e0; border-radius:8px; padding:10px 12px; font-size:14px; outline:none;" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 18px; display:flex; gap: 12px;">
|
||||
<van-button style="flex:1" @click="showDateCalendar = false">取消</van-button>
|
||||
<van-button style="flex:1" type="primary" @click="onDateConfirm">确定</van-button>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<van-popup v-model:show="showSearch" position="top" :style="{ height: 'auto' }">
|
||||
<van-search
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索护工姓名/技能/资质"
|
||||
shape="round"
|
||||
show-action
|
||||
@search="onSearch"
|
||||
@cancel="showSearch = false"
|
||||
>
|
||||
<template #action>
|
||||
<div @click="onSearch">搜索</div>
|
||||
</template>
|
||||
</van-search>
|
||||
</van-popup>
|
||||
|
||||
<!-- 底部栏 -->
|
||||
{include file="PatientAPP/Component/van-tabbar" /}
|
||||
</div>
|
||||
{/block}
|
||||
|
||||
{block name="js"}
|
||||
<script>
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'nurses',
|
||||
showSearch: false,
|
||||
searchKeyword: '',
|
||||
refreshing: false,
|
||||
loading: false,
|
||||
finished: true,
|
||||
filterSkill: '',
|
||||
filterPrice: '',
|
||||
filterExp: '',
|
||||
sortBy: 'rating',
|
||||
showFilterPanel: false,
|
||||
currentFilterType: '',
|
||||
showDateCalendar: false,
|
||||
filterDateRange: [],
|
||||
dateStart: '',
|
||||
dateEnd: '',
|
||||
skillOptions: [
|
||||
{ text: '全部技能', value: '' },
|
||||
{ text: '术后护理', value: '术后护理' },
|
||||
{ text: '老年照护', value: '老年照护' },
|
||||
{ text: '母婴护理', value: '母婴护理' },
|
||||
{ text: '康复训练', value: '康复训练' },
|
||||
{ text: '重症监护', value: '重症监护' },
|
||||
{ text: '输液换药', value: '输液换药' },
|
||||
{ text: '心理疏导', value: '心理疏导' },
|
||||
],
|
||||
priceOptions: [
|
||||
{ text: '价格排序', value: '' },
|
||||
{ text: '50元以下', value: 'low' },
|
||||
{ text: '50-70元', value: 'mid' },
|
||||
{ text: '70元以上', value: 'high' },
|
||||
],
|
||||
expOptions: [
|
||||
{ text: '经验不限', value: '' },
|
||||
{ text: '3年以下', value: 'junior' },
|
||||
{ text: '3-5年', value: 'mid' },
|
||||
{ text: '5年以上', value: 'senior' },
|
||||
],
|
||||
sortOptions: [
|
||||
{ text: '评分最高', value: 'rating' },
|
||||
{ text: '价格最低', value: 'price_asc' },
|
||||
{ text: '服务最多', value: 'orders' },
|
||||
{ text: '经验最久', value: 'exp' },
|
||||
],
|
||||
nurseList: [
|
||||
{ id: 1, name: '张秀芳', rating: 4.9, price: 68, exp: 8, orders: 320, isHot: true, certified: true, online: true, certs: ['护理证', '健康证'], skills: ['术后护理', '老年照护', '康复训练'] },
|
||||
{ id: 2, name: '李国华', rating: 4.8, price: 75, exp: 10, orders: 456, isHot: true, certified: true, online: true, certs: ['护士资格证', '急救证'], skills: ['重症监护', '输液换药', '心理疏导'] },
|
||||
{ id: 3, name: '王美玲', rating: 4.9, price: 62, exp: 5, orders: 218, isHot: false, certified: true, online: false, certs: ['护理证', '育婴师'], skills: ['母婴护理', '月子照护', '营养配餐'] },
|
||||
{ id: 4, name: '陈建国', rating: 4.7, price: 58, exp: 6, orders: 167, isHot: false, certified: true, online: true, certs: ['护理证', '康复师'], skills: ['行动辅助', '日常起居', '陪同就医'] },
|
||||
{ id: 5, name: '刘晓红', rating: 4.6, price: 55, exp: 3, orders: 89, isHot: false, certified: false, online: true, certs: ['护理证'], skills: ['基础护理', '饮食照料', '卫生清洁'] },
|
||||
{ id: 6, name: '赵文斌', rating: 4.8, price: 80, exp: 12, orders: 520, isHot: true, certified: true, online: false, certs: ['护士资格证', '急救证', '康复师'], skills: ['重症监护', '术后护理', '康复训练', '心理疏导'] },
|
||||
{ id: 7, name: '孙丽萍', rating: 4.5, price: 48, exp: 2, orders: 56, isHot: false, certified: false, online: true, certs: ['护理员培训证'], skills: ['生活照料', '陪伴聊天', '协助活动'] },
|
||||
{ id: 8, name: '周明辉', rating: 4.7, price: 65, exp: 7, orders: 289, isHot: false, certified: true, online: true, certs: ['健康证', '急救证'], skills: ['老年照护', '慢性病管理', '用药提醒'] },
|
||||
{ id: 9, name: '吴秀英', rating: 4.9, price: 72, exp: 9, orders: 380, isHot: true, certified: true, online: true, certs: ['护理证', '育婴师', '营养师证'], skills: ['母婴护理', '月子照护', '营养配餐', '产后恢复'] },
|
||||
{ id: 10, name: '郑海涛', rating: 4.6, price: 60, exp: 4, orders: 145, isHot: false, certified: true, online: false, certs: ['护理证', '康复师'], skills: ['康复训练', '行动辅助', '按摩理疗'] },
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredList() {
|
||||
let list = [...this.nurseList];
|
||||
if (this.searchKeyword) {
|
||||
const kw = this.searchKeyword.toLowerCase();
|
||||
list = list.filter(n =>
|
||||
n.name.includes(kw) ||
|
||||
n.skills.some(s => s.includes(kw)) ||
|
||||
n.certs.some(c => c.includes(kw))
|
||||
);
|
||||
}
|
||||
if (this.filterSkill) list = list.filter(n => n.skills.includes(this.filterSkill));
|
||||
if (this.filterPrice === 'low') list = list.filter(n => n.price < 50);
|
||||
else if (this.filterPrice === 'mid') list = list.filter(n => n.price >= 50 && n.price <= 70);
|
||||
else if (this.filterPrice === 'high') list = list.filter(n => n.price > 70);
|
||||
if (this.filterExp === 'junior') list = list.filter(n => n.exp < 3);
|
||||
else if (this.filterExp === 'mid') list = list.filter(n => n.exp >= 3 && n.exp <= 5);
|
||||
else if (this.filterExp === 'senior') list = list.filter(n => n.exp > 5);
|
||||
if (this.sortBy === 'rating') list.sort((a, b) => b.rating - a.rating);
|
||||
else if (this.sortBy === 'price_asc') list.sort((a, b) => a.price - b.price);
|
||||
else if (this.sortBy === 'orders') list.sort((a, b) => b.orders - a.orders);
|
||||
else if (this.sortBy === 'exp') list.sort((a, b) => b.exp - a.exp);
|
||||
return list;
|
||||
},
|
||||
currentPriceLabel() { const item = this.priceOptions.find(o => o.value === this.filterPrice); return item ? item.text : '价格区间'; },
|
||||
currentExpLabel() { const item = this.expOptions.find(o => o.value === this.filterExp); return item ? item.text : '工作经验'; },
|
||||
currentSortLabel() { const item = this.sortOptions.find(o => o.value === this.sortBy); return item ? item.text : '默认排序'; },
|
||||
currentDateLabel() {
|
||||
if (this.filterDateRange.length === 2) {
|
||||
return `${this.filterDateRange[0]} ~ ${this.filterDateRange[1]}`;
|
||||
}
|
||||
return '时间范围';
|
||||
},
|
||||
popupTitle() {
|
||||
const map = { skill: '技能类型', price: '价格区间', exp: '工作经验', sort: '排序方式' };
|
||||
return map[this.currentFilterType] || '';
|
||||
},
|
||||
popupOptions() {
|
||||
const map = { skill: this.skillOptions, price: this.priceOptions, exp: this.expOptions, sort: this.sortOptions };
|
||||
return map[this.currentFilterType] || [];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onBack() { history.back(); },
|
||||
onSearch() { this.showSearch = false; vant.showToast({ message: `搜索: ${this.searchKeyword}`, position: 'top' }); },
|
||||
toggleFilter(type) {
|
||||
if (this.showFilterPanel && this.currentFilterType === type) {
|
||||
this.showFilterPanel = false;
|
||||
} else {
|
||||
this.currentFilterType = type;
|
||||
this.showFilterPanel = true;
|
||||
}
|
||||
},
|
||||
isPopupSelected(val) {
|
||||
if (this.currentFilterType === 'skill') return this.filterSkill === val;
|
||||
if (this.currentFilterType === 'price') return this.filterPrice === val;
|
||||
if (this.currentFilterType === 'exp') return this.filterExp === val;
|
||||
if (this.currentFilterType === 'sort') return this.sortBy === val;
|
||||
return false;
|
||||
},
|
||||
onPopupSelect(val) {
|
||||
if (this.currentFilterType === 'skill') this.filterSkill = val;
|
||||
else if (this.currentFilterType === 'price') this.filterPrice = val;
|
||||
else if (this.currentFilterType === 'exp') this.filterExp = val;
|
||||
else if (this.currentFilterType === 'sort') this.sortBy = val;
|
||||
this.showFilterPanel = false;
|
||||
},
|
||||
onDateConfirm() {
|
||||
if (this.dateStart && this.dateEnd) {
|
||||
this.filterDateRange = [this.dateStart, this.dateEnd];
|
||||
} else {
|
||||
this.filterDateRange = [];
|
||||
}
|
||||
this.showDateCalendar = false;
|
||||
},
|
||||
onRefresh() {
|
||||
setTimeout(() => { this.refreshing = false; vant.showToast('刷新成功'); }, 1000);
|
||||
},
|
||||
onLoad() { this.loading = false; this.finished = true; },
|
||||
viewNurse(nurse) {
|
||||
vant.showDialog({
|
||||
title: nurse.name,
|
||||
message: `评分: ${nurse.rating}分\n价格: ¥${nurse.price}/小时\n经验: ${nurse.exp}年\n服务: ${nurse.orders}次\n资质: ${nurse.certs.join('、')}\n技能: ${nurse.skills.join('、')}`,
|
||||
});
|
||||
},
|
||||
bookNurse(nurse) {
|
||||
vant.showConfirmDialog({
|
||||
title: '确认预约',
|
||||
message: `确定要预约 ${nurse.name} 吗?\n价格: ¥${nurse.price}/小时`,
|
||||
}).then(() => {
|
||||
vant.showToast({ message: '预约成功,请等待确认', icon: 'success' });
|
||||
}).catch(() => {});
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeTab(val) { if (val !== 'nurses') { onTabBarChange(val); } },
|
||||
},
|
||||
});
|
||||
app.use(vant);
|
||||
app.use(vant.Lazyload);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
{/block}
|
||||
@@ -0,0 +1,553 @@
|
||||
{extend name="PatientAPP/layout" /}
|
||||
|
||||
{block name="css"}
|
||||
<link rel="stylesheet" href="/PatientAPP/Order/create.css" />
|
||||
{/block}
|
||||
|
||||
{block name="app"}
|
||||
<div id="app" v-cloak>
|
||||
<!-- 头部区域 -->
|
||||
<div class="header-section">
|
||||
<div class="header-top">
|
||||
<div class="header-left">
|
||||
<van-icon name="arrow-left" size="20" @click="goBack" />
|
||||
<span class="page-title">发布订单</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="icon-btn" @click="onDraft">
|
||||
<van-icon name="description" size="17" />
|
||||
<span style="font-size:11px;margin-left:3px;">草稿</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表单主体 -->
|
||||
<div class="form-wrapper">
|
||||
<!-- 服务类型选择 -->
|
||||
<div class="form-card form-card-type">
|
||||
<div class="fc-label"><van-icon name="apps-o" color="#018fa5" size="16" /> 选择服务类型</div>
|
||||
<div class="type-grid">
|
||||
<div class="type-item"
|
||||
:class="{ 'type-active': form.serviceType === item.value }"
|
||||
v-for="(item, index) in serviceTypes"
|
||||
:key="index"
|
||||
@click="selectServiceType(item.value)">
|
||||
<div class="type-icon-wrap" :class="'wrap-' + item.value">
|
||||
<van-icon :name="item.icon" size="24" :color="form.serviceType === item.value ? '#fff' : item.color" />
|
||||
</div>
|
||||
<span class="type-name">{{ item.label }}</span>
|
||||
<span class="type-desc">{{ item.desc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 患者信息 -->
|
||||
<div class="form-card">
|
||||
<div class="fc-header">
|
||||
<div class="fc-label"><van-icon name="contact" color="#018fa5" size="16" /> 患者信息</div>
|
||||
</div>
|
||||
<div class="fc-body">
|
||||
<van-field v-model="form.patientName" label="姓名" placeholder="请输入患者姓名" clearable />
|
||||
<van-field v-model="form.patientPhone" label="联系电话" type="tel" placeholder="请输入联系电话" clearable />
|
||||
<van-field v-model="form.patientAge" label="年龄" type="digit" placeholder="请输入年龄" />
|
||||
<van-field v-model="form.patientGender" label="性别" readonly placeholder="请选择" is-link @click="showGenderPicker = true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务时间 -->
|
||||
<div class="form-card">
|
||||
<div class="fc-header">
|
||||
<div class="fc-label"><van-icon name="clock-o" color="#018fa5" size="16" /> 服务时间</div>
|
||||
</div>
|
||||
<div class="fc-body">
|
||||
<van-field
|
||||
:model-value="form.startDate || ''"
|
||||
label="开始日期"
|
||||
readonly
|
||||
placeholder="请选择开始日期"
|
||||
is-link
|
||||
@click="openStartDatePicker"
|
||||
/>
|
||||
<van-field
|
||||
:model-value="form.startTime || ''"
|
||||
label="开始时间"
|
||||
readonly
|
||||
placeholder="请选择开始时间"
|
||||
is-link
|
||||
@click="openStartTimePicker"
|
||||
/>
|
||||
<van-field
|
||||
:model-value="form.endDate || ''"
|
||||
label="结束日期"
|
||||
readonly
|
||||
placeholder="请先选开始日期"
|
||||
is-link
|
||||
@click="openEndDatePicker"
|
||||
/>
|
||||
<van-field
|
||||
:model-value="form.endTime || ''"
|
||||
label="结束时间"
|
||||
readonly
|
||||
placeholder="请先选结束日期"
|
||||
is-link
|
||||
@click="openEndTimePicker"
|
||||
/>
|
||||
|
||||
<div class="duration-tip" v-if="calcDuration">
|
||||
<van-icon name="info-o" size="14" />
|
||||
<span>预计服务时长:<strong>{{ calcDuration }}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务地点 -->
|
||||
<div class="form-card">
|
||||
<div class="fc-header">
|
||||
<div class="fc-label"><van-icon name="location-o" color="#018fa5" size="16" /> 服务地点</div>
|
||||
<van-tag type="primary" size="medium" plain round @click="useCurrentLocation">使用当前位置</van-tag>
|
||||
</div>
|
||||
<div class="fc-body">
|
||||
<van-field v-model="form.hospitalName" label="医院/地点" placeholder="请输入医院名称或服务地点" />
|
||||
<van-field
|
||||
v-model="form.detailAddress"
|
||||
label="详细地址"
|
||||
type="textarea"
|
||||
rows="2"
|
||||
autosize
|
||||
placeholder="请输入详细地址,如病房号、楼层等"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 护工要求 -->
|
||||
<div class="form-card">
|
||||
<div class="fc-header">
|
||||
<div class="fc-label"><van-icon name="shield-o" color="#018fa5" size="16" /> 护工要求</div>
|
||||
</div>
|
||||
<div class="fc-body">
|
||||
<van-field v-model="form.nurseGender" label="护工性别" readonly placeholder="不限" is-link @click="showNurseGenderPicker = true" />
|
||||
<van-field v-model="form.experienceReq" label="经验要求" readonly placeholder="不限" is-link @click="showExpPicker = true" />
|
||||
<div class="skill-tags-section">
|
||||
<div class="skill-tags-label">特殊技能需求(可多选)</div>
|
||||
<div class="skill-tags-list">
|
||||
<span
|
||||
class="skill-tag"
|
||||
:class="{ 'skill-active': form.skills.includes(skill) }"
|
||||
v-for="(skill, index) in skillOptions"
|
||||
:key="index"
|
||||
@click="toggleSkill(skill)"
|
||||
>{{ skill }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 备注说明 -->
|
||||
<div class="form-card">
|
||||
<div class="fc-header">
|
||||
<div class="fc-label"><van-icon name="edit" color="#018fa5" size="16" /> 备注说明</div>
|
||||
</div>
|
||||
<div class="fc-body">
|
||||
<van-field
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
rows="4"
|
||||
autosize
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请详细描述患者的身体状况、护理需求等特殊情况,以便匹配合适的护工..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 费用预估 -->
|
||||
<div class="form-card price-preview" v-if="estimatedPrice > 0">
|
||||
<div class="price-row">
|
||||
<span class="price-label">费用预估</span>
|
||||
<div class="price-value">
|
||||
<span class="yen">¥</span>
|
||||
<span class="amount">{{ estimatedPrice }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="price-detail">
|
||||
<div class="price-item">
|
||||
<span>基础费用({{ basePrice }}元/小时 x {{ totalHours }}小时)</span>
|
||||
<span>¥{{ basePrice * totalHours }}</span>
|
||||
</div>
|
||||
<div class="price-item" v-if="form.skills.length > 0">
|
||||
<span>技能加成({{ form.skills.length }}项技能 +10%)</span>
|
||||
<span>¥{{ Math.round(basePrice * totalHours * 0.1) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="bottom-bar">
|
||||
<div class="bottom-left">
|
||||
<div class="total-price">
|
||||
<span class="tp-label">预估总价:</span>
|
||||
<span class="tp-value"><span class="tp-yen">¥</span>{{ estimatedPrice || '0' }}</span>
|
||||
</div>
|
||||
<div class="tp-desc">实际价格以接单后确认为准</div>
|
||||
</div>
|
||||
<van-button type="primary" class="submit-btn" round :loading="submitting" @click="submitOrder">
|
||||
发布订单
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<!-- 性别选择器 -->
|
||||
<van-popup v-model:show="showGenderPicker" position="bottom" round>
|
||||
<van-picker :columns="genderColumns" @confirm="onGenderConfirm" @cancel="showGenderPicker = false" title="选择性别" />
|
||||
</van-popup>
|
||||
|
||||
<!-- 护工性别选择器 -->
|
||||
<van-popup v-model:show="showNurseGenderPicker" position="bottom" round>
|
||||
<van-picker :columns="nurseGenderColumns" @confirm="onNurseGenderConfirm" @cancel="showNurseGenderPicker = false" title="选择护工性别" />
|
||||
</van-popup>
|
||||
|
||||
<!-- 经验要求选择器 -->
|
||||
<van-popup v-model:show="showExpPicker" position="bottom" round>
|
||||
<van-picker :columns="expColumns" @confirm="onExpConfirm" @cancel="showExpPicker = false" title="选择经验要求" />
|
||||
</van-popup>
|
||||
|
||||
<!-- 开始日期选择器 -->
|
||||
<van-popup v-model:show="showStartDatePicker" position="bottom" round>
|
||||
<van-date-picker
|
||||
:key="'sd-' + startDateKey"
|
||||
v-model="startDatePickerValue"
|
||||
title="选择开始日期"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
@confirm="onStartDateConfirm"
|
||||
@cancel="showStartDatePicker = false"
|
||||
/>
|
||||
</van-popup>
|
||||
|
||||
<!-- 开始时间选择器 -->
|
||||
<van-popup v-model:show="showStartTimePicker" position="bottom" round>
|
||||
<van-time-picker
|
||||
:key="'st-' + startTimeKey"
|
||||
v-model="startTimePickerValue"
|
||||
title="选择开始时间"
|
||||
:columns-type="['hour', 'minute']"
|
||||
@confirm="onStartTimeConfirm"
|
||||
@cancel="showStartTimePicker = false"
|
||||
/>
|
||||
</van-popup>
|
||||
|
||||
<!-- 结束日期选择器 -->
|
||||
<van-popup v-model:show="showEndDatePicker" position="bottom" round>
|
||||
<van-date-picker
|
||||
:key="'ed-' + endDateKey"
|
||||
v-model="endDatePickerValue"
|
||||
title="选择结束日期"
|
||||
:min-date="startMinEndDate"
|
||||
:max-date="maxDate"
|
||||
@confirm="onEndDateConfirm"
|
||||
@cancel="showEndDatePicker = false"
|
||||
/>
|
||||
</van-popup>
|
||||
|
||||
<!-- 结束时间选择器 -->
|
||||
<van-popup v-model:show="showEndTimePicker" position="bottom" round>
|
||||
<van-time-picker
|
||||
:key="'et-' + endTimeKey"
|
||||
v-model="endTimePickerValue"
|
||||
title="选择结束时间"
|
||||
:columns-type="['hour', 'minute']"
|
||||
@confirm="onEndTimeConfirm"
|
||||
@cancel="showEndTimePicker = false"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
{/block}
|
||||
|
||||
{block name="js"}
|
||||
<script>
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
const now = new Date();
|
||||
return {
|
||||
submitting: false,
|
||||
showGenderPicker: false,
|
||||
showNurseGenderPicker: false,
|
||||
showExpPicker: false,
|
||||
minDate: new Date(),
|
||||
maxDate: new Date(Date.now() + 365 * 86400000),
|
||||
|
||||
// 开始日期选择器
|
||||
showStartDatePicker: false,
|
||||
startDateKey: 0,
|
||||
startDatePickerValue: ['2026', '5', '9'],
|
||||
|
||||
// 开始时间选择器
|
||||
showStartTimePicker: false,
|
||||
startTimeKey: 0,
|
||||
startTimePickerValue: ['8', '0'],
|
||||
|
||||
// 结束日期选择器
|
||||
showEndDatePicker: false,
|
||||
endDateKey: 0,
|
||||
endDatePickerValue: ['2026', '5', '10'],
|
||||
|
||||
// 结束时间选择器
|
||||
showEndTimePicker: false,
|
||||
endTimeKey: 0,
|
||||
endTimePickerValue: ['18', '0'],
|
||||
|
||||
genderColumns: ['男', '女'],
|
||||
nurseGenderColumns: ['不限', '男', '女'],
|
||||
expColumns: ['不限', '1年以上', '3年以上', '5年以上', '8年以上'],
|
||||
|
||||
serviceTypes: [
|
||||
{ value: 'hospital', label: '住院陪护', icon: 'hotel-o', desc: '院内专业护理', color: '#018fa5' },
|
||||
{ value: 'outpatient', label: '门诊陪护', icon: 'guide-o', desc: '陪同就诊检查', color: '#e08c28' },
|
||||
{ value: 'home', label: '居家陪护', icon: 'home-o', desc: '上门居家照料', color: '#2ba245' },
|
||||
{ value: 'maternal', label: '母婴护理', icon: 'like-o', desc: '月子期专业照护', color: '#e91e8b' },
|
||||
],
|
||||
|
||||
skillOptions: ['术后护理', '老年照护', '康复训练', '输液换药', '重症监护', '母婴护理', '营养配餐', '心理疏导', '行动辅助', '中医理疗'],
|
||||
|
||||
form: {
|
||||
serviceType: '',
|
||||
patientName: '',
|
||||
patientPhone: '',
|
||||
patientAge: '',
|
||||
patientGender: '',
|
||||
startDate: '',
|
||||
startTime: '',
|
||||
endDate: '',
|
||||
endTime: '',
|
||||
hospitalName: '',
|
||||
detailAddress: '',
|
||||
nurseGender: '',
|
||||
experienceReq: '',
|
||||
skills: [],
|
||||
remark: '',
|
||||
},
|
||||
|
||||
basePriceMap: {
|
||||
hospital: 68,
|
||||
outpatient: 58,
|
||||
home: 52,
|
||||
maternal: 88,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
startMinEndDate() {
|
||||
if (!this.form.startDate) return this.minDate;
|
||||
const d = new Date(this.form.startDate.replace(/-/g, '/'));
|
||||
return d;
|
||||
},
|
||||
totalHours() {
|
||||
if (!this.form.startDate || !this.form.endDate) return 0;
|
||||
const start = new Date(`${this.form.startDate.replace(/-/g, '/')} ${this.form.startTime || '00:00'}`);
|
||||
const end = new Date(`${this.form.endDate.replace(/-/g, '/')} ${this.form.endTime || '00:00'}`);
|
||||
let diff = (end - start) / 3600000;
|
||||
if (diff <= 0) diff = 0;
|
||||
return Math.round(diff * 10) / 10;
|
||||
},
|
||||
totalDays() {
|
||||
if (!this.form.startDate || !this.form.endDate) return 0;
|
||||
const s = new Date(this.form.startDate.replace(/-/g, '/'));
|
||||
const e = new Date(this.form.endDate.replace(/-/g, '/'));
|
||||
return Math.ceil((e - s) / 86400000);
|
||||
},
|
||||
basePrice() {
|
||||
return this.basePriceMap[this.form.serviceType] || 60;
|
||||
},
|
||||
estimatedPrice() {
|
||||
if (this.totalHours === 0) return 0;
|
||||
let price = this.basePrice * this.totalHours;
|
||||
if (this.form.skills.length > 0) {
|
||||
price = Math.round(price * 1.1);
|
||||
}
|
||||
return price;
|
||||
},
|
||||
calcDuration() {
|
||||
if (!this.form.startDate || !this.form.endDate) return '';
|
||||
const days = this.totalDays;
|
||||
const hours = this.totalHours;
|
||||
if (days <= 0 && hours <= 0) return '';
|
||||
if (days > 0) return `${days}天 ${hours}小时`;
|
||||
return `${hours}小时`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
goBack() { history.back(); },
|
||||
onDraft() { vant.showToast('暂无草稿'); },
|
||||
selectServiceType(type) { this.form.serviceType = type; },
|
||||
useCurrentLocation() {
|
||||
vant.showToast({ message: '正在获取位置...', loading: true, duration: 1500 });
|
||||
setTimeout(() => {
|
||||
this.form.hospitalName = '当前位置附近';
|
||||
vant.clearToast();
|
||||
vant.showToast('已获取位置');
|
||||
}, 1000);
|
||||
},
|
||||
toggleSkill(skill) {
|
||||
const idx = this.form.skills.indexOf(skill);
|
||||
if (idx > -1) {
|
||||
this.form.skills.splice(idx, 1);
|
||||
} else {
|
||||
this.form.skills.push(skill);
|
||||
}
|
||||
},
|
||||
padZero(n) { return String(n).padStart(2, '0'); },
|
||||
openStartDatePicker() {
|
||||
if (this.form.startDate) {
|
||||
const p = this.form.startDate.split('-');
|
||||
this.startDatePickerValue = [p[0], String(parseInt(p[1])), String(parseInt(p[2]))];
|
||||
} else {
|
||||
const now = new Date();
|
||||
this.startDatePickerValue = [
|
||||
String(now.getFullYear()),
|
||||
String(now.getMonth() + 1),
|
||||
String(now.getDate())
|
||||
];
|
||||
}
|
||||
this.startDateKey++;
|
||||
this.showStartDatePicker = true;
|
||||
},
|
||||
openStartTimePicker() {
|
||||
if (!this.form.startDate) {
|
||||
vant.showToast('请先选择开始日期');
|
||||
return;
|
||||
}
|
||||
if (this.form.startTime) {
|
||||
const p = this.form.startTime.split(':');
|
||||
this.startTimePickerValue = [String(parseInt(p[0])), String(parseInt(p[1]))];
|
||||
} else {
|
||||
this.startTimePickerValue = ['8', '0'];
|
||||
}
|
||||
this.startTimeKey++;
|
||||
this.showStartTimePicker = true;
|
||||
},
|
||||
openEndDatePicker() {
|
||||
if (!this.form.startDate) {
|
||||
vant.showToast('请先选择开始日期');
|
||||
return;
|
||||
}
|
||||
if (this.form.endDate) {
|
||||
const p = this.form.endDate.split('-');
|
||||
this.endDatePickerValue = [p[0], String(parseInt(p[1])), String(parseInt(p[2]))];
|
||||
} else {
|
||||
const start = new Date(this.form.startDate.replace(/-/g, '/'));
|
||||
start.setDate(start.getDate() + 1);
|
||||
this.endDatePickerValue = [
|
||||
String(start.getFullYear()),
|
||||
String(start.getMonth() + 1),
|
||||
String(start.getDate())
|
||||
];
|
||||
}
|
||||
this.endDateKey++;
|
||||
this.showEndDatePicker = true;
|
||||
},
|
||||
openEndTimePicker() {
|
||||
if (!this.form.endDate) {
|
||||
vant.showToast('请先选择结束日期');
|
||||
return;
|
||||
}
|
||||
if (this.form.endTime) {
|
||||
const p = this.form.endTime.split(':');
|
||||
this.endTimePickerValue = [String(parseInt(p[0])), String(parseInt(p[1]))];
|
||||
} else {
|
||||
this.endTimePickerValue = ['18', '0'];
|
||||
}
|
||||
this.endTimeKey++;
|
||||
this.showEndTimePicker = true;
|
||||
},
|
||||
onStartDateConfirm({ selectedValues }) {
|
||||
const v = selectedValues;
|
||||
this.form.startDate = `${v[0]}-${this.padZero(v[1])}-${this.padZero(v[2])}`;
|
||||
// 如果已选结束日期早于开始日期,清空结束日期
|
||||
if (this.form.endDate && new Date(this.form.endDate.replace(/-/g, '/')) < new Date(this.form.startDate.replace(/-/g, '/'))) {
|
||||
this.form.endDate = '';
|
||||
this.form.endTime = '';
|
||||
}
|
||||
this.showStartDatePicker = false;
|
||||
},
|
||||
onStartTimeConfirm({ selectedValues }) {
|
||||
const v = selectedValues;
|
||||
this.form.startTime = `${this.padZero(v[0])}:${this.padZero(v[1])}`;
|
||||
this.showStartTimePicker = false;
|
||||
},
|
||||
onEndDateConfirm({ selectedValues }) {
|
||||
const v = selectedValues;
|
||||
this.form.endDate = `${v[0]}-${this.padZero(v[1])}-${this.padZero(v[2])}`;
|
||||
// 校验结束日期不能早于开始日期
|
||||
if (new Date(this.form.endDate.replace(/-/g, '/')) < new Date(this.form.startDate.replace(/-/g, '/'))) {
|
||||
vant.showToast('结束日期不能早于开始日期');
|
||||
this.form.endDate = '';
|
||||
return;
|
||||
}
|
||||
this.showEndDatePicker = false;
|
||||
},
|
||||
onEndTimeConfirm({ selectedValues }) {
|
||||
const v = selectedValues;
|
||||
const endTimeStr = `${this.padZero(v[0])}:${this.padZero(v[1])}`;
|
||||
// 校验结束时间须晚于开始时间
|
||||
if (this.form.endDate === this.form.startDate && this.form.startTime) {
|
||||
const endFull = new Date(`${this.form.endDate.replace(/-/g, '/')} ${endTimeStr}`);
|
||||
const startFull = new Date(`${this.form.startDate.replace(/-/g, '/')} ${this.form.startTime}`);
|
||||
if (endFull <= startFull) {
|
||||
vant.showToast('结束时间须晚于开始时间');
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.form.endTime = endTimeStr;
|
||||
this.showEndTimePicker = false;
|
||||
},
|
||||
onGenderConfirm({ selectedOptions }) { this.form.patientGender = selectedOptions[0]?.text || ''; this.showGenderPicker = false; },
|
||||
onNurseGenderConfirm({ selectedOptions }) { this.form.nurseGender = selectedOptions[0]?.text || ''; this.showNurseGenderPicker = false; },
|
||||
onExpConfirm({ selectedOptions }) { this.form.experienceReq = selectedOptions[0]?.text || ''; this.showExpPicker = false; },
|
||||
formatDateOnly(date) {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
},
|
||||
validateForm() {
|
||||
if (!this.form.serviceType) { vant.showToast('请选择服务类型'); return false; }
|
||||
if (!this.form.patientName) { vant.showToast('请输入患者姓名'); return false; }
|
||||
if (!this.form.patientPhone) { vant.showToast('请输入联系电话'); return false; }
|
||||
if (!/^1\d{10}$/.test(this.form.patientPhone)) { vant.showToast('手机号格式不正确'); return false; }
|
||||
if (!this.form.startDate || !this.form.startTime) { vant.showToast('请选择开始服务时间'); return false; }
|
||||
if (!this.form.endDate || !this.form.endTime) { vant.showToast('请选择结束服务时间'); return false; }
|
||||
if (this.totalHours <= 0) { vant.showToast('结束时间须晚于开始时间'); return false; }
|
||||
if (!this.form.hospitalName) { vant.showToast('请输入服务地点'); return false; }
|
||||
return true;
|
||||
},
|
||||
submitOrder() {
|
||||
if (!this.validateForm()) return;
|
||||
|
||||
vant.showConfirmDialog({
|
||||
title: '确认发布',
|
||||
message: `服务类型:${this.serviceTypes.find(s => s.value === this.form.serviceType)?.label}\n服务时间:${this.form.startDate} ${this.form.startTime} ~ ${this.form.endDate} ${this.form.endTime}\n服务时长:${this.calcDuration}\n预估费用:¥${this.estimatedPrice}`,
|
||||
confirmButtonText: '确认发布',
|
||||
cancelButtonText: '再想想',
|
||||
}).then(() => {
|
||||
this.submitting = true;
|
||||
setTimeout(() => {
|
||||
this.submitting = false;
|
||||
vant.showDialog({
|
||||
title: '发布成功',
|
||||
message: '订单已发布,系统将为您匹配合适的护工,请耐心等待接单。',
|
||||
confirmButtonText: '查看订单',
|
||||
}).then(() => {
|
||||
window.location.href = '/PatientApp/Order/index';
|
||||
});
|
||||
}, 1500);
|
||||
}).catch(() => {});
|
||||
},
|
||||
},
|
||||
watch: {},
|
||||
});
|
||||
app.use(vant);
|
||||
app.use(vant.Lazyload);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
{/block}
|
||||
@@ -0,0 +1,382 @@
|
||||
{extend name="PatientAPP/layout" /}
|
||||
|
||||
{block name="css"}
|
||||
<link rel="stylesheet" href="/PatientAPP/Order/order.css" />
|
||||
{/block}
|
||||
|
||||
{block name="app"}
|
||||
<div id="app" v-cloak>
|
||||
<!-- 头部区域 -->
|
||||
<div class="header-section">
|
||||
<div class="header-top">
|
||||
<div class="page-title">我的订单</div>
|
||||
<div class="header-actions">
|
||||
<div class="icon-btn" @click="onSearch">
|
||||
<van-icon name="search" size="17" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab标签栏 -->
|
||||
<van-tabs v-model:active="activeTab" swipeable sticky :offset-top="0" color="#fff" line-width="20" line-height="3">
|
||||
<van-tab title="全部" name="all"></van-tab>
|
||||
<van-tab title="待接单" name="pending"></van-tab>
|
||||
<van-tab title="进行中" name="ongoing"></van-tab>
|
||||
<van-tab title="已完成" name="completed"></van-tab>
|
||||
<van-tab title="已取消" name="cancelled"></van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<div class="order-list-wrapper">
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<van-list
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<div class="order-card" v-for="(order, index) in filteredOrders" :key="order.id" @click="viewOrder(order)">
|
||||
<!-- 卡片头部:状态 + 订单号 + 时间 -->
|
||||
<div class="oc-top">
|
||||
<div class="oc-top-left">
|
||||
<span class="oc-status-dot" :class="'dot-' + order.status"></span>
|
||||
<span class="oc-status-text" :class="'text-' + order.status">{{ order.statusText }}</span>
|
||||
<span class="oc-no">{{ order.orderNo }}</span>
|
||||
</div>
|
||||
<span class="oc-time">{{ order.createTime }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 核心信息区 -->
|
||||
<div class="oc-main">
|
||||
<!-- 左侧服务类型图标 -->
|
||||
<div class="oc-service-icon" :class="'icon-' + order.status">
|
||||
<van-icon :name="serviceIcon(order.serviceType)" size="24" />
|
||||
</div>
|
||||
<!-- 右侧详情 -->
|
||||
<div class="oc-detail">
|
||||
<div class="oc-title-row">
|
||||
<span class="oc-service-type">{{ order.serviceType }}</span>
|
||||
<van-tag v-if="order.status === 'ongoing'" type="primary" size="medium" round>服务中</van-tag>
|
||||
<van-tag v-if="order.status === 'pending'" color="#fef4ec" text-color="#e08c28" size="medium" round>等待接单</van-tag>
|
||||
</div>
|
||||
<div class="oc-meta-row">
|
||||
<div class="oc-meta-item">
|
||||
<van-icon name="clock-o" size="14" />
|
||||
<span>{{ order.serviceTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oc-meta-row">
|
||||
|
||||
</div>
|
||||
<div class="oc-meta-row">
|
||||
<div class="oc-meta-item">
|
||||
<van-icon name="location-o" size="14" />
|
||||
<span class="oc-location">{{ order.location }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 护工信息条 -->
|
||||
<div class="oc-nurse-bar" v-if="order.nurseName" @click.stop="togglePhone(order)">
|
||||
<div class="nurse-avatar-sm">
|
||||
<van-icon name="contact" size="14" />
|
||||
</div>
|
||||
<span class="nurse-name-sm">{{ order.nurseName }}</span>
|
||||
<span class="nurse-exp-sm">· {{ order.nurseExp }}年经验</span>
|
||||
<span class="nurse-phone-sm">
|
||||
{{ order.showPhone ? order.nursePhone : order.nursePhoneHidden }}
|
||||
<van-icon :name="order.showPhone ? 'eye-o' : 'closed-eye'" size="12" style="margin-left:4px;vertical-align:middle;" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 备注预览 -->
|
||||
<div class="oc-remark" v-if="order.remark">
|
||||
<!-- <van-icon name="chat-o" size="13" /> -->
|
||||
<span style="color: #cc6600;font-weight: 600;">订单备注:</span>
|
||||
<span>{{ order.remark }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="oc-bottom">
|
||||
<div class="oc-price-block">
|
||||
<span class="oc-price-label">实付金额</span>
|
||||
<div class="oc-price-value">
|
||||
<span class="oc-yen">¥</span>
|
||||
<span class="oc-amount">{{ order.totalAmount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oc-actions">
|
||||
<van-button
|
||||
v-if="order.status === 'pending'"
|
||||
size="small"
|
||||
round
|
||||
plain
|
||||
hairline
|
||||
@click.stop="cancelOrder(order)"
|
||||
>取消订单</van-button>
|
||||
<template v-if="order.status === 'ongoing'">
|
||||
<van-button
|
||||
size="small"
|
||||
round
|
||||
plain
|
||||
hairline
|
||||
type="primary"
|
||||
@click.stop="extendOrder(order)"
|
||||
>延长服务</van-button>
|
||||
<van-button
|
||||
size="small"
|
||||
round
|
||||
type="primary"
|
||||
@click.stop="contactNurse(order)"
|
||||
>联系护工</van-button>
|
||||
</template>
|
||||
<template v-if="order.status === 'completed' && !order.reviewed">
|
||||
<van-button
|
||||
size="small"
|
||||
round
|
||||
plain
|
||||
hairline
|
||||
@click.stop="viewOrder(order)"
|
||||
>查看详情</van-button>
|
||||
<van-button
|
||||
size="small"
|
||||
round
|
||||
type="success"
|
||||
@click.stop="reviewOrder(order)"
|
||||
>去评价</van-button>
|
||||
</template>
|
||||
<template v-if="order.status === 'completed' && order.reviewed">
|
||||
<van-button
|
||||
size="small"
|
||||
round
|
||||
plain
|
||||
hairline
|
||||
@click.stop="viewReview(order)"
|
||||
>查看评价</van-button>
|
||||
</template>
|
||||
<template v-if="order.status === 'cancelled'">
|
||||
<van-button
|
||||
size="small"
|
||||
round
|
||||
plain
|
||||
hairline
|
||||
@click.stop="reorder(order)"
|
||||
>再次下单</van-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<van-empty
|
||||
v-if="!loading && filteredOrders.length === 0"
|
||||
description="暂无订单记录"
|
||||
image="search"
|
||||
>
|
||||
<van-button round type="primary" size="small" class="empty-btn" @click="goPublish">立即下单</van-button>
|
||||
</van-empty>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
</div>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<van-popup v-model:show="showSearch" position="top" :style="{ height: 'auto' }">
|
||||
<van-search
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索订单号/服务类型/地点"
|
||||
shape="round"
|
||||
show-action
|
||||
@search="doSearch"
|
||||
@cancel="showSearch = false"
|
||||
>
|
||||
<template #action>
|
||||
<div @click="doSearch">搜索</div>
|
||||
</template>
|
||||
</van-search>
|
||||
</van-popup>
|
||||
|
||||
<!-- 底部栏 -->
|
||||
{include file="PatientAPP/Component/van-tabbar" /}
|
||||
</div>
|
||||
{/block}
|
||||
|
||||
{block name="js"}
|
||||
<script>
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'all',
|
||||
showSearch: false,
|
||||
searchKeyword: '',
|
||||
refreshing: false,
|
||||
loading: false,
|
||||
finished: true,
|
||||
stats: { pending: 2, ongoing: 1, completed: 8 },
|
||||
orderList: [
|
||||
{
|
||||
id: 1, orderNo: 'MC20260509001', status: 'ongoing', statusText: '进行中',
|
||||
createTime: '2026-05-08 14:30', serviceType: '住院陪护',
|
||||
serviceTime: '2026-05-08 15:00 ~ 2026-05-11 15:00 (3天)',
|
||||
location: '北京协和医院 住院部12楼1205病房',
|
||||
nurseName: '张秀芳', nurseExp: 8, nurseRating: 4.9,
|
||||
nursePhone: '13812345678', nursePhoneHidden: '138****5678', showPhone: false,
|
||||
remark: '术后护理,需要协助翻身、拍背,注意引流管',
|
||||
totalAmount: 2448, reviewed: false,
|
||||
},
|
||||
{
|
||||
id: 2, orderNo: 'MC20260508002', status: 'pending', statusText: '待接单',
|
||||
createTime: '2026-05-09 10:00', serviceType: '门诊陪护',
|
||||
serviceTime: '2026-05-10 08:30 ~ 12:30 (半天)',
|
||||
location: '北京大学第一医院 门诊楼3层',
|
||||
nurseName: '', nurseExp: '', nurseRating: '',
|
||||
remark: '陪同就诊,协助挂号、缴费、取药',
|
||||
totalAmount: 196, reviewed: false,
|
||||
},
|
||||
{
|
||||
id: 3, orderNo: 'MC20260507003', status: 'pending', statusText: '待接单',
|
||||
createTime: '2026-05-07 16:20', serviceType: '居家陪护',
|
||||
serviceTime: '2026-05-10 09:00 ~ 17:00 (1天)',
|
||||
location: '北京市朝阳区建国路88号院3号楼2单元501',
|
||||
nurseName: '', nurseExp: '', nurseRating: '',
|
||||
remark: '老人日常照料,做饭、打扫卫生、陪同散步',
|
||||
totalAmount: 520, reviewed: false,
|
||||
},
|
||||
{
|
||||
id: 4, orderNo: 'MC20260505004', status: 'completed', statusText: '已完成',
|
||||
createTime: '2026-05-03 09:15', serviceType: '住院陪护',
|
||||
serviceTime: '2026-05-03 10:00 ~ 2026-05-06 10:00 (3天)',
|
||||
location: '中日友好医院 住院部8楼802病房',
|
||||
nurseName: '李国华', nurseExp: 10, nurseRating: 4.8,
|
||||
nursePhone: '13987654321', nursePhoneHidden: '139****4321', showPhone: false,
|
||||
remark: '重症监护后转普通病房的康复期护理',
|
||||
totalAmount: 2700, reviewed: true,
|
||||
},
|
||||
{
|
||||
id: 5, orderNo: 'MC20260430005', status: 'completed', statusText: '已完成',
|
||||
createTime: '2026-04-28 11:40', serviceType: '母婴护理',
|
||||
serviceTime: '2026-04-29 08:00 ~ 2026-05-13 08:00 (14天)',
|
||||
location: '北京妇产医院 家庭住所(朝阳区望京)',
|
||||
nurseName: '王美玲', nurseExp: 5, nurseRating: 4.9,
|
||||
nursePhone: '13666668888', nursePhoneHidden: '3666668888', showPhone: false,
|
||||
remark: '月子期间母婴护理,新生儿照料和产妇恢复',
|
||||
totalAmount: 11200, reviewed: true,
|
||||
},
|
||||
{
|
||||
id: 6, orderNo: 'MC20260425006', status: 'completed', statusText: '已完成',
|
||||
createTime: '2026-04-23 15:00', serviceType: '门诊陪护',
|
||||
serviceTime: '2026-04-25 07:30 ~ 13:00 (半天)',
|
||||
location: '首都医科大学附属北京天坛医院',
|
||||
nurseName: '陈建国', nurseExp: 6, nurseRating: 4.7,
|
||||
nursePhone: '15811112222', nursePhoneHidden: '158****2222', showPhone: false,
|
||||
remark: '陪同做核磁共振检查,老人行动不便需轮椅辅助',
|
||||
totalAmount: 232, reviewed: false,
|
||||
},
|
||||
{
|
||||
id: 7, orderNo: 'MC20260420007', status: 'cancelled', statusText: '已取消',
|
||||
createTime: '2026-04-18 09:30', serviceType: '居家陪护',
|
||||
serviceTime: '2026-04-20 14:00 ~ 18:00 (4小时)',
|
||||
location: '北京市海淀区中关村大街1号',
|
||||
nurseName: '', nurseExp: '', nurseRating: '',
|
||||
remark: '临时有事取消了',
|
||||
totalAmount: 240, reviewed: false,
|
||||
},
|
||||
{
|
||||
id: 8, orderNo: 'MC20260415008', status: 'completed', statusText: '已完成',
|
||||
createTime: '2026-04-13 10:20', serviceType: '住院陪护',
|
||||
serviceTime: '2026-04-15 08:00 ~ 2026-04-18 08:00 (3天)',
|
||||
location: '中国人民解放军总医院(301医院) 住院部东楼608',
|
||||
nurseName: '赵文斌', nurseExp: 12, nurseRating: 4.8,
|
||||
nursePhone: '17799887766', nursePhoneHidden: '177****7766', showPhone: false,
|
||||
remark: '心脏手术后康复护理,监测生命体征',
|
||||
totalAmount: 2880, reviewed: true,
|
||||
},
|
||||
{
|
||||
id: 9, orderNo: 'MC20260410009', status: 'completed', statusText: '已完成',
|
||||
createTime: '2026-04-08 14:00', serviceType: '门诊陪护',
|
||||
serviceTime: '2026-04-10 09:00 ~ 11:30 (2.5小时)',
|
||||
location: '北京安贞医院 心脏中心',
|
||||
nurseName: '刘晓红', nurseExp: 3, nurseRating: 4.6,
|
||||
remark: '陪同复查心电图和超声心动图',
|
||||
totalAmount: 138, reviewed: true,
|
||||
},
|
||||
{
|
||||
id: 10, orderNo: 'MC20260405010', status: 'completed', statusText: '已完成',
|
||||
createTime: '2026-04-02 16:45', serviceType: '居家陪护',
|
||||
serviceTime: '2026-04-05 09:00 ~ 18:00 (连续5天)',
|
||||
location: '北京市丰台区方庄南路18号院',
|
||||
nurseName: '周明辉', nurseExp: 7, nurseRating: 4.7,
|
||||
remark: '脑梗塞后遗症患者居家康复训练和生活照料',
|
||||
totalAmount: 2250, reviewed: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredOrders() {
|
||||
let list = this.orderList;
|
||||
if (this.activeTab !== 'all') {
|
||||
list = list.filter(o => o.status === this.activeTab);
|
||||
}
|
||||
if (this.searchKeyword) {
|
||||
const kw = this.searchKeyword.toLowerCase();
|
||||
list = list.filter(o =>
|
||||
o.orderNo.toLowerCase().includes(kw) ||
|
||||
o.serviceType.includes(kw) ||
|
||||
o.location.includes(kw) ||
|
||||
(o.nurseName && o.nurseName.includes(kw))
|
||||
);
|
||||
}
|
||||
return list;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onSearch() { this.showSearch = true; },
|
||||
doSearch() { this.showSearch = false; vant.showToast({ message: `搜索: ${this.searchKeyword}`, position: 'top' }); },
|
||||
switchTab(tab) { this.activeTab = tab; },
|
||||
onRefresh() { setTimeout(() => { this.refreshing = false; vant.showToast('刷新成功'); }, 1000); },
|
||||
onLoad() { this.loading = false; this.finished = true; },
|
||||
togglePhone(order) { order.showPhone = !order.showPhone; },
|
||||
viewOrder(order) { vant.showDialog({ title: '订单详情', message: `订单号: ${order.orderNo}\n服务: ${order.serviceType}\n时间: ${order.serviceTime}\n金额: ¥${order.totalAmount}\n状态: ${order.statusText}` }); },
|
||||
cancelOrder(order) {
|
||||
vant.showConfirmDialog({ title: '确认取消', message: '确定要取消该订单吗?取消后需重新下单。' }).then(() => {
|
||||
order.status = 'cancelled';
|
||||
order.statusText = '已取消';
|
||||
this.stats.pending--;
|
||||
vant.showToast({ message: '订单已取消', icon: 'success' });
|
||||
}).catch(() => {});
|
||||
},
|
||||
contactNurse(order) {
|
||||
vant.showDialog({ title: '联系护工', message: `正在呼叫 ${order.nurseName}...\n电话: 138****8888` });
|
||||
},
|
||||
extendOrder(order) {
|
||||
vant.showConfirmDialog({ title: '延长服务', message: '确定要延长当前服务时长吗?护工将收到延长时间通知。' }).then(() => {
|
||||
vant.showToast({ message: '已发送延长请求', icon: 'success' });
|
||||
}).catch(() => {});
|
||||
},
|
||||
reviewOrder(order) {
|
||||
vant.showDialog({ title: '服务评价', message: '评价功能开发中...' });
|
||||
},
|
||||
viewReview(order) {
|
||||
vant.showToast('查看评价详情');
|
||||
},
|
||||
reorder(order) {
|
||||
vant.showToast({ message: '正在跳转到下单页面...', icon: 'success' });
|
||||
},
|
||||
goPublish() {
|
||||
vant.showToast('跳转到发布订单页');
|
||||
},
|
||||
serviceIcon(type) {
|
||||
const map = { '住院陪护': 'hotel-o', '门诊陪护': 'guide-o', '居家陪护': 'home-o', '母婴护理': 'like-o' };
|
||||
return map[type] || 'service-o';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeTab(val) { if (val !== 'orders') { onTabBarChange(val); } },
|
||||
},
|
||||
});
|
||||
app.use(vant);
|
||||
app.use(vant.Lazyload);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
{/block}
|
||||
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户端_智能陪护系统</title>
|
||||
<!-- 引入样式文件 -->
|
||||
<link rel="stylesheet" href="/Static/vant@4.css" />
|
||||
<!-- 解决加载闪烁 -->
|
||||
<style>
|
||||
[v-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
{block name="css"}{/block}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{block name="app"}{/block}
|
||||
</body>
|
||||
<!-- 引入 Vue 和 Vant 的 JS 文件 -->
|
||||
<script src="/Static/vue@3.js"></script>
|
||||
<script src="/Static/vant.min.js"></script>
|
||||
<script>
|
||||
{$GlobalJS|raw}
|
||||
</script>
|
||||
{block name="js"}{/block}
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
员工端
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
with open(r'd:\deskwork\L-GraduationThesis\MediCompanions\public\Static\vant.min.js', 'r', encoding='utf-8') as f:
|
||||
c = f.read()
|
||||
|
||||
# Ie("tabbar") returns [Mp, Fp, ?]
|
||||
# Mp is the component name string
|
||||
# Let's find where Mp is defined
|
||||
idx = c.find('Ie("tabbar")')
|
||||
# The destructuring is [Mp,Fp]=Ie("tabbar")
|
||||
# So Mp = "van-tabbar" (from Ie function: const t=`van-${e}`)
|
||||
print('Context around Ie("tabbar"):')
|
||||
print(repr(c[max(0,idx-30):idx+30]))
|
||||
|
||||
# Ie function creates: const t = `van-${e}`, so Mp = "van-tabbar"
|
||||
# Then name:Mp means the component name is "van-tabbar"
|
||||
# install registers: t.component("van-tabbar", component) and t.component(be("-van-tabbar"), component)
|
||||
# be("-van-tabbar") = converts to PascalCase = "VanTabbar" ?
|
||||
|
||||
# Let's verify be function more carefully
|
||||
idx2 = c.find('be=e=>e.replace(ge')
|
||||
if idx2 > 0:
|
||||
print('\nbe function context:')
|
||||
print(repr(c[max(0,idx2-100):idx2+100]))
|
||||
|
||||
# Find ge (the regex)
|
||||
idx3 = c.find('const ge=')
|
||||
if idx3 < 0:
|
||||
idx3 = c.find('ge=')
|
||||
if idx3 > 0:
|
||||
print('\nge definition:')
|
||||
# Be careful - ge might not be the right one due to minification
|
||||
print(repr(c[max(0,idx3-20):idx3+100]))
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "workerman/webman",
|
||||
"type": "project",
|
||||
"keywords": [
|
||||
"high performance",
|
||||
"http service"
|
||||
],
|
||||
"homepage": "https://www.workerman.net",
|
||||
"license": "MIT",
|
||||
"description": "High performance HTTP Service Framework.",
|
||||
"authors": [
|
||||
{
|
||||
"name": "walkor",
|
||||
"email": "walkor@workerman.net",
|
||||
"homepage": "https://www.workerman.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"email": "walkor@workerman.net",
|
||||
"issues": "https://github.com/walkor/webman/issues",
|
||||
"forum": "https://wenda.workerman.net/",
|
||||
"wiki": "https://workerman.net/doc/webman",
|
||||
"source": "https://github.com/walkor/webman"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"workerman/webman-framework": "^2.1",
|
||||
"monolog/monolog": "^2.0",
|
||||
"webman/console": "^2.2",
|
||||
"webman/validation": "^2.2",
|
||||
"webman/blade": "^1.5",
|
||||
"webman/admin": "~2.0",
|
||||
"topthink/think-template": "^3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-event": "For better performance. "
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"": "./",
|
||||
"app\\": "./app",
|
||||
"App\\": "./app",
|
||||
"app\\View\\Components\\": "./app/view/components"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-package-install": [
|
||||
"support\\Plugin::install"
|
||||
],
|
||||
"post-package-update": [
|
||||
"support\\Plugin::install"
|
||||
],
|
||||
"pre-package-uninstall": [
|
||||
"support\\Plugin::uninstall"
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"support\\Setup::run"
|
||||
],
|
||||
"setup-webman": [
|
||||
"support\\Setup::run"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
Generated
+5428
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use support\Request;
|
||||
|
||||
return [
|
||||
'debug' => true,
|
||||
'error_reporting' => E_ALL,
|
||||
'default_timezone' => 'Asia/Shanghai',
|
||||
'request_class' => Request::class,
|
||||
'public_path' => base_path() . DIRECTORY_SEPARATOR . 'public',
|
||||
'runtime_path' => base_path(false) . DIRECTORY_SEPARATOR . 'runtime',
|
||||
'controller_suffix' => 'Controller',
|
||||
'controller_reuse' => false,
|
||||
];
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
'files' => [
|
||||
base_path() . '/app/functions.php',
|
||||
base_path() . '/support/Request.php',
|
||||
base_path() . '/support/Response.php',
|
||||
]
|
||||
];
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
support\bootstrap\Session::class,
|
||||
];
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return new Webman\Container;
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
return [
|
||||
'default' => 'mysql',
|
||||
'connections' => [
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => '127.0.0.1',
|
||||
'port' => '3306',
|
||||
'database' => 'your_database',
|
||||
'username' => 'your_username',
|
||||
'password' => 'your_password',
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
'prefix' => '',
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => [
|
||||
PDO::ATTR_EMULATE_PREPARES => false, // Must be false for Swoole and Swow drivers.
|
||||
],
|
||||
'pool' => [
|
||||
'max_connections' => 5,
|
||||
'min_connections' => 1,
|
||||
'wait_timeout' => 3,
|
||||
'idle_timeout' => 60,
|
||||
'heartbeat_interval' => 50,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [];
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
];
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
'' => support\exception\Handler::class,
|
||||
];
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'handlers' => [
|
||||
[
|
||||
'class' => Monolog\Handler\RotatingFileHandler::class,
|
||||
'constructor' => [
|
||||
runtime_path() . '/logs/webman.log',
|
||||
7, //$maxFiles
|
||||
Monolog\Logger::DEBUG,
|
||||
],
|
||||
'formatter' => [
|
||||
'class' => Monolog\Formatter\LineFormatter::class,
|
||||
'constructor' => [null, 'Y-m-d H:i:s', true],
|
||||
],
|
||||
]
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [];
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
return [
|
||||
'enable' => true,
|
||||
|
||||
'build_dir' => BASE_PATH . DIRECTORY_SEPARATOR . 'build',
|
||||
|
||||
'phar_filename' => 'webman.phar',
|
||||
|
||||
'phar_format' => Phar::PHAR, // Phar archive format: Phar::PHAR, Phar::TAR, Phar::ZIP
|
||||
|
||||
'phar_compression' => Phar::NONE, // Compression method for Phar archive: Phar::NONE, Phar::GZ, Phar::BZ2
|
||||
|
||||
'bin_filename' => 'webman.bin',
|
||||
|
||||
'signature_algorithm'=> Phar::SHA256, //set the signature algorithm for a phar and apply it. The signature algorithm must be one of Phar::MD5, Phar::SHA1, Phar::SHA256, Phar::SHA512, or Phar::OPENSSL.
|
||||
|
||||
'private_key_file' => '', // The file path for certificate or OpenSSL private key file.
|
||||
|
||||
'exclude_pattern' => '#^(?!.*(composer.json|/.github/|/.idea/|/.git/|/.setting/|/runtime/|/vendor-bin/|/build/|/vendor/webman/admin/))(.*)$#',
|
||||
|
||||
'exclude_files' => [
|
||||
'.env', 'LICENSE', 'composer.json', 'composer.lock', 'start.php', 'webman.phar', 'webman.bin'
|
||||
],
|
||||
|
||||
'custom_ini' => '
|
||||
memory_limit = 256M
|
||||
',
|
||||
];
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
return [
|
||||
'enable' => true,
|
||||
];
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
Webman\Event\BootStrap::class,
|
||||
];
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Webman\Event\EventListCommand;
|
||||
|
||||
return [
|
||||
EventListCommand::class
|
||||
];
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use support\validation\ValidationException;
|
||||
|
||||
return [
|
||||
'enable' => true,
|
||||
'exception' => ValidationException::class,
|
||||
];
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Webman\Validation\Command\MakeValidatorCommand;
|
||||
|
||||
return [
|
||||
MakeValidatorCommand::class
|
||||
];
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
use Webman\Validation\Middleware;
|
||||
|
||||
return [
|
||||
'@' => [
|
||||
Middleware::class,
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use support\Log;
|
||||
use support\Request;
|
||||
use app\process\Http;
|
||||
|
||||
global $argv;
|
||||
|
||||
return [
|
||||
'webman' => [
|
||||
'handler' => Http::class,
|
||||
'listen' => 'http://0.0.0.0:8787',
|
||||
'count' => cpu_count() * 4,
|
||||
'user' => '',
|
||||
'group' => '',
|
||||
'reusePort' => false,
|
||||
'eventLoop' => '',
|
||||
'context' => [],
|
||||
'constructor' => [
|
||||
'requestClass' => Request::class,
|
||||
'logger' => Log::channel('default'),
|
||||
'appPath' => app_path(),
|
||||
'publicPath' => public_path()
|
||||
]
|
||||
],
|
||||
// File update detection and automatic reload
|
||||
'monitor' => [
|
||||
'handler' => app\process\Monitor::class,
|
||||
'reloadable' => false,
|
||||
'constructor' => [
|
||||
// Monitor these directories
|
||||
'monitorDir' => array_merge([
|
||||
app_path(),
|
||||
config_path(),
|
||||
base_path() . '/process',
|
||||
base_path() . '/support',
|
||||
base_path() . '/resource',
|
||||
base_path() . '/.env',
|
||||
], glob(base_path() . '/plugin/*/app'), glob(base_path() . '/plugin/*/config'), glob(base_path() . '/plugin/*/api')),
|
||||
// Files with these suffixes will be monitored
|
||||
'monitorExtensions' => [
|
||||
'php', 'html', 'htm', 'env'
|
||||
],
|
||||
'options' => [
|
||||
'enable_file_monitor' => !in_array('-d', $argv) && DIRECTORY_SEPARATOR === '/',
|
||||
'enable_memory_monitor' => DIRECTORY_SEPARATOR === '/',
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use Webman\Route;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
'event_loop' => '',
|
||||
'stop_timeout' => 2,
|
||||
'pid_file' => runtime_path() . '/webman.pid',
|
||||
'status_file' => runtime_path() . '/webman.status',
|
||||
'stdout_file' => runtime_path() . '/logs/stdout.log',
|
||||
'log_file' => runtime_path() . '/logs/workerman.log',
|
||||
'max_package_size' => 10 * 1024 * 1024
|
||||
];
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use Webman\Session\FileSessionHandler;
|
||||
use Webman\Session\RedisSessionHandler;
|
||||
use Webman\Session\RedisClusterSessionHandler;
|
||||
|
||||
return [
|
||||
|
||||
'type' => 'file', // or redis or redis_cluster
|
||||
|
||||
'handler' => FileSessionHandler::class,
|
||||
|
||||
'config' => [
|
||||
'file' => [
|
||||
'save_path' => runtime_path() . '/sessions',
|
||||
],
|
||||
'redis' => [
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 6379,
|
||||
'auth' => '',
|
||||
'timeout' => 2,
|
||||
'database' => '',
|
||||
'prefix' => 'redis_session_',
|
||||
],
|
||||
'redis_cluster' => [
|
||||
'host' => ['127.0.0.1:7000', '127.0.0.1:7001', '127.0.0.1:7001'],
|
||||
'timeout' => 2,
|
||||
'auth' => '',
|
||||
'prefix' => 'redis_session_',
|
||||
]
|
||||
],
|
||||
|
||||
'session_name' => 'PHPSID',
|
||||
|
||||
'auto_update_timestamp' => false,
|
||||
|
||||
'lifetime' => 7*24*60*60,
|
||||
|
||||
'cookie_lifetime' => 365*24*60*60,
|
||||
|
||||
'cookie_path' => '/',
|
||||
|
||||
'domain' => '',
|
||||
|
||||
'http_only' => true,
|
||||
|
||||
'secure' => false,
|
||||
|
||||
'same_site' => '',
|
||||
|
||||
'gc_probability' => [1, 1000],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
/**
|
||||
* Static file settings
|
||||
*/
|
||||
return [
|
||||
'enable' => true,
|
||||
'middleware' => [ // Static file Middleware
|
||||
//app\middleware\StaticFile::class,
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
/**
|
||||
* Multilingual configuration
|
||||
*/
|
||||
return [
|
||||
// Default language
|
||||
'locale' => 'zh_CN',
|
||||
// Fallback language
|
||||
'fallback_locale' => ['zh_CN', 'en'],
|
||||
// Folder where language files are stored
|
||||
'path' => base_path() . '/resource/translations',
|
||||
];
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use support\view\Raw;
|
||||
use support\view\Twig;
|
||||
use support\view\Blade;
|
||||
use support\view\ThinkPHP;
|
||||
|
||||
return [
|
||||
'handler' => ThinkPHP::class,
|
||||
'options' => [
|
||||
'view_suffix' => 'html',
|
||||
'tpl_begin' => '{',
|
||||
'tpl_end' => '}'
|
||||
]
|
||||
];
|
||||
@@ -0,0 +1,11 @@
|
||||
version: "3"
|
||||
services:
|
||||
webman:
|
||||
build: .
|
||||
container_name: docker-webman
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- "./:/app"
|
||||
ports:
|
||||
- "8787:8787"
|
||||
command: ["php", "start.php", "start" ]
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
namespace plugin\admin\api;
|
||||
|
||||
use plugin\admin\app\model\Role;
|
||||
use plugin\admin\app\model\Rule;
|
||||
use support\exception\BusinessException;
|
||||
use function admin;
|
||||
|
||||
/**
|
||||
* 对外提供的鉴权接口
|
||||
*/
|
||||
class Auth
|
||||
{
|
||||
/**
|
||||
* 判断权限
|
||||
* 如果没有权限则抛出异常
|
||||
* @param string $controller
|
||||
* @param string $action
|
||||
* @return void
|
||||
* @throws \ReflectionException|BusinessException
|
||||
*/
|
||||
public static function access(string $controller, string $action)
|
||||
{
|
||||
$code = 0;
|
||||
$msg = '';
|
||||
if (!static::canAccess($controller, $action, $code, $msg)) {
|
||||
throw new BusinessException($msg, $code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否有权限
|
||||
* @param string $controller
|
||||
* @param string $action
|
||||
* @param int $code
|
||||
* @param string $msg
|
||||
* @return bool
|
||||
* @throws \ReflectionException|BusinessException
|
||||
*/
|
||||
public static function canAccess(string $controller, string $action, int &$code = 0, string &$msg = ''): bool
|
||||
{
|
||||
// 无控制器信息说明是函数调用,函数不属于任何控制器,鉴权操作应该在函数内部完成。
|
||||
if (!$controller) {
|
||||
return true;
|
||||
}
|
||||
// 获取控制器鉴权信息
|
||||
$class = new \ReflectionClass($controller);
|
||||
$properties = $class->getDefaultProperties();
|
||||
$noNeedLogin = $properties['noNeedLogin'] ?? [];
|
||||
$noNeedAuth = $properties['noNeedAuth'] ?? [];
|
||||
|
||||
// 不需要登录
|
||||
if (in_array($action, $noNeedLogin)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取登录信息
|
||||
$admin = admin();
|
||||
if (!$admin) {
|
||||
$msg = '请登录';
|
||||
// 401是未登录固定的返回码
|
||||
$code = 401;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 不需要鉴权
|
||||
if (in_array($action, $noNeedAuth)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 当前管理员无角色
|
||||
$roles = $admin['roles'];
|
||||
if (!$roles) {
|
||||
$msg = '无权限';
|
||||
$code = 2;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 角色没有规则
|
||||
$rules = Role::whereIn('id', $roles)->pluck('rules');
|
||||
$rule_ids = [];
|
||||
foreach ($rules as $rule_string) {
|
||||
if (!$rule_string) {
|
||||
continue;
|
||||
}
|
||||
$rule_ids = array_merge($rule_ids, explode(',', $rule_string));
|
||||
}
|
||||
if (!$rule_ids) {
|
||||
$msg = '无权限';
|
||||
$code = 2;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 超级管理员
|
||||
if (in_array('*', $rule_ids)){
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果action为index,规则里有任意一个以$controller开头的权限即可
|
||||
if (strtolower($action) === 'index') {
|
||||
$rule = Rule::where(function ($query) use ($controller, $action) {
|
||||
$controller_like = str_replace('\\', '\\\\', $controller);
|
||||
$query->where('key', 'like', "$controller_like@%")->orWhere('key', $controller);
|
||||
})->whereIn('id', $rule_ids)->first();
|
||||
if ($rule) {
|
||||
return true;
|
||||
}
|
||||
$msg = '无权限';
|
||||
$code = 2;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 查询是否有当前控制器的规则
|
||||
$rule = Rule::where(function ($query) use ($controller, $action) {
|
||||
$query->where('key', "$controller@$action")->orWhere('key', $controller);
|
||||
})->whereIn('id', $rule_ids)->first();
|
||||
|
||||
if (!$rule) {
|
||||
$msg = '无权限';
|
||||
$code = 2;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\api;
|
||||
|
||||
class Install
|
||||
{
|
||||
/**
|
||||
* 安装
|
||||
*
|
||||
* @param $version
|
||||
* @return void
|
||||
*/
|
||||
public static function install($version)
|
||||
{
|
||||
// 导入菜单
|
||||
Menu::import(static::getMenus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载
|
||||
*
|
||||
* @param $version
|
||||
* @return void
|
||||
*/
|
||||
public static function uninstall($version)
|
||||
{
|
||||
// 删除菜单
|
||||
foreach (static::getMenus() as $menu) {
|
||||
Menu::delete($menu['name']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
*
|
||||
* @param $from_version
|
||||
* @param $to_version
|
||||
* @param $context
|
||||
* @return void
|
||||
*/
|
||||
public static function update($from_version, $to_version, $context = null)
|
||||
{
|
||||
// 删除不用的菜单
|
||||
if (isset($context['previous_menus'])) {
|
||||
static::removeUnnecessaryMenus($context['previous_menus']);
|
||||
}
|
||||
// 导入新菜单
|
||||
Menu::import(static::getMenus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新前数据收集等
|
||||
*
|
||||
* @param $from_version
|
||||
* @param $to_version
|
||||
* @return array|array[]
|
||||
*/
|
||||
public static function beforeUpdate($from_version, $to_version)
|
||||
{
|
||||
// 在更新之前获得老菜单,通过context传递给 update
|
||||
return ['previous_menus' => static::getMenus()];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单
|
||||
*
|
||||
* @return array|mixed
|
||||
*/
|
||||
public static function getMenus()
|
||||
{
|
||||
clearstatcache();
|
||||
if (is_file($menu_file = __DIR__ . '/../config/menu.php')) {
|
||||
$menus = include $menu_file;
|
||||
return $menus ?: [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除不需要的菜单
|
||||
*
|
||||
* @param $previous_menus
|
||||
* @return void
|
||||
*/
|
||||
public static function removeUnnecessaryMenus($previous_menus)
|
||||
{
|
||||
$menus_to_remove = array_diff(Menu::column($previous_menus, 'name'), Menu::column(static::getMenus(), 'name'));
|
||||
foreach ($menus_to_remove as $name) {
|
||||
Menu::delete($name);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
namespace plugin\admin\api;
|
||||
|
||||
use plugin\admin\app\model\Rule;
|
||||
|
||||
/**
|
||||
* 对外提供的菜单接口
|
||||
*/
|
||||
class Menu
|
||||
{
|
||||
|
||||
/**
|
||||
* 根据key获取菜单
|
||||
* @param $key
|
||||
* @return array
|
||||
*/
|
||||
public static function get($key)
|
||||
{
|
||||
$menu = Rule::where('key', $key)->first();
|
||||
return $menu ? $menu->toArray() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id获得菜单
|
||||
* @param $id
|
||||
* @return array
|
||||
*/
|
||||
public static function find($id): array
|
||||
{
|
||||
return Rule::find($id)->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加菜单
|
||||
* @param array $menu
|
||||
* @return int
|
||||
*/
|
||||
public static function add(array $menu)
|
||||
{
|
||||
$item = new Rule;
|
||||
foreach ($menu as $key => $value) {
|
||||
$item->$key = $value;
|
||||
}
|
||||
$item->save();
|
||||
return $item->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入菜单
|
||||
* @param array $menu_tree
|
||||
* @return void
|
||||
*/
|
||||
public static function import(array $menu_tree)
|
||||
{
|
||||
if (is_numeric(key($menu_tree)) && !isset($menu_tree['key'])) {
|
||||
foreach ($menu_tree as $item) {
|
||||
static::import($item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
$children = $menu_tree['children'] ?? [];
|
||||
unset($menu_tree['children']);
|
||||
if ($old_menu = Menu::get($menu_tree['key'])) {
|
||||
$pid = $old_menu['id'];
|
||||
Rule::where('key', $menu_tree['key'])->update($menu_tree);
|
||||
} else {
|
||||
$pid = static::add($menu_tree);
|
||||
}
|
||||
foreach ($children as $menu) {
|
||||
$menu['pid'] = $pid;
|
||||
static::import($menu);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除菜单
|
||||
* @param $key
|
||||
* @return void
|
||||
*/
|
||||
public static function delete($key)
|
||||
{
|
||||
$item = Rule::where('key', $key)->first();
|
||||
if (!$item) {
|
||||
return;
|
||||
}
|
||||
// 子规则一起删除
|
||||
$delete_ids = $children_ids = [$item['id']];
|
||||
while($children_ids) {
|
||||
$children_ids = Rule::whereIn('pid', $children_ids)->pluck('id')->toArray();
|
||||
$delete_ids = array_merge($delete_ids, $children_ids);
|
||||
}
|
||||
Rule::whereIn('id', $delete_ids)->delete();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取菜单中某个(些)字段的值
|
||||
* @param $menu
|
||||
* @param null $column
|
||||
* @param null $index
|
||||
* @return array|mixed
|
||||
*/
|
||||
public static function column($menu, $column = null, $index = null)
|
||||
{
|
||||
$values = [];
|
||||
if (is_numeric(key($menu)) && !isset($menu['key'])) {
|
||||
foreach ($menu as $item) {
|
||||
$values = array_merge($values, static::column($item, $column, $index));
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
$children = $menu['children'] ?? [];
|
||||
unset($menu['children']);
|
||||
if ($column === null) {
|
||||
if ($index) {
|
||||
$values[$menu[$index]] = $menu;
|
||||
} else {
|
||||
$values[] = $menu;
|
||||
}
|
||||
} else {
|
||||
if (is_array($column)) {
|
||||
$item = [];
|
||||
foreach ($column as $f) {
|
||||
$item[$f] = $menu[$f] ?? null;
|
||||
}
|
||||
if ($index) {
|
||||
$values[$menu[$index]] = $item;
|
||||
} else {
|
||||
$values[] = $item;
|
||||
}
|
||||
} else {
|
||||
$value = $menu[$column] ?? null;
|
||||
if ($index) {
|
||||
$values[$menu[$index]] = $value;
|
||||
} else {
|
||||
$values[] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($children as $child) {
|
||||
$values = array_merge($values, static::column($child, $column, $index));
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
namespace plugin\admin\api;
|
||||
|
||||
use ReflectionException;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use Webman\MiddlewareInterface;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
/**
|
||||
* 对外提供的鉴权中间件
|
||||
*/
|
||||
class Middleware implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* 鉴权
|
||||
* @param Request $request
|
||||
* @param callable $handler
|
||||
* @return Response
|
||||
* @throws ReflectionException
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$controller = $request->controller;
|
||||
$action = $request->action;
|
||||
|
||||
$code = 0;
|
||||
$msg = '';
|
||||
if (!Auth::canAccess($controller, $action, $code, $msg)) {
|
||||
if ($request->expectsJson()) {
|
||||
$response = json(['code' => $code, 'msg' => $msg, 'type' => 'error']);
|
||||
} else {
|
||||
if ($code === 401) {
|
||||
$response = admin_error_401_script();
|
||||
} else {
|
||||
$request->app = '';
|
||||
$request->plugin = 'admin';
|
||||
$response = view('common/error/403')->withStatus(403);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$response = $request->method() == 'OPTIONS' ? response('') : $handler($request);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
namespace plugin\admin\app\common;
|
||||
|
||||
|
||||
use plugin\admin\app\model\AdminRole;
|
||||
use plugin\admin\app\model\Role;
|
||||
|
||||
class Auth
|
||||
{
|
||||
/**
|
||||
* 获取权限范围内的所有角色id
|
||||
* @param bool $with_self
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function getScopeRoleIds(bool $with_self = false): array
|
||||
{
|
||||
if (!$admin = admin()) {
|
||||
return [];
|
||||
}
|
||||
$role_ids = $admin['roles'];
|
||||
$rules = Role::whereIn('id', $role_ids)->pluck('rules')->toArray();
|
||||
if ($rules && in_array('*', $rules)) {
|
||||
return Role::pluck('id')->toArray();
|
||||
}
|
||||
|
||||
$roles = Role::get();
|
||||
$tree = new Tree($roles);
|
||||
$descendants = $tree->getDescendant($role_ids, $with_self);
|
||||
return array_column($descendants, 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限范围内的所有管理员id
|
||||
* @param bool $with_self
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function getScopeAdminIds(bool $with_self = false): array
|
||||
{
|
||||
$role_ids = static::getScopeRoleIds();
|
||||
$admin_ids = AdminRole::whereIn('role_id', $role_ids)->pluck('admin_id')->toArray();
|
||||
if ($with_self) {
|
||||
$admin_ids[] = admin_id();
|
||||
}
|
||||
return array_unique($admin_ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧版本
|
||||
* @param int $admin_id
|
||||
* @deprecated
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSupperAdmin(int $admin_id = 0): bool
|
||||
{
|
||||
return static::isSuperAdmin($admin_id);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是超级管理员
|
||||
* @param int $admin_id
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function isSuperAdmin(int $admin_id = 0): bool
|
||||
{
|
||||
if (!$admin_id) {
|
||||
if (!$roles = admin('roles')) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$roles = AdminRole::where('admin_id', $admin_id)->pluck('role_id');
|
||||
}
|
||||
$rules = Role::whereIn('id', $roles)->pluck('rules');
|
||||
return $rules && in_array('*', $rules->toArray());
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
namespace plugin\admin\app\common;
|
||||
|
||||
|
||||
class Tree
|
||||
{
|
||||
|
||||
/**
|
||||
* 获取完整的树结构,包含祖先节点
|
||||
*/
|
||||
const INCLUDE_ANCESTORS = 1;
|
||||
|
||||
/**
|
||||
* 获取部分树,不包含祖先节点
|
||||
*/
|
||||
const EXCLUDE_ANCESTORS = 0;
|
||||
|
||||
/**
|
||||
* 数据
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* 哈希树
|
||||
* @var array
|
||||
*/
|
||||
protected $hashTree = [];
|
||||
|
||||
/**
|
||||
* 父级字段名
|
||||
* @var string
|
||||
*/
|
||||
protected $pidName = 'pid';
|
||||
|
||||
/**
|
||||
* @param $data
|
||||
* @param string $pid_name
|
||||
*/
|
||||
public function __construct($data, string $pid_name = 'pid')
|
||||
{
|
||||
$this->pidName = $pid_name;
|
||||
if (is_object($data) && method_exists($data, 'toArray')) {
|
||||
$this->data = $data->toArray();
|
||||
} else {
|
||||
$this->data = (array)$data;
|
||||
$this->data = array_map(function ($item) {
|
||||
if (is_object($item) && method_exists($item, 'toArray')) {
|
||||
return $item->toArray();
|
||||
}
|
||||
return $item;
|
||||
}, $this->data);
|
||||
}
|
||||
$this->hashTree = $this->getHashTree();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子孙节点
|
||||
* @param array $include
|
||||
* @param bool $with_self
|
||||
* @return array
|
||||
*/
|
||||
public function getDescendant(array $include, bool $with_self = false): array
|
||||
{
|
||||
$items = [];
|
||||
foreach ($include as $id) {
|
||||
if (!isset($this->hashTree[$id])) {
|
||||
return [];
|
||||
}
|
||||
if ($with_self) {
|
||||
$item = $this->hashTree[$id];
|
||||
unset($item['children']);
|
||||
$items[$item['id']] = $item;
|
||||
}
|
||||
foreach ($this->hashTree[$id]['children'] ?? [] as $item) {
|
||||
unset($item['children']);
|
||||
$items[$item['id']] = $item;
|
||||
foreach ($this->getDescendant([$item['id']]) as $it) {
|
||||
$items[$it['id']] = $it;
|
||||
}
|
||||
}
|
||||
}
|
||||
return array_values($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取哈希树
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function getHashTree(array $data = []): array
|
||||
{
|
||||
$data = $data ?: $this->data;
|
||||
$hash_tree = [];
|
||||
foreach ($data as $item) {
|
||||
$hash_tree[$item['id']] = $item;
|
||||
}
|
||||
foreach ($hash_tree as $index => $item) {
|
||||
if ($item[$this->pidName] && isset($hash_tree[$item[$this->pidName]])) {
|
||||
$hash_tree[$item[$this->pidName]]['children'][$hash_tree[$index]['id']] = &$hash_tree[$index];
|
||||
}
|
||||
}
|
||||
return $hash_tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取树
|
||||
* @param array $include
|
||||
* @param int $type
|
||||
* @return array|null
|
||||
*/
|
||||
public function getTree(array $include = [], int $type = 1): ?array
|
||||
{
|
||||
// $type === static::EXCLUDE_ANCESTORS
|
||||
if ($type === static::EXCLUDE_ANCESTORS) {
|
||||
$items = [];
|
||||
$include = array_unique($include);
|
||||
foreach ($include as $id) {
|
||||
if (!isset($this->hashTree[$id])) {
|
||||
return [];
|
||||
}
|
||||
$items[] = $this->hashTree[$id];
|
||||
}
|
||||
return static::arrayValues($items);
|
||||
}
|
||||
|
||||
// $type === static::INCLUDE_ANCESTORS
|
||||
$hash_tree = $this->hashTree;
|
||||
$items = [];
|
||||
if ($include) {
|
||||
$map = [];
|
||||
foreach ($include as $id) {
|
||||
if (!isset($hash_tree[$id])) {
|
||||
continue;
|
||||
}
|
||||
$item = $hash_tree[$id];
|
||||
$max_depth = 100;
|
||||
while ($max_depth-- > 0 && $item[$this->pidName] && isset($hash_tree[$item[$this->pidName]])) {
|
||||
$last_item = $item;
|
||||
$pid = $item[$this->pidName];
|
||||
$item = $hash_tree[$pid];
|
||||
$item_id = $item['id'];
|
||||
if (empty($map[$item_id])) {
|
||||
$map[$item_id] = 1;
|
||||
$hash_tree[$pid]['children'] = [];
|
||||
}
|
||||
$hash_tree[$pid]['children'][$last_item['id']] = $last_item;
|
||||
$item = $hash_tree[$pid];
|
||||
}
|
||||
$items[$item['id']] = $item;
|
||||
}
|
||||
} else {
|
||||
$items = $hash_tree;
|
||||
}
|
||||
$formatted_items = [];
|
||||
foreach ($items as $item) {
|
||||
if (!$item[$this->pidName] || !isset($hash_tree[$item[$this->pidName]])) {
|
||||
$formatted_items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return static::arrayValues($formatted_items);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归重建数组下标
|
||||
* @param $array
|
||||
* @return array
|
||||
*/
|
||||
public static function arrayValues($array): array
|
||||
{
|
||||
if (!$array) {
|
||||
return [];
|
||||
}
|
||||
if (!isset($array['children'])) {
|
||||
$current = current($array);
|
||||
if (!is_array($current)) {
|
||||
return $array;
|
||||
}
|
||||
$tree = array_values($array);
|
||||
foreach ($tree as $index => $item) {
|
||||
$tree[$index] = static::arrayValues($item);
|
||||
}
|
||||
return $tree;
|
||||
}
|
||||
$array['children'] = array_values($array['children']);
|
||||
foreach ($array['children'] as $index => $child) {
|
||||
$array['children'][$index] = static::arrayValues($child);
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\common;
|
||||
|
||||
use app\process\Monitor;
|
||||
use Throwable;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
use plugin\admin\app\model\Option;
|
||||
use support\exception\BusinessException;
|
||||
use support\Db;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Worker;
|
||||
|
||||
class Util
|
||||
{
|
||||
/**
|
||||
* 密码哈希
|
||||
* @param $password
|
||||
* @param string $algo
|
||||
* @return false|string|null
|
||||
*/
|
||||
public static function passwordHash($password, string $algo = PASSWORD_DEFAULT)
|
||||
{
|
||||
return password_hash($password, $algo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码哈希
|
||||
* @param string $password
|
||||
* @param string $hash
|
||||
* @return bool
|
||||
*/
|
||||
public static function passwordVerify(string $password, string $hash): bool
|
||||
{
|
||||
return password_verify($password, $hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取webman-admin数据库连接
|
||||
* @return Connection
|
||||
*/
|
||||
public static function db(): Connection
|
||||
{
|
||||
return Db::connection('plugin.admin.mysql');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SchemaBuilder
|
||||
* @return Builder
|
||||
*/
|
||||
public static function schema(): Builder
|
||||
{
|
||||
return Db::schema('plugin.admin.mysql');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取语义化时间
|
||||
* @param $time
|
||||
* @return false|string
|
||||
*/
|
||||
public static function humanDate($time)
|
||||
{
|
||||
$timestamp = is_numeric($time) ? $time : strtotime($time);
|
||||
$dur = time() - $timestamp;
|
||||
if ($dur < 0) {
|
||||
return date('Y-m-d', $timestamp);
|
||||
} else {
|
||||
if ($dur < 60) {
|
||||
return $dur . '秒前';
|
||||
} else {
|
||||
if ($dur < 3600) {
|
||||
return floor($dur / 60) . '分钟前';
|
||||
} else {
|
||||
if ($dur < 86400) {
|
||||
return floor($dur / 3600) . '小时前';
|
||||
} else {
|
||||
if ($dur < 2592000) { // 30天内
|
||||
return floor($dur / 86400) . '天前';
|
||||
} else {
|
||||
return date('Y-m-d', $timestamp);;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return date('Y-m-d', $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* @param $file_size
|
||||
* @return string
|
||||
*/
|
||||
public static function formatBytes($file_size): string
|
||||
{
|
||||
$size = sprintf("%u", $file_size);
|
||||
if($size == 0) {
|
||||
return("0 Bytes");
|
||||
}
|
||||
$size_name = array(" Bytes", " KB", " MB", " GB", " TB", " PB", " EB", " ZB", " YB");
|
||||
return round($size/pow(1024, ($i = floor(log($size, 1024)))), 2) . $size_name[$i];
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库字符串转义
|
||||
* @param $var
|
||||
* @return false|string
|
||||
*/
|
||||
public static function pdoQuote($var)
|
||||
{
|
||||
return Util::db()->getPdo()->quote($var);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查表名是否合法
|
||||
* @param string $table
|
||||
* @return string
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function checkTableName(string $table): string
|
||||
{
|
||||
if (!preg_match('/^[a-zA-Z_0-9]+$/', $table)) {
|
||||
throw new BusinessException('表名不合法');
|
||||
}
|
||||
return $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* 变量或数组中的元素只能是字母数字下划线组合
|
||||
* @param $var
|
||||
* @return mixed
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function filterAlphaNum($var)
|
||||
{
|
||||
$vars = (array)$var;
|
||||
array_walk_recursive($vars, function ($item) {
|
||||
if (is_string($item) && !preg_match('/^[a-zA-Z_0-9]+$/', $item)) {
|
||||
throw new BusinessException('参数不合法');
|
||||
}
|
||||
});
|
||||
return $var;
|
||||
}
|
||||
|
||||
/**
|
||||
* 变量或数组中的元素只能是字母数字
|
||||
* @param $var
|
||||
* @return mixed
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function filterNum($var)
|
||||
{
|
||||
$vars = (array)$var;
|
||||
array_walk_recursive($vars, function ($item) {
|
||||
if (is_string($item) && !preg_match('/^[0-9]+$/', $item)) {
|
||||
throw new BusinessException('参数不合法');
|
||||
}
|
||||
});
|
||||
return $var;
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc 检测是否是合法URL Path
|
||||
* @param $var
|
||||
* @return string
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function filterUrlPath($var): string
|
||||
{
|
||||
if (!is_string($var)) {
|
||||
throw new BusinessException('参数不合法,地址必须是一个字符串!');
|
||||
}
|
||||
|
||||
if (strpos($var, 'https://') === 0 || strpos($var, 'http://') === 0) {
|
||||
if (!filter_var($var, FILTER_VALIDATE_URL)) {
|
||||
throw new BusinessException('参数不合法,不是合法的URL地址!');
|
||||
}
|
||||
} elseif (!preg_match('/^[a-zA-Z0-9_\-\/&?.]+$/', $var)) {
|
||||
throw new BusinessException('参数不合法,不是合法的Path!');
|
||||
}
|
||||
return $var;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否是合法Path
|
||||
* @param $var
|
||||
* @return string
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function filterPath($var): string
|
||||
{
|
||||
if (!is_string($var) || !preg_match('/^[a-zA-Z0-9_\-\/]+$/', $var)) {
|
||||
throw new BusinessException('参数不合法');
|
||||
}
|
||||
return $var;
|
||||
}
|
||||
|
||||
/**
|
||||
* 类转换为url path
|
||||
* @param $controller_class
|
||||
* @return false|string
|
||||
*/
|
||||
static function controllerToUrlPath($controller_class)
|
||||
{
|
||||
$key = strtolower($controller_class);
|
||||
$action = '';
|
||||
if (strpos($key, '@')) {
|
||||
[$key, $action] = explode( '@', $key, 2);
|
||||
}
|
||||
$prefix = 'plugin';
|
||||
$paths = explode('\\', $key);
|
||||
if (count($paths) < 2) {
|
||||
return false;
|
||||
}
|
||||
$base = '';
|
||||
if (strpos($key, "$prefix\\") === 0) {
|
||||
if (count($paths) < 4) {
|
||||
return false;
|
||||
}
|
||||
array_shift($paths);
|
||||
$plugin = array_shift($paths);
|
||||
$base = "/app/$plugin/";
|
||||
}
|
||||
array_shift($paths);
|
||||
foreach ($paths as $index => $path) {
|
||||
if ($path === 'controller') {
|
||||
unset($paths[$index]);
|
||||
}
|
||||
}
|
||||
$suffix = 'controller';
|
||||
$code = $base . implode('/', $paths);
|
||||
if (substr($code, -strlen($suffix)) === $suffix) {
|
||||
$code = substr($code, 0, -strlen($suffix));
|
||||
}
|
||||
return $action ? "$code/$action" : $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为驼峰
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
public static function camel(string $value): string
|
||||
{
|
||||
static $cache = [];
|
||||
$key = $value;
|
||||
|
||||
if (isset($cache[$key])) {
|
||||
return $cache[$key];
|
||||
}
|
||||
|
||||
$value = ucwords(str_replace(['-', '_'], ' ', $value));
|
||||
|
||||
return $cache[$key] = str_replace(' ', '', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为小驼峰
|
||||
* @param $value
|
||||
* @return string
|
||||
*/
|
||||
public static function smCamel($value): string
|
||||
{
|
||||
return lcfirst(static::camel($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注释中第一行
|
||||
* @param $comment
|
||||
* @return false|mixed|string
|
||||
*/
|
||||
public static function getCommentFirstLine($comment)
|
||||
{
|
||||
if ($comment === false) {
|
||||
return false;
|
||||
}
|
||||
foreach (explode("\n", $comment) as $str) {
|
||||
if ($s = trim($str, "*/\ \t\n\r\0\x0B")) {
|
||||
return $s;
|
||||
}
|
||||
}
|
||||
return $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单类型到插件的映射
|
||||
* @return \string[][]
|
||||
*/
|
||||
public static function methodControlMap(): array
|
||||
{
|
||||
return [
|
||||
//method=>[控件]
|
||||
'integer' => ['InputNumber'],
|
||||
'string' => ['Input'],
|
||||
'text' => ['TextArea'],
|
||||
'date' => ['DatePicker'],
|
||||
'enum' => ['Select'],
|
||||
'float' => ['Input'],
|
||||
|
||||
'tinyInteger' => ['InputNumber'],
|
||||
'smallInteger' => ['InputNumber'],
|
||||
'mediumInteger' => ['InputNumber'],
|
||||
'bigInteger' => ['InputNumber'],
|
||||
|
||||
'unsignedInteger' => ['InputNumber'],
|
||||
'unsignedTinyInteger' => ['InputNumber'],
|
||||
'unsignedSmallInteger' => ['InputNumber'],
|
||||
'unsignedMediumInteger' => ['InputNumber'],
|
||||
'unsignedBigInteger' => ['InputNumber'],
|
||||
|
||||
'decimal' => ['Input'],
|
||||
'double' => ['Input'],
|
||||
|
||||
'mediumText' => ['TextArea'],
|
||||
'longText' => ['TextArea'],
|
||||
|
||||
'dateTime' => ['DateTimePicker'],
|
||||
|
||||
'time' => ['DateTimePicker'],
|
||||
'timestamp' => ['DateTimePicker'],
|
||||
|
||||
'char' => ['Input'],
|
||||
|
||||
'binary' => ['Input'],
|
||||
|
||||
'json' => ['input']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库类型到插件的转换
|
||||
* @param $type
|
||||
* @return string
|
||||
*/
|
||||
public static function typeToControl($type): string
|
||||
{
|
||||
if (stripos($type, 'int') !== false) {
|
||||
return 'inputNumber';
|
||||
}
|
||||
if (stripos($type, 'time') !== false || stripos($type, 'date') !== false) {
|
||||
return 'dateTimePicker';
|
||||
}
|
||||
if (stripos($type, 'text') !== false) {
|
||||
return 'textArea';
|
||||
}
|
||||
if ($type === 'enum') {
|
||||
return 'select';
|
||||
}
|
||||
return 'input';
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库类型到表单类型的转换
|
||||
* @param $type
|
||||
* @param $unsigned
|
||||
* @return string
|
||||
*/
|
||||
public static function typeToMethod($type, $unsigned = false)
|
||||
{
|
||||
if (stripos($type, 'int') !== false) {
|
||||
$type = str_replace('int', 'Integer', $type);
|
||||
return $unsigned ? "unsigned" . ucfirst($type) : lcfirst($type);
|
||||
}
|
||||
$map = [
|
||||
'int' => 'integer',
|
||||
'varchar' => 'string',
|
||||
'mediumtext' => 'mediumText',
|
||||
'longtext' => 'longText',
|
||||
'datetime' => 'dateTime',
|
||||
];
|
||||
return $map[$type] ?? $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按表获取摘要
|
||||
* @param $table
|
||||
* @param null $section
|
||||
* @return array|mixed
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function getSchema($table, $section = null)
|
||||
{
|
||||
Util::checkTableName($table);
|
||||
$database = config('database.connections')['plugin.admin.mysql']['database'];
|
||||
$schema_raw = $section !== 'table' ? Util::db()->select("select * from information_schema.COLUMNS where TABLE_SCHEMA = '$database' and table_name = '$table' order by ORDINAL_POSITION") : [];
|
||||
$forms = [];
|
||||
$columns = [];
|
||||
foreach ($schema_raw as $item) {
|
||||
$field = $item->COLUMN_NAME;
|
||||
$columns[$field] = [
|
||||
'field' => $field,
|
||||
'type' => Util::typeToMethod($item->DATA_TYPE, (bool)strpos($item->COLUMN_TYPE, 'unsigned')),
|
||||
'comment' => $item->COLUMN_COMMENT,
|
||||
'default' => $item->COLUMN_DEFAULT,
|
||||
'length' => static::getLengthValue($item),
|
||||
'nullable' => $item->IS_NULLABLE !== 'NO',
|
||||
'primary_key' => $item->COLUMN_KEY === 'PRI',
|
||||
'auto_increment' => strpos($item->EXTRA, 'auto_increment') !== false
|
||||
];
|
||||
|
||||
$forms[$field] = [
|
||||
'field' => $field,
|
||||
'comment' => $item->COLUMN_COMMENT,
|
||||
'control' => static::typeToControl($item->DATA_TYPE),
|
||||
'form_show' => $item->COLUMN_KEY !== 'PRI',
|
||||
'list_show' => true,
|
||||
'enable_sort' => false,
|
||||
'searchable' => false,
|
||||
'search_type' => 'normal',
|
||||
'control_args' => '',
|
||||
];
|
||||
}
|
||||
$table_schema = $section == 'table' || !$section ? Util::db()->select("SELECT TABLE_COMMENT FROM information_schema.`TABLES` WHERE TABLE_SCHEMA='$database' and TABLE_NAME='$table'") : [];
|
||||
$indexes = !$section || in_array($section, ['keys', 'table']) ? Util::db()->select("SHOW INDEX FROM `$table`") : [];
|
||||
$keys = [];
|
||||
$primary_key = [];
|
||||
foreach ($indexes as $index) {
|
||||
$key_name = $index->Key_name;
|
||||
if ($key_name == 'PRIMARY') {
|
||||
$primary_key[] = $index->Column_name;
|
||||
continue;
|
||||
}
|
||||
if (!isset($keys[$key_name])) {
|
||||
$keys[$key_name] = [
|
||||
'name' => $key_name,
|
||||
'columns' => [],
|
||||
'type' => $index->Non_unique == 0 ? 'unique' : 'normal'
|
||||
];
|
||||
}
|
||||
$keys[$key_name]['columns'][] = $index->Column_name;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'table' => ['name' => $table, 'comment' => $table_schema[0]->TABLE_COMMENT ?? '', 'primary_key' => $primary_key],
|
||||
'columns' => $columns,
|
||||
'forms' => $forms,
|
||||
'keys' => array_reverse($keys, true)
|
||||
];
|
||||
|
||||
$schema = Option::where('name', "table_form_schema_$table")->value('value');
|
||||
$form_schema_map = $schema ? json_decode($schema, true) : [];
|
||||
|
||||
foreach ($data['forms'] as $field => $item) {
|
||||
if (isset($form_schema_map[$field])) {
|
||||
$data['forms'][$field] = $form_schema_map[$field];
|
||||
}
|
||||
}
|
||||
|
||||
return $section ? $data[$section] : $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段长度或默认值
|
||||
* @param $schema
|
||||
* @return mixed|string
|
||||
*/
|
||||
public static function getLengthValue($schema)
|
||||
{
|
||||
$type = $schema->DATA_TYPE;
|
||||
if (in_array($type, ['float', 'decimal', 'double'])) {
|
||||
return "{$schema->NUMERIC_PRECISION},{$schema->NUMERIC_SCALE}";
|
||||
}
|
||||
if ($type === 'enum') {
|
||||
return implode(',', array_map(function($item){
|
||||
return trim($item, "'");
|
||||
}, explode(',', substr($schema->COLUMN_TYPE, 5, -1))));
|
||||
}
|
||||
if (in_array($type, ['varchar', 'text', 'char'])) {
|
||||
return $schema->CHARACTER_MAXIMUM_LENGTH;
|
||||
}
|
||||
if (in_array($type, ['time', 'datetime', 'timestamp'])) {
|
||||
return $schema->CHARACTER_MAXIMUM_LENGTH;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取控件参数
|
||||
* @param $control
|
||||
* @param $control_args
|
||||
* @return array
|
||||
*/
|
||||
public static function getControlProps($control, $control_args): array
|
||||
{
|
||||
if (!$control_args) {
|
||||
return [];
|
||||
}
|
||||
$control = strtolower($control);
|
||||
$props = [];
|
||||
$split = explode(';', $control_args);
|
||||
foreach ($split as $item) {
|
||||
$pos = strpos($item, ':');
|
||||
if ($pos === false) {
|
||||
continue;
|
||||
}
|
||||
$name = trim(substr($item, 0, $pos));
|
||||
$values = trim(substr($item, $pos + 1));
|
||||
// values = a:v,c:d
|
||||
$pos = strpos($values, ':');
|
||||
if ($pos !== false && strpos($values, "#") !== 0) {
|
||||
$options = explode(',', $values);
|
||||
$values = [];
|
||||
foreach ($options as $option) {
|
||||
[$v, $n] = explode(':', $option);
|
||||
if (in_array($control, ['select', 'selectmulti', 'treeselect', 'treemultiselect']) && $name == 'data') {
|
||||
$values[] = ['value' => $v, 'name' => $n];
|
||||
} else {
|
||||
$values[$v] = $n;
|
||||
}
|
||||
}
|
||||
}
|
||||
$props[$name] = $values;
|
||||
}
|
||||
return $props;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某个composer包的版本
|
||||
* @param string $package
|
||||
* @return mixed|string
|
||||
*/
|
||||
public static function getPackageVersion(string $package)
|
||||
{
|
||||
$installed_php = base_path('vendor/composer/installed.php');
|
||||
if (is_file($installed_php)) {
|
||||
$packages = include $installed_php;
|
||||
}
|
||||
return substr($packages['versions'][$package]['version'] ?? 'unknown ', 0, -2);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reload webman
|
||||
* @return bool
|
||||
*/
|
||||
public static function reloadWebman()
|
||||
{
|
||||
if (function_exists('posix_kill')) {
|
||||
try {
|
||||
posix_kill(posix_getppid(), SIGUSR1);
|
||||
return true;
|
||||
} catch (Throwable $e) {}
|
||||
} else {
|
||||
Timer::add(1, function () {
|
||||
Worker::stopAll();
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause file monitor
|
||||
* @return void
|
||||
*/
|
||||
public static function pauseFileMonitor()
|
||||
{
|
||||
if (method_exists(Monitor::class, 'pause')) {
|
||||
Monitor::pause();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume file monitor
|
||||
* @return void
|
||||
*/
|
||||
public static function resumeFileMonitor()
|
||||
{
|
||||
if (method_exists(Monitor::class, 'resume')) {
|
||||
Monitor::resume();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\controller;
|
||||
|
||||
use plugin\admin\app\common\Auth;
|
||||
use plugin\admin\app\common\Util;
|
||||
use plugin\admin\app\model\Admin;
|
||||
use support\exception\BusinessException;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
use Webman\Captcha\CaptchaBuilder;
|
||||
use Webman\Captcha\PhraseBuilder;
|
||||
|
||||
/**
|
||||
* 管理员账户
|
||||
*/
|
||||
class AccountController extends Crud
|
||||
{
|
||||
/**
|
||||
* 不需要登录的方法
|
||||
* @var string[]
|
||||
*/
|
||||
protected $noNeedLogin = ['login', 'logout', 'captcha'];
|
||||
|
||||
/**
|
||||
* 不需要鉴权的方法
|
||||
* @var string[]
|
||||
*/
|
||||
protected $noNeedAuth = ['info'];
|
||||
|
||||
/**
|
||||
* @var Admin
|
||||
*/
|
||||
protected $model = null;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new Admin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 账户设置
|
||||
* @return Response
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return raw_view('account/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function login(Request $request): Response
|
||||
{
|
||||
$this->checkDatabaseAvailable();
|
||||
$captcha = $request->post('captcha', '');
|
||||
if (strtolower($captcha) !== session('captcha-login')) {
|
||||
return $this->json(1, '验证码错误');
|
||||
}
|
||||
$request->session()->forget('captcha-login');
|
||||
$username = $request->post('username', '');
|
||||
$password = $request->post('password', '');
|
||||
if (!$username) {
|
||||
return $this->json(1, '用户名不能为空');
|
||||
}
|
||||
$this->checkLoginLimit($username);
|
||||
$admin = Admin::where('username', $username)->first();
|
||||
if (!$admin || !Util::passwordVerify($password, $admin->password)) {
|
||||
return $this->json(1, '账户不存在或密码错误');
|
||||
}
|
||||
if ($admin->status != 0) {
|
||||
return $this->json(1, '当前账户暂时无法登录');
|
||||
}
|
||||
$admin->login_at = date('Y-m-d H:i:s');
|
||||
$admin->save();
|
||||
$this->removeLoginLimit($username);
|
||||
$admin = $admin->toArray();
|
||||
$session = $request->session();
|
||||
$admin['password'] = md5($admin['password']);
|
||||
$session->set('admin', $admin);
|
||||
return $this->json(0, '登录成功', [
|
||||
'nickname' => $admin['nickname'],
|
||||
'token' => $request->sessionId(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function logout(Request $request): Response
|
||||
{
|
||||
$request->session()->delete('admin');
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录信息
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function info(Request $request): Response
|
||||
{
|
||||
$admin = admin();
|
||||
if (!$admin) {
|
||||
return $this->json(1);
|
||||
}
|
||||
$info = [
|
||||
'id' => $admin['id'],
|
||||
'username' => $admin['username'],
|
||||
'nickname' => $admin['nickname'],
|
||||
'avatar' => $admin['avatar'],
|
||||
'email' => $admin['email'],
|
||||
'mobile' => $admin['mobile'],
|
||||
'isSuperAdmin' => Auth::isSuperAdmin(),
|
||||
'token' => $request->sessionId(),
|
||||
];
|
||||
return $this->json(0, 'ok', $info);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
$allow_column = [
|
||||
'nickname' => 'nickname',
|
||||
'avatar' => 'avatar',
|
||||
'email' => 'email',
|
||||
'mobile' => 'mobile',
|
||||
];
|
||||
|
||||
$data = $request->post();
|
||||
$update_data = [];
|
||||
foreach ($allow_column as $key => $column) {
|
||||
if (isset($data[$key])) {
|
||||
$update_data[$column] = $data[$key];
|
||||
}
|
||||
}
|
||||
if (isset($update_data['password'])) {
|
||||
$update_data['password'] = Util::passwordHash($update_data['password']);
|
||||
}
|
||||
Admin::where('id', admin_id())->update($update_data);
|
||||
$admin = admin();
|
||||
unset($update_data['password']);
|
||||
foreach ($update_data as $key => $value) {
|
||||
$admin[$key] = $value;
|
||||
}
|
||||
$request->session()->set('admin', $admin);
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function password(Request $request): Response
|
||||
{
|
||||
$hash = Admin::find(admin_id())['password'];
|
||||
$password = $request->post('password');
|
||||
if (!$password) {
|
||||
return $this->json(2, '密码不能为空');
|
||||
}
|
||||
if ($request->post('password_confirm') !== $password) {
|
||||
return $this->json(3, '两次密码输入不一致');
|
||||
}
|
||||
if (!Util::passwordVerify($request->post('old_password'), $hash)) {
|
||||
return $this->json(1, '原始密码不正确');
|
||||
}
|
||||
$update_data = [
|
||||
'password' => Util::passwordHash($password)
|
||||
];
|
||||
Admin::where('id', admin_id())->update($update_data);
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
* @param Request $request
|
||||
* @param string $type
|
||||
* @return Response
|
||||
*/
|
||||
public function captcha(Request $request, string $type = 'login'): Response
|
||||
{
|
||||
$builder = new PhraseBuilder(4, 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ');
|
||||
$captcha = new CaptchaBuilder(null, $builder);
|
||||
$captcha->build(120);
|
||||
$request->session()->set("captcha-$type", strtolower($captcha->getPhrase()));
|
||||
$img_content = $captcha->get();
|
||||
return response($img_content, 200, ['Content-Type' => 'image/jpeg']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录频率限制
|
||||
* @param $username
|
||||
* @return void
|
||||
* @throws BusinessException
|
||||
*/
|
||||
protected function checkLoginLimit($username)
|
||||
{
|
||||
$limit_log_path = runtime_path() . '/login';
|
||||
if (!is_dir($limit_log_path)) {
|
||||
mkdir($limit_log_path, 0777, true);
|
||||
}
|
||||
$limit_file = $limit_log_path . '/' . md5($username) . '.limit';
|
||||
$time = date('YmdH') . ceil(date('i')/5);
|
||||
$limit_info = [];
|
||||
if (is_file($limit_file)) {
|
||||
$json_str = file_get_contents($limit_file);
|
||||
$limit_info = json_decode($json_str, true);
|
||||
}
|
||||
|
||||
if (!$limit_info || $limit_info['time'] != $time) {
|
||||
$limit_info = [
|
||||
'username' => $username,
|
||||
'count' => 0,
|
||||
'time' => $time
|
||||
];
|
||||
}
|
||||
$limit_info['count']++;
|
||||
file_put_contents($limit_file, json_encode($limit_info));
|
||||
if ($limit_info['count'] >= 5) {
|
||||
throw new BusinessException('登录失败次数过多,请5分钟后再试');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除登录频率限制
|
||||
* @param $username
|
||||
* @return void
|
||||
*/
|
||||
protected function removeLoginLimit($username)
|
||||
{
|
||||
$limit_log_path = runtime_path() . '/login';
|
||||
$limit_file = $limit_log_path . '/' . md5($username) . '.limit';
|
||||
if (is_file($limit_file)) {
|
||||
unlink($limit_file);
|
||||
}
|
||||
}
|
||||
|
||||
protected function checkDatabaseAvailable()
|
||||
{
|
||||
if (!config('plugin.admin.database')) {
|
||||
throw new BusinessException('请重启webman');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\controller;
|
||||
|
||||
use plugin\admin\app\common\Auth;
|
||||
use plugin\admin\app\model\Admin;
|
||||
use plugin\admin\app\model\AdminRole;
|
||||
use support\exception\BusinessException;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 管理员列表
|
||||
*/
|
||||
class AdminController extends Crud
|
||||
{
|
||||
/**
|
||||
* 不需要鉴权的方法
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedAuth = ['select'];
|
||||
|
||||
/**
|
||||
* @var Admin
|
||||
*/
|
||||
protected $model = null;
|
||||
|
||||
/**
|
||||
* 开启auth数据限制
|
||||
* @var string
|
||||
*/
|
||||
protected $dataLimit = 'auth';
|
||||
|
||||
/**
|
||||
* 以id为数据限制字段
|
||||
* @var string
|
||||
*/
|
||||
protected $dataLimitField = 'id';
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new Admin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览
|
||||
* @return Response
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function index(): Response
|
||||
{
|
||||
return raw_view('admin/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function select(Request $request): Response
|
||||
{
|
||||
[$where, $format, $limit, $field, $order] = $this->selectInput($request);
|
||||
$query = $this->doSelect($where, $field, $order);
|
||||
if ($format === 'select') {
|
||||
return $this->formatSelect($query->get());
|
||||
}
|
||||
$paginator = $query->paginate($limit);
|
||||
$items = $paginator->items();
|
||||
$admin_ids = array_column($items, 'id');
|
||||
$roles = AdminRole::whereIn('admin_id', $admin_ids)->get();
|
||||
$roles_map = [];
|
||||
foreach ($roles as $role) {
|
||||
$roles_map[$role['admin_id']][] = $role['role_id'];
|
||||
}
|
||||
$login_admin_id = admin_id();
|
||||
foreach ($items as $index => $item) {
|
||||
$admin_id = $item['id'];
|
||||
$items[$index]['roles'] = isset($roles_map[$admin_id]) ? implode(',', $roles_map[$admin_id]) : '';
|
||||
$items[$index]['show_toolbar'] = $admin_id != $login_admin_id;
|
||||
}
|
||||
return json(['code' => 0, 'msg' => 'ok', 'count' => $paginator->total(), 'data' => $items]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function insert(Request $request): Response
|
||||
{
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $this->insertInput($request);
|
||||
unset($data['id']);
|
||||
$admin_id = $this->doInsert($data);
|
||||
$role_ids = $request->post('roles');
|
||||
$role_ids = $role_ids ? explode(',', $role_ids) : [];
|
||||
if (!$role_ids) {
|
||||
return $this->json(1, '至少选择一个角色组');
|
||||
}
|
||||
if (!Auth::isSuperAdmin() && array_diff($role_ids, Auth::getScopeRoleIds())) {
|
||||
return $this->json(1, '角色超出权限范围');
|
||||
}
|
||||
AdminRole::where('admin_id', $admin_id)->delete();
|
||||
foreach ($role_ids as $id) {
|
||||
$admin_role = new AdminRole;
|
||||
$admin_role->admin_id = $admin_id;
|
||||
$admin_role->role_id = $id;
|
||||
$admin_role->save();
|
||||
}
|
||||
return $this->json(0, 'ok', ['id' => $admin_id]);
|
||||
}
|
||||
return raw_view('admin/insert');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
if ($request->method() === 'POST') {
|
||||
|
||||
[$id, $data] = $this->updateInput($request);
|
||||
$admin_id = $request->post('id');
|
||||
if (!$admin_id) {
|
||||
return $this->json(1, '缺少参数');
|
||||
}
|
||||
|
||||
// 不能禁用自己
|
||||
if (isset($data['status']) && $data['status'] == 1 && $id == admin_id()) {
|
||||
return $this->json(1, '不能禁用自己');
|
||||
}
|
||||
|
||||
// 需要更新角色
|
||||
$role_ids = $request->post('roles');
|
||||
if ($role_ids !== null) {
|
||||
if (!$role_ids) {
|
||||
return $this->json(1, '至少选择一个角色组');
|
||||
}
|
||||
$role_ids = explode(',', $role_ids);
|
||||
|
||||
$is_supper_admin = Auth::isSuperAdmin();
|
||||
$exist_role_ids = AdminRole::where('admin_id', $admin_id)->pluck('role_id')->toArray();
|
||||
$scope_role_ids = Auth::getScopeRoleIds();
|
||||
if (!$is_supper_admin && !array_intersect($exist_role_ids, $scope_role_ids)) {
|
||||
return $this->json(1, '无权限更改该记录');
|
||||
}
|
||||
if (!$is_supper_admin && array_diff($role_ids, $scope_role_ids)) {
|
||||
return $this->json(1, '角色超出权限范围');
|
||||
}
|
||||
|
||||
// 删除账户角色
|
||||
$delete_ids = array_diff($exist_role_ids, $role_ids);
|
||||
AdminRole::whereIn('role_id', $delete_ids)->where('admin_id', $admin_id)->delete();
|
||||
// 添加账户角色
|
||||
$add_ids = array_diff($role_ids, $exist_role_ids);
|
||||
foreach ($add_ids as $role_id) {
|
||||
$admin_role = new AdminRole;
|
||||
$admin_role->admin_id = $admin_id;
|
||||
$admin_role->role_id = $role_id;
|
||||
$admin_role->save();
|
||||
}
|
||||
}
|
||||
|
||||
$this->doUpdate($id, $data);
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
return raw_view('admin/update');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
$primary_key = $this->model->getKeyName();
|
||||
$ids = $request->post($primary_key);
|
||||
if (!$ids) {
|
||||
return $this->json(0);
|
||||
}
|
||||
$ids = (array)$ids;
|
||||
if (in_array(admin_id(), $ids)) {
|
||||
return $this->json(1, '不能删除自己');
|
||||
}
|
||||
if (!Auth::isSuperAdmin() && array_diff($ids, Auth::getScopeAdminIds())) {
|
||||
return $this->json(1, '无数据权限');
|
||||
}
|
||||
$this->model->whereIn($primary_key, $ids)->each(function (Admin $admin) {
|
||||
$admin->delete();
|
||||
});
|
||||
AdminRole::whereIn('admin_id', $ids)->each(function (AdminRole $admin_role) {
|
||||
$admin_role->delete();
|
||||
});
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\controller;
|
||||
|
||||
use support\Model;
|
||||
use support\Response;
|
||||
|
||||
/**
|
||||
* 基础控制器
|
||||
*/
|
||||
class Base
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Model
|
||||
*/
|
||||
protected $model = null;
|
||||
|
||||
/**
|
||||
* 无需登录及鉴权的方法
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* 需要登录无需鉴权的方法
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedAuth = [];
|
||||
|
||||
/**
|
||||
* 数据限制
|
||||
* null 不做限制,任何管理员都可以查看该表的所有数据
|
||||
* auth 管理员能看到自己以及自己的子管理员插入的数据
|
||||
* personal 管理员只能看到自己插入的数据
|
||||
* @var string
|
||||
*/
|
||||
protected $dataLimit = null;
|
||||
|
||||
/**
|
||||
* 数据限制字段
|
||||
*/
|
||||
protected $dataLimitField = 'admin_id';
|
||||
|
||||
/**
|
||||
* 返回格式化json数据
|
||||
*
|
||||
* @param int $code
|
||||
* @param string $msg
|
||||
* @param array $data
|
||||
* @return Response
|
||||
*/
|
||||
protected function json(int $code, string $msg = 'ok', array $data = []): Response
|
||||
{
|
||||
return json(['code' => $code, 'data' => $data, 'msg' => $msg]);
|
||||
}
|
||||
|
||||
protected function success(string $msg = '成功', array $data = []): Response
|
||||
{
|
||||
return $this->json(0, $msg, $data);
|
||||
}
|
||||
|
||||
protected function fail(string $msg = '失败', array $data = []): Response
|
||||
{
|
||||
return $this->json(1, $msg, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\controller;
|
||||
|
||||
use plugin\admin\app\common\Util;
|
||||
use plugin\admin\app\model\Option;
|
||||
use support\exception\BusinessException;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 系统设置
|
||||
*/
|
||||
class ConfigController extends Base
|
||||
{
|
||||
/**
|
||||
* 不需要验证权限的方法
|
||||
* @var string[]
|
||||
*/
|
||||
protected $noNeedAuth = ['get'];
|
||||
|
||||
/**
|
||||
* 账户设置
|
||||
* @return Response
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function index(): Response
|
||||
{
|
||||
return raw_view('config/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
* @return Response
|
||||
*/
|
||||
public function get(): Response
|
||||
{
|
||||
return json($this->getByDefault());
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于配置文件获取默认权限
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getByDefault()
|
||||
{
|
||||
$name = 'system_config';
|
||||
$config = Option::where('name', $name)->value('value');
|
||||
if (empty($config)) {
|
||||
$config = file_get_contents(base_path('plugin/admin/public/config/pear.config.json'));
|
||||
if ($config) {
|
||||
$option = new Option();
|
||||
$option->name = $name;
|
||||
$option->value = $config;
|
||||
$option->save();
|
||||
}
|
||||
}
|
||||
return json_decode($config, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更改
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
$post = $request->post();
|
||||
$config = $this->getByDefault();
|
||||
$data = [];
|
||||
foreach ($post as $section => $items) {
|
||||
if (!isset($config[$section])) {
|
||||
continue;
|
||||
}
|
||||
switch ($section) {
|
||||
case 'logo':
|
||||
$data[$section]['title'] = htmlspecialchars($items['title'] ?? '');
|
||||
$data[$section]['image'] = Util::filterUrlPath($items['image'] ?? '');
|
||||
$data[$section]['icp'] = htmlspecialchars($items['icp'] ?? '');
|
||||
$data[$section]['beian'] = htmlspecialchars($items['beian'] ?? '');
|
||||
$data[$section]['footer_txt'] = htmlspecialchars($items['footer_txt'] ?? '');
|
||||
break;
|
||||
case 'menu':
|
||||
$data[$section]['data'] = Util::filterUrlPath($items['data'] ?? '');
|
||||
$data[$section]['accordion'] = !empty($items['accordion']);
|
||||
$data[$section]['collapse'] = !empty($items['collapse']);
|
||||
$data[$section]['control'] = !empty($items['control']);
|
||||
$data[$section]['controlWidth'] = (int)($items['controlWidth'] ?? 2000);
|
||||
$data[$section]['select'] = (int)$items['select'] ?? 0;
|
||||
$data[$section]['async'] = true;
|
||||
break;
|
||||
case 'tab':
|
||||
$data[$section]['enable'] = true;
|
||||
$data[$section]['keepState'] = !empty($items['keepState']);
|
||||
$data[$section]['preload'] = !empty($items['preload']);
|
||||
$data[$section]['session'] = !empty($items['session']);
|
||||
$data[$section]['max'] = Util::filterNum($items['max'] ?? '30');
|
||||
$data[$section]['index']['id'] = Util::filterNum($items['index']['id'] ?? '0');
|
||||
$data[$section]['index']['href'] = Util::filterUrlPath($items['index']['href'] ?? '');
|
||||
$data[$section]['index']['title'] = htmlspecialchars($items['index']['title'] ?? '首页');
|
||||
break;
|
||||
case 'theme':
|
||||
$data[$section]['defaultColor'] = Util::filterNum($items['defaultColor'] ?? '2');
|
||||
$data[$section]['defaultMenu'] = $items['defaultMenu'] ?? '' == 'dark-theme' ? 'dark-theme' : 'light-theme';
|
||||
$data[$section]['defaultHeader'] = $items['defaultHeader'] ?? '' == 'dark-theme' ? 'dark-theme' : 'light-theme';
|
||||
$data[$section]['allowCustom'] = !empty($items['allowCustom']);
|
||||
$data[$section]['banner'] = !empty($items['banner']);
|
||||
break;
|
||||
case 'colors':
|
||||
foreach ($config['colors'] as $index => $item) {
|
||||
if (!isset($items[$index])) {
|
||||
$config['colors'][$index] = $item;
|
||||
continue;
|
||||
}
|
||||
$data_item = $items[$index];
|
||||
$data[$section][$index]['id'] = $index + 1;
|
||||
$data[$section][$index]['color'] = $this->filterColor($data_item['color'] ?? '');
|
||||
$data[$section][$index]['second'] = $this->filterColor($data_item['second'] ?? '');
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
$config = array_merge($config, $data);
|
||||
$name = 'system_config';
|
||||
Option::where('name', $name)->update([
|
||||
'value' => json_encode($config)
|
||||
]);
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 颜色检查
|
||||
* @param string $color
|
||||
* @return string
|
||||
* @throws BusinessException
|
||||
*/
|
||||
protected function filterColor(string $color): string
|
||||
{
|
||||
if (!preg_match('/\#[a-zA-Z]6/', $color)) {
|
||||
throw new BusinessException('参数错误');
|
||||
}
|
||||
return $color;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\controller;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use plugin\admin\app\common\Auth;
|
||||
use plugin\admin\app\common\Tree;
|
||||
use plugin\admin\app\common\Util;
|
||||
use support\exception\BusinessException;
|
||||
use support\Model;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
|
||||
class Crud extends Base
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Model
|
||||
*/
|
||||
protected $model = null;
|
||||
|
||||
/**
|
||||
* 查询
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function select(Request $request): Response
|
||||
{
|
||||
[$where, $format, $limit, $field, $order] = $this->selectInput($request);
|
||||
$query = $this->doSelect($where, $field, $order);
|
||||
return $this->doFormat($query, $format, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function insert(Request $request): Response
|
||||
{
|
||||
$data = $this->insertInput($request);
|
||||
$id = $this->doInsert($data);
|
||||
return $this->json(0, 'ok', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
[$id, $data] = $this->updateInput($request);
|
||||
$this->doUpdate($id, $data);
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
$ids = $this->deleteInput($request);
|
||||
$this->doDelete($ids);
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询前置
|
||||
* @param Request $request
|
||||
* @return array
|
||||
* @throws BusinessException
|
||||
*/
|
||||
protected function selectInput(Request $request): array
|
||||
{
|
||||
$field = $request->get('field');
|
||||
$order = $request->get('order', 'asc');
|
||||
$format = $request->get('format', 'normal');
|
||||
$limit = (int)$request->get('limit', $format === 'tree' ? 1000 : 10);
|
||||
$limit = $limit <= 0 ? 10 : $limit;
|
||||
$order = $order === 'asc' ? 'asc' : 'desc';
|
||||
$where = $request->get();
|
||||
$page = (int)$request->get('page');
|
||||
$page = $page > 0 ? $page : 1;
|
||||
$table = config('plugin.admin.database.connections.mysql.prefix') . $this->model->getTable();
|
||||
|
||||
$allow_column = Util::db()->select("desc `$table`");
|
||||
if (!$allow_column) {
|
||||
throw new BusinessException('表不存在');
|
||||
}
|
||||
$allow_column = array_column($allow_column, 'Field', 'Field');
|
||||
if (!in_array($field, $allow_column)) {
|
||||
$field = null;
|
||||
}
|
||||
foreach ($where as $column => $value) {
|
||||
if (
|
||||
$value === '' || !isset($allow_column[$column]) ||
|
||||
is_array($value) && (empty($value) || !in_array($value[0], ['null', 'not null']) && !isset($value[1]))
|
||||
) {
|
||||
unset($where[$column]);
|
||||
}
|
||||
}
|
||||
// 按照数据限制字段返回数据
|
||||
if (!Auth::isSuperAdmin()) {
|
||||
if ($this->dataLimit === 'personal') {
|
||||
$where[$this->dataLimitField] = admin_id();
|
||||
} elseif ($this->dataLimit === 'auth') {
|
||||
$primary_key = $this->model->getKeyName();
|
||||
if (!Auth::isSuperAdmin() && (!isset($where[$primary_key]) || $this->dataLimitField != $primary_key)) {
|
||||
$where[$this->dataLimitField] = ['in', Auth::getScopeAdminIds(true)];
|
||||
}
|
||||
}
|
||||
}
|
||||
return [$where, $format, $limit, $field, $order, $page];
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定查询where条件,并没有真正的查询数据库操作
|
||||
* @param array $where
|
||||
* @param string|null $field
|
||||
* @param string $order
|
||||
* @return EloquentBuilder|QueryBuilder|Model
|
||||
*/
|
||||
protected function doSelect(array $where, ?string $field = null, string $order= 'desc')
|
||||
{
|
||||
$model = $this->model;
|
||||
foreach ($where as $column => $value) {
|
||||
if (is_array($value)) {
|
||||
if ($value[0] === 'like' || $value[0] === 'not like') {
|
||||
$model = $model->where($column, $value[0], "%$value[1]%");
|
||||
} elseif (in_array($value[0], ['>', '=', '<', '<>'])) {
|
||||
$model = $model->where($column, $value[0], $value[1]);
|
||||
} elseif ($value[0] == 'in' && !empty($value[1])) {
|
||||
$valArr = $value[1];
|
||||
if (is_string($value[1])) {
|
||||
$valArr = explode(",", trim($value[1]));
|
||||
}
|
||||
$model = $model->whereIn($column, $valArr);
|
||||
} elseif ($value[0] == 'not in' && !empty($value[1])) {
|
||||
$valArr = $value[1];
|
||||
if (is_string($value[1])) {
|
||||
$valArr = explode(",", trim($value[1]));
|
||||
}
|
||||
$model = $model->whereNotIn($column, $valArr);
|
||||
}elseif ($value[0] == 'null') {
|
||||
$model = $model->whereNull($column);
|
||||
} elseif ($value[0] == 'not null') {
|
||||
$model = $model->whereNotNull($column);
|
||||
} elseif ($value[0] !== '' || $value[1] !== '') {
|
||||
$model = $model->whereBetween($column, $value);
|
||||
}
|
||||
} else {
|
||||
$model = $model->where($column, $value);
|
||||
}
|
||||
}
|
||||
if ($field) {
|
||||
$model = $model->orderBy($field, $order);
|
||||
}
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行真正查询,并返回格式化数据
|
||||
* @param $query
|
||||
* @param $format
|
||||
* @param $limit
|
||||
* @return Response
|
||||
*/
|
||||
protected function doFormat($query, $format, $limit): Response
|
||||
{
|
||||
$methods = [
|
||||
'select' => 'formatSelect',
|
||||
'tree' => 'formatTree',
|
||||
'table_tree' => 'formatTableTree',
|
||||
'normal' => 'formatNormal',
|
||||
];
|
||||
$paginator = $query->paginate($limit);
|
||||
$total = $paginator->total();
|
||||
$items = $paginator->items();
|
||||
if (method_exists($this, "afterQuery")) {
|
||||
$items = call_user_func([$this, "afterQuery"], $items);
|
||||
}
|
||||
$format_function = $methods[$format] ?? 'formatNormal';
|
||||
return call_user_func([$this, $format_function], $items, $total);
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入前置方法
|
||||
* @param Request $request
|
||||
* @return array
|
||||
* @throws BusinessException
|
||||
*/
|
||||
protected function insertInput(Request $request): array
|
||||
{
|
||||
$data = $this->inputFilter($request->post());
|
||||
$password_filed = 'password';
|
||||
if (isset($data[$password_filed])) {
|
||||
$data[$password_filed] = Util::passwordHash($data[$password_filed]);
|
||||
}
|
||||
|
||||
if (!Auth::isSuperAdmin()) {
|
||||
if ($this->dataLimit === 'personal') {
|
||||
$data[$this->dataLimitField] = admin_id();
|
||||
} elseif ($this->dataLimit === 'auth') {
|
||||
if (!empty($data[$this->dataLimitField])) {
|
||||
$admin_id = $data[$this->dataLimitField];
|
||||
if (!in_array($admin_id, Auth::getScopeAdminIds(true))) {
|
||||
throw new BusinessException('无数据权限');
|
||||
}
|
||||
} else {
|
||||
$data[$this->dataLimitField] = admin_id();
|
||||
}
|
||||
}
|
||||
} elseif ($this->dataLimit && empty($data[$this->dataLimitField])) {
|
||||
$data[$this->dataLimitField] = admin_id();
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行插入
|
||||
* @param array $data
|
||||
* @return mixed|null
|
||||
*/
|
||||
protected function doInsert(array $data)
|
||||
{
|
||||
$primary_key = $this->model->getKeyName();
|
||||
$model_class = get_class($this->model);
|
||||
$model = new $model_class;
|
||||
foreach ($data as $key => $val) {
|
||||
$model->{$key} = $val;
|
||||
}
|
||||
$model->save();
|
||||
return $primary_key ? $model->$primary_key : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新前置方法
|
||||
* @param Request $request
|
||||
* @return array
|
||||
* @throws BusinessException
|
||||
*/
|
||||
protected function updateInput(Request $request): array
|
||||
{
|
||||
$primary_key = $this->model->getKeyName();
|
||||
$id = $request->post($primary_key);
|
||||
$data = $this->inputFilter($request->post());
|
||||
$model = $this->model->find($id);
|
||||
if (!$model) {
|
||||
throw new BusinessException('记录不存在', 2);
|
||||
}
|
||||
|
||||
if (!Auth::isSuperAdmin()) {
|
||||
if ($this->dataLimit == 'personal') {
|
||||
if ($model->{$this->dataLimitField} != admin_id()) {
|
||||
throw new BusinessException('无数据权限');
|
||||
}
|
||||
} elseif ($this->dataLimit == 'auth') {
|
||||
$scopeAdminIds = Auth::getScopeAdminIds(true);
|
||||
$admin_ids = [
|
||||
$data[$this->dataLimitField] ?? false, // 检查要更新的数据admin_id是否是有权限的值
|
||||
$model->{$this->dataLimitField} ?? false // 检查要更新的记录的admin_id是否有权限
|
||||
];
|
||||
foreach ($admin_ids as $admin_id) {
|
||||
if ($admin_id && !in_array($admin_id, $scopeAdminIds)) {
|
||||
throw new BusinessException('无数据权限');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$password_filed = 'password';
|
||||
if (isset($data[$password_filed])) {
|
||||
// 密码为空,则不更新密码
|
||||
if ($data[$password_filed] === '') {
|
||||
unset($data[$password_filed]);
|
||||
} else {
|
||||
$data[$password_filed] = Util::passwordHash($data[$password_filed]);
|
||||
}
|
||||
}
|
||||
unset($data[$primary_key]);
|
||||
return [$id, $data];
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行更新
|
||||
* @param $id
|
||||
* @param $data
|
||||
* @return void
|
||||
*/
|
||||
protected function doUpdate($id, $data)
|
||||
{
|
||||
$model = $this->model->find($id);
|
||||
foreach ($data as $key => $val) {
|
||||
$model->{$key} = $val;
|
||||
}
|
||||
$model->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对用户输入表单过滤
|
||||
* @param array $data
|
||||
* @return array
|
||||
* @throws BusinessException
|
||||
*/
|
||||
protected function inputFilter(array $data): array
|
||||
{
|
||||
$table = config('plugin.admin.database.connections.mysql.prefix') . $this->model->getTable();
|
||||
$allow_column = $this->model->getConnection()->select("desc `$table`");
|
||||
if (!$allow_column) {
|
||||
throw new BusinessException('表不存在', 2);
|
||||
}
|
||||
$columns = array_column($allow_column, 'Type', 'Field');
|
||||
foreach ($data as $col => $item) {
|
||||
if (!isset($columns[$col])) {
|
||||
unset($data[$col]);
|
||||
continue;
|
||||
}
|
||||
// 非字符串类型传空则为null
|
||||
if ($item === '' && strpos(strtolower($columns[$col]), 'varchar') === false && strpos(strtolower($columns[$col]), 'text') === false) {
|
||||
$data[$col] = null;
|
||||
}
|
||||
if (is_array($item)) {
|
||||
$data[$col] = implode(',', $item);
|
||||
}
|
||||
}
|
||||
if (empty($data['created_at'])) {
|
||||
unset($data['created_at']);
|
||||
}
|
||||
if (empty($data['updated_at'])) {
|
||||
unset($data['updated_at']);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除前置方法
|
||||
* @param Request $request
|
||||
* @return array
|
||||
* @throws BusinessException
|
||||
*/
|
||||
protected function deleteInput(Request $request): array
|
||||
{
|
||||
$primary_key = $this->model->getKeyName();
|
||||
if (!$primary_key) {
|
||||
throw new BusinessException('该表无主键,不支持删除');
|
||||
}
|
||||
$ids = (array)$request->post($primary_key, []);
|
||||
if (!Auth::isSuperAdmin()){
|
||||
$admin_ids = [];
|
||||
if ($this->dataLimit) {
|
||||
$admin_ids = $this->model->where($primary_key, $ids)->pluck($this->dataLimitField)->toArray();
|
||||
}
|
||||
if ($this->dataLimit == 'personal') {
|
||||
if (!in_array(admin_id(), $admin_ids)) {
|
||||
throw new BusinessException('无数据权限');
|
||||
}
|
||||
} elseif ($this->dataLimit == 'auth') {
|
||||
if (array_diff($admin_ids, Auth::getScopeAdminIds(true))) {
|
||||
throw new BusinessException('无数据权限');
|
||||
}
|
||||
}
|
||||
}
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行删除
|
||||
* @param array $ids
|
||||
* @return void
|
||||
*/
|
||||
protected function doDelete(array $ids)
|
||||
{
|
||||
if (!$ids) {
|
||||
return;
|
||||
}
|
||||
$primary_key = $this->model->getKeyName();
|
||||
$this->model->whereIn($primary_key, $ids)->each(function ($model) {
|
||||
$model->delete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化树
|
||||
* @param $items
|
||||
* @return Response
|
||||
*/
|
||||
protected function formatTree($items): Response
|
||||
{
|
||||
$format_items = [];
|
||||
$primary_key = $this->model->getKeyName();
|
||||
foreach ($items as $item) {
|
||||
$format_items[] = [
|
||||
'name' => $this->guessName($item) ?: $item->$primary_key,
|
||||
'value' => (string)$item->$primary_key,
|
||||
'id' => $item->$primary_key,
|
||||
'pid' => $item->pid,
|
||||
];
|
||||
}
|
||||
$tree = new Tree($format_items);
|
||||
return $this->json(0, 'ok', $tree->getTree());
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化表格树
|
||||
* @param $items
|
||||
* @return Response
|
||||
*/
|
||||
protected function formatTableTree($items): Response
|
||||
{
|
||||
$tree = new Tree($items);
|
||||
return $this->json(0, 'ok', $tree->getTree());
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化下拉列表
|
||||
* @param $items
|
||||
* @return Response
|
||||
*/
|
||||
protected function formatSelect($items): Response
|
||||
{
|
||||
$formatted_items = [];
|
||||
$primary_key = $this->model->getKeyName();
|
||||
foreach ($items as $item) {
|
||||
$formatted_items[] = [
|
||||
'name' => $this->guessName($item) ?: $item->$primary_key,
|
||||
'value' => $item->$primary_key
|
||||
];
|
||||
}
|
||||
return $this->json(0, 'ok', $formatted_items);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用格式化
|
||||
* @param $items
|
||||
* @param $total
|
||||
* @return Response
|
||||
*/
|
||||
protected function formatNormal($items, $total): Response
|
||||
{
|
||||
return json(['code' => 0, 'msg' => 'ok', 'count' => $total, 'data' => $items]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询数据库后置方法,可用于修改数据
|
||||
* @param mixed $items 原数据
|
||||
* @return mixed 修改后数据
|
||||
*/
|
||||
protected function afterQuery($items)
|
||||
{
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 猜测记录名称
|
||||
* @param $item
|
||||
* @return mixed
|
||||
*/
|
||||
protected function guessName($item)
|
||||
{
|
||||
return $item->title ?? $item->name ?? $item->nickname ?? $item->username ?? $item->id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\controller;
|
||||
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 开发辅助相关
|
||||
*/
|
||||
class DevController
|
||||
{
|
||||
/**
|
||||
* 表单构建
|
||||
* @return Response
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function formBuild()
|
||||
{
|
||||
return raw_view('dev/form-build');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\controller;
|
||||
|
||||
use plugin\admin\app\model\Dict;
|
||||
use plugin\admin\app\model\Option;
|
||||
use support\exception\BusinessException;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 字典管理
|
||||
*/
|
||||
class DictController extends Base
|
||||
{
|
||||
/**
|
||||
* 不需要授权的方法
|
||||
*/
|
||||
protected $noNeedAuth = ['get'];
|
||||
|
||||
/**
|
||||
* 浏览
|
||||
* @return Response
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function index(): Response
|
||||
{
|
||||
return raw_view('dict/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function select(Request $request): Response
|
||||
{
|
||||
$name = $request->get('name', '');
|
||||
$page = $request->get('page', 1);
|
||||
$limit = $request->get('limit', 10);
|
||||
$offset = ($page-1)*$limit;
|
||||
if ($name && is_string($name)) {
|
||||
$items = Option::where('name', 'like', "dict_$name%")->limit($limit)->offset($offset)->get()->toArray();
|
||||
} else {
|
||||
$items = Option::where('name', 'like', 'dict_%')->limit($limit)->offset($offset)->get()->toArray();
|
||||
}
|
||||
$get_items = Option::where('name', 'like', "dict_$name%")->get()->toArray();
|
||||
$count = count($get_items);
|
||||
foreach ($items as &$item) {
|
||||
$item['name'] = Dict::optionNameTodictName($item['name']);
|
||||
}
|
||||
return json(['code' => 0, 'msg' => 'ok', 'count' => $count, 'data' => $items]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function insert(Request $request): Response
|
||||
{
|
||||
if ($request->method() === 'POST') {
|
||||
$name = $request->post('name');
|
||||
if (Dict::get($name)) {
|
||||
return $this->json(1, '字典已经存在');
|
||||
}
|
||||
if (!preg_match('/^[a-zA-Z0-9_]+$/', $name)) {
|
||||
return $this->json(2, '字典名称只能是字母数字下划线的组合');
|
||||
}
|
||||
$values = (array)$request->post('value', []);
|
||||
Dict::save($name, $values);
|
||||
}
|
||||
return raw_view('dict/insert');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
if ($request->method() === 'POST') {
|
||||
$name = $request->post('name');
|
||||
if (!Dict::get($name)) {
|
||||
return $this->json(1, '字典不存在');
|
||||
}
|
||||
if (!preg_match('/^[a-zA-Z0-9_]+$/', $name)) {
|
||||
return $this->json(2, '字典名称只能是字母数字下划线的组合');
|
||||
}
|
||||
Dict::save($name, $request->post('value'));
|
||||
}
|
||||
return raw_view('dict/update');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
$names = (array)$request->post('name');
|
||||
Dict::delete($names);
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取
|
||||
* @param Request $request
|
||||
* @param $name
|
||||
* @return Response
|
||||
*/
|
||||
public function get(Request $request, $name): Response
|
||||
{
|
||||
return $this->json(0, 'ok', (array)Dict::get($name));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\controller;
|
||||
|
||||
use plugin\admin\app\common\Util;
|
||||
use plugin\admin\app\model\Option;
|
||||
use plugin\admin\app\model\User;
|
||||
use support\exception\BusinessException;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use think\db\Where;
|
||||
use Throwable;
|
||||
use Workerman\Worker;
|
||||
|
||||
class IndexController
|
||||
{
|
||||
|
||||
/**
|
||||
* 无需登录的方法
|
||||
* @var string[]
|
||||
*/
|
||||
protected $noNeedLogin = ['index'];
|
||||
|
||||
/**
|
||||
* 不需要鉴权的方法
|
||||
* @var string[]
|
||||
*/
|
||||
protected $noNeedAuth = ['dashboard'];
|
||||
|
||||
/**
|
||||
* 后台主页
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
clearstatcache();
|
||||
if (!is_file(base_path('plugin/admin/config/database.php'))) {
|
||||
return raw_view('index/install');
|
||||
}
|
||||
$admin = admin();
|
||||
if (!$admin) {
|
||||
$name = 'system_config';
|
||||
$config = Option::where('name', $name)->value('value');
|
||||
$config = json_decode($config, true);
|
||||
$title = $config['logo']['title'] ?? 'webman admin';
|
||||
$logo = $config['logo']['image'] ?? '/app/admin/admin/images/logo.png';
|
||||
return raw_view('account/login',['logo'=>$logo,'title'=>$title]);
|
||||
}
|
||||
return raw_view('index/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 仪表板
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function dashboard(Request $request): Response
|
||||
{
|
||||
// 今日新增用户数
|
||||
$today_user_count = User::where('created_at', '>', date('Y-m-d') . ' 00:00:00')->count();
|
||||
// 7天内新增用户数
|
||||
$day7_user_count = User::where('created_at', '>', date('Y-m-d H:i:s', time() - 7 * 24 * 60 * 60))->count();
|
||||
// 30天内新增用户数
|
||||
$day30_user_count = User::where('created_at', '>', date('Y-m-d H:i:s', time() - 30 * 24 * 60 * 60))->count();
|
||||
// 总用户数
|
||||
$user_count = User::count();
|
||||
// mysql版本
|
||||
$version = Util::db()->select('select VERSION() as version');
|
||||
$mysql_version = $version[0]->version ?? 'unknown';
|
||||
|
||||
$day7_detail = [];
|
||||
$now = time();
|
||||
for ($i = 0; $i < 7; $i++) {
|
||||
$date = date('Y-m-d', $now - 24 * 60 * 60 * $i);
|
||||
$day7_detail[substr($date, 5)] = User::where('created_at', '>', "$date 00:00:00")
|
||||
->where('created_at', '<', "$date 23:59:59")->count();
|
||||
}
|
||||
|
||||
return raw_view('index/dashboard', [
|
||||
'today_user_count' => $today_user_count,
|
||||
'day7_user_count' => $day7_user_count,
|
||||
'day30_user_count' => $day30_user_count,
|
||||
'user_count' => $user_count,
|
||||
'php_version' => PHP_VERSION,
|
||||
'workerman_version' => Worker::VERSION,
|
||||
'webman_version' => Util::getPackageVersion('workerman/webman-framework'),
|
||||
'admin_version' => Util::getPackageVersion('webman/admin'),
|
||||
'mysql_version' => $mysql_version,
|
||||
'os' => PHP_OS,
|
||||
'day7_detail' => array_reverse($day7_detail),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\controller;
|
||||
|
||||
use Illuminate\Database\Capsule\Manager;
|
||||
use plugin\admin\app\common\Util;
|
||||
use support\exception\BusinessException;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use Webman\Captcha\CaptchaBuilder;
|
||||
|
||||
/**
|
||||
* 安装
|
||||
*/
|
||||
class InstallController extends Base
|
||||
{
|
||||
/**
|
||||
* 不需要登录的方法
|
||||
* @var string[]
|
||||
*/
|
||||
protected $noNeedLogin = ['step1', 'step2'];
|
||||
|
||||
/**
|
||||
* 设置数据库
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException|\Throwable
|
||||
*/
|
||||
public function step1(Request $request): Response
|
||||
{
|
||||
$database_config_file = base_path() . '/plugin/admin/config/database.php';
|
||||
clearstatcache();
|
||||
if (is_file($database_config_file)) {
|
||||
return $this->json(1, '管理后台已经安装!如需重新安装,请删除该插件数据库配置文件并重启');
|
||||
}
|
||||
|
||||
if (!class_exists(CaptchaBuilder::class) || !class_exists(Manager::class)) {
|
||||
return $this->json(1, '请运行 composer require -W illuminate/database 安装illuminate/database组件并重启');
|
||||
}
|
||||
|
||||
$user = $request->post('user');
|
||||
$password = $request->post('password');
|
||||
$database = $request->post('database');
|
||||
$host = $request->post('host');
|
||||
$port = (int)$request->post('port') ?: 3306;
|
||||
$overwrite = $request->post('overwrite');
|
||||
|
||||
try {
|
||||
$db = $this->getPdo($host, $user, $password, $port);
|
||||
$smt = $db->query("show databases like '$database'");
|
||||
if (empty($smt->fetchAll())) {
|
||||
$db->exec("create database $database");
|
||||
}
|
||||
$db->exec("use $database");
|
||||
$smt = $db->query("show tables");
|
||||
$tables = $smt->fetchAll();
|
||||
} catch (\Throwable $e) {
|
||||
if (stripos($e, 'Access denied for user')) {
|
||||
return $this->json(1, '数据库用户名或密码错误');
|
||||
}
|
||||
if (stripos($e, 'Connection refused')) {
|
||||
return $this->json(1, 'Connection refused. 请确认数据库IP端口是否正确,数据库已经启动');
|
||||
}
|
||||
if (stripos($e, 'timed out')) {
|
||||
return $this->json(1, '数据库连接超时,请确认数据库IP端口是否正确,安全组及防火墙已经放行端口');
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$tables_to_install = [
|
||||
'wa_admins',
|
||||
'wa_admin_roles',
|
||||
'wa_roles',
|
||||
'wa_rules',
|
||||
'wa_options',
|
||||
'wa_users',
|
||||
'wa_uploads',
|
||||
];
|
||||
|
||||
$tables_exist = [];
|
||||
foreach ($tables as $table) {
|
||||
$tables_exist[] = current($table);
|
||||
}
|
||||
$tables_conflict = array_intersect($tables_to_install, $tables_exist);
|
||||
if (!$overwrite) {
|
||||
if ($tables_conflict) {
|
||||
return $this->json(1, '以下表' . implode(',', $tables_conflict) . '已经存在,如需覆盖请选择强制覆盖');
|
||||
}
|
||||
} else {
|
||||
foreach ($tables_conflict as $table) {
|
||||
$db->exec("DROP TABLE `$table`");
|
||||
}
|
||||
}
|
||||
|
||||
$sql_file = base_path() . '/plugin/admin/install.sql';
|
||||
if (!is_file($sql_file)) {
|
||||
return $this->json(1, '数据库SQL文件不存在');
|
||||
}
|
||||
|
||||
$sql_query = file_get_contents($sql_file);
|
||||
$sql_query = $this->removeComments($sql_query);
|
||||
$sql_query = $this->splitSqlFile($sql_query, ';');
|
||||
foreach ($sql_query as $sql) {
|
||||
$db->exec($sql);
|
||||
}
|
||||
|
||||
// 导入菜单
|
||||
$menus = include base_path() . '/plugin/admin/config/menu.php';
|
||||
// 安装过程中没有数据库配置,无法使用api\Menu::import()方法
|
||||
$this->importMenu($menus, $db);
|
||||
|
||||
$config_content = <<<EOF
|
||||
<?php
|
||||
return [
|
||||
'default' => 'mysql',
|
||||
'connections' => [
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => '$host',
|
||||
'port' => '$port',
|
||||
'database' => '$database',
|
||||
'username' => '$user',
|
||||
'password' => '$password',
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
'prefix' => '',
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
],
|
||||
],
|
||||
];
|
||||
EOF;
|
||||
|
||||
file_put_contents($database_config_file, $config_content);
|
||||
|
||||
$think_orm_config = <<<EOF
|
||||
<?php
|
||||
return [
|
||||
'default' => 'mysql',
|
||||
'connections' => [
|
||||
'mysql' => [
|
||||
// 数据库类型
|
||||
'type' => 'mysql',
|
||||
// 服务器地址
|
||||
'hostname' => '$host',
|
||||
// 数据库名
|
||||
'database' => '$database',
|
||||
// 数据库用户名
|
||||
'username' => '$user',
|
||||
// 数据库密码
|
||||
'password' => '$password',
|
||||
// 数据库连接端口
|
||||
'hostport' => $port,
|
||||
// 数据库连接参数
|
||||
'params' => [
|
||||
// 连接超时3秒
|
||||
\PDO::ATTR_TIMEOUT => 3,
|
||||
],
|
||||
// 数据库编码默认采用utf8
|
||||
'charset' => 'utf8mb4',
|
||||
// 数据库表前缀
|
||||
'prefix' => '',
|
||||
// 断线重连
|
||||
'break_reconnect' => true,
|
||||
// 关闭SQL监听日志
|
||||
'trigger_sql' => true,
|
||||
// 自定义分页类
|
||||
'bootstrap' => ''
|
||||
],
|
||||
],
|
||||
];
|
||||
EOF;
|
||||
file_put_contents(base_path() . '/plugin/admin/config/thinkorm.php', $think_orm_config);
|
||||
|
||||
|
||||
// 尝试reload
|
||||
if (function_exists('posix_kill')) {
|
||||
set_error_handler(function () {});
|
||||
posix_kill(posix_getppid(), SIGUSR1);
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置管理员
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function step2(Request $request): Response
|
||||
{
|
||||
$username = $request->post('username');
|
||||
$password = $request->post('password');
|
||||
$password_confirm = $request->post('password_confirm');
|
||||
if ($password != $password_confirm) {
|
||||
return $this->json(1, '两次密码不一致');
|
||||
}
|
||||
if (!is_file($config_file = base_path() . '/plugin/admin/config/database.php')) {
|
||||
return $this->json(1, '请先完成第一步数据库配置');
|
||||
}
|
||||
$config = include $config_file;
|
||||
$connection = $config['connections']['mysql'];
|
||||
$pdo = $this->getPdo($connection['host'], $connection['username'], $connection['password'], $connection['port'], $connection['database']);
|
||||
|
||||
if ($pdo->query('select * from `wa_admins`')->fetchAll()) {
|
||||
return $this->json(1, '后台已经安装完毕,无法通过此页面创建管理员');
|
||||
}
|
||||
|
||||
$smt = $pdo->prepare("insert into `wa_admins` (`username`, `password`, `nickname`, `created_at`, `updated_at`) values (:username, :password, :nickname, :created_at, :updated_at)");
|
||||
$time = date('Y-m-d H:i:s');
|
||||
$data = [
|
||||
'username' => $username,
|
||||
'password' => Util::passwordHash($password),
|
||||
'nickname' => '超级管理员',
|
||||
'created_at' => $time,
|
||||
'updated_at' => $time
|
||||
];
|
||||
foreach ($data as $key => $value) {
|
||||
$smt->bindValue($key, $value);
|
||||
}
|
||||
$smt->execute();
|
||||
$admin_id = $pdo->lastInsertId();
|
||||
|
||||
$smt = $pdo->prepare("insert into `wa_admin_roles` (`role_id`, `admin_id`) values (:role_id, :admin_id)");
|
||||
$smt->bindValue('role_id', 1);
|
||||
$smt->bindValue('admin_id', $admin_id);
|
||||
$smt->execute();
|
||||
|
||||
$request->session()->flush();
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加菜单
|
||||
* @param array $menu
|
||||
* @param \PDO $pdo
|
||||
* @return int
|
||||
*/
|
||||
protected function addMenu(array $menu, \PDO $pdo): int
|
||||
{
|
||||
$allow_columns = ['title', 'key', 'icon', 'href', 'pid', 'weight', 'type'];
|
||||
$data = [];
|
||||
foreach ($allow_columns as $column) {
|
||||
if (isset($menu[$column])) {
|
||||
$data[$column] = $menu[$column];
|
||||
}
|
||||
}
|
||||
$time = date('Y-m-d H:i:s');
|
||||
$data['created_at'] = $data['updated_at'] = $time;
|
||||
$values = [];
|
||||
foreach ($data as $k => $v) {
|
||||
$values[] = ":$k";
|
||||
}
|
||||
$columns = array_keys($data);
|
||||
foreach ($columns as $k => $column) {
|
||||
$columns[$k] = "`$column`";
|
||||
}
|
||||
$sql = "insert into wa_rules (" .implode(',', $columns). ") values (" . implode(',', $values) . ")";
|
||||
$smt = $pdo->prepare($sql);
|
||||
foreach ($data as $key => $value) {
|
||||
$smt->bindValue($key, $value);
|
||||
}
|
||||
$smt->execute();
|
||||
return $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入菜单
|
||||
* @param array $menu_tree
|
||||
* @param \PDO $pdo
|
||||
* @return void
|
||||
*/
|
||||
protected function importMenu(array $menu_tree, \PDO $pdo)
|
||||
{
|
||||
if (is_numeric(key($menu_tree)) && !isset($menu_tree['key'])) {
|
||||
foreach ($menu_tree as $item) {
|
||||
$this->importMenu($item, $pdo);
|
||||
}
|
||||
return;
|
||||
}
|
||||
$children = $menu_tree['children'] ?? [];
|
||||
unset($menu_tree['children']);
|
||||
$smt = $pdo->prepare("select * from wa_rules where `key`=:key limit 1");
|
||||
$smt->execute(['key' => $menu_tree['key']]);
|
||||
$old_menu = $smt->fetch();
|
||||
if ($old_menu) {
|
||||
$pid = $old_menu['id'];
|
||||
$params = [
|
||||
'title' => $menu_tree['title'],
|
||||
'icon' => $menu_tree['icon'] ?? '',
|
||||
'key' => $menu_tree['key'],
|
||||
];
|
||||
$sql = "update wa_rules set title=:title, icon=:icon where `key`=:key";
|
||||
$smt = $pdo->prepare($sql);
|
||||
$smt->execute($params);
|
||||
} else {
|
||||
$pid = $this->addMenu($menu_tree, $pdo);
|
||||
}
|
||||
foreach ($children as $menu) {
|
||||
$menu['pid'] = $pid;
|
||||
$this->importMenu($menu, $pdo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 去除sql文件中的注释
|
||||
* @param $sql
|
||||
* @return string
|
||||
*/
|
||||
protected function removeComments($sql): string
|
||||
{
|
||||
return preg_replace("/(\n--[^\n]*)/","", $sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分割sql文件
|
||||
* @param $sql
|
||||
* @param $delimiter
|
||||
* @return array
|
||||
*/
|
||||
function splitSqlFile($sql, $delimiter): array
|
||||
{
|
||||
$tokens = explode($delimiter, $sql);
|
||||
$output = array();
|
||||
$matches = array();
|
||||
$token_count = count($tokens);
|
||||
for ($i = 0; $i < $token_count; $i++) {
|
||||
if (($i != ($token_count - 1)) || (strlen($tokens[$i] > 0))) {
|
||||
$total_quotes = preg_match_all("/'/", $tokens[$i], $matches);
|
||||
$escaped_quotes = preg_match_all("/(?<!\\\\)(\\\\\\\\)*\\\\'/", $tokens[$i], $matches);
|
||||
$unescaped_quotes = $total_quotes - $escaped_quotes;
|
||||
|
||||
if (($unescaped_quotes % 2) == 0) {
|
||||
$output[] = $tokens[$i];
|
||||
$tokens[$i] = "";
|
||||
} else {
|
||||
$temp = $tokens[$i] . $delimiter;
|
||||
$tokens[$i] = "";
|
||||
|
||||
$complete_stmt = false;
|
||||
for ($j = $i + 1; (!$complete_stmt && ($j < $token_count)); $j++) {
|
||||
$total_quotes = preg_match_all("/'/", $tokens[$j], $matches);
|
||||
$escaped_quotes = preg_match_all("/(?<!\\\\)(\\\\\\\\)*\\\\'/", $tokens[$j], $matches);
|
||||
$unescaped_quotes = $total_quotes - $escaped_quotes;
|
||||
if (($unescaped_quotes % 2) == 1) {
|
||||
$output[] = $temp . $tokens[$j];
|
||||
$tokens[$j] = "";
|
||||
$temp = "";
|
||||
$complete_stmt = true;
|
||||
$i = $j;
|
||||
} else {
|
||||
$temp .= $tokens[$j] . $delimiter;
|
||||
$tokens[$j] = "";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取pdo连接
|
||||
* @param $host
|
||||
* @param $username
|
||||
* @param $password
|
||||
* @param $port
|
||||
* @param $database
|
||||
* @return \PDO
|
||||
*/
|
||||
protected function getPdo($host, $username, $password, $port, $database = null): \PDO
|
||||
{
|
||||
$dsn = "mysql:host=$host;port=$port;";
|
||||
if ($database) {
|
||||
$dsn .= "dbname=$database";
|
||||
}
|
||||
$params = [
|
||||
\PDO::MYSQL_ATTR_INIT_COMMAND => "set names utf8mb4",
|
||||
\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
|
||||
\PDO::ATTR_EMULATE_PREPARES => false,
|
||||
\PDO::ATTR_TIMEOUT => 5,
|
||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||
];
|
||||
return new \PDO($dsn, $username, $password, $params);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\controller;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use plugin\admin\app\common\Util;
|
||||
use process\Monitor;
|
||||
use support\exception\BusinessException;
|
||||
use support\Log;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use ZIPARCHIVE;
|
||||
use function array_diff;
|
||||
use function ini_get;
|
||||
use function scandir;
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use const PATH_SEPARATOR;
|
||||
|
||||
class PluginController extends Base
|
||||
{
|
||||
/**
|
||||
* 不需要鉴权的方法
|
||||
* @var string[]
|
||||
*/
|
||||
protected $noNeedAuth = ['schema', 'captcha'];
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return string
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$client = $this->httpClient();
|
||||
$response = $client->get("/webman-admin/apps");
|
||||
return (string)$response->getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function list(Request $request): Response
|
||||
{
|
||||
$installed = $this->getLocalPlugins();
|
||||
|
||||
$client = $this->httpClient();
|
||||
$query = $request->get();
|
||||
$query['version'] = $this->getAdminVersion();
|
||||
$response = $client->get('/api/app/list', ['query' => $query]);
|
||||
$content = $response->getBody()->getContents();
|
||||
$data = json_decode($content, true);
|
||||
if (!$data) {
|
||||
$msg = "/api/app/list return $content";
|
||||
echo "msg\r\n";
|
||||
Log::error($msg);
|
||||
return $this->json(1, '获取数据出错');
|
||||
}
|
||||
$disabled = is_phar();
|
||||
foreach ($data['data']['items'] as $key => $item) {
|
||||
$name = $item['name'];
|
||||
$data['data']['items'][$key]['installed'] = $installed[$name] ?? 0;
|
||||
$data['data']['items'][$key]['disabled'] = $disabled;
|
||||
}
|
||||
$items = $data['data']['items'];
|
||||
$count = $data['data']['total'];
|
||||
return json(['code' => 0, 'msg' => 'ok', 'data' => $items, 'count' => $count]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws GuzzleException|BusinessException
|
||||
*/
|
||||
public function install(Request $request): Response
|
||||
{
|
||||
$name = $request->post('name');
|
||||
$version = $request->post('version');
|
||||
$installed_version = $this->getPluginVersion($name);
|
||||
if (!$name || !$version) {
|
||||
return $this->json(1, '缺少参数');
|
||||
}
|
||||
|
||||
$user = session('app-plugin-user');
|
||||
if (!$user) {
|
||||
return $this->json(-1, '请登录');
|
||||
}
|
||||
|
||||
// 获取下载zip文件url
|
||||
$data = $this->getDownloadUrl($name, $version);
|
||||
if ($data['code'] != 0) {
|
||||
return $this->json($data['code'], $data['msg'], $data['data'] ?? []);
|
||||
}
|
||||
|
||||
// 下载zip文件
|
||||
$base_path = base_path() . "/plugin/$name";
|
||||
$zip_file = "$base_path.zip";
|
||||
$extract_to = base_path() . '/plugin/';
|
||||
$this->downloadZipFile($data['data']['url'], $zip_file);
|
||||
|
||||
$has_zip_archive = class_exists(ZipArchive::class, false);
|
||||
if (!$has_zip_archive) {
|
||||
$cmd = $this->getUnzipCmd($zip_file, $extract_to);
|
||||
if (!$cmd) {
|
||||
throw new BusinessException('请给php安装zip模块或者给系统安装unzip命令');
|
||||
}
|
||||
if (!function_exists('proc_open')) {
|
||||
throw new BusinessException('请解除proc_open函数的禁用或者给php安装zip模块');
|
||||
}
|
||||
}
|
||||
|
||||
Util::pauseFileMonitor();
|
||||
try {
|
||||
// 解压zip到plugin目录
|
||||
if ($has_zip_archive) {
|
||||
$zip = new ZipArchive;
|
||||
$zip->open($zip_file);
|
||||
}
|
||||
|
||||
$context = null;
|
||||
$install_class = "\\plugin\\$name\\api\\Install";
|
||||
if ($installed_version) {
|
||||
// 执行beforeUpdate
|
||||
if (class_exists($install_class) && method_exists($install_class, 'beforeUpdate')) {
|
||||
$context = call_user_func([$install_class, 'beforeUpdate'], $installed_version, $version);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($zip)) {
|
||||
$zip->extractTo(base_path() . '/plugin/');
|
||||
unset($zip);
|
||||
} else {
|
||||
$this->unzipWithCmd($cmd);
|
||||
}
|
||||
|
||||
unlink($zip_file);
|
||||
|
||||
if ($installed_version) {
|
||||
// 执行update更新
|
||||
if (class_exists($install_class) && method_exists($install_class, 'update')) {
|
||||
call_user_func([$install_class, 'update'], $installed_version, $version, $context);
|
||||
}
|
||||
} else {
|
||||
// 执行install安装
|
||||
if (class_exists($install_class) && method_exists($install_class, 'install')) {
|
||||
call_user_func([$install_class, 'install'], $version);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Util::resumeFileMonitor();
|
||||
}
|
||||
|
||||
Util::reloadWebman();
|
||||
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function uninstall(Request $request): Response
|
||||
{
|
||||
$name = $request->post('name');
|
||||
$version = $request->post('version');
|
||||
if (!$name || !preg_match('/^[a-zA-Z0-9_]+$/', $name)) {
|
||||
return $this->json(1, '参数错误');
|
||||
}
|
||||
|
||||
// 获得插件路径
|
||||
clearstatcache();
|
||||
$path = get_realpath(base_path() . "/plugin/$name");
|
||||
if (!$path || !is_dir($path)) {
|
||||
return $this->json(1, '已经删除');
|
||||
}
|
||||
|
||||
// 执行uninstall卸载
|
||||
$install_class = "\\plugin\\$name\\api\\Install";
|
||||
if (class_exists($install_class) && method_exists($install_class, 'uninstall')) {
|
||||
call_user_func([$install_class, 'uninstall'], $version);
|
||||
}
|
||||
|
||||
// 删除目录
|
||||
clearstatcache();
|
||||
if (is_dir($path)) {
|
||||
$monitor_support_pause = method_exists(Monitor::class, 'pause');
|
||||
if ($monitor_support_pause) {
|
||||
Monitor::pause();
|
||||
}
|
||||
try {
|
||||
$this->rmDir($path);
|
||||
} finally {
|
||||
if ($monitor_support_pause) {
|
||||
Monitor::resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
clearstatcache();
|
||||
|
||||
Util::reloadWebman();
|
||||
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付
|
||||
* @param Request $request
|
||||
* @return string|Response
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function pay(Request $request)
|
||||
{
|
||||
$app = $request->get('app');
|
||||
if (!$app) {
|
||||
return response('app not found');
|
||||
}
|
||||
$token = session('app-plugin-token');
|
||||
if (!$token) {
|
||||
return 'Please login workerman.net';
|
||||
}
|
||||
$client = $this->httpClient();
|
||||
$response = $client->get("/payment/app/$app/$token");
|
||||
return (string)$response->getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录验证码
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function captcha(Request $request): Response
|
||||
{
|
||||
$client = $this->httpClient();
|
||||
$response = $client->get('/user/captcha?type=login');
|
||||
$sid_str = $response->getHeaderLine('Set-Cookie');
|
||||
if (preg_match('/PHPSID=([a-zA-Z_0-9]+?);/', $sid_str, $match)) {
|
||||
$sid = $match[1];
|
||||
session()->set('app-plugin-token', $sid);
|
||||
}
|
||||
return response($response->getBody()->getContents())->withHeader('Content-Type', 'image/jpeg');
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录官网
|
||||
* @param Request $request
|
||||
* @return Response|string
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function login(Request $request)
|
||||
{
|
||||
$client = $this->httpClient();
|
||||
if ($request->method() === 'GET') {
|
||||
$response = $client->get("/webman-admin/login");
|
||||
return (string)$response->getBody();
|
||||
}
|
||||
|
||||
$response = $client->post('/api/user/login', [
|
||||
'form_params' => [
|
||||
'email' => $request->post('username'),
|
||||
'password' => $request->post('password'),
|
||||
'captcha' => $request->post('captcha')
|
||||
]
|
||||
]);
|
||||
$content = $response->getBody()->getContents();
|
||||
$data = json_decode($content, true);
|
||||
if (!$data) {
|
||||
$msg = "/api/user/login return $content";
|
||||
echo "msg\r\n";
|
||||
Log::error($msg);
|
||||
return $this->json(1, '发生错误');
|
||||
}
|
||||
if ($data['code'] != 0) {
|
||||
return $this->json($data['code'], $data['msg']);
|
||||
}
|
||||
session()->set('app-plugin-user', [
|
||||
'uid' => $data['data']['uid']
|
||||
]);
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取zip下载url
|
||||
* @param $name
|
||||
* @param $version
|
||||
* @return mixed
|
||||
* @throws BusinessException
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
protected function getDownloadUrl($name, $version)
|
||||
{
|
||||
$client = $this->httpClient();
|
||||
$response = $client->get("/app/download/$name?version=$version");
|
||||
|
||||
$content = $response->getBody()->getContents();
|
||||
$data = json_decode($content, true);
|
||||
if (!$data) {
|
||||
$msg = "/api/app/download return $content";
|
||||
Log::error($msg);
|
||||
throw new BusinessException('访问官方接口失败 ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase());
|
||||
}
|
||||
if ($data['code'] && $data['code'] != -1 && $data['code'] != -2) {
|
||||
throw new BusinessException($data['msg']);
|
||||
}
|
||||
if ($data['code'] == 0 && !isset($data['data']['url'])) {
|
||||
throw new BusinessException('官方接口返回数据错误');
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载zip
|
||||
* @param $url
|
||||
* @param $file
|
||||
* @return void
|
||||
* @throws BusinessException
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
protected function downloadZipFile($url, $file)
|
||||
{
|
||||
$client = $this->downloadClient();
|
||||
$response = $client->get($url);
|
||||
$body = $response->getBody();
|
||||
$status = $response->getStatusCode();
|
||||
if ($status == 404) {
|
||||
throw new BusinessException('安装包不存在');
|
||||
}
|
||||
$zip_content = $body->getContents();
|
||||
if (empty($zip_content)) {
|
||||
throw new BusinessException('安装包不存在');
|
||||
}
|
||||
file_put_contents($file, $zip_content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统支持的解压命令
|
||||
* @param $zip_file
|
||||
* @param $extract_to
|
||||
* @return mixed|string|null
|
||||
*/
|
||||
protected function getUnzipCmd($zip_file, $extract_to)
|
||||
{
|
||||
if ($cmd = $this->findCmd('unzip')) {
|
||||
$cmd = "$cmd -o -qq $zip_file -d $extract_to";
|
||||
} else if ($cmd = $this->findCmd('7z')) {
|
||||
$cmd = "$cmd x -bb0 -y $zip_file -o$extract_to";
|
||||
} else if ($cmd = $this->findCmd('7zz')) {
|
||||
$cmd = "$cmd x -bb0 -y $zip_file -o$extract_to";
|
||||
}
|
||||
return $cmd;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用解压命令解压
|
||||
* @param $cmd
|
||||
* @return void
|
||||
* @throws BusinessException
|
||||
*/
|
||||
protected function unzipWithCmd($cmd)
|
||||
{
|
||||
$desc = [
|
||||
0 => ["pipe", "r"],
|
||||
1 => ["pipe", "w"],
|
||||
2 => ["pipe", "w"],
|
||||
];
|
||||
$handler = proc_open($cmd, $desc, $pipes);
|
||||
if (!is_resource($handler)) {
|
||||
throw new BusinessException("解压zip时出错:proc_open调用失败");
|
||||
}
|
||||
$err = fread($pipes[2], 1024);
|
||||
fclose($pipes[2]);
|
||||
proc_close($handler);
|
||||
if ($err) {
|
||||
throw new BusinessException("解压zip时出错:$err");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已安装的插件列表
|
||||
* @return array
|
||||
*/
|
||||
protected function getLocalPlugins(): array
|
||||
{
|
||||
clearstatcache();
|
||||
$installed = [];
|
||||
$plugin_names = array_diff(scandir(base_path() . '/plugin/'), array('.', '..')) ?: [];
|
||||
foreach ($plugin_names as $plugin_name) {
|
||||
if (is_dir(base_path() . "/plugin/$plugin_name") && $version = $this->getPluginVersion($plugin_name)) {
|
||||
$installed[$plugin_name] = $version;
|
||||
}
|
||||
}
|
||||
return $installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已安装的插件列表
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function getInstalledPlugins(Request $request): Response
|
||||
{
|
||||
return $this->json(0, 'ok', $this->getLocalPlugins());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取本地插件版本
|
||||
* @param $name
|
||||
* @return array|mixed|null
|
||||
*/
|
||||
protected function getPluginVersion($name)
|
||||
{
|
||||
if (!is_file($file = base_path() . "/plugin/$name/config/app.php")) {
|
||||
return null;
|
||||
}
|
||||
$config = include $file;
|
||||
return $config['version'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取webman/admin版本
|
||||
* @return string
|
||||
*/
|
||||
protected function getAdminVersion(): string
|
||||
{
|
||||
return config('plugin.admin.app.version', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除目录
|
||||
* @param $src
|
||||
* @return void
|
||||
*/
|
||||
protected function rmDir($src)
|
||||
{
|
||||
$dir = opendir($src);
|
||||
while (false !== ($file = readdir($dir))) {
|
||||
if (($file != '.') && ($file != '..')) {
|
||||
$full = $src . '/' . $file;
|
||||
if (is_dir($full)) {
|
||||
$this->rmDir($full);
|
||||
} else {
|
||||
unlink($full);
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir($dir);
|
||||
rmdir($src);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取httpclient
|
||||
* @return Client
|
||||
*/
|
||||
protected function httpClient(): Client
|
||||
{
|
||||
// 下载zip
|
||||
$options = [
|
||||
'base_uri' => config('plugin.admin.app.plugin_market_host'),
|
||||
'timeout' => 60,
|
||||
'connect_timeout' => 5,
|
||||
'verify' => false,
|
||||
'http_errors' => false,
|
||||
'headers' => [
|
||||
'Referer' => \request()->fullUrl(),
|
||||
'User-Agent' => 'webman-app-plugin',
|
||||
'Accept' => 'application/json;charset=UTF-8',
|
||||
]
|
||||
];
|
||||
if ($token = session('app-plugin-token')) {
|
||||
$options['headers']['Cookie'] = "PHPSID=$token;";
|
||||
}
|
||||
return new Client($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载httpclient
|
||||
* @return Client
|
||||
*/
|
||||
protected function downloadClient(): Client
|
||||
{
|
||||
// 下载zip
|
||||
$options = [
|
||||
'timeout' => 59,
|
||||
'connect_timeout' => 5,
|
||||
'verify' => false,
|
||||
'http_errors' => false,
|
||||
'headers' => [
|
||||
'Referer' => \request()->fullUrl(),
|
||||
'User-Agent' => 'webman-app-plugin',
|
||||
]
|
||||
];
|
||||
if ($token = session('app-plugin-token')) {
|
||||
$options['headers']['Cookie'] = "PHPSID=$token;";
|
||||
}
|
||||
return new Client($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找系统命令
|
||||
* @param string $name
|
||||
* @param string|null $default
|
||||
* @param array $extraDirs
|
||||
* @return mixed|string|null
|
||||
*/
|
||||
protected function findCmd(string $name, ?string $default = null, array $extraDirs = [])
|
||||
{
|
||||
if (ini_get('open_basedir')) {
|
||||
$searchPath = array_merge(explode(PATH_SEPARATOR, ini_get('open_basedir')), $extraDirs);
|
||||
$dirs = [];
|
||||
foreach ($searchPath as $path) {
|
||||
if (@is_dir($path)) {
|
||||
$dirs[] = $path;
|
||||
} else {
|
||||
if (basename($path) == $name && @is_executable($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$dirs = array_merge(
|
||||
explode(PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')),
|
||||
$extraDirs
|
||||
);
|
||||
}
|
||||
|
||||
$suffixes = [''];
|
||||
if ('\\' === DIRECTORY_SEPARATOR) {
|
||||
$pathExt = getenv('PATHEXT');
|
||||
$suffixes = array_merge($pathExt ? explode(PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com'], $suffixes);
|
||||
}
|
||||
foreach ($suffixes as $suffix) {
|
||||
foreach ($dirs as $dir) {
|
||||
if (@is_file($file = $dir . DIRECTORY_SEPARATOR . $name . $suffix) && ('\\' === DIRECTORY_SEPARATOR || @is_executable($file))) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\controller;
|
||||
|
||||
use plugin\admin\app\common\Auth;
|
||||
use plugin\admin\app\common\Tree;
|
||||
use plugin\admin\app\model\Role;
|
||||
use plugin\admin\app\model\Rule;
|
||||
use support\exception\BusinessException;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 角色管理
|
||||
*/
|
||||
class RoleController extends Crud
|
||||
{
|
||||
/**
|
||||
* 不需要鉴权的方法
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedAuth = ['select'];
|
||||
|
||||
/**
|
||||
* @var Role
|
||||
*/
|
||||
protected $model = null;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new Role;
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览
|
||||
* @return Response
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function index(): Response
|
||||
{
|
||||
return raw_view('role/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function select(Request $request): Response
|
||||
{
|
||||
$id = $request->get('id');
|
||||
[$where, $format, $limit, $field, $order] = $this->selectInput($request);
|
||||
$limit = 100000;
|
||||
$role_ids = Auth::getScopeRoleIds(true);
|
||||
if (!$id) {
|
||||
$where['id'] = ['in', $role_ids];
|
||||
} elseif (!in_array($id, $role_ids)) {
|
||||
throw new BusinessException('无权限');
|
||||
}
|
||||
$query = $this->doSelect($where, $field, $order);
|
||||
return $this->doFormat($query, $format, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function insert(Request $request): Response
|
||||
{
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $this->insertInput($request);
|
||||
$pid = $data['pid'] ?? null;
|
||||
if (!$pid) {
|
||||
return $this->json(1, '请选择父级角色组');
|
||||
}
|
||||
if (!Auth::isSuperAdmin() && !in_array($pid, Auth::getScopeRoleIds(true))) {
|
||||
return $this->json(1, '父级角色组超出权限范围');
|
||||
}
|
||||
$this->checkRules($pid, $data['rules'] ?? '');
|
||||
|
||||
$id = $this->doInsert($data);
|
||||
return $this->json(0, 'ok', ['id' => $id]);
|
||||
}
|
||||
return raw_view('role/insert');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
if ($request->method() === 'GET') {
|
||||
return raw_view('role/update');
|
||||
}
|
||||
[$id, $data] = $this->updateInput($request);
|
||||
$is_supper_admin = Auth::isSuperAdmin();
|
||||
$descendant_role_ids = Auth::getScopeRoleIds();
|
||||
if (!$is_supper_admin && !in_array($id, $descendant_role_ids)) {
|
||||
return $this->json(1, '无数据权限');
|
||||
}
|
||||
|
||||
$role = Role::find($id);
|
||||
if (!$role) {
|
||||
return $this->json(1, '数据不存在');
|
||||
}
|
||||
$is_supper_role = $role->rules === '*';
|
||||
|
||||
// 超级角色组不允许更改rules pid 字段
|
||||
if ($is_supper_role) {
|
||||
unset($data['rules'], $data['pid']);
|
||||
}
|
||||
|
||||
if (key_exists('pid', $data)) {
|
||||
$pid = $data['pid'];
|
||||
if (!$pid) {
|
||||
return $this->json(1, '请选择父级角色组');
|
||||
}
|
||||
if ($pid == $id) {
|
||||
return $this->json(1, '父级不能是自己');
|
||||
}
|
||||
if (!$is_supper_admin && !in_array($pid, Auth::getScopeRoleIds(true))) {
|
||||
return $this->json(1, '父级超出权限范围');
|
||||
}
|
||||
} else {
|
||||
$pid = $role->pid;
|
||||
}
|
||||
|
||||
if (!$is_supper_role) {
|
||||
$this->checkRules($pid, $data['rules'] ?? '');
|
||||
}
|
||||
|
||||
$this->doUpdate($id, $data);
|
||||
|
||||
// 删除所有子角色组中已经不存在的权限
|
||||
if (!$is_supper_role) {
|
||||
$tree = new Tree(Role::select(['id', 'pid'])->get());
|
||||
$descendant_roles = $tree->getDescendant([$id]);
|
||||
$descendant_role_ids = array_column($descendant_roles, 'id');
|
||||
$rule_ids = $data['rules'] ? explode(',', $data['rules']) : [];
|
||||
foreach ($descendant_role_ids as $role_id) {
|
||||
$tmp_role = Role::find($role_id);
|
||||
$tmp_rule_ids = $role->getRuleIds();
|
||||
$tmp_rule_ids = array_intersect(explode(',',$tmp_role->rules), $tmp_rule_ids);
|
||||
$tmp_role->rules = implode(',', $tmp_rule_ids);
|
||||
$tmp_role->save();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
$ids = $this->deleteInput($request);
|
||||
if (in_array(1, $ids)) {
|
||||
return $this->json(1, '无法删除超级管理员角色');
|
||||
}
|
||||
if (!Auth::isSuperAdmin() && array_diff($ids, Auth::getScopeRoleIds())) {
|
||||
return $this->json(1, '无删除权限');
|
||||
}
|
||||
$tree = new Tree(Role::get());
|
||||
$descendants = $tree->getDescendant($ids);
|
||||
if ($descendants) {
|
||||
$ids = array_merge($ids, array_column($descendants, 'id'));
|
||||
}
|
||||
$this->doDelete($ids);
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色权限
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function rules(Request $request): Response
|
||||
{
|
||||
$role_id = $request->get('id');
|
||||
if (empty($role_id)) {
|
||||
return $this->json(0, 'ok', []);
|
||||
}
|
||||
if (!Auth::isSuperAdmin() && !in_array($role_id, Auth::getScopeRoleIds(true))) {
|
||||
return $this->json(1, '角色组超出权限范围');
|
||||
}
|
||||
$rule_id_string = Role::where('id', $role_id)->value('rules');
|
||||
if ($rule_id_string === '') {
|
||||
return $this->json(0, 'ok', []);
|
||||
}
|
||||
$rules = Rule::get();
|
||||
$include = [];
|
||||
if ($rule_id_string !== '*') {
|
||||
$include = explode(',', $rule_id_string);
|
||||
}
|
||||
$items = [];
|
||||
foreach ($rules as $item) {
|
||||
$items[] = [
|
||||
'name' => $item->title ?? $item->name ?? $item->id,
|
||||
'value' => (string)$item->id,
|
||||
'id' => $item->id,
|
||||
'pid' => $item->pid,
|
||||
];
|
||||
}
|
||||
$tree = new Tree($items);
|
||||
return $this->json(0, 'ok', $tree->getTree($include));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查权限字典是否合法
|
||||
* @param int $role_id
|
||||
* @param $rule_ids
|
||||
* @return void
|
||||
* @throws BusinessException
|
||||
*/
|
||||
protected function checkRules(int $role_id, $rule_ids)
|
||||
{
|
||||
if ($rule_ids) {
|
||||
$rule_ids = explode(',', $rule_ids);
|
||||
if (in_array('*', $rule_ids)) {
|
||||
throw new BusinessException('非法数据');
|
||||
}
|
||||
$rule_exists = Rule::whereIn('id', $rule_ids)->pluck('id');
|
||||
if (count($rule_exists) != count($rule_ids)) {
|
||||
throw new BusinessException('权限不存在');
|
||||
}
|
||||
$rule_id_string = Role::where('id', $role_id)->value('rules');
|
||||
if ($rule_id_string === '') {
|
||||
throw new BusinessException('数据超出权限范围');
|
||||
}
|
||||
if ($rule_id_string === '*') {
|
||||
return;
|
||||
}
|
||||
$legal_rule_ids = explode(',', $rule_id_string);
|
||||
if (array_diff($rule_ids, $legal_rule_ids)) {
|
||||
throw new BusinessException('数据超出权限范围');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\controller;
|
||||
|
||||
use Exception;
|
||||
use plugin\admin\app\common\Tree;
|
||||
use plugin\admin\app\common\Util;
|
||||
use plugin\admin\app\model\Role;
|
||||
use plugin\admin\app\model\Rule;
|
||||
use support\exception\BusinessException;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 权限菜单
|
||||
*/
|
||||
class RuleController extends Crud
|
||||
{
|
||||
/**
|
||||
* 不需要权限的方法
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $noNeedAuth = ['get', 'permission'];
|
||||
|
||||
/**
|
||||
* @var Rule
|
||||
*/
|
||||
protected $model = null;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new Rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览
|
||||
* @return Response
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function index(): Response
|
||||
{
|
||||
return raw_view('rule/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function select(Request $request): Response
|
||||
{
|
||||
$this->syncRules();
|
||||
return parent::select($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
function get(Request $request): Response
|
||||
{
|
||||
$rules = $this->getRules(admin('roles'));
|
||||
$types = $request->get('type', '0,1');
|
||||
$types = is_string($types) ? explode(',', $types) : [0, 1];
|
||||
$items = Rule::orderBy('weight', 'desc')->get()->toArray();
|
||||
|
||||
$formatted_items = [];
|
||||
foreach ($items as $item) {
|
||||
$item['pid'] = (int)$item['pid'];
|
||||
$item['name'] = $item['title'];
|
||||
$item['value'] = $item['id'];
|
||||
$item['icon'] = $item['icon'] ? "layui-icon {$item['icon']}" : '';
|
||||
$formatted_items[] = $item;
|
||||
}
|
||||
|
||||
$tree = new Tree($formatted_items);
|
||||
$tree_items = $tree->getTree();
|
||||
// 超级管理员权限为 *
|
||||
if (!in_array('*', $rules)) {
|
||||
$this->removeNotContain($tree_items, 'id', $rules);
|
||||
}
|
||||
$this->removeNotContain($tree_items, 'type', $types);
|
||||
$menus = $this->empty_filter(Tree::arrayValues($tree_items));
|
||||
return $this->json(0, 'ok', $menus);
|
||||
}
|
||||
|
||||
private function empty_filter($menus)
|
||||
{
|
||||
return array_map(
|
||||
function ($menu) {
|
||||
if (isset($menu['children'])) {
|
||||
$menu['children'] = $this->empty_filter($menu['children']);
|
||||
}
|
||||
return $menu;
|
||||
},
|
||||
array_values(array_filter(
|
||||
$menus,
|
||||
function ($menu) {
|
||||
return $menu['type'] != 0 || isset($menu['children']) && count($this->empty_filter($menu['children'])) > 0;
|
||||
}
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
public function permission(Request $request): Response
|
||||
{
|
||||
$rules = $this->getRules(admin('roles'));
|
||||
// 超级管理员
|
||||
if (in_array('*', $rules)) {
|
||||
return $this->json(0, 'ok', ['*']);
|
||||
}
|
||||
$keys = Rule::whereIn('id', $rules)->pluck('key');
|
||||
$permissions = [];
|
||||
foreach ($keys as $key) {
|
||||
if (!$key = Util::controllerToUrlPath($key)) {
|
||||
continue;
|
||||
}
|
||||
$code = str_replace('/', '.', trim($key, '/'));
|
||||
$permissions[] = $code;
|
||||
}
|
||||
return $this->json(0, 'ok', $permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类同步规则到数据库
|
||||
* @return void
|
||||
*/
|
||||
protected function syncRules()
|
||||
{
|
||||
$items = $this->model->where('key', 'like', '%\\\\%')->get()->keyBy('key');
|
||||
$methods_in_db = [];
|
||||
$methods_in_files = [];
|
||||
foreach ($items as $item) {
|
||||
$class = $item->key;
|
||||
if (strpos($class, '@')) {
|
||||
$methods_in_db[$class] = $class;
|
||||
continue;
|
||||
}
|
||||
if (class_exists($class)) {
|
||||
$reflection = new \ReflectionClass($class);
|
||||
$properties = $reflection->getDefaultProperties();
|
||||
$no_need_auth = array_merge($properties['noNeedLogin'] ?? [], $properties['noNeedAuth'] ?? []);
|
||||
$class = $reflection->getName();
|
||||
$pid = $item->id;
|
||||
$methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
|
||||
foreach ($methods as $method) {
|
||||
$method_name = $method->getName();
|
||||
if (strtolower($method_name) === 'index' || strpos($method_name, '__') === 0 || in_array($method_name, $no_need_auth)) {
|
||||
continue;
|
||||
}
|
||||
$name = "$class@$method_name";
|
||||
|
||||
$methods_in_files[$name] = $name;
|
||||
$title = Util::getCommentFirstLine($method->getDocComment()) ?: $method_name;
|
||||
$menu = $items[$name] ?? [];
|
||||
if ($menu) {
|
||||
if ($menu->title != $title) {
|
||||
Rule::where('key', $name)->update(['title' => $title]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$menu = new Rule;
|
||||
$menu->pid = $pid;
|
||||
$menu->key = $name;
|
||||
$menu->title = $title;
|
||||
$menu->type = 2;
|
||||
$menu->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
// 从数据库中删除已经不存在的方法
|
||||
$menu_names_to_del = array_diff($methods_in_db, $methods_in_files);
|
||||
if ($menu_names_to_del) {
|
||||
//Rule::whereIn('key', $menu_names_to_del)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询前置方法
|
||||
* @param Request $request
|
||||
* @return array
|
||||
* @throws BusinessException
|
||||
*/
|
||||
protected function selectInput(Request $request): array
|
||||
{
|
||||
[$where, $format, $limit, $field, $order] = parent::selectInput($request);
|
||||
// 允许通过type=0,1格式传递菜单类型
|
||||
$types = $request->get('type');
|
||||
if ($types && is_string($types)) {
|
||||
$where['type'] = ['in', explode(',', $types)];
|
||||
}
|
||||
// 默认weight排序
|
||||
if (!$field) {
|
||||
$field = 'weight';
|
||||
$order = 'desc';
|
||||
}
|
||||
return [$where, $format, $limit, $field, $order];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function insert(Request $request): Response
|
||||
{
|
||||
if ($request->method() === 'GET') {
|
||||
return raw_view('rule/insert');
|
||||
}
|
||||
$data = $this->insertInput($request);
|
||||
if (empty($data['type'])) {
|
||||
$data['type'] = strpos($data['key'], '\\') ? 1 : 0;
|
||||
}
|
||||
$data['key'] = str_replace('\\\\', '\\', $data['key']);
|
||||
$key = $data['key'] ?? '';
|
||||
if ($this->model->where('key', $key)->first()) {
|
||||
return $this->json(1, "菜单标识 $key 已经存在");
|
||||
}
|
||||
$data['pid'] = empty($data['pid']) ? 0 : $data['pid'];
|
||||
$this->doInsert($data);
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
if ($request->method() === 'GET') {
|
||||
return raw_view('rule/update');
|
||||
}
|
||||
[$id, $data] = $this->updateInput($request);
|
||||
if (!$row = $this->model->find($id)) {
|
||||
return $this->json(2, '记录不存在');
|
||||
}
|
||||
if (isset($data['pid'])) {
|
||||
$data['pid'] = $data['pid'] ?: 0;
|
||||
if ($data['pid'] == $row['id']) {
|
||||
return $this->json(2, '不能将自己设置为上级菜单');
|
||||
}
|
||||
}
|
||||
if (isset($data['key'])) {
|
||||
$data['key'] = str_replace('\\\\', '\\', $data['key']);
|
||||
}
|
||||
$this->doUpdate($id, $data);
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
$ids = $this->deleteInput($request);
|
||||
// 子规则一起删除
|
||||
$delete_ids = $children_ids = $ids;
|
||||
while($children_ids) {
|
||||
$children_ids = $this->model->whereIn('pid', $children_ids)->pluck('id')->toArray();
|
||||
$delete_ids = array_merge($delete_ids, $children_ids);
|
||||
}
|
||||
$this->doDelete($delete_ids);
|
||||
return $this->json(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除不包含某些数据的数组
|
||||
* @param $array
|
||||
* @param $key
|
||||
* @param $values
|
||||
* @return void
|
||||
*/
|
||||
protected function removeNotContain(&$array, $key, $values)
|
||||
{
|
||||
foreach ($array as $k => &$item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
if (!$this->arrayContain($item, $key, $values)) {
|
||||
unset($array[$k]);
|
||||
} else {
|
||||
if (!isset($item['children'])) {
|
||||
continue;
|
||||
}
|
||||
$this->removeNotContain($item['children'], $key, $values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断数组是否包含某些数据
|
||||
* @param $array
|
||||
* @param $key
|
||||
* @param $values
|
||||
* @return bool
|
||||
*/
|
||||
protected function arrayContain(&$array, $key, $values): bool
|
||||
{
|
||||
if (!is_array($array)) {
|
||||
return false;
|
||||
}
|
||||
if (isset($array[$key]) && in_array($array[$key], $values)) {
|
||||
return true;
|
||||
}
|
||||
if (!isset($array['children'])) {
|
||||
return false;
|
||||
}
|
||||
foreach ($array['children'] as $item) {
|
||||
if ($this->arrayContain($item, $key, $values)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限规则
|
||||
* @param $roles
|
||||
* @return array
|
||||
*/
|
||||
protected function getRules($roles): array
|
||||
{
|
||||
$rules_strings = $roles ? Role::whereIn('id', $roles)->pluck('rules') : [];
|
||||
$rules = [];
|
||||
foreach ($rules_strings as $rule_string) {
|
||||
if (!$rule_string) {
|
||||
continue;
|
||||
}
|
||||
$rules = array_merge($rules, explode(',', $rule_string));
|
||||
}
|
||||
return $rules;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,339 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\controller;
|
||||
|
||||
use Exception;
|
||||
use Intervention\Image\ImageManagerStatic as Image;
|
||||
use plugin\admin\app\model\Upload;
|
||||
use support\exception\BusinessException;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 附件管理
|
||||
*/
|
||||
class UploadController extends Crud
|
||||
{
|
||||
/**
|
||||
* @var Upload
|
||||
*/
|
||||
protected $model = null;
|
||||
|
||||
/**
|
||||
* 只返回当前管理员数据
|
||||
* @var string
|
||||
*/
|
||||
protected $dataLimit = 'personal';
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new Upload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览
|
||||
* @return Response
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function index(): Response
|
||||
{
|
||||
return raw_view('upload/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览附件
|
||||
* @return Response
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function attachment(): Response
|
||||
{
|
||||
return raw_view('upload/attachment');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询附件
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function select(Request $request): Response
|
||||
{
|
||||
[$where, $format, $limit, $field, $order] = $this->selectInput($request);
|
||||
if (!empty($where['ext']) && is_string($where['ext'])) {
|
||||
$where['ext'] = ['in', explode(',', $where['ext'])];
|
||||
}
|
||||
if (!empty($where['name']) && is_string($where['name'])) {
|
||||
$where['name'] = ['like', "%{$where['name']}%"];
|
||||
}
|
||||
$query = $this->doSelect($where, $field, $order);
|
||||
return $this->doFormat($query, $format, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新附件
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
if ($request->method() === 'GET') {
|
||||
return raw_view('upload/update');
|
||||
}
|
||||
return parent::update($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加附件
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception|Throwable
|
||||
*/
|
||||
public function insert(Request $request): Response
|
||||
{
|
||||
if ($request->method() === 'GET') {
|
||||
return raw_view('upload/insert');
|
||||
}
|
||||
$file = current($request->file());
|
||||
if (!$file || !$file->isValid()) {
|
||||
return $this->json(1, '未找到文件');
|
||||
}
|
||||
$data = $this->base($request, '/upload/files/' . date('Ymd'));
|
||||
$upload = new Upload;
|
||||
$upload->admin_id = admin_id();
|
||||
$upload->name = $data['name'];
|
||||
[
|
||||
$upload->url,
|
||||
$upload->name,
|
||||
$_,
|
||||
$upload->file_size,
|
||||
$upload->mime_type,
|
||||
$upload->image_width,
|
||||
$upload->image_height,
|
||||
$upload->ext
|
||||
] = array_values($data);
|
||||
$upload->category = $request->post('category');
|
||||
$upload->save();
|
||||
return $this->json(0, '上传成功', [
|
||||
'url' => $data['url'],
|
||||
'name' => $data['name'],
|
||||
'size' => $data['size'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
public function file(Request $request): Response
|
||||
{
|
||||
$file = current($request->file());
|
||||
if (!$file || !$file->isValid()) {
|
||||
return $this->json(1, '未找到文件');
|
||||
}
|
||||
$img_exts = [
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png',
|
||||
'gif'
|
||||
];
|
||||
if (in_array($file->getUploadExtension(), $img_exts)) {
|
||||
return $this->image($request);
|
||||
}
|
||||
$data = $this->base($request, '/upload/files/' . date('Ymd'));
|
||||
return $this->json(0, '上传成功', [
|
||||
'url' => $data['url'],
|
||||
'name' => $data['name'],
|
||||
'size' => $data['size'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
public function image(Request $request): Response
|
||||
{
|
||||
$data = $this->base($request, '/upload/img/' . date('Ymd'));
|
||||
$realpath = $data['realpath'];
|
||||
try {
|
||||
$img = Image::make($realpath);
|
||||
$max_height = 1170;
|
||||
$max_width = 1170;
|
||||
$width = $img->width();
|
||||
$height = $img->height();
|
||||
$ratio = 1;
|
||||
if ($height > $max_height || $width > $max_width) {
|
||||
$ratio = $width > $height ? $max_width / $width : $max_height / $height;
|
||||
}
|
||||
$img->resize($width * $ratio, $height * $ratio)->save($realpath);
|
||||
} catch (Exception $e) {
|
||||
unlink($realpath);
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '处理图片发生错误'
|
||||
]);
|
||||
}
|
||||
return json([
|
||||
'code' => 0,
|
||||
'msg' => '上传成功',
|
||||
'data' => [
|
||||
'url' => $data['url'],
|
||||
'name' => $data['name'],
|
||||
'size' => $data['size'],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传头像
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
public function avatar(Request $request): Response
|
||||
{
|
||||
$file = current($request->file());
|
||||
if ($file && $file->isValid()) {
|
||||
$ext = strtolower($file->getUploadExtension());
|
||||
if (!in_array($ext, ['jpg', 'jpeg', 'gif', 'png'])) {
|
||||
return json(['code' => 2, 'msg' => '仅支持 jpg jpeg gif png格式']);
|
||||
}
|
||||
$image = Image::make($file);
|
||||
$width = $image->width();
|
||||
$height = $image->height();
|
||||
$size = min($width, $height);
|
||||
$relative_path = 'upload/avatar/' . date('Ym');
|
||||
$real_path = base_path() . "/plugin/admin/public/$relative_path";
|
||||
if (!is_dir($real_path)) {
|
||||
mkdir($real_path, 0777, true);
|
||||
}
|
||||
$name = bin2hex(pack('Nn', time(), random_int(1, 65535)));
|
||||
$ext = $file->getUploadExtension();
|
||||
|
||||
$image->crop($size, $size)->resize(300, 300);
|
||||
$path = base_path() . "/plugin/admin/public/$relative_path/$name.lg.$ext";
|
||||
$image->save($path);
|
||||
|
||||
$image->resize(120, 120);
|
||||
$path = base_path() . "/plugin/admin/public/$relative_path/$name.md.$ext";
|
||||
$image->save($path);
|
||||
|
||||
$image->resize(60, 60);
|
||||
$path = base_path() . "/plugin/admin/public/$relative_path/$name.$ext";
|
||||
$image->save($path);
|
||||
|
||||
$image->resize(30, 30);
|
||||
$path = base_path() . "/plugin/admin/public/$relative_path/$name.sm.$ext";
|
||||
$image->save($path);
|
||||
|
||||
return json([
|
||||
'code' => 0,
|
||||
'msg' => '上传成功',
|
||||
'data' => [
|
||||
'url' => "/app/admin/$relative_path/$name.md.$ext"
|
||||
]
|
||||
]);
|
||||
}
|
||||
return json(['code' => 1, 'msg' => 'file not found']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除附件
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
$ids = $this->deleteInput($request);
|
||||
$primary_key = $this->model->getKeyName();
|
||||
$files = $this->model->whereIn($primary_key, $ids)->get()->toArray();
|
||||
$file_list = array_map(function ($item) {
|
||||
$path = $item['url'];
|
||||
if (preg_match("#^/app/admin#", $path)) {
|
||||
$admin_public_path = config('plugin.admin.app.public_path') ?: base_path() . "/plugin/admin/public";
|
||||
return $admin_public_path . str_replace("/app/admin", "", $item['url']);
|
||||
}
|
||||
return null;
|
||||
}, $files);
|
||||
$file_list = array_filter($file_list, function ($item) {
|
||||
return !empty($item);
|
||||
});
|
||||
$result = parent::delete($request);
|
||||
if (($res = json_decode($result->rawBody())) && $res->code === 0) {
|
||||
foreach ($file_list as $file) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上传数据
|
||||
* @param Request $request
|
||||
* @param $relative_dir
|
||||
* @return array
|
||||
* @throws BusinessException|\Random\RandomException
|
||||
*/
|
||||
protected function base(Request $request, $relative_dir): array
|
||||
{
|
||||
$relative_dir = ltrim($relative_dir, '\\/');
|
||||
$file = current($request->file());
|
||||
if (!$file || !$file->isValid()) {
|
||||
throw new BusinessException('未找到上传文件', 400);
|
||||
}
|
||||
|
||||
$admin_public_path = rtrim(config('plugin.admin.app.public_path', ''), '\\/');
|
||||
$base_dir = $admin_public_path ? $admin_public_path . DIRECTORY_SEPARATOR : base_path() . '/plugin/admin/public/';
|
||||
$full_dir = $base_dir . $relative_dir;
|
||||
if (!is_dir($full_dir)) {
|
||||
mkdir($full_dir, 0777, true);
|
||||
}
|
||||
|
||||
$ext = $file->getUploadExtension() ?: null;
|
||||
$mime_type = $file->getUploadMimeType();
|
||||
$file_name = $file->getUploadName();
|
||||
$file_size = $file->getSize();
|
||||
|
||||
if (!$ext && $file_name === 'blob') {
|
||||
[$___image, $ext] = explode('/', $mime_type);
|
||||
unset($___image);
|
||||
}
|
||||
|
||||
$ext = strtolower($ext);
|
||||
$ext_forbidden_map = ['php', 'php3', 'php5', 'css', 'js', 'html', 'htm', 'asp', 'jsp'];
|
||||
if (in_array($ext, $ext_forbidden_map)) {
|
||||
throw new BusinessException('不支持该格式的文件上传', 400);
|
||||
}
|
||||
|
||||
$relative_path = $relative_dir . '/' . bin2hex(pack('Nn', time(), random_int(1, 65535))) . ".$ext";
|
||||
$full_path = $base_dir . $relative_path;
|
||||
$file->move($full_path);
|
||||
$image_with = $image_height = 0;
|
||||
if ($img_info = getimagesize($full_path)) {
|
||||
[$image_with, $image_height] = $img_info;
|
||||
$mime_type = $img_info['mime'];
|
||||
}
|
||||
return [
|
||||
'url' => "/app/admin/$relative_path",
|
||||
'name' => $file_name,
|
||||
'realpath' => $full_path,
|
||||
'size' => $file_size,
|
||||
'mime_type' => $mime_type,
|
||||
'image_with' => $image_with,
|
||||
'image_height' => $image_height,
|
||||
'ext' => $ext,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\controller;
|
||||
|
||||
use plugin\admin\app\model\User;
|
||||
use support\exception\BusinessException;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 用户管理
|
||||
*/
|
||||
class UserController extends Crud
|
||||
{
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
protected $model = null;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new User;
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览
|
||||
* @return Response
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function index(): Response
|
||||
{
|
||||
return raw_view('user/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function insert(Request $request): Response
|
||||
{
|
||||
if ($request->method() === 'POST') {
|
||||
return parent::insert($request);
|
||||
}
|
||||
return raw_view('user/insert');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
if ($request->method() === 'POST') {
|
||||
return parent::update($request);
|
||||
}
|
||||
return raw_view('user/update');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace plugin\admin\app\exception;
|
||||
|
||||
use Throwable;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
|
||||
/**
|
||||
* Class Handler
|
||||
* @package support\exception
|
||||
*/
|
||||
class Handler extends \support\exception\Handler
|
||||
{
|
||||
public function render(Request $request, Throwable $exception): Response
|
||||
{
|
||||
$code = $exception->getCode();
|
||||
$debug = $this->_debug ?? $this->debug;
|
||||
if ($request->expectsJson()) {
|
||||
$json = ['code' => $code ?: 500, 'msg' => $debug ? $exception->getMessage() : 'Server internal error', 'type' => 'failed'];
|
||||
$debug && $json['traces'] = (string)$exception;
|
||||
return new Response(200, ['Content-Type' => 'application/json'],
|
||||
\json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
$error = $debug ? \nl2br((string)$exception) : 'Server internal error';
|
||||
return new Response(500, [], $error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/**
|
||||
* Here is your custom functions.
|
||||
*/
|
||||
|
||||
use plugin\admin\app\model\Admin;
|
||||
use plugin\admin\app\model\AdminRole;
|
||||
use support\Response;
|
||||
|
||||
/**
|
||||
* 当前管理员id
|
||||
* @return integer|null
|
||||
*/
|
||||
function admin_id(): ?int
|
||||
{
|
||||
return session('admin.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前管理员
|
||||
* @param null|array|string $fields
|
||||
* @return array|mixed|null
|
||||
* @throws Exception
|
||||
*/
|
||||
function admin($fields = null)
|
||||
{
|
||||
refresh_admin_session();
|
||||
if (!$admin = session('admin')) {
|
||||
return null;
|
||||
}
|
||||
if ($fields === null) {
|
||||
return $admin;
|
||||
}
|
||||
if (is_array($fields)) {
|
||||
$results = [];
|
||||
foreach ($fields as $field) {
|
||||
$results[$field] = $admin[$field] ?? null;
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
return $admin[$fields] ?? null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 刷新当前管理员session
|
||||
* @param bool $force
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
function refresh_admin_session(bool $force = false)
|
||||
{
|
||||
$admin_session = session('admin');
|
||||
if (!$admin_session) {
|
||||
return null;
|
||||
}
|
||||
$admin_id = $admin_session['id'];
|
||||
$time_now = time();
|
||||
// session在2秒内不刷新
|
||||
$session_ttl = 2;
|
||||
$session_last_update_time = session('admin.session_last_update_time', 0);
|
||||
if (!$force && $time_now - $session_last_update_time < $session_ttl) {
|
||||
return null;
|
||||
}
|
||||
$session = request()->session();
|
||||
$admin = Admin::find($admin_id);
|
||||
if (!$admin) {
|
||||
$session->forget('admin');
|
||||
return null;
|
||||
}
|
||||
$admin = $admin->toArray();
|
||||
$admin['password'] = md5($admin['password']);
|
||||
$admin_session['password'] = $admin_session['password'] ?? '';
|
||||
if ($admin['password'] != $admin_session['password']) {
|
||||
$session->forget('admin');
|
||||
return null;
|
||||
}
|
||||
// 账户被禁用
|
||||
if ($admin['status'] != 0) {
|
||||
$session->forget('admin');
|
||||
return;
|
||||
}
|
||||
$admin['roles'] = AdminRole::where('admin_id', $admin_id)->pluck('role_id')->toArray();
|
||||
$admin['session_last_update_time'] = $time_now;
|
||||
$session->set('admin', $admin);
|
||||
}
|
||||
|
||||
function admin_error_401_script(): Response
|
||||
{
|
||||
return response(<<<EOF
|
||||
<script>top.location.href = '/app/admin';</script>
|
||||
EOF
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
namespace plugin\admin\app\middleware;
|
||||
|
||||
use plugin\admin\api\Auth;
|
||||
use ReflectionException;
|
||||
use support\exception\BusinessException;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use Webman\MiddlewareInterface;
|
||||
|
||||
class AccessControl implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param callable $handler
|
||||
* @return Response
|
||||
* @throws ReflectionException|BusinessException
|
||||
*/
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$controller = $request->controller;
|
||||
$action = $request->action;
|
||||
|
||||
$code = 0;
|
||||
$msg = '';
|
||||
if (!Auth::canAccess($controller, $action, $code, $msg)) {
|
||||
if ($request->expectsJson()) {
|
||||
$response = json(['code' => $code, 'msg' => $msg, 'data' => []]);
|
||||
} else {
|
||||
if ($code === 401) {
|
||||
$response = admin_error_401_script();
|
||||
} else {
|
||||
$request->app = '';
|
||||
$request->plugin = 'admin';
|
||||
$response = view('common/error/403')->withStatus(403);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
$response = $request->method() == 'OPTIONS' ? response('') : $handler($request);
|
||||
}
|
||||
|
||||
return $response;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\model;
|
||||
|
||||
use plugin\admin\app\model\Base;
|
||||
|
||||
/**
|
||||
* @property integer $id ID(主键)
|
||||
* @property string $username 用户名
|
||||
* @property string $nickname 昵称
|
||||
* @property string $password 密码
|
||||
* @property string $avatar 头像
|
||||
* @property string $email 邮箱
|
||||
* @property string $mobile 手机
|
||||
* @property string $created_at 创建时间
|
||||
* @property string $updated_at 更新时间
|
||||
* @property string $login_at 登录时间
|
||||
* @property string $roles 角色
|
||||
* @property integer $status 状态 0正常 1禁用
|
||||
*/
|
||||
class Admin extends Base
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'wa_admins';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\model;
|
||||
|
||||
use plugin\admin\app\model\Base;
|
||||
|
||||
/**
|
||||
* @property integer $id ID(主键)
|
||||
* @property string $admin_id 管理员id
|
||||
* @property string $role_id 角色id
|
||||
*/
|
||||
class AdminRole extends Base
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'wa_admin_roles';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\model;
|
||||
|
||||
use DateTimeInterface;
|
||||
use support\Model;
|
||||
|
||||
|
||||
class Base extends Model
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $connection = 'plugin.admin.mysql';
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*
|
||||
* @param DateTimeInterface $date
|
||||
* @return string
|
||||
*/
|
||||
protected function serializeDate(DateTimeInterface $date)
|
||||
{
|
||||
return $date->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\model;
|
||||
|
||||
|
||||
use support\exception\BusinessException;
|
||||
|
||||
/**
|
||||
* 字典相关
|
||||
*/
|
||||
class Dict
|
||||
{
|
||||
/**
|
||||
* 获取字典
|
||||
* @param $name
|
||||
* @return mixed|null
|
||||
*/
|
||||
public static function get($name)
|
||||
{
|
||||
$value = Option::where('name', static::dictNameToOptionName($name))->value('value');
|
||||
return $value ? json_decode($value, true) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存字典
|
||||
* @param $name
|
||||
* @param $values
|
||||
* @return void
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function save($name, $values)
|
||||
{
|
||||
if (!preg_match('/[a-zA-Z]/', $name)) {
|
||||
throw new BusinessException('字典名只能包含字母');
|
||||
}
|
||||
$option_name = static::dictNameToOptionName($name);
|
||||
if (!$option = Option::where('name', $option_name)->first()) {
|
||||
$option = new Option;
|
||||
}
|
||||
$format_values = static::filterValue($values);
|
||||
$option->name = $option_name;
|
||||
$option->value = json_encode($format_values, JSON_UNESCAPED_UNICODE);
|
||||
$option->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字典
|
||||
* @param array $names
|
||||
* @return void
|
||||
*/
|
||||
public static function delete(array $names)
|
||||
{
|
||||
foreach ($names as $index => $name) {
|
||||
$names[$index] = static::dictNameToOptionName($name);
|
||||
}
|
||||
Option::whereIn('name', $names)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典名到option名转换
|
||||
* @param string $name
|
||||
* @return string
|
||||
*/
|
||||
public static function dictNameToOptionName(string $name): string
|
||||
{
|
||||
return "dict_$name";
|
||||
}
|
||||
|
||||
/**
|
||||
* option名到字典名转换
|
||||
* @param string $name
|
||||
* @return string
|
||||
*/
|
||||
public static function optionNameToDictName(string $name): string
|
||||
{
|
||||
return substr($name, 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤值
|
||||
* @param array $values
|
||||
* @return array
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function filterValue(array $values): array
|
||||
{
|
||||
$format_values = [];
|
||||
foreach ($values as $item) {
|
||||
if (!isset($item['value']) || !isset($item['name'])) {
|
||||
throw new BusinessException('字典格式错误', 1);
|
||||
}
|
||||
$format_values[] = ['value' => $item['value'], 'name' => $item['name']];
|
||||
}
|
||||
return $format_values;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\model;
|
||||
|
||||
|
||||
/**
|
||||
* @property integer $id (主键)
|
||||
* @property string $name 键
|
||||
* @property mixed $value 值
|
||||
* @property string $created_at 创建时间
|
||||
* @property string $updated_at 更新时间
|
||||
*/
|
||||
class Option extends Base
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'wa_options';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\model;
|
||||
|
||||
|
||||
/**
|
||||
* @property integer $id 主键(主键)
|
||||
* @property string $name 角色名
|
||||
* @property string $rules 权限
|
||||
* @property string $created_at 创建时间
|
||||
* @property string $updated_at 更新时间
|
||||
* @property integer $pid 上级id
|
||||
*/
|
||||
class Role extends Base
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'wa_roles';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRuleIds()
|
||||
{
|
||||
return $this->rules ? explode(',', $this->rules) : [];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\model;
|
||||
|
||||
use plugin\admin\app\model\Base;
|
||||
|
||||
/**
|
||||
* @property integer $id 主键(主键)
|
||||
* @property string $title 标题
|
||||
* @property string $icon 图标
|
||||
* @property string $key 标识
|
||||
* @property integer $pid 上级菜单
|
||||
* @property string $created_at 创建时间
|
||||
* @property string $updated_at 更新时间
|
||||
* @property string $href url
|
||||
* @property integer $type 类型
|
||||
* @property integer $weight 排序
|
||||
*/
|
||||
class Rule extends Base
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'wa_rules';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\model;
|
||||
|
||||
use plugin\admin\app\model\Base;
|
||||
|
||||
/**
|
||||
* @property integer $id 主键(主键)
|
||||
* @property string $name 名称
|
||||
* @property string $url url
|
||||
* @property integer $admin_id 管理员
|
||||
* @property integer $user_id 用户
|
||||
* @property integer $file_size 文件大小
|
||||
* @property string $mime_type mime类型
|
||||
* @property integer $image_width 图片宽度
|
||||
* @property integer $image_height 图片高度
|
||||
* @property string $ext 扩展名
|
||||
* @property string $storage 存储位置
|
||||
* @property string $created_at 上传时间
|
||||
* @property string $category 类别
|
||||
* @property string $updated_at 更新时间
|
||||
*/
|
||||
class Upload extends Base
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'wa_uploads';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\admin\app\model;
|
||||
|
||||
use plugin\admin\app\model\Base;
|
||||
|
||||
/**
|
||||
* @property integer $id 主键(主键)
|
||||
* @property string $username 用户名
|
||||
* @property string $nickname 昵称
|
||||
* @property string $password 密码
|
||||
* @property string $sex 性别
|
||||
* @property string $avatar 头像
|
||||
* @property string $email 邮箱
|
||||
* @property string $mobile 手机
|
||||
* @property integer $level 等级
|
||||
* @property string $birthday 生日
|
||||
* @property integer $money 余额
|
||||
* @property integer $score 积分
|
||||
* @property string $last_time 登录时间
|
||||
* @property string $last_ip 登录ip
|
||||
* @property string $join_time 注册时间
|
||||
* @property string $join_ip 注册ip
|
||||
* @property string $token token
|
||||
* @property string $created_at 创建时间
|
||||
* @property string $updated_at 更新时间
|
||||
* @property integer $role 角色
|
||||
* @property integer $status 禁用
|
||||
*/
|
||||
class User extends Base
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'wa_users';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
<link rel="stylesheet" href="/app/admin/component/layui/css/layui.css?v=2.8.12" />
|
||||
<link rel="stylesheet" href="/app/admin/component/pear/css/pear.css" />
|
||||
<link rel="stylesheet" href="/app/admin/admin/css/reset.css" />
|
||||
</head>
|
||||
<body class="pear-container">
|
||||
<style>
|
||||
.layui-input-block input {
|
||||
width: 300px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-body">
|
||||
|
||||
<div class="layui-tab layui-tab-brief">
|
||||
<ul class="layui-tab-title">
|
||||
<li class="layui-this">基本信息</li>
|
||||
<li>安全设置</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<div class="layui-tab-item layui-show">
|
||||
|
||||
<form class="layui-form" lay-filter="baseInfo">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">昵称</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="nickname" required lay-verify="required" placeholder="请输入昵称" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">邮箱</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="email" placeholder="请输入邮箱" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">联系电话</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="mobile" placeholder="请输入联系电话" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button type="submit" class="pear-btn pear-btn-primary pear-btn-md" lay-submit="" lay-filter="saveBaseInfo">
|
||||
提交
|
||||
</button>
|
||||
<button type="reset" class="pear-btn pear-btn-md">
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="layui-tab-item">
|
||||
|
||||
<form class="layui-form" action="">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">原始密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="password" name="old_password" required lay-verify="required" placeholder="请输入原始密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">新密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="password" name="password" required lay-verify="required" placeholder="请输入新密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">确认新密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="password" name="password_confirm" required lay-verify="required" placeholder="请再次输入新密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button type="submit" class="pear-btn pear-btn-primary pear-btn-md" lay-submit="" lay-filter="savePassword">
|
||||
提交
|
||||
</button>
|
||||
<button type="reset" class="pear-btn pear-btn-md">
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="/app/admin/component/layui/layui.js?v=2.8.12"></script>
|
||||
<script src="/app/admin/component/pear/pear.js"></script>
|
||||
<script src="/app/admin/admin/js/permission.js"></script>
|
||||
<script>
|
||||
|
||||
layui.use(["form", "popup"], function () {
|
||||
let form = layui.form;
|
||||
let $ = layui.$;
|
||||
$.ajax({
|
||||
url: "/app/admin/account/info",
|
||||
dataType: "json",
|
||||
success: function (res) {
|
||||
form.val("baseInfo", res.data);
|
||||
}
|
||||
});
|
||||
|
||||
form.on("submit(saveBaseInfo)", function(data){
|
||||
$.ajax({
|
||||
url: "/app/admin/account/update",
|
||||
dataType: "json",
|
||||
type: "POST",
|
||||
data: data.field,
|
||||
success: function (res) {
|
||||
if (res.code) {
|
||||
return layui.popup.failure(res.msg);
|
||||
}
|
||||
return layui.popup.success("操作成功");
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
form.on("submit(savePassword)", function(data){
|
||||
$.ajax({
|
||||
url: "/app/admin/account/password",
|
||||
dataType: "json",
|
||||
type: "POST",
|
||||
data: data.field,
|
||||
success: function (res) {
|
||||
if (res.code) {
|
||||
return layui.popup.failure(res.msg);
|
||||
}
|
||||
return layui.popup.success("操作成功");
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,80 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>登录</title>
|
||||
<!-- 样 式 文 件 -->
|
||||
<link rel="stylesheet" href="/app/admin/component/pear/css/pear.css" />
|
||||
<link rel="stylesheet" href="/app/admin/admin/css/pages/login.css" />
|
||||
</head>
|
||||
<!-- 代 码 结 构 -->
|
||||
<body background="/app/admin/admin/images/background.svg" style="background-size: cover;">
|
||||
<form class="layui-form">
|
||||
<div class="layui-form-item">
|
||||
<img class="logo" src="<?=$logo?>" />
|
||||
<div class="title pear-text"><?=$title?></div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<input lay-verify="required" hover class="layui-input" type="text" name="username" value="" placeholder="用户名" />
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<input lay-verify="required" hover class="layui-input" type="password" name="password" value="" placeholder="密码" />
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<input hover lay-verify="required" class="code layui-input layui-input-inline" name="captcha" placeholder="验证码" />
|
||||
<img class="codeImage" width="120px"/>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<button type="submit" class="pear-btn pear-btn-primary login" lay-submit lay-filter="login">
|
||||
登 入
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
var color = localStorage.getItem("theme-color-color");
|
||||
var second = localStorage.getItem("theme-color-second");
|
||||
if (!color || !second) {
|
||||
localStorage.setItem("theme-color-color", "#2d8cf0");
|
||||
localStorage.setItem("theme-color-second", "#ecf5ff");
|
||||
}
|
||||
</script>
|
||||
<!-- 资 源 引 入 -->
|
||||
<script src="/app/admin/component/layui/layui.js?v=2.8.12"></script>
|
||||
<script src="/app/admin/component/pear/pear.js"></script>
|
||||
<script>
|
||||
layui.use(['form', 'button', 'popup', 'layer', 'theme', 'admin'], function() {
|
||||
|
||||
var $ = layui.$, layer = layui.layer, form = layui.form;
|
||||
function switchCaptcha() {
|
||||
$('.codeImage').attr("src", "/app/admin/account/captcha/login?v=" + new Date().getTime());
|
||||
}
|
||||
switchCaptcha();
|
||||
// 登 录 提 交
|
||||
form.on('submit(login)', function (data) {
|
||||
layer.load();
|
||||
$.ajax({
|
||||
url: '/app/admin/account/login',
|
||||
type: "POST",
|
||||
data: data.field,
|
||||
success: function (res) {
|
||||
layer.closeAll('loading');
|
||||
if (!res.code) {
|
||||
layui.popup.success('登录成功', function () {
|
||||
location.reload();
|
||||
})
|
||||
} else {
|
||||
layui.popup.failure(res.msg)
|
||||
switchCaptcha();
|
||||
}
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
$('.codeImage').on('click', function () {
|
||||
switchCaptcha();
|
||||
});
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,414 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>浏览页面</title>
|
||||
<link rel="stylesheet" href="/app/admin/component/pear/css/pear.css" />
|
||||
<link rel="stylesheet" href="/app/admin/admin/css/reset.css" />
|
||||
</head>
|
||||
<body class="pear-container">
|
||||
|
||||
<!-- 顶部查询表单 -->
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form top-search-from">
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">用户名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="username" value="" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">昵称</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="nickname" value="" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">邮箱</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="email" value="" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">手机</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="mobile" value="" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">创建时间</label>
|
||||
<div class="layui-input-block">
|
||||
<div class="layui-input-block" id="created_at">
|
||||
<input type="text" autocomplete="off" name="created_at[]" id="created_at-date-start" class="layui-input inline-block" placeholder="开始时间">
|
||||
-
|
||||
<input type="text" autocomplete="off" name="created_at[]" id="created_at-date-end" class="layui-input inline-block" placeholder="结束时间">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item layui-inline">
|
||||
<label class="layui-form-label"></label>
|
||||
<button class="pear-btn pear-btn-md pear-btn-primary" lay-submit lay-filter="table-query">
|
||||
<i class="layui-icon layui-icon-search"></i>查询
|
||||
</button>
|
||||
<button type="reset" class="pear-btn pear-btn-md" lay-submit lay-filter="table-reset">
|
||||
<i class="layui-icon layui-icon-refresh"></i>重置
|
||||
</button>
|
||||
</div>
|
||||
<div class="toggle-btn">
|
||||
<a class="layui-hide">展开<i class="layui-icon layui-icon-down"></i></a>
|
||||
<a class="layui-hide">收起<i class="layui-icon layui-icon-up"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-body">
|
||||
<table id="data-table" lay-filter="data-table"></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格顶部工具栏 -->
|
||||
<script type="text/html" id="table-toolbar">
|
||||
<button class="pear-btn pear-btn-primary pear-btn-md" lay-event="add" permission="app.admin.admin.insert">
|
||||
<i class="layui-icon layui-icon-add-1"></i>新增
|
||||
</button>
|
||||
<button class="pear-btn pear-btn-danger pear-btn-md" lay-event="batchRemove" permission="app.admin.admin.delete">
|
||||
<i class="layui-icon layui-icon-delete"></i>删除
|
||||
</button>
|
||||
</script>
|
||||
|
||||
<!-- 表格行工具栏 -->
|
||||
<script type="text/html" id="table-bar">
|
||||
{{# if(d.show_toolbar){ }}
|
||||
<button class="pear-btn pear-btn-xs tool-btn" lay-event="edit" permission="app.admin.admin.update">编辑</button>
|
||||
<button class="pear-btn pear-btn-xs tool-btn" lay-event="remove" permission="app.admin.admin.delete">删除</button>
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
<script src="/app/admin/component/layui/layui.js?v=2.8.12"></script>
|
||||
<script src="/app/admin/component/pear/pear.js"></script>
|
||||
<script src="/app/admin/admin/js/permission.js"></script>
|
||||
<script src="/app/admin/admin/js/common.js"></script>
|
||||
<script>
|
||||
|
||||
// 相关常量
|
||||
const PRIMARY_KEY = "id";
|
||||
const SELECT_API = "/app/admin/admin/select";
|
||||
const UPDATE_API = "/app/admin/admin/update";
|
||||
const DELETE_API = "/app/admin/admin/delete";
|
||||
const INSERT_URL = "/app/admin/admin/insert";
|
||||
const UPDATE_URL = "/app/admin/admin/update";
|
||||
|
||||
// 字段 创建时间 created_at
|
||||
layui.use(["laydate"], function() {
|
||||
layui.laydate.render({
|
||||
elem: "#created_at",
|
||||
range: ["#created_at-date-start", "#created_at-date-end"],
|
||||
});
|
||||
})
|
||||
|
||||
// 表格渲染
|
||||
layui.use(["table", "form", "common", "popup", "util"], function() {
|
||||
let table = layui.table;
|
||||
let form = layui.form;
|
||||
let $ = layui.$;
|
||||
let common = layui.common;
|
||||
let util = layui.util;
|
||||
|
||||
// 表头参数
|
||||
let cols = [
|
||||
{
|
||||
type: "checkbox"
|
||||
},{
|
||||
title: "ID",
|
||||
field: "id",
|
||||
width: 100,
|
||||
sort: true,
|
||||
},{
|
||||
title: "用户名",
|
||||
field: "username",
|
||||
},{
|
||||
title: "昵称",
|
||||
field: "nickname",
|
||||
},{
|
||||
title: "密码",
|
||||
field: "password",
|
||||
hide: true,
|
||||
},{
|
||||
title: "头像",
|
||||
field: "avatar",
|
||||
templet: function (d) {
|
||||
return '<img src="'+encodeURI(d['avatar'])+'" style="max-width:32px;max-height:32px;" alt="" />'
|
||||
},
|
||||
width: 90,
|
||||
},{
|
||||
title: "邮箱",
|
||||
field: "email",
|
||||
hide: true,
|
||||
},{
|
||||
title: "手机",
|
||||
field: "mobile",
|
||||
hide: true,
|
||||
},{
|
||||
title: "创建时间",
|
||||
field: "created_at",
|
||||
hide: true,
|
||||
},{
|
||||
title: "更新时间",
|
||||
field: "updated_at",
|
||||
hide: true,
|
||||
},{
|
||||
title: "登录时间",
|
||||
field: "login_at",
|
||||
},{
|
||||
title: "角色",
|
||||
field: "roles",
|
||||
templet: function (d) {
|
||||
let field = "roles";
|
||||
if (typeof d[field] == "undefined") return "";
|
||||
let items = [];
|
||||
layui.each((d[field] + "").split(","), function (k , v) {
|
||||
items.push(apiResults[field][v] || v);
|
||||
});
|
||||
return util.escape(items.join(","));
|
||||
}
|
||||
},{
|
||||
title: "禁用",
|
||||
field: "status",
|
||||
templet: function (d) {
|
||||
let field = "status";
|
||||
form.on("switch("+field+")", function (data) {
|
||||
let load = layer.load();
|
||||
let postData = {};
|
||||
postData[field] = data.elem.checked ? 1 : 0;
|
||||
postData[PRIMARY_KEY] = this.value;
|
||||
$.post(UPDATE_API, postData, function (res) {
|
||||
layer.close(load);
|
||||
if (res.code) {
|
||||
return layui.popup.failure(res.msg, function () {
|
||||
data.elem.checked = !data.elem.checked;
|
||||
form.render();
|
||||
});
|
||||
}
|
||||
return layui.popup.success("操作成功");
|
||||
})
|
||||
});
|
||||
let checked = d[field] === 1 ? "checked" : "";
|
||||
if (parent.Admin.Account.id === d.id) return '';
|
||||
return '<input type="checkbox" value="'+util.escape(d[PRIMARY_KEY])+'" lay-filter="'+util.escape(field)+'" lay-skin="switch" lay-text="'+util.escape('')+'" '+checked+'/>';
|
||||
},
|
||||
width: 90,
|
||||
},{
|
||||
title: "操作",
|
||||
toolbar: "#table-bar",
|
||||
align: "center",
|
||||
fixed: "right",
|
||||
width: 130,
|
||||
}
|
||||
];
|
||||
|
||||
// 渲染表格
|
||||
function render()
|
||||
{
|
||||
table.render({
|
||||
elem: "#data-table",
|
||||
url: SELECT_API,
|
||||
page: true,
|
||||
cols: [cols],
|
||||
skin: "line",
|
||||
size: "lg",
|
||||
toolbar: "#table-toolbar",
|
||||
autoSort: false,
|
||||
defaultToolbar: [{
|
||||
title: "刷新",
|
||||
layEvent: "refresh",
|
||||
icon: "layui-icon-refresh",
|
||||
}, "filter", "print", "exports"],
|
||||
done: function () {
|
||||
layer.photos({photos: 'div[lay-id="data-table"]', anim: 5});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取表格中下拉或树形组件数据
|
||||
let apis = [];
|
||||
apis.push(["roles", "/app/admin/role/select?format=select"]);
|
||||
let apiResults = {};
|
||||
apiResults["roles"] = [];
|
||||
let count = apis.length;
|
||||
layui.each(apis, function (k, item) {
|
||||
let [field, url] = item;
|
||||
$.ajax({
|
||||
url: url,
|
||||
dateType: "json",
|
||||
success: function (res) {
|
||||
if (res.code) {
|
||||
return layui.popup.failure(res.msg);
|
||||
}
|
||||
function travel(items) {
|
||||
for (let k in items) {
|
||||
let item = items[k];
|
||||
apiResults[field][item.value] = item.name;
|
||||
if (item.children) {
|
||||
travel(item.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
travel(res.data);
|
||||
},
|
||||
complete: function () {
|
||||
if (--count === 0) {
|
||||
render();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if (!count) {
|
||||
render();
|
||||
}
|
||||
|
||||
// 编辑或删除行事件
|
||||
table.on("tool(data-table)", function(obj) {
|
||||
if (obj.event === "remove") {
|
||||
remove(obj);
|
||||
} else if (obj.event === "edit") {
|
||||
edit(obj);
|
||||
}
|
||||
});
|
||||
|
||||
// 表格顶部工具栏事件
|
||||
table.on("toolbar(data-table)", function(obj) {
|
||||
if (obj.event === "add") {
|
||||
add();
|
||||
} else if (obj.event === "refresh") {
|
||||
refreshTable();
|
||||
} else if (obj.event === "batchRemove") {
|
||||
batchRemove(obj);
|
||||
}
|
||||
});
|
||||
|
||||
// 表格顶部搜索事件
|
||||
form.on("submit(table-query)", function(data) {
|
||||
table.reload("data-table", {
|
||||
page: {
|
||||
curr: 1
|
||||
},
|
||||
where: data.field
|
||||
})
|
||||
return false;
|
||||
});
|
||||
|
||||
// 表格顶部搜索重置事件
|
||||
form.on("submit(table-reset)", function(data) {
|
||||
table.reload("data-table", {
|
||||
where: []
|
||||
})
|
||||
});
|
||||
|
||||
// 表格排序事件
|
||||
table.on("sort(data-table)", function(obj){
|
||||
table.reload("data-table", {
|
||||
initSort: obj,
|
||||
scrollPos: "fixed",
|
||||
where: {
|
||||
field: obj.field,
|
||||
order: obj.type
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 表格新增数据
|
||||
let add = function() {
|
||||
layer.open({
|
||||
type: 2,
|
||||
title: "新增",
|
||||
shade: 0.1,
|
||||
area: [common.isModile()?"100%":"500px", common.isModile()?"100%":"450px"],
|
||||
content: INSERT_URL
|
||||
});
|
||||
}
|
||||
|
||||
// 表格编辑数据
|
||||
let edit = function(obj) {
|
||||
let value = obj.data[PRIMARY_KEY];
|
||||
layer.open({
|
||||
type: 2,
|
||||
title: "修改",
|
||||
shade: 0.1,
|
||||
area: [common.isModile()?"100%":"500px", common.isModile()?"100%":"450px"],
|
||||
content: UPDATE_URL + "?" + PRIMARY_KEY + "=" + value
|
||||
});
|
||||
}
|
||||
|
||||
// 删除一行
|
||||
let remove = function(obj) {
|
||||
return doRemove(obj.data[PRIMARY_KEY]);
|
||||
}
|
||||
|
||||
// 删除多行
|
||||
let batchRemove = function(obj) {
|
||||
let checkIds = common.checkField(obj, PRIMARY_KEY);
|
||||
if (checkIds === "") {
|
||||
layui.popup.warning("未选中数据");
|
||||
return false;
|
||||
}
|
||||
doRemove(checkIds.split(","));
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
let doRemove = function (ids) {
|
||||
let data = {};
|
||||
data[PRIMARY_KEY] = ids;
|
||||
layer.confirm("确定删除?", {
|
||||
icon: 3,
|
||||
title: "提示"
|
||||
}, function(index) {
|
||||
layer.close(index);
|
||||
let loading = layer.load();
|
||||
$.ajax({
|
||||
url: DELETE_API,
|
||||
data: data,
|
||||
dataType: "json",
|
||||
type: "post",
|
||||
success: function(res) {
|
||||
layer.close(loading);
|
||||
if (res.code) {
|
||||
return layui.popup.failure(res.msg);
|
||||
}
|
||||
return layui.popup.success("操作成功", refreshTable);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// 刷新表格数据
|
||||
window.refreshTable = function() {
|
||||
table.reloadData("data-table", {
|
||||
scrollPos: "fixed",
|
||||
done: function (res, curr) {
|
||||
if (curr > 1 && res.data && !res.data.length) {
|
||||
curr = curr - 1;
|
||||
table.reloadData("data-table", {
|
||||
page: {
|
||||
curr: curr
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,181 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>新增页面</title>
|
||||
<link rel="stylesheet" href="/app/admin/component/pear/css/pear.css" />
|
||||
<link rel="stylesheet" href="/app/admin/admin/css/reset.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<form class="layui-form" action="">
|
||||
|
||||
<div class="mainBox">
|
||||
<div class="main-container mr-5">
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label required">角色</label>
|
||||
<div class="layui-input-block">
|
||||
<div name="roles" id="roles" value=""></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label required">用户名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="username" value="" required lay-verify="required" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label required">昵称</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="nickname" value="" required lay-verify="required" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label required">密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="password" value="" required lay-verify="required" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">头像</label>
|
||||
<div class="layui-input-block">
|
||||
<img class="img-3" src=""/>
|
||||
<input type="text" style="display:none" name="avatar" value="/app/admin/avatar.png" />
|
||||
<button type="button" class="pear-btn pear-btn-primary pear-btn-sm" id="avatar">
|
||||
<i class="layui-icon layui-icon-upload"></i>上传图片
|
||||
</button>
|
||||
<button type="button" class="pear-btn pear-btn-primary pear-btn-sm" id="attachment-choose-avatar">
|
||||
<i class="layui-icon layui-icon-align-left"></i>选择图片
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">邮箱</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="email" value="" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">手机</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="mobile" value="" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
<div class="button-container">
|
||||
<button type="submit" class="pear-btn pear-btn-primary pear-btn-md" lay-submit=""
|
||||
lay-filter="save">
|
||||
提交
|
||||
</button>
|
||||
<button type="reset" class="pear-btn pear-btn-md">
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<script src="/app/admin/component/layui/layui.js?v=2.8.12"></script>
|
||||
<script src="/app/admin/component/pear/pear.js"></script>
|
||||
<script src="/app/admin/admin/js/permission.js"></script>
|
||||
<script>
|
||||
|
||||
// 相关接口
|
||||
const INSERT_API = "/app/admin/admin/insert";
|
||||
|
||||
// 字段 头像 avatar
|
||||
layui.use(["upload", "layer"], function() {
|
||||
let input = layui.$("#avatar").prev();
|
||||
input.prev().attr("src", input.val());
|
||||
layui.$("#attachment-choose-avatar").on("click", function() {
|
||||
parent.layer.open({
|
||||
type: 2,
|
||||
title: "选择附件",
|
||||
content: "/app/admin/upload/attachment?ext=jpg,jpeg,png,gif,bmp",
|
||||
area: ["95%", "90%"],
|
||||
success: function (layero, index) {
|
||||
parent.layui.$("#layui-layer" + index).data("callback", function (data) {
|
||||
input.val(data.url).prev().attr("src", data.url);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
layui.upload.render({
|
||||
elem: "#avatar",
|
||||
url: "/app/admin/upload/avatar",
|
||||
value: "/app/admin/avatar.png",
|
||||
acceptMime: "image/gif,image/jpeg,image/jpg,image/png",
|
||||
field: "__file__",
|
||||
done: function (res) {
|
||||
if (res.code > 0) return layui.layer.msg(res.msg);
|
||||
this.item.prev().val(res.data.url).prev().attr("src", res.data.url);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 字段 角色 roles
|
||||
layui.use(["jquery", "xmSelect", "popup"], function() {
|
||||
layui.$.ajax({
|
||||
url: "/app/admin/role/select?format=tree",
|
||||
dataType: "json",
|
||||
success: function (res) {
|
||||
let value = layui.$("#roles").attr("value");
|
||||
let initValue = value ? value.split(",") : [];
|
||||
if (!top.Admin.Account.isSuperAdmin) {
|
||||
layui.each(res.data, function (k, v) {
|
||||
v.disabled = true;
|
||||
});
|
||||
}
|
||||
layui.xmSelect.render({
|
||||
el: "#roles",
|
||||
name: "roles",
|
||||
initValue: initValue,
|
||||
data: res.data,
|
||||
layVerify: "required",
|
||||
tree: {"show":true, expandedKeys:true, strict:false},
|
||||
toolbar: {show:true, list:["ALL","CLEAR","REVERSE"]},
|
||||
});
|
||||
if (res.code) {
|
||||
layui.popup.failure(res.msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//提交事件
|
||||
layui.use(["form", "popup"], function () {
|
||||
layui.form.on("submit(save)", function (data) {
|
||||
layui.$.ajax({
|
||||
url: INSERT_API,
|
||||
type: "POST",
|
||||
dateType: "json",
|
||||
data: data.field,
|
||||
success: function (res) {
|
||||
if (res.code) {
|
||||
return layui.popup.failure(res.msg);
|
||||
}
|
||||
return layui.popup.success("操作成功", function () {
|
||||
parent.refreshTable();
|
||||
parent.layer.close(parent.layer.getFrameIndex(window.name));
|
||||
});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,215 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>更新页面</title>
|
||||
<link rel="stylesheet" href="/app/admin/component/pear/css/pear.css" />
|
||||
<link rel="stylesheet" href="/app/admin/admin/css/reset.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<form class="layui-form">
|
||||
|
||||
<div class="mainBox">
|
||||
<div class="main-container mr-5">
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label required">角色</label>
|
||||
<div class="layui-input-block">
|
||||
<div name="roles" id="roles" value="" ></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label required">用户名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="username" value="" required lay-verify="required" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label required">昵称</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="nickname" value="" required lay-verify="required" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="password" value="" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">头像</label>
|
||||
<div class="layui-input-block">
|
||||
<img class="img-3" src=""/>
|
||||
<input type="text" style="display:none" name="avatar" value="" />
|
||||
<button type="button" class="pear-btn pear-btn-primary pear-btn-sm" id="avatar" permission="app.admin.upload.avatar">
|
||||
<i class="layui-icon layui-icon-upload"></i>上传图片
|
||||
</button>
|
||||
<button type="button" class="pear-btn pear-btn-primary pear-btn-sm" id="attachment-choose-avatar" permission="app.admin.upload.attachment">
|
||||
<i class="layui-icon layui-icon-align-left"></i>选择图片
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">邮箱</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="email" value="" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">手机</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="mobile" value="" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
<div class="button-container">
|
||||
<button type="submit" class="pear-btn pear-btn-primary pear-btn-md" lay-submit="" lay-filter="save">
|
||||
提交
|
||||
</button>
|
||||
<button type="reset" class="pear-btn pear-btn-md">
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<script src="/app/admin/component/layui/layui.js?v=2.8.12"></script>
|
||||
<script src="/app/admin/component/pear/pear.js"></script>
|
||||
<script src="/app/admin/admin/js/permission.js"></script>
|
||||
<script>
|
||||
|
||||
// 相关接口
|
||||
const PRIMARY_KEY = "id";
|
||||
const SELECT_API = "/app/admin/admin/select" + location.search;
|
||||
const UPDATE_API = "/app/admin/admin/update";
|
||||
|
||||
// 获取数据库记录
|
||||
layui.use(["form", "util", "popup"], function () {
|
||||
let $ = layui.$;
|
||||
$.ajax({
|
||||
url: SELECT_API,
|
||||
dataType: "json",
|
||||
success: function (res) {
|
||||
|
||||
// 给表单初始化数据
|
||||
layui.each(res.data[0], function (key, value) {
|
||||
let obj = $('*[name="'+key+'"]');
|
||||
if (key === "password") {
|
||||
obj.attr("placeholder", "不更新密码请留空");
|
||||
return;
|
||||
}
|
||||
if (typeof obj[0] === "undefined" || !obj[0].nodeName) return;
|
||||
if (obj[0].nodeName.toLowerCase() === "textarea") {
|
||||
obj.val(layui.util.escape(value));
|
||||
} else {
|
||||
obj.attr("value", value);
|
||||
}
|
||||
});
|
||||
|
||||
// 字段 头像 avatar
|
||||
layui.use(["upload", "layer"], function() {
|
||||
let input = layui.$("#avatar").prev();
|
||||
input.prev().attr("src", input.val());
|
||||
layui.$("#attachment-choose-avatar").on("click", function() {
|
||||
parent.layer.open({
|
||||
type: 2,
|
||||
title: "选择附件",
|
||||
content: "/app/admin/upload/attachment?ext=jpg,jpeg,png,gif,bmp",
|
||||
area: ["95%", "90%"],
|
||||
success: function (layero, index) {
|
||||
parent.layui.$("#layui-layer" + index).data("callback", function (data) {
|
||||
input.val(data.url).prev().attr("src", data.url);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
layui.upload.render({
|
||||
elem: "#avatar",
|
||||
url: "/app/admin/upload/avatar",
|
||||
acceptMime: "image/gif,image/jpeg,image/jpg,image/png",
|
||||
field: "__file__",
|
||||
done: function (res) {
|
||||
if (res.code > 0) return layui.layer.msg(res.msg);
|
||||
this.item.prev().val(res.data.url).prev().attr("src", res.data.url);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 字段 角色 roles
|
||||
layui.use(["jquery", "xmSelect"], function() {
|
||||
layui.$.ajax({
|
||||
url: "/app/admin/role/select?format=tree",
|
||||
dataType: "json",
|
||||
success: function (res) {
|
||||
let value = layui.$("#roles").attr("value");
|
||||
let initValue = value ? value.split(",") : [];
|
||||
if (!top.Admin.Account.isSuperAdmin) {
|
||||
layui.each(res.data, function (k, v) {
|
||||
v.disabled = true;
|
||||
});
|
||||
}
|
||||
layui.xmSelect.render({
|
||||
el: "#roles",
|
||||
name: "roles",
|
||||
initValue: initValue,
|
||||
data: res.data,
|
||||
layVerify: "required",
|
||||
tree: {show: true, expandedKeys: true, strict: false},
|
||||
toolbar: {show: true, list: ["ALL","CLEAR","REVERSE"]},
|
||||
})
|
||||
if (res.code) {
|
||||
layui.popup.failure(res.msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ajax产生错误
|
||||
if (res.code) {
|
||||
layui.popup.failure(res.msg);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//提交事件
|
||||
layui.use(["form", "popup"], function () {
|
||||
layui.form.on("submit(save)", function (data) {
|
||||
data.field[PRIMARY_KEY] = layui.url().search[PRIMARY_KEY];
|
||||
layui.$.ajax({
|
||||
url: UPDATE_API,
|
||||
type: "POST",
|
||||
dateType: "json",
|
||||
data: data.field,
|
||||
success: function (res) {
|
||||
if (res.code) {
|
||||
return layui.popup.failure(res.msg);
|
||||
}
|
||||
return layui.popup.success("操作成功", function () {
|
||||
parent.refreshTable();
|
||||
parent.layer.close(parent.layer.getFrameIndex(window.name));
|
||||
});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title></title>
|
||||
<link href="/app/admin/component/pear/css/pear.css" rel="stylesheet" />
|
||||
<link href="/app/admin/admin/css/pages/error.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<img src="/app/admin/admin/images/403.svg" alt="">
|
||||
<div class="content-r">
|
||||
<h1>403</h1>
|
||||
<p>抱歉,你无权访问该页面</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/app/admin/component/layui/layui.js?v=2.8.12"></script>
|
||||
<script src="/app/admin/component/pear/pear.js"></script>
|
||||
<script src="/app/admin/admin/js/permission.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,326 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
<link rel="stylesheet" href="/app/admin/component/layui/css/layui.css?v=2.8.12" />
|
||||
<link rel="stylesheet" href="/app/admin/component/pear/css/pear.css" />
|
||||
<link rel="stylesheet" href="/app/admin/admin/css/reset.css" />
|
||||
</head>
|
||||
<body class="pear-container">
|
||||
<style>
|
||||
.layui-input-block input {
|
||||
width: 300px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-body">
|
||||
|
||||
<div class="layui-tab layui-tab-brief">
|
||||
<ul class="layui-tab-title">
|
||||
<li class="layui-this">基本信息</li>
|
||||
<li>菜单配置</li>
|
||||
<li>页面配置</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<div class="layui-tab-item layui-show">
|
||||
|
||||
<form class="layui-form" lay-filter="baseInfo">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">网站名称</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="title" required lay-verify="required" placeholder="请输入网站名称" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">网站Logo</label>
|
||||
<div class="layui-input-block">
|
||||
<img class="img-3" src=""/>
|
||||
<input type="text" style="display:none" name="image" value="/app/admin/avatar.png" />
|
||||
<button type="button" class="pear-btn pear-btn-primary pear-btn-sm" id="image" permission="app.admin.upload.avatar">
|
||||
<i class="layui-icon layui-icon-upload"></i>上传图片
|
||||
</button>
|
||||
<button type="button" class="pear-btn pear-btn-primary pear-btn-sm" id="attachment-choose-image" permission="app.admin.upload.attachment">
|
||||
<i class="layui-icon layui-icon-align-left"></i>选择图片
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">ICP备案号</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="icp" placeholder="请输入ICP备案号" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">公安网备</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="beian" placeholder="请输入公安网备号" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">其它</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="footer_txt" placeholder="其它展示在页面底部的内容" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button type="submit" class="pear-btn pear-btn-primary pear-btn-md" lay-submit="" lay-filter="saveBaseInfo">
|
||||
提交
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 菜单设置 -->
|
||||
<div class="layui-tab-item">
|
||||
|
||||
<form class="layui-form" lay-filter="menuInfo">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">菜单url</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="data" required lay-verify="required" placeholder="请输入菜单url" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">默认菜单ID</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="number" name="select" required lay-verify="required" placeholder="请输入默认菜单ID" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">开启手风琴</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="accordion" id="accordion" lay-skin="switch" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">折叠菜单</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" id="collapse" name="collapse" lay-skin="switch" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button type="submit" class="pear-btn pear-btn-primary pear-btn-md" lay-submit="" lay-filter="saveMenuInfo">
|
||||
提交
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- tab设置 -->
|
||||
<div class="layui-tab-item">
|
||||
|
||||
<form class="layui-form" lay-filter="tabInfo">
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">保持标签</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="keepState" lay-skin="switch" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">记住标签</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="session" lay-skin="switch" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">预加载标签</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="preload" lay-skin="switch" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">最大标签数</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="number" name="max" required lay-verify="required" placeholder="请输入最大标签数" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">主标签标题</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="title" required lay-verify="required" placeholder="请输入主页标签标题" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">主标签URL</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="href" required lay-verify="required" placeholder="请输入菜单url" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">主标签ID</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="number" name="id" required lay-verify="required" placeholder="请输入主页标签ID" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button type="submit" class="pear-btn pear-btn-primary pear-btn-md" lay-submit="" lay-filter="saveTabInfo">
|
||||
提交
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="/app/admin/component/layui/layui.js?v=2.8.12"></script>
|
||||
<script src="/app/admin/component/pear/pear.js"></script>
|
||||
<script src="/app/admin/admin/js/permission.js"></script>
|
||||
<script>
|
||||
|
||||
// 基础设置
|
||||
layui.use(["upload", "layer", "popup"], function() {
|
||||
let $ = layui.$;
|
||||
let form = layui.form;
|
||||
|
||||
// image
|
||||
let input = $("#image").prev();
|
||||
input.prev().attr("src", input.val());
|
||||
layui.$("#attachment-choose-image").on("click", function() {
|
||||
parent.layer.open({
|
||||
type: 2,
|
||||
title: "选择附件",
|
||||
content: "/app/admin/upload/attachment?ext=jpg,jpeg,png,gif,bmp",
|
||||
area: ["95%", "90%"],
|
||||
success: function (layero, index) {
|
||||
parent.layui.$("#layui-layer" + index).data("callback", function (data) {
|
||||
input.val(data.url).prev().attr("src", data.url);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
layui.upload.render({
|
||||
elem: "#image",
|
||||
url: "/app/admin/upload/avatar",
|
||||
acceptMime: "image/gif,image/jpeg,image/jpg,image/png",
|
||||
done: function (res) {
|
||||
if (res.code) {
|
||||
return layui.popup.failure(res.msg);
|
||||
}
|
||||
this.item.prev().val(res.data.url).prev().attr("src", res.data.url);
|
||||
}
|
||||
});
|
||||
|
||||
// 提交
|
||||
form.on("submit(saveBaseInfo)", function(data){
|
||||
$.ajax({
|
||||
url: "/app/admin/config/update",
|
||||
dataType: "json",
|
||||
type: "POST",
|
||||
data: {logo: data.field},
|
||||
success: function (res) {
|
||||
if (res.code) {
|
||||
return layui.popup.failure(res.msg);
|
||||
}
|
||||
return layui.popup.success("操作成功");
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
// 菜单设置
|
||||
layui.use(["upload", "layer", "popup"], function() {
|
||||
let $ = layui.$;
|
||||
let form = layui.form;
|
||||
// 提交
|
||||
form.on("submit(saveMenuInfo)", function(data){
|
||||
$.ajax({
|
||||
url: "/app/admin/config/update",
|
||||
dataType: "json",
|
||||
type: "POST",
|
||||
data: {menu: data.field},
|
||||
success: function (res) {
|
||||
if (res.code) {
|
||||
return layui.popup.failure(res.msg);
|
||||
}
|
||||
return layui.popup.success("操作成功");
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
// 标签设置
|
||||
layui.use(["upload", "layer", "popup"], function() {
|
||||
let $ = layui.$;
|
||||
let form = layui.form;
|
||||
// 提交
|
||||
form.on("submit(saveTabInfo)", function(data){
|
||||
let field = data.field;
|
||||
field.index = {
|
||||
id: field.id,
|
||||
href: field.href,
|
||||
title: field.title,
|
||||
};
|
||||
delete data.field;
|
||||
$.ajax({
|
||||
url: "/app/admin/config/update",
|
||||
dataType: "json",
|
||||
type: "POST",
|
||||
data: {tab: field},
|
||||
success: function (res) {
|
||||
if (res.code) {
|
||||
return layui.popup.failure(res.msg);
|
||||
}
|
||||
return layui.popup.success("操作成功");
|
||||
}
|
||||
});
|
||||
// 删除sessionStorage缓存
|
||||
sessionStorage.clear();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
layui.use(["form"], function () {
|
||||
let form = layui.form;
|
||||
let $ = layui.$;
|
||||
$.ajax({
|
||||
url: "/app/admin/config/get",
|
||||
dataType: "json",
|
||||
success: function (res) {
|
||||
if (res.code) {
|
||||
return layui.popup.failure(res.msg);
|
||||
}
|
||||
form.val("baseInfo", res.logo);
|
||||
$("#image").prev().val(res.logo.image).prev().attr("src", res.logo.image);
|
||||
form.val("menuInfo", res.menu);
|
||||
let tab = res.tab;
|
||||
let index = tab.index;
|
||||
delete tab.index;
|
||||
tab.id = index.id;
|
||||
tab.title = index.title;
|
||||
tab.href= index.href;
|
||||
form.val("tabInfo", res.tab);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user