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

@ -16,6 +16,8 @@ return new class () extends Migration {
Schema::create(config('catch.module.table_name'), function (Blueprint $table) {
$table->increments('id');
$table->string('title')->comment('模块标题');
$table->string('name')->comment('模块名称');
$table->string('path', 20)->comment('模块目录');

View File

@ -44,8 +44,8 @@ class DatabaseDriver implements ModuleRepositoryInterface
public function all(array $search): Collection
{
return $this->model::query()
->when($search['name'] ?? false, function ($query) use ($search) {
$query->where('name', 'like', '%'.$search['name'].'%');
->when($search['title'] ?? false, function ($query) use ($search) {
$query->where('title', 'like', '%'.$search['title'].'%');
})->get();
}
@ -60,7 +60,7 @@ class DatabaseDriver implements ModuleRepositoryInterface
$this->hasSameModule($module);
return $this->model->save([
'name' => $module['name'],
'title' => $module['title'],
'path' => $module['path'],
'description' => $module['desc'],
'keywords' => $module['keywords'],
@ -91,7 +91,8 @@ class DatabaseDriver implements ModuleRepositoryInterface
return $this->model->where('name', $name)
->update([
'name' => $module['name'],
'title' => $module['title'],
'name' => $module['path'],
'path' => $module['path'],
'description' => $module['desc'],
'keywords' => $module['keywords'],
@ -119,7 +120,7 @@ class DatabaseDriver implements ModuleRepositoryInterface
{
$module = $this->show($name);
$module->status = (int) $module->status;
$module->enable = (int) $module->enable;
return $module->save();
}
@ -132,7 +133,7 @@ class DatabaseDriver implements ModuleRepositoryInterface
public function getEnabled(): Collection
{
// TODO: Implement getEnabled() method.
return $this->model->where('status', Status::Enable->value())->get();
return $this->model->where('enable', Status::Enable->value())->get();
}
/**
@ -144,7 +145,7 @@ class DatabaseDriver implements ModuleRepositoryInterface
public function enabled(string $moduleName): bool
{
// TODO: Implement enabled() method.
return $this->getEnabled()->pluck('path')->contains($moduleName);
return $this->getEnabled()->pluck('name')->contains($moduleName);
}
/**
@ -157,10 +158,6 @@ class DatabaseDriver implements ModuleRepositoryInterface
if ($this->model->where('name', $module['name'])->first()) {
throw new FailedException(sprintf('Module [%s] has been created', $module['name']));
}
if ($this->model->where('path', $module['path'])->first()) {
throw new FailedException(sprintf('Module Alias [%s] has been exised', $module['alias']));
}
}
/**

View File

@ -55,14 +55,14 @@ class FileDriver implements ModuleRepositoryInterface
$modules = Collection::make(\json_decode(File::get($this->moduleJson), true))->values();
$name = $search['name'] ?? '';
$title = $search['title'] ?? '';
if (! $name) {
if (! $title) {
return $modules;
}
return $modules->filter(function ($module) use ($name) {
return Str::of($module['name'])->contains($name);
return $modules->filter(function ($module) use ($title) {
return Str::of($module['title'])->contains($title);
});
}
@ -82,6 +82,8 @@ class FileDriver implements ModuleRepositoryInterface
$module['version'] = '1.0.0';
$module['enable'] = true;
$this->removeDirs($module);
File::put($this->moduleJson, $modules->push($module)->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
return true;
@ -115,12 +117,13 @@ class FileDriver implements ModuleRepositoryInterface
{
File::put($this->moduleJson, $this->all()->map(function ($m) use ($module, $name) {
if (Str::of($name)->exactly($m['name'])) {
$m['path'] = $module['path'];
$m['name'] = $module['name'];
$m['title'] = $module['title'];
$m['description'] = $module['description'] ?? '';
$m['keywords'] = $module['keywords'] ?? '';
$m['enable'] = $module['enable'];
}
$this->removeDirs($m);
return $m;
})->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
@ -180,7 +183,7 @@ class FileDriver implements ModuleRepositoryInterface
public function enabled(string $moduleName): bool
{
// TODO: Implement enabled() method.
return $this->getEnabled()->pluck('path')->contains($moduleName);
return $this->getEnabled()->pluck('name')->contains($moduleName);
}
/**
@ -195,10 +198,18 @@ class FileDriver implements ModuleRepositoryInterface
if ($modules->pluck('name')->contains($module['name'])) {
throw new FailedException(sprintf('Module [%s] has been created', $module['name']));
}
}
}
if ($modules->pluck('path')->contains($module['path'])) {
throw new FailedException(sprintf('Module path [%s] has been existed', $module['path']));
}
/**
* remove dirs
*
* @param array $modules
*/
protected function removeDirs(array &$modules)
{
if ($modules['dirs'] ?? false) {
unset($modules['dirs']);
}
}
}

View File

@ -59,6 +59,8 @@ class ModuleRepository
*/
public function create(array $module): bool
{
$module['name'] = lcfirst($module['path']);
Event::dispatch(new Creating($module));
$this->moduleRepository->create($module);
@ -93,6 +95,8 @@ class ModuleRepository
*/
public function update(string $name, array $module): bool
{
$module['name'] = lcfirst($module['path']);
Event::dispatch(new Updating($name, $module));
$this->moduleRepository->update($name, $module);

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'

View File

@ -1,5 +1,5 @@
<template>
<el-select v-bind="$attrs">
<el-select v-bind="$attrs" class="w-full" clearable filterable>
<el-option-group v-for="group in elOptions" :key="group.label" :label="group.label" v-if="group">
<el-option v-for="item in group.options" :key="item.value" :label="item.label" :value="item.value" />
</el-option-group>
@ -11,7 +11,7 @@
<script lang="ts" setup>
import http from '/admin/support/http'
import { ref } from 'vue'
import { ref, watch } from 'vue'
const props = defineProps({
options: {
@ -26,6 +26,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
query: {
type: Object,
default: null,
},
})
interface Option {
@ -38,11 +42,21 @@ interface GroupOption {
options: Array<Option>
}
const elOptions = props.group ? ref<Array<GroupOption>>() : ref<Array<Option>>()
if (props.api) {
http.get('options/' + props.api).then(r => {
const getOptions = () => {
http.get('options/' + props.api, props.query).then(r => {
elOptions.value = r.data.data
})
}
const elOptions = props.group ? ref<Array<GroupOption>>() : ref<Array<Option>>()
if (props.api) {
if (!props.query) {
getOptions()
} else {
watch(props, function () {
getOptions()
})
}
} else {
elOptions.value = props.options
}

View File

@ -0,0 +1,332 @@
<template>
<div :class="`grid ${grid} gap-y-6`">
<div v-for="icon in icons" :key="icon" class="flex justify-center hover:cursor-pointer" @click="selectIcon(icon)">
<div v-if="modelValue === icon">
<div class="flex justify-center w-full text-violet-700"><Icon :name="icon" /></div>
<div class="text-sm text-violet-700">{{ icon }}</div>
</div>
<div v-else>
<div class="flex justify-center w-full"><Icon :name="icon" /></div>
<div class="text-sm">{{ icon }}</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({
modelValue: {
type: String,
require: true,
},
grid: {
type: String,
default: 'grid-cols-5',
},
})
const emits = defineEmits(['update:modelValue', 'close'])
const selectIcon = (icon: string) => {
emits('update:modelValue', icon)
emits('close')
}
// icons
const icons = [
'academic-cap',
'adjustments-horizontal',
'adjustments-vertical',
'archive-box-arrow-down',
'archive-box-x-mark',
'archive-box',
'arrow-down-circle',
'arrow-down-left',
'arrow-down-on-square-stack',
'arrow-down-on-square',
'arrow-down-right',
'arrow-down-tray',
'arrow-down',
'arrow-left-circle',
'arrow-left-on-rectangle',
'arrow-left',
'arrow-long-down',
'arrow-long-left',
'arrow-long-right',
'arrow-long-up',
'arrow-path-rounded-square',
'arrow-path',
'arrow-right-circle',
'arrow-right-on-rectangle',
'arrow-right',
'arrow-small-down',
'arrow-small-left',
'arrow-small-right',
'arrow-small-up',
'arrow-top-right-on-square',
'arrow-trending-down',
'arrow-trending-up',
'arrow-up-circle',
'arrow-up-left',
'arrow-up-on-square-stack',
'arrow-up-on-square',
'arrow-up-right',
'arrow-up-tray',
'arrow-up',
'arrow-uturn-down',
'arrow-uturn-left',
'arrow-uturn-right',
'arrow-uturn-up',
'arrows-pointing-in',
'arrows-pointing-out',
'arrows-right-left',
'arrows-up-down',
'at-symbol',
'backspace',
'backward',
'banknotes',
'bars-2',
'bars-3-bottom-left',
'bars-3-bottom-right',
'bars-3-center-left',
'bars-3',
'bars-4',
'bars-arrow-down',
'bars-arrow-up',
'battery-0',
'battery-100',
'battery-50',
'beaker',
'bell-alert',
'bell-slash',
'bell-snooze',
'bell',
'bolt-slash',
'bolt',
'book-open',
'bookmark-slash',
'bookmark-square',
'bookmark',
'briefcase',
'bug-ant',
'building-library',
'building-office-2',
'building-office',
'building-storefront',
'cake',
'calculator',
'calendar-days',
'calendar',
'camera',
'chart-bar-square',
'chart-bar',
'chart-pie',
'chat-bubble-bottom-center-text',
'chat-bubble-bottom-center',
'chat-bubble-left-ellipsis',
'chat-bubble-left-right',
'chat-bubble-left',
'chat-bubble-oval-left-ellipsis',
'chat-bubble-oval-left',
'check-badge',
'check-circle',
'check',
'chevron-double-down',
'chevron-double-left',
'chevron-double-right',
'chevron-double-up',
'chevron-down',
'chevron-left',
'chevron-right',
'chevron-up-down',
'chevron-up',
'circle-stack',
'clipboard-document-check',
'clipboard-document-list',
'clipboard-document',
'clipboard',
'clock',
'cloud-arrow-down',
'cloud-arrow-up',
'cloud',
'code-bracket-square',
'code-bracket',
'cog-6-tooth',
'cog-8-tooth',
'cog',
'command-line',
'computer-desktop',
'cpu-chip',
'credit-card',
'cube-transparent',
'cube',
'currency-bangladeshi',
'currency-dollar',
'currency-euro',
'currency-pound',
'currency-rupee',
'currency-yen',
'cursor-arrow-rays',
'cursor-arrow-ripple',
'device-phone-mobile',
'device-tablet',
'document-arrow-down',
'document-arrow-up',
'document-chart-bar',
'document-check',
'document-duplicate',
'document-magnifying-glass',
'document-minus',
'document-plus',
'document-text',
'document',
'ellipsis-horizontal-circle',
'ellipsis-horizontal',
'ellipsis-vertical',
'envelope-open',
'envelope',
'exclamation-circle',
'exclamation-triangle',
'eye-dropper',
'eye-slash',
'eye',
'face-frown',
'face-smile',
'film',
'finger-print',
'fire',
'flag',
'folder-arrow-down',
'folder-minus',
'folder-open',
'folder-plus',
'folder',
'forward',
'funnel',
'gif',
'gift-top',
'gift',
'globe-alt',
'globe-americas',
'globe-asia-australia',
'globe-europe-africa',
'hand-raised',
'hand-thumb-down',
'hand-thumb-up',
'hashtag',
'heart',
'home-modern',
'home',
'identification',
'inbox-arrow-down',
'inbox-stack',
'inbox',
'information-circle',
'key',
'language',
'lifebuoy',
'light-bulb',
'link',
'list-bullet',
'lock-closed',
'lock-open',
'magnifying-glass-circle',
'magnifying-glass-minus',
'magnifying-glass-plus',
'magnifying-glass',
'map-pin',
'map',
'megaphone',
'microphone',
'minus-circle',
'minus-small',
'minus',
'moon',
'musical-note',
'newspaper',
'no-symbol',
'paint-brush',
'paper-airplane',
'paper-clip',
'pause-circle',
'pause',
'pencil-square',
'pencil',
'phone-arrow-down-left',
'phone-arrow-up-right',
'phone-x-mark',
'phone',
'photo',
'play-circle',
'play-pause',
'play',
'plus-circle',
'plus-small',
'plus',
'power',
'presentation-chart-bar',
'presentation-chart-line',
'printer',
'puzzle-piece',
'qr-code',
'question-mark-circle',
'queue-list',
'radio',
'receipt-percent',
'receipt-refund',
'rectangle-group',
'rectangle-stack',
'rocket-launch',
'rss',
'scale',
'scissors',
'server-stack',
'server',
'share',
'shield-check',
'shield-exclamation',
'shopping-bag',
'shopping-cart',
'signal-slash',
'signal',
'sparkles',
'speaker-wave',
'speaker-x-mark',
'square-2-stack',
'square-3-stack-3d',
'squares-2x2',
'squares-plus',
'star',
'stop-circle',
'stop',
'sun',
'swatch',
'table-cells',
'tag',
'ticket',
'trash',
'trophy',
'truck',
'tv',
'user-circle',
'user-group',
'user-minus',
'user-plus',
'user',
'users',
'variable',
'video-camera-slash',
'video-camera',
'view-columns',
'viewfinder-circle',
'wallet',
'wifi',
'window',
'wrench-screwdriver',
'wrench',
'x-circle',
'x-mark',
]
</script>
<style scoped></style>

View File

@ -1,10 +1,11 @@
<template>
<el-switch @change="enabled(api, id)" :active-value="Status.ENABLE" :inactive-value="Status.DISABLE" :model-value="modelValue" :loading="loading" />
<el-switch @change="enabled(api, id)" :active-value="activeValue" :inactive-value="inactiveValue" :model-value="modelValue" :loading="loading" />
</template>
<script lang="ts" setup>
import { useEnabled } from '/admin/composables/curd/useEnabled'
import { Status } from '/admin/enum/app'
import { ref } from 'vue'
const props = defineProps({
modelValue: Boolean | Number | String,
@ -16,8 +17,19 @@ const emits = defineEmits(['update:modelValue', 'refresh'])
const { enabled, success, loading, afterEnabled } = useEnabled()
const activeValue = ref<boolean | number | string>()
const inactiveValue = ref<boolean | number | string>()
if (typeof props.modelValue === 'boolean') {
activeValue.value = true
inactiveValue.value = false
} else {
activeValue.value = Status.ENABLE
inactiveValue.value = Status.DISABLE
}
success(() => {
emits('update:modelValue', props.modelValue === Status.ENABLE ? Status.DISABLE : Status.ENABLE)
emits('update:modelValue', props.modelValue === activeValue.value ? inactiveValue.value : activeValue.value)
})
afterEnabled.value = () => {

View File

@ -1,12 +1,12 @@
import { ref } from 'vue'
import { t } from '/admin/support/helper'
const visible = ref<boolean>(false)
const id = ref(null)
const title = ref<string>('')
export function useOpen() {
const visible = ref<boolean>(false)
const id = ref(null)
const title = ref<string>('')
const open = (primary: any) => {
console.log(primary)
title.value = primary ? t('system.edit') : t('system.add')
id.value = primary
visible.value = true

View File

@ -2,14 +2,14 @@ import http from '/admin/support/http'
import { Ref, ref } from 'vue'
import { isFunction } from '../../support/helper'
const loading = ref<boolean>(true)
const data = ref<object>()
// 后置钩子
const afterShow = ref()
export function useShow(path: string, id: string | number, fillData: null | Ref = null) {
const loading = ref<boolean>(true)
const data = ref<object>()
// 后置钩子
const afterShow = ref()
http.get(path + '/' + id).then(r => {
loading.value = false
data.value = r.data.data

View File

@ -37,7 +37,7 @@ export const enum WhiteListPage {
* menu
*/
export const enum MenuType {
PAGE_TYPE = 1,
Button_Type,
TOP_TYPE = 1,
PAGE_TYPE = 2,
Button_Type = 3,
}

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { h, defineComponent, VNode } from 'vue'
import { h, defineComponent, VNode, toRaw } from 'vue'
import { usePermissionsStore } from '/admin/stores/modules/user/permissions'
import MenuItem from './item.vue'
import menus from './menus.vue'
@ -106,8 +106,10 @@ export default defineComponent({
// permissions undefined
const permissions = userStore.getPermissions === undefined ? [] : userStore.getPermissions
const vnodes = getVNodes(filterMenus(permissionsStore.getMenusFrom(permissions)), props.subMenuClass)
console.log(permissionsStore.getMenusFrom(permissions))
console.log(filterMenus(permissionsStore.getMenusFrom(permissions)))
const vnodes = getVNodes(filterMenus(permissionsStore.getMenusFrom(permissions)), props.subMenuClass)
return () => {
return h(
menus,

View File

@ -1,9 +1,16 @@
import { RouteRecordRaw } from 'vue-router'
// @ts-ignore
const modules = import.meta.glob('@/module/**/views/router.ts', { eager: true })
let moduleRoutes: RouteRecordRaw[] = []
export function getModuleRoutes() {
const modules = import.meta.glob('@/module/**/views/router.ts', { eager: true })
let moduleRoutes: RouteRecordRaw[] = []
Object.keys(modules).forEach(routePath => {
moduleRoutes = moduleRoutes.concat(modules[routePath].default)
})
export default moduleRoutes
Object.keys(modules).forEach(routePath => {
moduleRoutes = moduleRoutes.concat(modules[routePath].default)
})
return moduleRoutes
}
export function getModuleViewComponents() {
return import.meta.glob(['@/module/**/views/**/*.vue', '@/module/!User/views/**/*.vue', '@/module/!Develop/views/**/*.vue', '@/module/!Options/views/**/*.vue'])
}

View File

@ -5,6 +5,7 @@ import { WhiteListPage } from '/admin/enum/app'
import { Router, RouteRecordRaw } from 'vue-router'
import { usePermissionsStore } from '/admin/stores/modules/user/permissions'
import { Menu } from '/admin/types/Menu'
import { toRaw } from 'vue'
const guard = (router: Router) => {
// white list
@ -36,7 +37,7 @@ const guard = (router: Router) => {
// 挂载路由(实际是从后端获取用户的权限)
const permissionStore = usePermissionsStore()
// 动态路由挂载
const asyncRoutes = permissionStore.getAsyncMenusFrom(userStore.getPermissions)
const asyncRoutes = permissionStore.getAsyncMenusFrom(toRaw(userStore.getPermissions))
asyncRoutes.forEach((route: Menu) => {
router.addRoute(route as unknown as RouteRecordRaw)
})

View File

@ -1,8 +1,10 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import type { App } from 'vue'
// module routers
import moduleRoutes from './constantRoutes'
import { getModuleRoutes, getModuleViewComponents } from './constantRoutes'
const moduleRoutes = getModuleRoutes()
getModuleViewComponents()
export const constantRoutes: RouteRecordRaw[] = [
{
path: '/dashboard',

View File

@ -128,7 +128,7 @@ export const useUserStore = defineStore('UserStore', {
getUserInfo() {
return new Promise((resolve, reject) => {
http
.get('/user/info')
.get('/user/online')
.then(response => {
const { id, nickname, email, avatar, permissions, roles, rememberToken, status } = response.data.data
// set user info

View File

@ -4,6 +4,8 @@ import { MenuType } from '/admin/enum/app'
import { Menu } from '/admin/types/Menu'
import { constantRoutes } from '/admin/router'
import { RouteRecordRaw } from 'vue-router'
import { toRaw } from 'vue'
import { getModuleViewComponents } from '/admin/router/constantRoutes'
interface Permissions {
menus: Menu[]
@ -68,15 +70,15 @@ export const usePermissionsStore = defineStore('PermissionsStore', {
const menus: Permission[] = []
permissions.forEach(permission => {
if (permission.type === MenuType.PAGE_TYPE) {
if (permission.type === MenuType.PAGE_TYPE || permission.type === MenuType.TOP_TYPE) {
menus.push(permission)
}
// set map
this.menuPathMap.set(permission.route, permission.title)
this.menuPathMap.set(permission.route, permission.permission_name)
})
this.setAsyncMenus(this.getAsnycMenus(menus))
this.setAsyncMenus(this.getAsnycMenus(menus, 0, '', getModuleViewComponents()))
return this.asyncMenus
},
@ -94,7 +96,7 @@ export const usePermissionsStore = defineStore('PermissionsStore', {
}
const asyncMenus = this.getAsyncMenusFrom(permissions, force)
this.setMenus(asyncMenus)
this.setMenus(toRaw(asyncMenus))
return this.menus
},
@ -118,23 +120,31 @@ export const usePermissionsStore = defineStore('PermissionsStore', {
* @param permissions
* @param parentId
* @param path
* @param viewComponents
* @returns
*/
getAsnycMenus(permissions: Permission[], parentId: number = 0, path: string = ''): Menu[] {
getAsnycMenus(permissions: Permission[], parentId: number = 0, path: string = '', viewComponents: any): Menu[] {
const menus: Menu[] = []
console.log(viewComponents)
permissions.forEach(permission => {
if (permission.parent_id === parentId) {
// menu
let importComponent
if (permission.type === MenuType.TOP_TYPE) {
importComponent = () => import(permission.component)
} else {
importComponent = viewComponents['/modules' + permission.component]
}
const menu: Menu = Object.assign({
path: this.resoulveRoutePath(permission.route, path),
name: permission.module + '_' + permission.permission_mark,
component: importComponent,
redirect: permission.redirect,
meta: Object.assign({ title: permission.title, icon: permission.icon, hidden: permission.hidden, is_inner: permission.is_inner }),
meta: Object.assign({ title: permission.permission_name, icon: permission.icon, hidden: permission.hidden, is_inner: permission.is_inner }),
})
// child menu
const children = this.getAsnycMenus(permissions, permission.id, menu.path)
const children = this.getAsnycMenus(permissions, permission.id, menu.path, viewComponents)
if (children.length > 0) {
menu.children = children
}

View File

@ -3,7 +3,7 @@ export interface Permission {
parent_id: number
title: string
permission_name: string
type: number

0
text.txt Normal file
View File