first commit

This commit is contained in:
JaguarJack
2022-12-05 23:01:12 +08:00
commit 0024080c28
322 changed files with 27698 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
<?php
namespace Modules\Develop\Http\Controllers;
use Catch\Base\CatchController as Controller;
use Exception;
use Illuminate\Http\Request;
use Modules\Develop\Support\Generate\Generator;
class GenerateController extends Controller
{
/**
* @param Request $request
* @param Generator $generator
* @throws Exception
*/
public function index(Request $request, Generator $generator)
{
$generator->setParams($request->all())->generate();
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Modules\Develop\Http\Controllers;
use Catch\Base\CatchController;
use Catch\Support\Module\ModuleRepository;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
class ModuleController extends CatchController
{
protected ModuleRepository $repository;
/**
* @param ModuleRepository $repository
*/
public function __construct(ModuleRepository $repository)
{
$this->repository = $repository;
}
/**
* index
*
* @param Request $request
* @return Collection
*/
public function index(Request $request): Collection
{
return $this->repository->all($request->all());
}
/**
* store
*
* @param Request $request
* @return bool|int
*/
public function store(Request $request): bool|int
{
return $this->repository->create($request->all());
}
/**
* show
*
* @param string $name
* @return Collection
* @throws \Exception
*/
public function show(mixed $name): Collection
{
return $this->repository->show($name);
}
/**
* update
*
* @param $name
* @param Request $request
* @return bool|int
*/
public function update($name, Request $request): bool|int
{
return $this->repository->update($name, $request->all());
}
/**
* update
*
* @param $name
* @return bool|int
*/
public function enable($name): bool|int
{
return $this->repository->disOrEnable($name);
}
/**
* destroy
*
* @param $name
* @return bool|int
* @throws \Exception
*/
public function destroy($name): bool|int
{
return $this->repository->delete($name);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Modules\Develop\Http\Controllers;
use Catch\Base\CatchController;
use Illuminate\Http\Request;
use Modules\Develop\Models\Schemas;
/**
* SchemaController
*/
class SchemaController extends CatchController
{
public function __construct(
protected Schemas $schemas
) {
}
/**
* @return mixed
*/
public function index()
{
return $this->schemas->getList();
}
/**
* store
*
* @param Request $request
* @throws \Exception
* @return bool
*/
public function store(Request $request)
{
return $this->schemas->storeBy($request->all());
}
/**
* show
*
* @param $id
* @return mixed
*/
public function show($id)
{
return $this->schemas->show($id);
}
/**
* destroy
*
* @param $id
* @return bool|null
*/
public function destroy($id)
{
return $this->schemas->deleteBy($id);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Modules\Develop\Listeners;
use Catch\Events\Module\Created;
use Modules\Develop\Support\Generate\Module;
class CreatedListener
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
*
*
* @param Created $event
* @return void
*/
public function handle(Created $event): void
{
$module = $event->module;
(new Module(
$module['path'],
$module['dirs']['controllers'],
$module['dirs']['models'],
$module['dirs']['requests'],
$module['dirs']['database']
)
)->create();
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Modules\Develop\Listeners;
use Catch\CatchAdmin;
use Catch\Events\Module\Deleted;
class DeletedListener
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
*
*
* @param Deleted $event
* @return void
*/
public function handle(Deleted $event): void
{
CatchAdmin::deleteModulePath($event->module['path']);
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Modules\Develop\Models;
use Catch\Base\CatchModel;
use Catch\Enums\Status;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Modules\Develop\Support\Generate\Create\Schema;
use Illuminate\Support\Facades\Schema as SchemaFacade;
class Schemas extends CatchModel
{
/**
* @var string
*/
protected $table = 'schemas';
/**
* @var string[]
*/
protected $fillable = [
'id', 'module', 'name', 'columns', 'is_soft_delete', 'created_at', 'updated_at'
];
/**
* @var bool
*/
protected bool $isPaginate = true;
/**
* @var array|string[]
*/
public array $searchable = ['module' => 'like', 'name' => 'like'];
/**
* @var string[]
*/
protected array $mergeCasts = [
'is_soft_delete' => Status::class
];
/**
*
* @param array $data
* @return boolean
* @throws Exception
*/
public function storeBy(array $data): bool
{
$schema = $data['schema'];
$structures = $data['structures'];
$schemaId = parent::storeBy([
'module' => $schema['module'],
'name' => $schema['name'],
'columns' => implode(',', array_column($structures, 'field')),
'is_soft_delete' => $schema['deleted_at'] ? Status::Enable : Status::Disable
]);
try {
$schemaCreate = new Schema($schema['name'], $schema['engine'], $schema['charset'], $schema['collection'], $schema['comment']);
$schemaCreate->setStructures($structures)
->setModule($schema['module'])
->setCreatedAt($schema['created_at'])
->setCreatorId($schema['creator_id'])
->setUpdatedAt($schema['updated_at'])
->setDeletedAt($schema['deleted_at'])
->create();
} catch (Exception $e) {
parent::deleteBy($schemaId, true);
throw $e;
}
return true;
}
/**
* @param $id
* @return Model
*/
public function show($id): Model
{
$schema = parent::firstBy($id);
$columns = [];
foreach (getTableColumns($schema->name) as $columnString) {
$column = DB::connection()->getDoctrineColumn(DB::connection()->getTablePrefix().$schema->name, $columnString);
$columns[] = [
'name' => $column->getName(),
'type' => $column->getType()->getName(),
'nullable' => ! $column->getNotnull(),
'default' => $column->getDefault(),
'comment' => $column->getComment()
];
}
$schema->columns = $columns;
return $schema;
}
/**
* delete
*
* @param $id
* @param bool $force
* @return bool|null
*/
public function deleteBy($id, bool $force = false): ?bool
{
$schema = parent::firstBy($id);
if ($schema->delete()) {
SchemaFacade::dropIfExists($schema->name);
}
return true;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Modules\Develop\Providers;
use Catch\CatchAdmin;
use Catch\Events\Module\Created;
use Catch\Events\Module\Deleted;
use Catch\Providers\CatchModuleServiceProvider;
use Modules\Develop\Listeners\CreatedListener;
use Modules\Develop\Listeners\DeletedListener;
class DevelopServiceProvider extends CatchModuleServiceProvider
{
protected array $events = [
Created::class => CreatedListener::class,
Deleted::class => DeletedListener::class
];
/**
* route path
*
* @return string|array
*/
public function routePath(): string|array
{
// TODO: Implement path() method.
return CatchAdmin::getModuleRoutePath('develop');
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Modules\Develop\Providers;
use Catch\Support\Module\Installer;
/**
* install
*/
class Install extends Installer
{
protected function info(): array
{
// TODO: Implement info() method.
return [];
}
protected function migration(): string
{
// TODO: Implement migration() method.
return '';
}
protected function seeder(): string
{
// TODO: Implement seed() method.
return '';
}
protected function requirePackages(): void
{
// TODO: Implement requirePackages() method.
}
protected function removePackages(): void
{
// TODO: Implement removePackages() method.
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Modules\Develop\Support\Generate\Create;
use Catch\CatchAdmin;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class Controller extends Creator
{
/**
* @var array
*/
protected array $replace = [
'{namespace}', '{uses}', '{controller}', '{model}', '{request}'
];
/**
* @param string $controller
* @param string $model
* @param string|null $request
*/
public function __construct(
public readonly string $controller,
public readonly string $model,
public readonly ?string $request = null
) {
}
/**
* get file
*
* @return string
*/
public function getFile(): string
{
// TODO: Implement getFile() method.
return CatchAdmin::getModuleControllerPath($this->module).$this->getControllerName().$this->ext;
}
public function getContent(): string|bool
{
// TODO: Implement getContent() method.
return Str::of(File::get($this->getControllerStub()))->replace($this->replace, [
$this->getControllerNamespace(),
$this->getUses(),
$this->getControllerName(),
$this->model,
$this->request ?: 'Request'
])->toString();
}
/**
* get controller name
*
* @return string
*/
protected function getControllerName(): string
{
return Str::of($this->controller)->whenContains('Controller', function ($value) {
return Str::of($value)->ucfirst();
}, function ($value) {
return Str::of($value)->append('Controller')->ucfirst();
})->toString();
}
/**
* get uses
*
* @return string
*/
protected function getUses(): string
{
return Str::of('use ')
->append(CatchAdmin::getModuleModelNamespace($this->module).$this->model)
->append(';')
->newLine()
->append('use ')
->when($this->request, function ($str) {
return $str->append(CatchAdmin::getModuleRequestNamespace($this->module).$this->request);
}, function ($str) {
return $str->append("Illuminate\Http\Request");
})->append(';')->newLine()->toString();
}
/**
* get controller stub
*
* @return string
*/
protected function getControllerStub(): string
{
return dirname(__DIR__).DIRECTORY_SEPARATOR.'stubs'.DIRECTORY_SEPARATOR.'controller.stub';
}
/**
* get controller namespace
*
* @return string
*/
protected function getControllerNamespace(): string
{
return Str::of(CatchAdmin::getModuleControllerNamespace($this->module))->rtrim('\\')->append(';')->toString();
}
}

View File

@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Modules\Develop\Support\Generate\Create;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Facades\File;
/**
* creator
*/
abstract class Creator
{
/**
* @var string
*/
protected string $ext = '.php';
/**
* @var string
*/
protected string $module;
/**
* @var string
*/
protected string $file;
/**
* create
*
* @return bool|string
* @throws FileNotFoundException
*/
public function create(): bool|string
{
return $this->put();
}
/**
* the file which content put in
*
* @return string
*/
abstract public function getFile(): string;
/**
* get content
* @return string|bool
*/
abstract public function getContent(): string|bool;
/**
* @return string|bool
* @throws FileNotFoundException
*/
protected function put(): string|bool
{
if (! $this->getContent()) {
return false;
}
$this->file = $this->getFile();
File::put($this->file, $this->getContent());
if (File::exists($this->file)) {
return $this->file;
}
throw new FileNotFoundException("create [$this->file] failed");
}
/**
* set ext
*
* @param string $ext
* @return $this
*/
protected function setExt(string $ext): static
{
$this->ext = $ext;
return $this;
}
/**
* set module
*
* @param string $module
* @return $this
*/
public function setModule(string $module): static
{
$this->module = $module;
return $this;
}
/**
* get file
*
* @return string
*/
public function getGenerateFile(): string
{
return $this->file;
}
}

View File

@@ -0,0 +1,188 @@
<?php
// +----------------------------------------------------------------------
// | CatchAdmin [Just Like ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2021 https://catchadmin.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( https://github.com/JaguarJack/catchadmin-laravel/blob/master/LICENSE.md )
// +----------------------------------------------------------------------
// | Author: JaguarJack [ njphper@gmail.com ]
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace Modules\Develop\Support\Generate\Create;
use Catch\CatchAdmin;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class FrontForm extends Creator
{
/**
* @var string
*/
protected string $label = '{label}';
/**
* @var string
*/
protected string $prop = '{prop}';
/**
* @var string
*/
protected string $modelValue = '{model-value}';
/**
* @var string
*/
protected string $table = '{table}';
/**
* @var string
*/
protected string $search = '{search}';
/**
* @var string
*/
protected string $api = '{api}';
/**
* @var string
*/
protected string $formItems = '{formItems}';
/**
* @var string
*/
protected string $paginate = '{paginate}';
/**
* @var string
*/
protected string $useList = '{useList}';
/**
* @var array
*/
protected array $structures;
/**
* @param string $controller
*/
public function __construct(protected readonly string $controller)
{
}
/**
* get content
*
* @return string
*/
public function getContent(): string
{
// TODO: Implement getContent() method.
return Str::of(File::get($this->getFormStub()))->replace($this->formItems, $this->getFormContent())->toString();
}
/**
* get file
*
* @return string
*/
public function getFile(): string
{
// TODO: Implement getFile() method.
return CatchAdmin::makeDir(CatchAdmin::getModuleViewsPath($this->module).Str::of($this->controller)->replace('Controller', '')->lcfirst()).DIRECTORY_SEPARATOR.'create.vue';
}
/**
* get form content
*
* @return string
*/
protected function getFormContent(): string
{
$form = Str::of('');
$formComponents = $this->formComponents();
foreach ($this->structures as $structure) {
if ($structure['label'] && $structure['form_component'] && $structure['form']) {
if (isset($formComponents[$structure['form_component']])) {
$form = $form->append(
Str::of($formComponents[$structure['form_component']])
->replace(
[$this->label, $this->prop, $this->modelValue],
[$structure['label'], $structure['field'], sprintf('formData.%s', $structure['field'])]
)
);
}
}
}
return $form->trim(PHP_EOL)->toString();
}
/**
* form components
*
* @return array
*/
protected function formComponents(): array
{
$components = [];
foreach (File::glob(
$this->getFormItemStub()
) as $stub) {
$components[File::name($stub)] = File::get($stub);
}
return $components;
}
/**
* get formItem stub
*
* @return string
*/
protected function getFormItemStub(): string
{
return dirname(__DIR__).DIRECTORY_SEPARATOR.'stubs'
.DIRECTORY_SEPARATOR.'vue'.DIRECTORY_SEPARATOR
.'formItems'.DIRECTORY_SEPARATOR.'*.stub';
}
/**
* get form stub
*
* @return string
*/
public function getFormStub(): string
{
return dirname(__DIR__).DIRECTORY_SEPARATOR.'stubs'
.DIRECTORY_SEPARATOR.'vue'.DIRECTORY_SEPARATOR.'form.stub';
}
/**
* set structures
*
* @param array $structures
* @return $this
*/
public function setStructures(array $structures): static
{
$this->structures = $structures;
return $this;
}
}

View File

@@ -0,0 +1,252 @@
<?php
// +----------------------------------------------------------------------
// | CatchAdmin [Just Like ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2021 https://catchadmin.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( https://github.com/JaguarJack/catchadmin-laravel/blob/master/LICENSE.md )
// +----------------------------------------------------------------------
// | Author: JaguarJack [ njphper@gmail.com ]
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace Modules\Develop\Support\Generate\Create;
use Catch\CatchAdmin;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class FrontTable extends Creator
{
/**
* @var string
*/
protected string $label = '{label}';
/**
* @var string
*/
protected string $prop = '{prop}';
/**
* @var string
*/
protected string $modelValue = '{model-value}';
/**
* @var string
*/
protected string $table = '{table}';
/**
* @var string
*/
protected string $search = '{search}';
/**
* @var string
*/
protected string $api = '{api}';
/**
* @var string
*/
protected string $formItems = '{formItems}';
/**
* @var string
*/
protected string $paginate = '{paginate}';
/**
* @var string
*/
protected string $useList = '{useList}';
/**
* @var array
*/
protected array $structures;
/**
* @param string $controller
* @param bool $hasPaginate
* @param string $apiString
*/
public function __construct(
protected readonly string $controller,
protected readonly bool $hasPaginate,
protected readonly string $apiString
) {
}
/**
* get content
*
* @return string
*/
public function getContent(): string
{
// TODO: Implement getContent() method.
return Str::of(File::get($this->getTableStub()))->replace([
$this->table, $this->search, $this->api, $this->paginate, $this->useList
], [
$this->getTableContent(), $this->getSearchContent(),
"'{$this->apiString}'", $this->getPaginateStubContent(), $this->getUseList()
])->toString();
}
/**
* get file
*
* @return string
*/
public function getFile(): string
{
// TODO: Implement getFile() method.
return CatchAdmin::makeDir(CatchAdmin::getModuleViewsPath($this->module).Str::of($this->controller)->replace('Controller', '')->lcfirst()).DIRECTORY_SEPARATOR.'index.vue';
}
/**
* get search content
*
* @return string
*/
protected function getSearchContent(): string
{
$search = Str::of('');
$formComponents = $this->formComponents();
foreach ($this->structures as $structure) {
if ($structure['label'] && $structure['form_component'] && $structure['search']) {
if (isset($formComponents[$structure['form_component']])) {
$search = $search->append(
Str::of($formComponents[$structure['form_component']])
->replace(
[$this->label, $this->prop, $this->modelValue],
[$structure['label'], $structure['field'], sprintf('query.%s', $structure['field'])]
)
);
}
}
}
return $search->trim(PHP_EOL)->toString();
}
/**
* get list content;
*
* @return string
*/
protected function getTableContent(): string
{
$tableColumn = <<<HTML
<el-table-column prop="{prop}" label="{label}" />
HTML;
$table = Str::of('');
foreach ($this->structures as $structure) {
if ($structure['field'] && $structure['label'] && $structure['list']) {
$table = $table->append(
Str::of($tableColumn)->replace([$this->label, $this->prop], [$structure['label'], $structure['field']])
)->newLine();
}
}
return $table->trim(PHP_EOL)->toString();
}
/**
* form components
*
* @return array
*/
protected function formComponents(): array
{
$components = [];
foreach (File::glob(
$this->getFormItemStub()
) as $stub) {
$components[File::name($stub)] = File::get($stub);
}
return $components;
}
/**
* get formItem stub
*
* @return string
*/
protected function getFormItemStub(): string
{
return dirname(__DIR__).DIRECTORY_SEPARATOR.'stubs'
.DIRECTORY_SEPARATOR.'vue'.DIRECTORY_SEPARATOR
.'formItems'.DIRECTORY_SEPARATOR.'*.stub';
}
/**
* get table stub
*
* @return string
*/
protected function getTableStub(): string
{
return dirname(__DIR__).DIRECTORY_SEPARATOR.'stubs'
.DIRECTORY_SEPARATOR.'vue'.DIRECTORY_SEPARATOR.'table.stub';
}
/**
* get paginate stub content
*
* @return string
*/
protected function getPaginateStubContent(): string
{
return $this->hasPaginate ?
File::get(
dirname(__DIR__).DIRECTORY_SEPARATOR
.'stubs'.DIRECTORY_SEPARATOR.'vue'.
DIRECTORY_SEPARATOR.'paginate.stub'
)
: '';
}
/**
* get use List
* @return string
*/
protected function getUseList(): string
{
return $this->hasPaginate ?
'const { data, query, search, reset, changePage, changeLimit, loading } = useGetList(api)' :
'const { data, query, search, reset, loading } = useGetList(api)';
}
/**
* set structures
*
* @param array $structures
* @return $this
*/
public function setStructures(array $structures): static
{
$this->structures = $structures;
return $this;
}
}

View File

@@ -0,0 +1,276 @@
<?php
// +----------------------------------------------------------------------
// | CatchAdmin [Just Like ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2021 https://catchadmin.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( https://github.com/JaguarJack/catchadmin-laravel/blob/master/LICENSE.md )
// +----------------------------------------------------------------------
// | Author: JaguarJack [ njphper@gmail.com ]
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace Modules\Develop\Support\Generate\Create;
use Catch\CatchAdmin;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema as SchemaFacade;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model as EloquentModel;
class Model extends Creator
{
protected array $replace = [
'{uses}',
'{property}',
'{namespace}',
'{model}',
'{traits}',
'{table}',
'{fillable}',
'{searchable}',
'{fieldsInList}',
'{isPaginate}', '{form}'
];
protected array $structures;
protected bool $softDelete;
/**
* @param string $modelName
* @param string $tableName
* @param bool $isPaginate
*/
public function __construct(
protected string $modelName,
protected readonly string $tableName,
protected readonly bool $isPaginate
) {
$model = new class () extends EloquentModel {
use SoftDeletes;
};
$this->softDelete = in_array($model->getDeletedAtColumn(), SchemaFacade::getColumnListing($this->tableName));
}
/**
* get file
*
* @return string
*/
public function getFile(): string
{
// TODO: Implement getFile() method.
return CatchAdmin::getModuleModelPath($this->module).$this->getModelName().$this->ext;
}
/**
* get content
*
* @return string
*/
public function getContent(): string
{
$modelStub = File::get($this->getModelStub());
return Str::of($modelStub)->replace($this->replace, [$this->getUses(),
$this->getProperties(),
$this->getModelNamespace(),
$this->getModelName(),
$this->getTraits(),
$this->tableName,
$this->getFillable(),
$this->getSearchable(),
$this->getFieldsInList(),
$this->isPaginate(),
$this->getInForm()
])->toString();
}
/**
* get model namespace
*
* @return string
*/
public function getModelNamespace(): string
{
return Str::of(CatchAdmin::getModuleModelNamespace($this->module))->trim('\\')->append(';')->toString();
}
/**
* get model name
*
* @return string
*/
public function getModelName(): string
{
$modelName = Str::of($this->modelName);
if (! $modelName->length()) {
$modelName = Str::of($this->tableName)->camel();
}
return $modelName->ucfirst()->whenContains('Model', function ($value) {
return Str::of($value);
}, function ($value) {
return Str::of($value)->append('Model');
})->toString();
}
/**
* get uses
*
* @return string
*/
protected function getUses(): string
{
if (! $this->softDelete) {
return <<<Text
use Catch\Traits\DB\BaseOperate;
use Catch\Traits\DB\ScopeTrait;
use Catch\Traits\DB\Trans;
use Illuminate\Database\Eloquent\Model;
Text;
} else {
return <<<Text
use Catch\Base\CatchModel as Model;
Text;
}
}
/**
* get traits
*
* @return string
*/
protected function getTraits(): string
{
return $this->softDelete ? '' : 'use BaseOperate, Trans, ScopeTrait;';
}
/**
*
* @return string
*/
protected function getProperties(): string
{
$comment = Str::of('/**')->newLine();
foreach ($this->getTableColumns() as $column) {
$comment = $comment->append(sprintf(' * @property $%s', $column))->newLine();
}
return $comment->append('*/')->toString();
}
/**
* get fillable
*
* @return string
*/
protected function getFillable(): string
{
$fillable = Str::of('');
foreach ($this->getTableColumns() as $column) {
$fillable = $fillable->append(" '{$column}'")->append(',');
}
return $fillable->rtrim(',')->toString();
}
/**
*
* @return array
*/
protected function getTableColumns(): array
{
return getTableColumns($this->tableName);
}
/**
* get field in list
*
* @return string
*/
protected function getFieldsInList(): string
{
$str = Str::of('');
foreach ($this->structures as $structure) {
if ($structure['list']) {
$str = $str->append("'{$structure['field']}'")->append(',');
}
}
return $str->rtrim(',')->toString();
}
/**
* get field in list
*
* @return string
*/
protected function getInForm(): string
{
$str = Str::of('');
foreach ($this->structures as $structure) {
if ($structure['form']) {
$str = $str->append("'{$structure['field']}'")->append(',');
}
}
return $str->rtrim(',')->toString();
}
/**
* searchable
*
* @return string
*/
protected function getSearchable(): string
{
$searchable = Str::of('');
foreach ($this->structures as $structure) {
if ($structure['search'] && $structure['field'] && $structure['search_op']) {
$searchable = $searchable->append(sprintf("'%s' => '%s'", $structure['field'], $structure['search_op']))->append(',')->newLine();
}
}
return $searchable->toString();
}
/**
* @return string
*/
protected function isPaginate(): string
{
return $this->isPaginate ? '' : Str::of('protected bool $isPaginate = false;')->toString();
}
/**
* @param array $structures
* @return $this
*/
public function setStructures(array $structures): static
{
$this->structures = $structures;
return $this;
}
/**
* get stub
*
* @return string
*/
protected function getModelStub(): string
{
return dirname(__DIR__).DIRECTORY_SEPARATOR.'stubs'.DIRECTORY_SEPARATOR.'model.stub';
}
}

View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace Modules\Develop\Support\Generate\Create;
use Catch\CatchAdmin;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class Request extends Creator
{
/**
* @var array
*/
protected array $structures;
/**
* @var array|string[]
*/
protected array $replace = ['{namespace}', '{request}', '{rule}'];
/**
* @param string $controller
*/
public function __construct(public readonly string $controller)
{
}
/**
* @return string
*/
public function getFile(): string
{
return CatchAdmin::getModuleRequestPath($this->module).$this->getRequestName().$this->ext;
}
/**
* get content
*
* @return string|bool
*/
public function getContent(): string|bool
{
$rule = $this->getRulesString();
if (! $rule) {
return false;
}
return Str::of(
File::get(dirname(__DIR__).DIRECTORY_SEPARATOR.'stubs'.DIRECTORY_SEPARATOR.'request.stub')
)->replace($this->replace, [$this->getNamespace(), $this->getRequestName(), $rule])->toString();
}
/**
* get namespace
*
* @return string
*/
protected function getNamespace(): string
{
return Str::of(CatchAdmin::getModuleRequestNamespace($this->module))->rtrim('\\')->append(';')->toString();
}
/**
* get request name
*
* @return ?string
*/
public function getRequestName(): ?string
{
if ($this->getRules()) {
return Str::of($this->controller)->remove('Controller')->append('Request')->ucfirst()->toString();
}
return null;
}
/**
* get rule
*
* @return string|bool
*/
public function getRulesString(): string|bool
{
$rules = $this->getRules();
if (! count($rules)) {
return false;
}
$rule = Str::of('');
foreach ($rules as $field => $validates) {
$rule = $rule->append("'{$field}'")
->append(' => ')
->append('\'')
->append(Arr::join($validates, '|'))
->append('\',')
->newLine();
}
return $rule->toString();
}
/**
* get rules
*
* @return array
*/
protected function getRules(): array
{
$rules = [];
foreach ($this->structures as $structure) {
if ($structure['field'] && count($structure['validates'])) {
$rules[$structure['field']] = $structure['validates'];
}
}
return $rules;
}
/**
* set structures
*
* @param array $structures
* @return $this
*/
public function setStructures(array $structures): static
{
$this->structures = $structures;
return $this;
}
}

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Modules\Develop\Support\Generate\Create;
use Catch\CatchAdmin;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
/**
* Route
*/
class Route extends Creator
{
/**
* @param string $controller
*/
public function __construct(public readonly string $controller)
{
}
/**
* get file
*
* @return string
*/
public function getFile(): string
{
return CatchAdmin::getModuleRoutePath($this->module);
}
/**
* get content
*
* @return string
*/
public function getContent(): string
{
// route 主要添加两个点
// use Controller
// 添加路由
$route = Str::of('');
$originContent = File::get(CatchAdmin::getModuleRoutePath($this->module));
// 如果已经有 controller就不再追加路由
if (Str::of($originContent)->contains($this->getUserController())) {
return $originContent;
}
File::lines(CatchAdmin::getModuleRoutePath($this->module))
->each(function ($line) use (&$route) {
if (Str::of($line)->contains('Route::prefix')) {
$route = $route->trim(PHP_EOL)
->newLine()
->append($this->getUserController())
->append(';')
->newLine(2)
->append($line)
->newLine();
} else {
$route = $route->append($line)->newLine();
}
});
$apiResource = "Route::apiResource('{api}', {controller}::class);";
return Str::of($route->toString())->replace(
['{module}', '//next'],
[
lcfirst($this->module),
Str::of($apiResource)->replace(['{api}', '{controller}'], [$this->getApiString(), $this->getControllerName()])
->prepend("\t")
->prepend(PHP_EOL)
->newLine()->append("\t//next")]
)->toString();
}
/**
* get api
*
* @return string
*/
public function getApiString(): string
{
return Str::of($this->getControllerName())->remove('Controller')->snake('_')->replace('_', '/')->toString();
}
/**
* get api route
*
* @return string
*/
public function getApiRute(): string
{
return lcfirst($this->module).'/'.$this->getApiString();
}
/**
* use controller
*
* @return string
*/
protected function getUserController(): string
{
return 'use '.CatchAdmin::getModuleControllerNamespace($this->module).$this->getControllerName();
}
/**
* get controller name
*
* @return string
*/
protected function getControllerName(): string
{
return Str::of($this->controller)->whenContains('Controller', function ($value) {
return Str::of($value)->ucfirst();
}, function ($value) {
return Str::of($value)->append('Controller')->ucfirst();
})->toString();
}
}

View File

@@ -0,0 +1,317 @@
<?php
// +----------------------------------------------------------------------
// | CatchAdmin [Just Like ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2021 https://catchadmin.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( https://github.com/JaguarJack/catchadmin-laravel/blob/master/LICENSE.md )
// +----------------------------------------------------------------------
// | Author: JaguarJack [ njphper@gmail.com ]
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace Modules\Develop\Support\Generate\Create;
use Catch\CatchAdmin;
use Exception;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Schema as MigrationSchema;
/**
* schema
*/
class Schema extends Creator
{
/**
* @var bool
*/
protected bool $createdAt = true;
/**
* @var bool
*/
protected bool $updatedAt = true;
/**
* @var bool
*/
protected bool $deletedAt = true;
/**
* @var bool
*/
protected bool $creatorId = true;
/**
* @var array
*/
protected array $structures = [];
/**
* @param string $table
* @param string $engine
* @param string $charset
* @param string $collection
* @param string $comment
*/
public function __construct(
public readonly string $table,
public readonly string $engine,
public readonly string $charset,
public readonly string $collection,
public readonly string $comment
) {
}
/**
* create
*
* @return string|bool
* @throws Exception
*/
public function create(): string|bool
{
if (! count($this->structures)) {
return false;
}
if (MigrationSchema::hasTable($this->table)) {
throw new Exception(sprintf('[%s] 表已经存在', $this->table));
}
try {
$this->createTable();
if (MigrationSchema::hasTable($this->table)) {
return parent::create();
}
return false;
} catch (Exception $e) {
MigrationSchema::dropIfExists($this->table);
throw new Exception("由于{$e->getMessage()}, 表[{$this->table}]创建失败");
}
}
/**
* get file
*
* @return string
*/
public function getFile(): string
{
// TODO: Implement getFile() method.
return CatchAdmin::getModuleMigrationPath($this->module).date('Y_m_d_his_').'create_'.$this->table.'.php';
}
/**
* create table
*
* @throws Exception
*/
protected function createTable(): void
{
MigrationSchema::create($this->table, function (Blueprint $table) {
foreach ($this->structures as $structure) {
// if field && type hava value
if ($structure['type'] && $structure['field']) {
if ($structure['type'] == 'string') {
$column = $table->string($structure['field'], $structure['length'] ?: 255);
} elseif ($structure['type'] == 'char') {
$column = $table->char($structure['field'], $structure['length']);
} else {
$column = $table->{$structure['type']}($structure['field']);
}
$column = $column->nullable($structure['nullable']);
if ($structure['default']) {
$column = $column->default($structure['default']);
}
if ($structure['comment']) {
$column = $column->comment($structure['comment']);
}
if ($structure['unique']) {
$column->unique($structure['unique']);
}
}
}
if ($this->creatorId) {
$table->creatorId();
}
if ($this->createdAt) {
$table->createdAt();
}
if ($this->updatedAt) {
$table->updatedAt();
}
if ($this->deletedAt) {
$table->deletedAt();
}
$table->charset = $this->charset;
$table->engine = $this->engine;
$table->collation = $this->collection;
$table->comment($this->comment);
});
}
/**
* get migration content
*
* @return string
*/
public function getContent(): string
{
$stub = File::get($this->getStub());
return Str::of($stub)->replace(['{method}','{table}', '{content}'], ['create', $this->table, $this->getMigrationContent()])->toString();
}
/**
* get content
*
* @return string
*/
public function getMigrationContent(): string
{
$content = Str::of('');
foreach ($this->structures as $structure) {
$begin = Str::of('$table->');
$type = Str::of($structure['type']);
if ($type->exactly('string')) {
$begin = $begin->append(sprintf("string('%s'%s)", $structure['field'], $structure['length'] ? ", {$structure['length']}" : ''));
} elseif ($type->exactly('char')) {
$begin = $begin->append(sprintf("char('%s', %s)", $structure['field'], $structure['length']));
} elseif ($type->exactly('id')) {
$begin = $begin->append(Str::of($structure['field'])->exactly('id') ? 'id()' : sprintf("id('%s')", $structure['field']));
} else {
$begin = $begin->append(sprintf("%s('%s')", $structure['type'], $structure['field']));
}
$content = $content->append($begin)
->when($structure['nullable'], function ($str) {
return $str->append('->nullable()');
})
->when(isset($structure['default']), function ($str, $default) {
if (is_numeric($default)) {
return $str->append("->default({$default})");
}
if ($default) {
return $str->append("->default('{$default}')");
}
return $str;
})
->when($structure['unique'], function ($str) {
return $str->append("->unique()");
})
->when($structure['comment'], function ($str, $comment) {
return $str->append("->comment('{$comment}')");
})
->append(';')
->newLine();
}
if ($this->creatorId) {
$content = $content->append(Str::of('$table->')->append('creatorId();'))->newLine();
}
if ($this->createdAt) {
$content = $content->append(Str::of('$table->')->append('createdAt();'))->newLine();
}
if ($this->updatedAt) {
$content = $content->append(Str::of('$table->')->append('updatedAt();'))->newLine();
}
if ($this->deletedAt) {
$content = $content->append(Str::of('$table->')->append('deletedAt();'))->newLine();
}
return $content->newLine()
->append("\$table->engine='{$this->engine}'")
->append(';')
->newLine()
->append("\$table->comment('{$this->comment}')")
->append(';')
->toString();
}
/**
* @param bool $createdAt
* @return $this
*/
public function setCreatedAt(bool $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
/**
* @param bool $updatedAt
* @return $this
*/
public function setUpdatedAt(bool $updatedAt): static
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* @param bool $deletedAt
* @return $this
*/
public function setDeletedAt(bool $deletedAt): static
{
$this->deletedAt = $deletedAt;
return $this;
}
/**
* @param bool $creatorId
* @return $this
*/
public function setCreatorId(bool $creatorId): static
{
$this->creatorId = $creatorId;
return $this;
}
/**
* @param array $structures
* @return $this
*/
public function setStructures(array $structures): static
{
$this->structures = $structures;
return $this;
}
/**
* get stub
*
* @return string
*/
protected function getStub(): string
{
return dirname(__DIR__).DIRECTORY_SEPARATOR.'stubs'.DIRECTORY_SEPARATOR.'migration.stub';
}
}

View File

@@ -0,0 +1,214 @@
<?php
// +----------------------------------------------------------------------
// | CatchAdmin [Just Like ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2021 https://catchadmin.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( https://github.com/JaguarJack/catchadmin-laravel/blob/master/LICENSE.md )
// +----------------------------------------------------------------------
// | Author: JaguarJack [ njphper@gmail.com ]
// +----------------------------------------------------------------------
namespace Modules\Develop\Support\Generate;
use Catch\Exceptions\FailedException;
use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Modules\Develop\Support\Generate\Create\Controller;
use Modules\Develop\Support\Generate\Create\FrontForm;
use Modules\Develop\Support\Generate\Create\FrontTable;
use Modules\Develop\Support\Generate\Create\Model;
use Modules\Develop\Support\Generate\Create\Request;
use Modules\Develop\Support\Generate\Create\Route;
/**
* @class Generator
*/
class Generator
{
/**
* @var array{module:string,controller:string,model:string,paginate: bool,schema: string}
*/
protected array $gen;
/**
* @var array{name: string,charset: string, collection: string,
* comment:string,created_at: bool, updated_at: bool, deleted_at: bool,
* creator_id: bool, updated_at: bool, engine: string}
*/
protected array $schema;
/**
* @var array
*/
protected array $structures;
/**
* @var array
*/
protected array $files = [];
/**
* this model name from controller
*
* @var string
*/
protected string $modelName;
/**
* this request name for controller
*
* @var ?string
*/
protected ?string $requestName;
/**
* generate
*
* @throws Exception
* @return bool
*/
public function generate(): bool
{
try {
$this->files[] = $this->createModel();
$this->files[] = $this->createRequest();
$this->files[] = $this->createController();
$this->files[] = $this->createFrontTable();
$this->files[] = $this->createFrontForm();
$this->files[] = $this->createRoute();
} catch (Exception $e) {
$this->rollback();
throw new FailedException($e->getMessage());
}
$this->files = [];
return true;
}
/**
* create route
*
* @throws FileNotFoundException
* @return bool|string
*/
public function createRoute(): bool|string
{
// 保存之前的 route 文件
$route = new Route($this->gen['controller']);
return $route->setModule($this->gen['module'])->create();
}
/**
* create font
*
* @throws FileNotFoundException
* @return bool|string|null
*/
public function createFrontTable(): bool|string|null
{
$table = new FrontTable($this->gen['controller'], $this->gen['paginate'], (new Route($this->gen['controller']))->setModule($this->gen['module'])->getApiRute());
return $table->setModule($this->gen['module'])->setStructures($this->structures)->create();
}
/**
* create font
*
* @throws FileNotFoundException
* @return bool|string|null
*/
public function createFrontForm(): bool|string|null
{
$form = new FrontForm($this->gen['controller']);
return $form->setModule($this->gen['module'])->setStructures($this->structures)->create();
}
/**
* create model
*
* @throws FileNotFoundException
* @return bool|string
*/
protected function createModel(): bool|string
{
$model = new Model($this->gen['model'], $this->gen['schema'], $this->gen['module']);
$this->modelName = $model->getModelName();
return $model->setModule($this->gen['module'])->setStructures($this->structures)->create();
}
/**
* create request
*
* @throws FileNotFoundException
* @return bool|string
*/
protected function createRequest(): bool|string
{
$request = new Request($this->gen['controller']);
$file = $request->setStructures($this->structures)->setModule($this->gen['module'])->create();
$this->requestName = $request->getRequestName();
return $file;
}
/**
* create controller
*
* @throws FileNotFoundException
* @return bool|string
*/
protected function createController(): bool|string
{
$controller = new Controller($this->gen['controller'], $this->modelName, $this->requestName);
return $controller->setModule($this->gen['module'])->create();
}
/**
* rollback
*
* @return void
*/
protected function rollback(): void
{
// delete controller & model & migration file
foreach ($this->files as $file) {
unlink($file);
}
// 回填之前的 route 文件
}
/**
* set params
*
* @param array $params
* @return $this
*/
public function setParams(array $params): Generator
{
$this->gen = $params['codeGen'];
$this->structures = $params['structures'];
return $this;
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Modules\Develop\Support\Generate;
use Catch\CatchAdmin;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class Module
{
public function __construct(
public string $module,
protected bool $controller,
protected bool $models,
protected bool $requests,
protected bool $database
) {
}
/**
* create
*
* @return void
*/
public function create(): void
{
if ($this->controller) {
CatchAdmin::getModuleControllerPath($this->module);
}
if ($this->models) {
CatchAdmin::getModuleModelPath($this->module);
}
if ($this->requests) {
CatchAdmin::getModuleRequestPath($this->module);
}
if ($this->database) {
CatchAdmin::getModuleMigrationPath($this->module);
CatchAdmin::getModuleSeederPath($this->module);
}
$this->createProvider();
$this->createRoute();
}
/**
* delete
*
* @return void
*/
public function delete(): void
{
}
/**
* create provider
*
* @return void
*/
protected function createProvider(): void
{
CatchAdmin::getModuleProviderPath($this->module);
File::put(
CatchAdmin::getModuleProviderPath($this->module).sprintf('%sServiceProvider.php', ucfirst($this->module)),
Str::of(
File::get(__DIR__.DIRECTORY_SEPARATOR.'stubs'.DIRECTORY_SEPARATOR.'provider.stub')
)->replace(['{Module}', '{module}'], [ucfirst($this->module), $this->module])
);
}
/**
* create route
*
* @return void
*/
protected function createRoute(): void
{
File::copy(__DIR__.DIRECTORY_SEPARATOR.'stubs'.DIRECTORY_SEPARATOR.'route.stub', CatchAdmin::getModuleRoutePath($this->module));
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace {namespace}
use Catch\Base\CatchController as Controller;
{uses}
class {controller} extends Controller
{
public function __construct(
protected readonly {model} $model
){}
/**
* @param Request $request
* @return mixed
*/
public function index(Request $request): mixed
{
return $this->model->getList();
}
public function store({request} $request)
{
return $this->model->storeBy($request->all());
}
public function show($id)
{
return $this->model->firstBy($id);
}
public function update($id, {request} $request)
{
return $this->model->updateBy($id, $request->all());
}
public function destroy($id)
{
return $this->model->deleteBy($id);
}
}

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::{method}('{table}', function (Blueprint $table) {
{content}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('{table}');
}
};

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace {namespace}
{uses}
{property}
class {model} extends Model
{
{traits}
protected $table = '{table}';
protected $fillable = [{fillable} ];
/**
* @var array
*/
protected array $fieldsInList = [{fieldsInList}];
/**
* @var array
*/
protected array $form = [{form}];
/**
* @var array
*/
public array $searchable = [
{searchable}
];
{isPaginate}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Modules\{Module}\Providers;
use Catch\CatchAdmin;
use Catch\Providers\CatchModuleServiceProvider;
class {Module}ServiceProvider extends CatchModuleServiceProvider
{
/**
* route path
*
* @return string|array
*/
public function routePath(): string|array
{
// TODO: Implement path() method.
return CatchAdmin::getModuleRoutePath('{module}');
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace {namespace}
use Illuminate\Foundation\Http\FormRequest as Request;
class {request} extends Request
{
/**
* @return array
*/
public function rules(): array
{
return [
{rule}
];
}
/**
*
* @return array
*/
public function message(): array
{
return [];
}
}

View File

@@ -0,0 +1,7 @@
<?php
use Illuminate\Support\Facades\Route;
Route::prefix('{module}')->group(function(){
//next
});

View File

@@ -0,0 +1,38 @@
<template>
<el-form :model="formData" label-width="120px" ref="form" v-loading="loading" class="pr-4">
{formItems}
<div class="flex justify-end">
<el-button type="primary" @click="submitForm(form)">{{ $t('system.confirm') }}</el-button>
</div>
</el-form>
</template>
<script lang="ts" setup>
import { useCreate } from '/admin/composables/curd/useCreate'
import { useShow } from '/admin/composables/curd/useShow'
import { onMounted, watch } from 'vue'
const props = defineProps({
primary: String | Number,
api: String,
})
const emit = defineEmits(['close'])
const { formData, form, loading, submitForm, isClose } = useCreate(props.api, props.primary)
watch(isClose, function (value) {
if (value) {
emit('close')
}
})
onMounted(() => {
if (props.primary) {
useShow(props.api, props.primary).then(r => {
formData.value = r.data
})
}
})
</script>

View File

@@ -0,0 +1,3 @@
<el-form-item label="{label}" prop="{prop}">
<el-cascader v-model="{model-value}" :options="options" />
</el-form-item>

View File

@@ -0,0 +1,9 @@
<el-form-item label="{label}" prop="{prop}">
<el-date-picker
v-model="{model-value}"
type="date"
name="{prop}"
placeholder="Pick a day"
clearable
/>
</el-form-item>

View File

@@ -0,0 +1,9 @@
<el-form-item label="{label}" prop="{prop}">
<el-date-picker
v-model="{model-value}"
type="datetime"
name="{prop}"
placeholder="Pick a day"
clearable
/>
</el-form-item>

View File

@@ -0,0 +1,3 @@
<el-form-item label="{label}" prop="{prop}">
<el-input-number v-model="{model-value}" name="{prop}" :min="1" />
</el-form-item>

View File

@@ -0,0 +1,3 @@
<el-form-item label="{label}" prop="{prop}">
<el-input v-model="{model-value}" name="{prop}" clearable />
</el-form-item>

View File

@@ -0,0 +1,5 @@
<el-form-item label="{label}" prop="{prop}">
<el-radio-group v-model="{model-value}">
<el-radio v-for="item in options" :key="item.value" :label="item.value" name="{prop}">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>

View File

@@ -0,0 +1,3 @@
<el-form-item label="{label}" prop="{prop}">
<el-rate v-model="{model-value}" name="{prop}" clearable />
</el-form-item>

View File

@@ -0,0 +1,10 @@
<el-form-item label="{label}" prop="{prop}">
<el-select v-model="{model-value}" placeholder="请选择" clearable ${multiple}>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>

View File

@@ -0,0 +1,3 @@
<el-form-item label="{label}" prop="{prop}">
<el-switch v-model="{model-value}" name="{prop}" />
</el-form-item>

View File

@@ -0,0 +1,9 @@
<el-form-item label="{label}" prop="{prop}">
<el-tree-select
v-model="{model-value}"
:data="data"
check-strictly
multiple
show-checkbox
/>
</el-form-item>

View File

@@ -0,0 +1,8 @@
<el-form-item label="{label}" prop="{prop}">
<el-tree
:data="data"
show-checkbox
node-key="id"
/>
</el-form-item>

View File

@@ -0,0 +1,12 @@
<div class="pt-2 pb-2 flex justify-end">
<el-pagination
background
layout="total,sizes,prev,pager,next"
:current-page="query.page"
:page-size="query.limit"
@current-change="changePage"
@size-change="changeLimit"
:total="total"
:page-sizes="[1, 10, 20, 30, 50]"
/>
</div>

View File

@@ -0,0 +1,78 @@
<template>
<div>
<div class="w-full min-h-0 bg-white dark:bg-regal-dark pl-5 pt-5 pr-5 rounded-lg">
<el-form :inline="true">
{search}
<el-form-item>
<el-button type="primary" @click="search()">
<Icon name="magnifying-glass" class="w-4 mr-1 -ml-1" />
搜索
</el-button>
<el-button @click="reset()">
<Icon name="arrow-path" class="w-4 mr-1 -ml-1" />
重置
</el-button>
</el-form-item>
</el-form>
</div>
<div class="pl-2 pr-2 bg-white dark:bg-regal-dark rounded-lg mt-4">
<div class="pt-5 pl-2">
<Add @click="show(null)" />
</div>
<el-table :data="tableData" class="mt-3" v-loading="loading">
{table}
<el-table-column label="操作" width="200">
<template #default="scope">
<Update @click="show(scope.row.id)" />
<Destroy @click="destroy(api, scope.row.id)" />
</template>
</el-table-column>
</el-table>
{paginate}
</div>
<Dialog v-model="visible" :title="title" destroy-on-close>
<Create @close="close" :primary="id" :api="api" />
</Dialog>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import Create from './create.vue'
import { useGetList } from '/admin/composables/curd/useGetList'
import { useDestroy } from '/admin/composables/curd/useDestroy'
import { useEnabled } from '/admin/composables/curd/useEnabled'
import { t } from '/admin/support/helper'
const visible = ref<boolean>(false)
const id = ref(null)
const api = {api}
const title = ref<string>('');
{useList}
const { destroy, isDeleted } = useDestroy()
const { enabled } = useEnabled()
onMounted(() => search())
const tableData = computed(() => data.value?.data)
const total = computed(() => data.value?.total)
const close = () => {
visible.value = false
reset()
}
const show = primary => {
title.value = primary ? t('system.edit') : t('system.add')
id.value = primary
visible.value = true
}
watch(isDeleted, function (){
// change origin status
isDeleted.value = false
reset();
})
</script>

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
return new class () extends Migration {
public function up()
{
Schema::create('schemas', function (Blueprint $table) {
$table->increments('id');
$table->string('module')->nullable(false)->comment('模块名称');
$table->string('name')->nullable(false)->comment('schema 名称');
$table->string('columns')->nullable(false)->comment('字段');
$table->boolean('is_soft_delete')->default(1)->comment('1 是 2 否');
$table->createdAt();
$table->updatedAt();
$table->deletedAt();
});
}
};

14
modules/Develop/route.php Normal file
View File

@@ -0,0 +1,14 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\Develop\Http\Controllers\ModuleController;
use Modules\Develop\Http\Controllers\GenerateController;
use Modules\Develop\Http\Controllers\SchemaController;
Route::apiResource('module', ModuleController::class);
Route::put('module/enable/{name}', [ModuleController::class, 'enable']);
Route::post('generate', [GenerateController::class, 'index']);
Route::apiResource('schema', SchemaController::class)->only(['index', 'show', 'store', 'destroy']);

View File

@@ -0,0 +1,109 @@
<template>
<el-card class="box-card" shadow="never">
<template #header>
<div>
<span>{{ $t('generate.code.title') }}</span>
</div>
</template>
<div class="w-full sm:w-[40%] mx-auto">
<el-form :model="gen" ref="form" label-width="100px">
<el-form-item
:label="$t('generate.code.module.name')"
prop="module"
:rules="[
{
required: true,
message: $t('generate.code.module.verify'),
},
]"
>
<Select v-model="gen.module" clearable :placeholder="$t('generate.code.module.placeholder')" api="modules" class="w-full" filterable />
</el-form-item>
<el-form-item
:label="$t('generate.code.controller.name')"
prop="controller"
:rules="[
{
required: true,
message: $t('generate.code.controller.verify'),
},
]"
>
<el-input v-model="gen.controller" clearable :placeholder="$t('generate.code.controller.placeholder')" />
</el-form-item>
<el-form-item :label="$t('generate.code.model.name')" prop="model">
<el-input v-model="gen.model" clearable :placeholder="$t('generate.code.model.placeholder')" />
</el-form-item>
<div class="flex">
<el-form-item :label="$t('generate.code.paginate')" prop="paginate">
<el-switch v-model="gen.paginate" inline-prompt :active-text="$t('system.yes')" :inactive-text="$t('system.no')" />
</el-form-item>
<el-form-item label-width="15px">
<div class="text-sm text-gray-300">控制列表是否使用分页功能</div>
</el-form-item>
</div>
</el-form>
</div>
<Structure />
<div class="w-full flex justify-center pt-5">
<router-link to="/develop/schemas">
<el-button>{{ $t('system.back') }}</el-button>
</router-link>
<el-button type="primary" @click="submitGenerate(form)" class="ml-5">{{ $t('system.finish') }}</el-button>
</div>
</el-card>
</template>
<script lang="ts" setup>
import { watch, onMounted, reactive, ref } from 'vue'
import { useGenerateStore } from './store'
import type { FormInstance } from 'element-plus'
import http from '/admin/support/http'
import Structure from './structure.vue'
import { useRouter } from 'vue-router'
const generateStore = useGenerateStore()
const gen = reactive(generateStore.getCodeGen)
const router = useRouter()
const schemaId = router.currentRoute.value.params.schema
onMounted(() => {
if (!generateStore.getSchemaId) {
generateStore.setSchemaId(schemaId)
getSchema()
} else {
if (schemaId !== generateStore.getSchemaId) {
generateStore.setSchemaId(schemaId)
generateStore.resetStructures()
getSchema()
}
}
})
const getSchema = () => {
http.get('schema/' + schemaId).then(r => {
gen.module = r.data.data.module
gen.schema = r.data.data.name
gen.model = r.data.data.name.replace(/\_(\w)/g, (value, letter) => {
return letter.toUpperCase()
})
generateStore.initStructures(r.data.data.columns)
})
}
const form = ref<FormInstance>()
const submitGenerate = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate(valid => {
if (valid) {
http.post('generate', generateStore.$state).then(r => {})
//emits('next')
//generateStore.$reset()
} else {
return false
}
})
}
</script>

View File

@@ -0,0 +1,112 @@
import { defineStore } from 'pinia'
/**
* 表结构信息
*/
export interface Structure {
field: string
label: string
form_component: string
list: boolean
form: boolean
search: boolean
search_op: string
validates: string[]
}
/**
* CodeGen
*/
export interface CodeGen {
module: string
controller: string
model: string
paginate: true
schema: string
}
/**
* generate
*/
interface generate {
schemaId: number
structures: Structure[]
codeGen: CodeGen
}
/**
* useGenerateStore
*/
export const useGenerateStore = defineStore('generateStore', {
state(): generate {
return {
// schema id
schemaId: 0,
// structures
structures: [] as Structure[],
// codeGen
codeGen: Object.assign({
module: '',
controller: '',
model: '',
paginate: true,
schema: '',
}),
}
},
// store getters
getters: {
getSchemaId(): any {
return this.schemaId
},
getStructures(): Structure[] {
return this.structures
},
getCodeGen(): CodeGen {
return this.codeGen
},
},
// store actions
actions: {
// set schema
setSchemaId(schemaId: any): void {
this.schemaId = schemaId
},
// reset
resetStructures(): void {
this.structures = []
},
// filter structures
filterStructures(field: string) {
this.structures = this.structures.filter((s: Structure) => {
return !(s.field === field)
})
},
// init structure
initStructures(fields: Array<any>): void {
const unSupportFields = ['deleted_at', 'creator_id']
fields.forEach(field => {
if (!unSupportFields.includes(field.name)) {
this.structures.push(
Object.assign({
field: field.name,
label: '',
form_component: 'input',
list: true,
form: true,
search: false,
search_op: '',
validates: [],
}),
)
}
})
},
},
})

View File

@@ -0,0 +1,99 @@
<template>
<div>
<el-table :data="structures">
<el-table-column prop="field" :label="$t('generate.schema.structure.field_name.name')" width="100px" />
<el-table-column prop="label" :label="$t('generate.schema.structure.form_label')" width="150px">
<template #default="scope">
<el-input v-model="scope.row.label" />
</template>
</el-table-column>
<el-table-column prop="label" :label="$t('generate.schema.structure.form_component')" width="110px">
<template #default="scope">
<el-select v-model="scope.row.form_component" class="w-full" filterable>
<el-option v-for="component in formComponents" :key="component" :label="component" :value="component" />
</el-select>
</template>
</el-table-column>
<el-table-column prop="list" :label="$t('generate.schema.structure.list')">
<template #default="scope">
<el-switch v-model="scope.row.list" inline-prompt :active-text="$t('system.yes')" :inactive-text="$t('system.no')" width="45px" />
</template>
</el-table-column>
<el-table-column prop="form" :label="$t('generate.schema.structure.form')">
<template #default="scope">
<el-switch v-model="scope.row.form" inline-prompt :active-text="$t('system.yes')" :inactive-text="$t('system.no')" width="45px" />
</template>
</el-table-column>
<el-table-column prop="search" :label="$t('generate.schema.structure.search')">
<template #default="scope">
<el-switch v-model="scope.row.search" inline-prompt :active-text="$t('system.yes')" :inactive-text="$t('system.no')" width="45px" />
</template>
</el-table-column>
<el-table-column prop="search_op" :label="$t('generate.schema.structure.search_op.name')" width="150px">
<template #default="scope">
<el-select v-model="scope.row.search_op" :placeholder="$t('generate.schema.structure.search_op.placeholder')" class="w-full">
<el-option v-for="op in operates" :key="op" :label="op" :value="op" />
</el-select>
</template>
</el-table-column>
<el-table-column prop="validates" :label="$t('generate.schema.structure.rules.name')" width="250px">
<template #default="scope">
<el-select v-model="scope.row.validates" :placeholder="$t('generate.schema.structure.rules.placeholder')" multiple filterable allow-create clearable class="w-full">
<el-option v-for="validate in validates" :key="validate" :label="validate" :value="validate" />
</el-select>
</template>
</el-table-column>
<!--<el-table-column prop="comment" label="注释" />-->
<el-table-column prop="id" :label="$t('generate.schema.structure.operate')" width="120px">
<template #default="scope">
<el-button type="danger" :icon="Delete" @click="deleteField(scope.row.field)" size="small" />
</template>
</el-table-column>
</el-table>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useGenerateStore } from './store'
import { Delete } from '@element-plus/icons-vue'
const generateStore = useGenerateStore()
const structures = computed(() => {
return generateStore.getStructures
})
const deleteField = (field: string) => {
generateStore.filterStructures(field)
}
const operates: string[] = ['=', '!=', '>', '>=', '<', '<=', 'like', 'RLike', 'LLike', 'in']
const validates: string[] = [
'required',
'integer',
'numeric',
'string',
'timezone',
'url',
'uuid',
'date',
'alpha',
'alpha_dash',
'alpha_num',
'boolean',
'email',
'image',
'file',
'ip',
'ipv4',
'ipv6',
'mac_address',
'json',
'nullable',
'present',
'prohibited',
]
const formComponents: string[] = ['cascader', 'date', 'datetime', 'input', 'input-number', 'radio', 'rate', 'select', 'tree', 'tree-select']
</script>

View File

@@ -0,0 +1,10 @@
<template>
<div class="bg-white">
<CodeGen />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import CodeGen from './components/codeGen.vue'
</script>

View File

@@ -0,0 +1,89 @@
<template>
<el-form :model="formData" label-width="120px" ref="form" v-loading="loading" class="pr-4">
<el-form-item
:label="$t('module.form.name.title')"
prop="name"
:rules="[
{
required: true,
message: $t('module.form.name.required'),
},
]"
>
<el-input v-model="formData.name" />
</el-form-item>
<el-form-item
:label="$t('module.form.path.title')"
prop="path"
:rules="[
{
required: true,
message: $t('module.form.path.required'),
},
]"
>
<el-input v-model="formData.path" :disabled="!!primary" />
</el-form-item>
<el-form-item :label="$t('module.form.keywords.title')" prop="keywords">
<el-input v-model="formData.keywords" />
</el-form-item>
<el-form-item :label="$t('module.form.desc.title')" prop="desc">
<el-input v-model="formData.description" type="textarea" />
</el-form-item>
<el-form-item :label="$t('module.form.dirs.title')" prop="dirs" v-if="!primary">
<el-checkbox v-model="formData['dirs']['controllers']" label="Controllers" size="large" />
<el-checkbox v-model="formData['dirs']['models']" label="Models" size="large" />
<el-checkbox v-model="formData['dirs']['database']" label="Database" size="large" />
<el-checkbox v-model="formData['dirs']['requests']" label="Requests" size="large" />
</el-form-item>
<div class="flex justify-end">
<el-button type="primary" @click="submitForm(form)">{{ $t('system.confirm') }}</el-button>
</div>
</el-form>
</template>
<script lang="ts" setup>
import { useCreate } from '/admin/composables/curd/useCreate'
import { useShow } from '/admin/composables/curd/useShow'
import { onMounted, watch } from 'vue'
const props = defineProps({
primary: String | Number,
api: String,
})
const { formData, form, loading, submitForm, isClose } = useCreate(
props.api,
props.primary,
Object.assign({
name: '',
path: '',
keywords: '',
description: '',
dirs: {
controllers: true,
models: true,
database: true,
requests: false,
},
}),
)
const emit = defineEmits(['close'])
watch(isClose, function (value) {
if (value) {
emit('close')
}
})
onMounted(() => {
if (props.primary) {
useShow(props.api, props.primary).then(r => {
formData.value = r.data
})
}
})
</script>

View File

@@ -0,0 +1,103 @@
<template>
<div>
<div class="w-full min-h-0 bg-white dark:bg-regal-dark pl-5 pt-5 pr-5 rounded-lg">
<el-form :inline="true">
<el-form-item label="模块名称">
<el-input v-model="query.name" name="name" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search()">
<Icon name="magnifying-glass" class="w-4 mr-1 -ml-1" />
搜索
</el-button>
<el-button @click="reset()">
<Icon name="arrow-path" class="w-4 mr-1 -ml-1" />
重置
</el-button>
</el-form-item>
</el-form>
</div>
<div class="pl-2 pr-2 bg-white dark:bg-regal-dark rounded-lg mt-4">
<div class="pt-5 pl-2">
<Add @click="show(null)" />
</div>
<el-table :data="tableData" class="mt-3" v-loading="loading">
<el-table-column prop="name" label="模块名称" width="180" />
<el-table-column prop="path" label="模块目录" width="180" />
<el-table-column prop="version" label="模块版本">
<template #default="scope">
<el-tag type="warning">{{ scope.row.version }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="enable" label="模块状态">
<template #default="scope">
<el-switch v-model="scope.row.enable" @change="enabled(api, scope.row.name)" />
</template>
</el-table-column>
<el-table-column label="操作" width="300">
<template #default="scope">
<Update @click="show(scope.row.name)" />
<Destroy @click="destroy(api, scope.row.name)" />
</template>
</el-table-column>
</el-table>
<div class="pt-2 pb-2 flex justify-end">
<el-pagination
background
layout="total,sizes,prev, pager,next"
:current-page="query.page"
:page-size="query.limit"
@current-change="changePage"
@size-change="changeLimit"
:total="total"
:page-sizes="[10, 20, 30, 50]"
/>
</div>
</div>
<Dialog v-model="visible" :title="title" destroy-on-close>
<Create @close="close" :primary="id" :api="api" />
</Dialog>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import Create from './create.vue'
import { useGetList } from '/admin/composables/curd/useGetList'
import { useDestroy } from '/admin/composables/curd/useDestroy'
import { useEnabled } from '/admin/composables/curd/useEnabled'
import { t } from '/admin/support/helper'
import Sortable from 'sortablejs'
const visible = ref<boolean>(false)
const id = ref(null)
const api = 'module'
const title = ref<string>('')
const { data, query, search, reset, changePage, changeLimit, loading } = useGetList(api)
const { destroy, isDeleted } = useDestroy('确认删除吗? ⚠️将会删除模块下所有文件')
const { enabled } = useEnabled()
onMounted(() => search())
const tableData = computed(() => data.value?.data)
const total = computed(() => data.value?.total)
const close = () => {
visible.value = false
reset()
}
const show = primary => {
title.value = primary ? t('system.edit') : t('system.add')
id.value = primary
visible.value = true
}
watch(isDeleted, function () {
isDeleted.value = false
reset()
})
</script>

View File

@@ -0,0 +1,32 @@
import { RouteRecordRaw } from 'vue-router'
// @ts-ignore
const router: RouteRecordRaw[] = [
{
path: '/develop',
component: () => import('/admin/layout/index.vue'),
meta: { title: '开发工具', icon: 'wrench-screwdriver', hidden: false },
children: [
{
path: 'modules',
name: 'modules',
meta: { title: '模块管理', icon: 'home', hidden: false },
component: () => import('./module/index.vue'),
},
{
path: 'schemas',
name: 'schemas',
meta: { title: 'Schemas', icon: 'home', hidden: false },
component: () => import('./schema/index.vue'),
},
{
path: 'generate/:schema',
name: 'generate',
meta: { title: '代码生成', hidden: true, is_inner: true },
component: () => import('./generate/index.vue'),
},
],
},
]
export default router

View File

@@ -0,0 +1,35 @@
<template>
<div>
<Schema v-if="active === 1" @next="next" @prev="prev" />
<Structure v-if="active === 2" @next="next" @prev="prev" />
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import Schema from './steps/schema.vue'
import Structure from './steps/structure.vue'
import { useSchemaStore } from './store'
const schemaStore = useSchemaStore()
const active = ref(1)
const next = () => {
if (active.value++ >= 2) {
active.value = 2
}
}
const prev = () => {
if (active.value-- === 1) {
active.value = 1
}
}
const emit = defineEmits(['close'])
watch(() => schemaStore.getFinished, function (value){
if (value) {
emit('close')
}
})
</script>

View File

@@ -0,0 +1,120 @@
<template>
<div>
<div class="w-full min-h-0 bg-white dark:bg-regal-dark pl-5 pt-5 pr-5 rounded-lg">
<el-form :inline="true">
<el-form-item label="模块名称">
<el-input v-model="query.module" name="module" clearable />
</el-form-item>
<el-form-item label="Schema 名称">
<el-input v-model="query.name" name="name" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search()">
<Icon name="magnifying-glass" class="w-4 mr-1 -ml-1" />
搜索
</el-button>
<el-button @click="reset()">
<Icon name="arrow-path" class="w-4 mr-1 -ml-1" />
重置
</el-button>
</el-form-item>
</el-form>
</div>
<div class="pl-2 pr-2 bg-white dark:bg-regal-dark rounded-lg mt-4">
<div class="pt-5 pl-2">
<Add @click="add(null)" />
</div>
<el-table :data="tableData" class="mt-3" v-loading="loading">
<el-table-column prop="module" label="所属模块" />
<el-table-column prop="name" label="schema 名称" />
<el-table-column prop="columns" label="字段">
<template #default="scope">
<el-button size="small" type="success" @click="show(scope.row.id)"><Icon name="eye" class="w-3 mr-1" /> 查看</el-button>
</template>
</el-table-column>
<el-table-column prop="is_soft_delete" label="?软删">
<template #default="scope">
<el-tag v-if="scope.row.is_soft_delete"></el-tag>
<el-tag type="danger" v-else></el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" />
<el-table-column label="操作" width="250">
<template #default="scope">
<router-link :to="'/develop/generate/' + scope.row.id">
<el-button type="warning" size="small"><Icon name="wrench-screwdriver" class="w-3 mr-1" /> 生成代码</el-button>
</router-link>
<Destroy @click="destroy(api, scope.row.id)" class="ml-2" />
</template>
</el-table-column>
</el-table>
<div class="pt-2 pb-2 flex justify-end">
<el-pagination
background
layout="total,sizes,prev, pager,next"
:current-page="query.page"
:page-size="query.limit"
@current-change="changePage"
@size-change="changeLimit"
:total="total"
:page-sizes="[10, 20, 30, 50]"
/>
</div>
</div>
<!-- schema 创建 -->
<Dialog v-model="visible" :title="$t('generate.schema.title')" width="650px" destroy-on-close>
<Create @close="close" :primary="id" :api="api" />
</Dialog>
<!-- schema 表结构 -->
<Dialog v-model="showVisible" title="Schema 结构" width="650px" destroy-on-close>
<Show :id="id" :api="api" />
</Dialog>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import Create from './create.vue'
import Show from './show.vue'
import { useGetList } from '/admin/composables/curd/useGetList'
import { useDestroy } from '/admin/composables/curd/useDestroy'
const visible = ref<boolean>(false)
const showVisible = ref<boolean>(false)
const id = ref<number>()
const api = 'schema'
const title = ref<string>('')
const { data, query, search, reset, changePage, changeLimit, loading } = useGetList(api)
const { destroy, isDeleted } = useDestroy('确认删除吗? 将会删除数据库的 Schema请提前做好备份一旦删除将无法恢复!')
onMounted(() => search())
const tableData = computed(() => data.value?.data)
const total = computed(() => data.value?.total)
const close = () => {
visible.value = false
reset()
}
const add = () => {
visible.value = true
}
const show = primaryId => {
showVisible.value = true
id.value = primaryId
}
watch(isDeleted, function () {
isDeleted.value = false
reset()
})
</script>

View File

@@ -0,0 +1,38 @@
<template>
<el-table :data="data" class="mt-3" v-loading="loading">
<el-table-column prop="name" label="字段名称" />
<el-table-column prop="type" label="类型" />
<el-table-column prop="nullable" label="nullable">
<template #default="scope">
<el-tag v-if="scope.row.nullable"></el-tag>
<el-tag type="danger" v-else></el-tag>
</template>
</el-table-column>
<el-table-column prop="default" label="默认值">
<template #default="scope"> </template>
</el-table-column>
<el-table-column prop="comment" label="注释" />
</el-table>
</template>
<script lang="ts" setup>
import { useShow } from '/admin/composables/curd/useShow'
import { onMounted } from 'vue'
import { ref } from 'vue'
const props = defineProps({
id: {
type: Number,
required: true,
},
})
const data = ref<Array<object>>()
onMounted(() => {
useShow('schema', props.id).then(r => {
data.value = r.data.columns
})
})
</script>
<style scoped></style>

View File

@@ -0,0 +1,107 @@
<template>
<div class="w-full sm:w-[90%] mx-auto">
<el-form :model="schema" ref="form" label-width="80px">
<el-form-item
:label="$t('generate.code.module.name')"
prop="module"
:rules="[
{
// required: true,
message: $t('generate.code.module.verify'),
},
]"
>
<Select v-model="schema.module" clearable :placeholder="$t('generate.code.module.placeholder')" api="modules" class="w-full" filterable />
</el-form-item>
<el-form-item
:label="$t('generate.schema.name')"
prop="name"
:rules="[
{
required: true,
message: $t('generate.schema.name_verify'),
},
]"
>
<el-input v-model="schema.name" clearable />
</el-form-item>
<el-form-item
:label="$t('generate.schema.engine.name')"
prop="engine"
:rules="[
{
required: true,
message: $t('generate.schema.engine.verify'),
},
]"
>
<el-select class="w-full" v-model="schema.engine" :placeholder="$t('generate.schema.engine.placeholder')" clearable>
<el-option v-for="engine in engines" :key="engine.value" :label="engine.label" :value="engine.value" />
</el-select>
</el-form-item>
<el-form-item :label="$t('generate.schema.default_field.name')">
<el-checkbox v-model="schema.created_at" :label="$t('generate.schema.default_field.created_at')" size="large" />
<el-checkbox v-model="schema.updated_at" :label="$t('generate.schema.default_field.updated_at')" size="large" />
<el-checkbox v-model="schema.creator_id" :label="$t('generate.schema.default_field.creator')" size="large" />
<el-checkbox v-model="schema.deleted_at" :label="$t('generate.schema.default_field.delete_at')" size="large" />
</el-form-item>
<el-form-item
:label="$t('generate.schema.comment.name')"
prop="comment"
:rules="[
{
required: true,
message: $t('generate.schema.comment.verify'),
},
]"
>
<el-input v-model="schema.comment" type="textarea" />
</el-form-item>
</el-form>
<div class="w-full sm:w-96 justify-between mx-auto pl-24 mt-4">
<el-button class="mt-5" @click="$emit('prev')">{{ $t('system.prev') }}</el-button>
<el-button class="mt-5" type="primary" @click="submitCreateTable(form)">{{ $t('system.next') }}</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, computed, ref, unref } from 'vue'
import { useSchemaStore } from '../store'
import type { FormInstance } from 'element-plus'
const schemaStore = useSchemaStore()
schemaStore.start()
const emits = defineEmits(['prev', 'next'])
const schema = reactive(schemaStore.getSchema)
const form = ref<FormInstance>()
const submitCreateTable = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate(valid => {
if (valid) {
emits('next')
schemaStore.setSchema(unref(schema))
} else {
return false
}
})
}
const engines = computed(() => {
return [
{
value: 'InnoDB',
label: 'InnoDB',
},
{
value: 'MyISAM',
label: 'MyISAM',
},
{
value: 'Memory',
label: 'Memory',
},
]
})
</script>

View File

@@ -0,0 +1,231 @@
<template>
<div>
<el-table :data="structures" class="draggable">
<el-table-column prop="field" :label="$t('generate.schema.structure.field_name.name')" />
<el-table-column prop="type" :label="$t('generate.schema.structure.type.name')">
<template #default="scope">
<el-tag type="success">{{ scope.row.type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="nullable" :label="$t('generate.schema.structure.nullable')" width="90px">
<template #default="scope">
<el-tag v-if="scope.row.nullable">{{ $t('system.yes') }}</el-tag>
<el-tag v-else type="info">{{ $t('system.no') }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="default" :label="$t('generate.schema.structure.default')" />
<!--<el-table-column prop="comment" label="注释" />-->
<el-table-column prop="id" :label="$t('generate.schema.structure.operate')" width="120px">
<template #default="scope">
<el-button type="primary" :icon="Edit" @click="updateField(scope.row.id)" size="small" />
<el-button type="danger" :icon="Delete" @click="deleteField(scope.row.id)" size="small" />
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<el-button type="success" :icon="Plus" @click="addField">{{ $t('system.add') }}</el-button>
</div>
<div class="w-full sm:w-96 justify-between mx-auto pl-24 mt-2">
<el-button class="mt-5" @click="emits('prev')">{{ $t('system.prev') }}</el-button>
<el-button class="mt-5" type="primary" @click="next">{{ $t('system.confirm') }}</el-button>
</div>
<Dialog v-model="visible" :title="$t('system.add')">
<el-form :model="structure" status-icon label-width="120px" ref="form">
<el-form-item
:label="$t('generate.schema.structure.field_name.name')"
prop="field"
:rules="[
{
required: true,
message: $t('generate.schema.structure.field_name.verify'),
},
]"
>
<el-input v-model="structure.field" />
</el-form-item>
<div class="flex justify-between">
<el-form-item
class="w-full"
:label="$t('generate.schema.structure.type.name')"
prop="type"
:rules="[
{
required: true,
message: $t('generate.schema.structure.type.verify'),
},
]"
>
<el-select v-model="structure.type" :placeholder="$t('generate.schema.structure.type.placeholder')" filterable class="w-full">
<el-option v-for="item in types" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</div>
<el-form-item :label="$t('generate.schema.structure.length')" prop="length">
<el-input-number v-model="structure.length" :min="0" />
</el-form-item>
<div class="flex justify-between">
<el-form-item label="nullable" prop="nullable">
<el-switch v-model="structure.nullable" inline-prompt :active-text="$t('system.yes')" :inactive-text="$t('system.no')" />
</el-form-item>
<el-form-item :label="$t('generate.schema.structure.default')" prop="default" v-if="!structure.nullable">
<el-input v-model="structure.default" />
</el-form-item>
</div>
<el-form-item :label="$t('generate.schema.structure.unique')" prop="unique">
<el-switch v-model="structure.unique" inline-prompt :active-text="$t('system.yes')" :inactive-text="$t('system.no')" />
</el-form-item>
<el-form-item :label="$t('generate.schema.structure.comment')" prop="comment">
<el-input v-model="structure.comment" text />
</el-form-item>
<div class="flex justify-end">
<el-button type="primary" @click="submitStructure(form)">{{ $t('system.confirm') }}</el-button>
</div>
</el-form>
</Dialog>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, Ref, ref } from 'vue'
import { useSchemaStore, Structure } from '../store'
import { Delete, Plus, Edit } from '@element-plus/icons-vue'
import type { FormInstance } from 'element-plus'
import Message from '/admin/support/message'
import http from '/admin/support/http'
import { Code } from '/admin/enum/app'
import Sortable from 'sortablejs'
const schemaStore = useSchemaStore()
const emits = defineEmits(['prev', 'next'])
const visible = ref(false)
const structures = computed(() => {
return schemaStore.getStructures
})
const structure: Ref<Structure> = ref(schemaStore.initStructure())
// structure
const addField = async () => {
await form.value?.clearValidate()
visible.value = true
}
const updateField = (id: number) => {
visible.value = true
schemaStore.getStructures.forEach(s => {
if (s.id === id) {
structure.value = s
}
})
}
onMounted(() => {
const tbody = document.querySelector('.draggable .el-table__body-wrapper tbody')
const structures = schemaStore.getStructures
Sortable.create(tbody, {
draggable: 'tr',
onEnd({ newIndex, oldIndex }) {
const newStructures = []
const s = structures.splice(oldIndex, newIndex - oldIndex)
console.log(s, structures, oldIndex, newIndex)
s.concat(structures).forEach(item => {
newStructures.push(item)
})
schemaStore.setStructures(newStructures)
// console.log(structure)
// structures[newIndex] = structures[oldIndex]
// structures[oldIndex] = temp
},
})
})
const form = ref<FormInstance>()
const submitStructure = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate(valid => {
if (valid) {
visible.value = !visible.value
schemaStore.addStructure(structure.value)
structure.value = schemaStore.initStructure()
} else {
return false
}
})
}
const deleteField = (id: number) => {
schemaStore.filterStructures(id)
}
const next = () => {
if (schemaStore.getStructures.length < 1) {
Message.error('请先填写表结构数据')
} else {
http.post('schema', schemaStore.$state).then(r => {
if (r.data.code == Code.SUCCESS) {
Message.success('创建成功')
schemaStore.finished()
}
})
}
}
const types: string[] = [
'id',
'smallIncrements',
'mediumIncrements',
'increments',
'smallInteger',
'integer',
'bigIncrements',
'bigInteger',
'mediumInteger',
'unsignedInteger',
'unsignedMediumInteger',
'unsignedSmallInteger',
'unsignedTinyInteger',
'string',
'text',
'binary',
'boolean',
'char',
'dateTimeTz',
'dateTime',
'date',
'decimal',
'double',
'float',
'geometryCollection',
'geometry',
'ipAddress',
'json',
'jsonb',
'lineString',
'longText',
'macAddress',
'mediumText',
'multiLineString',
'multiPoint',
'multiPolygon',
'nullableMorphs',
'nullableTimestamps',
'nullableUuidMorphs',
'point',
'polygon',
'timeTz',
'time',
'timestampTz',
'timestamp',
'timestampsTz',
'timestamps',
'tinyIncrements',
'tinyInteger',
'tinyText',
'unsignedDecimal',
'uuid',
'year',
]
</script>

View File

@@ -0,0 +1,146 @@
import { defineStore } from 'pinia'
/**
* 表信息
*/
export interface Schema {
module: string
name: string
comment: string
engine: string
charset: string
collaction: string
created_at: boolean
creator_id: boolean
updated_at: boolean
deleted_at: boolean
}
/**
* 表结构信息
*/
export interface Structure {
id: number
field: string
length: number
type: string
nullable: boolean
unique: boolean
default: number | string
comment: string
}
/**
* generate
*/
interface CreateSchema {
schema: Schema
structures: Structure[]
is_finished: boolean
}
/**
* useSchemaStore
*/
export const useSchemaStore = defineStore('schemaStore', {
state(): CreateSchema {
return {
// schema
schema: Object.assign({
module: '',
name: '',
comment: '',
engine: 'InnoDB',
charset: 'utf8mb4',
collection: 'utf8mb4_unicode_ci',
created_at: true,
creator_id: true,
updated_at: true,
deleted_at: true,
}),
// structures
structures: [] as Structure[],
// is finished
is_finished: false,
}
},
// store getters
getters: {
getSchema(): Schema {
return this.schema
},
getStructures(): Structure[] {
return this.structures
},
getFinished(): boolean {
return this.is_finished
},
},
// store actions
actions: {
// set schema
setSchema(schema: Schema): void {
this.schema = schema
},
setStructures(structures: Array<Structure>): void {
this.structures = structures
},
// add structure
addStructure(structure: Structure): void {
if (structure.id) {
this.structures = this.structures.filter((s: Structure) => {
if (s.id === structure.id) {
s = structure
}
return s
})
} else {
structure.id = this.structures.length + 1
this.structures.push(structure)
}
},
// filter structures
filterStructures(id: number) {
this.structures = this.structures.filter((s: Structure) => {
return !(s.id === id)
})
},
// init structure
initStructure(): Structure {
return Object.assign({
id: 0,
field: '',
label: '',
type: '',
length: 0,
nullable: false,
unique: false,
default: '',
comment: '',
})
},
/**
* finished
*/
finished(): void {
this.$reset()
this.is_finished = true
},
/**
* unfinished
*/
start(): void {
this.is_finished = false
},
},
})

View File

@@ -0,0 +1,20 @@
<?php
namespace Modules\Options\Http;
use Exception;
use Modules\Options\Repository\Factory;
class OptionController
{
/**
* @param $name
* @param Factory $factory
* @return array
* @throws Exception
*/
public function index($name, Factory $factory): array
{
return $factory->make($name)->get();
}
}

View File

@@ -0,0 +1,2 @@
## 介绍
这是一个公共模块,不耦合其他项目,用于给前端提供统一 select options 数据接口

View File

@@ -0,0 +1,38 @@
<?php
namespace Modules\Options\Repository;
use Modules\Permissions\Enums\DataRange as DataRangeEnum;
class DataRange implements OptionInterface
{
public function get(): array
{
return [
[
'label' => DataRangeEnum::All_Data->name(),
'value' => DataRangeEnum::All_Data->value()
],
[
'label' => DataRangeEnum::Personal_Choose->name(),
'value' => DataRangeEnum::Personal_Choose->value()
],
[
'label' => DataRangeEnum::Personal_Data->name(),
'value' => DataRangeEnum::Personal_Data->value()
],
[
'label' => DataRangeEnum::Department_Data->name(),
'value' => DataRangeEnum::Department_Data->value()
],
[
'label' => DataRangeEnum::Department_DOWN_Data->name(),
'value' => DataRangeEnum::Department_DOWN_Data->value()
]
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Modules\Options\Repository;
use Exception;
use Illuminate\Support\Str;
class Factory
{
/**
* make
* @param string $optionName
* @return OptionInterface
* @throws Exception
*/
public function make(string $optionName): OptionInterface
{
$className = __NAMESPACE__.'\\'.Str::of($optionName)->ucfirst()->toString();
$class = new $className();
if (! $class instanceof OptionInterface) {
throw new Exception('option must be implement [OptionInterface]');
}
return $class;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Modules\Options\Repository;
use Catch\Support\Module\ModuleRepository;
class Modules implements OptionInterface
{
public function get(): array
{
$modules = [];
app(ModuleRepository::class)->all([])
->each(function ($module) use (&$modules) {
$modules[] = [
'label' => $module['name'],
'value' => $module['path']
];
});
return $modules;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Modules\Options\Repository;
interface OptionInterface
{
/**
* @return array{label: string, value: string|number }
*/
public function get(): array;
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Modules\Options\Repository;
use Catch\Enums\Status as StatusEnum;
class Status implements OptionInterface
{
public function get(): array
{
return [
[
'label' => StatusEnum::Enable->name(),
'value' => StatusEnum::Enable->value()
],
[
'label' => StatusEnum::Disable->name(),
'value' => StatusEnum::Disable->value()
]
];
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Modules\Permissions\Enums;
use Catch\Enums\Enum;
enum DataRange: int implements Enum
{
case All_Data = 1; // 全部数据
case Personal_Choose = 2; // 自定义数据
case Personal_Data = 3; // 本人数据
case Department_Data = 4; // 部门数据
case Department_DOWN_Data = 5; // 部门及以下数据
public function value(): int
{
// TODO: Implement value() method.
return match ($this) {
self::All_Data => 1,
self::Personal_Choose => 2,
self::Personal_Data => 3,
self::Department_Data => 4,
self::Department_DOWN_Data => 5,
};
}
public function name(): string
{
// TODO: Implement name() method.
return match ($this) {
self::All_Data => '全部数据',
self::Personal_Choose => '自定义数据',
self::Personal_Data => '本人数据',
self::Department_Data => '部门数据',
self::Department_DOWN_Data => '部门及以下数据',
};
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Modules\Permissions\Http\Controllers;
use Catch\Base\CatchController as Controller;
use Modules\Permissions\Models\RolesModel;
use Illuminate\Http\Request;
use Modules\Permissions\Http\Requests\RoleRequest;
class RolesController extends Controller
{
public function __construct(
protected readonly RolesModel $model
){}
/**
* @return mixed
*/
public function index(): mixed
{
return $this->model->getList();
}
public function store(RoleRequest $request)
{
return $this->model->storeBy($request->all());
}
public function show($id)
{
return $this->model->firstBy($id);
}
public function update($id, RoleRequest $request)
{
return $this->model->updateBy($id, $request->all());
}
public function destroy($id)
{
return $this->model->deleteBy($id);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Modules\Permissions\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Modules\Permissions\Models\RolesModel;
class RoleRequest extends FormRequest
{
/**
* rules
*
* @return array
*/
public function rules(): array
{
return [
'role_name' => sprintf('required|unique:%s,%s,%s', RolesModel::class, 'role_name', $this->get('id')),
'identify' => sprintf('required|alpha|unique:%s,%s,%s', RolesModel::class, 'role_name', $this->get('id')),
];
}
/**
* messages
*
* @return string[]
*/
public function messages(): array
{
return [
'role_name.required' => '角色名称必须填写',
'role_name.unique' => '角色名称已存在',
'identify.required' => '角色标识必须填写',
'identify.alpha' => '角色名称只允许字母组成',
'identify.unique' => '角色标识已存在',
];
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Modules\Permissions\Models;
use Catch\Base\CatchModel as Model;
use Illuminate\Support\Facades\Request;
/**
* @property $role_name
* @property $identify
* @property $parent_id
* @property $description
* @property $data_range
* @property $creator_id
* @property $created_at
* @property $updated_at
* @property $deleted_at
*/
class RolesModel extends Model
{
protected $table = 'roles';
protected $fillable = [ 'id', 'role_name', 'identify', 'parent_id', 'description', 'data_range', 'creator_id', 'created_at', 'updated_at', 'deleted_at' ];
/**
* @var array
*/
protected array $fieldsInList = ['id', 'role_name','identify','parent_id','description','data_range', 'created_at', 'updated_at'];
/**
* @var array
*/
protected array $form = ['role_name','identify','parent_id','description','data_range'];
/**
* @var array
*/
public array $searchable = [
'role_name' => 'like',
];
/**
* @return mixed
*/
public function getList(): mixed
{
return self::query()->select($this->fieldsInList)->quickSearch()->get()->toTree();
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Modules\Permissions\Providers;
use Catch\CatchAdmin;
use Catch\Providers\CatchModuleServiceProvider;
class PermissionsServiceProvider extends CatchModuleServiceProvider
{
/**
* route path
*
* @return string|array
*/
public function routePath(): string|array
{
// TODO: Implement path() method.
return CatchAdmin::getModuleRoutePath('Permissions');
}
}

View File

@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('roles', function (Blueprint $table) {
$table->increments('id');
$table->string('role_name', 30)->comment('角色名称');
$table->string('identify', 30)->nullable()->comment('角色的标识,用英文表示');
$table->integer('parent_id')->default(0)->comment('父级ID');
$table->string('description')->nullable()->comment('角色描述');
$table->smallInteger('data_range')->default(0)->comment('1 全部数据 2 自定义数据 3 仅本人数据 4 部门数据 5 部门及以下数据');
$table->creatorId();
$table->createdAt();
$table->updatedAt();
$table->deletedAt();
$table->engine = 'InnoDB';
$table->comment('角色表');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('roles');
}
};

View File

@@ -0,0 +1,11 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\Permissions\Http\Controllers\RolesController;
Route::prefix('permissions')->group(function () {
Route::apiResource('roles', RolesController::class);
//next
});

View File

@@ -0,0 +1,95 @@
<template>
<el-form :model="formData" label-width="120px" ref="form" v-loading="loading" class="pr-6">
<el-form-item label="上级角色" prop="parent_id">
<el-cascader :options="roles" name="parent_id" v-model="formData.parent_id" clearable :props="{ value: 'id', label: 'role_name', checkStrictly: true }" class="w-full" />
</el-form-item>
<el-form-item
label="角色名称"
prop="role_name"
:rules="[
{
required: true,
message: '角色名称必须填写',
},
]"
>
<el-input v-model="formData.role_name" name="role_name" clearable />
</el-form-item>
<el-form-item
label="角色标识"
prop="identify"
:rules="[
{
required: true,
message: '角色标识必须填写',
},
]"
>
<el-input v-model="formData.identify" name="identify" clearable />
</el-form-item>
<el-form-item label="角色描述" prop="description">
<el-input v-model="formData.description" name="description" clearable type="textarea" />
</el-form-item>
<el-form-item label="数据权限" prop="data_range">
<Select v-model="formData.data_range" name="data_range" clearable api="dataRange" class="w-full" />
</el-form-item>
<div class="flex justify-end">
<el-button type="primary" @click="submitForm(form)">{{ $t('system.confirm') }}</el-button>
</div>
</el-form>
</template>
<script lang="ts" setup>
import { useCreate } from '/admin/composables/curd/useCreate'
import { useShow } from '/admin/composables/curd/useShow'
import { onMounted, ref, watch } from 'vue'
import http from '/admin/support/http'
const props = defineProps({
primary: String | Number,
api: String,
})
const emit = defineEmits(['close'])
const { formData, form, loading, submitForm, isClose, beforeCreate, beforeUpdate } = useCreate(props.api, props.primary)
beforeCreate.value = () => {
formData.value.parent_id = getParent(formData.value.parent_id)
}
beforeUpdate.value = () => {
formData.value.parent_id = getParent(formData.value.parent_id)
}
const getParent = (parentId: any) => {
return typeof parentId === 'undefined' ? 0 : parentId[parentId.length - 1]
}
watch(isClose, function (value) {
if (value) {
emit('close')
}
})
const roles = ref()
onMounted(() => {
if (props.primary) {
useShow(props.api, props.primary).then(r => {
r.data.parent_id = r.data.parent_id ? [r.data.parent_id ] : 0;
formData.value = r.data
if (!formData.value.data_range) {
formData.value.data_range = null
}
})
}
http.get(props.api).then(r => {
roles.value = r.data.data
})
})
</script>

View File

@@ -0,0 +1,80 @@
<template>
<div>
<div class="w-full min-h-0 bg-white dark:bg-regal-dark pl-5 pt-5 pr-5 rounded-lg">
<el-form :inline="true">
<el-form-item label="角色名称" prop="role_name">
<el-input v-model="query.role_name" name="role_name" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search()">
<Icon name="magnifying-glass" class="w-4 mr-1 -ml-1" />
搜索
</el-button>
<el-button @click="reset()">
<Icon name="arrow-path" class="w-4 mr-1 -ml-1" />
重置
</el-button>
</el-form-item>
</el-form>
</div>
<div class="pl-2 pr-2 bg-white dark:bg-regal-dark rounded-lg mt-4 pb-10">
<div class="pt-5 pl-2">
<Add @click="show(null)" />
</div>
<el-table :data="tableData" class="mt-3" v-loading="loading" row-key="id" default-expand-all :tree-props="{ children: 'children' }">
<el-table-column prop="role_name" label="角色名称" />
<el-table-column prop="identify" label="角色标识" />
<el-table-column prop="description" label="角色描述" />
<el-table-column prop="created_at" label="创建时间" />
<el-table-column label="操作" width="200">
<template #default="scope">
<Update @click="show(scope.row.id)" />
<Destroy @click="destroy(api, scope.row.id)" />
</template>
</el-table-column>
</el-table>
</div>
<Dialog v-model="visible" :title="title" destroy-on-close>
<Create @close="close" :primary="id" :api="api" />
</Dialog>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import Create from './create.vue'
import { useGetList } from '/admin/composables/curd/useGetList'
import { useDestroy } from '/admin/composables/curd/useDestroy'
import { useEnabled } from '/admin/composables/curd/useEnabled'
import { t } from '/admin/support/helper'
const visible = ref<boolean>(false)
const id = ref(null)
const api = 'permissions/roles'
const title = ref<string>('')
const { data, query, search, reset, loading } = useGetList(api)
const { destroy, isDeleted } = useDestroy()
onMounted(() => search())
const tableData = computed(() => data.value?.data)
const close = () => {
visible.value = false
reset()
}
const show = primary => {
title.value = primary ? t('system.edit') : t('system.add')
id.value = primary
visible.value = true
}
watch(isDeleted, function () {
// change origin status
isDeleted.value = false
reset()
})
</script>

View File

@@ -0,0 +1,20 @@
import { RouteRecordRaw } from 'vue-router'
// @ts-ignore
const router: RouteRecordRaw[] = [
{
path: '/permission',
component: () => import('/admin/layout/index.vue'),
meta: { title: '权限管理', icon: 'user' },
children: [
{
path: 'roles',
name: 'roles',
meta: { title: '角色管理', icon: 'home' },
component: () => import('./roles/index.vue'),
},
],
},
]
export default router

View File

@@ -0,0 +1,24 @@
<?php
namespace Modules\User\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Http\Request;
use Illuminate\Queue\SerializesModels;
class Login
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(
public Request $request,
public string $token
) {
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Modules\User\Http\Controllers;
use Catch\Base\CatchController as Controller;
use Catch\Exceptions\FailedException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Event;
use Modules\User\Events\Login;
class AuthController extends Controller
{
/**
* @param Request $request
* @return array
*/
public function login(Request $request)
{
$token = Auth::guard(getGuardName())->attempt($request->only(['email', 'password']));
Event::dispatch(new Login($request, $token));
if (! $token) {
throw new FailedException('登录失败!请检查邮箱或者密码');
}
return compact('token');
}
/**
* logout
*
* @return bool
*/
public function logout()
{
// Auth::guard(Helper::getGuardName())->logout();
return true;
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Modules\User\Http\Controllers;
use Catch\Base\CatchController as Controller;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\Request;
use Modules\User\Models\LogLogin;
use Modules\User\Models\Users;
class UserController extends Controller
{
public function __construct(
protected readonly Users $user
) {
}
/**
* get list
*
* @return mixed
*/
public function index()
{
return $this->user->getList();
}
/**
* store
*
* @param Request $request
* @return false|mixed
*/
public function store(Request $request)
{
return $this->user->storeBy($request->all());
}
/**
* show
*
* @param $id
* @return mixed
*/
public function show($id)
{
return $this->user->firstBy($id)->makeHidden('password');
}
/**
* update
*
* @param $id
* @param Request $request
* @return mixed
*/
public function update($id, Request $request)
{
return $this->user->updateBy($id, $request->all());
}
/**
* destroy
*
* @param $id
* @return bool|null
*/
public function destroy($id)
{
return $this->user->deleteBy($id);
}
/**
* enable
*
* @param $id
* @return bool
*/
public function enable($id)
{
return $this->user->disOrEnable($id);
}
/**
* online user
*
* @return Authenticatable
*/
public function online(Request $request)
{
/* @var Users $user */
$user = $this->getLoginUser();
if ($request->isMethod('post')) {
return $user->updateBy($user->id, $request->all());
}
return $user;
}
/**
* login log
* @param LogLogin $logLogin
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
public function loginLog(LogLogin $logLogin)
{
return $logLogin->getUserLogBy($this->getLoginUser()->email);
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Modules\User\Listeners;
use Catch\Enums\Status;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
use Modules\User\Events\Login as Event;
use Modules\User\Models\LogLogin;
use Modules\User\Models\Users;
class Login
{
/**
* Handle the event.
*
* @param Event $event
* @return void
*/
public function handle(Event $event): void
{
$request = $event->request;
$this->log($request, (bool) $event->token);
if ($event->token) {
/* @var Users $user */
$user = Auth::guard(getGuardName())->user();
$user->login_ip = $request->ip();
$user->login_at = time();
$user->remember_token = $event->token;
$user->save();
}
}
/**
* login log
*
* @param Request $request
* @param int $isSuccess
* @return void
*/
protected function log(Request $request, int $isSuccess): void
{
LogLogin::insert([
'account' => $request->get('email'),
'login_ip' => $request->ip(),
'browser' => $this->getBrowserFrom(Str::of($request->userAgent())),
'platform' => $this->getPlatformFrom(Str::of($request->userAgent())),
'login_at' => time(),
'status' => $isSuccess ? Status::Enable : Status::Disable
]);
}
/**
* get platform
*
* @param Stringable $userAgent
* @return string
*/
protected function getBrowserFrom(Stringable $userAgent): string
{
return match (true) {
$userAgent->contains('MSIE', true) => 'IE',
$userAgent->contains('Firefox', true) => 'Firefox',
$userAgent->contains('Chrome', true) => 'Chrome',
$userAgent->contains('Opera', true) => 'Opera',
$userAgent->contains('Safari', true) => 'Safari',
default => 'unknown'
};
}
/**
* get os name
*
* @param Stringable $userAgent
* @return string
*/
protected function getPlatformFrom(Stringable $userAgent): string
{
return match (true) {
$userAgent->contains('win', true) => 'Windows',
$userAgent->contains('mac', true) => 'Mac OS',
$userAgent->contains('linux', true) => 'Linux',
$userAgent->contains('iphone', true) => 'iphone',
$userAgent->contains('android', true) => 'Android',
default => 'unknown'
};
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Modules\User\Models;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Model;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class LogLogin extends Model
{
protected $table = 'log_login';
public $timestamps = false;
protected $fillable = [
'id', 'account', 'login_ip', 'browser', 'platform', 'login_at', 'status',
];
protected $casts = [
'login_at' => 'datetime:Y-m-d H:i'
];
/**
*
* @param string $email
* @return LengthAwarePaginator
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function getUserLogBy(string $email): LengthAwarePaginator
{
return self::query()->where('account', $email)
->paginate(request()->get('limit', 10));
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Modules\User\Models;
use Catch\Base\CatchModel as Model;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Auth\Authenticatable;
/**
* @property int $id
* @property string $username
* @property string $email
* @property string $avatar
* @property string $password
* @property int $creator_id
* @property int $status
* @property string $login_ip
* @property int $login_at
* @property int $created_at
* @property int $updated_at
* @property string $remember_token
*/
class Users extends Model implements AuthenticatableContract, JWTSubject
{
use Authenticatable;
protected $fillable = [
'id', 'username', 'email', 'avatar', 'password', 'remember_token', 'creator_id', 'status', 'login_ip', 'login_at', 'created_at', 'updated_at', 'deleted_at'
];
/**
* @var array|string[]
*/
public array $searchable = [
'username' => 'like',
'email' => 'like',
'status' => '='
];
/**
* @var string
*/
protected $table = 'users';
/**
* @var array|string[]
*/
protected array $form = ['username', 'email', 'password'];
/**
*
* @return mixed
*/
public function getJWTIdentifier(): mixed
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims(): array
{
return [];
}
/**
* password
*
* @return Attribute
*/
protected function password(): Attribute
{
return new Attribute(
// get: fn($value) => '',
set: fn ($value) => bcrypt($value),
);
}
/**
* update
* @param $id
* @param array $data
* @return mixed
*/
public function updateBy($id, array $data): mixed
{
if (isset($data['password']) && ! $data['password']) {
unset($data['password']);
}
return parent::updateBy($id, $data);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Modules\User\Providers;
use Catch\CatchAdmin;
use Catch\Providers\CatchModuleServiceProvider;
use Modules\User\Events\Login;
use Modules\User\Listeners\Login as LoginListener;
class UserServiceProvider extends CatchModuleServiceProvider
{
protected array $events = [
Login::class => LoginListener::class
];
/**
* route path
*
* @return string|array
*/
public function routePath(): string|array
{
// TODO: Implement path() method.
return CatchAdmin::getModuleRoutePath('user');
}
public function registerEvents(array $events): void
{
parent::registerEvents($events); // TODO: Change the autogenerated stub
}
}

View File

@@ -0,0 +1,54 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('username')->comment('昵称');
$table->string('password')->comment('密码');
$table->string('email')->comment('邮箱');
$table->string('avatar')->comment('头像');
$table->string('remember_token', 1000)->comment('token');
$table->integer('creator_id');
$table->status();
$table->string('login_ip')->comment('登录IP');
$table->integer('login_at')->comment('登录时间');
$table->createdAt();
$table->updatedAt();
$table->deletedAt();
$table->comment('用户表');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
}
};

View File

@@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('log_login', function (Blueprint $table) {
$table->increments('id');
$table->string('account')->comment('登录账户');
$table->string('login_ip')->comment('登录的IP');
$table->string('browser')->comment('浏览器');
$table->string('platform')->comment('平台');
$table->integer('login_at')->comment('平台');
$table->status();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
}
};

15
modules/User/route.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\User\Http\Controllers\AuthController;
use Modules\User\Http\Controllers\UserController;
// login route
Route::post('login', [AuthController::class, 'login'])->withoutMiddleware(config('catch.route.middlewares'));
Route::post('logout', [AuthController::class, 'logout']);
// users route
Route::apiResource('users', UserController::class);
Route::put('users/enable/{id}', [UserController::class, 'enable']);
Route::match(['post', 'get'], 'user/online', [UserController::class, 'online']);
Route::get('user/login/log', [UserController::class, 'loginLog']);

View File

@@ -0,0 +1,26 @@
import { RouteRecordRaw } from 'vue-router'
// @ts-ignore
const router: RouteRecordRaw[] = [
{
path: '/users',
component: () => import('/admin/layout/index.vue'),
meta: { title: '用户管理', icon: 'user' },
children: [
{
path: 'index',
name: 'users',
meta: { title: '账号管理', icon: 'home' },
component: () => import('./user/index.vue'),
},
{
path: 'center',
name: 'center',
meta: { title: '个人中心', icon: 'home' },
component: () => import('./user/center.vue'),
},
],
},
]
export default router

View File

@@ -0,0 +1,44 @@
<template>
<div class="flex flex-col sm:flex-row dark:bg-regal-dark w-full">
<el-card shadow="never" class="w-full sm:w-[35rem]">
<template #header>
<div class="card-header">
<span>个人资料</span>
</div>
</template>
<div class="flex flex-col w-full">
<div class="w-full">
<Profile />
</div>
</div>
</el-card>
<el-tabs v-model="activeName" class="pl-3 pr-3 bg-white dark:bg-regal-dark mt-2 sm:mt-0 w-full ml-2">
<el-tab-pane label="登录日志" name="login_log">
<LoginLog />
</el-tab-pane>
<el-tab-pane label="操作日志" name="operation_log">
<OperateLog />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import Profile from './components/profile.vue'
import LoginLog from './components/loginLog.vue'
import OperateLog from './components/operateLog.vue'
const activeName = ref('login_log')
</script>
<style scoped>
.el-tabs {
--el-tabs-header-height: 62px !important;
}
.el-tabs .el-tabs__item {
font-size: 18px !important;
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<el-table :data="tableData" class="mt-3" v-loading="loading">
<el-table-column prop="account" label="账户" width="150px" />
<el-table-column prop="browser" label="浏览器" width="100px" />
<el-table-column prop="platform" label="平台" width="100px" />
<el-table-column prop="login_ip" label="IP" width="120px" />
<el-table-column prop="status" label="状态" width="100px">
<template #default="scope">
<el-tag type="success" v-if="scope.row.status === 1">成功</el-tag>
<el-tag type="danger" v-else>失败</el-tag>
</template>
</el-table-column>
<el-table-column prop="login_at" label="登录时间" />
</el-table>
<div class="pt-2 pb-2 flex justify-end">
<el-pagination
background
v-if="total > query.limit"
layout="total,sizes,prev, pager,next"
:current-page="query.page"
:page-size="query.limit"
@current-change="changePage"
@size-change="changeLimit"
:total="total"
:page-sizes="[10, 20, 30, 50]"
/>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted } from 'vue'
import { useGetList } from '/admin/composables/curd/useGetList'
const api = 'user/login/log'
const { data, query, search, changePage, changeLimit, loading } = useGetList(api)
onMounted(() => search())
const tableData = computed(() => data.value?.data)
const total = computed(() => data.value?.total)
</script>
<style scoped></style>

View File

@@ -0,0 +1,43 @@
<template>
<el-table :data="tableData" class="mt-3" v-loading="loading">
<el-table-column prop="username" label="用户名" width="180" />
<el-table-column prop="avatar" label="头像" width="180" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="status" label="状态">
<template #default="scope">
<Status v-model="scope.row.status" :id="scope.row.id" :api="api" />
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" />
</el-table>
<div class="pt-2 pb-2 flex justify-end">
<el-pagination
background
v-if="total > query.limit"
layout="total,sizes,prev, pager,next"
:current-page="query.page"
:page-size="query.limit"
@current-change="changePage"
@size-change="changeLimit"
:total="total"
:page-sizes="[10, 20, 30, 50]"
/>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted } from 'vue'
import { useGetList } from '/admin/composables/curd/useGetList'
const api = 'users'
const { data, query, search, reset, changePage, changeLimit, loading } = useGetList(api)
onMounted(() => search())
const tableData = computed(() => data.value?.data)
const total = computed(() => data.value?.total)
</script>
<style scoped></style>

View File

@@ -0,0 +1,88 @@
<template>
<el-form :model="profile" ref="form" v-loading="loading" label-position="top">
<el-upload
class="w-28 h-28 rounded-full mx-auto"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img src="https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg" class="h-28 rounded-full" />
</el-upload>
<el-form-item
label="昵称"
prop="username"
class="mt-2"
:rules="[
{
required: true,
message: '昵称必须填写',
},
]"
>
<el-input v-model="profile.username" placeholder="请填写昵称" />
</el-form-item>
<el-form-item
label="邮箱"
prop="email"
:rules="[
{
required: true,
message: '邮箱必须填写',
},
{
type: 'email',
message: '邮箱格式不正确',
},
]"
>
<el-input v-model="profile.email" placeholder="请填写邮箱" />
</el-form-item>
<el-form-item
label="密码"
prop="password"
:rules="[
{
pattern: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,20}$/,
message: '必须包含大小写字母和数字的组合可以使用特殊字符长度在6-20之间',
},
]"
>
<el-input v-model="profile.password" type="password" show-password placeholder="请输入密码" />
</el-form-item>
<div class="flex justify-center">
<el-button type="primary" @click="submitForm(form)">{{ $t('system.update') }}</el-button>
</div>
</el-form>
</template>
<script lang="ts" setup>
import { onMounted, ref, unref } from 'vue'
import { useCreate } from '/admin/composables/curd/useCreate'
import http from '/admin/support/http'
interface profile {
avatar: string
username: string
email: string
password: string
}
const profile = ref<profile>(
Object.assign({
avatar: '',
username: '',
email: '',
password: '',
}),
)
onMounted(() => {
http.get('user/online').then(r => {
profile.value.username = r.data.data.username
profile.value.avatar = r.data.data.avatar
profile.value.email = r.data.data.email
})
})
const { form, loading, submitForm } = useCreate('user/online', null, profile)
</script>

View File

@@ -0,0 +1,82 @@
<template>
<el-form :model="formData" label-width="120px" ref="form" v-loading="loading" class="pr-4">
<el-form-item
label="昵称"
prop="username"
:rules="[
{
required: true,
message: '昵称必须填写',
},
]"
>
<el-input v-model="formData.username" placeholder="请填写昵称" />
</el-form-item>
<el-form-item
label="邮箱"
prop="email"
:rules="[
{
required: true,
message: '邮箱必须填写',
},
{
type: 'email',
message: '邮箱格式不正确',
},
]"
>
<el-input v-model="formData.email" placeholder="请填写邮箱" />
</el-form-item>
<el-form-item label="密码" prop="password" :rules="passwordRules">
<el-input v-model="formData.password" type="password" show-password placeholder="请输入密码" />
</el-form-item>
<div class="flex justify-end">
<el-button type="primary" @click="submitForm(form)">{{ $t('system.confirm') }}</el-button>
</div>
</el-form>
</template>
<script lang="ts" setup>
import { useCreate } from '/admin/composables/curd/useCreate'
import { useShow } from '/admin/composables/curd/useShow'
import { onMounted, watch, ref } from 'vue'
const props = defineProps({
primary: String | Number,
api: String,
})
const passwordRules = [
{
required: true,
message: '密码必须填写',
},
{
pattern: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,20}$/,
message: '必须包含大小写字母和数字的组合可以使用特殊字符长度在6-20之间',
},
]
if (props.primary) {
passwordRules.shift()
}
const { formData, form, loading, submitForm, isClose } = useCreate(props.api, props.primary)
const emit = defineEmits(['close'])
watch(isClose, function (value) {
if (value) {
emit('close')
}
})
onMounted(() => {
if (props.primary) {
useShow(props.api, props.primary).then(r => {
formData.value = r.data
})
}
})
</script>

View File

@@ -0,0 +1,104 @@
<template>
<div>
<div class="w-full min-h-0 bg-white dark:bg-regal-dark pl-5 pt-5 pr-5 rounded-lg">
<el-form :inline="true">
<el-form-item label="用户名">
<el-input v-model="query.username" clearable />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="query.email" clearable />
</el-form-item>
<el-form-item label="状态">
<Select v-model="query.status" clearable api="status" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search()">
<Icon name="magnifying-glass" class="w-4 mr-1 -ml-1" />
搜索
</el-button>
<el-button @click="reset()">
<Icon name="arrow-path" class="w-4 mr-1 -ml-1" />
重置
</el-button>
</el-form-item>
</el-form>
</div>
<div class="pl-2 pr-2 bg-white dark:bg-regal-dark rounded-lg mt-4">
<div class="pt-5 pl-2">
<Add @click="show(null)" />
</div>
<el-table :data="tableData" class="mt-3" v-loading="loading">
<el-table-column prop="username" label="用户名" width="180" />
<el-table-column prop="avatar" label="头像" width="180" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="status" label="状态">
<template #default="scope">
<Status v-model="scope.row.status" :id="scope.row.id" :api="api" />
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" />
<el-table-column label="操作" width="200">
<template #default="scope">
<Update @click="show(scope.row.id)" />
<Destroy @click="destroy(api, scope.row.id)" />
</template>
</el-table-column>
</el-table>
<div class="pt-2 pb-2 flex justify-end">
<el-pagination
background
layout="total,sizes,prev, pager,next"
:current-page="query.page"
:page-size="query.limit"
@current-change="changePage"
@size-change="changeLimit"
:total="total"
:page-sizes="[10, 20, 30, 50]"
/>
</div>
</div>
<Dialog v-model="visible" :title="title" destroy-on-close>
<Create @close="close" :primary="id" :api="api" />
</Dialog>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import Create from './create.vue'
import { useGetList } from '/admin/composables/curd/useGetList'
import { useDestroy } from '/admin/composables/curd/useDestroy'
import { useEnabled } from '/admin/composables/curd/useEnabled'
import { t } from '/admin/support/helper'
const visible = ref<boolean>(false)
const id = ref(null)
const api = 'users'
const title = ref<string>('')
const { data, query, search, reset, changePage, changeLimit, loading } = useGetList(api)
const { destroy, isDeleted } = useDestroy()
onMounted(() => search())
const tableData = computed(() => data.value?.data)
const total = computed(() => data.value?.total)
const close = () => {
visible.value = false
reset()
}
const show = primary => {
title.value = primary ? t('system.edit') : t('system.add')
id.value = primary
visible.value = true
}
watch(isDeleted, function () {
isDeleted.value = false
reset()
})
</script>