This commit is contained in:
2025-04-30 10:53:02 +08:00
commit 701b9a909f
1007 changed files with 353274 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
# db
DB_HOST = '127.0.0.1'
DB_PORT = 3306
DB_NAME = 'car_info_uat'
DB_USER = 'car_info_uat'
DB_PASSWORD = 'neFDRNZiPzKQDAHH'
WEBMAN_LISTEN = 'http://0.0.0.0:9025'
WEBMAN_COUNT = 2
+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>
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace app\controller;
use support\Request;
class IndexController
{
public function index(Request $request)
{
static $readme;
if (!$readme) {
$readme = file_get_contents(base_path('README.md'));
}
return $readme;
}
public function view(Request $request)
{
return view('index/view', ['name' => 'webman']);
}
public function json(Request $request)
{
return json(['code' => 0, 'msg' => 'ok']);
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace app\controller;
use app\model\Location;
use util\Util;
use support\Request;
class LocationController
{
/**
* Get all locations that have vehicles
*/
public function index(Request $request)
{
$locations = Location::with('vehicles')
->with('fboVehicles')
->get()
->map(function ($location) {
$location->vehicle_count = $location->vehicles->count();
$location->fbo_vehicle_count = $location->fboVehicles->count();
return $location;
});
return Util::success($locations);
}
/**
* Get a specific location with its vehicles
*/
public function show(Request $request, $id)
{
$location = Location::hasVehicles()
->with('vehicles')
->find($id);
if (!$location) {
return Util::fail(null, 'Location not found or has no vehicles');
}
return Util::success($location);
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace app\controller;
//use support\Request;
//use support\Log;
//use Webman\Http\Response;
use app\service\OssService;
use Shopwwi\WebmanFilesystem\Facade\Storage;
use support\Log;
use support\Request;
use util\Util;
class UploadController
{
/**
* 不需要登录的方法
*/
protected $noNeedLogin = ['uploadImage'];
// 上传单文件图片
public function uploadImage(Request $request)
{
try {
$file = $request->file('files');
if (!$file || !$file->isValid()) {
return Util::fail($file,'上传失败');
}
$result = Storage::adapter('oss')
->path('lingke_crm/'.date('Ymd'))
->size(1024*1024*5)
->extYes(['image/jpeg','image/png'])
->extNo(['image/gif'])
->upload($file);
$ossservice = new OssService();
$data = [
'path' => $result->file_name,
'original_name' => $result->origin_name,
'image_url' => $ossservice->generatePrivateUrl($result->file_name)
];
return Util::success($data,'上传成功');
}catch (\Throwable $e){
Log::error('文件上传失败: ' . $e->getMessage());
return Util::fail([],'file:'.$e->getFile().'line:'.$e->getLine().'message:'.$e->getMessage());
}
}
}
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace app\controller;
use app\model\Vehicle;
use plugin\admin\app\model\Location;
use util\Util;
use support\Request;
class VehicleController
{
protected $status = ['未知','待发车','在途(发往fbo港口)','抵达发货港口','在途(发往目的地仓库)','抵达仓库','售出'];
/**
* Mask VIN number by showing only first 2 and last 6 characters
*/
private function maskVin($vin)
{
if (strlen($vin) <= 8) {
return $vin;
}
return substr($vin, 0, 2) . str_repeat('*', strlen($vin) - 8) . substr($vin, -6);
}
/**
* Get all vehicles with their locations
*/
public function index(Request $request)
{
$vehicles = Vehicle::with('location')->with('finalLocation')->get();
$vehicles->each(function ($vehicle) {
if (isset($vehicle->vin)) {
$vehicle->vin = $this->maskVin($vehicle->vin);
}
$vehicle->status = $this->status[$vehicle->status]??$this->status[0];
// $location = Location::find($vehicle->locations_id);
// $vehicle->location_name = $location ? $location->name : '';
// $final_location = Location::find($vehicle->final_locations_id);
// $vehicle->final_location_name = $final_location ? $final_location->name : '';
$fbo_location = Location::find($vehicle->fbo_locations_id);
$vehicle->fbo_destination = $fbo_location ? $fbo_location->name : '';
});
$result = ['list' => $vehicles,'count'=>count($vehicles)];
return Util::success($result);
}
/**
* Get a specific vehicle with its location
*/
public function show(Request $request, $id)
{
$vehicle = Vehicle::with('location')->find($id);
if (!$vehicle) {
return Util::fail($vehicle,'Vehicle not found');
}
if (isset($vehicle->vin)) {
$vehicle->vin = $this->maskVin($vehicle->vin);
}
return Util::success($vehicle);
}
}
+4
View File
@@ -0,0 +1,4 @@
<?php
/**
* Here is your custom functions.
*/
+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 $next): Response
{
// Access to files beginning with. Is prohibited
if (strpos($request->path(), '/.') !== false) {
return response('<h1>403 forbidden</h1>', 403);
}
/** @var Response $response */
$response = $next($request);
// Add cross domain HTTP header
/*$response->withHeaders([
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Credentials' => 'true',
]);*/
return $response;
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
namespace app\model;
use support\Model;
class Location extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'locations';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
/**
* Get the vehicles for the location.
*/
public function vehicles()
{
return $this->hasMany(Vehicle::class, 'locations_id', 'id');
}
/**
* 目的地关联
* Get the vehicles for the location.
*/
public function finalVehicles()
{
return $this->hasMany(Vehicle::class, 'final_locations_id', 'id');
}
/**
* Get the FBO vehicles for the location.
*/
public function fboVehicles()
{
return $this->hasMany(Vehicle::class, 'fbo_locations_id', 'id');
}
/**
* Scope a query to only include locations that have vehicles.
*/
public function scopeHasVehicles($query)
{
return $query->whereHas('vehicles');
}
}
+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;
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace app\model;
use support\Model;
class Vehicle extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'vehicles';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
/**
* Get the location that owns the vehicle.
*/
public function location()
{
return $this->belongsTo(Location::class, 'locations_id', 'id');
}
/**
* Get the location that owns the vehicle.
*/
public function finalLocation()
{
return $this->belongsTo(Location::class, 'final_locations_id', 'id');
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace app\process;
use Webman\App;
class Http extends App
{
}
+282
View File
@@ -0,0 +1,282 @@
<?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;
}
echo $file . " updated and reload\n";
// send SIGUSR1 signal to master process for reload
if (DIRECTORY_SEPARATOR === '/' && $this->ppid > 1) {
posix_kill($this->ppid, SIGUSR1);
} else {
return true;
}
break;
}
}
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 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;
}
$ppid = $this->ppid;
if ($ppid <= 1) {
return;
}
$processPath = "/proc/$ppid";
if (!is_dir($processPath)) {
// Process not exist
$this->ppid = 0;
return;
}
$childrenFile = "/proc/$ppid/task/$ppid/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;
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace app\service;
use OSS\OssClient;
use OSS\Core\OssException;
class OssService
{
private $accessKeyId;
private $accessKeySecret;
private $bucket;
private $endpoint;
private $ossClient;
public function __construct()
{
$this->accessKeyId = 'LTAI5tAGiH7bkVkmrniXnfkA';
$this->accessKeySecret = 'nqklJBft6t3S0A4ZvxF4ci9ni91kmm';
$this->bucket = 'vehicle-album';
$this->endpoint = 'https://oss-cn-beijing.aliyuncs.com';
$this->ossClient = new OssClient($this->accessKeyId, $this->accessKeySecret, $this->endpoint);
}
/**
* 上传图片到 OSS
* @param string $filePath 本地文件路径
* @param string $objectKey OSS上存储的对象名称
* @return string 图片的 URL
*/
public function uploadImage($filePath, $objectKey)
{
try {
// 上传文件到 OSS
$this->ossClient->uploadFile($this->bucket, $objectKey, $filePath);
// 构造图片的私有 URL
$url = $this->generatePrivateUrl($objectKey);
return $url;
} catch (OssException $e) {
// 错误处理
throw new \RuntimeException('OSS Error: ' . $e->getMessage());
}
}
/**
* 生成图片的私有访问 URL
* @param string $objectKey
* @return string
*/
public function generatePrivateUrl($objectKey, $expireTime = 3600)
{
// 生成 OSS 私有 URL(有效期为 3600 秒)
return $this->ossClient->signUrl($this->bucket, $objectKey, $expireTime);
}
}
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/favicon.ico"/>
<title>webman</title>
</head>
<body>
hello <?=htmlspecialchars($name)?>
</body>
</html>
+61
View File
@@ -0,0 +1,61 @@
{
"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/admin": "^2.1",
"vlucas/phpdotenv": "^5.6",
"illuminate/pagination": "^12.10",
"shopwwi/webman-filesystem": "^2.0",
"shopwwi/filesystem-oss": "^1.0",
"iidestiny/flysystem-oss": "4"
},
"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"
]
},
"minimum-stability": "dev",
"prefer-stable": true
}
Generated
+5289
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' => getenv('DB_HOST'),
'port' => getenv('DB_PORT'),
'database' => getenv('DB_NAME'),
'username' => getenv('DB_USER'),
'password' => getenv('DB_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 [];
+110
View File
@@ -0,0 +1,110 @@
<?php
return [
'enable' => true,
'default' => 'local',
'max_size' => 1024 * 1024 * 10, //单个文件大小10M
'ext_yes' => [], //允许上传文件类型 为空则为允许所有
'ext_no' => [], // 不允许上传文件类型 为空则不限制
'storage' => [
'public' => [
'driver' => \Shopwwi\WebmanFilesystem\Adapter\LocalAdapterFactory::class,
'root' => public_path(),
'url' => '//127.0.0.1:8787' // 静态文件访问域名
],
'local' => [
'driver' => \Shopwwi\WebmanFilesystem\Adapter\LocalAdapterFactory::class,
'root' => runtime_path(),
'url' => '//127.0.0.1:8787' // 静态文件访问域名
],
'ftp' => [
'driver' => \Shopwwi\WebmanFilesystem\Adapter\FtpAdapterFactory::class,
'host' => 'ftp.example.com',
'username' => 'username',
'password' => 'password',
'url' => '' // 静态文件访问域名
// 'port' => 21,
// 'root' => '/path/to/root',
// 'passive' => true,
// 'ssl' => true,
// 'timeout' => 30,
// 'ignorePassiveAddress' => false,
// 'timestampsOnUnixListingsEnabled' => true,
],
'memory' => [
'driver' => \Shopwwi\WebmanFilesystem\Adapter\MemoryAdapterFactory::class,
],
's3' => [
'driver' => \Shopwwi\WebmanFilesystem\Adapter\S3AdapterFactory::class,
'credentials' => [
'key' => 'S3_KEY',
'secret' => 'S3_SECRET',
],
'region' => 'S3_REGION',
'version' => 'latest',
'bucket_endpoint' => false,
'use_path_style_endpoint' => false,
'endpoint' => 'S3_ENDPOINT',
'bucket_name' => 'S3_BUCKET',
'url' => '' // 静态文件访问域名
],
'minio' => [
'driver' => \Shopwwi\WebmanFilesystem\Adapter\S3AdapterFactory::class,
'credentials' => [
'key' => 'S3_KEY',
'secret' => 'S3_SECRET',
],
'region' => 'S3_REGION',
'version' => 'latest',
'bucket_endpoint' => false,
'use_path_style_endpoint' => true,
'endpoint' => 'S3_ENDPOINT',
'bucket_name' => 'S3_BUCKET',
'url' => '' // 静态文件访问域名
],
'oss' => [
'driver' => \Shopwwi\WebmanFilesystem\Adapter\AliyunOssAdapterFactory::class,
// 'accessId' => 'OSS_ACCESS_ID',
// 'accessSecret' => 'OSS_ACCESS_SECRET',
// 'bucket' => 'OSS_BUCKET',
// 'endpoint' => 'OSS_ENDPOINT',
'accessKey' => 'LTAI5tAGiH7bkVkmrniXnfkA',
'secretKey' => 'nqklJBft6t3S0A4ZvxF4ci9ni91kmm',
'bucket' => 'vehicle-album',
'endpoint' => 'https://oss-cn-beijing.aliyuncs.com',
'url' => '', // 静态文件访问域名
// 'timeout' => 3600,
// 'connectTimeout' => 10,
'isCName' => false,
'prefix' => '',
// 'token' => null,
// 'proxy' => null,
],
'qiniu' => [
'driver' => \Shopwwi\WebmanFilesystem\Adapter\QiniuAdapterFactory::class,
'accessKey' => 'QINIU_ACCESS_KEY',
'secretKey' => 'QINIU_SECRET_KEY',
'bucket' => 'QINIU_BUCKET',
'domain' => 'QINBIU_DOMAIN',
'url' => '' // 静态文件访问域名
],
'cos' => [
'driver' => \Shopwwi\WebmanFilesystem\Adapter\CosAdapterFactory::class,
'region' => 'COS_REGION',
'app_id' => 'COS_APPID',
'secret_id' => 'COS_SECRET_ID',
'secret_key' => 'COS_SECRET_KEY',
// 可选,如果 bucket 为私有访问请打开此项
// 'signed_url' => false,
'bucket' => 'COS_BUCKET',
'read_from_cdn' => false,
'url' => '' // 静态文件访问域名
// 'timeout' => 60,
// 'connect_timeout' => 60,
// 'cdn' => '',
// 'scheme' => 'https',
],
],
];
+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
];
+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' => getenv('WEBMAN_LISTEN'),
'count' => getenv('WEBMAN_COUNT'),
'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',
];
+22
View File
@@ -0,0 +1,22 @@
<?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' => Raw::class
];
+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 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,312 @@
<?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 plugin\admin\app\model\AdminRole;
use plugin\admin\app\model\User;
use support\exception\BusinessException;
use support\Request;
use support\Response;
use Throwable;
use Webman\Captcha\CaptchaBuilder;
use Webman\Captcha\PhraseBuilder;
use Webman\Config;
/**
* 管理员账户
*/
class AccountController extends Crud
{
/**
* 不需要登录的方法
* @var string[]
*/
protected $noNeedLogin = ['login', 'logout', 'captcha', 'register', 'registerPage'];
/**
* 不需要鉴权的方法
* @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');
}
}
/**
* 注册页面
* @return Response
*/
public function registerPage(Request $request): Response
{
$config = Config::get('plugin.admin.app', []);
$data = [
'logo' => $config['logo'] ?? '/app/admin/admin/images/logo.png',
'title' => $config['name'] ?? '管理系统',
];
return view('account/register', $data);
}
/**
* 注册
* @param Request $request
* @return Response
*/
public function register(Request $request): Response
{
$username = $request->post('username');
$nickname = $request->post('nickname');
$password = $request->post('password');
if (empty($username) || empty($password) || empty($nickname)) {
return json(['code' => 1, 'msg' => '用户名、昵称和密码不能为空']);
}
// 检查用户名是否已存在
if (Admin::where('username', $username)->first()) {
return json(['code' => 1, 'msg' => '用户名已存在']);
}
// 创建新用户
$user = new Admin();
$user->username = $username;
$user->nickname = $nickname;
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->created_at = date('Y-m-d H:i:s');
$user->save();
$adminRole = new AdminRole();
$adminRole->admin_id = $user->id;
$adminRole->role_id = 3;
$adminRole->save();
return json(['code' => 0, 'msg' => '注册成功']);
}
}
@@ -0,0 +1,261 @@
<?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 plugin\admin\app\model\AdminRole;
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', 'register', 'registerPage'];
/**
* 不需要鉴权的方法
* @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 @@
@@ -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,79 @@
<?php
namespace plugin\admin\app\controller;
use support\Request;
use support\Response;
use plugin\admin\app\model\Location;
use plugin\admin\app\controller\Crud;
use support\exception\BusinessException;
use util\Util;
/**
* 仓库/港口
*/
class LocationController extends Crud
{
/**
* @var Location
*/
protected $model = null;
/**
* 构造函数
* @return void
*/
public function __construct()
{
$this->model = new Location;
}
/**
* 浏览
* @return Response
*/
public function index(): Response
{
return view('location/index');
}
/**
* 插入
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function insert(Request $request): Response
{
if ($request->method() === 'POST') {
return parent::insert($request);
}
return view('location/insert');
}
/**
* 更新
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function update(Request $request): Response
{
if ($request->method() === 'POST') {
return parent::update($request);
}
return view('location/update');
}
/**
* 获取下拉选项接口
*/
public function getAll()
{
$locations = Location::get();
return Util::success($locations);
}
}
@@ -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,354 @@
<?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;
}
$permissions = ["app.admin.vehicle.select","app.admin.vehicle.detail"];
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,378 @@
<?php
namespace plugin\admin\app\controller;
use app\service\OssService;
use Exception;
use Intervention\Image\ImageManagerStatic as Image;
use plugin\admin\app\model\Upload;
use Shopwwi\WebmanFilesystem\Facade\Storage;
use support\exception\BusinessException;
use support\Log;
use support\Request;
use support\Response;
use Throwable;
use util\Util;
/**
* 附件管理
*/
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,
];
}
// 上传单文件图片
public function uploadImage(Request $request)
{
try {
// $file = $request->file('file');
$file = current($request->file());
if (!$file || !$file->isValid()) {
return Util::fail($file,'上传失败2');
}
$result = Storage::adapter('oss')
->path('lingke_crm/'.date('Ymd'))
->size(1024*1024*5)
->extYes(['image/jpeg','image/png'])
->extNo(['image/gif'])
->upload($file);
$ossservice = new OssService();
$data = [
'path' => $result->file_name,
'original_name' => $result->origin_name,
'image_url' => $ossservice->generatePrivateUrl($result->file_name)
];
return Util::success($data,'上传成功');
}catch (\Throwable $e){
Log::error('文件上传失败: ' . $e->getMessage());
return Util::fail([],'file:'.$e->getFile().'line:'.$e->getLine().'message:'.$e->getMessage());
}
}
}
@@ -0,0 +1,69 @@
<?php
namespace plugin\admin\app\controller;
use plugin\admin\app\model\User;
use support\exception\BusinessException;
use support\Request;
use support\Response;
use Throwable;
/**
* 用户管理
*/
class UserController extends Crud
{
/**
* @var User
*/
protected $model = null;
/**
* 构造函数
* @return void
*/
public function __construct()
{
$this->model = new User;
}
/**
* 浏览
* @return Response
* @throws Throwable
*/
public function index(): Response
{
return raw_view('user/index');
}
/**
* 插入
* @param Request $request
* @return Response
* @throws BusinessException|Throwable
*/
public function insert(Request $request): Response
{
if ($request->method() === 'POST') {
return parent::insert($request);
}
return raw_view('user/insert');
}
/**
* 更新
* @param Request $request
* @return Response
* @throws BusinessException|Throwable
*/
public function update(Request $request): Response
{
if ($request->method() === 'POST') {
return parent::update($request);
}
return raw_view('user/update');
}
}
@@ -0,0 +1,132 @@
<?php
namespace plugin\admin\app\controller;
use plugin\admin\app\model\Location;
use support\Request;
use support\Response;
use plugin\admin\app\model\Vehicle;
use plugin\admin\app\controller\Crud;
use support\exception\BusinessException;
/**
* 车辆信息
*/
class VehicleController extends Crud
{
/**
* 不需要登录的方法
* @var string[]
*/
protected $noNeedLogin = ['detail'];
/**
* 不需要鉴权的方法
* @var string[]
*/
protected $noNeedAuth = ['detail'];
protected $status = ['未知','待发车','在途(发往fbo港口)','抵达发货港口','在途(发往目的地仓库)','抵达仓库','售出'];
/**
* @var Vehicle
*/
protected $model = null;
/**
* 构造函数
* @return void
*/
public function __construct()
{
$this->model = new Vehicle;
}
/**
* 浏览
* @return Response
*/
public function index(): Response
{
return view('vehicle/index');
}
/**
* 插入
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function insert(Request $request): Response
{
if ($request->method() === 'POST') {
return parent::insert($request);
}
return view('vehicle/insert');
}
/**
* 更新
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function update(Request $request): Response
{
if ($request->method() === 'POST') {
return parent::update($request);
}
return view('vehicle/update');
}
/**
* 详情
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function detail(Request $request): Response
{
return view('vehicle/detail');
}
/**
* 查询
* @param Request $request
* @return Response
* @throws BusinessException
*/
/* public function list(Request $request)
{
[$where, $format, $limit, $field, $order] = $this->selectInput($request);
return json($where);
$query = $this->doSelect($where, $field, $order);
$data = $this->doFormat($query, $format, $limit);
// foreach ($data['data'] as &$value) {
// $value['locations'] = Location::find($value['locations_id'])->name;
// }
// $data = json_decode(json_encode($data), true);
return $data;
}*/
/**
* 查询后置处理,添加位置信息
* @param mixed $items
* @return mixed
*/
protected function afterQuery($items)
{
foreach ($items as &$item) {
$location = Location::find($item->locations_id);
$item->status = $this->status[$item->status]??$this->status[0];
$item->location_name = $location ? $location->name : '';
$final_location = Location::find($item->final_locations_id);
$item->final_location_name = $final_location ? $final_location->name : '';
$fbo_location = Location::find($item->fbo_locations_id);
$item->fbo_location_name = $fbo_location ? $fbo_location->name : '';
}
return $items;
}
}
+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;
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace plugin\admin\app\model;
use plugin\admin\app\model\Base;
/**
* @property integer $id 唯一标识(主键)
* @property string $name 港口/仓库名称
* @property string $name_en 英文名称
* @property string $address 详细地址
* @property string $area 所在地区
* @property string $type 类型:港口/仓库
* @property integer $num 车辆数
* @property mixed $created_at 创建时间
* @property mixed $updated_at 更新时间
*/
class Location extends Base
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'locations';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
}
+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';
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace plugin\admin\app\model;
use plugin\admin\app\model\Base;
/**
* @property integer $id 自增主键(主键)
* @property string $vin 车架号(17位国际标准)
* @property string $classify
* @property string $brand 车辆品牌
* @property string $model 车辆型号
* @property string $status 车辆状态(如:待发运/运输中/已到达)
* @property string $purchase_location 买车地点
* @property string $procedure_location 手续办理地
* @property string $fbo_destination FBO最终目的地
* @property integer $fbo_locations_id fbo目的地id
* @property integer $locations_id 车辆所在地
* @property integer $final_locations_id 最终目的地
* @property string $departure_time 实际发车时间
* @property string $arrival_time 实际到达时间
* @property integer $shipping_cycle 运输周期(天)
* @property string $img_url 图片
* @property string $created_at 记录创建时间
* @property string $updated_at 最后更新时间
*/
class Vehicle extends Base
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'vehicles';
/**
* 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>
+85
View File
@@ -0,0 +1,85 @@
<!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>
<div class="layui-form-item" style="text-align: center; margin-top: 10px;">
<!-- <a href="/app/admin/account/registerPage" class="pear-text">还没有账号?立即注册</a>-->
<a href="/app/admin/account/register/page" class="pear-text">还没有账号?立即注册</a>
</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());
$('.codeImage').attr("src", "/admin/account/captcha/login?v=" + new Date().getTime());
}
switchCaptcha();
// 登 录 提 交
form.on('submit(login)', function (data) {
layer.load();
$.ajax({
url: '/app/admin/account/login',
type: "POST",
data: data.field,
success: function (res) {
layer.closeAll('loading');
if (!res.code) {
layui.popup.success('登录成功', function () {
location.reload();
})
} else {
layui.popup.failure(res.msg)
switchCaptcha();
}
}
});
return false;
});
$('.codeImage').on('click', function () {
switchCaptcha();
});
})
</script>
</body>
</html>
@@ -0,0 +1,78 @@
<!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 class="desc">注册新账号</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="text" name="nickname" 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 lay-verify="required" hover class="layui-input" type="password" name="confirm_password" value="" placeholder="确认密码" />
</div>
<div class="layui-form-item">
<button type="submit" class="pear-btn pear-btn-primary login" lay-submit lay-filter="register">
注 册
</button>
</div>
<div class="layui-form-item" style="text-align: center; margin-top: 10px;">
<a href="/app/admin" class="pear-text">已有账号?立即登录</a>
</div>
</form>
<!-- 资 源 引 入 -->
<script src="/app/admin/component/layui/layui.js"></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;
// 注 册 提 交
form.on('submit(register)', function (data) {
if (data.field.password !== data.field.confirm_password) {
layui.popup.failure('两次输入的密码不一致');
return false;
}
layer.load();
$.ajax({
url: '/app/admin/account/register',
type: "POST",
data: data.field,
success: function (res) {
layer.closeAll('loading');
if (res.code === 0) {
layui.popup.success('注册成功', function () {
location.href = '/app/admin';
});
} else {
layui.popup.failure(res.msg);
}
},
error: function() {
layer.closeAll('loading');
layui.popup.failure('注册失败,请稍后重试');
}
});
return false;
});
});
</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>
+113
View File
@@ -0,0 +1,113 @@
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>表单生成器</title>
<link rel="stylesheet" href="/app/admin/component/pear/css/pear.css" />
<style>
html,body{background-color: whitesmoke}
.layui-fluid{margin-top: 15px;}
.content{min-height: 1200px;}
.nav{text-align: center;}
.nav button{margin-bottom: 3px;width: 100%;margin-top: 3px;margin-bottom: 3px;border-radius: 1px;}
.nav button:hover{background-color: #5FB878;border: 1px solid #5FB878;color: white;}
.layui-card-body .layui-btn+.layui-btn{margin-left: 0px;}
.code-show{min-height: 700px;}
.js-show{min-height: 360px;}
.layui-card-body {padding: 10px;}
.button{line-height: 100% !important;}
.content .pear-btn-md, .content .pear-btn-sm, .content .pear-btn-xs, .content .pear-btn {
line-height: 100%;
letter-spacing: 2px;
padding: 0 15px;
font-weight: 400;
font-size: 14px;
}
</style>
</head>
<body>
<div class="layui-fluid">
<div class="layui-row layui-col-space10">
<div class="layui-col-md1">
<div class="layui-card nav click-but">
<div class="layui-card-header">长</div>
<div class="layui-card-body">
<button class="pear-btn pear-btn-sm" plain data-size="block" data-type="text">输入框</button>
<button class="pear-btn pear-btn-sm" plain data-size="block" data-type="password">密码框</button>
<button class="pear-btn pear-btn-sm" plain data-size="block" data-type="select">选择框</button>
<button class="pear-btn pear-btn-sm" plain data-size="block" data-type="checkbox_a">复选框</button>
<button class="pear-btn pear-btn-sm" plain data-size="block" data-type="checkbox_b">开关</button>
<button class="pear-btn pear-btn-sm" plain data-size="block" data-type="radio">单选框</button>
<button class="pear-btn pear-btn-sm" plain data-size="block" data-type="textarea">文本域</button>
<button class="pear-btn pear-btn-sm" plain data-size="block" data-type="icon">图标</button>
<button class="pear-btn pear-btn-sm" plain data-size="block" data-type="multiSelect">下拉多选</button>
<button class="pear-btn pear-btn-sm" plain data-size="block" data-type="treeSelectOne">树单选</button>
<button class="pear-btn pear-btn-sm" plain data-size="block" data-type="tree">树多选</button>
<button class="pear-btn pear-btn-sm" plain data-size="block" data-type="upload">上传文件</button>
<button class="pear-btn pear-btn-sm" plain data-size="block" data-type="uploadImg">上传图片</button>
<button class="pear-btn pear-btn-sm" plain data-size="block" data-type="submit">提交</button>
</div>
</div>
<div class="layui-card nav">
<div class="layui-card-header">短</div>
<div class="layui-card-body">
<button class="pear-btn pear-btn-sm" plain data-size="inline" data-type="text">输入框</button>
<button class="pear-btn pear-btn-sm" plain data-size="inline" data-type="password">密码框</button>
<button class="pear-btn pear-btn-sm" plain data-size="inline" data-type="select">选择框</button>
<button class="pear-btn pear-btn-sm" plain data-size="inline" data-type="checkbox_a">复选框</button>
<button class="pear-btn pear-btn-sm" plain data-size="inline" data-type="checkbox_b">开关</button>
<button class="pear-btn pear-btn-sm" plain data-size="inline" data-type="radio">单选框</button>
<button class="pear-btn pear-btn-sm" plain data-size="inline" data-type="textarea">文本域</button>
<button class="pear-btn pear-btn-sm" plain data-size="inline" data-type="icon">图标</button>
<button class="pear-btn pear-btn-sm" plain data-size="inline" data-type="multiSelect">下拉多选</button>
<button class="pear-btn pear-btn-sm" plain data-size="inline" data-type="treeSelectOne">树单选</button>
<button class="pear-btn pear-btn-sm" plain data-size="inline" data-type="tree">树多选</button>
<button class="pear-btn pear-btn-sm" plain data-size="inline" data-type="upload">上传文件</button>
<button class="pear-btn pear-btn-sm" plain data-size="inline" data-type="uploadImg">上传图片</button>
<button class="pear-btn pear-btn-sm" plain data-size="block" data-type="submit">提交</button>
</div>
</div>
<div class="layui-card nav">
<div class="layui-card-body">
<button class="pear-btn pear-btn-danger pear-btn-sm del-form" data-type="del"> <i class="layui-icon layui-font-16">&#xe640;</i></button>
</div>
</div>
</div>
<div class="layui-col-md5">
<div class="layui-card content">
<div class="layui-card-header">
view
</div>
<div class="layui-card-body code">
<form class="layui-form" action="" onsubmit="return false">
</form>
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-card r-code-html">
<div class="layui-card-header">html</div>
<div class="layui-card-body">
<textarea name="" class="layui-textarea code-show"></textarea>
</div>
</div>
<div class="layui-card r-code-js">
<div class="layui-card-header">code</div>
<div class="layui-card-body">
<textarea name="" class="layui-textarea js-show"></textarea>
</div>
</div>
</div>
</div>
</div>
</body>
<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("design");
</script>
</html>
+323
View File
@@ -0,0 +1,323 @@
<!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="name" value="" class="layui-input">
</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.dict.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.dict.delete">
<i class="layui-icon layui-icon-delete"></i>删除
</button>
</script>
<!-- 表格行工具栏 -->
<script type="text/html" id="table-bar">
<button class="pear-btn pear-btn-xs tool-btn" lay-event="edit" permission="app.admin.dict.update">编辑</button>
<button class="pear-btn pear-btn-xs tool-btn" lay-event="remove" permission="app.admin.dict.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 SELECT_API = "/app/admin/dict/select";
const UPDATE_API = "/app/admin/dict/update";
const DELETE_API = "/app/admin/dict/delete";
const INSERT_URL = "/app/admin/dict/insert";
const UPDATE_URL = "/app/admin/dict/update";
// 表格渲染
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 : "名称",
field : "name",
width : 200,
templet: function (d) {
let field = "name";
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 : "value",
templet: function (d) {
const dicts = JSON.parse(d.value)
return dicts.map(dict => `<span class="layui-badge layui-bg-blue">${dict.name}: ${dict.value}</span>`).join(' ');
},
}, {
title: "创建时间",
field: "created_at",
width: 180,
hide : true,
}, {
title: "更新时间",
field: "updated_at",
width: 180,
hide : true,
}, {
title : "操作",
toolbar: "#table-bar",
align : "center",
width : 130,
fixed : "right",
},
];
let apiResults = {};
const loadDictNames = () => {
let apis = [];
apis.push(["name", "/app/admin/dict/get/dict_name"]);
apiResults["name"] = [];
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}: ${item.value}`;
if (item.children) {
travel(item.children);
}
}
}
travel(res.data);
},
complete: function () {
if (--count === 0) {
render();
}
},
});
});
}
loadDictNames();
// 渲染表格
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"]
});
}
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%":"500px"],
content: INSERT_URL
});
}
// 表格编辑数据
let edit = function(obj) {
let value = obj.data["name"];
layer.open({
type: 2,
title: "修改",
shade: 0.1,
area: [common.isModile()?"100%":"500px", common.isModile()?"100%":"500px"],
content: UPDATE_URL + "?name=" + value
});
}
// 删除一行
let remove = function(obj) {
return doRemove(obj.data["name"]);
}
// 删除多行
let batchRemove = function(obj) {
let checkIds = common.checkField(obj, "name");
if (checkIds === "") {
layui.popup.warning("未选中数据");
return false;
}
doRemove(checkIds.split(","));
}
// 执行删除
let doRemove = function (ids) {
let data = {};
data["name"] = 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
},
})
}
loadDictNames()
}
});
}
})
</script>
</body>
</html>
+224
View File
@@ -0,0 +1,224 @@
<!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>
<style>
.mainBox {
width: auto !important;
}
.layui-tab .layui-table-cell {
overflow:visible !important;
}
.layui-table-body ,.layui-table-box{
overflow:visible !important;
}
.layui-tab .layui-form-select dl {
max-height: 190px;
}
.layui-table-body .layui-table-col-special:last-child {
width: 100% !important;
border-right: 1px solid #eee !important;
}
xm-select {
min-height: 38px;
line-height: 38px;
}
xm-select .xm-body .xm-option .xm-option-icon {
font-size: 18px !important;
}
</style>
<form class="layui-form" action="" lay-filter="create-dict-form">
<div class="mainBox">
<div class="main-container mr-5">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label required" style="width:auto">字典名</label>
<div class="layui-input-inline">
<input type="text" name="name" required lay-verify="required" autocomplete="off" class="layui-input" placeholder="请输入英文字母组合">
</div>
</div>
</div>
<div>
<!-- 字段属性 -->
<table id="column-table" lay-filter="column-table"></table>
<script type="text/html" id="column-toolbar">
<button type="button" class="pear-btn pear-btn-primary pear-btn-md" lay-event="add">
<i class="layui-icon layui-icon-add-1"></i>新增
</button>
<button type="button" class="pear-btn pear-btn-danger pear-btn-md" lay-event="batchRemove">
<i class="layui-icon layui-icon-delete"></i>删除
</button>
</script>
<script type="text/html" id="col-value">
<input type="text" name="value[{{ d.LAY_NUM-1 }}][value]" placeholder="值" autocomplete="off" class="layui-input" value="{{ d.value }}">
<input type="hidden" name="value[{{ d.LAY_NUM-1 }}][_field_id]" value="{{ d._field_id }}">
</script>
<script type="text/html" id="col-name">
<input type="text" name="value[{{ d.LAY_NUM-1 }}][name]" placeholder="标题" autocomplete="off" class="layui-input" value="{{ d.name }}">
</script>
</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/dict/insert";
// 字段设置
layui.use(["table", "common", "popup"], function () {
let table = layui.table;
let common = layui.common;
let cols = [
{
type: "checkbox",
width: 50,
},
{
title: "值",
field: "value",
templet: "#col-value"
},
{
title: "标题",
field: "name",
templet: "#col-name",
}
];
window._field_id = 0;
let data = [{
_field_id: _field_id++,
value : "",
name: "",
}];
table.render({
elem: "#column-table",
cols: [cols],
data: data,
cellMinWidth: 40,
skin: "line",
size: "lg",
limit: 10000,
page: false,
toolbar: "#column-toolbar",
defaultToolbar: [],
});
table.on("toolbar(column-table)", function(obj) {
if (obj.event === "add") {
add();
} else if (obj.event === "batchRemove") {
batchRemove(obj);
}
});
let add = function() {
syncTableData();
let options = table.getData("column-table");
options.push({
_field_id: _field_id++,
value : "",
name: "",
});
table.reloadData("column-table", {data:options});
}
let batchRemove = function(obj) {
var checkIds = common.checkField(obj,"_field_id");
if (checkIds === "") return layui.popup.warning("未选中数据");
let data = table.getData("column-table");
let newData = [];
let deleteIds = checkIds.split(",");
layui.each(data, function (index, item) {
if (deleteIds.indexOf(item._field_id + "") === -1) {
newData.push(item);
}
});
table.reloadData("column-table", {data: newData})
}
});
layui.use(["form", "popup"], function () {
//提交事件
layui.form.on("submit(save)", function () {
let data = layui.form.val("create-dict-form");
layui.$.ajax({
url: INSERT_API,
type: "POST",
dateType: "json",
data: data,
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;
});
});
window.syncTableData = function () {
let tableData = layui.form.val("create-dict-form");
let columnTableData = [];
let len = Object.keys(tableData).length;
let id = 0;
window._key_id = 0;
while (id < len) {
// column data
if (typeof tableData["value[" + id + "][_field_id]"] !== "undefined") {
columnTableData.push({
_field_id: tableData["value[" + id + "][_field_id]"],
name : tableData["value[" + id + "][name]"],
value: tableData["value[" + id + "][value]"],
});
}
_key_id++;
id++;
}
layui.table.reloadData("column-table", {data: columnTableData});
}
</script>
</body>
</html>
+240
View File
@@ -0,0 +1,240 @@
<!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>
<style>
.mainBox {
width: auto !important;
}
.layui-tab .layui-table-cell {
overflow:visible !important;
}
.layui-table-body ,.layui-table-box{
overflow:visible !important;
}
.layui-tab .layui-form-select dl {
max-height: 190px;
}
.layui-table-body .layui-table-col-special:last-child {
width: 100% !important;
border-right: 1px solid #eee !important;
}
xm-select {
min-height: 38px;
line-height: 38px;
}
xm-select .xm-body .xm-option .xm-option-icon {
font-size: 18px !important;
}
</style>
<form class="layui-form" action="" lay-filter="create-dict-form">
<div class="mainBox">
<div class="main-container mr-5">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label required" style="width:auto">字典名</label>
<div class="layui-input-inline">
<input type="text" name="name" required lay-verify="required" disabled autocomplete="off" class="layui-input" placeholder="请输入英文字母组合">
</div>
</div>
</div>
<div>
<!-- 字段属性 -->
<table id="column-table" lay-filter="column-table"></table>
<script type="text/html" id="column-toolbar">
<button type="button" class="pear-btn pear-btn-primary pear-btn-md" lay-event="add">
<i class="layui-icon layui-icon-add-1"></i>新增
</button>
<button type="button" class="pear-btn pear-btn-danger pear-btn-md" lay-event="batchRemove">
<i class="layui-icon layui-icon-delete"></i>删除
</button>
</script>
<script type="text/html" id="col-value">
<input type="text" name="value[{{ d.LAY_NUM-1 }}][value]" placeholder="值" autocomplete="off" class="layui-input" value="{{ d.value }}">
<input type="hidden" name="value[{{ d.LAY_NUM-1 }}][_field_id]" value="{{ d._field_id }}">
</script>
<script type="text/html" id="col-name">
<input type="text" name="value[{{ d.LAY_NUM-1 }}][name]" placeholder="标题" autocomplete="off" class="layui-input" value="{{ d.name }}">
</script>
</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 DICT_NAME = layui.url().search.name;
const UPDATE_API = "/app/admin/dict/update";
const SELECT_API = "/app/admin/dict/get/" + DICT_NAME;
// 字段设置
layui.use(["table", "common", "popup"], function () {
let table = layui.table;
let common = layui.common;
let $ = layui.$;
let cols = [
{
type: "checkbox",
width: 50,
},
{
title: "值",
field: "value",
templet: "#col-value"
},
{
title: "标题",
field: "name",
templet: "#col-name",
}
];
$('input[name="name"]').val(DICT_NAME);
window._field_id = 0;
let data = [];
$.ajax({
url: SELECT_API,
dataType: "json",
async: false,
success: function (res) {
data = res.data;
layui.each(data, function (k, v) {
data[k]["_field_id"] = _field_id++;
})
// ajax产生错误
if (res.code) {
layui.popup.failure(res.msg);
}
}
});
table.render({
elem: "#column-table",
cols: [cols],
data: data,
cellMinWidth: 40,
skin: "line",
size: "lg",
limit: 10000,
page: false,
toolbar: "#column-toolbar",
defaultToolbar: [],
});
table.on("toolbar(column-table)", function(obj) {
if (obj.event === "add") {
add();
} else if (obj.event === "batchRemove") {
batchRemove(obj);
}
});
let add = function() {
syncTableData();
let options = table.getData("column-table");
options.push({
_field_id: _field_id++,
value : "",
name: "",
});
table.reloadData("column-table", {data:options});
}
let batchRemove = function(obj) {
var checkIds = common.checkField(obj,"_field_id");
if (checkIds === "") return layui.popup.warning("未选中数据");
let data = table.getData("column-table");
let newData = [];
let deleteIds = checkIds.split(",");
layui.each(data, function (index, item) {
if (deleteIds.indexOf(item._field_id + "") === -1) {
newData.push(item);
}
});
table.reloadData("column-table", {data: newData})
}
});
layui.use(["form", "popup"], function () {
//提交事件
layui.form.on("submit(save)", function () {
let data = layui.form.val("create-dict-form");
layui.$.ajax({
url: UPDATE_API,
type: "POST",
dateType: "json",
data: data,
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;
});
});
window.syncTableData = function () {
let tableData = layui.form.val("create-dict-form");
let columnTableData = [];
let len = Object.keys(tableData).length;
let id = 0;
window._key_id = 0;
while (id < len) {
// column data
if (typeof tableData["value[" + id + "][_field_id]"] !== "undefined") {
columnTableData.push({
_field_id: tableData["value[" + id + "][_field_id]"],
name : tableData["value[" + id + "][name]"],
value: tableData["value[" + id + "][value]"],
});
}
_key_id++;
id++;
}
layui.table.reloadData("column-table", {data: columnTableData});
}
</script>
</body>
</html>
+358
View File
@@ -0,0 +1,358 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<title>控制后台</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="/app/admin/component/pear/css/pear.css" />
<link rel="stylesheet" href="/app/admin/demos/css/console1.css" />
</head>
<body class="pear-container">
<div>
<div class="layui-row layui-col-space10">
<div class="layui-col-xs6 layui-col-md3">
<div class="layui-card top-panel">
<div class="layui-card-header">今日注册</div>
<div class="layui-card-body">
<div class="layui-row layui-col-space5">
<div class="layui-col-xs8 layui-col-md8 top-panel-number" style="color: #28333E;" id="value1">
0
</div>
<div class="layui-col-xs4 layui-col-md4 top-panel-tips">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 1024 1024" width="200" height="200" t="1591462258798"
p-id="942" version="1.1">
<path fill="#fcc66f" d="M 262.7 835 c -15.3 0 -28.1 -11.4 -29.8 -26.6 L 174.1 291 c -0.6 -5.1 1 -10.2 4.5 -14 s 8.3 -6 13.4 -6 h 640 c 5.1 0 10 2.2 13.4 6 s 5 8.9 4.5 14 l -58.8 517.4 c -1.7 15.2 -14.5 26.6 -29.8 26.6 H 262.7 Z"
p-id="943" />
<path fill="#ffd79c" d="M 802 289 l -58.8 517.4 c -0.7 6.1 -5.8 10.6 -11.9 10.6 h 30 c 6.1 0 11.2 -4.6 11.9 -10.6 L 832 289 h -30 Z"
p-id="944" />
<path fill="#f56e73" d="M 164 307 c -16.5 0 -30 -13.5 -30 -30 v -58 c 0 -16.5 13.5 -30 30 -30 h 696 c 16.5 0 30 13.5 30 30 v 58 c 0 16.5 -13.5 30 -30 30 H 164 Z"
p-id="945" />
<path fill="#ffa1a8" d="M 860 207 h -30 c 6.6 0 12 5.4 12 12 v 58 c 0 6.6 -5.4 12 -12 12 h 30 c 6.6 0 12 -5.4 12 -12 v -58 c 0 -6.6 -5.4 -12 -12 -12 Z"
p-id="946" />
<path fill="#65c8ff" d="M 190.9 651.5 c -31.4 0 -56.9 -25.5 -56.9 -56.9 V 219 c 0 -16.5 13.5 -30 30 -30 h 466.2 c 9.9 0 18 8.1 18 18 v 301.1 c 0 34.7 -28.2 62.9 -62.9 62.9 s -62.9 -28.2 -62.9 -62.9 V 393.5 c 0 -23.2 -18.8 -42 -42 -42 s -42 18.8 -42 42 v 68.1 c 0 29.4 -23.9 53.4 -53.4 53.4 s -53.4 -23.9 -53.4 -53.4 v -68.1 c 0 -23.2 -18.8 -42 -42 -42 s -42 18.8 -42 42 v 201.1 c 0.1 31.4 -25.4 56.9 -56.7 56.9 Z"
p-id="947" />
<path fill="#b3eaff" d="M 277.8 321.5 c -33.1 0 -60 26.9 -60 60 v 201.1 c 0 21.5 -17.4 38.9 -38.9 38.9 c -7.7 0 -14.8 -2.2 -20.8 -6.1 c 6.9 10.9 19 18.1 32.8 18.1 c 21.5 0 38.9 -17.4 38.9 -38.9 V 393.5 c 0 -33.1 26.9 -60 60 -60 c 13.5 0 25.9 4.5 36 12 c -11 -14.5 -28.4 -24 -48 -24 Z M 618.3 207 v 289.1 c 0 24.8 -20.1 44.9 -44.9 44.9 c -9.3 0 -18 -2.8 -25.2 -7.7 c 8.1 11.9 21.7 19.7 37.2 19.7 c 24.8 0 44.9 -20.1 44.9 -44.9 V 207 h -12 Z M 468.5 321.5 c -33.1 0 -60 26.9 -60 60 v 68.1 c 0 19.5 -15.8 35.4 -35.4 35.4 c -6.7 0 -12.9 -1.9 -18.3 -5.1 c 6.2 10.2 17.4 17.1 30.3 17.1 c 19.5 0 35.4 -15.8 35.4 -35.4 v -68.1 c 0 -33.1 26.9 -60 60 -60 c 13.5 0 25.9 4.5 36 12 c -11 -14.5 -28.4 -24 -48 -24 Z"
p-id="948" />
<path fill="#453b56" d="M 698 729.4 m -18 0 a 18 18 0 1 0 36 0 a 18 18 0 1 0 -36 0 Z" p-id="949" />
<path fill="#453b56" d="M 860 171 H 632.5 v 0.1 c -0.7 0 -1.5 -0.1 -2.2 -0.1 H 164 c -26.5 0 -48 21.5 -48 48 v 375.6 c 0 41.3 33.6 74.9 74.9 74.9 c 2.7 0 5.4 -0.2 8.1 -0.5 l 16 141.4 c 2.8 24.3 23.3 42.6 47.7 42.6 h 498.6 c 24.4 0 44.9 -18.3 47.7 -42.6 l 55.2 -485.6 c 24.5 -2.1 43.8 -22.7 43.8 -47.8 v -58 c 0 -26.5 -21.5 -48 -48 -48 Z M 190.9 633.5 c -21.5 0 -38.9 -17.4 -38.9 -38.9 V 219 c 0 -6.6 5.4 -12 12 -12 h 466.3 v 301.1 c 0 24.8 -20.1 44.9 -44.9 44.9 c -24.8 0 -44.9 -20.1 -44.9 -44.9 V 393.5 c 0 -33.1 -26.9 -60 -60 -60 s -60 26.9 -60 60 v 68.1 c 0 19.5 -15.8 35.4 -35.4 35.4 c -19.5 0 -35.4 -15.8 -35.4 -35.4 v -68.1 c 0 -33.1 -26.9 -60 -60 -60 s -60 26.9 -60 60 v 201.1 c 0.1 21.5 -17.4 38.9 -38.8 38.9 Z m 582.3 172.9 c -0.7 6.1 -5.8 10.6 -11.9 10.6 H 262.7 c -6.1 0 -11.2 -4.6 -11.9 -10.6 l -6.7 -59 h 396.6 c 9.9 0 18 -8.1 18 -18 s -8.1 -18 -18 -18 H 240 l -6.3 -55.4 c 19.3 -13.6 32.1 -36 32.1 -61.3 V 393.5 c 0 -13.2 10.8 -24 24 -24 s 24 10.8 24 24 v 68.1 c 0 39.4 32 71.4 71.4 71.4 s 71.4 -32 71.4 -71.4 v -68.1 c 0 -13.2 10.8 -24 24 -24 s 24 10.8 24 24 v 114.6 c 0 44.6 36.3 80.9 80.9 80.9 c 44.6 0 80.9 -36.3 80.9 -80.9 V 325 h 161.7 l -54.9 481.4 Z M 872 277 c 0 6.6 -5.4 12 -12 12 H 666.3 v -82 H 860 c 6.6 0 12 5.4 12 12 v 58 Z"
p-id="950" /></svg>
</div>
</div>
</div>
</div>
</div>
<div class="layui-col-xs6 layui-col-md3">
<div class="layui-card top-panel">
<div class="layui-card-header">7日内注册</div>
<div class="layui-card-body">
<div class="layui-row layui-col-space5">
<div class="layui-col-xs8 layui-col-md8 top-panel-number" style="color: #28333E;" id="value2">
0
</div>
<div class="layui-col-xs4 layui-col-md4 top-panel-tips">
<svg t="1591462430908" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="3170" width="200" height="200">
<path d="M532 784.2c0 24.4-19.8 44.3-44.3 44.3s-44.3-19.8-44.3-44.3c0-24.4 44.3-80.3 44.3-80.3s44.3 55.8 44.3 80.3zM766 784.2c0 24.4 19.8 44.3 44.3 44.3 24.4 0 44.3-19.8 44.3-44.3 0-24.4-44.3-80.3-44.3-80.3S766 759.7 766 784.2z"
fill="#97DCFF" p-id="3171"></path>
<path d="M123.5 471.3c-9.9 0-18-8.1-18-18v-302c0-9.9 8.1-18 18-18h58c9.9 0 18 8.1 18 18v302c0 9.9-8.1 18-18 18h-58z"
fill="#FCC66F" p-id="3172"></path>
<path d="M181.5 151.3v302h-58v-302h58m0-36h-58c-19.9 0-36 16.1-36 36v302c0 19.9 16.1 36 36 36h58c19.9 0 36-16.1 36-36v-302c0-19.8-16.1-36-36-36z"
fill="#453B56" p-id="3173"></path>
<path d="M266.4 210.7m-18 0a18 18 0 1 0 36 0 18 18 0 1 0-36 0Z" fill="#453B56" p-id="3174"></path>
<path d="M430.8 641.1c-9.9 0-18-8.1-18-18v-21.6c0-130.3 106-236.3 236.3-236.3s236.3 106 236.3 236.3v21.6c0 9.9-8.1 18-18 18H430.8z"
fill="#FCC66F" p-id="3175"></path>
<path d="M649 383.2c-5 0-10 0.2-15 0.6 113.5 7.7 203.3 102.2 203.3 217.7v21.6h30v-21.6c0-120.6-97.7-218.3-218.3-218.3z"
fill="#FFD79C" p-id="3176"></path>
<path d="M419.6 694.4c-22.1 0-40.1-18-40.1-40.1s18-40.1 40.1-40.1h458.8c22.1 0 40.1 18 40.1 40.1s-18 40.1-40.1 40.1H419.6z"
fill="#F56E73" p-id="3177"></path>
<path d="M878.4 632.3h-30c12.2 0 22.1 9.9 22.1 22.1s-9.9 22.1-22.1 22.1h30c12.2 0 22.1-9.9 22.1-22.1s-9.9-22.1-22.1-22.1z"
fill="#FFA1A8" p-id="3178"></path>
<path d="M693.3 846.4c0 24.4-19.8 44.3-44.3 44.3-24.4 0-44.3-19.8-44.3-44.3s44.3-80.3 44.3-80.3 44.3 55.9 44.3 80.3z"
fill="#97DCFF" p-id="3179"></path>
<path d="M649 908.7c-34.3 0-62.3-27.9-62.3-62.3 0-28.5 36.9-77.2 48.1-91.4 3.4-4.3 8.6-6.8 14.1-6.8s10.7 2.5 14.1 6.8c11.3 14.2 48.1 62.9 48.1 91.4 0.2 34.3-27.8 62.3-62.1 62.3z m0-112.3c-14.1 20.4-26.3 41.9-26.3 50 0 14.5 11.8 26.3 26.3 26.3s26.3-11.8 26.3-26.3c0-8.1-12.1-29.6-26.3-50z"
fill="#453B56" p-id="3180"></path>
<path d="M903.3 601.9v-0.5c0-134.1-104.4-244.3-236.3-253.6v-30.7c0-68.7-55.9-124.6-124.6-124.6H326.5c-9.9 0-18 8.1-18 18s8.1 18 18 18h215.9c48.8 0 88.6 39.7 88.6 88.6v30.7c-131.8 9.3-236.3 119.4-236.3 253.6v0.5c-19.6 9.3-33.2 29.3-33.2 52.4 0 32 26 58.1 58.1 58.1H459c-14.8 21-33.5 51.5-33.5 71.8 0 34.3 27.9 62.3 62.3 62.3 34.3 0 62.2-27.9 62.2-62.3 0-20.3-18.6-50.7-33.5-71.8h264.9c-14.8 21-33.5 51.5-33.5 71.8 0 34.3 27.9 62.3 62.3 62.3 34.3 0 62.3-27.9 62.3-62.3 0-20.3-18.6-50.7-33.5-71.8h39.4c32 0 58.1-26 58.1-58.1 0-23.1-13.6-43-33.2-52.4zM487.8 810.4c-14.5 0-26.3-11.8-26.3-26.3 0-8.1 12.1-29.6 26.3-50 14.1 20.4 26.2 41.9 26.2 50 0 14.5-11.8 26.3-26.2 26.3z m322.5 0c-14.5 0-26.3-11.8-26.3-26.3 0-8.1 12.1-29.6 26.3-50 14.1 20.4 26.3 41.9 26.3 50-0.1 14.5-11.9 26.3-26.3 26.3zM649 383.2c118.8 0 215.4 94.9 218.1 213.1H430.9c2.8-118.1 99.3-213.1 218.1-213.1z m251.5 271.1c0 12.2-9.9 22.1-22.1 22.1H419.6c-12.2 0-22.1-9.9-22.1-22.1 0-12.2 9.9-22.1 22.1-22.1h458.8c12.2 0.1 22.1 10 22.1 22.1z"
fill="#453B56" p-id="3181"></path>
</svg>
</div>
</div>
</div>
</div>
</div>
<div class="layui-col-xs6 layui-col-md3">
<div class="layui-card top-panel">
<div class="layui-card-header">30日内注册</div>
<div class="layui-card-body">
<div class="layui-row layui-col-space5">
<div class="layui-col-xs8 layui-col-md8 top-panel-number" style="color: #28333E;" id="value3">
0
</div>
<div class="layui-col-xs4 layui-col-md4 top-panel-tips">
<svg t="1591462464512" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="3311" width="200" height="200">
<path d="M750.4 216.5h-130v-15.3c0-32.9-26.8-59.7-59.7-59.7h-97.3c-32.9 0-59.7 26.8-59.7 59.7v15.3h-130c-30.7 0-55.6 25-55.6 55.6v72.4c0 9.9 8.1 18 18 18h31.5v478c0 23.2 18.8 42 42 42h405c23.2 0 42-18.8 42-42v-478H788c9.9 0 18-8.1 18-18v-72.4c0-30.6-25-55.6-55.6-55.6z"
fill="#FCC66F" p-id="3312"></path>
<path d="M708.5 344.5v496c0 13.3-10.7 24-24 24h30c13.3 0 24-10.7 24-24v-496h-30z" fill="#FFD79C" p-id="3313"></path>
<path d="M309.5 882.5c-23.2 0-42-18.8-42-42V596c0-9.9 8.1-18 18-18h36.8c30.2 0 54.8 24.6 54.8 54.8v231.7c0 9.9-8.1 18-18 18h-49.6zM664.9 882.5c-9.9 0-18-8.1-18-18V632.8c0-30.2 24.6-54.8 54.8-54.8h36.8c9.9 0 18 8.1 18 18v244.5c0 23.2-18.8 42-42 42h-49.6z"
fill="#F56E73" p-id="3314"></path>
<path d="M708.5 596v244.5c0 13.3-10.7 24-24 24h30c13.3 0 24-10.7 24-24V596h-30z" fill="#FFA1A8" p-id="3315"></path>
<path d="M475.2 882.5c-9.9 0-18-8.1-18-18V632.8c0-30.2 24.6-54.8 54.8-54.8 30.2 0 54.8 24.6 54.8 54.8v231.7c0 9.9-8.1 18-18 18h-73.6z"
fill="#F56E73" p-id="3316"></path>
<path d="M560.7 159.5h-18c23 0 41.7 18.7 41.7 41.7V221h18v-19.8c-0.1-23-18.7-41.7-41.7-41.7zM750.4 234.5h-30c20.8 0 37.6 16.8 37.6 37.6v72.4h30v-72.4c0-20.8-16.8-37.6-37.6-37.6z"
fill="#FFD79C" p-id="3317"></path>
<path d="M750.4 198.5H638.2c-1.4-41.6-35.6-75-77.5-75h-97.3c-41.9 0-76.1 33.4-77.5 75H273.6c-40.6 0-73.6 33-73.6 73.6v72.4c0 19.9 16.1 36 36 36h13.5v460c0 33.1 26.9 60 60 60H714.7c33.1 0 60-26.9 60-60v-460H788c19.9 0 36-16.1 36-36v-72.4c0-40.6-33-73.6-73.6-73.6z m-287.1-39h97.3c22.1 0 40.2 17.2 41.5 39H421.8c1.4-21.8 19.4-39 41.5-39z m-104.2 705h-49.6c-13.3 0-24-10.7-24-24V596h36.8c20.3 0 36.8 16.5 36.8 36.8v231.7z m189.7 0h-73.6V632.8c0-20.3 16.5-36.8 36.8-36.8 20.3 0 36.8 16.5 36.8 36.8v231.7z m189.7-24c0 13.3-10.7 24-24 24h-49.6V632.8c0-20.3 16.5-36.8 36.8-36.8h36.8v244.5z m0-280.5h-36.8c-40.1 0-72.8 32.6-72.8 72.8v231.7h-44.2V632.8c0-40.1-32.6-72.8-72.8-72.8-40.1 0-72.8 32.6-72.8 72.8v231.7h-44.2V632.8c0-40.1-32.6-72.8-72.8-72.8h-36.8v-74.5h279c9.9 0 18-8.1 18-18s-8.1-18-18-18h-279v-69h453V560zM788 344.5H236v-72.4c0-20.8 16.8-37.6 37.6-37.6h476.8c20.8 0 37.6 16.8 37.6 37.6v72.4z"
fill="#453B56" p-id="3318"></path>
<path d="M621.8 467.5m-18 0a18 18 0 1 0 36 0 18 18 0 1 0-36 0Z" fill="#453B56" p-id="3319"></path>
</svg>
</div>
</div>
</div>
</div>
</div>
<div class="layui-col-xs6 layui-col-md3">
<div class="layui-card top-panel">
<div class="layui-card-header">总注册</div>
<div class="layui-card-body">
<div class="layui-row layui-col-space5">
<div class="layui-col-xs8 layui-col-md8 top-panel-number" style="color: #28333E;" id="value4">
0
</div>
<div class="layui-col-xs4 layui-col-md4 top-panel-tips">
<svg t="1591462491887" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="3449" width="200" height="200">
<path d="M363.2 807c-9.9 0-18-8.1-18-18v-75.5c0-9.9 8.1-18 18-18h108.5c9.9 0 18 8.1 18 18V789c0 9.9-8.1 18-18 18H363.2z"
fill="#F56E73" p-id="3450"></path>
<path d="M441.7 713.5h30V789h-30z" fill="#FFA1A8" p-id="3451"></path>
<path d="M259.6 398c-9.9 0-18-8.1-18-18V178.6c0-23.8 19.3-43.1 43.1-43.1s43.1 19.3 43.1 43.1V380c0 9.9-8.1 18-18 18h-50.2zM525.1 398c-9.9 0-18-8.1-18-18V178.6c0-23.8 19.3-43.1 43.1-43.1s43.1 19.3 43.1 43.1V380c0 9.9-8.1 18-18 18h-50.2z"
fill="#65C8FF" p-id="3452"></path>
<path d="M550.2 153.5c-3.2 0-6.2 0.7-9 1.7 9.4 3.6 16.1 12.7 16.1 23.4V380h18V178.6c0.1-13.9-11.2-25.1-25.1-25.1z"
fill="#97DCFF" p-id="3453"></path>
<path d="M686 330.5H149c-9.9 0-18 8.1-18 18v63c0 9.9 8.1 18 18 18h33.2l45 225c8.7 43.4 47.1 75 91.4 75h197.6c44.3 0 82.7-31.5 91.4-75l45-225H686c9.9 0 18-8.1 18-18v-63c0-9.9-8.1-18-18-18z"
fill="#FCC66F" p-id="3454"></path>
<path d="M608 411.5L560.1 651c-7 35.2-37.9 60.5-73.8 60.5h30c35.9 0 66.7-25.3 73.8-60.5L638 411.5h-30zM656 348.5h30v63h-30z"
fill="#FFD79C" p-id="3455"></path>
<path d="M474.2 543.5m-18 0a18 18 0 1 0 36 0 18 18 0 1 0-36 0Z" fill="#453B56" p-id="3456"></path>
<path d="M416.9 525.5h-125c-9.9 0-18 8.1-18 18s8.1 18 18 18h125c9.9 0 18-8.1 18-18s-8.1-18-18-18zM893 543.5h-33.4c-65.2 0-118.2 53-118.2 118.2v19.6c0 9.9 8.1 18 18 18s18-8.1 18-18v-19.6c0-45.3 36.9-82.2 82.2-82.2H893c9.9 0 18-8.1 18-18s-8-18-18-18zM772.2 744.2c7-7 7-18.4 0-25.5-7-7-18.4-7-25.5 0s-7 18.4 0 25.5 18.4 7.1 25.5 0z"
fill="#453B56" p-id="3457"></path>
<path d="M759.5 761.6c-9.9 0-18 8.1-18 18v11.6c0 43.7-35.6 79.3-79.3 79.3H487.3c-26.4 0-48.3-19.9-51.4-45.5h35.8c19.9 0 36-16.1 36-36v-41.5h8.6c52.8 0 98.7-37.6 109.1-89.4l42.1-210.6H686c19.9 0 36-16.1 36-36v-63c0-19.9-16.1-36-36-36h-74.6V178.6c0-33.7-27.4-61.1-61.1-61.1s-61.1 27.4-61.1 61.1v133.9H345.9V178.6c0-33.7-27.4-61.1-61.1-61.1s-61.1 27.4-61.1 61.1v133.9H149c-19.9 0-36 16.1-36 36v63c0 19.9 16.1 36 36 36h18.5l42.1 210.6c10.4 51.8 56.2 89.4 109.1 89.4h8.6V789c0 19.9 16.1 36 36 36h36.6c3.3 45.5 41.2 81.5 87.5 81.5h174.8c63.6 0 115.3-51.7 115.3-115.3v-11.6c0-10-8.1-18-18-18z m-234.4-583c0-13.9 11.2-25.1 25.1-25.1s25.1 11.2 25.1 25.1v133.9H525V178.6z m-265.5 0c0-13.9 11.2-25.1 25.1-25.1s25.1 11.2 25.1 25.1v133.9h-50.3V178.6zM149 411.5v-63h537v63H149z m169.7 300c-35.9 0-66.7-25.3-73.8-60.5l-40.7-203.5h426.6L590.1 651c-7 35.2-37.9 60.5-73.8 60.5H318.7z m44.5 77.5v-41.5h108.5V789H363.2z"
fill="#453B56" p-id="3458"></path>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="layui-row layui-col-space10">
<div class="layui-col-md9">
<div class="layui-card">
<div class="layui-card-body">
<div class="layui-tab custom-tab layui-tab-brief" lay-filter="docDemoTabBrief">
<div id="echarts-records" style="background-color:#ffffff;min-height:400px;padding: 10px"></div>
</div>
</div>
</div>
</div>
<div class="layui-col-md3">
<div class="layui-card">
<div class="layui-card-header">系统信息</div>
<div class="layui-card-body">
<table class="layui-table">
<colgroup>
<col width="50%">
<col>
</colgroup>
<tbody>
<tr>
<td>Workerman版本</td>
<td><?=$workerman_version?></td>
</tr>
<tr>
<td>Webman版本</td>
<td><?=$webman_version?></td>
</tr>
<tr>
<td>WebmanAdmin版本</td>
<td><?=$admin_version?></td>
</tr>
<tr>
<td>PHP版本</td>
<td><?=$php_version?></td>
</tr>
<tr>
<td>MYSQL版本</td>
<td><?=$mysql_version?></td>
</tr>
<tr>
<td>操作系统</td>
<td><?=$os?></td>
</tr>
</tbody>
</table>
</div>
</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>
layui.use(['layer', 'echarts', 'element', 'count'], function() {
var $ = layui.jquery,
layer = layui.layer,
element = layui.element,
count = layui.count,
echarts = layui.echarts;
count.up("value1", {
time: 4000,
num: <?=$today_user_count?>,
bit: 0,
regulator: 50
})
count.up("value2", {
time: 4000,
num: <?=$day7_user_count?>,
bit: 0,
regulator: 50
})
count.up("value3", {
time: 4000,
num: <?=$day30_user_count?>,
bit: 0,
regulator: 50
})
count.up("value4", {
time: 4000,
bit: 0,
num: <?=$user_count?>,
regulator: 50
})
var echartsRecords = echarts.init(document.getElementById('echarts-records'), 'walden');
const colorList = ["#9E87FF", '#73DDFF', '#fe9a8b', '#F56948', '#9E87FF']
var option = {
backgroundColor: '#fff',
tooltip: {
show: false
},
grid: {
top: '10%',
bottom: '6%',
left: '6%',
right: '6%',
containLabel: true
},
xAxis: [{
type: 'category',
boundaryGap: false,
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
margin: 10,
//textStyle: {
fontSize: 14,
color: 'rgba(#999)'
//}
},
splitLine: {
show: true,
lineStyle: {
color: '#939ab6',
opacity: .15
}
},
data: <?=json_encode(array_keys($day7_detail))?>
}, ],
yAxis: [{
type: 'value',
offset: 15,
max: 100,
min: 0,
axisTick: {
show: false
},
axisLine: {
show: false
},
axisLabel: {
margin: 10,
//textStyle: {
fontSize: 14,
color: '#999'
//}
},
splitLine: {
show: false
}
}],
series: [{
name: '2',
type: 'line',
z: 3,
showSymbol: false,
smoothMonotone: 'x',
lineStyle: {
width: 3,
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0,
color: 'rgba(59,102,246)' // 0% 处的颜色
}, {
offset: 1,
color: 'rgba(118,237,252)' // 100% 处的颜色
}]
},
shadowBlur: 4,
shadowColor: 'rgba(69,126,247,.2)',
shadowOffsetY: 4
},
areaStyle: {
//normal: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0,
color: 'rgba(227,233,250,.9)' // 0% 处的颜色
}, {
offset: 1,
color: 'rgba(248,251,252,.3)' // 100% 处的颜色
}]
}
//}
},
smooth: true,
data: <?=json_encode(array_values($day7_detail))?>
},]
};
echartsRecords.setOption(option);
window.onresize = function() {
echartsRecords.resize();
}
});
</script>
</body>
</html>
+143
View File
@@ -0,0 +1,143 @@
<!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/loader.css" />
<!-- 布 局 样 式 -->
<link rel="stylesheet" href="/app/admin/admin/css/admin.css" />
<!-- 重置样式 -->
<link rel="stylesheet" href="/app/admin/admin/css/reset.css" />
</head>
<!-- 结 构 代 码 -->
<body class="layui-layout-body pear-admin">
<!-- 布 局 框 架 -->
<div class="layui-layout layui-layout-admin">
<!-- 顶 部 样 式 -->
<div class="layui-header">
<!-- 菜 单 顶 部 -->
<div class="layui-logo">
<!-- 图 标 -->
<img class="logo">
<!-- 标 题 -->
<span class="title"></span>
</div>
<!-- 顶 部 左 侧 功 能 -->
<ul class="layui-nav layui-layout-left">
<li class="collapse layui-nav-item" id="menu_but"><a href="#" id="menu_but1" class="layui-icon layui-icon-shrink-right"></a></li>
<li class="refresh layui-nav-item"><a href="#" class="layui-icon layui-icon-refresh-1" loading = 600></a></li>
</ul>
<!-- 多 系 统 菜 单 -->
<div id="control" class="layui-layout-control"></div>
<!-- 顶 部 右 侧 菜 单 -->
<ul class="layui-nav layui-layout-right">
<li class="layui-nav-item layui-hide-xs"><a href="#" class="menuSearch layui-icon layui-icon-search"></a></li>
<li class="layui-nav-item layui-hide-xs"><a href="#" class="fullScreen layui-icon layui-icon-screen-full"></a></li>
<li class="layui-nav-item layui-hide-xs message"></li>
<li class="layui-nav-item user">
<!-- 头 像 -->
<a class="layui-icon layui-icon-username" href="javascript:;"></a>
<!-- 功 能 菜 单 -->
<dl class="layui-nav-child">
<dd><a user-menu-url="/app/admin/account/index" user-menu-id="10" user-menu-title="基本资料">基本资料</a></dd>
<dd><a href="javascript:void(0);" class="logout">注销登录</a></dd>
</dl>
</li>
<!-- 主 题 配 置 -->
<li class="layui-nav-item setting"><a href="#" class="layui-icon layui-icon-more-vertical"></a></li>
</ul>
</div>
<!-- 侧 边 区 域 -->
<div class="layui-side layui-bg-black">
<!-- 菜 单 顶 部 -->
<div class="layui-logo">
<!-- 图 标 -->
<img class="logo">
<!-- 标 题 -->
<a href="/"><span class="title"></span></a>
</div>
<!-- 菜 单 内 容 -->
<div class="layui-side-scroll">
<div id="sideMenu"></div>
</div>
</div>
<!-- 视 图 页 面 -->
<div class="layui-body">
<!-- 内 容 页 面 -->
<div id="content"></div>
</div>
<!-- 页脚 -->
<div class="layui-footer layui-text">
<span class="left">
Released under the MIT license.
</span>
<span class="center"></span>
</div>
<!-- 遮 盖 层 -->
<div class="pear-cover"></div>
<!-- 加 载 动 画 -->
<div class="loader-main">
<!-- 动 画 对 象 -->
<div class="loader"></div>
</div>
</div>
<!-- 移 动 端 便 捷 操 作 -->
<div class="pear-collapsed-pe collapse">
<a href="#" class="layui-icon layui-icon-shrink-right"></a>
</div>
<!-- 依 赖 脚 本 -->
<script src="/app/admin/component/layui/layui.js?v=2.8.12"></script>
<script src="/app/admin/component/pear/pear.js"></script>
<!-- 框 架 初 始 化 -->
<script>
// Admin
window.Admin = {
Account: {}
};
layui.use(["admin","jquery","popup","drawer"], function() {
var $ = layui.$;
var admin = layui.admin;
var popup = layui.popup;
admin.setConfigType("json");
admin.setConfigPath("/app/admin/config/get");
admin.render();
// 登出逻辑
admin.logout(function(){
$.ajax({
url: "/app/admin/account/logout",
dataType: "json",
success: function (res) {
if (res.code) {
return popup.error(res.msg);
}
popup.success("注销成功",function(){
location.reload();
})
}
});
return false;
})
$.ajax({
url: "/app/admin/account/info",
dataType: 'json',
success: function (res) {
window.Admin.Account = res.data;
}
});
// 消息点击回调
//admin.message(function(id, title, context, form) {});
});
</script>
</body>
</html>
+214
View File
@@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<title>Webman Admin 安装</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-row layui-col-space10">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card">
<div class="layui-card-body" style="padding-top: 40px;">
<h1 style="text-align:center;margin: 20px 0 40px">Webman Admin 安装</h1>
<div class="layui-carousel" id="stepForm" lay-filter="stepForm" style="margin: 0 auto;">
<div carousel-item>
<div>
<form class="layui-form" action="javascript:void(0);" style="margin: 0 auto;max-width: 460px;padding-top: 40px;">
<div class="layui-form-item">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" placeholder="请填写入用户名" name="user" class="layui-input" lay-verify="required" required value="root" autocomplete="off"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">密码</label>
<div class="layui-input-block">
<input type="password" placeholder="请填写入密码" name="password" class="layui-input" autocomplete="off"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">数据库</label>
<div class="layui-input-block">
<input type="text" placeholder="请填写入数据库" name="database" class="layui-input" lay-verify="required" required value="webman_admin"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">host</label>
<div class="layui-input-block">
<input type="text" placeholder="请填写入数据库host" name="host" class="layui-input" lay-verify="required" required value="127.0.0.1"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">端口</label>
<div class="layui-input-block">
<input type="number" placeholder="请填写入数据库端口" name="port" class="layui-input" required value="3306" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">强制覆盖</label>
<div class="layui-input-block">
<input type="checkbox" name="overwrite" lay-skin="primary">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="button" class="pear-btn next">跳过此步骤</button>
<button class="pear-btn pear-btn-primary" lay-submit lay-filter="formStep">
&emsp;下一步&emsp;
</button>
</div>
</div>
</form>
</div>
<div>
<form class="layui-form" action="javascript:void(0);" style="margin: 0 auto;max-width: 460px;padding-top: 40px;">
<div class="layui-form-item">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" placeholder="请填写入用户名" name="username" class="layui-input" lay-verify="required" required />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">密码</label>
<div class="layui-input-block">
<input type="password" placeholder="请填写入密码" name="password" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">确认密码</label>
<div class="layui-input-block">
<input type="password" placeholder="请填再次写入密码" name="password_confirm" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="button" class="pear-btn pre">上一步</button>
<button class="pear-btn pear-btn-primary" lay-submit lay-filter="formStep2">
&emsp;提交&emsp;
</button>
</div>
</div>
</form>
</div>
<div>
<div style="text-align: center;margin-top: 90px;">
<i class="layui-icon layui-circle pear-back" style="color: white;font-size:30px;font-weight:bold;background: #52C41A;padding: 20px;line-height: 80px;">&#xe605;</i>
<div style="font-size: 24px;color: #333;font-weight: 500;margin-top: 30px;">
安装成功
</div>
<div style="font-size: 14px;color: #666;margin-top: 20px;">需要重启才能生效</div>
</div>
<div style="text-align: center;margin-top: 50px;">
<button class="pear-btn pear-btn-primary finish">进入后台</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<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", "step","code","element", "popup"], function() {
var $ = layui.$,
form = layui.form,
step = layui.step;
layui.code();
step.render({
elem: "#stepForm",
filter: "stepForm",
width: "100%",
stepWidth: "70%",
height: "500px",
stepItems: [{
title: "填写数据库信息"
}, {
title: "填写管理员账户"
}, {
title: "完成"
}]
});
form.on("submit(formStep)", function(data) {
let loading = layer.load();
$.ajax({
url: "/app/admin/install/step1",
type: "POST",
dataType: "json",
data: data.field,
success: function (res) {
if (res.code) {
return layui.popup.failure(res.msg);
}
layui.popup.success("操作成功", function () {
step.next("#stepForm");
});
},
complete: function () {
layer.close(loading);
}
})
return false;
});
form.on("submit(formStep2)", function(data) {
let loading = layer.load();
$.ajax({
url: "/app/admin/install/step2",
type: "POST",
dataType: "json",
data: data.field,
success: function (res) {
if (res.code) {
return layui.popup.failure(res.msg);
}
//layui.popup.success("安装成功");
step.next("#stepForm");
layer.close(loading);
},
complete: function () {
layer.close(loading);
}
})
return false;
});
$(".pre").click(function() {
step.pre("#stepForm");
return false;
});
$(".next").click(function() {
step.next("#stepForm");
return false;
});
$(".finish").click(function() {
location.reload();
});
})
</script>
</body>
</html>
+280
View File
@@ -0,0 +1,280 @@
<!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">
<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.location.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.location.delete">
<i class="layui-icon layui-icon-delete"></i>删除
</button>
</script>
<!-- 表格行工具栏 -->
<script type="text/html" id="table-bar">
<button class="pear-btn pear-btn-xs tool-btn" lay-event="edit" permission="app.admin.location.update">编辑</button>
<button class="pear-btn pear-btn-xs tool-btn" lay-event="remove" permission="app.admin.location.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/location/select";
const UPDATE_API = "/app/admin/location/update";
const DELETE_API = "/app/admin/location/delete";
const INSERT_URL = "/app/admin/location/insert";
const UPDATE_URL = "/app/admin/location/update";
// 表格渲染
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",
align: "center"
},{
title: "唯一标识",align: "center",
field: "id",
},{
title: "港口/仓库名称",align: "center",
field: "name",
},{
title: "英文名称",align: "center",
field: "name_en",
},{
title: "详细地址",align: "center",
field: "address",
},{
title: "所在地区",align: "center",
field: "area",
},{
title: "类型:港口/仓库",align: "center",
field: "type",
templet: function (d) {
let field = "type";
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: "车辆数",align: "center",
field: "num",
},{
title: "创建时间",align: "center",
field: "created_at",
},{
title: "更新时间",align: "center",
field: "updated_at",
},{
title: "操作",
toolbar: "#table-bar",
align: "center",
fixed: "right",
width: 120,
}
];
// 渲染表格
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 apiResults = {};
apiResults["type"] = [];
// 编辑或删除行事件
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: []
})
});
// 字段允许为空
form.verify({
phone: [/(^$)|^1\d{10}$/, "请输入正确的手机号"],
email: [/(^$)|^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/, "邮箱格式不正确"],
url: [/(^$)|(^#)|(^http(s*):\/\/[^\s]+\.[^\s]+)/, "链接格式不正确"],
number: [/(^$)|^\d+$/,'只能填写数字'],
date: [/(^$)|^(\d{4})[-\/](\d{1}|0\d{1}|1[0-2])([-\/](\d{1}|0\d{1}|[1-2][0-9]|3[0-1]))*$/, "日期格式不正确"],
identity: [/(^$)|(^\d{15}$)|(^\d{17}(x|X|\d)$)/, "请输入正确的身份证号"]
});
// 表格排序事件
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,
maxmin: true,
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,
maxmin: true,
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>
+153
View File
@@ -0,0 +1,153 @@
<!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/component/jsoneditor/css/jsoneditor.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">
<input type="text" name="name" 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="name_en" 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="address" 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="area" 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">-->
<!-- <div name="type" id="type" value="port" ></div>-->
<!-- </div>-->
<!-- </div>-->
<!-- -->
<!-- <div class="layui-form-item">-->
<!-- <label class="layui-form-label">车辆数</label>-->
<!-- <div class="layui-input-block">-->
<!-- <input type="number" name="num" 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/component/jsoneditor/jsoneditor.js"></script>
<script src="/app/admin/admin/js/permission.js"></script>
<script>
// 相关接口
const INSERT_API = "/app/admin/location/insert";
// 字段 类型:港口/仓库 type
layui.use(["jquery", "xmSelect"], function() {
let value = layui.$("#type").attr("value");
let initValue = value ? value.split(",") : [];
layui.xmSelect.render({
el: "#type",
name: "type",
filterable: true,
initValue: initValue,
value: "port",
model: {"icon":"hidden","label":{"type":"text"}},
clickClose: true,
radio: true,
})
});
// 字段 创建时间 created_at
layui.use(["laydate"], function() {
layui.laydate.render({
elem: "#created_at",
type: "datetime",
});
})
// 字段 更新时间 updated_at
layui.use(["laydate"], function() {
layui.laydate.render({
elem: "#updated_at",
type: "datetime",
});
})
//提交事件
layui.use(["form", "popup"], function () {
// 字段验证允许为空
layui.form.verify({
phone: [/(^$)|^1\d{10}$/, "请输入正确的手机号"],
email: [/(^$)|^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/, "邮箱格式不正确"],
url: [/(^$)|(^#)|(^http(s*):\/\/[^\s]+\.[^\s]+)/, "链接格式不正确"],
number: [/(^$)|^\d+$/,'只能填写数字'],
date: [/(^$)|^(\d{4})[-\/](\d{1}|0\d{1}|1[0-2])([-\/](\d{1}|0\d{1}|[1-2][0-9]|3[0-1]))*$/, "日期格式不正确"],
identity: [/(^$)|(^\d{15}$)|(^\d{17}(x|X|\d)$)/, "请输入正确的身份证号"]
});
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/component/jsoneditor/css/jsoneditor.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">
<input type="text" name="name" 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="name_en" 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="address" value="" 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="area" 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">
<div name="type" id="type" value="" ></div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">车辆数</label>
<div class="layui-input-block">
<input type="number" name="num" 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="created_at" id="created_at" 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="updated_at" id="updated_at" autocomplete="off" 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/component/jsoneditor/jsoneditor.js"></script>
<script src="/app/admin/admin/js/permission.js"></script>
<script>
// 相关接口
const PRIMARY_KEY = "id";
const SELECT_API = "/app/admin/location/select" + location.search;
const UPDATE_API = "/app/admin/location/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(value);
} else {
obj.attr("value", value);
obj[0].value = value;
}
// 多图渲染
if (obj[0].classList.contains('uploader-list')) {
let multiple_images = value.split(",");
$.each(multiple_images, function(index, value) {
$('#uploader-list-'+ key).append(
'<div class="file-iteme">' +
'<div class="handle"><i class="layui-icon layui-icon-delete"></i></div>' +
'<img src='+value +' alt="'+ value +'" >' +
'</div>'
);
});
}
});
// 字段 类型:港口/仓库 type
layui.use(["jquery", "xmSelect"], function() {
let value = layui.$("#type").attr("value");
let initValue = value ? value.split(",") : [];
layui.xmSelect.render({
el: "#type",
name: "type",
filterable: true,
initValue: initValue,
model: {"icon":"hidden","label":{"type":"text"}},
clickClose: true,
radio: true,
})
});
// 字段 创建时间 created_at
layui.use(["laydate"], function() {
layui.laydate.render({
elem: "#created_at",
type: "datetime",
});
})
// 字段 更新时间 updated_at
layui.use(["laydate"], function() {
layui.laydate.render({
elem: "#updated_at",
type: "datetime",
});
})
// ajax返回失败
if (res.code) {
layui.popup.failure(res.msg);
}
}
});
});
//提交事件
layui.use(["form", "popup"], function () {
// 字段验证允许为空
layui.form.verify({
phone: [/(^$)|^1\d{10}$/, "请输入正确的手机号"],
email: [/(^$)|^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/, "邮箱格式不正确"],
url: [/(^$)|(^#)|(^http(s*):\/\/[^\s]+\.[^\s]+)/, "链接格式不正确"],
number: [/(^$)|^\d+$/,'只能填写数字'],
date: [/(^$)|^(\d{4})[-\/](\d{1}|0\d{1}|1[0-2])([-\/](\d{1}|0\d{1}|[1-2][0-9]|3[0-1]))*$/, "日期格式不正确"],
identity: [/(^$)|(^\d{15}$)|(^\d{17}(x|X|\d)$)/, "请输入正确的身份证号"]
});
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>
+310
View File
@@ -0,0 +1,310 @@
<!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">
<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.role.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.role.delete">
<i class="layui-icon layui-icon-delete"></i>删除
</button>
</script>
<!-- 表格行工具栏 -->
<script type="text/html" id="table-bar">
{{# if(d.id!==1&&d.pid&&!d.isRoot){ }}
<button class="pear-btn pear-btn-xs tool-btn" lay-event="edit" permission="app.admin.role.update">编辑</button>
<button class="pear-btn pear-btn-xs tool-btn" lay-event="remove" permission="app.admin.role.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/role/select";
const UPDATE_API = "/app/admin/role/update";
const DELETE_API = "/app/admin/role/delete";
const INSERT_URL = "/app/admin/role/insert";
const UPDATE_URL = "/app/admin/role/update";
// 表格渲染
layui.use(["table", "treetable", "form", "common", "popup", "util"], function() {
let treeTable = layui.treetable;
let table = layui.table;
let form = layui.form;
let $ = layui.$;
let common = layui.common;
let util = layui.util;
// 表头参数
let cols = [
{
type: "checkbox"
},{
title: "角色组",
field: "name",
},{
title: "主键",
field: "id",
},{
title: "权限",
field: "rules",
templet: function (d) {
let field = "rules";
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(","));
},
hide: true,
},{
title: "创建时间",
field: "created_at",
},{
title: "更新时间",
field: "updated_at",
},{
title: "父级",
field: "pid",
templet: function (d) {
let field = "pid";
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(","));
},
hide: true,
},{
title: "操作",
toolbar: "#table-bar",
align: "center",
fixed: "right",
width: 120,
}
];
// 渲染表格
function render()
{
treeTable.render({
elem: "#data-table",
url: SELECT_API,
treeColIndex: 1,
treeIdName: "id",
treePidName: "pid",
treeDefaultClose: false,
cols: [cols],
skin: "line",
size: "lg",
toolbar: "#table-toolbar",
defaultToolbar: [{
title: "刷新",
layEvent: "refresh",
icon: "layui-icon-refresh",
}, "filter", "print", "exports"]
});
}
// 获取表格中下拉或树形组件数据
let apis = [];
apis.push(["rules", "/app/admin/rule/get?type=0,1,2"]);
apis.push(["pid", "/app/admin/role/select?format=tree"]);
let apiResults = {};
apiResults["rules"] = [];
apiResults["pid"] = [];
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>
+149
View File
@@ -0,0 +1,149 @@
<!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">父级</label>
<div class="layui-input-block">
<div name="pid" id="pid" value="1" ></div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label required">角色名</label>
<div class="layui-input-block">
<input type="text" name="name" 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">
<div name="rules" id="rules" value="" ></div>
</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/role/insert";
// 字段 权限 rules
layui.use(["jquery", "xmSelect", "popup"], function() {
layui.$.ajax({
url: "/app/admin/role/rules?id=1",
dataType: "json",
success: function (res) {
let value = layui.$("#rules").attr("value");
let initValue = value ? value.split(",") : [];
layui.xmSelect.render({
el: "#rules",
name: "rules",
initValue: initValue,
data: res.data,
tree: {"show":true,expandedKeys:initValue},
toolbar: {show:true,list:["ALL","CLEAR","REVERSE"]},
})
}
});
});
// 字段 父级 pid
layui.use(["jquery", "xmSelect", "popup"], function() {
layui.$.ajax({
url: "/app/admin/role/select?format=tree",
dataType: "json",
success: function (res) {
let value = layui.$("#pid").attr("value");
let initValue = value ? value.split(",") : [];
layui.xmSelect.render({
el: "#pid",
name: "pid",
initValue: initValue,
tips: "请选择",
data: res.data,
value: "0",
model: {"icon":"hidden","label":{"type":"text"}},
clickClose: true,
radio: true,
tree: {show: true,"strict":false,"clickCheck":true,"clickExpand":false,expandedKeys:true},
on: function(data){
let id = data.arr[0] ? data.arr[0].value : "";
if (!id) return;
layui.$.ajax({
url: '/app/admin/role/rules?id=' + id,
dataType: 'json',
success: function (res) {
if (res.code) {
return layui.popup.failure(res.msg);
}
layui.xmSelect.get('#rules')[0].update({data:res.data});
}
});
}
})
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>

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