Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8f65a2bf99 | ||
![]() |
5ab1b77e0b | ||
![]() |
d787a6ad95 | ||
![]() |
fc5312692f | ||
![]() |
86bfcea374 | ||
![]() |
0218207848 | ||
![]() |
984c00fe6f | ||
![]() |
c03213e7c3 | ||
![]() |
3867be14b2 | ||
![]() |
08c3cc9a78 | ||
![]() |
af7ac14d5d | ||
![]() |
6ff2204368 | ||
![]() |
a3c3948bc6 | ||
![]() |
c1aa54bbce | ||
![]() |
b53c617984 | ||
![]() |
592cae1e2f | ||
![]() |
d24036d9f1 | ||
![]() |
e18b30e89a | ||
![]() |
bfe3221795 | ||
![]() |
f354da3878 | ||
![]() |
3ac356a83f | ||
![]() |
d1189f9aea | ||
![]() |
c1e4275399 | ||
![]() |
61aea21d5f | ||
![]() |
e96267ecc0 | ||
![]() |
c9c1ffa82b | ||
![]() |
2a134b7e3b | ||
![]() |
fc2dc38223 | ||
![]() |
8a4fd7f66f | ||
![]() |
a8c01de529 | ||
![]() |
027535cd68 | ||
![]() |
e6eb130ac4 | ||
![]() |
0d387175e0 | ||
![]() |
c25da3cfb2 | ||
![]() |
5fe198a2b2 | ||
![]() |
dad0bf4444 | ||
![]() |
72c68507e5 | ||
![]() |
1d694a81c5 | ||
![]() |
51be5c648b | ||
![]() |
bc59731083 | ||
![]() |
eeb6fd4f41 | ||
![]() |
3c4ebb86e7 | ||
![]() |
dede7b0ba0 | ||
![]() |
d995a8ce0d | ||
![]() |
898ce1305d | ||
![]() |
7362bdd70f | ||
![]() |
4e104bfd47 | ||
![]() |
4700990507 | ||
![]() |
808dd7118d | ||
![]() |
0f2c2c644f | ||
![]() |
118fc1aaab | ||
![]() |
6de3edd4fc | ||
![]() |
de31bf23cd | ||
![]() |
817e8ea64d | ||
![]() |
8d97ff8867 | ||
![]() |
490c573e61 | ||
![]() |
4ffd3eed52 | ||
![]() |
d47ccb586f | ||
![]() |
b81b9b66c8 | ||
![]() |
c3eb2443b6 | ||
![]() |
4696461d72 | ||
![]() |
aabaf99c1b | ||
![]() |
6f28c25a30 | ||
![]() |
667f6353d5 | ||
![]() |
e69cc0e147 | ||
![]() |
e5be0ca2f8 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,5 +1,3 @@
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
@@ -14,13 +12,8 @@ nohup.out
|
||||
.php-cs-fixer.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
auth.json
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
composer.lock
|
||||
/.fleet
|
||||
/.idea
|
||||
/.vscode
|
||||
components.d.ts
|
||||
auto-imports.d.ts
|
||||
/web
|
||||
|
10
.prettierrc
10
.prettierrc
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"semi": false,
|
||||
"printWidth": 200,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"singleQuote": true,
|
||||
"arrowParens": "avoid",
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true
|
||||
}
|
28
README.md
28
README.md
@@ -1,7 +1,21 @@
|
||||
## 介绍
|
||||
### 这是 catchadmin 完全分离的版本
|
||||
`CatchAdmin`是一款基于[Laravel](https://laravel.com)和[Element Plus](https://element-plus.org)二次开发而成后台管理系统。`Laravel` 社区也有许多非常优秀的后台管理系统,例如 `Nova`, 官方出品,当然是收费的,免费的有基于 `Livewire` 的 `Filament`,还有不得不说的 `Laravel Admin`。`CatchAdmin` 还是采用传统的前后端分离策略,`Laravel` 框架仅仅作为 `Api` 输出。将管理系统模块之间的耦合降到了最低限度。每个模块之间都有独立的控制器,路由,模型,数据表。在开发上尽可能将模块之间的影响降到最低,降低了开发上的难度。基于 `CatchAdmin `可以开发 `CMS`,`CRM`,`OA` 等 等系统。也封装了很多实用的工具,提升开发体验。
|
||||
|
||||
## 前端项目
|
||||
[catchadmin-vue](https://gitee.com/catchadmin/catch-admin-vue)
|
||||
|
||||
## Laravel 入门教程
|
||||
[Laravel 免费入门教程](https://laravel-study.catchadmin.com)
|
||||
|
||||
[中文](./README.md)|[英文](./README-en.md)
|
||||
## 其他版本
|
||||
- [tp8 新版本](https://gitee.com/catchamin/catchadmin-tp)
|
||||
- [webman 高性能版本](https://gitee.com/catchamin/catchadmin-webman)
|
||||
|
||||
## 新功能
|
||||
- [动态表单](https://catchadmin.com/docs/3.0/front/catch-form)
|
||||
- [动态表格](https://catchadmin.com/docs/3.0/front/catch-table)
|
||||
|
||||
## 专业版
|
||||
[专业版本官方地址](https://license.catchadmin.com)
|
||||
@@ -12,13 +26,6 @@
|
||||
|
||||
我深信,付费后台管理系统将为您带来更多的价值和便利,帮助您提升工作效率
|
||||
|
||||
|
||||
## ⚠️Thinkphp 用户注意
|
||||
由于新版本使用 `Laravel` 开发,所以请使用 `thinkphp` 分支或者 tag2.6.2,thinkphp 版本已经非常稳定了。
|
||||
|
||||
## 为什么是 Laravel
|
||||
`V2` 版本使用`Thinkphp`,但从其社区来看,从我个人角度来看开发组的心思已经不在维护框架上,因为据观察,每一次小版本发布都会引发一些小问题,虽然不大,但给人一种不够稳定的感觉,所以思索再三,使用 `Laravel`。`Laravel` 社区非常繁荣,他们每周都会发布新版本,以及围绕`Laravel`构建的生态也非常完善,有 `Horizon` 队列管理工具, `Telescope` 调试工具,`Octane`(基于 `Swoole` 和 `RoadRunner` 提高性能)等等一系列的工具,而且都是免费的。
|
||||
|
||||
## 功能
|
||||
- [x] 用户管理 后台用户管理
|
||||
- [x] 部门管理 配置公司的部门结构,支持树形结构
|
||||
@@ -37,10 +44,7 @@
|
||||
- 加入 Q 群 `302266230` 暗号 `catchadmin`。
|
||||
- 加微信入群,新建🆕
|
||||
|
||||
<img src="wechat.png" width="300"/>
|
||||
|
||||
## 额外模块
|
||||
- [CMS 模块](https://github.com/catch-admin/cms)
|
||||
<img src="wechat.png" width="200"/>
|
||||
|
||||
## 项目地址
|
||||
- [github catchadmin](https://github.com/jaguarjack/catch-admin)
|
||||
@@ -87,7 +91,7 @@ composer cs-diff
|
||||
- [Laravel](https://laravel.com)
|
||||
- [Vue](https://cn.vuejs.org/)
|
||||
- [ElementPlus](https://element-plus.org)
|
||||
- [Docusaurus](https://docusaurus.com)
|
||||
- [VitePress](https://vitepress.dev/zh/)
|
||||
- [JetBrains](https://www.jetbrains.com/)
|
||||
|
||||
|
||||
|
@@ -11,19 +11,17 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"php": "^8.2",
|
||||
"ext-pdo": "*",
|
||||
"ext-zip": "*",
|
||||
"doctrine/dbal": "^3.4",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"laravel/framework": "^10.0",
|
||||
"laravel/tinker": "^2.8",
|
||||
"catchadmin/core": "^0.2.7"
|
||||
"laravel/framework": "^v12.8.1",
|
||||
"laravel/tinker": "^v2.10.1",
|
||||
"catchadmin/core": "^0.4.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"mockery/mockery": "^1.4.4",
|
||||
"pestphp/pest": "^1.22"
|
||||
"fakerphp/faker": "^v1.24.1",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"pestphp/pest": "^v3.7.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -54,6 +52,10 @@
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd\" \"cd web && yarn dev\" \"php artisan serve\" --names='vite,server'"
|
||||
],
|
||||
"cs-diff": "./fixer/vendor/bin/php-cs-fixer fix --dry-run --diff",
|
||||
"cs": "./fixer/vendor/bin/php-cs-fixer fix"
|
||||
},
|
||||
|
@@ -71,6 +71,9 @@ return [
|
||||
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
|
||||
// 创建 storage 对应的软连接
|
||||
public_path('uploads') => storage_path('uploads')
|
||||
],
|
||||
|
||||
];
|
||||
|
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"_comment": "This file is used to trick IntelliJ/Webstorm/PHPStorm to use the correct alias as defined in vite.config.js",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"/admin/*": ["resources/admin/*"],
|
||||
"@/module/*": ["modules/*/views/*"]
|
||||
}
|
||||
}
|
||||
}
|
Submodule modules/Cms deleted from 36e9e66e38
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Modules\Common\Repository\Options;
|
||||
|
||||
use Catch\CatchAdmin;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@@ -14,29 +13,45 @@ class Components implements OptionInterface
|
||||
protected array $components = [
|
||||
[
|
||||
'label' => 'layout',
|
||||
'value' => '/admin/layout/index.vue'
|
||||
]
|
||||
'value' => '/layout/index.vue',
|
||||
],
|
||||
];
|
||||
|
||||
public function get(): array
|
||||
{
|
||||
try {
|
||||
$viewRootPath = config('catch.views_path');
|
||||
|
||||
if ($module = request()->get('module')) {
|
||||
$components = File::glob(CatchAdmin::getModuleViewsPath($module).'*'.DIRECTORY_SEPARATOR.'*.vue');
|
||||
if (!File::exists($viewRootPath . $module . DIRECTORY_SEPARATOR)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$components = File::allFiles($viewRootPath . $module . DIRECTORY_SEPARATOR);
|
||||
|
||||
foreach ($components as $component) {
|
||||
$_component = Str::of($component)
|
||||
->replace(CatchAdmin::moduleRootPath(), '')
|
||||
// 过滤非 vue 文件
|
||||
if ($component->getExtension() !== 'vue') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$_component = Str::of($component->getPathname())
|
||||
->replace($viewRootPath, '')
|
||||
->explode(DIRECTORY_SEPARATOR);
|
||||
$_component->shift(2);
|
||||
|
||||
$_component->shift(1);
|
||||
|
||||
$this->components[] = [
|
||||
'label' => Str::of($_component->implode('/'))->replace('.vue', ''),
|
||||
|
||||
'value' => Str::of($component)->replace(CatchAdmin::moduleRootPath(), '')->prepend('/')
|
||||
'value' => Str::of($component)->replace($viewRootPath, '')->prepend('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->components;
|
||||
} catch (\Throwable $exception) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
30
modules/Common/Repository/Options/Schemas.php
Normal file
30
modules/Common/Repository/Options/Schemas.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Common\Repository\Options;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Schemas implements OptionInterface
|
||||
{
|
||||
public function get(): array
|
||||
{
|
||||
$options = [];
|
||||
|
||||
$connection = DB::connection();
|
||||
$databaseName = $connection->getDatabaseName();
|
||||
$tablePrefix = $connection->getTablePrefix();
|
||||
|
||||
foreach (Schema::getTables($databaseName) as $table) {
|
||||
$tableName = Str::of($table['name'])->remove($tablePrefix);
|
||||
|
||||
$options[] = [
|
||||
'label' => $tableName . "\t\t\t\t" . $table['comment'],
|
||||
'value' => $tableName,
|
||||
];
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
@@ -6,8 +6,8 @@ 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
|
||||
{
|
||||
@@ -48,6 +48,18 @@ class Schemas extends CatchModel
|
||||
*/
|
||||
public function storeBy(array $data): bool
|
||||
{
|
||||
// 从已有 schema 中选择
|
||||
if (isset($data['schema_name'])) {
|
||||
$columns = SchemaFacade::getColumnListing($data['schema_name']);
|
||||
|
||||
return parent::storeBy([
|
||||
'module' => $data['module'],
|
||||
'name' => $data['schema_name'],
|
||||
'columns' => implode(',', $columns),
|
||||
'is_soft_delete' => isset($columns['deleted_at']) ? Status::Enable : Status::Disable,
|
||||
]);
|
||||
}
|
||||
|
||||
$schema = $data['schema'];
|
||||
|
||||
$structures = $data['structures'];
|
||||
@@ -90,16 +102,13 @@ class Schemas extends CatchModel
|
||||
{
|
||||
$schema = parent::firstBy($id);
|
||||
|
||||
$columns = [];
|
||||
|
||||
foreach (getTableColumns($schema->name) as $columnString) {
|
||||
$column = DB::connection()->getDoctrineColumn(DB::connection()->getTablePrefix().$schema->name, $columnString);
|
||||
foreach (SchemaFacade::getColumns($schema->name) as $column) {
|
||||
$columns[] = [
|
||||
'name' => $column->getName(),
|
||||
'type' => $column->getType()->getName(),
|
||||
'nullable' => ! $column->getNotnull(),
|
||||
'default' => $column->getDefault(),
|
||||
'comment' => $column->getComment()
|
||||
'name' => $column['name'],
|
||||
'type' => $column['type_name'],
|
||||
'nullable' => $column['nullable'],
|
||||
'default' => $column['default'],
|
||||
'comment' => $column['comment'],
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -95,8 +95,10 @@ class FrontForm extends Creator
|
||||
*/
|
||||
public function getFile(): string
|
||||
{
|
||||
$path = config('catch.views_path').lcfirst($this->module).DIRECTORY_SEPARATOR;
|
||||
|
||||
// TODO: Implement getFile() method.
|
||||
return CatchAdmin::makeDir(CatchAdmin::getModuleViewsPath($this->module).Str::of($this->controller)->replace('Controller', '')->lcfirst()).DIRECTORY_SEPARATOR.'create.vue';
|
||||
return CatchAdmin::makeDir($path.Str::of($this->controller)->replace('Controller', '')->lcfirst()).DIRECTORY_SEPARATOR.'create.vue';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -114,7 +114,9 @@ class FrontTable extends Creator
|
||||
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';
|
||||
$path = config('catch.views_path').lcfirst($this->module).DIRECTORY_SEPARATOR;
|
||||
|
||||
return CatchAdmin::makeDir($path.Str::of($this->controller)->replace('Controller', '')->lcfirst()).DIRECTORY_SEPARATOR.'index.vue';
|
||||
}
|
||||
|
||||
|
||||
|
@@ -21,7 +21,7 @@ class {request} extends Request
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function message(): array
|
||||
public function messages(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
@@ -8,12 +8,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useCreate } from '/admin/composables/curd/useCreate'
|
||||
import { useShow } from '/admin/composables/curd/useShow'
|
||||
import { useCreate } from '@/composables/curd/useCreate'
|
||||
import { useShow } from '@/composables/curd/useShow'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
primary: String | Number,
|
||||
primary: [String, Number],
|
||||
api: String,
|
||||
})
|
||||
|
||||
|
@@ -28,9 +28,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted } from 'vue'
|
||||
import Create from './create.vue'
|
||||
import { useGetList } from '/admin/composables/curd/useGetList'
|
||||
import { useDestroy } from '/admin/composables/curd/useDestroy'
|
||||
import { useOpen } from '/admin/composables/curd/useOpen'
|
||||
import { useGetList } from '@/composables/curd/useGetList'
|
||||
import { useDestroy } from '@/composables/curd/useDestroy'
|
||||
import { useOpen } from '@/composables/curd/useOpen'
|
||||
|
||||
const api = '{api}'
|
||||
|
||||
|
@@ -1,109 +0,0 @@
|
||||
<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>
|
@@ -1,112 +0,0 @@
|
||||
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: [],
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
@@ -1,113 +0,0 @@
|
||||
<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(() => {
|
||||
generateStore.getStructures.forEach(struct => {
|
||||
if (struct.field === 'id' || struct.field === 'created_at' || struct.field === 'updated_at') {
|
||||
struct.form = false
|
||||
}
|
||||
|
||||
if (struct.field === 'sort') {
|
||||
struct.form_component = 'input-number'
|
||||
}
|
||||
|
||||
if (struct.field === 'status') {
|
||||
struct.form_component = 'select'
|
||||
}
|
||||
})
|
||||
|
||||
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', 'textarea', 'upload']
|
||||
</script>
|
@@ -1,10 +0,0 @@
|
||||
<template>
|
||||
<div class="bg-white">
|
||||
<CodeGen />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
import CodeGen from './components/codeGen.vue'
|
||||
</script>
|
@@ -1,84 +0,0 @@
|
||||
<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="title"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: $t('module.form.name.required'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<el-input v-model="formData.title" />
|
||||
</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 } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
primary: String | Number,
|
||||
api: String,
|
||||
})
|
||||
|
||||
const { formData, form, loading, submitForm, close } = useCreate(
|
||||
props.api,
|
||||
props.primary,
|
||||
Object.assign({
|
||||
title: '',
|
||||
path: '',
|
||||
keywords: '',
|
||||
description: '',
|
||||
dirs: {
|
||||
controllers: true,
|
||||
models: true,
|
||||
database: true,
|
||||
requests: false,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
if (props.primary) {
|
||||
useShow(props.api, props.primary, formData)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
close(() => emit('close'))
|
||||
})
|
||||
</script>
|
@@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Search :search="search" :reset="reset">
|
||||
<template v-slot:body>
|
||||
<el-form-item label="模块名称">
|
||||
<el-input v-model="query.title" name="title" clearable />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</Search>
|
||||
<div class="table-default">
|
||||
<Operate :show="open">
|
||||
<template v-slot:operate>
|
||||
<!-- header 插槽的内容放这里 -->
|
||||
<el-button type="success" class="float-right" @click="installVisible = true"><Icon name="cog-6-tooth" class="mr-1 w-4 h-4" /> 安装</el-button>
|
||||
</template>
|
||||
</Operate>
|
||||
<el-table :data="tableData" class="mt-3" v-loading="loading">
|
||||
<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">
|
||||
<el-tag type="warning">{{ scope.row.version }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="enable" label="模块状态">
|
||||
<template #default="scope">
|
||||
<Status v-model="scope.row.enable" :id="scope.row.name" :api="api" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300">
|
||||
<template #default="scope">
|
||||
<Update @click="open(scope.row.name)" />
|
||||
<Destroy @click="destroy(api, scope.row.name)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<Dialog v-model="visible" :title="title" destroy-on-close>
|
||||
<Create @close="close(reset)" :primary="id" :api="api" />
|
||||
</Dialog>
|
||||
|
||||
<!-- 安装 -->
|
||||
<Dialog v-model="installVisible" title="安装模块" destroy-on-close>
|
||||
<Install @close="closeInstall" />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import Create from './create.vue'
|
||||
import Install from './install.vue'
|
||||
import { useGetList } from '/admin/composables/curd/useGetList'
|
||||
import { useDestroy } from '/admin/composables/curd/useDestroy'
|
||||
import { useOpen } from '/admin/composables/curd/useOpen'
|
||||
|
||||
const api = 'module'
|
||||
|
||||
const { data, query, search, reset, loading } = useGetList(api)
|
||||
const { destroy, deleted } = useDestroy('确认删除吗? ⚠️将会删除模块下所有文件')
|
||||
const { open, close, title, visible, id } = useOpen()
|
||||
|
||||
const tableData = computed(() => data.value?.data)
|
||||
const installVisible = ref<boolean>(false)
|
||||
const closeInstall = () => {
|
||||
installVisible.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
search()
|
||||
|
||||
deleted(reset)
|
||||
})
|
||||
</script>
|
@@ -1,93 +0,0 @@
|
||||
<template>
|
||||
<el-form :model="formData" label-width="120px" ref="form" v-loading="loading" class="pr-4">
|
||||
<el-form-item label="安装方式" prop="type">
|
||||
<el-radio-group v-model="formData.type">
|
||||
<el-radio-button
|
||||
v-for="item in [
|
||||
{ label: '普通安装', value: 1 },
|
||||
{ label: 'ZIP 安装', value: 2 },
|
||||
]"
|
||||
:key="item.value"
|
||||
:label="item.value"
|
||||
name="type"
|
||||
>{{ item.label }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="模块名称"
|
||||
prop="title"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '模块名称必须填写',
|
||||
},
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (! /^[A-Za-z]+$/.test(value)) {
|
||||
callback('模块名称只允许大小字母组合')
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<el-select v-model="formData.title" placeholder="选择安装模块">
|
||||
<el-option v-for="item in modules" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="上传 ZIP" prop="file" v-if="formData.type === 2">
|
||||
<Upload action="module/upload" :limit="1" accept=".zip" :on-success="moduleUpload">
|
||||
<template #trigger>
|
||||
<el-button type="primary">选择模块文件</el-button>
|
||||
</template>
|
||||
</Upload>
|
||||
</el-form-item>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<el-button type="primary" @click="submitForm(form)">安装</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useCreate } from '/admin/composables/curd/useCreate'
|
||||
|
||||
import { onMounted } from 'vue'
|
||||
import { Code } from '/admin/enum/app'
|
||||
import Message from '/admin/support/message'
|
||||
|
||||
const { formData, form, loading, submitForm, close } = useCreate('module/install')
|
||||
formData.value.type = 1
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
onMounted(() => {
|
||||
close(() => emit('close'))
|
||||
})
|
||||
|
||||
const moduleUpload = (response, uploadFile) => {
|
||||
if (response.code === Code.SUCCESS) {
|
||||
formData.value.file = response.data
|
||||
} else {
|
||||
Message.error(response.message)
|
||||
}
|
||||
}
|
||||
|
||||
const modules = [
|
||||
{
|
||||
label: '权限管理',
|
||||
value: 'permissions',
|
||||
},
|
||||
{
|
||||
label: '内容管理',
|
||||
value: 'cms',
|
||||
},
|
||||
{
|
||||
label: '系统管理',
|
||||
value: 'system',
|
||||
},
|
||||
]
|
||||
</script>
|
@@ -1,32 +0,0 @@
|
||||
import { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
// @ts-ignore
|
||||
const router: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/develop',
|
||||
component: () => import('/admin/layout/index.vue'),
|
||||
meta: { title: '开发工具', icon: 'wrench-screwdriver' },
|
||||
children: [
|
||||
{
|
||||
path: 'modules',
|
||||
name: 'modules',
|
||||
meta: { title: '模块管理', icon: 'queue-list' },
|
||||
component: () => import('./module/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'schemas',
|
||||
name: 'schemas',
|
||||
meta: { title: 'Schemas', icon: 'list-bullet' },
|
||||
component: () => import('./schema/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'generate/:schema',
|
||||
name: 'generate',
|
||||
meta: { title: '代码生成', hidden: true, active_menu: '/develop/schemas' },
|
||||
component: () => import('./generate/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export default router
|
@@ -1,36 +0,0 @@
|
||||
<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>
|
@@ -1,83 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Search :search="search" :reset="reset">
|
||||
<template v-slot:body>
|
||||
<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>
|
||||
</template>
|
||||
</Search>
|
||||
<div class="table-default">
|
||||
<Operate :show="open" />
|
||||
<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="view(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>
|
||||
<Paginate />
|
||||
</div>
|
||||
|
||||
<!-- schema 创建 -->
|
||||
<Dialog v-model="visible" :title="title" width="650px" destroy-on-close>
|
||||
<Create @close="close(reset)" :api="api" />
|
||||
</Dialog>
|
||||
|
||||
<!-- schema 表结构 -->
|
||||
<Dialog v-model="schemaVisible" title="Schema 结构" width="650px" destroy-on-close>
|
||||
<Show :id="id" :api="api" />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } 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'
|
||||
import { useOpen } from '/admin/composables/curd/useOpen'
|
||||
|
||||
const schemaVisible = ref<boolean>(false)
|
||||
|
||||
const api = 'schema'
|
||||
|
||||
const { data, query, search, reset, loading } = useGetList(api)
|
||||
const { destroy, deleted } = useDestroy('确认删除吗? 删除后数据表将会保留,如需删除相关表,请手动进行删除!')
|
||||
const { open, close, title, visible, id } = useOpen()
|
||||
|
||||
const tableData = computed(() => data.value?.data)
|
||||
|
||||
const view = primaryId => {
|
||||
schemaVisible.value = true
|
||||
|
||||
id.value = primaryId
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
search()
|
||||
|
||||
deleted(reset)
|
||||
})
|
||||
</script>
|
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<el-table :data="data?.columns" 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'
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
// const data = ref<Array<object>>()
|
||||
const { data, loading } = useShow('schema', props.id)
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@@ -1,107 +0,0 @@
|
||||
<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 = ref(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>
|
@@ -1,208 +0,0 @@
|
||||
<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, 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'
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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>
|
@@ -1,146 +0,0 @@
|
||||
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
|
||||
},
|
||||
},
|
||||
})
|
@@ -28,11 +28,13 @@ class PermissionsController extends Controller
|
||||
public function index(Request $request): mixed
|
||||
{
|
||||
if ($request->get('from') == 'role') {
|
||||
return $this->model->getList();
|
||||
return $this->model->setBeforeGetList(function ($query){
|
||||
return $query->orderByDesc('sort');
|
||||
})->getList();
|
||||
}
|
||||
|
||||
return $this->model->setBeforeGetList(function ($query) {
|
||||
return $query->with('actions')->whereIn('type', [MenuType::Top->value(), MenuType::Menu->value()]);
|
||||
return $query->with('actions')->whereIn('type', [MenuType::Top->value(), MenuType::Menu->value()])->orderByDesc('sort');
|
||||
})->getList();
|
||||
}
|
||||
|
||||
|
@@ -6,7 +6,9 @@ namespace Modules\Permissions\Http\Controllers;
|
||||
|
||||
use Catch\Base\CatchController as Controller;
|
||||
use Catch\Exceptions\FailedException;
|
||||
use Catch\Support\ResponseBuilder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Foundation\Exceptions\ReportableHandler;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\Permissions\Enums\DataRange;
|
||||
use Modules\Permissions\Models\Roles;
|
||||
|
@@ -7,6 +7,7 @@ namespace Modules\Permissions\Models;
|
||||
use Catch\Base\CatchModel as Model;
|
||||
use Catch\CatchAdmin;
|
||||
use Catch\Enums\Status;
|
||||
use Catch\Exceptions\FailedException;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -189,6 +190,10 @@ class Permissions extends Model
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($data['type'] != MenuType::Top->value() && ! $data['parent_id']) {
|
||||
throw new FailedException('请选择父级菜单');
|
||||
}
|
||||
|
||||
$model = $this->fill($data);
|
||||
|
||||
if ($model->isAction()) {
|
||||
@@ -240,6 +245,10 @@ class Permissions extends Model
|
||||
*/
|
||||
public function updateBy($id, array $data): mixed
|
||||
{
|
||||
if ($data['type'] != MenuType::Top->value() && ! $data['parent_id']) {
|
||||
throw new FailedException('请选择父级菜单');
|
||||
}
|
||||
|
||||
$model = $this->fill($data);
|
||||
|
||||
if ($model->isAction()) {
|
||||
|
@@ -30,7 +30,7 @@ return new class extends Seeder
|
||||
'icon' => 'arrow-down-on-square-stack',
|
||||
'module' => 'permissions',
|
||||
'permission_mark' => '',
|
||||
'component' => '/admin/layout/index.vue',
|
||||
'component' => '/layout/index.vue',
|
||||
'redirect' => NULL,
|
||||
'keepalive' => 1,
|
||||
'type' => 1,
|
||||
@@ -51,7 +51,7 @@ return new class extends Seeder
|
||||
'icon' => 'arrow-left-circle',
|
||||
'module' => 'permissions',
|
||||
'permission_mark' => 'Roles',
|
||||
'component' => '/Permissions/views/roles/index.vue',
|
||||
'component' => '/permissions/roles/index.vue',
|
||||
'redirect' => NULL,
|
||||
'keepalive' => 1,
|
||||
'type' => 2,
|
||||
@@ -174,7 +174,7 @@ return new class extends Seeder
|
||||
'icon' => 'finger-print',
|
||||
'module' => 'permissions',
|
||||
'permission_mark' => 'Permissions',
|
||||
'component' => '/Permissions/views/permissions/index.vue',
|
||||
'component' => '/permissions/permissions/index.vue',
|
||||
'redirect' => NULL,
|
||||
'keepalive' => 1,
|
||||
'type' => 2,
|
||||
@@ -317,7 +317,7 @@ return new class extends Seeder
|
||||
'icon' => 'globe-americas',
|
||||
'module' => 'permissions',
|
||||
'permission_mark' => 'Jobs',
|
||||
'component' => '/Permissions/views/jobs/index.vue',
|
||||
'component' => '/permissions/jobs/index.vue',
|
||||
'redirect' => NULL,
|
||||
'keepalive' => 1,
|
||||
'type' => 2,
|
||||
@@ -460,7 +460,7 @@ return new class extends Seeder
|
||||
'icon' => 'table-cells',
|
||||
'module' => 'permissions',
|
||||
'permission_mark' => 'Departments',
|
||||
'component' => '/Permissions/views/departments/index.vue',
|
||||
'component' => '/permissions/departments/index.vue',
|
||||
'redirect' => NULL,
|
||||
'keepalive' => 1,
|
||||
'type' => 2,
|
||||
@@ -594,6 +594,178 @@ return new class extends Seeder
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
4 =>
|
||||
array (
|
||||
'id' => 223,
|
||||
'parent_id' => 1,
|
||||
'permission_name' => '用户管理',
|
||||
'route' => 'user',
|
||||
'icon' => 'users',
|
||||
'module' => 'user',
|
||||
'permission_mark' => 'user',
|
||||
'component' => '/user/index.vue',
|
||||
'redirect' => NULL,
|
||||
'keepalive' => 1,
|
||||
'type' => 2,
|
||||
'hidden' => 1,
|
||||
'sort' => 1,
|
||||
'active_menu' => '',
|
||||
'creator_id' => 1,
|
||||
'created_at' => 1709342019,
|
||||
'updated_at' => 1709342019,
|
||||
'deleted_at' => 0,
|
||||
'children' =>
|
||||
array (
|
||||
0 =>
|
||||
array (
|
||||
'id' => 224,
|
||||
'parent_id' => 223,
|
||||
'permission_name' => '列表',
|
||||
'route' => '',
|
||||
'icon' => '',
|
||||
'module' => 'user',
|
||||
'permission_mark' => 'user@index',
|
||||
'component' => '',
|
||||
'redirect' => '',
|
||||
'keepalive' => 1,
|
||||
'type' => 3,
|
||||
'hidden' => 1,
|
||||
'sort' => 1,
|
||||
'active_menu' => '',
|
||||
'creator_id' => 1,
|
||||
'created_at' => 1709373354,
|
||||
'updated_at' => 1709373354,
|
||||
'deleted_at' => 0,
|
||||
),
|
||||
1 =>
|
||||
array (
|
||||
'id' => 225,
|
||||
'parent_id' => 223,
|
||||
'permission_name' => '新增',
|
||||
'route' => '',
|
||||
'icon' => '',
|
||||
'module' => 'user',
|
||||
'permission_mark' => 'user@store',
|
||||
'component' => '',
|
||||
'redirect' => '',
|
||||
'keepalive' => 1,
|
||||
'type' => 3,
|
||||
'hidden' => 1,
|
||||
'sort' => 2,
|
||||
'active_menu' => '',
|
||||
'creator_id' => 1,
|
||||
'created_at' => 1709373354,
|
||||
'updated_at' => 1709373354,
|
||||
'deleted_at' => 0,
|
||||
),
|
||||
2 =>
|
||||
array (
|
||||
'id' => 226,
|
||||
'parent_id' => 223,
|
||||
'permission_name' => '读取',
|
||||
'route' => '',
|
||||
'icon' => '',
|
||||
'module' => 'user',
|
||||
'permission_mark' => 'user@show',
|
||||
'component' => '',
|
||||
'redirect' => '',
|
||||
'keepalive' => 1,
|
||||
'type' => 3,
|
||||
'hidden' => 1,
|
||||
'sort' => 3,
|
||||
'active_menu' => '',
|
||||
'creator_id' => 1,
|
||||
'created_at' => 1709373354,
|
||||
'updated_at' => 1709373354,
|
||||
'deleted_at' => 0,
|
||||
),
|
||||
3 =>
|
||||
array (
|
||||
'id' => 227,
|
||||
'parent_id' => 223,
|
||||
'permission_name' => '更新',
|
||||
'route' => '',
|
||||
'icon' => '',
|
||||
'module' => 'user',
|
||||
'permission_mark' => 'user@update',
|
||||
'component' => '',
|
||||
'redirect' => '',
|
||||
'keepalive' => 1,
|
||||
'type' => 3,
|
||||
'hidden' => 1,
|
||||
'sort' => 4,
|
||||
'active_menu' => '',
|
||||
'creator_id' => 1,
|
||||
'created_at' => 1709373354,
|
||||
'updated_at' => 1709373354,
|
||||
'deleted_at' => 0,
|
||||
),
|
||||
4 =>
|
||||
array (
|
||||
'id' => 228,
|
||||
'parent_id' => 223,
|
||||
'permission_name' => '删除',
|
||||
'route' => '',
|
||||
'icon' => '',
|
||||
'module' => 'user',
|
||||
'permission_mark' => 'user@destroy',
|
||||
'component' => '',
|
||||
'redirect' => '',
|
||||
'keepalive' => 1,
|
||||
'type' => 3,
|
||||
'hidden' => 1,
|
||||
'sort' => 5,
|
||||
'active_menu' => '',
|
||||
'creator_id' => 1,
|
||||
'created_at' => 1709373354,
|
||||
'updated_at' => 1709373354,
|
||||
'deleted_at' => 0,
|
||||
),
|
||||
5 =>
|
||||
array (
|
||||
'id' => 229,
|
||||
'parent_id' => 223,
|
||||
'permission_name' => '禁用/启用',
|
||||
'route' => '',
|
||||
'icon' => '',
|
||||
'module' => 'user',
|
||||
'permission_mark' => 'user@enable',
|
||||
'component' => '',
|
||||
'redirect' => '',
|
||||
'keepalive' => 1,
|
||||
'type' => 3,
|
||||
'hidden' => 1,
|
||||
'sort' => 6,
|
||||
'active_menu' => '',
|
||||
'creator_id' => 1,
|
||||
'created_at' => 1709373354,
|
||||
'updated_at' => 1709373354,
|
||||
'deleted_at' => 0,
|
||||
),
|
||||
6 =>
|
||||
array (
|
||||
'id' => 230,
|
||||
'parent_id' => 223,
|
||||
'permission_name' => '导出',
|
||||
'route' => '',
|
||||
'icon' => '',
|
||||
'module' => 'user',
|
||||
'permission_mark' => 'user@export',
|
||||
'component' => '',
|
||||
'redirect' => '',
|
||||
'keepalive' => 1,
|
||||
'type' => 3,
|
||||
'hidden' => 1,
|
||||
'sort' => 10,
|
||||
'active_menu' => '',
|
||||
'creator_id' => 1,
|
||||
'created_at' => 1709373354,
|
||||
'updated_at' => 1709373354,
|
||||
'deleted_at' => 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
BIN
modules/Permissions/views/.DS_Store
vendored
BIN
modules/Permissions/views/.DS_Store
vendored
Binary file not shown.
@@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<el-form :model="formData" label-width="120px" ref="form" v-loading="loading" class="pr-4">
|
||||
<el-form-item label="父级部门" prop="parent_id">
|
||||
<el-cascader :options="departments" name="parent_id" v-model="formData.parent_id" clearable :props="{ value: 'id', label: 'department_name', checkStrictly: true }" class="w-full" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门名称" prop="department_name" :rules="[{ required: true, message: '部门名称必须填写' }]">
|
||||
<el-input v-model="formData.department_name" name="department_name" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门联系人" prop="principal">
|
||||
<el-input v-model="formData.principal" name="principal" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="mobile">
|
||||
<el-input v-model="formData.mobile" name="mobile" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="formData.email" name="email" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" name="sort" :min="1" />
|
||||
</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 http from '/admin/support/http'
|
||||
import { onMounted, ref, unref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
primary: String | Number,
|
||||
api: String,
|
||||
})
|
||||
|
||||
const { formData, form, loading, submitForm, close, beforeCreate, beforeUpdate } = useCreate(props.api, props.primary)
|
||||
formData.value.sort = 1
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
if (props.primary) {
|
||||
const { afterShow } = useShow(props.api, props.primary, formData)
|
||||
afterShow.value = formData => {
|
||||
const data = unref(formData)
|
||||
data.parent_id = data.parent_id ? [data.parent_id] : 0
|
||||
|
||||
if (!data.data_range) {
|
||||
data.data_range = null
|
||||
}
|
||||
|
||||
formData.value = data
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const departments = ref()
|
||||
onMounted(() => {
|
||||
http.get(props.api).then(r => {
|
||||
departments.value = r.data.data
|
||||
})
|
||||
|
||||
close(() => {
|
||||
emit('close')
|
||||
})
|
||||
})
|
||||
</script>
|
@@ -1,56 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Search :search="search" :reset="reset">
|
||||
<template v-slot:body>
|
||||
<el-form-item label="部门名称" prop="department_name">
|
||||
<el-input v-model="query.department_name" name="department_name" clearable />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</Search>
|
||||
<div class="table-default">
|
||||
<Operate :show="open" />
|
||||
<el-table :data="tableData" class="mt-3" v-loading="loading" row-key="id" default-expand-all :tree-props="{ children: 'children' }">
|
||||
<el-table-column prop="department_name" label="部门名称" />
|
||||
<el-table-column prop="sort" label="排序" />
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template #default="scope">
|
||||
<Status v-model="scope.row.status" :id="scope.row.id" :api="api" @refresh="search" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<Update @click="open(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(reset)" :primary="id" :api="api" />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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'
|
||||
|
||||
const api = 'permissions/departments'
|
||||
|
||||
const { data, query, search, reset, loading } = useGetList(api, false)
|
||||
const { destroy, deleted } = useDestroy()
|
||||
const { open, close, title, visible, id } = useOpen()
|
||||
|
||||
const tableData = computed(() => data.value?.data)
|
||||
|
||||
onMounted(() => {
|
||||
search()
|
||||
|
||||
deleted(reset)
|
||||
})
|
||||
</script>
|
@@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<el-form :model="formData" label-width="120px" ref="form" v-loading="loading" class="pr-4">
|
||||
<el-form-item label="岗位名称" prop="job_name" :rules="[{ required: true, message: '岗位名称必须填写' }]">
|
||||
<el-input v-model="formData.job_name" name="job_name" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位编码" prop="coding">
|
||||
<el-input v-model="formData.coding" name="coding" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio v-for="item in options" :key="item.value" :label="item.value" name="status">{{ item.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</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="岗位描述" prop="description">
|
||||
<el-input v-model="formData.description" name="description" clearable type="textarea" />
|
||||
</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 } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
primary: String | Number,
|
||||
api: String,
|
||||
})
|
||||
|
||||
const { formData, form, loading, submitForm, close } = useCreate(props.api, props.primary)
|
||||
|
||||
formData.value.status = 1
|
||||
formData.value.sort = 1
|
||||
|
||||
if (props.primary) {
|
||||
useShow(props.api, props.primary, formData)
|
||||
}
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
onMounted(() => {
|
||||
close(() => emit('close'))
|
||||
})
|
||||
|
||||
const options = [
|
||||
{ label: '正常', value: 1 },
|
||||
{ label: '禁用', value: 2 },
|
||||
]
|
||||
</script>
|
@@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Search :search="search" :reset="reset">
|
||||
<template v-slot:body>
|
||||
<el-form-item label="岗位名称" prop="job_name">
|
||||
<el-input v-model="query.job_name" name="job_name" clearable />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</Search>
|
||||
<div class="table-default">
|
||||
<Operate :show="open" />
|
||||
<el-table :data="tableData" class="mt-3" v-loading="loading">
|
||||
<el-table-column prop="job_name" label="岗位名称" />
|
||||
<el-table-column prop="coding" 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="sort" label="排序" />
|
||||
<el-table-column prop="description" label="岗位描述" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<Update @click="open(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(reset)" :primary="id" :api="api" />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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'
|
||||
|
||||
const api = 'permissions/jobs'
|
||||
|
||||
const { data, query, search, reset, loading } = useGetList(api)
|
||||
const { destroy, deleted } = useDestroy()
|
||||
|
||||
const tableData = computed(() => data.value?.data)
|
||||
const { open, close, title, visible, id } = useOpen()
|
||||
|
||||
onMounted(() => {
|
||||
search()
|
||||
|
||||
deleted(reset)
|
||||
})
|
||||
</script>
|
@@ -1,212 +0,0 @@
|
||||
<template>
|
||||
<el-form :model="formData" label-width="85px" ref="form" v-loading="loading" class="pr-4">
|
||||
<div class="flex flex-row justify-between">
|
||||
<div>
|
||||
<el-form-item label="菜单类型" prop="type">
|
||||
<el-radio-group v-model="formData.type">
|
||||
<el-radio-button
|
||||
v-for="item in [
|
||||
{ label: '目录', value: 1 },
|
||||
{ label: '菜单', value: 2 },
|
||||
{ label: '按钮', value: 3 },
|
||||
]"
|
||||
:key="item.value"
|
||||
:label="item.value"
|
||||
name="type"
|
||||
>{{ item.label }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单名称" prop="permission_name" :rules="[{ required: true, message: '菜单名称必须填写' }]">
|
||||
<el-input v-model="formData.permission_name" name="permission_name" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属模块" prop="module" :rules="[{ required: true, message: '所属模块必须填写' }]" v-if="!isAction">
|
||||
<Select v-model="formData.module" api="modules" allow-create @clear="clearModule" />
|
||||
</el-form-item>
|
||||
<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" v-if="!isAction">
|
||||
<el-input v-model="formData.redirect" name="redirect" clearable />
|
||||
</el-form-item>
|
||||
<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">
|
||||
<el-input v-model="formData.permission_mark" name="permission_mark" clearable v-if="isAction" />
|
||||
<Select v-model="formData.permission_mark" allow-create placeholder="请选择" api="controllers" :query="{ module: formData.module }" v-else />
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单Icon" prop="icon" v-if="!isAction">
|
||||
<el-popover placement="right" :width="400" trigger="click">
|
||||
<template #reference>
|
||||
<el-input v-model="formData.icon" name="icon" clearable />
|
||||
</template>
|
||||
<div>
|
||||
<Icons v-model="formData.icon" @close="closeSelectIcon" />
|
||||
</div>
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属组件" prop="component" 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 [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 2 },
|
||||
]"
|
||||
:key="item.value"
|
||||
:label="item.value"
|
||||
name="keepalive"
|
||||
>{{ item.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-form-item label="激活菜单" prop="active_menu" v-if="isMenu">
|
||||
<div class="w-full flex flex-row">
|
||||
<el-input v-model="formData.active_menu" name="active_menu" clearable class="w-3/4" />
|
||||
<el-tooltip effect="dark" :content="activeMenuIntro" raw-content placement="top">
|
||||
<div class="text-red-500 cursor-pointer w-1/4 ml-2 justify-center flex">说明</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<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 { useOpen } from '/admin/composables/curd/useOpen'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import http from '/admin/support/http'
|
||||
import { MenuType } from '/admin/enum/app'
|
||||
|
||||
const props = defineProps({
|
||||
primary: String | Number,
|
||||
api: String,
|
||||
})
|
||||
|
||||
const activeMenuIntro =
|
||||
'<div>如果是访问内页的菜单路由,例如创建文章 create/post, 虽然它隶属于文章列表,但实际上并不会嵌套在文章列表路由里</div><div>而是单独的一个路由,并且是不显示在左侧菜单的。所以在访问它的时候,需要左侧菜单高亮,则需要设置该参数</div>'
|
||||
|
||||
const { formData, form, loading, submitForm, close, beforeCreate, beforeUpdate } = useCreate(props.api, props.primary)
|
||||
|
||||
// 选择 icon
|
||||
const { open, visible } = useOpen()
|
||||
// 关闭选择 icon
|
||||
const closeSelectIcon = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 默认值
|
||||
const defaultSort = 1
|
||||
const defaultKeepalive = 1
|
||||
const defaultHidden = 1
|
||||
|
||||
// 初始化
|
||||
formData.value.sort = defaultSort
|
||||
formData.value.keepalive = defaultKeepalive
|
||||
formData.value.type = MenuType.TOP_TYPE
|
||||
formData.value.hidden = defaultHidden
|
||||
|
||||
// 默认目录
|
||||
const isTop = ref<boolean>(true)
|
||||
const isMenu = ref<boolean>(false)
|
||||
const isAction = ref<boolean>(false)
|
||||
|
||||
// 回显示表单
|
||||
if (props.primary) {
|
||||
const { afterShow } = useShow(props.api, props.primary, formData)
|
||||
|
||||
afterShow.value = formData => {
|
||||
if (formData.value.permission_mark.indexOf('@') !== -1) {
|
||||
formData.value.permission_mark = formData.value.permission_mark.split('@')[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
const permissions = ref()
|
||||
onMounted(() => {
|
||||
http.get(props.api).then(r => {
|
||||
permissions.value = r.data.data
|
||||
})
|
||||
|
||||
close(() => emit('close'))
|
||||
|
||||
// 监听 form data
|
||||
watch(
|
||||
formData,
|
||||
() => {
|
||||
const type: number = formData.value.type
|
||||
if (type === MenuType.TOP_TYPE) {
|
||||
isTop.value = true
|
||||
isMenu.value = isAction.value = false
|
||||
} else if (type === MenuType.PAGE_TYPE) {
|
||||
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 === MenuType.TOP_TYPE || formData.value.type === MenuType.PAGE_TYPE) {
|
||||
formData.value.component = null
|
||||
}
|
||||
if (formData.value.type === MenuType.PAGE_TYPE) {
|
||||
formData.value.permission_mark = null
|
||||
}
|
||||
}
|
||||
|
||||
// 创建前的钩子
|
||||
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) => {
|
||||
if (typeof parentId === 'number') {
|
||||
return parentId
|
||||
}
|
||||
|
||||
return typeof parentId === 'undefined' ? 0 : parentId[parentId.length - 1]
|
||||
}
|
||||
</script>
|
@@ -1,90 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Search :search="search" :reset="reset">
|
||||
<template v-slot:body>
|
||||
<el-form-item label="菜单名称" prop="permission_name">
|
||||
<el-input v-model="query.permission_name" name="permission_name" clearable />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</Search>
|
||||
<div class="table-default">
|
||||
<Operate :show="open" />
|
||||
<el-table :data="tableData" class="mt-3" v-loading="loading" row-key="id" default-expand-all :tree-props="{ children: 'children' }">
|
||||
<el-table-column prop="permission_name" label="菜单名称" />
|
||||
<el-table-column prop="route" label="菜单路由" />
|
||||
<el-table-column prop="permission_mark" label="权限标识" width="330">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.actions.length" class="flex grid gap-1 grid-cols-4">
|
||||
<el-tag v-for="action in scope.row.actions" class="cursor-pointer min-w-fit" @click="open(action.id)" closable @close="destroy(api, action.id)">{{ action.permission_name }}</el-tag>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-popconfirm confirm-button-text="确认" title="添加基础actions" @confirm="actionGenerate(scope.row.id)" placement="top">
|
||||
<template #reference>
|
||||
<el-tag class="cursor-pointer w-8" v-if="scope.row.type === MenuType.PAGE_TYPE">
|
||||
<Icon name="cog-6-tooth" class="animate-spin w-5 h-5" v-if="generateId === scope.row.id" />
|
||||
<Icon name="plus" className="w-4 h-4" v-else />
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="hidden" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<Status v-model="scope.row.hidden" :id="scope.row.id" :api="api" @refresh="search" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<Update @click="open(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(reset)" :primary="id" :api="api" />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } 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'
|
||||
import { MenuType } from '/admin/enum/app'
|
||||
import http from '../../../../resources/admin/support/http'
|
||||
|
||||
const api = 'permissions/permissions'
|
||||
|
||||
const { data, query, search, reset, loading } = useGetList(api, false)
|
||||
const { destroy, deleted } = useDestroy()
|
||||
const { open, close, title, visible, id } = useOpen()
|
||||
|
||||
const tableData = computed(() => data.value?.data)
|
||||
|
||||
onMounted(() => {
|
||||
search()
|
||||
deleted(reset)
|
||||
})
|
||||
|
||||
const actionLoading = ref<boolean>(false)
|
||||
const generateId = ref<number>(0)
|
||||
const actionGenerate = async (id: number) => {
|
||||
generateId.value = id
|
||||
http
|
||||
.post(api, { parent_id: id, actions: true })
|
||||
.then(r => {
|
||||
search()
|
||||
generateId.value = 0
|
||||
})
|
||||
.catch(e => {
|
||||
generateId.value = 0
|
||||
catchtable.value.reset()
|
||||
})
|
||||
}
|
||||
</script>
|
@@ -1,229 +0,0 @@
|
||||
<template>
|
||||
<el-form :model="formData" label-width="120px" ref="form" v-loading="loading" class="pr-6">
|
||||
<el-form-item label="上级角色" prop="parent_id" v-if="!primary">
|
||||
<el-cascader
|
||||
:options="roles"
|
||||
name="parent_id"
|
||||
v-model="formData.parent_id"
|
||||
clearable
|
||||
check-strictly
|
||||
class="w-full"
|
||||
@change="getPermissions"
|
||||
:props="{ value: 'id', label: 'role_name', checkStrictly: true }"
|
||||
/>
|
||||
</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>
|
||||
<el-form-item
|
||||
label="自定义权限"
|
||||
prop="departments"
|
||||
v-if="showDepartments"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '自定义权限必须选择',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<el-tree-select
|
||||
v-model="formData.departments"
|
||||
value-key="id"
|
||||
class="w-full"
|
||||
:data="departments"
|
||||
:render-after-expand="false"
|
||||
show-checkbox
|
||||
multiple
|
||||
:props="{ value: 'id', label: 'department_name' }"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色权限" prop="permissions">
|
||||
<div class="h-40 overflow-auto w-full border border-gray-300 rounded pt-2 pl-2">
|
||||
<el-tree
|
||||
ref="permissionTree"
|
||||
v-model="formData.permissions"
|
||||
:data="permissions"
|
||||
node-key="id"
|
||||
class="w-full"
|
||||
:props="{ label: 'permission_name', value: 'id' }"
|
||||
show-checkbox
|
||||
:default-expand-all="false"
|
||||
@check="selectPermissions"
|
||||
:empty-text="permissionLoadingText"
|
||||
/>
|
||||
</div>
|
||||
</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 { nextTick, onMounted, ref, unref, watch } from 'vue'
|
||||
import http from '/admin/support/http'
|
||||
|
||||
const props = defineProps({
|
||||
primary: String | Number,
|
||||
api: String,
|
||||
hasPermissions: Array<Object>,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const { formData, form, loading, submitForm, close, beforeCreate, beforeUpdate } = useCreate(props.api, props.primary)
|
||||
|
||||
if (props.primary) {
|
||||
const { afterShow } = useShow(props.api, props.primary, formData)
|
||||
// 更新角色值
|
||||
afterShow.value = formData => {
|
||||
const data = unref(formData)
|
||||
data.parent_id = data.parent_id ? [data.parent_id] : 0
|
||||
|
||||
if (!data.data_range) {
|
||||
data.data_range = null
|
||||
}
|
||||
|
||||
formData.value = data
|
||||
// 这里需要获取角色的上级的权限以限制可用权限范围
|
||||
getPermissions(data.parent_id)
|
||||
}
|
||||
}
|
||||
|
||||
const roles = ref()
|
||||
const permissions = ref()
|
||||
// 权限树对象
|
||||
const permissionTree = ref()
|
||||
// 部门
|
||||
const departments = ref()
|
||||
const showDepartments = ref<boolean>(false)
|
||||
|
||||
const permissionLoadingText = ref<string>('加载中...')
|
||||
|
||||
// 获取权限
|
||||
const getPermissions = async (value: number = 0) => {
|
||||
if (value) {
|
||||
// 获取角色权限
|
||||
http.get('permissions/roles/' + getParent(value), { from: 'parent_role' }).then(r => {
|
||||
permissions.value = r.data.data.permissions
|
||||
setCheckedPermissions()
|
||||
})
|
||||
} else {
|
||||
http.get('permissions/permissions', { from: 'role' }).then(r => {
|
||||
permissions.value = r.data.data
|
||||
setCheckedPermissions()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 设置已选权限
|
||||
const setCheckedPermissions = () => {
|
||||
nextTick(() => {
|
||||
props.hasPermissions.forEach(p => {
|
||||
permissionTree.value.setChecked(p.id, true, false)
|
||||
})
|
||||
})
|
||||
|
||||
if (!permissions.value.length) {
|
||||
permissionLoadingText.value = '暂无数据'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色信息
|
||||
const getRoles = () => {
|
||||
http.get(props.api, { id: props.primary ? props.primary : '' }).then(r => {
|
||||
roles.value = r.data.data
|
||||
})
|
||||
}
|
||||
|
||||
const getDepartments = () => {
|
||||
http.get('permissions/departments').then(r => {
|
||||
departments.value = r.data.data
|
||||
})
|
||||
}
|
||||
|
||||
// 新增默认获取全部权限
|
||||
if (!props.primary) {
|
||||
getPermissions()
|
||||
}
|
||||
|
||||
// 页面挂载完成后
|
||||
onMounted(() => {
|
||||
getRoles()
|
||||
getDepartments()
|
||||
close(() => emit('close'))
|
||||
watch(
|
||||
formData,
|
||||
function (value) {
|
||||
// 如果数据权限是自定义数据
|
||||
showDepartments.value = value.data_range === 2
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
})
|
||||
|
||||
const selectPermissions = (checkedNodes, checkedKeys) => {
|
||||
formData.value.permissions = checkedKeys.checkedKeys.concat(checkedKeys.halfCheckedKeys).sort()
|
||||
}
|
||||
|
||||
// 创建前的钩子
|
||||
beforeCreate.value = () => {
|
||||
formData.value.parent_id = getParent(formData.value.parent_id)
|
||||
}
|
||||
|
||||
// 更新前的钩子
|
||||
beforeUpdate.value = () => {
|
||||
const permissionIds = []
|
||||
formData.value.permissions.forEach(item => {
|
||||
permissionIds.push(item)
|
||||
})
|
||||
|
||||
formData.value.permissions = permissionIds
|
||||
formData.value.parent_id = getParent(formData.value.parent_id)
|
||||
}
|
||||
|
||||
const getParent = (parentId: any) => {
|
||||
return typeof parentId === 'undefined' ? 0 : parentId[parentId.length - 1]
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
:deep(.el-tree .el-tree__empty-block .el-tree__empty-text) {
|
||||
@apply left-10 top-4;
|
||||
}
|
||||
:deep(.el-tree-node .is-expanded .el-tree-node__children) {
|
||||
@apply flex flex-wrap pl-9;
|
||||
}
|
||||
:deep(.el-tree-node .is-expanded .el-tree-node__children .el-tree-node__content) {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
</style>
|
@@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Search :search="search" :reset="reset">
|
||||
<template v-slot:body>
|
||||
<el-form-item label="角色名称" prop="role_name">
|
||||
<el-input v-model="query.role_name" name="role_name" clearable />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</Search>
|
||||
<div class="table-default">
|
||||
<div class="pt-5 pl-2">
|
||||
<Add @click="openRoleForm(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="openRoleForm(scope.row.id, scope.row.permissions)" />
|
||||
<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(reset)" :primary="id" :api="api" :has-permissions="rolePermissions" />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } 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'
|
||||
|
||||
const api = 'permissions/roles'
|
||||
|
||||
const { data, query, search, reset, loading } = useGetList(api, false)
|
||||
const { destroy, deleted } = useDestroy()
|
||||
const { open, close, title, visible, id } = useOpen()
|
||||
|
||||
const tableData = computed(() => data.value?.data)
|
||||
|
||||
const rolePermissions = ref<Array<number>>([])
|
||||
const openRoleForm = (id, permissions) => {
|
||||
rolePermissions.value = permissions
|
||||
open(id)
|
||||
}
|
||||
onMounted(() => {
|
||||
search()
|
||||
|
||||
deleted(reset)
|
||||
})
|
||||
</script>
|
@@ -50,7 +50,7 @@ return new class extends Seeder
|
||||
'icon' => '',
|
||||
'module' => 'system',
|
||||
'permission_mark' => 'dictionary',
|
||||
'component' => '/System/views/dictionary/index.vue',
|
||||
'component' => '/system/dictionary/index.vue',
|
||||
'redirect' => '',
|
||||
'keepalive' => 1,
|
||||
'type' => 2,
|
||||
@@ -176,7 +176,7 @@ return new class extends Seeder
|
||||
'icon' => '',
|
||||
'module' => 'system',
|
||||
'permission_mark' => 'dictionaryValues',
|
||||
'component' => '/System/views/dictionaryValues/index.vue',
|
||||
'component' => '/system/dictionaryValues/index.vue',
|
||||
'redirect' => '',
|
||||
'keepalive' => 2,
|
||||
'type' => 2,
|
||||
|
@@ -1,56 +0,0 @@
|
||||
<template>
|
||||
<el-form :model="formData" label-width="120px" ref="form" v-loading="loading" class="pr-4">
|
||||
<el-form-item
|
||||
label="字典名称"
|
||||
prop="name"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '字典名称必须填写',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<el-input v-model="formData.name" name="name" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="字典键名"
|
||||
prop="key"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '字典键名必须填写',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<el-input v-model="formData.key" name="key" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典描述" prop="description">
|
||||
<el-input v-model="formData.description" name="description" clearable type="textarea" />
|
||||
</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 } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
primary: String | Number,
|
||||
api: String,
|
||||
})
|
||||
|
||||
const { formData, form, loading, submitForm, close } = useCreate(props.api, props.primary)
|
||||
|
||||
if (props.primary) {
|
||||
useShow(props.api, props.primary, formData)
|
||||
}
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
onMounted(() => {
|
||||
close(() => emit('close'))
|
||||
})
|
||||
</script>
|
@@ -1,73 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Search :search="search" :reset="reset">
|
||||
<template v-slot:body>
|
||||
<el-form-item label="字典名称" prop="name">
|
||||
<el-input v-model="query.name" name="name" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典键名" prop="key">
|
||||
<el-input v-model="query.key" name="key" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典状态" prop="status">
|
||||
<el-input v-model="query.status" name="status" clearable />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</Search>
|
||||
|
||||
<div class="table-default">
|
||||
<Operate :show="open" />
|
||||
<el-table :data="tableData" class="mt-3" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="100" />
|
||||
<el-table-column prop="name" label="字典名称" />
|
||||
<el-table-column prop="key" label="字典键名">
|
||||
<template #default="scope">
|
||||
<router-link :to="{ path: '/system/dictionary/values/' + scope.row.id }">
|
||||
<el-text type="primary">{{ scope.row.key }}</el-text>
|
||||
</router-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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="description" label="字典描述" />
|
||||
<el-table-column label="操作" width="300">
|
||||
<template #default="scope">
|
||||
<Update @click="open(scope.row.id)" />
|
||||
<Destroy @click="destroy(api, scope.row.id)" />
|
||||
<router-link :to="{ path: '/system/dictionary/values/' + scope.row.id }">
|
||||
<Show text="列表" class="ml-3" />
|
||||
</router-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<Paginate />
|
||||
</div>
|
||||
|
||||
<Dialog v-model="visible" :title="title" destroy-on-close>
|
||||
<Create @close="close(reset)" :primary="id" :api="api" />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted } from 'vue'
|
||||
import Create from './create.vue'
|
||||
import { useGetList } from '/admin/composables/curd/useGetList'
|
||||
import { useDestroy } from '/admin/composables/curd/useDestroy'
|
||||
import { useOpen } from '/admin/composables/curd/useOpen'
|
||||
|
||||
const api = 'system/dictionary'
|
||||
|
||||
const { data, query, search, reset, loading } = useGetList(api)
|
||||
const { destroy, deleted } = useDestroy()
|
||||
const { open, close, title, visible, id } = useOpen()
|
||||
|
||||
const tableData = computed(() => data.value?.data)
|
||||
|
||||
onMounted(() => {
|
||||
search()
|
||||
deleted(reset)
|
||||
})
|
||||
</script>
|
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<el-form :model="formData" label-width="120px" ref="form" v-loading="loading" class="pr-4">
|
||||
<el-form-item
|
||||
label="字典值名"
|
||||
prop="label"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '字典值名必须填写',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<el-input v-model="formData.label" name="label" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="字典键值"
|
||||
prop="value"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '字典键值必须填写',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<el-input-number v-model="formData.value" name="value" clearable :min="1" />
|
||||
</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="描述" prop="description">
|
||||
<el-input v-model="formData.description" name="description" clearable type="textarea" />
|
||||
</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 } from 'vue'
|
||||
import router from '/admin/router'
|
||||
|
||||
const props = defineProps({
|
||||
primary: String | Number,
|
||||
api: String,
|
||||
})
|
||||
|
||||
const { formData, form, loading, submitForm, close } = useCreate(props.api, props.primary)
|
||||
|
||||
// 默认值
|
||||
formData.value.value = 1
|
||||
formData.value.sort = 1
|
||||
formData.value.dic_id = router.currentRoute.value.params.id
|
||||
|
||||
if (props.primary) {
|
||||
useShow(props.api, props.primary, formData)
|
||||
}
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
onMounted(() => {
|
||||
close(() => emit('close'))
|
||||
})
|
||||
</script>
|
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Search :search="search" :reset="reset">
|
||||
<template v-slot:body>
|
||||
<el-form-item label="字典值名" prop="label">
|
||||
<el-input v-model="query.label" name="label" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-input v-model="query.status" name="status" clearable />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</Search>
|
||||
<div class="table-default">
|
||||
<Operate :show="open" />
|
||||
<el-table :data="tableData" class="mt-3" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" />
|
||||
<el-table-column prop="label" label="字典值名称" />
|
||||
<el-table-column prop="value" label="字典键值" />
|
||||
<el-table-column prop="sort" 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="description" label="描述" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<Update @click="open(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(reset)" :primary="id" :api="api" />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted } from 'vue'
|
||||
import Create from './create.vue'
|
||||
import { useGetList } from '/admin/composables/curd/useGetList'
|
||||
import { useDestroy } from '/admin/composables/curd/useDestroy'
|
||||
import { useOpen } from '/admin/composables/curd/useOpen'
|
||||
import router from '/admin/router'
|
||||
|
||||
const api = 'system/dic/values'
|
||||
|
||||
const { data, query, search, reset, loading } = useGetList(api)
|
||||
query.value.dic_id = router.currentRoute.value.params.id
|
||||
|
||||
const { destroy, deleted } = useDestroy()
|
||||
const { open, close, title, visible, id } = useOpen()
|
||||
|
||||
const tableData = computed(() => data.value?.data)
|
||||
|
||||
onMounted(() => {
|
||||
search()
|
||||
deleted(reset)
|
||||
})
|
||||
</script>
|
@@ -6,17 +6,17 @@ use Catch\CatchAdmin;
|
||||
use Catch\Traits\DB\BaseOperate;
|
||||
use Catch\Traits\DB\ScopeTrait;
|
||||
use Catch\Traits\DB\Trans;
|
||||
use Catch\Traits\DB\WithAttributes;
|
||||
use Illuminate\Contracts\Http\Kernel;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class LogOperate extends Model
|
||||
{
|
||||
use BaseOperate, Trans, ScopeTrait;
|
||||
use BaseOperate, Trans, ScopeTrait, WithAttributes;
|
||||
|
||||
protected $table = 'log_operate';
|
||||
|
||||
|
@@ -59,7 +59,7 @@ trait UserRelations
|
||||
/* @var Permissions $permissionsModel */
|
||||
$permissionsModel = app($this->getPermissionsModel());
|
||||
if ($this->isSuperAdmin()) {
|
||||
$permissions = $permissionsModel->get();
|
||||
$permissions = $permissionsModel->orderByDesc('sort')->get();
|
||||
} else {
|
||||
$permissionIds = Collection::make();
|
||||
$this->roles()->with('permissions')->get()
|
||||
@@ -67,7 +67,7 @@ trait UserRelations
|
||||
$permissionIds = $permissionIds->concat($role->permissions?->pluck('id'));
|
||||
});
|
||||
|
||||
$permissions = $permissionsModel->whereIn('id', $permissionIds->unique())->get();
|
||||
$permissions = $permissionsModel->whereIn('id', $permissionIds->unique())->orderByDesc('sort')->get();
|
||||
}
|
||||
|
||||
$this->setAttribute('permissions', $permissions->each(fn ($permission) => $permission->setAttribute('hidden', $permission->isHidden())));
|
||||
|
@@ -32,6 +32,8 @@ class User extends Model implements AuthenticatableContract
|
||||
'id', 'username', 'email', 'avatar', 'password', 'remember_token', 'creator_id', 'status', 'department_id', 'login_ip', 'login_at', 'created_at', 'updated_at', 'deleted_at'
|
||||
];
|
||||
|
||||
protected array $defaultHidden = ['password', 'remember_token'];
|
||||
|
||||
/**
|
||||
* @var array|string[]
|
||||
*/
|
||||
|
@@ -1,26 +0,0 @@
|
||||
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: 'user-account',
|
||||
meta: { title: '账号管理', icon: 'home' },
|
||||
component: () => import('./user/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'center',
|
||||
name: 'user-center',
|
||||
meta: { title: '个人中心', icon: 'home' },
|
||||
component: () => import('./user/center.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export default router
|
@@ -1,44 +0,0 @@
|
||||
<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] h-[32rem]">
|
||||
<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-0 sm: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>
|
@@ -1,43 +0,0 @@
|
||||
<template>
|
||||
<div class="w-full sm:w-[28rem] min-h-[30rem] bg-white">
|
||||
<el-tree :data="departments" :props="{ label: 'department_name', value: 'id'}" @node-click="clickDepartment" class="p-5" :expand-on-click-node="false" :highlight-current="true"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import http from '/admin/support/http'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const departments = ref()
|
||||
|
||||
onMounted(() => {
|
||||
http.get('permissions/departments').then(r => {
|
||||
departments.value = r.data.data
|
||||
})
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:modelValue', 'searchDepartmentUsers'])
|
||||
|
||||
const clickDepartment = (node) => {
|
||||
emits('update:modelValue', node.id)
|
||||
|
||||
emits('searchDepartmentUsers')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-tree .el-tree-node) {
|
||||
@apply p-0.5
|
||||
}
|
||||
|
||||
:deep(.el-tree .el-tree-node .el-tree-node__content) {
|
||||
@apply h-8 rounded
|
||||
}
|
||||
</style>
|
@@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<div class="table-default">
|
||||
<el-table :data="tableData" class="mt-3" v-loading="loading">
|
||||
<el-table-column prop="account" label="账户" />
|
||||
<el-table-column prop="browser" label="浏览器" />
|
||||
<el-table-column prop="platform" label="平台" />
|
||||
<el-table-column prop="login_ip" label="IP" />
|
||||
<el-table-column prop="status" label="状态">
|
||||
<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>
|
||||
|
||||
<Paginate />
|
||||
</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, reset, loading } = useGetList(api)
|
||||
|
||||
onMounted(() => search())
|
||||
|
||||
const tableData = computed(() => data.value?.data)
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<div class="table-default">
|
||||
<div class="w-full flex justify-end">
|
||||
<el-radio-group v-model="query.scope" size="small" @change="search">
|
||||
<el-radio-button label="self">只看自己</el-radio-button>
|
||||
<el-radio-button label="all">全部</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<el-table :data="tableData" class="mt-3" v-loading="loading">
|
||||
<el-table-column prop="creator" label="创建人" />
|
||||
<el-table-column prop="module" label="模块" />
|
||||
<el-table-column prop="action" label="操作" width="150" />
|
||||
<el-table-column prop="http_method" label="请求方法" width="90" />
|
||||
<el-table-column prop="http_code" label="请求状态" width="90">
|
||||
<template #default="scope">
|
||||
<el-tag type="success" v-if="scope.row.http_code >= 200 && scope.row.http_code < 300"> {{ scope.row.http_code }}</el-tag>
|
||||
<el-tag type="danger" v-else>{{ scope.row.http_code }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="time_taken" label="耗时" />
|
||||
<el-table-column prop="params" label="参数">
|
||||
<template #default="scope">
|
||||
<el-tooltip class="box-item" effect="dark" :content="scope.row.params" placement="top-start">
|
||||
<el-button size="small" type="primary">查看</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<Paginate />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useGetList } from '/admin/composables/curd/useGetList'
|
||||
|
||||
const api = 'user/operate/log'
|
||||
|
||||
const { data, query, search, reset, loading } = useGetList(api)
|
||||
|
||||
query.value.scope = 'self'
|
||||
|
||||
onMounted(() => search())
|
||||
|
||||
const tableData = computed(() => data.value?.data)
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@@ -1,98 +0,0 @@
|
||||
<template>
|
||||
<el-form :model="profile" ref="form" v-loading="loading" label-position="top">
|
||||
<Upload imageClass="w-28 h-28 rounded-full mx-auto" v-model="profile.avatar" />
|
||||
<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 } from 'vue'
|
||||
import { useCreate } from '/admin/composables/curd/useCreate'
|
||||
import http from '/admin/support/http'
|
||||
import { Code } from '/admin/enum/app'
|
||||
import Message from '/admin/support/message'
|
||||
import { useUserStore } from '/admin/stores/modules/user'
|
||||
|
||||
interface profile {
|
||||
avatar: string
|
||||
username: string
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
const profile = ref<profile>({
|
||||
avatar: '',
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
const { form, loading, submitForm, afterCreate } = useCreate('user/online', null, profile)
|
||||
|
||||
const getUserInfo = () => {
|
||||
loading.value = true
|
||||
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
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getUserInfo()
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
afterCreate.value = () => {
|
||||
userStore.getUserInfo()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-upload) {
|
||||
@apply h-full w-full;
|
||||
}
|
||||
</style>
|
@@ -1,129 +0,0 @@
|
||||
<template>
|
||||
<el-form :model="formData" label-width="80px" ref="form" v-loading="loading" class="pr-4">
|
||||
<div class="flex flex-row justify-between">
|
||||
<div :class="hasRoles ? 'w-1/2' : 'w-full'">
|
||||
<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>
|
||||
|
||||
<el-form-item label="角色" prop="roles" v-if="hasRoles" :rules="[{ required: true, message: '请选择角色' }]">
|
||||
<el-tree-select
|
||||
v-model="formData.roles"
|
||||
:default-expanded-keys="formData.roles"
|
||||
:data="roles"
|
||||
value-key="id"
|
||||
check-strictly
|
||||
class="w-full"
|
||||
:props="{ label: 'role_name', value: 'id' }"
|
||||
clearable
|
||||
multiple
|
||||
show-checkbox
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="w-1/2" v-if="hasRoles">
|
||||
<el-form-item label="部门" prop="department_id">
|
||||
<el-tree-select v-model="formData.department_id" :data="departments" check-strictly :props="{ label: 'department_name', value: 'id' }" />
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位" prop="department_id">
|
||||
<el-select v-model="formData.jobs" multiple>
|
||||
<el-option v-for="item in jobs" :key="item.id" :label="item.job_name" :value="item.id" />
|
||||
</el-select>
|
||||
</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>
|
||||
</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 http from '/admin/support/http'
|
||||
|
||||
const props = defineProps({
|
||||
primary: String | Number,
|
||||
api: String,
|
||||
hasRoles: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
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, close } = useCreate(props.api, props.primary)
|
||||
|
||||
if (props.primary) {
|
||||
useShow(props.api, props.primary, formData)
|
||||
}
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
close(() => emit('close'))
|
||||
|
||||
const departments = ref()
|
||||
const jobs = ref()
|
||||
const roles = ref()
|
||||
|
||||
onMounted(() => {
|
||||
if (props.hasRoles) {
|
||||
http.get('permissions/departments').then(r => {
|
||||
departments.value = r.data.data
|
||||
})
|
||||
|
||||
http.get('permissions/jobs').then(r => {
|
||||
jobs.value = r.data.data
|
||||
})
|
||||
|
||||
http.get('permissions/roles').then(r => {
|
||||
roles.value = r.data.data
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
@@ -1,88 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col sm:flex-row w-full justify-between">
|
||||
<Department v-model="query.department_id" @searchDepartmentUsers="search" v-if="hasRoles" class="dark:bg-regal-dark" />
|
||||
<div :class="hasRoles ? 'w-full ml-0 sm:ml-2 mt-2 sm:mt-0' : 'w-full'">
|
||||
<Search :search="search" :reset="reset">
|
||||
<template v-slot:body>
|
||||
<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>
|
||||
</template>
|
||||
</Search>
|
||||
<div class="table-default">
|
||||
<Operate :show="open">
|
||||
<template #operate>
|
||||
<el-button @click="download('/user')">导出</el-button>
|
||||
</template>
|
||||
</Operate>
|
||||
<el-table :data="tableData" class="mt-3" v-loading="loading">
|
||||
<el-table-column prop="username" label="用户名" width="150" />
|
||||
<el-table-column prop="avatar" label="头像">
|
||||
<template #default="scope">
|
||||
<el-avatar :icon="UserFilled" v-if="!scope.row.avatar" />
|
||||
<el-avatar :src="scope.row.avatar" v-else />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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="open(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(reset)" :primary="id" :api="api" :has-roles="hasRoles" />
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// @ts-nocheck
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import Create from './create.vue'
|
||||
import { useGetList } from '/admin/composables/curd/useGetList'
|
||||
import { useDestroy } from '/admin/composables/curd/useDestroy'
|
||||
import { useOpen } from '/admin/composables/curd/useOpen'
|
||||
import Department from './components/department.vue'
|
||||
import { useUserStore } from '/admin/stores/modules/user'
|
||||
import { isUndefined } from '/admin/support/helper'
|
||||
import { UserFilled } from '@element-plus/icons-vue'
|
||||
import { useExcelDownload } from '/resources/admin/composables/curd/useExcelDownload'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const api = 'users'
|
||||
const { data, query, search, reset, loading } = useGetList(api)
|
||||
const { destroy, deleted } = useDestroy()
|
||||
const { open, close, title, visible, id } = useOpen()
|
||||
const { download } = useExcelDownload()
|
||||
|
||||
const tableData = computed(() => data.value?.data)
|
||||
|
||||
const roles = ref<Array<Object>>()
|
||||
const hasRoles = ref<boolean>(false)
|
||||
|
||||
onMounted(() => {
|
||||
search()
|
||||
deleted(reset)
|
||||
hasRoles.value = !isUndefined(userStore.getRoles)
|
||||
})
|
||||
</script>
|
55
package.json
55
package.json
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"name": "catchadmin",
|
||||
"private": false,
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/vue": "^2.1.1",
|
||||
"@tinymce/tinymce-vue": "^5.1.1",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"element-plus": "^2.5.3",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
"terser": "^5.16.6",
|
||||
"vue": "^3.4.15",
|
||||
"vue-i18n": "9",
|
||||
"vue-router": "4.2.5",
|
||||
"vuedraggable": "^2.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/logos": "^1.1.31",
|
||||
"@rollup/plugin-alias": "^5.0.0",
|
||||
"@types/mockjs": "^1.0.7",
|
||||
"@types/node": "^20.2.3",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.1",
|
||||
"@typescript-eslint/parser": "^6.19.1",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"axios": "^1.5.1",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-n": "^16.0.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"mockjs": "^1.1.0",
|
||||
"postcss": "^8.4.33",
|
||||
"prettier": "2.8.8",
|
||||
"sass": "^1.62.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.1.6",
|
||||
"unplugin-auto-import": "^0.17.3",
|
||||
"unplugin-icons": "^0.18.2",
|
||||
"unplugin-vue-components": "^0.25.2",
|
||||
"vite": "^5.0.12",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"vite-plugin-mock": "^3.0.0",
|
||||
"vue-tsc": "^1.6.5"
|
||||
}
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
@@ -1,7 +0,0 @@
|
||||
import '/admin/styles/index.scss'
|
||||
|
||||
import CatchAdmin from './support/catchAdmin'
|
||||
|
||||
const admin = new CatchAdmin()
|
||||
|
||||
admin.bootstrap()
|
Binary file not shown.
Before Width: | Height: | Size: 67 KiB |
Binary file not shown.
Before Width: | Height: | Size: 40 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.1 KiB |
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<div :style="bgColor" class="flex flex-col w-full">
|
||||
<img :src="notFound" class="w-full sm:w-3/5 m-auto" />
|
||||
<div class="mr-auto w-full bottom-0 m-auto">
|
||||
<div class="w-full text-center text-base text-gray-400">抱歉,您访问的页面不存在</div>
|
||||
<div @click="push('/')" class="text-center w-full mt-2">
|
||||
<el-button type="primary">回到首页</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '/admin/stores/modules/app'
|
||||
import { computed } from 'vue'
|
||||
import notFound from '/admin/assets/404.png'
|
||||
|
||||
const { push } = useRouter()
|
||||
|
||||
const dark: string = '#161d31;'
|
||||
const light: string = 'rgb(241,245,249);'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const bgColor = computed(() => {
|
||||
return 'background-color:' + (appStore.getIsDarkMode ? dark : light)
|
||||
})
|
||||
</script>
|
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<el-button type="primary" :size="size"><Icon name="plus" className="w-4 h-4 mr-1" /> {{ text }}</el-button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '新增',
|
||||
},
|
||||
})
|
||||
</script>
|
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<el-button type="danger" :size="size"><Icon name="trash" className="w-4 h-4 mr-1" /> {{ text }}</el-button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'small',
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '删除',
|
||||
},
|
||||
})
|
||||
</script>
|
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<el-button type="primary" :size="size"><Icon name="eye" className="w-4 h-4 mr-1" /> {{ text }}</el-button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'small',
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '详情',
|
||||
},
|
||||
})
|
||||
</script>
|
@@ -1,18 +0,0 @@
|
||||
<template>
|
||||
<el-button type="success" :size="size"><Icon name="pencil-square" className="w-4 h-4 mr-1" /> {{ text }}</el-button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'small',
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '更新',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@@ -1,100 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog :model-value="modelValue" :show-close="false" :fullscreen="isFullscreen" v-bind="$attrs" :width="width" :close="close" :before-close="beforeClose" draggable>
|
||||
<template #header="{ titleId, titleClass }">
|
||||
<div class="flex justify-between w-full">
|
||||
<div>
|
||||
<h4 :id="titleId" :class="titleClass">{{ title }}</h4>
|
||||
</div>
|
||||
<div class="flex w-12 justify-end">
|
||||
<!--<Icon :name="fullscreenIcon" @click="fullscreen" className="hover:cursor-pointer w-4 h-4" />-->
|
||||
<Icon name="x-mark" className="hover:cursor-pointer w-5 h-5" @click="close" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<slot />
|
||||
<template #footer v-if="showFooter">
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="close">{{ $t('system.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="close">{{ $t('system.confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
require: true,
|
||||
},
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
width: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
|
||||
const isFullscreen = ref(false)
|
||||
|
||||
const fullscreenIcon = computed(() => {
|
||||
return isFullscreen.value ? 'arrows-pointing-in' : 'arrows-pointing-out'
|
||||
})
|
||||
const fullscreen = () => {
|
||||
isFullscreen.value = !isFullscreen.value
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
emits('update:modelValue', false)
|
||||
}
|
||||
|
||||
// 遮罩关闭调用
|
||||
const beforeClose = () => {
|
||||
emits('update:modelValue', false)
|
||||
}
|
||||
const width = ref<string>('')
|
||||
|
||||
onMounted(() => {
|
||||
width.value = props.width ? props.width : getWidth()
|
||||
})
|
||||
|
||||
// 窗口尺寸
|
||||
const getWidth = () => {
|
||||
const clientWidth = window.document.body.clientWidth
|
||||
|
||||
if (clientWidth <= 726) {
|
||||
return '100%'
|
||||
}
|
||||
|
||||
if (clientWidth > 726 && clientWidth < 1440) {
|
||||
return '60%'
|
||||
}
|
||||
|
||||
return '650px'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 0.5rem;
|
||||
.el-dialog__header {
|
||||
margin-right: 0 !important;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,360 +0,0 @@
|
||||
<template>
|
||||
<div class="h-84 pl-2 pr-2">
|
||||
<div :class="`grid ${grid} gap-y-4 gap-x-4` + ' mt-3 h-72'">
|
||||
<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" className="w-5 h-5" /></div>
|
||||
<div class="text-[1px] text-violet-700">{{ icon }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="flex justify-center w-full"><Icon :name="icon" className="w-5 h-5" /></div>
|
||||
<div class="text-[1px]">{{ icon }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mt-6">
|
||||
<el-pagination layout="prev,next" :page-size="limit" :total="total" prev-text="上一页" next-text="下一页" @next-click="handleNext" @prev-click="handlePrev" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
require: true,
|
||||
},
|
||||
grid: {
|
||||
type: String,
|
||||
default: 'grid-cols-4',
|
||||
},
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
const limit = ref<number>(16)
|
||||
const icons = ref<Array<string>>([])
|
||||
const total = ref<number>(0)
|
||||
function getIcons(page = 1) {
|
||||
const start = (page - 1) * limit.value
|
||||
const end = start + limit.value
|
||||
icons.value = constIcons.slice(start, end)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getIcons()
|
||||
total.value = constIcons.length
|
||||
})
|
||||
const handleNext = (value: number) => {
|
||||
getIcons(value)
|
||||
}
|
||||
|
||||
const handlePrev = (value: number) => {
|
||||
getIcons(value)
|
||||
}
|
||||
const selectIcon = (icon: string) => {
|
||||
emits('update:modelValue', icon)
|
||||
emits('close')
|
||||
}
|
||||
// icons
|
||||
const constIcons = [
|
||||
'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>
|
@@ -1,25 +0,0 @@
|
||||
<template>
|
||||
<div class="flex justify-end pt-5">
|
||||
<el-pagination background :layout="layout" :current-page="page" :page-size="limit" @current-change="changePage" @size-change="changeLimit" :total="total" :page-sizes="pageSizes" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject } from 'vue'
|
||||
|
||||
const layout = 'total,sizes,prev, pager,next'
|
||||
|
||||
const pageSizes = [10, 20, 30, 50]
|
||||
|
||||
interface paginate {
|
||||
page: number
|
||||
limit: number
|
||||
total: number
|
||||
changePage: number
|
||||
changeLimit: number
|
||||
}
|
||||
|
||||
const { page, limit, total, changePage, changeLimit } = inject('paginate') as paginate
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<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>
|
||||
<el-option v-for="option in elOptions" :key="option.value" :label="option.label" :value="option.value" v-else>
|
||||
<slot />
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import http from '/admin/support/http'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
api: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
group: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
query: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
interface Option {
|
||||
label: string
|
||||
value: string | number
|
||||
}
|
||||
|
||||
interface GroupOption {
|
||||
label: string
|
||||
options: Array<Option>
|
||||
}
|
||||
|
||||
const getOptions = () => {
|
||||
http.get('options/' + props.api, props.query).then(r => {
|
||||
elOptions.value = r.data.data
|
||||
})
|
||||
}
|
||||
|
||||
const elOptions: any = 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
|
||||
}
|
||||
</script>
|
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<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],
|
||||
api: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
id: {
|
||||
required: true,
|
||||
type: [String, Number],
|
||||
},
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:modelValue', 'refresh'])
|
||||
// @ts-ignore
|
||||
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 === activeValue.value ? inactiveValue.value : activeValue.value)
|
||||
})
|
||||
|
||||
afterEnabled.value = () => {
|
||||
emits('refresh')
|
||||
}
|
||||
</script>
|
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<div class="pt-5 pl-2">
|
||||
<Add @click="show(showParams)" />
|
||||
<slot name="operate" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
show: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
|
||||
showParams: null,
|
||||
})
|
||||
</script>
|
@@ -1,33 +0,0 @@
|
||||
<template>
|
||||
<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">
|
||||
<slot name="body" />
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="search()">
|
||||
<Icon name="magnifying-glass" className="w-4 h-4 mr-1 -ml-1" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="reset()">
|
||||
<Icon name="arrow-path" className="w-4 h-4 mr-1 -ml-1" />
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
search: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
|
||||
reset: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<el-upload
|
||||
ref="upload"
|
||||
:action="actionApi"
|
||||
:show-file-list="false"
|
||||
name="image"
|
||||
:auto-upload="auto"
|
||||
:headers="{ authorization: token, 'Request-from': 'Dashboard' }"
|
||||
v-bind="$attrs"
|
||||
:on-success="handleSuccess"
|
||||
>
|
||||
<template v-for="(index, name) in $slots" v-slot:[name]>
|
||||
<slot :name="name"></slot>
|
||||
</template>
|
||||
<img :src="modelValue" v-if="modelValue" :class="imageClass" />
|
||||
<div v-else class="w-24 h-24 border-blue-100 border-dashed border rounded flex justify-center pt-8">
|
||||
<Icon name="plus" />
|
||||
</div>
|
||||
</el-upload>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { env } from '/admin/support/helper'
|
||||
import { getAuthToken } from '/admin/support/helper'
|
||||
import { Code } from '/admin/enum/app'
|
||||
import Message from '/admin/support/message'
|
||||
|
||||
const props = defineProps({
|
||||
action: {
|
||||
type: String,
|
||||
default: 'upload/image',
|
||||
},
|
||||
auto: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
require: true,
|
||||
},
|
||||
imageClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
const baseURL = env('VITE_BASE_URL')
|
||||
|
||||
const actionApi = ref<string>('')
|
||||
|
||||
actionApi.value = baseURL + props.action
|
||||
|
||||
const token = ref<string>()
|
||||
token.value = 'Bearer ' + getAuthToken()
|
||||
|
||||
const handleSuccess = (response: any) => {
|
||||
if (response.code === Code.SUCCESS) {
|
||||
emits('update:modelValue', response.data.path)
|
||||
} else {
|
||||
Message.error(response.message)
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -1,59 +0,0 @@
|
||||
<template>
|
||||
<el-upload ref="upload" :action="action" :auto-upload="auto" v-bind="$attrs" :data="data" :before-upload="initOss" :on-success="handleSuccess">
|
||||
<template v-for="(index, name) in $slots" v-slot:[name]>
|
||||
<slot :name="name"></slot>
|
||||
</template>
|
||||
</el-upload>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import http from '/admin/support/http'
|
||||
import { ref } from 'vue'
|
||||
import Message from '/admin/support/message'
|
||||
const props = defineProps({
|
||||
auto: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
require: true,
|
||||
},
|
||||
})
|
||||
|
||||
const action = ref('')
|
||||
const data = ref({
|
||||
OSSAccessKeyId: '',
|
||||
policy: '',
|
||||
Signature: '',
|
||||
key: '',
|
||||
host: '',
|
||||
dir: '',
|
||||
expire: '',
|
||||
success_action_status: 200,
|
||||
})
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
const initOss = async (file: { size: number; name: any }) => {
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
Message.error('最大支持 10MB 文件')
|
||||
return
|
||||
}
|
||||
|
||||
await http.get('upload/oss').then(r => {
|
||||
const { accessKeyId, bucket, dir, expire, host, policy, signature, url } = r.data.data
|
||||
action.value = host
|
||||
data.value.OSSAccessKeyId = accessKeyId
|
||||
data.value.policy = policy
|
||||
data.value.Signature = signature
|
||||
data.value.key = dir + file.name
|
||||
data.value.host = host
|
||||
data.value.dir = dir
|
||||
data.value.expire = expire
|
||||
})
|
||||
}
|
||||
|
||||
const handleSuccess = (r: any) => {
|
||||
emits('update:modelValue', action.value + data.value.key)
|
||||
}
|
||||
</script>
|
@@ -1,71 +0,0 @@
|
||||
<template>
|
||||
<el-breadcrumb separator="/" class="flex sm:text-sm lg:text-base">
|
||||
<transition-group name="breadcrumb">
|
||||
<!--<el-breadcrumb-item :to="{ path: '/' }" class="text-blue=">Dashboard</el-breadcrumb-item>-->
|
||||
|
||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="index" class="text">{{ item }}</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import router from '/admin/router'
|
||||
import { watch, onMounted, ref } from 'vue'
|
||||
import { useAppStore } from '/admin/stores/modules/app'
|
||||
import { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const breadcrumbs = ref<string[]>([])
|
||||
|
||||
// 监听当前路由的变化
|
||||
watch(router.currentRoute, (newValue, oldValue) => {
|
||||
// 激活菜单
|
||||
if (newValue.meta.active_menu) {
|
||||
appStore.setActiveMenu(newValue.meta.active_menu as string)
|
||||
}
|
||||
setActiveMenu(newValue)
|
||||
getBreadcrumbs(newValue)
|
||||
})
|
||||
|
||||
// get init breadcrumb
|
||||
onMounted(() => {
|
||||
setActiveMenu(router.currentRoute.value)
|
||||
getBreadcrumbs(router.currentRoute.value)
|
||||
})
|
||||
|
||||
const setActiveMenu = (route: RouteLocationNormalizedLoaded) => {
|
||||
if (route.path !== '/') {
|
||||
// 如果是内页,并且设置激活菜单
|
||||
if (route.meta.active_menu) {
|
||||
appStore.setActiveMenu(route.meta.active_menu as string)
|
||||
} else {
|
||||
appStore.setActiveMenu(route.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get breadcrums
|
||||
function getBreadcrumbs(newRoute: RouteLocationNormalizedLoaded) {
|
||||
breadcrumbs.value = []
|
||||
breadcrumbs.value.push('首页')
|
||||
newRoute.matched.forEach(m => {
|
||||
if (m.meta.title !== undefined) {
|
||||
breadcrumbs.value.push(m.meta?.title as string)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.breadcrumb-leave-active {
|
||||
transition: all 1s linear;
|
||||
}
|
||||
|
||||
.breadcrumb-leave-to {
|
||||
opacity: 0;
|
||||
transition: all 0.3s linear;
|
||||
}
|
||||
.el-breadcrumb {
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
@@ -1,160 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Editor :api-key="aipKey" :init="config" v-model="content" v-bind="$attrs" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import '/admin/public/tinymce/tinymce.min'
|
||||
import '/admin/public/tinymce/themes/silver/theme.min'
|
||||
import '/admin/public/tinymce/icons/default/icons.min'
|
||||
import '/admin/public/tinymce/models/dom/model.min'
|
||||
// css
|
||||
import '/admin/public/tinymce/skins/ui/oxide/skin.min.css'
|
||||
|
||||
// plugins
|
||||
import '/admin/public/tinymce/plugins/preview/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/searchreplace/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/autolink/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/directionality/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/visualblocks/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/visualchars/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/fullscreen/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/image/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/link/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/media/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/template/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/code/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/codesample/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/table/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/charmap/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/pagebreak/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/nonbreaking/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/anchor/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/insertdatetime/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/advlist/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/lists/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/wordcount/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/autosave/plugin.min'
|
||||
import '/admin/public/tinymce/plugins/emoticons/plugin.min'
|
||||
|
||||
// lang
|
||||
import '/admin/public/tinymce/langs/zh-CN'
|
||||
|
||||
import Editor from '@tinymce/tinymce-vue'
|
||||
import { env } from '/admin/support/helper'
|
||||
import Http from '/admin/support/http'
|
||||
import Message from '/admin/support/message'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
require: true,
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
required: false,
|
||||
default: 'auto',
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
required: false,
|
||||
default: 'auto',
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'zh-CN',
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '在这里输入内容',
|
||||
},
|
||||
|
||||
plugins: {
|
||||
type: String,
|
||||
default:
|
||||
'preview searchreplace autolink directionality visualblocks visualchars fullscreen image link media template code ' +
|
||||
'codesample table charmap pagebreak nonbreaking anchor insertdatetime advlist lists wordcount autosave emoticons',
|
||||
},
|
||||
toolbar: {
|
||||
type: Array,
|
||||
default: [
|
||||
'undo redo restoredraft cut copy paste pastetext forecolor backcolor bold italic underline strikethrough link anchor alignleft aligncenter alignright alignjustify outdent indent bullist numlist blockquote subscript superscript removeformat styleselect formatselect fontselect fontsizeselect ' +
|
||||
'table upload image axupimgs media emoticons charmap hr pagebreak insertdatetime ' +
|
||||
'selectall visualblocks searchreplace code print preview indent2em fullscreen',
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const aipKey: string = 's1ntkmnev0ggx0hhaqnubrdxhv0ly99uyrdbckeaycx7iz6v'
|
||||
const uploaded = (blobInfo: any, progress: any) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (blobInfo.blob().size / 1024 / 1024 > 10) {
|
||||
Message.error('上传失败,图片大小请控制在 10M 以内')
|
||||
} else {
|
||||
let params = new FormData()
|
||||
params.append('image', blobInfo.blob())
|
||||
Http.post(env('VITE_BASE_URL') + 'upload/image', params)
|
||||
.then(res => {
|
||||
if (res.data.code === 10000) {
|
||||
resolve(res.data.data.path)
|
||||
} else {
|
||||
Message.error(res.data.message)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
Message.error('Server Error!')
|
||||
})
|
||||
}
|
||||
})
|
||||
const config = {
|
||||
base_url: '/admin/public/tinymce',
|
||||
language: props.language,
|
||||
placeholder: props.placeholder,
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
plugins: props.plugins,
|
||||
toolbar: props.toolbar,
|
||||
branding: false,
|
||||
// menubar: false,
|
||||
images_upload_handler: uploaded,
|
||||
}
|
||||
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
const content = ref(props.modelValue)
|
||||
// 创建的时候
|
||||
watch(content, value => {
|
||||
emits('update:modelValue', value)
|
||||
})
|
||||
|
||||
// 回显监听
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
value => {
|
||||
content.value = value
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tinymce-boxz > textarea {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
/* 隐藏apikey没有绑定这个域名的提示 */
|
||||
.tox-notifications-container .tox-notification--warning {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.tox {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
.tox-promotion {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<component :is="icon" :class="className" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import * as heroIcons from '@heroicons/vue/24/outline'
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "w-5 h-5"
|
||||
}
|
||||
})
|
||||
|
||||
const icon = computed(() => {
|
||||
let name = ''
|
||||
props.name.split('-').forEach(v => {
|
||||
name += v[0].toUpperCase() + v.substr(1)
|
||||
})
|
||||
return heroIcons[name + 'Icon']
|
||||
})
|
||||
</script>
|
@@ -1,86 +0,0 @@
|
||||
import http from '/admin/support/http'
|
||||
import { ref, unref, watch } from 'vue'
|
||||
import { Code } from '/admin/enum/app'
|
||||
import Message from '/admin/support/message'
|
||||
import { FormInstance } from 'element-plus'
|
||||
import { AxiosResponse } from 'axios'
|
||||
import { isFunction } from '/admin/support/helper'
|
||||
|
||||
// get table list
|
||||
export function useCreate(path: string, id: string | number | null = null, _formData: object = {}) {
|
||||
const formData = ref<object>(_formData)
|
||||
|
||||
const loading = ref<boolean>()
|
||||
const isClose = ref<boolean>(false)
|
||||
|
||||
// 创建前 hook
|
||||
const beforeCreate = ref()
|
||||
// 更新前 hook
|
||||
const beforeUpdate = ref()
|
||||
|
||||
const afterCreate = ref()
|
||||
const afterUpdate = ref()
|
||||
// store
|
||||
function store(path: string, id: string | number | null = null) {
|
||||
loading.value = true
|
||||
let promise: Promise<AxiosResponse> | null = null
|
||||
if (id) {
|
||||
if (isFunction(beforeUpdate.value)) {
|
||||
beforeUpdate.value()
|
||||
}
|
||||
|
||||
promise = http.put(path + '/' + id, unref(formData))
|
||||
} else {
|
||||
if (isFunction(beforeCreate.value)) {
|
||||
beforeCreate.value()
|
||||
}
|
||||
|
||||
promise = http.post(path, unref(formData))
|
||||
}
|
||||
|
||||
promise
|
||||
.then(r => {
|
||||
if (r.data.code === Code.SUCCESS) {
|
||||
isClose.value = true
|
||||
Message.success(r.data.message)
|
||||
// 创建后的操作
|
||||
if (!id && isFunction(afterCreate.value)) {
|
||||
afterCreate.value()
|
||||
}
|
||||
// 更新后的操作
|
||||
if (id && isFunction(afterUpdate.value)) {
|
||||
afterUpdate.value()
|
||||
}
|
||||
} else {
|
||||
Message.error(r.data.message)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const form = ref<FormInstance>()
|
||||
const submitForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl
|
||||
.validate(valid => {
|
||||
if (valid) {
|
||||
store(path, id)
|
||||
} else {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
.then(() => {})
|
||||
}
|
||||
|
||||
const close = (func: Function) => {
|
||||
watch(isClose, function (value) {
|
||||
if (value && isFunction(func)) {
|
||||
func()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { formData, loading, form, submitForm, close, beforeCreate, beforeUpdate, afterCreate, afterUpdate }
|
||||
}
|
@@ -1,44 +0,0 @@
|
||||
import http from '/admin/support/http'
|
||||
import { Code } from '/admin/enum/app'
|
||||
import Message from '/admin/support/message'
|
||||
import { ref, watch } from 'vue'
|
||||
import { isFunction } from '/admin/support/helper'
|
||||
|
||||
export function useDestroy(confirm: string = '确认删除吗') {
|
||||
const isDeleted = ref(false)
|
||||
|
||||
const beforeDestroy = ref()
|
||||
|
||||
// fetch list
|
||||
function destroy(path: string, id: string | number) {
|
||||
Message.confirm(confirm + '?', function () {
|
||||
// before destroy
|
||||
if (isFunction(beforeDestroy.value)) {
|
||||
beforeDestroy.value()
|
||||
}
|
||||
|
||||
http
|
||||
.delete(path + '/' + id)
|
||||
.then(r => {
|
||||
if (r.data.code === Code.SUCCESS) {
|
||||
Message.success(r.data.message)
|
||||
isDeleted.value = true
|
||||
} else {
|
||||
Message.error(r.data.message)
|
||||
}
|
||||
})
|
||||
.finally(() => {})
|
||||
})
|
||||
}
|
||||
|
||||
const deleted = (reset: Function) => {
|
||||
watch(isDeleted, function (value) {
|
||||
if (value) {
|
||||
isDeleted.value = false
|
||||
reset()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { destroy, deleted }
|
||||
}
|
@@ -1,39 +0,0 @@
|
||||
import http from '/admin/support/http'
|
||||
import { Code } from '/admin/enum/app'
|
||||
import Message from '/admin/support/message'
|
||||
import { ref, watch } from 'vue'
|
||||
import { isFunction } from '/admin/support/helper'
|
||||
|
||||
export function useEnabled() {
|
||||
const isSuccess = ref(false)
|
||||
const loading = ref<boolean>(false)
|
||||
const afterEnabled = ref()
|
||||
function enabled(path: string, id: string | number, data: object = {}) {
|
||||
loading.value = true
|
||||
http
|
||||
.put(path + '/enable/' + id, data)
|
||||
.then(r => {
|
||||
if (r.data.code === Code.SUCCESS) {
|
||||
isSuccess.value = true
|
||||
Message.success(r.data.message)
|
||||
if (isFunction(afterEnabled.value)) {
|
||||
afterEnabled.value()
|
||||
}
|
||||
} else {
|
||||
Message.error(r.data.message)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const success = (func: Function) => {
|
||||
watch(isSuccess, function () {
|
||||
isSuccess.value = false
|
||||
func()
|
||||
})
|
||||
}
|
||||
|
||||
return { enabled, success, loading, afterEnabled }
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
import Request from '/admin/support/request'
|
||||
import { ref, watch } from 'vue'
|
||||
import Message from '/admin/support/message'
|
||||
|
||||
export function useExcelDownload() {
|
||||
const http = new Request()
|
||||
const isSuccess = ref(false)
|
||||
const loading = ref<boolean>(false)
|
||||
const afterDownload = ref()
|
||||
function download(path: string, data: object = {}) {
|
||||
loading.value = true
|
||||
http
|
||||
.setResponseType('blob')
|
||||
.init()
|
||||
.get(path + '/export', data)
|
||||
.then(r => {
|
||||
if (r.headers['content-type'] === 'application/json') {
|
||||
const blob = new Blob([r.data], { type: r.headers['content-type'] })
|
||||
const blobReader = new Response(blob).json()
|
||||
blobReader.then(res => {
|
||||
if (res.code === 1e4) {
|
||||
Message.success(res.message)
|
||||
} else {
|
||||
Message.error(res.message)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const downloadLink = document.createElement('a')
|
||||
const blob = new Blob([r.data], { type: r.headers['content-type'] })
|
||||
downloadLink.href = URL.createObjectURL(blob)
|
||||
downloadLink.download = r.headers.filename
|
||||
document.body.appendChild(downloadLink)
|
||||
downloadLink.click()
|
||||
document.body.removeChild(downloadLink)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const success = (func: Function) => {
|
||||
watch(isSuccess, function () {
|
||||
isSuccess.value = false
|
||||
func()
|
||||
})
|
||||
}
|
||||
|
||||
return { download, success, loading, afterDownload }
|
||||
}
|
@@ -1,92 +0,0 @@
|
||||
import http from '/admin/support/http'
|
||||
import { provide, ref, unref } from 'vue'
|
||||
import { Code } from '/admin/enum/app'
|
||||
import Message from '/admin/support/message'
|
||||
|
||||
const initLimit = 10
|
||||
const initPage = 1
|
||||
const initTotal = 10
|
||||
|
||||
// get table list
|
||||
export function useGetList(path: string, isPaginate: boolean = true) {
|
||||
const data = ref<object>()
|
||||
const page = ref<number>(initPage)
|
||||
const limit = ref<number>(initLimit)
|
||||
const total = ref<number>(initTotal)
|
||||
const query = ref<object>({})
|
||||
if (isPaginate) {
|
||||
query.value = Object.assign({
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
})
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
// fetch list
|
||||
function getList() {
|
||||
// when table's data page >= 100, it will loading
|
||||
if (page.value >= 100) {
|
||||
loading.value = true
|
||||
}
|
||||
http
|
||||
.get(path, unref(query))
|
||||
.then(r => {
|
||||
closeLoading()
|
||||
if (r.data.code === Code.SUCCESS) {
|
||||
data.value = r.data
|
||||
// @ts-ignore
|
||||
total.value = data.value?.total
|
||||
} else {
|
||||
Message.error(r.data.message)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
closeLoading()
|
||||
})
|
||||
}
|
||||
|
||||
// close loading
|
||||
function closeLoading() {
|
||||
loading.value = false
|
||||
}
|
||||
// search
|
||||
function search() {
|
||||
getList()
|
||||
}
|
||||
|
||||
// reset
|
||||
function reset() {
|
||||
resetPage()
|
||||
query.value = Object.assign(isPaginate ? { page: page.value, limit: limit.value } : {})
|
||||
getList()
|
||||
}
|
||||
|
||||
// change page
|
||||
function changePage(p: number) {
|
||||
page.value = p
|
||||
// @ts-ignore
|
||||
query.value.page = p
|
||||
search()
|
||||
}
|
||||
|
||||
function resetPage() {
|
||||
page.value = 1
|
||||
}
|
||||
|
||||
// change limit
|
||||
function changeLimit(l: number) {
|
||||
limit.value = l
|
||||
resetPage()
|
||||
// @ts-ignore
|
||||
query.value.page = 1
|
||||
// @ts-ignore
|
||||
query.value.limit = l
|
||||
|
||||
search()
|
||||
}
|
||||
|
||||
// provider for paginate component
|
||||
provide('paginate', { page, limit, total, changePage, changeLimit })
|
||||
|
||||
return { data, query, search, reset, loading }
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
import { ref } from 'vue'
|
||||
import { t } from '/admin/support/helper'
|
||||
|
||||
export function useOpen() {
|
||||
const visible = ref<boolean>(false)
|
||||
const id = ref(null)
|
||||
const title = ref<string>('')
|
||||
|
||||
const open = (primary: any = null) => {
|
||||
title.value = primary ? t('system.edit') : t('system.add')
|
||||
id.value = primary
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const close = (func: Function) => {
|
||||
visible.value = false
|
||||
func()
|
||||
}
|
||||
|
||||
return { open, close, title, visible, id }
|
||||
}
|
@@ -1,26 +0,0 @@
|
||||
import http from '/admin/support/http'
|
||||
import { Ref, ref } from 'vue'
|
||||
import { isFunction } from '../../support/helper'
|
||||
|
||||
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
|
||||
if (fillData) {
|
||||
fillData.value = r.data.data
|
||||
|
||||
if (isFunction(afterShow.value)) {
|
||||
afterShow.value(fillData)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return { data, loading, afterShow }
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
import type { App } from 'vue'
|
||||
|
||||
import action from './permission/action'
|
||||
export function bootstrapDirectives(app: App): void {
|
||||
app.directive('action', action)
|
||||
}
|
@@ -1,33 +0,0 @@
|
||||
import { useUserStore } from '/admin/stores/modules/user'
|
||||
import { MenuType } from '/admin/enum/app'
|
||||
function checkAction(el: any, action: any) {
|
||||
if (action.value && typeof action.value === 'string') {
|
||||
const userStore = useUserStore()
|
||||
const permissions = userStore.getPermissions
|
||||
|
||||
action = action.value.replace('@', '.').toLowerCase()
|
||||
const hasAction = permissions?.some(permission => {
|
||||
if (permission.type === MenuType.Button_Type) {
|
||||
const a: string = permission.module + '.' + permission.permission_mark.replace('@', '.')
|
||||
return action === a.toLowerCase()
|
||||
}
|
||||
})
|
||||
|
||||
if (!hasAction) {
|
||||
// el.style.display = 'none'
|
||||
el.parentNode && el.parentNode.removeChild(el)
|
||||
}
|
||||
} else {
|
||||
throw new Error(`need action! Like v-action="module.controller.action"`)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
mounted(el: any, binding: any) {
|
||||
checkAction(el, binding)
|
||||
},
|
||||
|
||||
updated(el: any, binding: any) {
|
||||
checkAction(el, binding)
|
||||
},
|
||||
}
|
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* 服务端返回码
|
||||
*/
|
||||
export const enum Code {
|
||||
SUCCESS = 10000, // 成功
|
||||
LOST_LOGIN = 10001, // 登录失效
|
||||
VALIDATE_FAILED = 10002, // 验证错误
|
||||
PERMISSION_FORBIDDEN = 10003, // 权限禁止
|
||||
LOGIN_FAILED = 10004, // 登录失败
|
||||
FAILED = 10005, // 操作失败
|
||||
LOGIN_EXPIRED = 10006, // 登录失效
|
||||
LOGIN_BLACKLIST = 10007, // 黑名单
|
||||
USER_FORBIDDEN = 10008, // 账户被禁
|
||||
WECHAT_RESPONSE_ERROR = 40000,
|
||||
}
|
||||
|
||||
/**
|
||||
* status
|
||||
*/
|
||||
export const enum Status {
|
||||
ENABLE = 1,
|
||||
DISABLE = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* 白名单页面
|
||||
*
|
||||
* 不需要权限认证
|
||||
*/
|
||||
export const enum WhiteListPage {
|
||||
LOGIN_PATH = '/login',
|
||||
|
||||
NOT_FOUND_PATH = '/404',
|
||||
}
|
||||
|
||||
/**
|
||||
* menu 类型
|
||||
*/
|
||||
export const enum MenuType {
|
||||
TOP_TYPE = 1,
|
||||
PAGE_TYPE = 2,
|
||||
Button_Type = 3,
|
||||
}
|
8
resources/admin/env.d.ts
vendored
8
resources/admin/env.d.ts
vendored
@@ -1,8 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
@@ -1,22 +0,0 @@
|
||||
import Cache from '/admin/support/cache'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import en from './languages/en'
|
||||
import zh from './languages/zh'
|
||||
import type { App } from 'vue'
|
||||
|
||||
const messages = {
|
||||
en,
|
||||
zh,
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: Cache.get('language') || 'zh',
|
||||
messages,
|
||||
globalInjection: true,
|
||||
})
|
||||
|
||||
export function bootstrapI18n(app: App): void {
|
||||
app.use(i18n)
|
||||
}
|
||||
|
||||
export default i18n
|
@@ -1,150 +0,0 @@
|
||||
const en = {
|
||||
system: {
|
||||
name: 'CatchAdmin Dashboard',
|
||||
chinese: 'Chinese',
|
||||
english: 'English',
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
warning: 'Warning',
|
||||
next: 'Next',
|
||||
prev: 'Prev',
|
||||
yes: 'Y',
|
||||
no: 'N',
|
||||
add: 'Add',
|
||||
finish: 'Finish',
|
||||
back: 'Back',
|
||||
update: 'Update',
|
||||
},
|
||||
|
||||
login: {
|
||||
email: 'Email',
|
||||
password: 'Password',
|
||||
sign_in: 'Sign In',
|
||||
welcome: 'Welcome Back👏',
|
||||
lost_password: 'lost password?',
|
||||
remember: 'Remember me',
|
||||
verify: {
|
||||
email: {
|
||||
required: 'Please input email first',
|
||||
invalid: 'Email address is invalid',
|
||||
},
|
||||
|
||||
password: {
|
||||
required: 'Please input password first',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
register: {
|
||||
sign_up: 'Sign Up',
|
||||
},
|
||||
|
||||
generate: {
|
||||
schema: {
|
||||
title: 'Create Schema',
|
||||
name: 'Schema Name',
|
||||
name_verify: 'please input schema name',
|
||||
engine: {
|
||||
name: 'Search Engine',
|
||||
verify: 'please select schema engine',
|
||||
placeholder: 'select schema engine',
|
||||
},
|
||||
default_field: {
|
||||
name: 'Default Field',
|
||||
created_at: 'Create time',
|
||||
updated_at: 'Update Time',
|
||||
creator: 'Creator',
|
||||
delete_at: 'SoftDelete',
|
||||
},
|
||||
comment: {
|
||||
name: 'Schema Comment',
|
||||
verify: 'please input schema comment',
|
||||
},
|
||||
|
||||
structure: {
|
||||
title: 'Create Schema Structure',
|
||||
field_name: {
|
||||
name: 'Field Name',
|
||||
verify: 'please input field name',
|
||||
},
|
||||
length: 'Length',
|
||||
type: {
|
||||
name: 'Field Type',
|
||||
placeholder: 'select field type',
|
||||
verify: 'please select field type',
|
||||
},
|
||||
form_label: 'Form Label',
|
||||
form_component: 'Component',
|
||||
list: 'List',
|
||||
form: 'Form',
|
||||
unique: 'Unique',
|
||||
search: 'Search',
|
||||
search_op: {
|
||||
name: 'Search Operate',
|
||||
placeholder: 'select search operate',
|
||||
},
|
||||
nullable: 'Nullable',
|
||||
default: 'Default',
|
||||
rules: {
|
||||
name: 'Verify Rules',
|
||||
placeholder: 'select verify rules',
|
||||
},
|
||||
operate: 'Operate',
|
||||
comment: 'Field Comment',
|
||||
},
|
||||
},
|
||||
code: {
|
||||
title: 'Code Gen',
|
||||
module: {
|
||||
name: 'module',
|
||||
placeholder: 'please select module',
|
||||
verify: 'please select module first',
|
||||
},
|
||||
controller: {
|
||||
name: 'Controller',
|
||||
placeholder: 'please input controller name',
|
||||
verify: 'please input Controller name first',
|
||||
},
|
||||
model: {
|
||||
name: 'Model',
|
||||
placeholder: 'please input model name',
|
||||
verify: 'please input model name first',
|
||||
},
|
||||
paginate: 'Paginate',
|
||||
},
|
||||
},
|
||||
|
||||
module: {
|
||||
create: 'Create Module',
|
||||
update: 'Update Module',
|
||||
form: {
|
||||
name: {
|
||||
title: 'Module Name',
|
||||
required: 'module name required',
|
||||
},
|
||||
|
||||
path: {
|
||||
title: 'Module Path',
|
||||
required: 'module Path required',
|
||||
},
|
||||
|
||||
desc: {
|
||||
title: 'Description',
|
||||
},
|
||||
|
||||
keywords: {
|
||||
title: 'Keywords',
|
||||
},
|
||||
|
||||
dirs: {
|
||||
title: 'Default Dirs',
|
||||
Controller: 'Controller',
|
||||
Model: 'Model',
|
||||
Database: 'Database',
|
||||
Request: 'Request',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default en
|
@@ -1,155 +0,0 @@
|
||||
const zh = {
|
||||
system: {
|
||||
name: 'CatchAdmin 管理系统',
|
||||
chinese: '中文',
|
||||
english: '英文',
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
warning: '警告',
|
||||
next: '下一步',
|
||||
prev: '上一步',
|
||||
yes: '是',
|
||||
no: '否',
|
||||
add: '新增',
|
||||
edit: '编辑',
|
||||
finish: '完成',
|
||||
back: '返回',
|
||||
update: '更新',
|
||||
},
|
||||
|
||||
login: {
|
||||
email: '邮箱',
|
||||
password: '密码',
|
||||
sign_in: '登录',
|
||||
welcome: '👏欢迎回来',
|
||||
lost_password: '忘记密码?',
|
||||
remember: '记住我',
|
||||
verify: {
|
||||
email: {
|
||||
required: '请先输入邮箱',
|
||||
invalid: '邮箱地址无效',
|
||||
},
|
||||
|
||||
password: {
|
||||
required: '请先输入密码',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
register: {
|
||||
sign_up: '注册',
|
||||
},
|
||||
generate: {
|
||||
schema: {
|
||||
title: '创建数据表',
|
||||
name: '表名称',
|
||||
name_verify: '请输入表名称',
|
||||
engine: {
|
||||
name: '表引擎',
|
||||
verify: '请选择表引擎',
|
||||
placeholder: '选择表引擎',
|
||||
},
|
||||
default_field: {
|
||||
name: '默认字段',
|
||||
created_at: '创建时间',
|
||||
updated_at: '更新时间',
|
||||
creator: '创建人',
|
||||
delete_at: '软删除',
|
||||
},
|
||||
comment: {
|
||||
name: '表注释',
|
||||
verify: '请填写表注释/说明',
|
||||
},
|
||||
|
||||
structure: {
|
||||
title: '创建数据结构',
|
||||
field_name: {
|
||||
name: '字段名称',
|
||||
verify: '请填写字段名称',
|
||||
},
|
||||
length: '长度',
|
||||
type: {
|
||||
name: '类型',
|
||||
placeholder: '选择字段类型',
|
||||
verify: '请先选择字段类型',
|
||||
},
|
||||
form_label: '表单 Label',
|
||||
form_component: '表单组件',
|
||||
list: '列表',
|
||||
form: '表单',
|
||||
unique: '唯一',
|
||||
search: '查询',
|
||||
search_op: {
|
||||
name: '搜索操作符',
|
||||
placeholder: '选择搜索操作符',
|
||||
},
|
||||
nullable: 'nullable',
|
||||
default: '默认值',
|
||||
rules: {
|
||||
name: '验证规则',
|
||||
placeholder: '选择验证规则',
|
||||
},
|
||||
operate: '操作',
|
||||
comment: '字段注释',
|
||||
},
|
||||
},
|
||||
code: {
|
||||
title: '生成代码',
|
||||
module: {
|
||||
name: '模块',
|
||||
placeholder: '请选择模块',
|
||||
verify: '请选择模块',
|
||||
},
|
||||
controller: {
|
||||
name: '控制器',
|
||||
placeholder: '请输入控制器名称',
|
||||
verify: '请输入控制器名称',
|
||||
},
|
||||
model: {
|
||||
name: '模型',
|
||||
placeholder: '请输入模型名称',
|
||||
verify: '请输入模型名称',
|
||||
},
|
||||
paginate: '分页',
|
||||
menu: {
|
||||
name: '菜单名称',
|
||||
placeholder: '请输入菜单名称',
|
||||
verify: '请输入菜单名称',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
module: {
|
||||
create: '创建模块',
|
||||
update: '更新模块',
|
||||
form: {
|
||||
name: {
|
||||
title: '模块名称',
|
||||
required: '请输入模块名称',
|
||||
},
|
||||
|
||||
path: {
|
||||
title: '模块目录',
|
||||
required: '请输入模块目录',
|
||||
},
|
||||
|
||||
desc: {
|
||||
title: '模块描述',
|
||||
},
|
||||
|
||||
keywords: {
|
||||
title: '模块关键字',
|
||||
},
|
||||
|
||||
dirs: {
|
||||
title: '默认目录',
|
||||
Controller: 'Controller 目录',
|
||||
Model: 'Model 目录',
|
||||
Database: 'Database 目录',
|
||||
Request: 'Request 目录',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default zh
|
@@ -1,124 +0,0 @@
|
||||
<script lang="ts">
|
||||
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'
|
||||
import { useUserStore } from '/admin/stores/modules/user'
|
||||
import { Menu } from '/admin/types/Menu'
|
||||
|
||||
/**
|
||||
* 递归渲染 Menu 节点
|
||||
*/
|
||||
function getVNodes(menus: Menu[] | undefined, _subMenuClass: string | undefined): VNode[] {
|
||||
const vnodes: VNode[] = []
|
||||
menus?.forEach(menu => {
|
||||
if (!menu.meta?.hidden) {
|
||||
let vnode: VNode
|
||||
const len = menu.children?.length
|
||||
if (len) {
|
||||
vnode = h(
|
||||
MenuItem,
|
||||
{
|
||||
subMenuClass: _subMenuClass,
|
||||
menu,
|
||||
},
|
||||
{
|
||||
default: () => getVNodes(menu.children, 'children-menu'),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
vnode = h(MenuItem, {
|
||||
subMenuClass: _subMenuClass,
|
||||
menu,
|
||||
})
|
||||
}
|
||||
vnodes.push(vnode)
|
||||
}
|
||||
})
|
||||
|
||||
return vnodes
|
||||
}
|
||||
|
||||
/**
|
||||
* filter menus
|
||||
*
|
||||
* @param menus
|
||||
*/
|
||||
function filterMenus(menus: Menu[] | undefined): Menu[] {
|
||||
const newMenus: Menu[] = []
|
||||
|
||||
menus?.forEach(m => {
|
||||
if (m.meta?.hidden) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isHasOnlyChild(m) && m.children?.length) {
|
||||
newMenus.push(
|
||||
Object.assign({
|
||||
path: m.children[0].path,
|
||||
meta: m.children[0].meta,
|
||||
name: m.name,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
newMenus.push(m)
|
||||
}
|
||||
})
|
||||
|
||||
return newMenus
|
||||
}
|
||||
|
||||
/**
|
||||
* is has only child
|
||||
*
|
||||
* @param menu
|
||||
*/
|
||||
function isHasOnlyChild(menu: Menu): boolean {
|
||||
if (menu.children === undefined) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (menu.children.length > 1 || !menu.children.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (menu.children[0].children?.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
subMenuClass: {
|
||||
type: String,
|
||||
require: true,
|
||||
},
|
||||
menuClass: {
|
||||
type: String,
|
||||
require: true,
|
||||
},
|
||||
},
|
||||
setup(props, ctx) {
|
||||
const permissionsStore = usePermissionsStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 后端的 permissions 返回 undefined,则认为该后端无权限系统
|
||||
const permissions = userStore.getPermissions === undefined ? [] : userStore.getPermissions
|
||||
|
||||
const vnodes = getVNodes(filterMenus(permissionsStore.getMenusFrom(permissions, true)), props.subMenuClass)
|
||||
return () => {
|
||||
return h(
|
||||
menus,
|
||||
{
|
||||
class: 'border-none side-menu ' + props.menuClass,
|
||||
},
|
||||
{
|
||||
default: () => vnodes,
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
@@ -1,67 +0,0 @@
|
||||
<template>
|
||||
<el-sub-menu :index="menu?.path" :class="subMenuClass" v-if="menu?.children?.length">
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<Icon :name="menu?.meta?.icon" v-if="menu?.meta?.icon" class="text-sm" />
|
||||
</el-icon>
|
||||
<span>{{ menu?.meta?.title }}</span>
|
||||
</template>
|
||||
<slot />
|
||||
</el-sub-menu>
|
||||
|
||||
<el-menu-item v-else class="ct-menu-item" :index="menu?.path" @click="isMiniScreen() && store.changeExpaned()">
|
||||
<el-icon>
|
||||
<Icon :name="menu?.meta?.icon" v-if="menu?.meta?.icon" class="text-sm" />
|
||||
</el-icon>
|
||||
<span v-if="menu?.path.indexOf('https://') !== -1 || menu?.path.indexOf('http://') !== -1">
|
||||
<span @click="openUrl(menu?.path as string)">{{ menu?.meta?.title }}</span>
|
||||
</span>
|
||||
<span v-else>{{ menu?.meta?.title }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Menu } from '/admin/types/Menu'
|
||||
import { PropType } from 'vue'
|
||||
import { useAppStore } from '/admin/stores/modules/app'
|
||||
import { isMiniScreen } from '/admin/support/helper'
|
||||
|
||||
const store = useAppStore()
|
||||
|
||||
defineProps({
|
||||
subMenuClass: {
|
||||
type: String,
|
||||
require: true,
|
||||
default: '',
|
||||
},
|
||||
|
||||
menu: {
|
||||
type: Object as PropType<Menu>,
|
||||
require: true,
|
||||
},
|
||||
})
|
||||
|
||||
const openUrl = (path: string) => {
|
||||
const start = path.indexOf('https://') || path.indexOf('http://')
|
||||
window.open(path.substring(start))
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-menu) {
|
||||
background-color: var(--sider-sub-menu-bg-color);
|
||||
}
|
||||
|
||||
.ct-menu-item:hover {
|
||||
background-color: var(--sider-sub-menu-hover-bg-color) !important;
|
||||
}
|
||||
|
||||
:deep(.children-menu .el-sub-menu__title) {
|
||||
background-color: var(--sider-sub-menu-bg-color) !important;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item-group__title) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
@@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<div class="block sm:hidden z-40 w-screen h-full absolute mask-bg left-0 top-0" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mask-bg {
|
||||
background-color: #00000080;
|
||||
}
|
||||
</style>
|
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<el-menu
|
||||
:default-active="appStore.getActiveMenu"
|
||||
background-color="var(--sider-menu-bg-color)"
|
||||
active-text-color="var(--sider-ment-active-text-color)"
|
||||
text-color="var(--sider-menu-text-color)"
|
||||
:collapse="!appStore.isExpand"
|
||||
:collapse-transition="false"
|
||||
:router="true"
|
||||
:unique-opened="true"
|
||||
>
|
||||
<slot />
|
||||
</el-menu>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore } from '/admin/stores/modules/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
:deep(.is-active) {
|
||||
background-color: var(--side-active-menu-bg-color) !important;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu__title) {
|
||||
padding-left: calc(calc(var(--el-menu-base-level-padding) + var(--el-menu-level) * var(--el-menu-level-padding)));
|
||||
|
||||
color: var(--sider-menu-text-color);
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu) {
|
||||
color: var(--sider-sub-menu-bg-color);
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu__title:hover) {
|
||||
background-color: var(--sider-menu-bg-color);
|
||||
}
|
||||
|
||||
:deep(.el-menu--popup .el-menu-item:hover) {
|
||||
background-color: var(--sider-menu-bg-color) !important;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item:hover) {
|
||||
background-color: var(--sider-sub-menu-hover-bg-color) !important;
|
||||
border-right: 3px solid;
|
||||
border-right-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
:deep(.el-menu-item.is-active) {
|
||||
border-right: 3px solid;
|
||||
border-right-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
height: 50px !important;
|
||||
}
|
||||
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user