This commit is contained in:
Lwei
2026-06-22 11:23:05 +08:00
commit 234d126ff6
998 changed files with 355523 additions and 0 deletions
@@ -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版本的组件,整体美观页面风格一致
+8
View File
@@ -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
View File
@@ -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
+21
View File
@@ -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.
+70
View File
@@ -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>
+20
View File
@@ -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');
}
}
+44
View File
@@ -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);
}
}
}
+42
View File
@@ -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;
}
}
+29
View File
@@ -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;
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace app\process;
use Webman\App;
class Http extends App
{
}
+305
View File
@@ -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>
+67
View File
@@ -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('');
+218
View File
@@ -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">&nbsp;推荐护工</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>&yen;</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}
+332
View File
@@ -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}
+318
View File
@@ -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">&yen;</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}
+553
View File
@@ -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">&yen;</span>
<span class="amount">{{ estimatedPrice }}</span>
</div>
</div>
<div class="price-detail">
<div class="price-item">
<span>基础费用({{ basePrice }}元/小时 x {{ totalHours }}小时)</span>
<span>&yen;{{ basePrice * totalHours }}</span>
</div>
<div class="price-item" v-if="form.skills.length > 0">
<span>技能加成({{ form.skills.length }}项技能 +10%</span>
<span>&yen;{{ 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">&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}
+382
View File
@@ -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">&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}
+30
View File
@@ -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>
+11
View File
@@ -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>
View File
+31
View File
@@ -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]))
+66
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -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,
];
+21
View File
@@ -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',
]
];
+17
View File
@@ -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,
];
+15
View File
@@ -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;
+29
View File
@@ -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,
],
],
],
];
+15
View File
@@ -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 [];
+5
View File
@@ -0,0 +1,5 @@
<?php
return [
];
+17
View File
@@ -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,
];
+32
View File
@@ -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],
],
]
],
],
];
+15
View File
@@ -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 [];
+28
View File
@@ -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
',
];
+4
View File
@@ -0,0 +1,4 @@
<?php
return [
'enable' => true,
];
+17
View File
@@ -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,
];
+7
View File
@@ -0,0 +1,7 @@
<?php
use Webman\Event\EventListCommand;
return [
EventListCommand::class
];
+8
View File
@@ -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,
],
];
+62
View File
@@ -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 === '/',
]
]
]
];
+21
View File
@@ -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;
+23
View File
@@ -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
];
+65
View File
@@ -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],
];
+23
View File
@@ -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,
],
];
+25
View File
@@ -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',
];
+27
View File
@@ -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' => '}'
]
];
+11
View File
@@ -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" ]
+127
View File
@@ -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;
}
}
+93
View File
@@ -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);
}
}
}
+147
View File
@@ -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;
}
}
+48
View File
@@ -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;
}
}
+80
View File
@@ -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
+193
View File
@@ -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;
}
}
+575
View File
@@ -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);
}
}
+67
View File
@@ -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;
}
}
+469
View File
@@ -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');
}
}
+40
View File
@@ -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);
}
}
+94
View File
@@ -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;
}
}
+40
View File
@@ -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';
}
+31
View File
@@ -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;
}
+26
View File
@@ -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');
}
}
+96
View File
@@ -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;
}
}
+29
View File
@@ -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';
}
+38
View File
@@ -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) : [];
}
}
+38
View File
@@ -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';
}
+42
View File
@@ -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';
}
+49
View File
@@ -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';
}
+158
View File
@@ -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>
+80
View File
@@ -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>
+414
View File
@@ -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>
+181
View File
@@ -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>
+215
View File
@@ -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>
+326
View File
@@ -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