├── public ├── .gitignore ├── robots.txt ├── favicon.ico ├── images │ ├── avatar.gif │ └── avatar.png ├── .htaccess └── index.php ├── database ├── .gitignore ├── seeders │ ├── DatabaseSeeder.php │ └── UsersTableSeeder.php ├── migrations │ ├── 2014_10_12_100000_create_password_resets_table.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2022_05_20_092434_create_logs_table.php │ ├── 2022_05_24_145840_alter_uni_name_to_users_table.php │ ├── 2022_05_21_034423_create_requests_table.php │ ├── 2019_12_14_000001_create_personal_access_tokens_table.php │ ├── 2014_10_12_000000_create_users_table.php │ └── 2019_04_20_130706_setup_role_permissions.php └── factories │ └── UserFactory.php ├── bootstrap ├── cache │ └── .gitignore └── app.php ├── storage ├── logs │ └── .gitignore ├── app │ ├── public │ │ └── .gitignore │ └── .gitignore └── framework │ ├── testing │ └── .gitignore │ ├── views │ └── .gitignore │ ├── cache │ ├── data │ │ └── .gitignore │ └── .gitignore │ ├── sessions │ └── .gitignore │ └── .gitignore ├── resources ├── js │ ├── layout │ │ ├── index.js │ │ ├── components │ │ │ ├── Sidebar │ │ │ │ ├── index.js │ │ │ │ ├── Link.vue │ │ │ │ ├── Logo.vue │ │ │ │ ├── SidebarItem.vue │ │ │ │ └── Sidebar.vue │ │ │ ├── Hamburger │ │ │ │ ├── index.js │ │ │ │ └── Hamburger.vue │ │ │ ├── TagsView │ │ │ │ └── index.js │ │ │ ├── Breadcrumb │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── hook │ │ │ └── ResizeHandler.js │ │ └── Layout.vue │ ├── views │ │ ├── dashboard │ │ │ ├── Admin │ │ │ │ ├── index.js │ │ │ │ └── components │ │ │ │ │ ├── TransactionTable.vue │ │ │ │ │ ├── TodoList │ │ │ │ │ └── Todo.vue │ │ │ │ │ ├── PieChart.vue │ │ │ │ │ ├── BarChart.vue │ │ │ │ │ ├── RaddarChart.vue │ │ │ │ │ └── LineChart.vue │ │ │ ├── Editor │ │ │ │ ├── index.js │ │ │ │ └── Editor.vue │ │ │ ├── PanThumb │ │ │ │ └── index.js │ │ │ ├── GithubCorner │ │ │ │ ├── index.js │ │ │ │ └── GithubCorner.vue │ │ │ ├── index.vue │ │ │ └── TextHoverEffect │ │ │ │ └── Mallki.vue │ │ ├── App.vue │ │ ├── login │ │ │ └── AuthRedirect.vue │ │ ├── charts │ │ │ ├── Line.vue │ │ │ ├── MixChart.vue │ │ │ └── Keyboard.vue │ │ ├── users │ │ │ ├── SelfProfile.vue │ │ │ ├── UserProfile.vue │ │ │ └── components │ │ │ │ ├── UserBio.vue │ │ │ │ └── UserCard.vue │ │ └── i18n │ │ │ └── local.js │ ├── assets │ │ ├── login │ │ │ ├── logo.png │ │ │ ├── favicon.ico │ │ │ └── background.jpg │ │ ├── 401_images │ │ │ ├── 401.gif │ │ │ └── 401.jpg │ │ └── 404_images │ │ │ ├── 404.png │ │ │ └── 404_cloud.png │ ├── api │ │ ├── order.js │ │ ├── role.js │ │ ├── auth.js │ │ ├── user.js │ │ ├── article.js │ │ └── resource.js │ ├── directive │ │ ├── role │ │ │ ├── index.js │ │ │ └── role.js │ │ ├── waves │ │ │ ├── index.js │ │ │ ├── waves.css │ │ │ └── waves.js │ │ ├── el-drag-dialog │ │ │ ├── index.js │ │ │ └── drag.js │ │ ├── permission │ │ │ ├── index.js │ │ │ └── permission.js │ │ └── el-table │ │ │ ├── index.js │ │ │ └── adaptive.js │ ├── styles │ │ ├── variables-to-js.scss │ │ ├── variables.scss │ │ ├── transition.scss │ │ ├── index.scss │ │ └── elemenet-style-overflow.scss │ ├── utils │ │ ├── auth.js │ │ ├── get-page-title.js │ │ ├── i18n.js │ │ ├── role.js │ │ ├── permission.js │ │ ├── request.js │ │ ├── validate.js │ │ └── scroll-to.js │ ├── router │ │ ├── modules │ │ │ ├── error.js │ │ │ ├── charts.js │ │ │ └── admin.js │ │ └── index.js │ ├── components │ │ ├── ElSvgIcon.vue │ │ ├── Item │ │ │ ├── ElSvgItem.vue │ │ │ └── Item.jsx │ │ ├── Charts │ │ │ └── mixins │ │ │ │ └── resize.js │ │ ├── Icon │ │ │ └── Icon.vue │ │ ├── SizeSelect │ │ │ └── index.vue │ │ ├── Hamburger │ │ │ └── index.vue │ │ ├── ScreenFull │ │ │ └── index.vue │ │ ├── LangSelect │ │ │ └── index.vue │ │ ├── GithubCorner │ │ │ └── index.vue │ │ └── Breadcrumb │ │ │ └── index.vue │ ├── app.js │ ├── bootstrap.js │ ├── lang │ │ └── index.js │ ├── settings.js │ ├── store │ │ └── permission.js │ └── permission.js ├── lang │ └── en │ │ ├── pagination.php │ │ ├── auth.php │ │ └── passwords.php └── views │ └── index.blade.php ├── .prettierrc.js ├── app ├── Models │ ├── Log.php │ ├── Request.php │ ├── Model.php │ ├── Role.php │ ├── Permission.php │ └── Acl.php ├── Http │ ├── Controllers │ │ ├── Api │ │ │ ├── BaseController.php │ │ │ ├── LogController.php │ │ │ ├── RequestController.php │ │ │ ├── PermissionController.php │ │ │ ├── AuthController.php │ │ │ └── RoleController.php │ │ ├── Controller.php │ │ └── HomeController.php │ ├── Middleware │ │ ├── EncryptCookies.php │ │ ├── VerifyCsrfToken.php │ │ ├── PreventRequestsDuringMaintenance.php │ │ ├── TrimStrings.php │ │ ├── TrustHosts.php │ │ ├── Authenticate.php │ │ ├── ValidateSignature.php │ │ ├── TrustProxies.php │ │ ├── RedirectIfAuthenticated.php │ │ └── RequestLog.php │ └── Resources │ │ ├── PermissionResource.php │ │ ├── RoleResource.php │ │ └── UserResource.php ├── Providers │ ├── BroadcastServiceProvider.php │ ├── AuthServiceProvider.php │ ├── EventServiceProvider.php │ ├── AppServiceProvider.php │ └── RouteServiceProvider.php ├── Console │ └── Kernel.php ├── Exceptions │ └── Handler.php └── Jobs │ └── ProcessRequestLog.php ├── .prettierignore ├── babel.config.js ├── .gitattributes ├── tests ├── TestCase.php ├── Unit │ └── ExampleTest.php ├── Feature │ └── ExampleTest.php └── CreatesApplication.php ├── .postcssrc.js ├── .prettierrc ├── .styleci.yml ├── .gitignore ├── supervisor ├── conf.d │ ├── serve.conf │ └── laravel-vue-admin.conf └── supervisord.conf ├── .editorconfig ├── .babelrc ├── routes ├── channels.php ├── console.php ├── web.php └── api.php ├── cmd.sh ├── server.php ├── docker-compose.yml ├── Dockerfile ├── config ├── cors.php ├── services.php ├── content.php ├── view.php ├── hashing.php ├── broadcasting.php ├── filesystems.php └── sanctum.php ├── LICENSE ├── .env.example ├── .env.docker ├── phpunit.xml ├── .eslintrc-auto-import.json ├── tsconfig.json ├── package.json ├── artisan ├── .eslintrc.js ├── composer.json └── README.md /public/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumanwong/laravel-vue-admin/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /resources/js/layout/index.js: -------------------------------------------------------------------------------- 1 | import component from './Layout.vue' 2 | 3 | export default component 4 | -------------------------------------------------------------------------------- /public/images/avatar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumanwong/laravel-vue-admin/HEAD/public/images/avatar.gif -------------------------------------------------------------------------------- /public/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumanwong/laravel-vue-admin/HEAD/public/images/avatar.png -------------------------------------------------------------------------------- /resources/js/views/dashboard/Admin/index.js: -------------------------------------------------------------------------------- 1 | import component from './Admin.vue' 2 | export default component 3 | -------------------------------------------------------------------------------- /resources/js/layout/components/Sidebar/index.js: -------------------------------------------------------------------------------- 1 | import component from './Sidebar.vue' 2 | export default component 3 | -------------------------------------------------------------------------------- /resources/js/views/dashboard/Editor/index.js: -------------------------------------------------------------------------------- 1 | import component from './Editor.vue' 2 | export default component 3 | -------------------------------------------------------------------------------- /resources/js/views/dashboard/PanThumb/index.js: -------------------------------------------------------------------------------- 1 | import component from './PanThumb.vue' 2 | export default component 3 | -------------------------------------------------------------------------------- /resources/js/layout/components/Hamburger/index.js: -------------------------------------------------------------------------------- 1 | import component from './Hamburger.vue' 2 | export default component 3 | -------------------------------------------------------------------------------- /resources/js/layout/components/TagsView/index.js: -------------------------------------------------------------------------------- 1 | import component from './TagsView.vue' 2 | export default component 3 | -------------------------------------------------------------------------------- /resources/js/layout/components/Breadcrumb/index.js: -------------------------------------------------------------------------------- 1 | import component from './Breadcrumb.vue' 2 | export default component 3 | -------------------------------------------------------------------------------- /resources/js/views/dashboard/GithubCorner/index.js: -------------------------------------------------------------------------------- 1 | import component from './GithubCorner.vue' 2 | export default component 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | semi: true, 4 | tabWidth: 2, 5 | trailingComma: 'es5', 6 | } 7 | -------------------------------------------------------------------------------- /app/Models/Log.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | //change esm to require 3 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'] 4 | } 5 | -------------------------------------------------------------------------------- /app/Models/Request.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/js/directive/permission/index.js: -------------------------------------------------------------------------------- 1 | import permission from './permission'; 2 | 3 | const install = function(Vue) { 4 | Vue.directive('permission', permission); 5 | }; 6 | 7 | if (window.Vue) { 8 | window['permission'] = permission; 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | permission.install = install; 13 | export default permission; 14 | -------------------------------------------------------------------------------- /supervisor/conf.d/laravel-vue-admin.conf: -------------------------------------------------------------------------------- 1 | [program:laravel-vue-admin] 2 | process_name=%(program_name)s_%(process_num)02d 3 | command=php /var/www/artisan queue:work redis --timeout=360 --sleep=3 --tries=3 4 | autostart=true 5 | autorestart=true 6 | user=www-data 7 | numprocs=4 8 | redirect_stderr=true 9 | stdout_logfile=/var/www/storage/logs/worker.log 10 | stopwaitsecs=3600 -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "corejs": "3", 7 | "useBuiltIns": "entry" 8 | } 9 | ] 10 | ], 11 | "plugins": [ 12 | "@vue/babel-plugin-jsx", 13 | "babel-plugin-syntax-dynamic-import", 14 | "@babel/plugin-syntax-dynamic-import", 15 | "@babel/plugin-transform-runtime" 16 | ] 17 | } -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call(UsersTableSeeder::class); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/js/directive/el-table/index.js: -------------------------------------------------------------------------------- 1 | 2 | import adaptive from './adaptive'; 3 | 4 | const install = function(Vue) { 5 | Vue.directive('el-height-adaptive-table', adaptive); 6 | }; 7 | 8 | if (window.Vue) { 9 | window['el-height-adaptive-table'] = adaptive; 10 | Vue.use(install); // eslint-disable-line 11 | } 12 | 13 | adaptive.install = install; 14 | export default adaptive; 15 | -------------------------------------------------------------------------------- /app/Models/Model.php: -------------------------------------------------------------------------------- 1 | format('Y-m-d H:i:s'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /resources/js/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // 全局变量定义 2 | $menuText: #bfcbd9; 3 | $menuActiveText: #409eff; 4 | $subMenuActiveText: #f4f4f5; 5 | 6 | $menuBg: #304156; 7 | $menuHover: #263445; 8 | 9 | $subMenuBg: #1f2d3d; 10 | $subMenuHover: #001528; 11 | $sideBarWidth: 210px; 12 | 13 | //navbar 14 | $navBarHeight: 50px; 15 | //tagsView 16 | $tagViewHeight: 32px; 17 | //app-main padding 18 | $appMainPadding: 10px; 19 | -------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /resources/js/api/role.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import Resource from '@/api/resource'; 3 | 4 | class RoleResource extends Resource { 5 | constructor() { 6 | super('roles'); 7 | } 8 | 9 | permissions(id) { 10 | return request({ 11 | url: '/' + this.uri + '/' + id + '/permissions', 12 | method: 'get', 13 | }); 14 | } 15 | } 16 | 17 | export { RoleResource as default }; 18 | -------------------------------------------------------------------------------- /resources/js/views/login/AuthRedirect.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /resources/js/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie'; 2 | 3 | const TokenKey = 'token'; 4 | 5 | export function isLogged() { 6 | return !!Cookies.get(TokenKey); 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookies.set(TokenKey, token); 11 | } 12 | 13 | export function getToken() { 14 | return Cookies.get(TokenKey); 15 | } 16 | 17 | export function removeToken() { 18 | return Cookies.remove(TokenKey); 19 | } 20 | -------------------------------------------------------------------------------- /resources/js/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings'; 2 | import i18n from '@/lang'; 3 | 4 | const title = defaultSettings.title || 'Laravel Vue Admin'; 5 | 6 | export default function getPageTitle(key) { 7 | const hasKey = i18n.global.te(`route.${key}`); 8 | if (hasKey) { 9 | const pageName = i18n.global.t(`route.${key}`); 10 | return `${pageName} - ${title}`; 11 | } 12 | return `${title}`; 13 | } 14 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | get('/'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /resources/js/utils/i18n.js: -------------------------------------------------------------------------------- 1 | // translate router.meta.title, be used in breadcrumb sidebar tagsview 2 | import {useI18n} from 'vue-i18n' 3 | 4 | export default function () { 5 | const {t, te} = useI18n({useScope: 'global'}) 6 | const generateTitle = (title) => { 7 | let hasKey = te('route.' + title) 8 | if (hasKey) { 9 | return t('route.' + title) 10 | } 11 | return title 12 | } 13 | 14 | return { 15 | generateTitle 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Controllers/HomeController.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'current_password', 16 | 'password', 17 | 'password_confirmation', 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /resources/js/views/charts/Line.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /resources/js/views/charts/MixChart.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustHosts.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function hosts(): array 15 | { 16 | return [ 17 | $this->allSubdomainsOfApplicationUrl(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /resources/js/views/charts/Keyboard.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | expectsJson() ? null : route('login'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'name' => $this->name, 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /resources/js/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/Models/Role.php: -------------------------------------------------------------------------------- 1 | name === Acl::ROLE_ADMIN; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Http/Middleware/ValidateSignature.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 'fbclid', 16 | // 'utm_campaign', 17 | // 'utm_content', 18 | // 'utm_medium', 19 | // 'utm_source', 20 | // 'utm_term', 21 | ]; 22 | } 23 | -------------------------------------------------------------------------------- /resources/js/utils/role.js: -------------------------------------------------------------------------------- 1 | import {userStore} from '@/store/user'; 2 | 3 | /** 4 | * @param {Array} value 5 | * @returns {Boolean} 6 | * @example see @/views/permission/Directive.vue 7 | */ 8 | export default function checkRole(value) { 9 | if (value && value instanceof Array && value.length > 0) { 10 | const roles = userStore.roles; 11 | const requiredRoles = value; 12 | 13 | return roles.some(role => { 14 | return requiredRoles.includes(role); 15 | }); 16 | } else { 17 | console.error(`Need roles! Like v-role="['admin','editor']"`); 18 | return false; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/LogController.php: -------------------------------------------------------------------------------- 1 | where('user_id', $user->id)->orderBy('id', 'desc')->paginate(10); 18 | return responseSuccess($data); 19 | } 20 | } -------------------------------------------------------------------------------- /resources/js/api/auth.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export function login(data) { 4 | return request({ 5 | url: '/auth/login', 6 | method: 'post', 7 | data: data, 8 | }); 9 | } 10 | 11 | export function getInfo(token) { 12 | return request({ 13 | url: '/user', 14 | method: 'get', 15 | }); 16 | } 17 | 18 | export function logout() { 19 | return request({ 20 | url: '/auth/logout', 21 | method: 'post', 22 | }); 23 | } 24 | 25 | export function csrf() { 26 | return request({ 27 | url: '/sanctum/csrf-cookie', 28 | method: 'get', 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /app/Models/Permission.php: -------------------------------------------------------------------------------- 1 | where('name', '!=', Acl::PERMISSION_PERMISSION_MANAGE); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Http/Resources/RoleResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'name' => $this->name, 20 | 'permissions' => PermissionResource::collection($this->permissions), 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cmd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | 4 | ENV_FILE=".env" 5 | MYSQL_HOST=$(cat $ENV_FILE | grep "DB_HOST" | cut -d"=" -f2) 6 | MYSQL_PORT=$(cat $ENV_FILE | grep "DB_PORT" | cut -d"=" -f2) 7 | 8 | composer install 9 | 10 | echo "Wait for MySQL to be ready" 11 | while true; 12 | do 13 | nc -z $MYSQL_HOST $MYSQL_PORT && break 14 | sleep 1 15 | done 16 | 17 | MIGRATE_NOT_MIGRATED_STATUS=$(php artisan migrate:status | grep "not found" | wc -l) 18 | if [ $MIGRATE_NOT_MIGRATED_STATUS = "1" ]; 19 | then 20 | php artisan migrate --seed 21 | fi 22 | 23 | /etc/init.d/supervisor start 24 | 25 | npm install && npm run build 26 | 27 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | 'web'], function () { 17 | Route::get('', 'HomeController@index')->where('any', '.*'); 18 | }); 19 | -------------------------------------------------------------------------------- /resources/lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /server.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | $uri = urldecode( 11 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?? '' 12 | ); 13 | 14 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the 15 | // built-in PHP web server. This provides a convenient way to test a Laravel 16 | // application without having installed a "real" web server software here. 17 | if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { 18 | return false; 19 | } 20 | 21 | require_once __DIR__.'/public/index.php'; 22 | -------------------------------------------------------------------------------- /resources/js/directive/role/role.js: -------------------------------------------------------------------------------- 1 | import {userStore} from '@/store/user'; 2 | 3 | export default { 4 | inserted(el, binding, vnode) { 5 | const {value} = binding; 6 | const roles = userStore.roles; 7 | 8 | if (value && value instanceof Array && value.length > 0) { 9 | const requiredRoles = value; 10 | const hasRole = roles.some(role => { 11 | return requiredRoles.includes(role); 12 | }); 13 | 14 | if (!hasRole) { 15 | el.parentNode && el.parentNode.removeChild(el); 16 | } 17 | } else { 18 | throw new Error(`Roles are required! Example: v-role="['admin','editor']"`); 19 | } 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /resources/lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /resources/js/utils/permission.js: -------------------------------------------------------------------------------- 1 | import {userStore} from '@/store/user'; 2 | 3 | /** 4 | * @param {Array} value 5 | * @returns {Boolean} 6 | * @example see @/views/permission/Directive.vue 7 | */ 8 | export default function checkPermission(value) { 9 | const useUserStore = userStore() 10 | if (value && value instanceof Array && value.length > 0) { 11 | const permissions = useUserStore.permissions 12 | const requiredPermissions = value 13 | 14 | return permissions.some(permission => { 15 | return requiredPermissions.includes(permission) 16 | }) 17 | } else { 18 | console.error(`Need permissions! Like v-permission="['manage permission','edit article']"`) 19 | return false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $policies = [ 16 | // 'App\Models\Model' => 'App\Policies\ModelPolicy', 17 | ]; 18 | 19 | /** 20 | * Register any authentication / authorization services. 21 | * 22 | * @return void 23 | */ 24 | public function boot() 25 | { 26 | $this->registerPolicies(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /resources/js/directive/permission/permission.js: -------------------------------------------------------------------------------- 1 | import {userStore} from '@/store/user'; 2 | 3 | export default { 4 | inserted(el, binding, vnode) { 5 | const {value} = binding; 6 | const permissions = userStore.permissions; 7 | 8 | if (value && value instanceof Array && value.length > 0) { 9 | const requiredPermissions = value; 10 | const hasPermission = permissions.some(permission => { 11 | return requiredPermissions.includes(permission); 12 | }); 13 | 14 | if (!hasPermission) { 15 | el.parentNode && el.parentNode.removeChild(el); 16 | } 17 | } else { 18 | throw new Error(`Permissions are required! Example: v-permission="['manage user','manage permission']"`); 19 | } 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | |string|null 14 | */ 15 | protected $proxies; 16 | 17 | /** 18 | * The headers that should be used to detect proxies. 19 | * 20 | * @var int 21 | */ 22 | protected $headers = 23 | Request::HEADER_X_FORWARDED_FOR | 24 | Request::HEADER_X_FORWARDED_HOST | 25 | Request::HEADER_X_FORWARDED_PORT | 26 | Request::HEADER_X_FORWARDED_PROTO | 27 | Request::HEADER_X_FORWARDED_AWS_ELB; 28 | } 29 | -------------------------------------------------------------------------------- /resources/js/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import Resource from '@/api/resource'; 3 | 4 | class UserResource extends Resource { 5 | constructor() { 6 | super('users'); 7 | } 8 | 9 | permissions(id) { 10 | return request({ 11 | url: '/' + this.uri + '/' + id + '/permissions', 12 | method: 'get', 13 | }); 14 | } 15 | 16 | updatePermission(id, permissions) { 17 | return request({ 18 | url: '/' + this.uri + '/' + id + '/permissions', 19 | method: 'put', 20 | data: permissions, 21 | }); 22 | } 23 | 24 | logs(id, params) { 25 | return request({ 26 | url: '/' + this.uri + '/' + id + '/logs', 27 | method: 'get', 28 | params: params 29 | }); 30 | } 31 | } 32 | 33 | export { UserResource as default }; 34 | -------------------------------------------------------------------------------- /resources/js/api/article.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export function fetchList(query) { 4 | return request({ 5 | url: '/articles', 6 | method: 'get', 7 | params: query, 8 | }); 9 | } 10 | 11 | export function fetchArticle(id) { 12 | return request({ 13 | url: '/articles/' + id, 14 | method: 'get', 15 | }); 16 | } 17 | 18 | export function fetchPv(id) { 19 | return request({ 20 | url: '/articles/' + id + '/pageviews', 21 | method: 'get', 22 | }); 23 | } 24 | 25 | export function createArticle(data) { 26 | return request({ 27 | url: '/article/create', 28 | method: 'post', 29 | data, 30 | }); 31 | } 32 | 33 | export function updateArticle(data) { 34 | return request({ 35 | url: '/article/update', 36 | method: 'post', 37 | data, 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire')->hourly(); 19 | } 20 | 21 | /** 22 | * Register the commands for the application. 23 | * 24 | * @return void 25 | */ 26 | protected function commands() 27 | { 28 | $this->load(__DIR__.'/Commands'); 29 | 30 | require base_path('routes/console.php'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /resources/js/router/modules/error.js: -------------------------------------------------------------------------------- 1 | /** When your routing table is too long, you can split it into small modules**/ 2 | import Layout from '@/layout/Layout.vue' 3 | 4 | const errorRoutes = { 5 | path: '/error', 6 | component: Layout, 7 | redirect: 'noredirect', 8 | name: 'ErrorPages', 9 | meta: { 10 | title: 'errorPages', 11 | icon: '404', 12 | }, 13 | hidden: true, 14 | children: [ 15 | { 16 | path: '401', 17 | component: () => import('@/views/error-page/401.vue'), 18 | name: 'Page401', 19 | meta: { title: 'page401', noCache: true }, 20 | }, 21 | { 22 | path: '404', 23 | component: () => import('@/views/error-page/404.vue'), 24 | name: 'Page404', 25 | meta: { title: 'page404', noCache: true }, 26 | }, 27 | ], 28 | } 29 | 30 | export default errorRoutes 31 | -------------------------------------------------------------------------------- /resources/js/components/ElSvgIcon.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 29 | 30 | 39 | -------------------------------------------------------------------------------- /resources/lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset!', 17 | 'sent' => 'We have emailed your password reset link!', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_100000_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 18 | $table->string('token'); 19 | $table->timestamp('created_at')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('password_resets'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | laravel: 5 | build: 6 | context: . 7 | working_dir: /var/www 8 | container_name: laravel-vue-admin 9 | volumes: 10 | - ./:/var/www 11 | - .env.docker:/var/www/.env 12 | ports: 13 | - "8000:8000" 14 | 15 | depends_on: 16 | - mysql 17 | - redis 18 | 19 | redis: 20 | image: redis:7.0 21 | container_name: redis7.0 22 | restart: always 23 | ports: 24 | - "6379:6379" 25 | 26 | mysql: 27 | image: mysql:8.0.29 28 | container_name: mysql8.0.29 29 | ports: 30 | - "3306:3306" 31 | volumes: 32 | - ./docker/mysql:/var/lib/mysql 33 | environment: 34 | MYSQL_DATABASE: laravel-vue-admin 35 | MYSQL_USER: laravel-vue-admin 36 | MYSQL_PASSWORD: laravel-vue-admin 37 | MYSQL_ROOT_PASSWORD: laravel-vue-admin 38 | 39 | -------------------------------------------------------------------------------- /app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | > 16 | */ 17 | protected $listen = [ 18 | Registered::class => [ 19 | SendEmailVerificationNotification::class, 20 | ], 21 | ]; 22 | 23 | /** 24 | * Register any events for your application. 25 | * 26 | * @return void 27 | */ 28 | public function boot() 29 | { 30 | // 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | sql . ' ' . implode(', ', $query->bindings)); 32 | }); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 24 | return redirect(RouteServiceProvider::HOME); 25 | } 26 | } 27 | 28 | return $next($request); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Middleware/RequestLog.php: -------------------------------------------------------------------------------- 1 | method(), $request->getRequestUri(), Carbon::now()); 31 | return $next($request); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /resources/js/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /resources/js/components/Item/ElSvgItem.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{title}} 7 | 8 | 9 | 10 | 30 | 31 | 40 | -------------------------------------------------------------------------------- /resources/js/directive/waves/waves.css: -------------------------------------------------------------------------------- 1 | .waves-ripple { 2 | position: absolute; 3 | border-radius: 100%; 4 | background-color: rgba(0, 0, 0, 0.15); 5 | background-clip: padding-box; 6 | pointer-events: none; 7 | -webkit-user-select: none; 8 | -moz-user-select: none; 9 | -ms-user-select: none; 10 | user-select: none; 11 | -webkit-transform: scale(0); 12 | -ms-transform: scale(0); 13 | transform: scale(0); 14 | opacity: 1; 15 | } 16 | 17 | .waves-ripple.z-active { 18 | opacity: 0; 19 | -webkit-transform: scale(2); 20 | -ms-transform: scale(2); 21 | transform: scale(2); 22 | -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 23 | transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 24 | transition: opacity 1.2s ease-out, transform 0.6s ease-out; 25 | transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out; 26 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1-fpm 2 | 3 | WORKDIR /var/www 4 | 5 | # Update packages 6 | 7 | RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - \ 8 | && apt-get update \ 9 | && apt-get install -y supervisor nodejs netcat libmcrypt-dev libzip-dev libonig-dev libpng-dev libwebp-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libfreetype6-dev libbz2-dev git \ 10 | && apt-get clean && rm -rf /etc/supervisor/* 11 | 12 | # Install extensions 13 | RUN docker-php-ext-install pdo_mysql mbstring zip exif pcntl 14 | RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp 15 | RUN docker-php-ext-install gd 16 | RUN pecl install -o -f redis && rm -rf /tmp/pear && docker-php-ext-enable redis 17 | 18 | COPY . . 19 | COPY .env.docker .env 20 | COPY supervisor /etc/supervisor 21 | 22 | # Install composer 23 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 24 | 25 | CMD ["bash", "./cmd.sh"] 26 | 27 | -------------------------------------------------------------------------------- /resources/js/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // vue global transition css define 2 | /* fade */ 3 | .fade-enter-active, 4 | .fade-leave-active { 5 | transition: opacity 0.3s; 6 | } 7 | 8 | .fade-enter-from, 9 | .fade-leave-active { 10 | opacity: 0; 11 | } 12 | 13 | /* fade-transform AppMain*/ 14 | .fade-transform-leave-active, 15 | .fade-transform-enter-active { 16 | transition: all 0.3s; 17 | } 18 | 19 | .fade-transform-enter-from { 20 | opacity: 0; 21 | transform: translateX(-30px); 22 | } 23 | 24 | .fade-transform-leave-to { 25 | opacity: 0; 26 | transform: translateX(30px); 27 | } 28 | .fade-transform-active { 29 | position: absolute; 30 | } 31 | 32 | /* breadcrumb transition */ 33 | .breadcrumb-enter-active, 34 | .breadcrumb-leave-active { 35 | transition: all 0.3s; 36 | } 37 | 38 | .breadcrumb-enter-from, 39 | .breadcrumb-leave-active { 40 | opacity: 0; 41 | transform: translateX(20px); 42 | } 43 | 44 | .breadcrumb-leave-active { 45 | position: absolute; 46 | } 47 | -------------------------------------------------------------------------------- /resources/js/api/resource.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | /** 4 | * Simple RESTful resource class 5 | */ 6 | class Resource { 7 | constructor(uri) { 8 | this.uri = uri; 9 | } 10 | list(query) { 11 | return request({ 12 | url: '/' + this.uri, 13 | method: 'get', 14 | params: query, 15 | }); 16 | } 17 | get(id) { 18 | return request({ 19 | url: '/' + this.uri + '/' + id, 20 | method: 'get', 21 | }); 22 | } 23 | store(resource) { 24 | return request({ 25 | url: '/' + this.uri, 26 | method: 'post', 27 | data: resource, 28 | }); 29 | } 30 | update(id, resource) { 31 | return request({ 32 | url: '/' + this.uri + '/' + id, 33 | method: 'put', 34 | data: resource, 35 | }); 36 | } 37 | destroy(id) { 38 | return request({ 39 | url: '/' + this.uri + '/' + id, 40 | method: 'delete', 41 | }); 42 | } 43 | } 44 | 45 | export { Resource as default }; 46 | -------------------------------------------------------------------------------- /resources/js/components/Charts/mixins/resize.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '@/utils'; 2 | 3 | export default { 4 | data() { 5 | return { 6 | sidebarElm: null, 7 | }; 8 | }, 9 | mounted() { 10 | this.__resizeHandler = debounce(() => { 11 | if (this.chart) { 12 | this.chart.resize(); 13 | } 14 | }, 100); 15 | window.addEventListener('resize', this.__resizeHandler); 16 | 17 | this.sidebarElm = document.getElementsByClassName('sidebar-container')[0]; 18 | this.sidebarElm && this.sidebarElm.addEventListener('transitionend', this.sidebarResizeHandler); 19 | }, 20 | beforeDestroy() { 21 | window.removeEventListener('resize', this.__resizeHandler); 22 | 23 | this.sidebarElm && this.sidebarElm.removeEventListener('transitionend', this.sidebarResizeHandler); 24 | }, 25 | methods: { 26 | sidebarResizeHandler(e) { 27 | if (e.propertyName === 'width') { 28 | this.__resizeHandler(); 29 | } 30 | }, 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('uuid')->unique(); 19 | $table->text('connection'); 20 | $table->text('queue'); 21 | $table->longText('payload'); 22 | $table->longText('exception'); 23 | $table->timestamp('failed_at')->useCurrent(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('failed_jobs'); 35 | } 36 | }; -------------------------------------------------------------------------------- /resources/js/components/Item/Item.jsx: -------------------------------------------------------------------------------- 1 | import ElSvgItem from './ElSvgItem.vue' 2 | 3 | export default defineComponent({ 4 | components: {ElSvgItem}, 5 | props: { 6 | meta: { 7 | type: Object, 8 | default: null 9 | }, 10 | }, 11 | setup(props) { 12 | const renderItem = () => { 13 | if (props.meta?.elSvgIcon) { 14 | // using element-plus svg icon 15 | // element-plus remove el-icon,using 'svg icon' to replace 16 | // view https://element-plus.org/zh-CN/component/icon.html 17 | return 18 | } else if (props.meta?.bootstrapIcon) { 19 | return 20 | } 21 | } 22 | return () => { 23 | return renderItem() 24 | } 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/RequestController.php: -------------------------------------------------------------------------------- 1 | where('visit_at', '>=', Carbon::now()->startOfDay()) 16 | ->where('visit_at', '<=', Carbon::now()->endOfDay()); 17 | $visits = (clone $query) 18 | ->groupBy(DB::raw("DATE_FORMAT(visit_at, '%H')")) 19 | ->select([ 20 | DB::raw('COUNT(*) as total'), 21 | DB::raw("DATE_FORMAT(visit_at, '%H') as visit_at") 22 | ]) 23 | ->get(); 24 | 25 | $res = array_fill(0, 24, 0); 26 | foreach ($visits as $visit) { 27 | $res[intval($visit->visit_at)] = $visit->total; 28 | } 29 | 30 | return responseSuccess($res); 31 | } 32 | } -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*', 'sanctum/csrf-cookie'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => explode(',', env('ALLOWED_ORIGINS', '*')), 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => [], 29 | 30 | 'max_age' => 0, 31 | 32 | 'supports_credentials' => true, 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /database/migrations/2022_05_20_092434_create_logs_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->unsignedBigInteger('user_id'); 19 | $table->unsignedBigInteger('operator_id'); 20 | $table->string('title'); 21 | $table->string('content'); 22 | $table->index('user_id'); 23 | $table->index('operator_id'); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('logs'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2022_05_24_145840_alter_uni_name_to_users_table.php: -------------------------------------------------------------------------------- 1 | dropUnique('users_name_unique'); 18 | $table->dropUnique('users_email_unique'); 19 | $table->unique(['name', 'deleted_at'], 'uni_name_deleted_at'); 20 | $table->unique(['email', 'deleted_at'], 'uni_email_deleted_at'); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::table('users', function (Blueprint $table) { 32 | // 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import './styles/index.scss'; 2 | import {createApp} from 'vue' 3 | import Cookies from 'js-cookie'; 4 | import ElementPlus from 'element-plus' 5 | import 'element-plus/dist/index.css' 6 | import router from './router'; 7 | import i18n from './lang'; // Internationalization 8 | import App from './views/App.vue' 9 | import './permission'; // permission control 10 | import 'bootstrap-icons/font/bootstrap-icons.scss' 11 | import Icon from './components/Icon/Icon.vue' 12 | 13 | const app = createApp(App) 14 | app.use(i18n) 15 | 16 | app.config.globalProperties.productionTip = false; 17 | 18 | app.use(ElementPlus, { 19 | size: Cookies.get('size') || 'medium', // set element-plus default size 20 | i18n: (key, value) => i18n.t(key, value), 21 | }); 22 | 23 | // pinia 24 | import {createPinia} from 'pinia' 25 | app.use(createPinia()) 26 | 27 | // element svg icon 28 | import ElSvgIcon from '@/components/ElSvgIcon.vue' 29 | 30 | app.component('ElSvgIcon', ElSvgIcon) 31 | 32 | app.component('Icon', Icon) 33 | 34 | app.use(router).mount('#app') -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import axios from 'axios'; 3 | 4 | /** 5 | * We'll load the axios HTTP library which allows us to easily issue requests 6 | * to our Laravel back-end. This library automatically handles sending the 7 | * CSRF token as a header based on the value of the "XSRF" token cookie. 8 | */ 9 | 10 | window.axios = axios; 11 | 12 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 13 | window.axios.defaults.withCredentials = true; 14 | 15 | /** 16 | * Echo exposes an expressive API for subscribing to channels and listening 17 | * for events that are broadcast by Laravel. Echo and event broadcasting 18 | * allows your team to easily build robust real-time web applications. 19 | */ 20 | 21 | // import Echo from 'laravel-echo'; 22 | 23 | // window.Pusher = require('pusher-js'); 24 | 25 | // window.Echo = new Echo({ 26 | // broadcaster: 'pusher', 27 | // key: process.env.MIX_PUSHER_APP_KEY, 28 | // cluster: process.env.MIX_PUSHER_APP_CLUSTER, 29 | // forceTLS: true 30 | // }); 31 | -------------------------------------------------------------------------------- /resources/js/components/Icon/Icon.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 27 | 28 | 46 | -------------------------------------------------------------------------------- /database/migrations/2022_05_21_034423_create_requests_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('method'); 19 | $table->string('uri'); 20 | $table->string('ip'); 21 | $table->string('visit_at'); 22 | }); 23 | 24 | Schema::table('requests', function (Blueprint $table) { 25 | \Illuminate\Support\Facades\DB::statement('alter table requests modify ip VARBINARY(16)'); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('requests'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /resources/views/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Laravel Vue Admin 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{ vite_assets() }} 17 | 18 | @if(config('content.google.open')) 19 | 20 | 21 | 32 | @endif 33 | 34 | 35 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 21 | ], 22 | 23 | 'postmark' => [ 24 | 'token' => env('POSTMARK_TOKEN'), 25 | ], 26 | 27 | 'ses' => [ 28 | 'key' => env('AWS_ACCESS_KEY_ID'), 29 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 30 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 31 | ], 32 | 33 | ]; 34 | -------------------------------------------------------------------------------- /resources/js/layout/hook/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | const { body } = document 2 | const WIDTH = 992 3 | import { appStore } from '@/store/app' 4 | export default function () { 5 | const useAppStore = appStore() 6 | const $_isMobile = () => { 7 | const rect = body.getBoundingClientRect() 8 | return rect.width - 1 < WIDTH 9 | } 10 | const $_resizeHandler = () => { 11 | if (!document.hidden) { 12 | const isMobile = $_isMobile() 13 | if (isMobile) { 14 | // console.log('closeSideBar') 15 | /*此处只做根据window尺寸关闭sideBar功能*/ 16 | 17 | useAppStore.openSideBar(false) 18 | } else { 19 | useAppStore.openSideBar(true) 20 | } 21 | } 22 | } 23 | onBeforeMount(() => { 24 | window.addEventListener('resize', $_resizeHandler) 25 | }) 26 | onMounted(() => { 27 | const isMobile = $_isMobile() 28 | if (isMobile) { 29 | useAppStore.openSideBar(false) 30 | } else { 31 | useAppStore.openSideBar(true) 32 | } 33 | }) 34 | onBeforeUnmount(() => { 35 | window.removeEventListener('resize', $_resizeHandler) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | > 15 | */ 16 | protected $dontReport = [ 17 | // 18 | ]; 19 | 20 | /** 21 | * A list of the inputs that are never flashed for validation exceptions. 22 | * 23 | * @var array 24 | */ 25 | protected $dontFlash = [ 26 | 'current_password', 27 | 'password', 28 | 'password_confirmation', 29 | ]; 30 | 31 | /** 32 | * Register the exception handling callbacks for the application. 33 | * 34 | * @return void 35 | */ 36 | public function register() 37 | { 38 | $this->reportable(function (Throwable $e) { 39 | Log::error($e->getMessage() . $e->getTraceAsString()); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->morphs('tokenable'); 19 | $table->string('name'); 20 | $table->string('token', 64)->unique(); 21 | $table->text('abilities')->nullable(); 22 | $table->timestamp('last_used_at')->nullable(); 23 | $table->timestamp('expires_at')->nullable(); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('personal_access_tokens'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 trumanwong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/content.php: -------------------------------------------------------------------------------- 1 | env('APP_SUPER_ADMIN') ?: 1, 6 | 7 | // Admin Credentials 8 | 'admin_name' => env('ADMIN_NAME', 'admin'), 9 | 'admin_email' => env('ADMIN_EMAIL', 'admin@laravel-vue-admin.eu.org'), 10 | 'admin_password' => env('ADMIN_PASSWORD', '123456'), 11 | 12 | // Default Avatar 13 | 'default_avatar' => env('DEFAULT_AVATAR', '/images/avatar.png'), 14 | 15 | // Color Theme 16 | 'color_theme' => 'gray-theme', 17 | 18 | // Meta 19 | 'meta' => [ 20 | 'keywords' => 'Laravel Vue Admin,laravel,vuejs', 21 | 'description' => 'Talk is cheap. Show me the code.' 22 | ], 23 | 24 | // Google Analytics 25 | 'google' => [ 26 | 'id' => env('GOOGLE_ANALYTICS_ID', 'Google-Analytics-ID'), 27 | 'open' => env('GOOGLE_OPEN') ?: false 28 | ], 29 | 30 | // Footer 31 | 'footer' => [ 32 | 'github' => [ 33 | 'open' => true, 34 | 'url' => 'https://github.com/trumanwong', 35 | ], 36 | 'meta' => '© Laravel Vue Admin 2022. Powered By Truman', 37 | ], 38 | ]; 39 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->name(), 19 | 'email' => $this->faker->unique()->safeEmail(), 20 | 'email_verified_at' => now(), 21 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 22 | 'remember_token' => Str::random(10), 23 | ]; 24 | } 25 | 26 | /** 27 | * Indicate that the model's email address should be unverified. 28 | * 29 | * @return \Illuminate\Database\Eloquent\Factories\Factory 30 | */ 31 | public function unverified() 32 | { 33 | return $this->state(function (array $attributes) { 34 | return [ 35 | 'email_verified_at' => null, 36 | ]; 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /resources/js/lang/index.js: -------------------------------------------------------------------------------- 1 | import {createI18n} from 'vue-i18n' 2 | import Cookies from 'js-cookie' 3 | import elementEnLocale from 'element-plus/dist/locale/en.mjs' // element-plus lang 4 | import elementZhLocale from 'element-plus/dist/locale/zh-cn.mjs'// element-plus lang 5 | import enLocale from './en' 6 | import zhLocale from './zh' 7 | 8 | const messages = { 9 | en: { 10 | ...enLocale, 11 | ...elementEnLocale, 12 | }, 13 | zh: { 14 | ...zhLocale, 15 | ...elementZhLocale, 16 | } 17 | } 18 | 19 | export function getLanguage() { 20 | const chooseLanguage = Cookies.get('language') 21 | if (chooseLanguage) { 22 | return chooseLanguage 23 | } 24 | 25 | // if has not choose language 26 | const language = (navigator.language || navigator.browserLanguage).toLowerCase() 27 | const locales = Object.keys(messages) 28 | for (const locale of locales) { 29 | if (language.indexOf(locale) > -1) { 30 | return locale 31 | } 32 | } 33 | return 'en' 34 | } 35 | 36 | const i18n = createI18n({ 37 | messages, 38 | locale: getLanguage(), 39 | legacy: false, 40 | globalInjection: true, 41 | }) 42 | 43 | export default i18n 44 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | 7 | LOG_CHANNEL=stack 8 | LOG_DEPRECATIONS_CHANNEL=null 9 | LOG_LEVEL=debug 10 | 11 | DB_CONNECTION=mysql 12 | DB_HOST=127.0.0.1 13 | DB_PORT=3306 14 | DB_DATABASE=laravel 15 | DB_USERNAME=root 16 | DB_PASSWORD= 17 | 18 | BROADCAST_DRIVER=log 19 | CACHE_DRIVER=file 20 | FILESYSTEM_DRIVER=local 21 | QUEUE_CONNECTION=sync 22 | SESSION_DRIVER=file 23 | SESSION_LIFETIME=120 24 | 25 | MEMCACHED_HOST=127.0.0.1 26 | 27 | REDIS_HOST=127.0.0.1 28 | REDIS_PASSWORD=null 29 | REDIS_PORT=6379 30 | 31 | MAIL_MAILER=smtp 32 | MAIL_HOST=mailhog 33 | MAIL_PORT=1025 34 | MAIL_USERNAME=null 35 | MAIL_PASSWORD=null 36 | MAIL_ENCRYPTION=null 37 | MAIL_FROM_ADDRESS=null 38 | MAIL_FROM_NAME="${APP_NAME}" 39 | 40 | AWS_ACCESS_KEY_ID= 41 | AWS_SECRET_ACCESS_KEY= 42 | AWS_DEFAULT_REGION=us-east-1 43 | AWS_BUCKET= 44 | AWS_USE_PATH_STYLE_ENDPOINT=false 45 | 46 | PUSHER_APP_ID= 47 | PUSHER_APP_KEY= 48 | PUSHER_APP_SECRET= 49 | PUSHER_APP_CLUSTER=mt1 50 | 51 | MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 52 | MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 53 | 54 | SANCTUM_STATEFUL_DOMAINS=http://127.0.0.1 -------------------------------------------------------------------------------- /database/seeders/UsersTableSeeder.php: -------------------------------------------------------------------------------- 1 | firstOrCreate([ 24 | 'name' => config('content.admin_name'), 25 | ], [ 26 | 'email' => config('content.admin_email'), 27 | 'password' => Hash::make(config('content.admin_password')), 28 | 'status' => true, 29 | 'sex' => 0, 30 | 'birthday' => '2006-01-02 15:04:05', 31 | 'description' => 'Talk is cheap. Show me the code', 32 | 'created_at' => Carbon::now(), 33 | 'updated_at' => Carbon::now() 34 | ]); 35 | 36 | $role = Role::findByName(Acl::ROLE_ADMIN); 37 | $user->syncRoles($role); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /resources/js/router/modules/charts.js: -------------------------------------------------------------------------------- 1 | /** When your routing table is too long, you can split it into small modules**/ 2 | import Layout from '@/layout/Layout.vue' 3 | 4 | const chartsRoutes = { 5 | path: '/charts', 6 | component: Layout, 7 | redirect: 'noRedirect', 8 | name: 'Charts', 9 | meta: { 10 | title: 'charts', 11 | bootstrapIcon: 'bar-chart-fill', 12 | permissions: ['view menu charts'], 13 | }, 14 | children: [ 15 | { 16 | path: 'keyboard', 17 | component: () => import('@/views/charts/Keyboard.vue'), 18 | name: 'KeyboardChart', 19 | meta: {title: 'keyboardChart', bootstrapIcon: 'bar-chart-steps', noCache: true}, 20 | }, 21 | { 22 | path: 'line', 23 | component: () => import('@/views/charts/Line.vue'), 24 | name: 'LineChart', 25 | meta: {title: 'lineChart', bootstrapIcon: 'pie-chart-fill', noCache: true}, 26 | }, 27 | { 28 | path: 'mixchart', 29 | component: () => import('@/views/charts/MixChart.vue'), 30 | name: 'MixChart', 31 | meta: {title: 'mixChart', bootstrapIcon: 'file-earmark-bar-graph-fill', noCache: true}, 32 | }, 33 | ], 34 | } 35 | 36 | export default chartsRoutes 37 | -------------------------------------------------------------------------------- /resources/js/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './transition.scss'; 3 | @import './scss-suger.scss'; 4 | @import './elemenet-style-overflow.scss'; 5 | @import "./sidebar.scss"; 6 | 7 | //scroll 8 | @mixin main-show-wh() { 9 | /* css 声明 */ 10 | height: calc(100vh - #{$navBarHeight} - #{$tagViewHeight} - #{$appMainPadding * 2}); 11 | width: 100%; 12 | } 13 | .scroll-y { 14 | @include main-show-wh(); 15 | overflow-y: auto; 16 | } 17 | .scroll-x { 18 | @include main-show-wh(); 19 | overflow-x: auto; 20 | } 21 | .scroll-xy { 22 | @include main-show-wh(); 23 | overflow: auto; 24 | } 25 | 26 | #app { 27 | height: 100%; 28 | } 29 | 30 | html { 31 | height: 100%; 32 | box-sizing: border-box; 33 | } 34 | 35 | body { 36 | height: 100%; 37 | margin: 0; 38 | padding: 0; 39 | font-size: 14px; 40 | } 41 | 42 | label { 43 | font-weight: 700; 44 | } 45 | 46 | .filter-container { 47 | padding-bottom: 10px; 48 | 49 | .filter-item { 50 | vertical-align: middle; 51 | margin-bottom: 10px; 52 | } 53 | } 54 | .box-center { 55 | margin: 0 auto; 56 | display: table; 57 | } 58 | 59 | span, 60 | output { 61 | display: inline-block; 62 | line-height: 1; 63 | } -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /resources/js/views/users/SelfProfile.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 48 | -------------------------------------------------------------------------------- /.env.docker: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel-Vue-Admin 2 | APP_ENV=local 3 | APP_KEY=base64:1h7nR0wpjoinY5TCaPM6OR4HOCH7ssV1FjSPiZUy7I0= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost:8000 6 | 7 | LOG_CHANNEL=daily 8 | LOG_DEPRECATIONS_CHANNEL=null 9 | LOG_LEVEL=debug 10 | 11 | DB_CONNECTION=mysql 12 | DB_HOST=mysql 13 | DB_PORT=3306 14 | DB_DATABASE=laravel-vue-admin 15 | DB_USERNAME=laravel-vue-admin 16 | DB_PASSWORD=laravel-vue-admin 17 | 18 | BROADCAST_DRIVER=log 19 | CACHE_DRIVER=redis 20 | FILESYSTEM_DRIVER=local 21 | QUEUE_CONNECTION=redis 22 | SESSION_DRIVER=file 23 | SESSION_LIFETIME=120 24 | 25 | MEMCACHED_HOST=127.0.0.1 26 | 27 | REDIS_HOST=redis 28 | REDIS_PASSWORD=null 29 | REDIS_PORT=6379 30 | 31 | MAIL_MAILER=smtp 32 | MAIL_HOST=mailhog 33 | MAIL_PORT=1025 34 | MAIL_USERNAME=null 35 | MAIL_PASSWORD=null 36 | MAIL_ENCRYPTION=null 37 | MAIL_FROM_ADDRESS=null 38 | MAIL_FROM_NAME="${APP_NAME}" 39 | 40 | AWS_ACCESS_KEY_ID= 41 | AWS_SECRET_ACCESS_KEY= 42 | AWS_DEFAULT_REGION=us-east-1 43 | AWS_BUCKET= 44 | AWS_USE_PATH_STYLE_ENDPOINT=false 45 | 46 | PUSHER_APP_ID= 47 | PUSHER_APP_KEY= 48 | PUSHER_APP_SECRET= 49 | PUSHER_APP_CLUSTER=mt1 50 | 51 | MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 52 | MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 53 | 54 | SANCTUM_STATEFUL_DOMAINS=http://localhost:8000 -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | 17 | ./app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /resources/js/components/SizeSelect/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 14 | {{ item.label }} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 44 | 45 | -------------------------------------------------------------------------------- /app/Http/Resources/UserResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'name' => $this->name, 20 | 'email' => $this->email, 21 | 'status' => $this->status, 22 | 'avatar' => $this->avatar, 23 | 'sex' => $this->sex, 24 | 'sex_format' => $this->sex_format, 25 | 'age' => $this->age, 26 | 'birthday' => $this->birthday, 27 | 'description' => $this->description, 28 | 'roles' => array_map( 29 | function ($role) { 30 | return $role['name']; 31 | }, 32 | $this->roles->toArray() 33 | ), 34 | 'permissions' => array_map( 35 | function ($permission) { 36 | return $permission['name']; 37 | }, 38 | $this->getAllPermissions()->toArray() 39 | ) 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /resources/js/directive/el-table/adaptive.js: -------------------------------------------------------------------------------- 1 | 2 | import { addResizeListener, removeResizeListener } from 'element-plus/src/utils/resize-event'; 3 | 4 | /** 5 | * How to use 6 | * ... 7 | * el-table height is must be set 8 | * bottomOffset: 30(default) // The height of the table from the bottom of the page. 9 | */ 10 | 11 | const doResize = (el, binding, vnode) => { 12 | const { componentInstance: $table } = vnode; 13 | 14 | const { value } = binding; 15 | 16 | if (!$table.height) { 17 | throw new Error(`el-$table must set the height. Such as height='100px'`); 18 | } 19 | const bottomOffset = (value && value.bottomOffset) || 30; 20 | 21 | if (!$table) { 22 | return; 23 | } 24 | 25 | const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset; 26 | $table.layout.setHeight(height); 27 | $table.doLayout(); 28 | }; 29 | 30 | export default { 31 | bind(el, binding, vnode) { 32 | el.resizeListener = () => { 33 | doResize(el, binding, vnode); 34 | }; 35 | 36 | addResizeListener(el, el.resizeListener); 37 | }, 38 | inserted(el, binding, vnode) { 39 | doResize(el, binding, vnode); 40 | }, 41 | unbind(el) { 42 | removeResizeListener(el, el.resizeListener); 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /resources/js/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 14 | 15 | 16 | 17 | 18 | 31 | 32 | 44 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('name', 20)->unique(); 19 | $table->string('nickname', 20)->default(''); 20 | $table->string('avatar', 160)->default(''); 21 | $table->string('email', 50)->unique(); 22 | $table->tinyInteger('status')->default(1); 23 | $table->tinyInteger('sex')->default(0)->comment('0:man 1:woman'); 24 | $table->timestamp('birthday')->nullable(); 25 | $table->string('description')->default(''); 26 | $table->string('password'); 27 | $table->rememberToken(); 28 | $table->timestamps(); 29 | $table->softDeletes(); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | * 36 | * @return void 37 | */ 38 | public function down() 39 | { 40 | Schema::drop('users'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /resources/js/layout/components/Hamburger/Hamburger.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 14 | 15 | 16 | 17 | 18 | 31 | 32 | 44 | -------------------------------------------------------------------------------- /resources/js/router/modules/admin.js: -------------------------------------------------------------------------------- 1 | /** When your routing table is too long, you can split it into small modules**/ 2 | import Layout from '@/layout/Layout.vue' 3 | 4 | const adminRoutes = { 5 | path: '/administrator', 6 | component: Layout, 7 | redirect: '/administrator/users', 8 | name: 'Administrator', 9 | alwaysShow: true, 10 | meta: { 11 | title: 'administrator', 12 | bootstrapIcon: 'person-workspace', 13 | permissions: ['view menu administrator'], 14 | }, 15 | children: [ 16 | /** User managements */ 17 | { 18 | path: 'users/edit/:id(\\d+)', 19 | component: () => import('@/views/users/UserProfile.vue'), 20 | name: 'UserProfile', 21 | meta: { title: 'userProfile', noCache: true, permissions: ['manage user'] }, 22 | hidden: true, 23 | }, 24 | { 25 | path: 'users', 26 | component: () => import('@/views/users/List.vue'), 27 | name: 'UserList', 28 | meta: {title: 'users', bootstrapIcon: 'people', permissions: ['manage user']}, 29 | }, 30 | /** Role and permission */ 31 | { 32 | path: 'roles', 33 | component: () => import('@/views/role-permission/List.vue'), 34 | name: 'RoleList', 35 | meta: {title: 'rolePermission', bootstrapIcon: 'person-lines-fill', permissions: ['manage permission']}, 36 | }, 37 | ], 38 | } 39 | 40 | export default adminRoutes 41 | -------------------------------------------------------------------------------- /app/Jobs/ProcessRequestLog.php: -------------------------------------------------------------------------------- 1 | ip = $ip; 33 | $this->method = $method; 34 | $this->uri = $uri; 35 | $this->visitAt = $visitAt; 36 | } 37 | 38 | /** 39 | * Execute the job. 40 | * 41 | * @return void 42 | */ 43 | public function handle() 44 | { 45 | Request::query()->create([ 46 | 'ip' => DB::raw("INET6_ATON('$this->ip')"), 47 | 'method' => $this->method, 48 | 'uri' => $this->uri, 49 | 'visit_at' => $this->visitAt 50 | ]); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /resources/js/components/ScreenFull/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 50 | 51 | 58 | -------------------------------------------------------------------------------- /resources/js/utils/request.js: -------------------------------------------------------------------------------- 1 | import '@/bootstrap'; 2 | import { ElMessage } from 'element-plus'; 3 | import { isLogged, getToken } from '@/utils/auth'; 4 | 5 | // Create axios instance 6 | const service = window.axios.create({ 7 | baseURL: '/api', 8 | timeout: 10000, // Request timeout 9 | }); 10 | 11 | // Request intercepter 12 | service.interceptors.request.use( 13 | config => { 14 | const token = isLogged(); 15 | if (token) { 16 | config.headers['Authorization'] = 'Bearer ' + getToken(); // Set JWT token 17 | } 18 | return config; 19 | }, 20 | error => { 21 | // Do something with request error 22 | console.log(error); // for debug 23 | Promise.reject(error); 24 | } 25 | ); 26 | 27 | // response pre-processing 28 | service.interceptors.response.use( 29 | response => { 30 | // if (response.headers.authorization) { 31 | // setLogged(response.headers.authorization); 32 | // response.data.token = response.headers.authorization; 33 | // } 34 | 35 | return response.data; 36 | }, 37 | error => { 38 | let message = error.message; 39 | if (error.response.data && error.response.data.message) { 40 | message = error.response.data.message; 41 | } 42 | 43 | ElMessage({ 44 | message: message, 45 | type: 'error', 46 | duration: 5 * 1000, 47 | }); 48 | return Promise.reject(error); 49 | } 50 | ); 51 | 52 | export default service; 53 | -------------------------------------------------------------------------------- /.eslintrc-auto-import.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "computed": true, 4 | "createApp": true, 5 | "customRef": true, 6 | "defineAsyncComponent": true, 7 | "defineComponent": true, 8 | "effectScope": true, 9 | "EffectScope": true, 10 | "getCurrentInstance": true, 11 | "getCurrentScope": true, 12 | "h": true, 13 | "inject": true, 14 | "isReadonly": true, 15 | "isRef": true, 16 | "markRaw": true, 17 | "nextTick": true, 18 | "onActivated": true, 19 | "onBeforeMount": true, 20 | "onBeforeUnmount": true, 21 | "onBeforeUpdate": true, 22 | "onDeactivated": true, 23 | "onErrorCaptured": true, 24 | "onMounted": true, 25 | "onRenderTracked": true, 26 | "onRenderTriggered": true, 27 | "onScopeDispose": true, 28 | "onServerPrefetch": true, 29 | "onUnmounted": true, 30 | "onUpdated": true, 31 | "provide": true, 32 | "reactive": true, 33 | "readonly": true, 34 | "ref": true, 35 | "resolveComponent": true, 36 | "shallowReactive": true, 37 | "shallowReadonly": true, 38 | "shallowRef": true, 39 | "toRaw": true, 40 | "toRef": true, 41 | "toRefs": true, 42 | "triggerRef": true, 43 | "unref": true, 44 | "useAttrs": true, 45 | "useCssModule": true, 46 | "useCssVars": true, 47 | "useRoute": true, 48 | "useRouter": true, 49 | "useSlots": true, 50 | "watch": true, 51 | "watchEffect": true 52 | } 53 | } -------------------------------------------------------------------------------- /supervisor/supervisord.conf: -------------------------------------------------------------------------------- 1 | ; supervisor config file 2 | 3 | [unix_http_server] 4 | file=/var/run/supervisor.sock ; (the path to the socket file) 5 | chmod=0700 ; sockef file mode (default 0700) 6 | 7 | [supervisord] 8 | ; The important part here is the nodaemon=true, which instructs supervisor to start in the foreground instead of as a service. This lets Docker to manage its lifecycle. 9 | nodaemon=true 10 | logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log) 11 | pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid) 12 | childlogdir=/var/log/supervisor ; ('AUTO' child log dir, default $TEMP) 13 | 14 | ; the below section must remain in the config file for RPC 15 | ; (supervisorctl/web interface) to work, additional interfaces may be 16 | ; added by defining them in separate rpcinterface: sections 17 | [rpcinterface:supervisor] 18 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 19 | 20 | [supervisorctl] 21 | serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket 22 | 23 | ; The [include] section can just contain the "files" setting. This 24 | ; setting can list multiple files (separated by whitespace or 25 | ; newlines). It can also contain wildcards. The filenames are 26 | ; interpreted as relative to this file. Included files *cannot* 27 | ; include files themselves. 28 | 29 | [include] 30 | files = /etc/supervisor/conf.d/*.conf -------------------------------------------------------------------------------- /app/Http/Controllers/Api/PermissionController.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 54 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | // 启用所有严格类型检查选项。 6 | //启用 --strict相当于启用 --noImplicitAny, --noImplicitThis, --alwaysStrict, --strictNullChecks和 --strictFunctionTypes和--strictPropertyInitialization。 7 | "strict": true, 8 | // 允许编译器编译JS,JSX文件 9 | "allowJs": true, 10 | // 允许在JS文件中报错,通常与allowJS一起使用 11 | "checkJs": false, 12 | // 允许使用jsx 13 | "jsx": "preserve", 14 | "declaration": true, 15 | //移除注解 16 | "removeComments": true, 17 | //不可以忽略any 18 | "noImplicitAny": true, 19 | //关闭 this 类型注解提示 20 | "noImplicitThis": true, 21 | //null/undefined不能作为其他类型的子类型: 22 | //let a: number = null; //这里会报错. 23 | "strictNullChecks": true, 24 | //生成枚举的映射代码 25 | "preserveConstEnums": true, 26 | //根目录 27 | //输出目录 28 | "outDir": "./ts-out-dir", 29 | //是否输出src2.js.map文件 30 | "sourceMap": true, 31 | //变量定义了但是未使用 32 | "noUnusedLocals": false, 33 | //是否允许把json文件当做模块进行解析 34 | "resolveJsonModule": true, 35 | //和noUnusedLocals一样,针对func 36 | "noUnusedParameters": false, 37 | // 模块解析策略,ts默认用node的解析策略,即相对的方式导入 38 | "moduleResolution": "node", 39 | //允许export=导出,由import from 导入 40 | "esModuleInterop": true, 41 | //忽略所有的声明文件( *.d.ts)的类型检查。 42 | "skipLibCheck": true, 43 | "baseUrl": ".", 44 | "paths": { 45 | "@/*": ["resources/js/*"], 46 | }, 47 | //指定默认读取的目录 48 | //"typeRoots": ["./node_modules/@types/", "./types"], 49 | "lib": ["ES2018", "DOM"] 50 | }, 51 | //"files": [], 52 | //include包含文件夹会被ts进行读取 53 | "include": ["resources/js"] 54 | } 55 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/AuthController.php: -------------------------------------------------------------------------------- 1 | validate([ 27 | 'email' => 'required|email', 28 | 'password' => 'required' 29 | ]); 30 | $user = User::query()->where('email', $request->input('email'))->first(); 31 | if (empty($user) || !Hash::check($request->input('password'), $user->password)) { 32 | return responseFailed('These credentials do not match our records.', Response::HTTP_UNAUTHORIZED); 33 | } 34 | 35 | $user->token = $user->createToken('laravel-vue-admin')->plainTextToken; 36 | 37 | return responseSuccess($user); 38 | } 39 | 40 | /** 41 | * @param Request $request 42 | * @return JsonResponse 43 | */ 44 | public function logout(Request $request): JsonResponse 45 | { 46 | Auth::guard('web')->logout(); 47 | return responseSuccess(); 48 | } 49 | 50 | public function user(Request $request): UserResource 51 | { 52 | return new UserResource($request->user()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "vite build", 5 | "watch": "vite build --watch", 6 | "lint": "eslint --ext .js,.vue resources/js", 7 | "prepare": "husky install" 8 | }, 9 | "devDependencies": { 10 | "@babel/preset-typescript": "^7.23.2", 11 | "@element-plus/icons-vue": "^2.1.0", 12 | "@types/node": "^20.8.9", 13 | "@typescript-eslint/eslint-plugin": "^6.9.0", 14 | "@typescript-eslint/parser": "^6.9.0", 15 | "@vitejs/plugin-vue": "^4.4.0", 16 | "@vitejs/plugin-vue-jsx": "^3.0.2", 17 | "axios": "^1.6.0", 18 | "bootstrap-icons": "^1.11.1", 19 | "dayjs": "^1.11.10", 20 | "echarts": "^5.4.3", 21 | "element-plus": "^2.4.1", 22 | "eslint": "^8.52.0", 23 | "eslint-plugin-vue": "^9.18.1", 24 | "husky": "^8.0.3", 25 | "js-cookie": "^3.0.5", 26 | "normalize.css": "^8.0.1", 27 | "nprogress": "^0.2.0", 28 | "path": "^0.12.7", 29 | "path-to-regexp": "^6.2.1", 30 | "pinia": "^2.1.7", 31 | "sass": "^1.69.5", 32 | "sass-loader": "^13.3.2", 33 | "screenfull": "^6.0.2", 34 | "svg-sprite-loader": "^6.0.11", 35 | "typescript": "^5.2.2", 36 | "unplugin-auto-import": "^0.16.7", 37 | "vite": "^4.5.0", 38 | "vite-plugin-laravel": "^0.3.1", 39 | "vite-plugin-vue-setup-extend-plus": "^0.1.0", 40 | "vue": "^3.3.7", 41 | "vue-i18n": "^9.6.1", 42 | "vue-loader": "^17.0.1", 43 | "vue-router": "^4.2.5", 44 | "vuedraggable": "^2.24.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | configureRateLimiting(); 30 | 31 | $this->routes(function () { 32 | Route::prefix('api') 33 | ->middleware('api') 34 | ->namespace($this->namespace) 35 | ->group(base_path('routes/api.php')); 36 | 37 | Route::middleware('web') 38 | ->namespace($this->namespace) 39 | ->group(base_path('routes/web.php')); 40 | }); 41 | } 42 | 43 | /** 44 | * Configure the rate limiters for the application. 45 | * 46 | * @return void 47 | */ 48 | protected function configureRateLimiting() 49 | { 50 | RateLimiter::for('api', function (Request $request) { 51 | return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip()); 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /resources/js/utils/validate.js: -------------------------------------------------------------------------------- 1 | /* All validations should be defined here */ 2 | 3 | export function isExternal(path) { 4 | return /^(https?:|mailto:|tel:)/.test(path); 5 | } 6 | 7 | /** 8 | * Validate a valid URL 9 | * @param {String} textval 10 | * @return {Boolean} 11 | */ 12 | export function validURL(url) { 13 | const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/; 14 | return reg.test(url); 15 | } 16 | 17 | /** 18 | * Validate a full-lowercase string 19 | * @return {Boolean} 20 | * @param {String} str 21 | */ 22 | export function validLowerCase(str) { 23 | const reg = /^[a-z]+$/; 24 | return reg.test(str); 25 | } 26 | 27 | /** 28 | * Validate a full-uppercase string 29 | * @return {Boolean} 30 | * @param {String} str 31 | */ 32 | export function validUpperCase(str) { 33 | const reg = /^[A-Z]+$/; 34 | return reg.test(str); 35 | } 36 | 37 | /** 38 | * Check if a string contains only alphabet 39 | * @param {String} str 40 | * @param {Boolean} 41 | */ 42 | export function validAlphabets(str) { 43 | const reg = /^[A-Za-z]+$/; 44 | return reg.test(str); 45 | } 46 | 47 | /** 48 | * Validate an email address 49 | * @param {String} email 50 | * @return {Boolean} 51 | */ 52 | export function validEmail(email) { 53 | const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 54 | return re.test(email); 55 | } 56 | -------------------------------------------------------------------------------- /resources/js/views/dashboard/Admin/components/TransactionTable.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ row.order_no?.substring(0, 30) }} 6 | 7 | 8 | 9 | ¥{{ toThousandFilter(scope.row.price) }} 10 | 11 | 12 | 13 | 14 | {{ row.status }} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /resources/js/components/LangSelect/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 14 | {{ item.label }} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 54 | 55 | 63 | -------------------------------------------------------------------------------- /config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env('BCRYPT_ROUNDS', 10), 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Argon Options 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here you may specify the configuration options that should be used when 41 | | passwords are hashed using the Argon algorithm. These will allow you 42 | | to control the amount of time it takes to hash the given password. 43 | | 44 | */ 45 | 46 | 'argon' => [ 47 | 'memory' => 65536, 48 | 'threads' => 1, 49 | 'time' => 4, 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /resources/js/utils/scroll-to.js: -------------------------------------------------------------------------------- 1 | Math.easeInOutQuad = function(t, b, c, d) { 2 | t /= d / 2; 3 | if (t < 1) { 4 | return c / 2 * t * t + b; 5 | } 6 | t--; 7 | return (-c / 2 * (t * (t - 2) - 1)) + b; 8 | }; 9 | 10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts 11 | var requestAnimFrame = (function() { 12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { 13 | window.setTimeout(callback, 1000 / 60); 14 | }; 15 | })(); 16 | 17 | // because it's so fucking difficult to detect the scrolling element, just move them all 18 | function move(amount) { 19 | document.documentElement.scrollTop = amount; 20 | document.body.parentNode.scrollTop = amount; 21 | document.body.scrollTop = amount; 22 | } 23 | 24 | function position() { 25 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop; 26 | } 27 | 28 | export function scrollTo(to, duration, callback) { 29 | const start = position(); 30 | const change = to - start; 31 | const increment = 20; 32 | let currentTime = 0; 33 | duration = (typeof (duration) === 'undefined') ? 500 : duration; 34 | var animateScroll = function() { 35 | // increment the time 36 | currentTime += increment; 37 | // find the value with the quadratic in-out easing function 38 | var val = Math.easeInOutQuad(currentTime, start, change, duration); 39 | // move the document.body 40 | move(val); 41 | // do the animation unless its over 42 | if (currentTime < duration) { 43 | requestAnimFrame(animateScroll); 44 | } else { 45 | if (callback && typeof (callback) === 'function') { 46 | // the animation is done so lets callback 47 | callback(); 48 | } 49 | } 50 | }; 51 | animateScroll(); 52 | } 53 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class); 50 | 51 | $response = $kernel->handle( 52 | $request = Request::capture() 53 | )->send(); 54 | 55 | $kernel->terminate($request, $response); 56 | -------------------------------------------------------------------------------- /config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env('PUSHER_APP_KEY'), 36 | 'secret' => env('PUSHER_APP_SECRET'), 37 | 'app_id' => env('PUSHER_APP_ID'), 38 | 'options' => [ 39 | 'cluster' => env('PUSHER_APP_CLUSTER'), 40 | 'useTLS' => true, 41 | ], 42 | ], 43 | 44 | 'ably' => [ 45 | 'driver' => 'ably', 46 | 'key' => env('ABLY_KEY'), 47 | ], 48 | 49 | 'redis' => [ 50 | 'driver' => 'redis', 51 | 'connection' => 'default', 52 | ], 53 | 54 | 'log' => [ 55 | 'driver' => 'log', 56 | ], 57 | 58 | 'null' => [ 59 | 'driver' => 'null', 60 | ], 61 | 62 | ], 63 | 64 | ]; 65 | -------------------------------------------------------------------------------- /resources/js/layout/Layout.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 | 43 | 44 | 81 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://blog.csdn.net/Sheng_zhenzhen/article/details/108685176 2 | module.exports = { 3 | root: true, 4 | env: { 5 | browser: true, 6 | commonjs: true, 7 | es6: true, 8 | node: true 9 | }, 10 | 11 | globals: { 12 | defineEmits: true, 13 | document: true, 14 | localStorage: true, 15 | GLOBAL_VAR: true, 16 | window: true, 17 | defineProps: true, 18 | defineExpose: true, 19 | $ref: true 20 | }, 21 | plugins: ['@typescript-eslint', 'prettier', 'import'], 22 | extends: [ 23 | 'eslint:recommended', 24 | 'plugin:@typescript-eslint/recommended', 25 | 'plugin:vue/vue3-recommended', 26 | 'prettier', 27 | './.eslintrc-auto-import.json', 28 | './tests/.eslintrc-unit-test.json' 29 | ], 30 | parserOptions: { 31 | parser: '@typescript-eslint/parser', 32 | sourceType: 'module', 33 | ecmaFeatures: { 34 | jsx: true, 35 | tsx: true 36 | } 37 | }, 38 | rules: { 39 | //close lf error 40 | 'import/no-unresolved': [0], 41 | 'vue/multi-word-component-names': 'off', 42 | 'vue/no-deprecated-router-link-tag-prop': 'off', 43 | 'import/extensions': 'off', 44 | 'import/no-absolute-path': 'off', 45 | 'no-async-promise-executor': 'off', 46 | 'import/no-extraneous-dependencies': 'off', 47 | 'vue/no-multiple-template-root': 'off', 48 | 'vue/html-self-closing': 'off', 49 | 'no-console': 'off', 50 | 'no-plusplus': 'off', 51 | 'no-useless-escape': 'off', 52 | 'no-bitwise': 'off', 53 | '@typescript-eslint/no-explicit-any': ['off'], 54 | '@typescript-eslint/explicit-module-boundary-types': ['off'], 55 | '@typescript-eslint/ban-ts-comment': ['off'], 56 | 'vue/no-setup-props-destructure': ['off'], 57 | '@typescript-eslint/no-empty-function': ['off'], 58 | 'vue/script-setup-uses-vars': ['off'], 59 | //can config to 2 if need more then required 60 | '@typescript-eslint/no-unused-vars': [0], 61 | 'no-param-reassign': ['off'] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /resources/js/components/GithubCorner/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 17 | 22 | 23 | 24 | 25 | 26 | 55 | -------------------------------------------------------------------------------- /resources/js/views/dashboard/GithubCorner/GithubCorner.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 16 | 21 | 22 | 23 | 24 | 25 | 54 | -------------------------------------------------------------------------------- /resources/js/views/users/components/UserBio.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ t('user.about_me') }} 5 | 6 | 7 | 8 | 9 | {{ t('user.education') }} 10 | 11 | 12 | 13 | B.S. in Communication Engineering from Hunan University of Technology 14 | 15 | 16 | 17 | 18 | 19 | 20 | {{ t('user.skills') }} 21 | 22 | 23 | 24 | Laravel 25 | 26 | 27 | 28 | Vue 29 | 30 | 31 | 32 | JavaScript 33 | 34 | 35 | 36 | HTML & CSS 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 70 | -------------------------------------------------------------------------------- /resources/js/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ title }} 7 | 8 | 9 | 10 | {{ title }} 11 | 12 | 13 | 14 | 15 | 16 | 32 | 33 | 77 | -------------------------------------------------------------------------------- /resources/js/views/dashboard/Admin/components/TodoList/Todo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 17 | 18 | 19 | 20 | 84 | -------------------------------------------------------------------------------- /resources/js/settings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | /** 3 | * @type {String} 4 | */ 5 | title: 'Laravel Vue Admin', 6 | theme: '#1890ff', 7 | 8 | /** 9 | * @type {boolean} true | false 10 | * @description Whether show the settings right-panel 11 | */ 12 | showSettings: true, 13 | 14 | /** 15 | * @type {boolean} true | false 16 | * @description Whether need tagsView 17 | */ 18 | tagsView: true, 19 | 20 | /** 21 | * @type {boolean} true | false 22 | * @description Whether fix the header 23 | */ 24 | fixedHeader: false, 25 | 26 | /** 27 | * @type {boolean} true | false 28 | * @description Whether show the logo in sidebar 29 | */ 30 | sidebarLogo: true, 31 | 32 | /** 33 | * @type {string | array} 'production' | ['production','development'] 34 | * @description Need show err logs component. 35 | * The default is only used in the production env 36 | * If you want to also use it in dev, you can pass ['production','development'] 37 | */ 38 | errorLog: 'production', 39 | /** 40 | * @type {boolean} true | false 41 | * @description Whether show TagsView 42 | */ 43 | showTagsView: true, 44 | /** 45 | * @description TagsView show number 46 | */ 47 | tagsViewNum: 6, 48 | /** 49 | * @type {boolean} true | false 50 | * @description Whether show the top Navbar 51 | */ 52 | showTopNavbar: true, 53 | /* page animation related*/ 54 | /** 55 | * @type {boolean} true | false 56 | * @description Whether need animation of main area 57 | */ 58 | mainNeedAnimation: true, 59 | /* 60 | * setting default defaultSize 61 | * large / default /small 62 | * */ 63 | defaultSize: 'small', 64 | /** 65 | * @type {boolean} true | false 66 | * @description Whether show Hamburger 67 | */ 68 | showHamburger: true, 69 | /** 70 | * @type {boolean} true | false 71 | * @description Whether show the settings right-panel 72 | */ 73 | showLeftMenu: true, 74 | /** 75 | * @type {boolean} true | false 76 | * @description Whether show the title in Navbar 77 | */ 78 | showNavbarTitle: false, 79 | /** 80 | * @type {boolean} true | false 81 | * @description Whether show the drop-down 82 | */ 83 | ShowDropDown: true, 84 | }; 85 | -------------------------------------------------------------------------------- /resources/js/router/index.js: -------------------------------------------------------------------------------- 1 | import {createRouter, createWebHashHistory} from 'vue-router' 2 | 3 | /* Layout */ 4 | import Layout from '@/layout/Layout.vue' 5 | 6 | /* Router for modules */ 7 | import chartsRoutes from './modules/charts' 8 | import adminRoutes from './modules/admin' 9 | import errorRoutes from './modules/error' 10 | 11 | export const constantRoutes = [ 12 | { 13 | path: '/login', 14 | component: () => import('@/views/login/index.vue'), 15 | hidden: true, 16 | }, 17 | { 18 | path: '/auth-redirect', 19 | component: () => import('@/views/login/AuthRedirect.vue'), 20 | hidden: true, 21 | }, 22 | { 23 | path: '/404', 24 | redirect: { name: 'Page404' }, 25 | component: () => import('@/views/error-page/404.vue'), 26 | hidden: true, 27 | }, 28 | { 29 | path: '/401', 30 | component: () => import('@/views/error-page/401.vue'), 31 | hidden: true, 32 | }, 33 | { 34 | path: '/', 35 | component: Layout, 36 | redirect: '/dashboard', 37 | children: [ 38 | { 39 | path: 'dashboard', 40 | component: () => import('@/views/dashboard/index.vue'), 41 | name: 'Dashboard', 42 | meta: { title: 'dashboard', bootstrapIcon: 'house-fill', noCache: false }, 43 | }, 44 | ], 45 | }, 46 | { 47 | path: '/profile', 48 | component: Layout, 49 | redirect: '/profile/edit', 50 | children: [ 51 | { 52 | path: 'edit', 53 | component: () => import('@/views/users/SelfProfile.vue'), 54 | name: 'SelfProfile', 55 | meta: {title: 'userProfile', bootstrapIcon: 'person-circle', noCache: true}, 56 | }, 57 | ], 58 | }, 59 | ] 60 | 61 | export const asyncRoutes = [ 62 | chartsRoutes, 63 | adminRoutes, 64 | errorRoutes, 65 | { path: '/:pathMatch(.*)*', name: 'NotFound', redirect: '/404', hidden: true } 66 | ] 67 | 68 | const router = createRouter({ 69 | routes: constantRoutes, 70 | scrollBehavior: () => ({ top: 0 }), 71 | history: createWebHashHistory(), 72 | }) 73 | 74 | export function resetRouter() { 75 | const asyncRouterNameArr = asyncRoutes.map((mItem) => mItem.name) 76 | asyncRouterNameArr.forEach((name) => { 77 | if (router.hasRoute(name)) { 78 | router.removeRoute(name) 79 | } 80 | }) 81 | } 82 | 83 | export default router 84 | -------------------------------------------------------------------------------- /resources/js/views/i18n/local.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | en: { 4 | i18nView: { 5 | title: 'Switch Language', 6 | note: 'The internationalization of this project is based on vue-i18n', 7 | datePlaceholder: 'Pick a day', 8 | selectPlaceholder: 'Select', 9 | tableDate: 'tableDate', 10 | tableName: 'tableName', 11 | tableAddress: 'tableAddress', 12 | default: 'default:', 13 | primary: 'primary', 14 | success: 'success', 15 | info: 'info', 16 | warning: 'warning', 17 | danger: 'danger', 18 | one: 'One', 19 | two: 'Two', 20 | three: 'Three', 21 | }, 22 | }, 23 | ru: { 24 | i18nView: { 25 | title: 'Сменить язык', 26 | note: 'Многоязычность проекта реализована при помощи vue-i18n', 27 | datePlaceholder: 'Выберите дату', 28 | selectPlaceholder: 'Список', 29 | tableDate: 'Дата', 30 | tableName: 'Имя', 31 | tableAddress: 'Адрес', 32 | default: 'default:', 33 | primary: 'primary', 34 | success: 'success', 35 | info: 'info', 36 | warning: 'warning', 37 | danger: 'danger', 38 | one: 'Один', 39 | two: 'Два', 40 | three: 'Три', 41 | }, 42 | }, 43 | vi: { 44 | i18nView: { 45 | title: 'Switch Language', 46 | note: 'The internationalization of this project is based on vue-i18n', 47 | datePlaceholder: 'Pick a day', 48 | selectPlaceholder: 'Select', 49 | tableDate: 'tableDate', 50 | tableName: 'tableName', 51 | tableAddress: 'tableAddress', 52 | default: 'mặc định:', 53 | primary: 'primary', 54 | success: 'thành công', 55 | info: 'thông tin', 56 | warning: 'cảnh báo', 57 | danger: 'nguy hiểm', 58 | one: 'Một', 59 | two: 'Hai', 60 | three: 'Ba', 61 | }, 62 | }, 63 | zh: { 64 | i18nView: { 65 | title: '切换语言', 66 | note: '本项目国际化基于 vue-i18n', 67 | datePlaceholder: '请选择日期', 68 | selectPlaceholder: '请选择', 69 | tableDate: '日期', 70 | tableName: '姓名', 71 | tableAddress: '地址', 72 | default: '默认按钮', 73 | primary: '主要按钮', 74 | success: '成功按钮', 75 | info: '信息按钮', 76 | warning: '警告按钮', 77 | danger: '危险按钮', 78 | one: '一', 79 | two: '二', 80 | three: '三', 81 | }, 82 | }, 83 | }; 84 | -------------------------------------------------------------------------------- /database/migrations/2019_04_20_130706_setup_role_permissions.php: -------------------------------------------------------------------------------- 1 | givePermissionTo(Acl::permissions()); 35 | $managerRole->givePermissionTo(Acl::permissions([Acl::PERMISSION_PERMISSION_MANAGE])); 36 | $editorRole->givePermissionTo(Acl::menuPermissions()); 37 | $userRole->givePermissionTo([ 38 | Acl::PERMISSION_VIEW_MENU_PERMISSION, 39 | ]); 40 | $visitorRole->givePermissionTo([ 41 | Acl::PERMISSION_VIEW_MENU_PERMISSION, 42 | ]); 43 | } 44 | 45 | /** 46 | * Reverse the migrations. 47 | * 48 | * @return void 49 | */ 50 | public function down() 51 | { 52 | if (!Schema::hasColumn('users', 'role')) { 53 | Schema::table('users', function (Blueprint $table) { 54 | $table->string('role')->default('editor'); 55 | }); 56 | } 57 | 58 | /** @var \App\User[] $users */ 59 | $users = \App\Laravue\Models\User::all(); 60 | foreach ($users as $user) { 61 | $roles = array_reverse(Acl::roles()); 62 | foreach ($roles as $role) { 63 | if ($user->hasRole($role)) { 64 | $user->role = $role; 65 | $user->save(); 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /resources/js/views/dashboard/Editor/Editor.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Your roles: 6 | {{ item }} 7 | 8 | 9 | 10 | {{ name }} 11 | Editor's Dashboard 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 43 | 44 | 77 | -------------------------------------------------------------------------------- /resources/js/views/dashboard/Admin/components/PieChart.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/laravel", 3 | "type": "project", 4 | "description": "The Laravel Framework.", 5 | "keywords": ["framework", "laravel"], 6 | "license": "MIT", 7 | "require": { 8 | "php": "^8.1", 9 | "doctrine/dbal": "^3.6", 10 | "guzzlehttp/guzzle": "^7.2", 11 | "laravel/framework": "^10.0", 12 | "laravel/sanctum": "^3.2", 13 | "laravel/tinker": "^2.8", 14 | "laravel/ui": "^4.2", 15 | "league/fractal": "^0.20.1", 16 | "spatie/laravel-permission": "^5.10", 17 | "tucker-eric/eloquentfilter": "^3.2" 18 | }, 19 | "require-dev": { 20 | "fakerphp/faker": "^1.9.1", 21 | "laravel/pint": "^1.0", 22 | "laravel/sail": "^1.18", 23 | "mockery/mockery": "^1.4.4", 24 | "nunomaduro/collision": "^7.0", 25 | "phpunit/phpunit": "^10.0", 26 | "spatie/laravel-ignition": "^2.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "App\\": "app/", 31 | "Database\\Factories\\": "database/factories/", 32 | "Database\\Seeders\\": "database/seeders/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Tests\\": "tests/" 38 | }, 39 | "files": [ 40 | "app/helpers.php" 41 | ] 42 | }, 43 | "scripts": { 44 | "post-autoload-dump": [ 45 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 46 | "@php artisan package:discover --ansi" 47 | ], 48 | "post-update-cmd": [ 49 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force" 50 | ], 51 | "post-root-package-install": [ 52 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 53 | ], 54 | "post-create-project-cmd": [ 55 | "@php artisan key:generate --ansi" 56 | ] 57 | }, 58 | "extra": { 59 | "laravel": { 60 | "dont-discover": [] 61 | } 62 | }, 63 | "config": { 64 | "optimize-autoloader": true, 65 | "preferred-install": "dist", 66 | "sort-packages": true, 67 | "allow-plugins": { 68 | "pestphp/pest-plugin": true, 69 | "php-http/discovery": true 70 | } 71 | }, 72 | "minimum-stability": "stable", 73 | "prefer-stable": true 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Vue Admin 2 | 3 | [Laravel Vue Admin](https://laravel-vue-admin.eu.org) is a beautiful dashboard combination of [Laravel](https://laravel.com/), [Vue3](https://github.com/vuejs/vue) and the UI Toolkit [Element Plus](https://element-plus.org/). 4 | 5 | ## Getting started 6 | 7 | ### Installing 8 | 9 | #### Manual 10 | 11 | ```bash 12 | # Clone the project and run composer 13 | git clone https://github.com/trumanwong/laravel-vue-admin 14 | cd laravel-vue-admin 15 | 16 | # Migration and DB seeder (after changing your DB settings in .env) 17 | php artisan migrate --seed 18 | 19 | # Install dependency with NPM 20 | npm install 21 | 22 | # develop 23 | npm run watch 24 | 25 | # Build on production 26 | npm run build 27 | ``` 28 | 29 | #### Docker 30 | 31 | ```sh 32 | docker-compose up -d 33 | ``` 34 | 35 | Build static files within `Laravel` container with `npm` 36 | 37 | ```sh 38 | docker exec -it laravel-vue-admin npm run watch 39 | ``` 40 | 41 | Open http://localhost:8000 (laravel container port declared in `docker-compose.yml`) to access Laravel Vue Admin. 42 | 43 | ## Built with 44 | 45 | * [Laravel](https://laravel.com/) - The PHP Framework For Web Artisans 46 | * [Laravel Sanctum](https://github.com/laravel/sanctum/) - Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs. 47 | * [spatie/laravel-permission](https://github.com/spatie/laravel-permission) - Associate users with permissions and roles. 48 | * [VueJS](https://vuejs.org/) - The Progressive JavaScript Framework 49 | * [Element Plus](https://element-plus.org/) -A Vue.js 3 UI library 50 | * [vue3-admin-ts](https://github.com/jzfai/vue3-admin-ts) - A minimal vue3 admin template with Element-Plus UI & axios & permission control & lint & hook 51 | 52 | ## License 53 | 54 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE) file for details. 55 | 56 | ## Acknowledgements 57 | 58 | * [Laravel](https://laravel.com/) - The PHP Framework For Web Artisans 59 | * [VueJS](https://vuejs.org/) - The Progressive JavaScript Framework 60 | * [vue3-admin-ts](https://panjiachen.github.io/vue-element-admin/#/) A minimal vue3 admin template with Element-Plus UI & axios & permission control & lint & hook 61 | * [Echarts](http://echarts.apache.org/) - A powerful, interactive charting and visualization library for browser. 62 | * [Cloudflare](https://https://www.cloudflare.com/) - A global network built for the cloud 63 | -------------------------------------------------------------------------------- /resources/js/store/permission.js: -------------------------------------------------------------------------------- 1 | import { asyncRoutes, constantRoutes } from '../router'; 2 | import {defineStore} from "pinia"; 3 | 4 | /** 5 | * Check if it matches the current user right by meta.role 6 | * @param {String[]} roles 7 | * @param {String[]} permissions 8 | * @param route 9 | */ 10 | function canAccess(roles, permissions, route) { 11 | if (route.meta) { 12 | let hasRole = true; 13 | let hasPermission = true; 14 | if (route.meta.roles || route.meta.permissions) { 15 | // If it has meta.roles or meta.permissions, accessible = hasRole || permission 16 | hasRole = false; 17 | hasPermission = false; 18 | if (route.meta.roles) { 19 | hasRole = roles.some(role => route.meta.roles.includes(role)); 20 | } 21 | 22 | if (route.meta.permissions) { 23 | hasPermission = permissions.some(permission => route.meta.permissions.includes(permission)); 24 | } 25 | } 26 | 27 | return hasRole || hasPermission; 28 | } 29 | 30 | // If no meta.roles/meta.permissions inputted - the route should be accessible 31 | return true; 32 | } 33 | 34 | /** 35 | * Find all routes of this role 36 | * @param routes asyncRoutes 37 | * @param roles 38 | */ 39 | function filterAsyncRoutes(routes, roles, permissions) { 40 | const res = []; 41 | 42 | routes.forEach(route => { 43 | const tmp = { ...route }; 44 | if (canAccess(roles, permissions, tmp)) { 45 | if (tmp.children) { 46 | tmp.children = filterAsyncRoutes( 47 | tmp.children, 48 | roles, 49 | permissions 50 | ); 51 | } 52 | res.push(tmp); 53 | } 54 | }); 55 | 56 | return res; 57 | } 58 | 59 | export const permissionStore = defineStore('permission', { 60 | state: () => { 61 | return { 62 | routes: [], 63 | addRoutes: [], 64 | } 65 | }, 66 | actions: { 67 | generateRoutes(roles, permissions) { 68 | return new Promise(resolve => { 69 | let accessedRoutes; 70 | if (roles.includes('admin')) { 71 | accessedRoutes = asyncRoutes || []; 72 | } else { 73 | accessedRoutes = filterAsyncRoutes(asyncRoutes, roles, permissions); 74 | } 75 | 76 | this.$patch((state) => { 77 | state.addRoutes = accessedRoutes; 78 | state.routes = constantRoutes.concat(accessedRoutes); 79 | }) 80 | resolve(accessedRoutes); 81 | }); 82 | } 83 | } 84 | }) 85 | -------------------------------------------------------------------------------- /resources/js/views/dashboard/Admin/components/BarChart.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 103 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | group(function() { 19 | Route::post('auth/login', 'AuthController@login'); 20 | Route::group(['middleware' => 'auth:sanctum'], function () { 21 | Route::post('auth/logout', 'AuthController@logout'); 22 | 23 | Route::get('/user', 'AuthController@user'); 24 | 25 | // Api resource routes 26 | Route::apiResource('roles', 'RoleController')->middleware('permission:' . Acl::PERMISSION_PERMISSION_MANAGE); 27 | Route::apiResource('users', 'UserController')->middleware('permission:' . Acl::PERMISSION_USER_MANAGE); 28 | Route::apiResource('permissions', 'PermissionController')->middleware('permission:' . Acl::PERMISSION_PERMISSION_MANAGE); 29 | 30 | // Custom routes 31 | Route::group(['prefix' => 'users'], function (RouteContract $api) { 32 | $api->get('{user}/permissions', 'UserController@permissions')->middleware('permission:' . Acl::PERMISSION_PERMISSION_MANAGE); 33 | $api->put('{user}/permissions', 'UserController@updatePermissions')->middleware('permission:' .Acl::PERMISSION_PERMISSION_MANAGE); 34 | $api->get('{user}/logs', 'LogController@index'); 35 | }); 36 | 37 | Route::get('roles/{role}/permissions', 'RoleController@permissions')->middleware('permission:' . Acl::PERMISSION_PERMISSION_MANAGE); 38 | Route::get('requests', 'RequestController@index'); 39 | }); 40 | }); 41 | 42 | Route::get('/orders', function () { 43 | $rowsNumber = 8; 44 | $data = []; 45 | for ($rowIndex = 0; $rowIndex < $rowsNumber; $rowIndex++) { 46 | $row = [ 47 | 'order_no' => 'LARAVUE' . mt_rand(1000000, 9999999), 48 | 'price' => mt_rand(10000, 999999), 49 | 'status' => randomInArray(['success', 'pending']), 50 | ]; 51 | 52 | $data[] = $row; 53 | } 54 | 55 | return responseSuccess(['items' => $data]); 56 | }); -------------------------------------------------------------------------------- /resources/js/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ generateTitle(onlyOneChild.meta?.title) }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{ generateTitle(item.meta.title) }} 15 | 16 | 23 | 24 | 25 | 26 | 27 | 79 | -------------------------------------------------------------------------------- /resources/js/views/dashboard/Admin/components/RaddarChart.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /resources/js/views/users/components/UserCard.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ user.name }} 10 | 11 | 12 | {{ getRole() }} 13 | 14 | 15 | 16 | 21 | {{ user.name }} 22 | {{ user.email }} 23 | {{ user.sex ? t('user.male') : t('user.female') }} 24 | {{ user.age }} 25 | 26 | {{user.description}} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 60 | 61 | 90 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DRIVER', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure as many filesystem "disks" as you wish, and you 24 | | may even configure multiple disks of the same driver. Defaults have 25 | | been setup for each driver as an example of the required options. 26 | | 27 | | Supported Drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app'), 36 | ], 37 | 38 | 'public' => [ 39 | 'driver' => 'local', 40 | 'root' => storage_path('app/public'), 41 | 'url' => env('APP_URL').'/storage', 42 | 'visibility' => 'public', 43 | ], 44 | 45 | 's3' => [ 46 | 'driver' => 's3', 47 | 'key' => env('AWS_ACCESS_KEY_ID'), 48 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 49 | 'region' => env('AWS_DEFAULT_REGION'), 50 | 'bucket' => env('AWS_BUCKET'), 51 | 'url' => env('AWS_URL'), 52 | 'endpoint' => env('AWS_ENDPOINT'), 53 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 54 | ], 55 | 56 | ], 57 | 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | Symbolic Links 61 | |-------------------------------------------------------------------------- 62 | | 63 | | Here you may configure the symbolic links that will be created when the 64 | | `storage:link` Artisan command is executed. The array keys should be 65 | | the locations of the links and the values should be their targets. 66 | | 67 | */ 68 | 69 | 'links' => [ 70 | public_path('storage') => storage_path('app/public'), 71 | ], 72 | 73 | ]; 74 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/RoleController.php: -------------------------------------------------------------------------------- 1 | paginate($request->input('per_page', 10)); 25 | return RoleResource::collection($list); 26 | } 27 | 28 | /** 29 | * Store a newly created resource in storage. 30 | * 31 | * @param \Illuminate\Http\Request $request 32 | * @return \Illuminate\Http\Response 33 | */ 34 | public function store(Request $request) 35 | { 36 | // 37 | } 38 | 39 | /** 40 | * Display the specified resource. 41 | * 42 | * @param Role 43 | * @return \Illuminate\Http\Response 44 | */ 45 | public function show(Role $role) 46 | { 47 | // 48 | } 49 | 50 | /** 51 | * Update the specified resource in storage. 52 | * 53 | * @param \Illuminate\Http\Request $request 54 | * @param Role $role 55 | * @return RoleResource|\Illuminate\Http\JsonResponse 56 | */ 57 | public function update(Request $request, Role $role) 58 | { 59 | if ($role === null || $role->isAdmin()) { 60 | return responseFailed('Role not found', Response::HTTP_NOT_FOUND); 61 | } 62 | 63 | $permissionIds = $request->get('permissions', []); 64 | $permissions = Permission::allowed()->whereIn('id', $permissionIds)->get(); 65 | $role->syncPermissions($permissions); 66 | $role->save(); 67 | return new RoleResource($role); 68 | } 69 | 70 | /** 71 | * Remove the specified resource from storage. 72 | * 73 | * @param int $id 74 | * @return \Illuminate\Http\Response 75 | */ 76 | public function destroy($id) 77 | { 78 | // 79 | } 80 | 81 | /** 82 | * Get permissions from role 83 | * 84 | * @param Role $role 85 | * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection 86 | */ 87 | public function permissions(Role $role) 88 | { 89 | return PermissionResource::collection($role->permissions); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/Models/Acl.php: -------------------------------------------------------------------------------- 1 | getConstants(); 37 | $permissions = Arr::where($constants, function($value, $key) use ($exclusives) { 38 | return !in_array($value, $exclusives) && Str::startsWith($key, 'PERMISSION_'); 39 | }); 40 | 41 | return array_values($permissions); 42 | } catch (\ReflectionException $exception) { 43 | return []; 44 | } 45 | } 46 | 47 | public static function menuPermissions(): array 48 | { 49 | try { 50 | $class = new \ReflectionClass(__CLASS__); 51 | $constants = $class->getConstants(); 52 | $permissions = Arr::where($constants, function($value, $key) { 53 | return Str::startsWith($key, 'PERMISSION_VIEW_MENU_'); 54 | }); 55 | 56 | return array_values($permissions); 57 | } catch (\ReflectionException $exception) { 58 | return []; 59 | } 60 | } 61 | 62 | /** 63 | * @return array 64 | */ 65 | public static function roles(): array 66 | { 67 | try { 68 | $class = new \ReflectionClass(__CLASS__); 69 | $constants = $class->getConstants(); 70 | $roles = Arr::where($constants, function($value, $key) { 71 | return Str::startsWith($key, 'ROLE_'); 72 | }); 73 | 74 | return array_values($roles); 75 | } catch (\ReflectionException $exception) { 76 | return []; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /resources/js/styles/elemenet-style-overflow.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type='file'] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | .cell { 19 | .el-tag { 20 | margin-right: 0; 21 | } 22 | } 23 | 24 | .small-padding { 25 | .cell { 26 | padding-left: 5px; 27 | padding-right: 5px; 28 | } 29 | } 30 | 31 | .fixed-width { 32 | .el-button--mini { 33 | padding: 7px 10px; 34 | min-width: 60px; 35 | } 36 | } 37 | 38 | .status-col { 39 | .cell { 40 | padding: 0 10px; 41 | text-align: center; 42 | 43 | .el-tag { 44 | margin-right: 0px; 45 | } 46 | } 47 | } 48 | 49 | // to fixed https://github.com/ElemeFE/element/issues/2461 50 | .el-dialog { 51 | transform: none; 52 | left: 0; 53 | position: relative; 54 | margin: 0 auto; 55 | } 56 | 57 | // refine element ui upload 58 | .upload-container { 59 | .el-upload { 60 | width: 100%; 61 | 62 | .el-upload-dragger { 63 | width: 100%; 64 | height: 200px; 65 | } 66 | } 67 | } 68 | // dropdown 69 | //.el-dropdown-menu { 70 | // a { 71 | // display: block 72 | // } 73 | //} 74 | 75 | // fix date-picker ui bug in filter-item 76 | .el-range-editor.el-input__inner { 77 | display: inline-flex !important; 78 | } 79 | 80 | // to fix el-date-picker css style 81 | .el-range-separator { 82 | box-sizing: content-box; 83 | } 84 | 85 | /* 86 | element-ui 样式重置 87 | */ 88 | .elODialogModal { 89 | .el-dialog { 90 | margin-top: 7vh !important; 91 | } 92 | .el-dialog__header { 93 | border-bottom: 1px solid #ddd; 94 | } 95 | .el-dialog__body { 96 | padding-top: 10px; 97 | overflow-y: auto; 98 | } 99 | } 100 | .elODialogModalBodyH60vh { 101 | .el-dialog__body { 102 | min-height: 50vh; 103 | max-height: 65vh; 104 | } 105 | } 106 | 107 | .elODialogModalBodyH70vh { 108 | .el-dialog__body { 109 | min-height: 60vh; 110 | max-height: 70vh; 111 | } 112 | } 113 | 114 | .elODialogModalBodyH40vh { 115 | .el-dialog__body { 116 | min-height: 40vh; 117 | max-height: 55vh; 118 | } 119 | } 120 | .elODialogModalBodyW80vh { 121 | .el-dialog__body { 122 | width: 80vh; 123 | } 124 | } 125 | 126 | .elFormItemMarginB0px { 127 | .el-form-item { 128 | margin-bottom: 12px !important; 129 | } 130 | } 131 | 132 | .el-button [class*=el-icon]+span { 133 | margin-left: 5px; 134 | } -------------------------------------------------------------------------------- /resources/js/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ generateTitle(item.meta.title) }} 7 | 8 | {{ generateTitle(item.meta.title) }} 9 | 10 | 11 | 12 | 13 | 14 | 77 | 78 | 91 | -------------------------------------------------------------------------------- /resources/js/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import {ElMessage} from 'element-plus' 3 | import NProgress from 'nprogress' // progress bar 4 | import 'nprogress/nprogress.css' // progress bar style 5 | import {isLogged} from '@/utils/auth' 6 | import getPageTitle from '@/utils/get-page-title' 7 | import {userStore} from "@/store/user" 8 | import {permissionStore} from "@/store/permission" 9 | 10 | NProgress.configure({showSpinner: false}) // NProgress Configuration 11 | 12 | const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist 13 | 14 | router.beforeEach(async (to, from, next) => { 15 | // start progress bar 16 | NProgress.start() 17 | // set page title 18 | document.title = getPageTitle(to.meta.title) 19 | 20 | // determine whether the user has logged in 21 | const isUserLogged = isLogged() 22 | const useUserStore = userStore() 23 | const usePermissionStore = permissionStore() 24 | 25 | if (isUserLogged) { 26 | if (to.path === '/login') { 27 | // if is logged in, redirect to the home page 28 | next({path: '/'}) 29 | NProgress.done() 30 | } else { 31 | // determine whether the user has obtained his permission roles through getInfo 32 | const hasRoles = useUserStore.roles && useUserStore.roles.length > 0 33 | if (hasRoles) { 34 | next() 35 | } else { 36 | try { 37 | // get user info 38 | // note: roles must be a object array! such as: ['admin'] or ,['manager','editor'] 39 | const {roles, permissions} = await useUserStore.getInfo() 40 | 41 | // generate accessible routes map based on roles 42 | const accessRoutes = await usePermissionStore.generateRoutes(roles, permissions) 43 | accessRoutes.forEach((item) => { 44 | router.addRoute(item) 45 | }) 46 | next({...to, replace: true}) 47 | } catch (error) { 48 | // remove token and go to login page to re-login 49 | await useUserStore.resetToken() 50 | ElMessage.error(error.message || 'Has Error') 51 | next(`/login?redirect=${to.path}`) 52 | NProgress.done() 53 | } 54 | } 55 | } 56 | } else { 57 | /* has no token*/ 58 | 59 | if (whiteList.indexOf(to.matched[0] ? to.matched[0].path : '') !== -1) { 60 | // in the free login whitelist, go directly 61 | next() 62 | } else { 63 | // other pages that do not have permission to access are redirected to the login page. 64 | next(`/login?redirect=${to.path}`) 65 | NProgress.done() 66 | } 67 | } 68 | }) 69 | 70 | router.afterEach(() => { 71 | // finish progress bar 72 | NProgress.done() 73 | }) -------------------------------------------------------------------------------- /resources/js/directive/waves/waves.js: -------------------------------------------------------------------------------- 1 | import './waves.css'; 2 | 3 | const context = '@@wavesContext'; 4 | 5 | function handleClick(el, binding) { 6 | function handle(e) { 7 | const customOpts = Object.assign({}, binding.value); 8 | const opts = Object.assign( 9 | { 10 | ele: el, // Main wave element 11 | type: 'hit', // hit: Wave at click location. center: Center point 12 | color: 'rgba(0, 0, 0, 0.15)', // Ripple color 13 | }, 14 | customOpts 15 | ); 16 | const target = opts.ele; 17 | if (target) { 18 | target.style.position = 'relative'; 19 | target.style.overflow = 'hidden'; 20 | const rect = target.getBoundingClientRect(); 21 | let ripple = target.querySelector('.waves-ripple'); 22 | if (!ripple) { 23 | ripple = document.createElement('span'); 24 | ripple.className = 'waves-ripple'; 25 | ripple.style.height = ripple.style.width = 26 | Math.max(rect.width, rect.height) + 'px'; 27 | target.appendChild(ripple); 28 | } else { 29 | ripple.className = 'waves-ripple'; 30 | } 31 | switch (opts.type) { 32 | case 'center': 33 | ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'; 34 | ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px'; 35 | break; 36 | default: 37 | ripple.style.top = 38 | (e.pageY - 39 | rect.top - 40 | ripple.offsetHeight / 2 - 41 | document.documentElement.scrollTop || document.body.scrollTop) + 42 | 'px'; 43 | ripple.style.left = 44 | (e.pageX - 45 | rect.left - 46 | ripple.offsetWidth / 2 - 47 | document.documentElement.scrollLeft || document.body.scrollLeft) + 48 | 'px'; 49 | } 50 | ripple.style.backgroundColor = opts.color; 51 | ripple.className = 'waves-ripple z-active'; 52 | return false; 53 | } 54 | } 55 | 56 | if (!el[context]) { 57 | el[context] = { 58 | removeHandle: handle, 59 | }; 60 | } else { 61 | el[context].removeHandle = handle; 62 | } 63 | 64 | return handle; 65 | } 66 | 67 | export default { 68 | bind(el, binding) { 69 | el.addEventListener('click', handleClick(el, binding), false); 70 | }, 71 | update(el, binding) { 72 | el.removeEventListener('click', el[context].removeHandle, true); 73 | el.addEventListener('click', handleClick(el, binding), false); 74 | }, 75 | unbind(el) { 76 | el.removeEventListener('click', el[context].removeHandle, false); 77 | el[context] = null; 78 | delete el[context]; 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /resources/js/layout/components/Sidebar/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 69 | 70 | 93 | -------------------------------------------------------------------------------- /config/sanctum.php: -------------------------------------------------------------------------------- 1 | explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1')), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Sanctum Guards 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This array contains the authentication guards that will be checked when 24 | | Sanctum is trying to authenticate a request. If none of these guards 25 | | are able to authenticate the request, Sanctum will use the bearer 26 | | token that's present on an incoming request for authentication. 27 | | 28 | */ 29 | 30 | 'guard' => ['web'], 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | Expiration Minutes 35 | |-------------------------------------------------------------------------- 36 | | 37 | | This value controls the number of minutes until an issued token will be 38 | | considered expired. If this value is null, personal access tokens do 39 | | not expire. This won't tweak the lifetime of first-party sessions. 40 | | 41 | */ 42 | 43 | 'expiration' => null, 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Sanctum Middleware 48 | |-------------------------------------------------------------------------- 49 | | 50 | | When authenticating your first-party SPA with Sanctum you may need to 51 | | customize some of the middleware Sanctum uses while processing the 52 | | request. You may change the middleware listed below as required. 53 | | 54 | */ 55 | 56 | 'middleware' => [ 57 | 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, 58 | 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, 59 | ], 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Sanctum Prefix 64 | |-------------------------------------------------------------------------- 65 | | 66 | | Move CSRF protection to /api prefix for easier in cors configuration 67 | | 68 | */ 69 | 'prefix' => 'api/sanctum' 70 | ]; 71 | -------------------------------------------------------------------------------- /resources/js/views/dashboard/Admin/components/LineChart.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /resources/js/directive/el-drag-dialog/drag.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bind(el, binding, vnode) { 3 | const dialogHeaderEl = el.querySelector('.el-dialog__header'); 4 | const dragDom = el.querySelector('.el-dialog'); 5 | dialogHeaderEl.style.cssText += ';cursor:move;'; 6 | dragDom.style.cssText += ';top:0px;'; 7 | 8 | const getStyle = (function() { 9 | if (window.document.currentStyle) { 10 | return (dom, attr) => dom.currentStyle[attr]; 11 | } else { 12 | return (dom, attr) => getComputedStyle(dom, false)[attr]; 13 | } 14 | })(); 15 | 16 | dialogHeaderEl.onmousedown = (e) => { 17 | // Mouse down to calculate the distance of the current element from the viewable area 18 | const disX = e.clientX - dialogHeaderEl.offsetLeft; 19 | const disY = e.clientY - dialogHeaderEl.offsetTop; 20 | 21 | const dragDomWidth = dragDom.offsetWidth; 22 | const dragDomHeight = dragDom.offsetHeight; 23 | 24 | const screenWidth = document.body.clientWidth; 25 | const screenHeight = document.body.clientHeight; 26 | 27 | const minDragDomLeft = dragDom.offsetLeft; 28 | const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth; 29 | 30 | const minDragDomTop = dragDom.offsetTop; 31 | const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight; 32 | 33 | // Get the value with px regular match replacement 34 | let styL = getStyle(dragDom, 'left'); 35 | let styT = getStyle(dragDom, 'top'); 36 | 37 | if (styL.includes('%')) { 38 | styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100); 39 | styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100); 40 | } else { 41 | styL = +styL.replace(/\px/g, ''); 42 | styT = +styT.replace(/\px/g, ''); 43 | } 44 | 45 | document.onmousemove = function(e) { 46 | // Calculate the distance moved by event delegate 47 | let left = e.clientX - disX; 48 | let top = e.clientY - disY; 49 | 50 | // Boundary processing 51 | if (-(left) > minDragDomLeft) { 52 | left = -minDragDomLeft; 53 | } else if (left > maxDragDomLeft) { 54 | left = maxDragDomLeft; 55 | } 56 | 57 | if (-(top) > minDragDomTop) { 58 | top = -minDragDomTop; 59 | } else if (top > maxDragDomTop) { 60 | top = maxDragDomTop; 61 | } 62 | 63 | // Move current element 64 | dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`; 65 | 66 | // emit onDrag event 67 | vnode.child.$emit('dragDialog'); 68 | }; 69 | 70 | document.onmouseup = function(e) { 71 | document.onmousemove = null; 72 | document.onmouseup = null; 73 | }; 74 | }; 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /resources/js/views/dashboard/TextHoverEffect/Mallki.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ text }} 4 | 5 | 6 | 7 | 8 | 9 | 23 | 24 | 114 | --------------------------------------------------------------------------------