feat:动态权限

This commit is contained in:
JaguarJack
2022-12-10 18:29:42 +08:00
parent 8c537e6656
commit c4270a2fc8
46 changed files with 948 additions and 177 deletions

View File

@@ -2,7 +2,7 @@
<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"
prop="title"
:rules="[
{
required: true,
@@ -10,7 +10,7 @@
},
]"
>
<el-input v-model="formData.name" />
<el-input v-model="formData.title" />
</el-form-item>
<el-form-item
:label="$t('module.form.path.title')"
@@ -48,18 +48,18 @@
import { useCreate } from '/admin/composables/curd/useCreate'
import { useShow } from '/admin/composables/curd/useShow'
import { computed, onMounted, watch } from 'vue'
import { onMounted } from 'vue'
const props = defineProps({
primary: String | Number,
api: String,
})
const { formData, form, loading, submitForm, isClose } = useCreate(
const { formData, form, loading, submitForm, close } = useCreate(
props.api,
props.primary,
Object.assign({
name: '',
title: '',
path: '',
keywords: '',
description: '',
@@ -73,13 +73,12 @@ const { formData, form, loading, submitForm, isClose } = useCreate(
)
const emit = defineEmits(['close'])
watch(isClose, function (value) {
if (value) {
emit('close')
}
})
if (props.primary) {
useShow(props.api, props.primary, formData)
}
onMounted(() => {
close(() => emit('close'))
})
</script>

View File

@@ -3,14 +3,14 @@
<Search :search="search" :reset="reset">
<template v-slot:body>
<el-form-item label="模块名称">
<el-input v-model="query.name" name="name" clearable />
<el-input v-model="query.title" name="title" clearable />
</el-form-item>
</template>
</Search>
<div class="pl-2 pr-2 bg-white dark:bg-regal-dark rounded-lg mt-4 pb-6">
<Operate :show="open" />
<el-table :data="tableData" class="mt-3" v-loading="loading">
<el-table-column prop="name" label="模块名称" width="180" />
<el-table-column prop="title" label="模块名称" width="180" />
<el-table-column prop="path" label="模块目录" width="180" />
<el-table-column prop="version" label="模块版本">
<template #default="scope">
@@ -19,7 +19,7 @@
</el-table-column>
<el-table-column prop="enable" label="模块状态">
<template #default="scope">
<Status v-model="scope.row.status" :id="scope.row.id" :api="api" />
<Status v-model="scope.row.enable" :id="scope.row.name" :api="api" />
</template>
</el-table-column>
<el-table-column label="操作" width="300">
@@ -31,7 +31,7 @@
</el-table>
</div>
<Dialog v-model="visible" :title="title" destroy-on-close>
<Create @close="close" :primary="id" :api="api" />
<Create @close="close(reset)" :primary="id" :api="api" />
</Dialog>
</div>
</template>
@@ -54,6 +54,6 @@ const tableData = computed(() => data.value?.data)
onMounted(() => {
search()
deleted()
deleted(reset)
})
</script>

View File

@@ -0,0 +1,40 @@
<?php
namespace Modules\Options\Repository;
use Catch\CatchAdmin;
use Catch\Support\Composer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class Components implements OptionInterface
{
/**
* @var array|string[]
*/
protected array $components = [
[
'label' => 'layout',
'value' => '/admin/layout/index.vue'
]
];
public function get(): array
{
if ($module = request()->get('module')) {
$components = File::glob(CatchAdmin::getModuleViewsPath($module) . '*/*.vue');
foreach ($components as $component) {
$this->components[] = [
'label' => Str::of($component)->explode(DIRECTORY_SEPARATOR)->pop(2)->pop(),
'value' => Str::of($component)->replace(CatchAdmin::moduleRootPath(), '')->prepend('/')
];
}
}
return $this->components;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Modules\Options\Repository;
use Catch\CatchAdmin;
use Catch\Support\Composer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class Controllers implements OptionInterface
{
public function get(): array
{
$controllers = [];
if ($module = request()->get('module')) {
$controllerFiles = File::glob(CatchAdmin::getModuleControllerPath($module) . '*.php');
foreach ($controllerFiles as $controllerFile) {
$controllers[] = [
'label' => Str::of(File::name($controllerFile))->remove('Controller'),
'value' => Str::of(File::name($controllerFile))->remove('Controller'),
];
}
}
return $controllers;
}
}

View File

@@ -43,4 +43,15 @@ class PermissionsController extends Controller
{
return $this->model->deleteBy($id);
}
/**
* enable
*
* @param $id
* @return bool
*/
public function enable($id)
{
return $this->model->disOrEnable($id, 'hidden');
}
}

View File

@@ -55,4 +55,14 @@ class PermissionsModel extends Model
{
return self::query()->select($this->fieldsInList)->quickSearch()->get()->toTree();
}
/**
* is hidden
*
* @return bool
*/
public function isHidden(): bool
{
return $this->hidden === 2;
}
}

View File

@@ -4,7 +4,8 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
return new class extends Migration
{
/**
* Run the migrations.
*
@@ -12,6 +13,10 @@ return new class () extends Migration {
*/
public function up()
{
if (Schema::hasTable('roles')) {
return ;
}
Schema::create('roles', function (Blueprint $table) {
$table->increments('id');
$table->string('role_name', 30)->comment('角色名称');

View File

@@ -4,7 +4,8 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
return new class extends Migration
{
/**
* Run the migrations.
*
@@ -12,6 +13,10 @@ return new class () extends Migration {
*/
public function up()
{
if (Schema::hasTable('jobs')) {
return ;
}
Schema::create('jobs', function (Blueprint $table) {
$table->increments('id');
$table->string('job_name', 50)->comment('岗位名称');

View File

@@ -4,7 +4,8 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
return new class extends Migration
{
/**
* Run the migrations.
*
@@ -12,6 +13,10 @@ return new class () extends Migration {
*/
public function up()
{
if (Schema::hasTable('departments')) {
return;
}
Schema::create('departments', function (Blueprint $table) {
$table->increments('id');
$table->integer('parent_id')->default(0)->comment('父级ID');

View File

@@ -1,48 +0,0 @@
<?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('permissions', function (Blueprint $table) {
$table->increments('id');
$table->string('permission_name', 30)->comment('菜单名称');
$table->integer('parent_id')->default('1')->comment('父级菜单');
$table->string('route', 50)->comment('路由');
$table->string('icon', 50)->comment('菜单 icon');
$table->string('module', 50)->comment('所属模块');
$table->string('permission_mark')->comment('权限标识');
$table->string('component')->comment('组件');
$table->string('redirect')->comment('组件跳转');
$table->tinyInteger('keepalive')->default('1')->comment('1 缓存 2 不缓存');
$table->tinyInteger('type')->default('1')->comment('1 菜单 2 按钮');
$table->tinyInteger('hidden')->default('1')->comment('1 显示 2 隐藏');
$table->integer('sort')->default('1')->comment('排序');
$table->creatorId();
$table->createdAt();
$table->updatedAt();
$table->deletedAt();
$table->engine = 'InnoDB';
$table->comment('权限模块');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('permissions');
}
};

View File

@@ -12,6 +12,10 @@ return new class () extends Migration {
*/
public function up()
{
if (Schema::hasTable('permissions')) {
return;
}
Schema::create('permissions', function (Blueprint $table) {
$table->increments('id');
$table->integer('parent_id')->default(0)->comment('父级菜单');
@@ -19,7 +23,7 @@ return new class () extends Migration {
$table->string('route', 50)->comment('前端路由');
$table->string('icon', 50)->comment('菜单 icon');
$table->string('module', 50)->comment('所属模块');
$table->string('permission_mark', 100)->comment('权限标识,使用 @ 分割');
$table->string('permission_mark', 100)->default('')->comment('权限标识,使用 @ 分割');
$table->string('component')->comment('组件');
$table->string('redirect')->nullable()->comment('跳转地址');
$table->tinyInteger('keepalive')->default('1')->comment('1 缓存 2 不缓存');

View File

@@ -0,0 +1,34 @@
<?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('user_has_roles', function (Blueprint $table) {
$table->integer('user_id')->comment('users primary key');
$table->integer('role_id')->comment('roles primary key');
$table->comment('user relate roles');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
}
};

View File

@@ -0,0 +1,34 @@
<?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('role_has_permissions', function (Blueprint $table) {
$table->integer('role_id')->comment('roles primary key');
$table->integer('permission_id')->comment('permissions primary key');
$table->comment('role relate permissions');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
}
};

View File

@@ -0,0 +1,34 @@
<?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('user_has_jobs', function (Blueprint $table) {
$table->integer('user_id')->comment('users primary key');
$table->integer('job_id')->comment('jobs primary key');
$table->comment('user relate jobs');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
}
};

View File

@@ -0,0 +1,34 @@
<?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('role_has_departments', function (Blueprint $table) {
$table->integer('role_id')->comment('roles primary key');
$table->integer('department_id')->comment('departments primary key');
$table->comment('role relate departments');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
}
};

View File

@@ -16,5 +16,6 @@ Route::prefix('permissions')->group(function () {
Route::put('departments/enable/{id}', [DepartmentsController::class, 'enable']);
Route::apiResource('permissions', PermissionsController::class);
Route::put('permissions/enable/{id}', [PermissionsController::class, 'enable']);
//next
});

View File

@@ -35,7 +35,7 @@
<script lang="ts" setup>
import { computed, onMounted } from 'vue'
import Create from './create.vue'
import Create from './form/create.vue'
import { useGetList } from '/admin/composables/curd/useGetList'
import { useDestroy } from '/admin/composables/curd/useDestroy'
import { useOpen } from '/admin/composables/curd/useOpen'

View File

@@ -37,7 +37,7 @@
<script lang="ts" setup>
import { computed, onMounted } from 'vue'
import Create from './create.vue'
import Create from './form/create.vue'
import { useGetList } from '/admin/composables/curd/useGetList'
import { useDestroy } from '/admin/composables/curd/useDestroy'
import { useOpen } from '/admin/composables/curd/useOpen'

View File

@@ -17,19 +17,53 @@
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单名称" prop="permission_name">
<el-input v-model="formData.permission_name" name="permission_name" clearable />
<el-form-item label="菜单名称" prop="permission_name" :rules="[{ required: true, message: '菜单名称必须填写' }]">
<Select v-model="formData.permission_name" name="permission_name" :options="actionMenuNames" v-if="isAction" />
<el-input v-model="formData.permission_name" name="permission_name" clearable v-else />
</el-form-item>
<el-form-item label="所属模块" prop="module">
<Select v-model="formData.module" clearable api="modules" class="w-full" filterable />
<el-form-item label="所属模块" prop="module" :rules="[{ required: true, message: '所属模块必须填写' }]" v-if="!isAction">
<Select v-model="formData.module" api="modules" @clear="clearModule" />
</el-form-item>
<el-form-item label="菜单路由" prop="route">
<el-form-item label="路由Path" prop="route" :rules="[{ required: true, message: '路由Path必须填写' }]" v-if="!isAction">
<el-input v-model="formData.route" name="route" clearable />
</el-form-item>
<el-form-item label="Redirect" prop="redirect">
<el-form-item label="Redirect" prop="redirect" v-if="!isAction">
<el-input v-model="formData.redirect" name="redirect" clearable />
</el-form-item>
<el-form-item label="Keepalive" prop="keepalive">
<el-form-item label="排序" prop="sort">
<el-input-number v-model="formData.sort" name="sort" :min="1" />
</el-form-item>
</div>
<div>
<el-form-item label="父级菜单" prop="parent_id">
<el-cascader :options="permissions" name="parent_id" v-model="formData.parent_id" clearable :props="{ value: 'id', label: 'permission_name', checkStrictly: true }" class="w-full" />
</el-form-item>
<el-form-item label="权限标识" prop="permission_mark" :rules="[{ required: true, message: '权限标识必须填写' }]" v-if="!isTop">
<Select v-model="formData.permission_mark" name="permission_mark" :options="actionMenuNames" allow-create v-if="isAction" />
<Select v-model="formData.permission_mark" placeholder="请选择" api="controllers" :query="{ module: formData.module }" v-else />
</el-form-item>
<el-form-item label="菜单Icon" prop="icon" v-if="!isAction">
<el-input v-model="formData.icon" name="icon" clearable @focus="open" />
</el-form-item>
<el-form-item label="所属组件" prop="component" :rules="[{ required: true, message: '所属组件必须填写' }]" v-if="!isAction">
<Select v-model="formData.component" placeholder="请选择" allow-create api="components" :query="{ module: formData.module }" />
</el-form-item>
<el-form-item label="Hidden" prop="hidden" v-if="!isAction">
<el-radio-group v-model="formData.hidden">
<el-radio
v-for="item in [
{ label: '显示', value: 1 },
{ label: '隐藏', value: 2 },
]"
:key="item.value"
:label="item.value"
name="hidden"
>{{ item.label }}</el-radio
>
</el-radio-group>
</el-form-item>
<el-form-item label="Keepalive" prop="keepalive" v-if="!isAction">
<el-radio-group v-model="formData.keepalive">
<el-radio
v-for="item in [
@@ -44,48 +78,23 @@
</el-radio-group>
</el-form-item>
</div>
<div>
<el-form-item label="父级菜单" prop="parent_id">
<el-cascader :options="permissions" name="parent_id" v-model="formData.parent_id" clearable :props="{ value: 'id', label: 'permission_name', checkStrictly: true }" class="w-full" />
</el-form-item>
<el-form-item label="权限标识" prop="permission_mark">
<el-input v-model="formData.permission_mark" name="permission_mark" clearable />
</el-form-item>
<el-form-item label="菜单Icon" prop="icon">
<el-input v-model="formData.icon" name="icon" clearable />
</el-form-item>
<el-form-item label="所属组件" prop="component">
<el-select v-model="formData.component" placeholder="请选择" clearable class="w-full" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="formData.sort" name="sort" :min="1" />
</el-form-item>
<el-form-item label="Hidden" prop="hidden">
<el-radio-group v-model="formData.hidden">
<el-radio
v-for="item in [
{ label: '显示', value: 1 },
{ label: '隐藏', value: 2 },
]"
:key="item.value"
:label="item.value"
name="hidden"
>{{ item.label }}</el-radio
>
</el-radio-group>
</el-form-item>
</div>
</div>
<div class="flex justify-end">
<el-button type="primary" @click="submitForm(form)">{{ $t('system.confirm') }}</el-button>
</div>
</el-form>
<Dialog v-model="visible" title="选择 Icon" width="1000px" destroy-on-close>
<Icons v-model="formData.icon" @close="closeSelectIcon" />
</Dialog>
</template>
<script lang="ts" setup>
import { useCreate } from '/admin/composables/curd/useCreate'
import { useShow } from '/admin/composables/curd/useShow'
import { onMounted, ref } from 'vue'
import { useOpen } from '/admin/composables/curd/useOpen'
import { onMounted, ref, watch } from 'vue'
import http from '/admin/support/http'
const props = defineProps({
@@ -94,12 +103,23 @@ const props = defineProps({
})
const { formData, form, loading, submitForm, close, beforeCreate, beforeUpdate } = useCreate(props.api, props.primary)
const { open, visible } = useOpen()
//
const closeSelectIcon = () => {
visible.value = false
}
//
formData.value.sort = 1
formData.value.keepalive = 1
formData.value.type = 1
formData.value.hidden = 1
//
const isTop = ref<boolean>(true)
const isMenu = ref<boolean>(false)
const isAction = ref<boolean>(false)
//
if (props.primary) {
useShow(props.api, props.primary, formData)
}
@@ -110,14 +130,68 @@ onMounted(() => {
http.get(props.api).then(r => {
permissions.value = r.data.data
})
// close dialog
close(() => emit('close'))
// form data
watch(
formData,
(value, oldValue) => {
const type: number = formData.value.type
if (type === 1) {
isTop.value = true
isMenu.value = isAction.value = false
} else if (type === 2) {
isMenu.value = true
isTop.value = isAction.value = false
} else {
isAction.value = true
isTop.value = isMenu.value = false
}
},
{ deep: true },
)
})
// &
const clearModule = () => {
if (formData.value.type === 1 || formData.value.type === 2) {
formData.value.component = null
}
if (formData.value.type === 2) {
formData.value.permission_mark = null
}
}
// ,
const actionMenuNames = [
{ label: '列表', value: '列表' },
{ label: '新增', value: '新增' },
{ label: '读取', value: '读取' },
{ label: '更新', value: '更新' },
{ label: '删除', value: '删除' },
{ label: '禁用/启用', value: '禁用/启用' },
{ label: '导入', value: '导入' },
{ label: '导出', value: '导出' },
]
const actionMenuMark = [
{ label: 'index', value: 'index' },
{ label: 'store', value: 'store' },
{ label: 'show', value: 'show' },
{ label: 'update', value: 'update' },
{ label: 'destroy', value: 'destroy' },
{ label: 'enable', value: 'enable' },
{ label: 'import', value: 'import' },
{ label: 'export', value: 'export' },
]
//
beforeCreate.value = () => {
formData.value.parent_id = getParent(formData.value.parent_id)
}
//
beforeUpdate.value = () => {
formData.value.parent_id = getParent(formData.value.parent_id)
}

View File

@@ -13,7 +13,11 @@
<el-table-column prop="permission_name" label="菜单名称" />
<el-table-column prop="route" label="菜单路由" />
<el-table-column prop="permission_mark" label="权限标识" />
<el-table-column prop="hidden" label="状态" />
<el-table-column prop="hidden" label="状态">
<template #default="scope">
<Status v-model="scope.row.hidden" :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">
@@ -31,8 +35,8 @@
</template>
<script lang="ts" setup>
import { computed, onMounted, watch } from 'vue'
import Create from './create.vue'
import { computed, onMounted } from 'vue'
import Create from './form/create.vue'
import { useGetList } from '/admin/composables/curd/useGetList'
import { useDestroy } from '/admin/composables/curd/useDestroy'
import { useOpen } from '/admin/composables/curd/useOpen'

View File

@@ -32,7 +32,7 @@
<script lang="ts" setup>
import { computed, onMounted } from 'vue'
import Create from './create.vue'
import Create from './form/create.vue'
import { useGetList } from '/admin/composables/curd/useGetList'
import { useDestroy } from '/admin/composables/curd/useDestroy'
import { useOpen } from '/admin/composables/curd/useOpen'

View File

@@ -35,4 +35,4 @@ const router: RouteRecordRaw[] = [
},
]
export default router
export default []

View File

@@ -89,7 +89,7 @@ class UserController extends Controller
public function online(Request $request)
{
/* @var Users $user */
$user = $this->getLoginUser();
$user = $this->getLoginUser()->withPermissions();
if ($request->isMethod('post')) {
return $user->updateBy($user->id, $request->all());

View File

@@ -0,0 +1,98 @@
<?php
namespace Modules\User\Models\Traits;
use Catch\CatchAdmin;
use Catch\Support\Module\ModuleRepository;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
trait UserRelations
{
/**
* init traits
*/
public function initializeUserRelations(): void
{
if (app(ModuleRepository::class)->enabled('permissions')) {
$this->with = ['roles', 'jobs'];
}
}
/**
* roles
*
* @return BelongsToMany
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany($this->getRolesModel(), 'user_has_roles', 'user_id', 'role_id');
}
/**
* jobs
*
* @return BelongsToMany
*/
public function jobs(): BelongsToMany
{
return $this->belongsToMany($this->getJobsModel(), 'user_has_jobs', 'user_id', 'job_id');
}
/**
* permissions
*/
public function withPermissions(): self
{
/* @var \Modules\Permissions\Models\PermissionsModel $permissionsModel */
$permissionsModel = app($this->getPermissionsModel());
if ($this->isSuperAdmin()) {
$permissions = $permissionsModel->get();
} else {
$roles = app($this->getRolesModel())->with(['permissions'])->get();
$permissions = [];
}
$this->setAttribute('permissions', $permissions->each(fn($permission) => $permission->setAttribute('hidden', $permission->isHidden())));
return $this;
}
/**
* get RolesModel
*
* @see \Modules\Permissions\Models\RolesModel
* @return string
*/
protected function getRolesModel(): string
{
return '\\' . CatchAdmin::getModuleModelNamespace('permissions') . 'RolesModel';
}
/**
* get JobsModel
*
* @see \Modules\Permissions\Models\JobsModel
* @return string
*/
protected function getJobsModel(): string
{
return '\\'. CatchAdmin::getModuleModelNamespace('permissions') . 'JobsModel';
}
/**
* get PermissionsModel
*
* @see \Modules\Permissions\Models\PermissionsModel
* @return string
*/
protected function getPermissionsModel(): string
{
return '\\'. CatchAdmin::getModuleModelNamespace('permissions') . 'PermissionsModel';
}
}

View File

@@ -5,6 +5,7 @@ namespace Modules\User\Models;
use Catch\Base\CatchModel as Model;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Modules\User\Models\Traits\UserRelations;
use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Auth\Authenticatable;
@@ -24,7 +25,7 @@ use Illuminate\Auth\Authenticatable;
*/
class Users extends Model implements AuthenticatableContract, JWTSubject
{
use Authenticatable;
use Authenticatable, UserRelations;
protected $fillable = [
'id', 'username', 'email', 'avatar', 'password', 'remember_token', 'creator_id', 'status', 'login_ip', 'login_at', 'created_at', 'updated_at', 'deleted_at'