60 Commits

Author SHA1 Message Date
JaguarJack
fc5312692f up to 0.4.2 2025-01-18 09:19:49 +08:00
JaguarJack
86bfcea374 remove 2025-01-16 21:20:07 +08:00
JaguarJack
0218207848 up to 0.4.1 2025-01-16 18:23:06 +08:00
JaguarJack
984c00fe6f feat:自定义响应 2025-01-16 18:19:12 +08:00
JaguarJack
c03213e7c3 feat: 支持已有表生成代码 2024-12-22 07:46:20 +08:00
JaguarJack
3867be14b2 up to 0.4.0 2024-12-20 08:40:46 +08:00
JaguarJack
08c3cc9a78 fix: 添加分页属性 2024-12-19 17:22:38 +08:00
JaguarJack
af7ac14d5d chore:添加软连接配置 2024-10-26 12:37:18 +08:00
JaguarJack
6ff2204368 chore 2024-08-12 12:02:33 +08:00
JaguarJack
a3c3948bc6 添加桌面端 2024-08-12 11:58:29 +08:00
JaguarJack
c1aa54bbce update version 2024-07-16 15:33:26 +08:00
JaguarJack
b53c617984 chore:修改模版 2024-07-06 14:54:32 +08:00
JaguarJack
592cae1e2f Merge branch 'server' of https://github.com/JaguarJack/catch-admin into server 2024-07-06 12:43:36 +08:00
JaguarJack
d24036d9f1 Merge pull request #70 from LTaooo/patch-2
make password hidden
2024-07-05 09:00:14 +08:00
Liu
e18b30e89a make password hidden 2024-07-04 20:53:06 +08:00
JaguarJack
bfe3221795 update readme 2024-06-21 08:08:24 +08:00
JaguarJack
f354da3878 fix: 修复 actions 生成 2024-06-14 17:17:17 +08:00
JaguarJack
3ac356a83f up to 0.3.7 2024-06-13 12:32:56 +08:00
JaguarJack
d1189f9aea fix: 修复条件错误 2024-06-11 15:00:07 +08:00
JaguarJack
c1e4275399 fix: 修复未选父级菜单导致数据错乱 2024-06-06 18:42:06 +08:00
JaguarJack
61aea21d5f update 2024-05-21 16:32:16 +08:00
JaguarJack
e96267ecc0 update readme 2024-04-26 13:34:34 +08:00
JaguarJack
c9c1ffa82b update readme 2024-04-26 12:59:39 +08:00
JaguarJack
2a134b7e3b update version 2024-04-26 11:32:17 +08:00
JaguarJack
fc2dc38223 添加用户管理 2024-04-26 09:23:20 +08:00
JaguarJack
8a4fd7f66f chore:修改前端表单模板 2024-04-26 09:23:19 +08:00
JaguarJack
a8c01de529 chore:修改初始化数据 2024-04-25 20:39:43 +08:00
JaguarJack
027535cd68 feat:修改前端组件生成 2024-04-25 20:34:00 +08:00
JaguarJack
e6eb130ac4 feat:修改前端生成目录 2024-04-25 20:31:43 +08:00
JaguarJack
0d387175e0 chore 2024-04-25 20:23:56 +08:00
JaguarJack
c25da3cfb2 remove vue 2024-04-25 09:20:13 +08:00
JaguarJack
5fe198a2b2 docs: readme 2024-04-25 08:57:34 +08:00
JaguarJack
dad0bf4444 feat:修改 catch form 组件默认 2024-04-24 22:12:08 +08:00
JaguarJack
72c68507e5 fix:修改菜单排序 2024-04-23 17:47:49 +08:00
JaguarJack
1d694a81c5 feat: 新增动态表单示例 2024-04-23 13:13:52 +08:00
JaguarJack
51be5c648b feat: 新增 upload hook 2024-04-23 13:12:36 +08:00
JaguarJack
bc59731083 feat:新增 form 组件 2024-04-23 13:12:02 +08:00
JaguarJack
eeb6fd4f41 feat:新增 table 组件 2024-04-23 13:11:49 +08:00
JaguarJack
3c4ebb86e7 chore:移除unsignedDecimal类型 2024-04-22 17:36:26 +08:00
JaguarJack
dede7b0ba0 fix:字段无法删除 2024-04-22 17:24:51 +08:00
JaguarJack
d995a8ce0d feat:创建数据表字段,支持拖拽 2024-04-21 13:46:53 +08:00
JaguarJack
898ce1305d fix:修复 Laravel11 获取表栏目 2024-04-15 09:17:33 +08:00
JaguarJack
7362bdd70f chore: update version 2024-04-15 09:05:24 +08:00
JaguarJack
4e104bfd47 chore: update version 2024-04-15 09:04:36 +08:00
JaguarJack
4700990507 chore: update version 2024-04-15 09:02:51 +08:00
JaguarJack
808dd7118d chore: update version 2024-04-15 09:00:17 +08:00
JaguarJack
0f2c2c644f fix:vue router name重复 2024-04-02 17:50:16 +08:00
JaguarJack
118fc1aaab fix:vue router name重复 2024-04-02 17:48:07 +08:00
JaguarJack
6de3edd4fc fix:修复选项卡删除逻辑 2024-04-02 11:01:32 +08:00
JaguarJack
de31bf23cd chore:修复 ElementPlus Select 组件宽度 2024-04-02 10:48:54 +08:00
JaguarJack
817e8ea64d chore:修复菜单唯一key 2024-04-02 10:48:10 +08:00
JaguarJack
8d97ff8867 fix:回退 element plus 版本 2024-04-02 09:20:23 +08:00
JaguarJack
490c573e61 chore 2024-03-28 08:11:33 +08:00
JaguarJack
4ffd3eed52 readme 2024-03-23 21:26:10 +08:00
JaguarJack
d47ccb586f update version 2024-03-23 17:21:52 +08:00
JaguarJack
b81b9b66c8 feat:新增 tagview 导航页 2024-03-23 17:21:39 +08:00
JaguarJack
c3eb2443b6 chore:修改分页错误 2024-03-23 17:21:03 +08:00
JaguarJack
4696461d72 chore:修改 radio api 接口 2024-03-23 17:20:00 +08:00
JaguarJack
aabaf99c1b fix:权限列表错误 2024-03-22 16:17:51 +08:00
JaguarJack
6f28c25a30 chore:调整Dialog内容样式 2024-03-14 10:27:18 +08:00
215 changed files with 318 additions and 10949 deletions

9
.gitignore vendored
View File

@@ -1,5 +1,3 @@
/node_modules
/public/build
/public/hot /public/hot
/public/storage /public/storage
/storage/*.key /storage/*.key
@@ -14,13 +12,8 @@ nohup.out
.php-cs-fixer.cache .php-cs-fixer.cache
Homestead.json Homestead.json
Homestead.yaml Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
yarn.lock
composer.lock composer.lock
/.fleet /.fleet
/.idea /.idea
/.vscode /.vscode
components.d.ts /web
auto-imports.d.ts

View File

@@ -1,10 +0,0 @@
{
"semi": false,
"printWidth": 200,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"arrowParens": "avoid",
"trailingComma": "all",
"bracketSpacing": true
}

View File

@@ -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`是一款基于[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) [中文](./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) [专业版本官方地址](https://license.catchadmin.com)
@@ -12,13 +26,14 @@
我深信,付费后台管理系统将为您带来更多的价值和便利,帮助您提升工作效率 我深信,付费后台管理系统将为您带来更多的价值和便利,帮助您提升工作效率
## 桌面端(付费)
如果需要桌面端后台,使用 `Electron` 技术栈。可以联系微信咨询
<img src="wechat.png" width="200"/>
## ⚠Thinkphp 用户注意 ## ⚠Thinkphp 用户注意
由于新版本使用 `Laravel` 开发,所以请使用 `thinkphp` 分支或者 tag2.6.2thinkphp 版本已经非常稳定了。 由于新版本使用 `Laravel` 开发,所以请使用 `thinkphp` 分支或者 tag2.6.2thinkphp 版本已经非常稳定了。
## 为什么是 Laravel
`V2` 版本使用`Thinkphp`,但从其社区来看,从我个人角度来看开发组的心思已经不在维护框架上,因为据观察,每一次小版本发布都会引发一些小问题,虽然不大,但给人一种不够稳定的感觉,所以思索再三,使用 `Laravel``Laravel` 社区非常繁荣,他们每周都会发布新版本,以及围绕`Laravel`构建的生态也非常完善,有 `Horizon` 队列管理工具, `Telescope` 调试工具,`Octane`(基于 `Swoole``RoadRunner` 提高性能)等等一系列的工具,而且都是免费的。
## 功能 ## 功能
- [x] 用户管理 后台用户管理 - [x] 用户管理 后台用户管理
- [x] 部门管理 配置公司的部门结构,支持树形结构 - [x] 部门管理 配置公司的部门结构,支持树形结构
@@ -37,10 +52,7 @@
- 加入 Q 群 `302266230` 暗号 `catchadmin` - 加入 Q 群 `302266230` 暗号 `catchadmin`
- 加微信入群,新建🆕 - 加微信入群,新建🆕
<img src="wechat.png" width="300"/> <img src="wechat.png" width="200"/>
## 额外模块
- [CMS 模块](https://github.com/catch-admin/cms)
## 项目地址 ## 项目地址
- [github catchadmin](https://github.com/jaguarjack/catch-admin) - [github catchadmin](https://github.com/jaguarjack/catch-admin)
@@ -87,7 +99,7 @@ composer cs-diff
- [Laravel](https://laravel.com) - [Laravel](https://laravel.com)
- [Vue](https://cn.vuejs.org/) - [Vue](https://cn.vuejs.org/)
- [ElementPlus](https://element-plus.org) - [ElementPlus](https://element-plus.org)
- [Docusaurus](https://docusaurus.com) - [VitePress](https://vitepress.dev/zh/)
- [JetBrains](https://www.jetbrains.com/) - [JetBrains](https://www.jetbrains.com/)

View File

@@ -11,19 +11,18 @@
], ],
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.1", "php": "^8.2",
"ext-pdo": "*", "ext-pdo": "*",
"ext-zip": "*", "ext-zip": "*",
"doctrine/dbal": "^3.4", "guzzlehttp/guzzle": "^7.8.1",
"guzzlehttp/guzzle": "^7.2", "laravel/framework": "^v11.36.1",
"laravel/framework": "^10.0", "laravel/tinker": "^v2.9.0",
"laravel/tinker": "^2.8", "catchadmin/core": "^0.4.2"
"catchadmin/core": "^0.2.7"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^v1.23.1",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.6.9",
"pestphp/pest": "^1.22" "pestphp/pest": "^v2.34.2"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

View File

@@ -71,6 +71,9 @@ return [
'links' => [ 'links' => [
public_path('storage') => storage_path('app/public'), public_path('storage') => storage_path('app/public'),
// 创建 storage 对应的软连接
public_path('uploads') => storage_path('uploads')
], ],
]; ];

View File

@@ -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

View File

@@ -2,7 +2,6 @@
namespace Modules\Common\Repository\Options; namespace Modules\Common\Repository\Options;
use Catch\CatchAdmin;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -14,26 +13,38 @@ class Components implements OptionInterface
protected array $components = [ protected array $components = [
[ [
'label' => 'layout', 'label' => 'layout',
'value' => '/admin/layout/index.vue' 'value' => '/layout/index.vue',
] ],
]; ];
public function get(): array public function get(): array
{ {
try { try {
$viewRootPath = config('catch.views_path');
if ($module = request()->get('module')) { 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) { foreach ($components as $component) {
$_component = Str::of($component) // 过滤非 vue 文件
->replace(CatchAdmin::moduleRootPath(), '') if ($component->getExtension() !== 'vue') {
continue;
}
$_component = Str::of($component->getPathname())
->replace($viewRootPath, '')
->explode(DIRECTORY_SEPARATOR); ->explode(DIRECTORY_SEPARATOR);
$_component->shift(2);
$_component->shift(1);
$this->components[] = [ $this->components[] = [
'label' => Str::of($_component->implode('/'))->replace('.vue', ''), 'label' => Str::of($_component->implode('/'))->replace('.vue', ''),
'value' => Str::of($component)->replace(CatchAdmin::moduleRootPath(), '')->prepend('/') 'value' => Str::of($component)->replace($viewRootPath, '')->prepend('/'),
]; ];
} }
} }

View File

@@ -0,0 +1,28 @@
<?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 = [];
$tablePrefix = DB::connection()->getTablePrefix();
foreach (Schema::getTables() as $table) {
$tableName = Str::of($table['name'])->remove($tablePrefix);
$options[] = [
'label' => $tableName . "\t\t\t\t" . $table['comment'],
'value' => $tableName,
];
}
return $options;
}
}

View File

@@ -6,8 +6,8 @@ use Catch\Base\CatchModel;
use Catch\Enums\Status; use Catch\Enums\Status;
use Exception; use Exception;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Modules\Develop\Support\Generate\Create\Schema; use Modules\Develop\Support\Generate\Create\Schema;
use Illuminate\Support\Facades\Schema as SchemaFacade;
class Schemas extends CatchModel class Schemas extends CatchModel
{ {
@@ -48,6 +48,18 @@ class Schemas extends CatchModel
*/ */
public function storeBy(array $data): bool 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']; $schema = $data['schema'];
$structures = $data['structures']; $structures = $data['structures'];
@@ -90,16 +102,13 @@ class Schemas extends CatchModel
{ {
$schema = parent::firstBy($id); $schema = parent::firstBy($id);
$columns = []; foreach (SchemaFacade::getColumns($schema->name) as $column) {
foreach (getTableColumns($schema->name) as $columnString) {
$column = DB::connection()->getDoctrineColumn(DB::connection()->getTablePrefix().$schema->name, $columnString);
$columns[] = [ $columns[] = [
'name' => $column->getName(), 'name' => $column['name'],
'type' => $column->getType()->getName(), 'type' => $column['type_name'],
'nullable' => ! $column->getNotnull(), 'nullable' => $column['nullable'],
'default' => $column->getDefault(), 'default' => $column['default'],
'comment' => $column->getComment() 'comment' => $column['comment'],
]; ];
} }

View File

@@ -95,8 +95,10 @@ class FrontForm extends Creator
*/ */
public function getFile(): string public function getFile(): string
{ {
$path = config('catch.views_path').lcfirst($this->module).DIRECTORY_SEPARATOR;
// TODO: Implement getFile() method. // 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';
} }
/** /**

View File

@@ -114,7 +114,9 @@ class FrontTable extends Creator
public function getFile(): string public function getFile(): string
{ {
// TODO: Implement getFile() method. // 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';
} }

View File

@@ -8,12 +8,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useCreate } from '/admin/composables/curd/useCreate' import { useCreate } from '@/composables/curd/useCreate'
import { useShow } from '/admin/composables/curd/useShow' import { useShow } from '@/composables/curd/useShow'
import { onMounted } from 'vue' import { onMounted } from 'vue'
const props = defineProps({ const props = defineProps({
primary: String | Number, primary: [String, Number],
api: String, api: String,
}) })

View File

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

View File

@@ -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>

View File

@@ -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: [],
}),
)
}
})
},
},
})

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
},
},
})

View File

@@ -28,11 +28,13 @@ class PermissionsController extends Controller
public function index(Request $request): mixed public function index(Request $request): mixed
{ {
if ($request->get('from') == 'role') { 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 $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(); })->getList();
} }

View File

@@ -6,7 +6,9 @@ namespace Modules\Permissions\Http\Controllers;
use Catch\Base\CatchController as Controller; use Catch\Base\CatchController as Controller;
use Catch\Exceptions\FailedException; use Catch\Exceptions\FailedException;
use Catch\Support\ResponseBuilder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Exceptions\ReportableHandler;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Modules\Permissions\Enums\DataRange; use Modules\Permissions\Enums\DataRange;
use Modules\Permissions\Models\Roles; use Modules\Permissions\Models\Roles;

View File

@@ -7,6 +7,7 @@ namespace Modules\Permissions\Models;
use Catch\Base\CatchModel as Model; use Catch\Base\CatchModel as Model;
use Catch\CatchAdmin; use Catch\CatchAdmin;
use Catch\Enums\Status; use Catch\Enums\Status;
use Catch\Exceptions\FailedException;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -189,6 +190,10 @@ class Permissions extends Model
return true; return true;
} }
if ($data['type'] != MenuType::Top->value() && ! $data['parent_id']) {
throw new FailedException('请选择父级菜单');
}
$model = $this->fill($data); $model = $this->fill($data);
if ($model->isAction()) { if ($model->isAction()) {
@@ -240,6 +245,10 @@ class Permissions extends Model
*/ */
public function updateBy($id, array $data): mixed public function updateBy($id, array $data): mixed
{ {
if ($data['type'] != MenuType::Top->value() && ! $data['parent_id']) {
throw new FailedException('请选择父级菜单');
}
$model = $this->fill($data); $model = $this->fill($data);
if ($model->isAction()) { if ($model->isAction()) {

View File

@@ -30,7 +30,7 @@ return new class extends Seeder
'icon' => 'arrow-down-on-square-stack', 'icon' => 'arrow-down-on-square-stack',
'module' => 'permissions', 'module' => 'permissions',
'permission_mark' => '', 'permission_mark' => '',
'component' => '/admin/layout/index.vue', 'component' => '/layout/index.vue',
'redirect' => NULL, 'redirect' => NULL,
'keepalive' => 1, 'keepalive' => 1,
'type' => 1, 'type' => 1,
@@ -51,7 +51,7 @@ return new class extends Seeder
'icon' => 'arrow-left-circle', 'icon' => 'arrow-left-circle',
'module' => 'permissions', 'module' => 'permissions',
'permission_mark' => 'Roles', 'permission_mark' => 'Roles',
'component' => '/Permissions/views/roles/index.vue', 'component' => '/permissions/roles/index.vue',
'redirect' => NULL, 'redirect' => NULL,
'keepalive' => 1, 'keepalive' => 1,
'type' => 2, 'type' => 2,
@@ -174,7 +174,7 @@ return new class extends Seeder
'icon' => 'finger-print', 'icon' => 'finger-print',
'module' => 'permissions', 'module' => 'permissions',
'permission_mark' => 'Permissions', 'permission_mark' => 'Permissions',
'component' => '/Permissions/views/permissions/index.vue', 'component' => '/permissions/permissions/index.vue',
'redirect' => NULL, 'redirect' => NULL,
'keepalive' => 1, 'keepalive' => 1,
'type' => 2, 'type' => 2,
@@ -317,7 +317,7 @@ return new class extends Seeder
'icon' => 'globe-americas', 'icon' => 'globe-americas',
'module' => 'permissions', 'module' => 'permissions',
'permission_mark' => 'Jobs', 'permission_mark' => 'Jobs',
'component' => '/Permissions/views/jobs/index.vue', 'component' => '/permissions/jobs/index.vue',
'redirect' => NULL, 'redirect' => NULL,
'keepalive' => 1, 'keepalive' => 1,
'type' => 2, 'type' => 2,
@@ -460,7 +460,7 @@ return new class extends Seeder
'icon' => 'table-cells', 'icon' => 'table-cells',
'module' => 'permissions', 'module' => 'permissions',
'permission_mark' => 'Departments', 'permission_mark' => 'Departments',
'component' => '/Permissions/views/departments/index.vue', 'component' => '/permissions/departments/index.vue',
'redirect' => NULL, 'redirect' => NULL,
'keepalive' => 1, 'keepalive' => 1,
'type' => 2, '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,
),
),
),
), ),
), ),
); );

Binary file not shown.

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -20,7 +20,7 @@ return new class extends Seeder
public function menus(): array public function menus(): array
{ {
return array ( return array (
0 => 0 =>
array ( array (
'id' => 96, 'id' => 96,
'parent_id' => 0, 'parent_id' => 0,
@@ -41,7 +41,7 @@ return new class extends Seeder
'updated_at' => 1683535826, 'updated_at' => 1683535826,
'deleted_at' => 0, 'deleted_at' => 0,
), ),
1 => 1 =>
array ( array (
'id' => 97, 'id' => 97,
'parent_id' => 96, 'parent_id' => 96,
@@ -50,7 +50,7 @@ return new class extends Seeder
'icon' => '', 'icon' => '',
'module' => 'system', 'module' => 'system',
'permission_mark' => 'dictionary', 'permission_mark' => 'dictionary',
'component' => '/System/views/dictionary/index.vue', 'component' => '/system/dictionary/index.vue',
'redirect' => '', 'redirect' => '',
'keepalive' => 1, 'keepalive' => 1,
'type' => 2, 'type' => 2,
@@ -62,7 +62,7 @@ return new class extends Seeder
'updated_at' => 1683535874, 'updated_at' => 1683535874,
'deleted_at' => 0, 'deleted_at' => 0,
), ),
2 => 2 =>
array ( array (
'id' => 103, 'id' => 103,
'parent_id' => 97, 'parent_id' => 97,
@@ -83,7 +83,7 @@ return new class extends Seeder
'updated_at' => 1683535980, 'updated_at' => 1683535980,
'deleted_at' => 0, 'deleted_at' => 0,
), ),
3 => 3 =>
array ( array (
'id' => 99, 'id' => 99,
'parent_id' => 97, 'parent_id' => 97,
@@ -104,7 +104,7 @@ return new class extends Seeder
'updated_at' => 1683535980, 'updated_at' => 1683535980,
'deleted_at' => 0, 'deleted_at' => 0,
), ),
4 => 4 =>
array ( array (
'id' => 101, 'id' => 101,
'parent_id' => 97, 'parent_id' => 97,
@@ -125,7 +125,7 @@ return new class extends Seeder
'updated_at' => 1683535980, 'updated_at' => 1683535980,
'deleted_at' => 0, 'deleted_at' => 0,
), ),
5 => 5 =>
array ( array (
'id' => 100, 'id' => 100,
'parent_id' => 97, 'parent_id' => 97,
@@ -146,7 +146,7 @@ return new class extends Seeder
'updated_at' => 1683535980, 'updated_at' => 1683535980,
'deleted_at' => 0, 'deleted_at' => 0,
), ),
6 => 6 =>
array ( array (
'id' => 102, 'id' => 102,
'parent_id' => 97, 'parent_id' => 97,
@@ -167,7 +167,7 @@ return new class extends Seeder
'updated_at' => 1683535980, 'updated_at' => 1683535980,
'deleted_at' => 0, 'deleted_at' => 0,
), ),
7 => 7 =>
array ( array (
'id' => 98, 'id' => 98,
'parent_id' => 96, 'parent_id' => 96,
@@ -176,7 +176,7 @@ return new class extends Seeder
'icon' => '', 'icon' => '',
'module' => 'system', 'module' => 'system',
'permission_mark' => 'dictionaryValues', 'permission_mark' => 'dictionaryValues',
'component' => '/System/views/dictionaryValues/index.vue', 'component' => '/system/dictionaryValues/index.vue',
'redirect' => '', 'redirect' => '',
'keepalive' => 2, 'keepalive' => 2,
'type' => 2, 'type' => 2,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -6,17 +6,17 @@ use Catch\CatchAdmin;
use Catch\Traits\DB\BaseOperate; use Catch\Traits\DB\BaseOperate;
use Catch\Traits\DB\ScopeTrait; use Catch\Traits\DB\ScopeTrait;
use Catch\Traits\DB\Trans; use Catch\Traits\DB\Trans;
use Catch\Traits\DB\WithAttributes;
use Illuminate\Contracts\Http\Kernel; use Illuminate\Contracts\Http\Kernel;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
class LogOperate extends Model class LogOperate extends Model
{ {
use BaseOperate, Trans, ScopeTrait; use BaseOperate, Trans, ScopeTrait, WithAttributes;
protected $table = 'log_operate'; protected $table = 'log_operate';

View File

@@ -59,7 +59,7 @@ trait UserRelations
/* @var Permissions $permissionsModel */ /* @var Permissions $permissionsModel */
$permissionsModel = app($this->getPermissionsModel()); $permissionsModel = app($this->getPermissionsModel());
if ($this->isSuperAdmin()) { if ($this->isSuperAdmin()) {
$permissions = $permissionsModel->get(); $permissions = $permissionsModel->orderByDesc('sort')->get();
} else { } else {
$permissionIds = Collection::make(); $permissionIds = Collection::make();
$this->roles()->with('permissions')->get() $this->roles()->with('permissions')->get()
@@ -67,7 +67,7 @@ trait UserRelations
$permissionIds = $permissionIds->concat($role->permissions?->pluck('id')); $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()))); $this->setAttribute('permissions', $permissions->each(fn ($permission) => $permission->setAttribute('hidden', $permission->isHidden())));

View File

@@ -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' '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[] * @var array|string[]
*/ */

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"
}
}

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}

View File

@@ -1,3 +0,0 @@
<template>
<router-view />
</template>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 }
}

View File

@@ -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 }
}

View File

@@ -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 }
}

View File

@@ -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 }
}

View File

@@ -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 }
}

View File

@@ -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 }
}

View File

@@ -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 }
}

View File

@@ -1,6 +0,0 @@
import type { App } from 'vue'
import action from './permission/action'
export function bootstrapDirectives(app: App): void {
app.directive('action', action)
}

View File

@@ -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.replaceAll('@', '.')
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)
},
}

View File

@@ -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,
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,33 +0,0 @@
<template>
<div :class="'w-full h-screen flex flex-col transition-spacing duration-300 ease-linear overflow-hidden ' + mlClass">
<!-- Header -->
<Header />
<!-- Tag view -->
<!--<div class=""></div>-->
<!-- Container -->
<div class="p-1 sm:p-3 max-w-full h-screen overflow-auto sm:overflow-x-hidden">
<div class="min-h-[calc(100vh-8rem)]">
<router-view />
</div>
<div class="w-full text-center text-gray-400 h-4 leading-10">
<el-link href="https://catchadmin.com/" target="_blank">CatchAdmin 管理系统 </el-link> @copyright 2018 ~ {{ year }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useAppStore } from '/admin/stores/modules/app'
const appStore = useAppStore()
const mlClass = computed(() => {
return appStore.isExpand ? 'ml-0 sm:ml-56' : 'ml-0 sm:ml-16'
})
const year = computed(() => {
const date = new Date()
return date.getFullYear()
})
</script>

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