first commit
This commit is contained in:
3
resources/admin/App.vue
Normal file
3
resources/admin/App.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
7
resources/admin/app.ts
Normal file
7
resources/admin/app.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import '/admin/styles/index.scss'
|
||||
|
||||
import CatchAdmin from './support/catchAdmin'
|
||||
|
||||
const admin = new CatchAdmin()
|
||||
|
||||
admin.bootstrap()
|
BIN
resources/admin/assets/404.png
Normal file
BIN
resources/admin/assets/404.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
35
resources/admin/assets/enum/app.ts
Normal file
35
resources/admin/assets/enum/app.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 服务端返回码
|
||||
*/
|
||||
export const enum Code {
|
||||
SUCCESS = 10000, // 成功
|
||||
LOST_LOGIN = 10001, // 登录失效
|
||||
VALIDATE_FAILED = 10002, // 验证错误
|
||||
PERMISSION_FORBIDDEN = 10003, // 权限禁止
|
||||
LOGIN_FAILED = 10004, // 登录失败
|
||||
FAILED = 10005, // 操作失败
|
||||
LOGIN_EXPIRED = 10006, // 登录失效
|
||||
LOGIN_BLACKLIST = 10007, // 黑名单
|
||||
USER_FORBIDDEN = 10008, // 账户被禁
|
||||
WECHAT_RESPONSE_ERROR = 40000,
|
||||
}
|
||||
|
||||
/**
|
||||
* 白名单页面
|
||||
*
|
||||
* 不需要权限认证
|
||||
*/
|
||||
export const enum WhiteListPage {
|
||||
LOGIN_PATH = '/login',
|
||||
|
||||
NOT_FOUND_PATH = '/404'
|
||||
}
|
||||
|
||||
/**
|
||||
* menu 类型
|
||||
*/
|
||||
export const enum MenuType {
|
||||
PAGE_TYPE = 1,
|
||||
|
||||
Button_Type
|
||||
}
|
BIN
resources/admin/assets/login-left.png
Normal file
BIN
resources/admin/assets/login-left.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
BIN
resources/admin/assets/logo.png
Normal file
BIN
resources/admin/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
27
resources/admin/components/404/index.vue
Normal file
27
resources/admin/components/404/index.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div :style="bgColor" class="flex flex-col w-full">
|
||||
<img src="/admin/assets/404.png" class="w-full sm:w-3/5 m-auto" />
|
||||
<div class="mr-auto w-full bottom-0 m-auto">
|
||||
<div class="w-full text-center text-base text-gray-400">抱歉,您访问的页面不存在</div>
|
||||
<div @click="push('/')" class="text-center w-full mt-2">
|
||||
<el-button type="primary">回到首页</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '/admin/stores/modules/app'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { push } = useRouter()
|
||||
|
||||
const dark: string = '#161d31;'
|
||||
const light: string = 'rgb(241,245,249);'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const bgColor = computed(() => {
|
||||
return 'background-color:' + (appStore.getIsDarkMode ? dark : light)
|
||||
})
|
||||
</script>
|
97
resources/admin/components/HelloWorld.vue
Normal file
97
resources/admin/components/HelloWorld.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-table :data="tableData" style="width: 100%">
|
||||
<el-table-column prop="date" label="Date" width="180" />
|
||||
<el-table-column prop="name" label="Name" width="180" />
|
||||
<el-table-column prop="address" label="Address" />
|
||||
</el-table>
|
||||
|
||||
<el-button text @click="visible = true">Click to open the Dialog</el-button>
|
||||
<ct-dialog v-model="visible" title="show me the code" :show-footer="true">
|
||||
<el-form :model="form">
|
||||
<el-form-item label="Promotion name" :label-width="formLabelWidth">
|
||||
<el-input v-model="form.name" autocomplete="off" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Zones" :label-width="formLabelWidth">
|
||||
<el-select v-model="form.region" placeholder="Please select a zone">
|
||||
<el-option label="Zone No.1" value="shanghai" />
|
||||
<el-option label="Zone No.2" value="beijing" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ct-dialog>
|
||||
|
||||
<el-tabs v-model="activeName" type="border-card" class="demo-tabs" @tab-click="handleClick">
|
||||
<el-tab-pane label="User" name="first">User</el-tab-pane>
|
||||
<el-tab-pane label="Config" name="second">Config</el-tab-pane>
|
||||
<el-tab-pane label="Role" name="third">Role</el-tab-pane>
|
||||
<el-tab-pane label="Task" name="fourth">Task</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CtDialog from './admin/Dialog.vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
import type { TabsPaneContext } from 'element-plus'
|
||||
const visible = ref(false)
|
||||
const formLabelWidth = '140px'
|
||||
|
||||
const activeName = ref('first')
|
||||
|
||||
const handleClick = (tab: TabsPaneContext, event: Event) => {
|
||||
console.log(tab, event)
|
||||
}
|
||||
const form = reactive({
|
||||
name: '',
|
||||
region: '',
|
||||
date1: '',
|
||||
date2: '',
|
||||
delivery: false,
|
||||
type: [],
|
||||
resource: '',
|
||||
desc: ''
|
||||
})
|
||||
const tableData = [
|
||||
{
|
||||
date: '2016-05-03',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles'
|
||||
},
|
||||
{
|
||||
date: '2016-05-02',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles'
|
||||
},
|
||||
{
|
||||
date: '2016-05-04',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles'
|
||||
},
|
||||
{
|
||||
date: '2016-05-01',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles'
|
||||
},
|
||||
{
|
||||
date: '2016-05-03',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles'
|
||||
},
|
||||
{
|
||||
date: '2016-05-02',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles'
|
||||
},
|
||||
{
|
||||
date: '2016-05-04',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
<style scoped>
|
||||
.dialog-footer button:first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
49
resources/admin/components/admin/Select/index.vue
Normal file
49
resources/admin/components/admin/Select/index.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<el-select v-bind="$attrs">
|
||||
<el-option-group v-for="group in elOptions" :key="group.label" :label="group.label" v-if="group">
|
||||
<el-option v-for="item in group.options" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-option-group>
|
||||
<el-option v-for="option in elOptions" :key="option.value" :label="option.label" :value="option.value" v-else>
|
||||
<slot />
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import http from '/admin/support/http'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
api: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
group: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
interface Option {
|
||||
label: string
|
||||
value: string | number
|
||||
}
|
||||
|
||||
interface GroupOption {
|
||||
label: string
|
||||
options: Array<Option>
|
||||
}
|
||||
|
||||
const elOptions = props.group ? ref<Array<GroupOption>>() : ref<Array<Option>>()
|
||||
if (props.api) {
|
||||
http.get('options/' + props.api).then(r => {
|
||||
elOptions.value = r.data.data
|
||||
})
|
||||
} else {
|
||||
elOptions.value = props.options
|
||||
}
|
||||
</script>
|
16
resources/admin/components/admin/buttons/add.vue
Normal file
16
resources/admin/components/admin/buttons/add.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<el-button type="primary" :size="size"><Icon name="plus" class="w-4 mr-1" /> {{ text }}</el-button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '新增',
|
||||
},
|
||||
})
|
||||
</script>
|
16
resources/admin/components/admin/buttons/destroy.vue
Normal file
16
resources/admin/components/admin/buttons/destroy.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<el-button type="danger" :size="size"><Icon name="trash" class="w-4 mr-1" /> {{ text }}</el-button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'small',
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '删除',
|
||||
},
|
||||
})
|
||||
</script>
|
16
resources/admin/components/admin/buttons/show.vue
Normal file
16
resources/admin/components/admin/buttons/show.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<el-button type="primary" :size="size"><Icon name="eye" class="w-4 mr-1" /> {{ text }}</el-button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'small',
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '详情',
|
||||
},
|
||||
})
|
||||
</script>
|
18
resources/admin/components/admin/buttons/update.vue
Normal file
18
resources/admin/components/admin/buttons/update.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<el-button type="success" :size="size"><Icon name="pencil-square" class="w-4 mr-1" /> {{ text }}</el-button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'small',
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '更新',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
100
resources/admin/components/admin/dialog/index.vue
Normal file
100
resources/admin/components/admin/dialog/index.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog :model-value="modelValue" :show-close="false" :fullscreen="isFullscreen" v-bind="$attrs" :width="width" :close="close" :before-close="beforeClose" draggable>
|
||||
<template #header="{ titleId, titleClass }">
|
||||
<div class="flex justify-between w-full">
|
||||
<div>
|
||||
<h4 :id="titleId" :class="titleClass">{{ title }}</h4>
|
||||
</div>
|
||||
<div class="flex w-14 justify-between">
|
||||
<Icon :name="fullscreenIcon" @click="fullscreen" class="hover:cursor-pointer" />
|
||||
<Icon name="x-mark" class="hover:cursor-pointer" @click="close" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<slot />
|
||||
<template #footer v-if="showFooter">
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="close">{{ $t('system.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="close">{{ $t('system.confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
require: true,
|
||||
},
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
width: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
|
||||
const isFullscreen = ref(false)
|
||||
|
||||
const fullscreenIcon = computed(() => {
|
||||
return isFullscreen.value ? 'arrows-pointing-in' : 'arrows-pointing-out'
|
||||
})
|
||||
const fullscreen = () => {
|
||||
isFullscreen.value = !isFullscreen.value
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
emits('update:modelValue', false)
|
||||
}
|
||||
|
||||
// 遮罩关闭调用
|
||||
const beforeClose = () => {
|
||||
emits('update:modelValue', false)
|
||||
}
|
||||
const width = ref<string>('')
|
||||
|
||||
onMounted(() => {
|
||||
width.value = props.width ? props.width : getWidth()
|
||||
})
|
||||
|
||||
// 窗口尺寸
|
||||
const getWidth = () => {
|
||||
const clientWidth = window.document.body.clientWidth
|
||||
|
||||
if (clientWidth <= 726) {
|
||||
return '100%'
|
||||
}
|
||||
|
||||
if (clientWidth > 726 && clientWidth < 1440) {
|
||||
return '60%'
|
||||
}
|
||||
|
||||
return '650px'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 0.5rem;
|
||||
.el-dialog__header {
|
||||
margin-right: 0 !important;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
}
|
||||
</style>
|
24
resources/admin/components/admin/status/index.vue
Normal file
24
resources/admin/components/admin/status/index.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<el-switch @change="enabled(api, id)" :active-value="Status.ENABLE" :inactive-value="Status.DISABLE" :model-value="modelValue" :loading="loading" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useEnabled } from '/admin/composables/curd/useEnabled'
|
||||
import { Status } from '/admin/enum/app'
|
||||
import { watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: Boolean | Number | String,
|
||||
api: String,
|
||||
id: Number | String,
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
|
||||
const { enabled, success, loading } = useEnabled()
|
||||
|
||||
watch(success, function () {
|
||||
emits('update:modelValue', props.modelValue === Status.ENABLE ? Status.DISABLE : Status.ENABLE)
|
||||
success.value = false
|
||||
})
|
||||
</script>
|
59
resources/admin/components/breadcrumbs/index.vue
Normal file
59
resources/admin/components/breadcrumbs/index.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<el-breadcrumb separator="/" class="flex">
|
||||
<transition-group name="breadcrumb">
|
||||
<!--<el-breadcrumb-item :to="{ path: '/' }" class="text-blue=">Dashboard</el-breadcrumb-item>-->
|
||||
|
||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="index" class="text">{{ item }}</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import router from '/admin/router'
|
||||
import { watch, onMounted, ref } from 'vue'
|
||||
import { useAppStore } from '/admin/stores/modules/app'
|
||||
import { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const breadcrumbs = ref<string[]>([])
|
||||
|
||||
// 监听当前路由的变化
|
||||
watch(router.currentRoute, (newValue, oldValue) => {
|
||||
// 如果是内页,则不切换激活菜单
|
||||
if (newValue.meta.is_inner === undefined || !newValue.meta.is_inner) {
|
||||
appStore.setActiveMenu(newValue.path)
|
||||
}
|
||||
getBreadcrumbs(newValue)
|
||||
})
|
||||
|
||||
// get init breadcrumb
|
||||
onMounted(() => {
|
||||
if (router.currentRoute.value.path !== '/') {
|
||||
appStore.setActiveMenu(router.currentRoute.value.path)
|
||||
}
|
||||
|
||||
getBreadcrumbs(router.currentRoute.value)
|
||||
})
|
||||
|
||||
// get breadcrums
|
||||
function getBreadcrumbs(newRoute: RouteLocationNormalizedLoaded) {
|
||||
breadcrumbs.value = []
|
||||
breadcrumbs.value.push('首页')
|
||||
newRoute.matched.forEach(m => {
|
||||
if (m.meta.title !== undefined) {
|
||||
breadcrumbs.value.push(m.meta?.title as string)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.breadcrumb-leave-active {
|
||||
transition: all 1s linear;
|
||||
}
|
||||
|
||||
.breadcrumb-leave-to {
|
||||
opacity: 0;
|
||||
transition: all 0.3s linear;
|
||||
}
|
||||
</style>
|
23
resources/admin/components/icon/index.vue
Normal file
23
resources/admin/components/icon/index.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<component :is="icon" class="w-5 h-5" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import * as heroIcons from '@heroicons/vue/24/outline'
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const icon = computed(() => {
|
||||
let name = ''
|
||||
props.name.split('-').forEach(v => {
|
||||
name += v[0].toUpperCase() + v.substr(1)
|
||||
})
|
||||
return heroIcons[name + 'Icon']
|
||||
})
|
||||
</script>
|
69
resources/admin/composables/curd/useCreate.ts
Normal file
69
resources/admin/composables/curd/useCreate.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import http from '/admin/support/http'
|
||||
import { ref, unref } from 'vue'
|
||||
import { Code } from '/admin/enum/app'
|
||||
import Message from '/admin/support/message'
|
||||
import { FormInstance } from 'element-plus'
|
||||
import { AxiosResponse } from 'axios'
|
||||
import { isFunction } from'/admin/support/helper'
|
||||
|
||||
// get table list
|
||||
export function useCreate(path: string, id: string | number | null = null, _formData: object = {}) {
|
||||
const formData = ref<object>(_formData)
|
||||
|
||||
const loading = ref<boolean>()
|
||||
const isClose = ref<boolean>(false)
|
||||
|
||||
// 创建前 hook
|
||||
const beforeCreate = ref()
|
||||
// 更新前 hook
|
||||
const beforeUpdate = ref()
|
||||
|
||||
// store
|
||||
function store(path: string, id: string | number | null = null) {
|
||||
loading.value = true
|
||||
let promise: Promise<AxiosResponse> | null = null
|
||||
if (id) {
|
||||
if (isFunction(beforeUpdate.value)) {
|
||||
beforeUpdate.value()
|
||||
}
|
||||
|
||||
promise = http.put(path + '/' + id, unref(formData))
|
||||
} else {
|
||||
console.log(isFunction(beforeCreate.value), beforeCreate.value)
|
||||
if (isFunction(beforeCreate.value)) {
|
||||
beforeCreate.value()
|
||||
}
|
||||
|
||||
promise = http.post(path, unref(formData))
|
||||
}
|
||||
|
||||
promise
|
||||
.then(r => {
|
||||
if (r.data.code === Code.SUCCESS) {
|
||||
isClose.value = true
|
||||
Message.success(r.data.message)
|
||||
} else {
|
||||
Message.error(r.data.message)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const form = ref<FormInstance>()
|
||||
const submitForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl
|
||||
.validate(valid => {
|
||||
if (valid) {
|
||||
store(path, id)
|
||||
} else {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
.then(() => {})
|
||||
}
|
||||
|
||||
return { formData, loading, form, submitForm, isClose, beforeCreate, beforeUpdate }
|
||||
}
|
36
resources/admin/composables/curd/useDestroy.ts
Normal file
36
resources/admin/composables/curd/useDestroy.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import http from '/admin/support/http'
|
||||
import { Code } from '/admin/enum/app'
|
||||
import Message from '/admin/support/message'
|
||||
import { ref } from 'vue'
|
||||
import { isFunction } from'/admin/support/helper'
|
||||
|
||||
export function useDestroy(confirm: string = '确认删除吗') {
|
||||
const isDeleted = ref(false)
|
||||
|
||||
const beforeDestroy = ref()
|
||||
|
||||
// fetch list
|
||||
function destroy(path: string, id: string | number) {
|
||||
Message.confirm(confirm + '?', function () {
|
||||
|
||||
// before destroy
|
||||
if (isFunction(beforeDestroy.value)) {
|
||||
beforeDestroy.value()
|
||||
}
|
||||
|
||||
http
|
||||
.delete(path + '/' + id)
|
||||
.then(r => {
|
||||
if (r.data.code === Code.SUCCESS) {
|
||||
Message.success(r.data.message)
|
||||
isDeleted.value = true
|
||||
} else {
|
||||
Message.error(r.data.message)
|
||||
}
|
||||
})
|
||||
.finally(() => {})
|
||||
})
|
||||
}
|
||||
|
||||
return { destroy, isDeleted }
|
||||
}
|
27
resources/admin/composables/curd/useEnabled.ts
Normal file
27
resources/admin/composables/curd/useEnabled.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import http from '/admin/support/http'
|
||||
import { Code } from '/admin/assets/enum/app'
|
||||
import Message from '/admin/support/message'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export function useEnabled() {
|
||||
const success = ref(false)
|
||||
const loading = ref<boolean>(false)
|
||||
function enabled(path: string, id: string | number, data: object = {}) {
|
||||
loading.value = true
|
||||
http
|
||||
.put(path + '/enable/' + id, data)
|
||||
.then(r => {
|
||||
if (r.data.code === Code.SUCCESS) {
|
||||
success.value = true
|
||||
Message.success(r.data.message)
|
||||
} else {
|
||||
Message.error(r.data.message)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
return { enabled, success, loading }
|
||||
}
|
75
resources/admin/composables/curd/useGetList.ts
Normal file
75
resources/admin/composables/curd/useGetList.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import http from '/admin/support/http'
|
||||
import { ref, unref } from 'vue'
|
||||
import { Code } from '/admin/enum/app'
|
||||
import Message from '/admin/support/message'
|
||||
|
||||
const initLimit = 10
|
||||
const initPage = 1;
|
||||
|
||||
// get table list
|
||||
export function useGetList(path: string) {
|
||||
const data = ref<object>()
|
||||
const page = ref(initPage)
|
||||
const limit = ref(initLimit)
|
||||
const query = ref<object>({
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
})
|
||||
const loading = ref(true)
|
||||
// fetch list
|
||||
function getList() {
|
||||
// when table's data page >= 100, it will loading
|
||||
if (page.value >= 100) {
|
||||
loading.value = true
|
||||
}
|
||||
http
|
||||
.get(path, unref(query))
|
||||
.then(r => {
|
||||
closeLoading()
|
||||
if (r.data.code === Code.SUCCESS) {
|
||||
data.value = r.data
|
||||
} else {
|
||||
Message.error(r.data.message)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
closeLoading()
|
||||
})
|
||||
}
|
||||
|
||||
// close loading
|
||||
function closeLoading() {
|
||||
loading.value = false
|
||||
}
|
||||
// search
|
||||
function search() {
|
||||
getList()
|
||||
}
|
||||
|
||||
// reset
|
||||
function reset() {
|
||||
query.value = Object.assign({ page: page.value, limit: limit.value })
|
||||
|
||||
getList()
|
||||
}
|
||||
|
||||
// change page
|
||||
function changePage(p: number) {
|
||||
page.value = p
|
||||
// @ts-ignore
|
||||
query.value.page = p
|
||||
search()
|
||||
}
|
||||
|
||||
// change limit
|
||||
function changeLimit(l: number) {
|
||||
limit.value = l
|
||||
// @ts-ignore
|
||||
query.value.page = 1
|
||||
// @ts-ignore
|
||||
query.value.limit = l
|
||||
search()
|
||||
}
|
||||
|
||||
return { data, query, search, reset, changePage, changeLimit, loading }
|
||||
}
|
14
resources/admin/composables/curd/useShow.ts
Normal file
14
resources/admin/composables/curd/useShow.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import http from '/admin/support/http'
|
||||
|
||||
export function useShow(path: string, id: string | number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
http
|
||||
.get(path + '/' + id)
|
||||
.then(response => {
|
||||
resolve(response.data)
|
||||
})
|
||||
.catch(e => {
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
}
|
43
resources/admin/enum/app.ts
Normal file
43
resources/admin/enum/app.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 服务端返回码
|
||||
*/
|
||||
export const enum Code {
|
||||
SUCCESS = 10000, // 成功
|
||||
LOST_LOGIN = 10001, // 登录失效
|
||||
VALIDATE_FAILED = 10002, // 验证错误
|
||||
PERMISSION_FORBIDDEN = 10003, // 权限禁止
|
||||
LOGIN_FAILED = 10004, // 登录失败
|
||||
FAILED = 10005, // 操作失败
|
||||
LOGIN_EXPIRED = 10006, // 登录失效
|
||||
LOGIN_BLACKLIST = 10007, // 黑名单
|
||||
USER_FORBIDDEN = 10008, // 账户被禁
|
||||
WECHAT_RESPONSE_ERROR = 40000,
|
||||
}
|
||||
|
||||
/**
|
||||
* status
|
||||
*/
|
||||
export const enum Status {
|
||||
ENABLE = 1,
|
||||
DISABLE = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* 白名单页面
|
||||
*
|
||||
* 不需要权限认证
|
||||
*/
|
||||
export const enum WhiteListPage {
|
||||
LOGIN_PATH = '/login',
|
||||
|
||||
NOT_FOUND_PATH = '/404',
|
||||
}
|
||||
|
||||
/**
|
||||
* menu 类型
|
||||
*/
|
||||
export const enum MenuType {
|
||||
PAGE_TYPE = 1,
|
||||
|
||||
Button_Type,
|
||||
}
|
8
resources/admin/env.d.ts
vendored
Normal file
8
resources/admin/env.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
22
resources/admin/i18n/index.ts
Normal file
22
resources/admin/i18n/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import Cache from '/admin/support/cache'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import en from './languages/en'
|
||||
import zh from './languages/zh'
|
||||
import type { App } from 'vue'
|
||||
|
||||
const messages = {
|
||||
en,
|
||||
zh,
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: Cache.get('language') || 'zh',
|
||||
messages,
|
||||
globalInjection: true,
|
||||
})
|
||||
|
||||
export function bootstrapI18n(app: App): void {
|
||||
app.use(i18n)
|
||||
}
|
||||
|
||||
export default i18n
|
150
resources/admin/i18n/languages/en.ts
Normal file
150
resources/admin/i18n/languages/en.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
const en = {
|
||||
system: {
|
||||
name: 'CatchAdmin Dashboard',
|
||||
chinese: 'Chinese',
|
||||
english: 'English',
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
warning: 'Warning',
|
||||
next: 'Next',
|
||||
prev: 'Prev',
|
||||
yes: 'Y',
|
||||
no: 'N',
|
||||
add: 'Add',
|
||||
finish: 'Finish',
|
||||
back: 'Back',
|
||||
update: 'Update',
|
||||
},
|
||||
|
||||
login: {
|
||||
email: 'Email',
|
||||
password: 'Password',
|
||||
sign_in: 'Sign In',
|
||||
welcome: 'Welcome Back👏',
|
||||
lost_password: 'lost password?',
|
||||
remember: 'Remember me',
|
||||
verify: {
|
||||
email: {
|
||||
required: 'Please input email first',
|
||||
invalid: 'Email address is invalid',
|
||||
},
|
||||
|
||||
password: {
|
||||
required: 'Please input password first',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
register: {
|
||||
sign_up: 'Sign Up',
|
||||
},
|
||||
|
||||
generate: {
|
||||
schema: {
|
||||
title: 'Create Schema',
|
||||
name: 'Schema Name',
|
||||
name_verify: 'please input schema name',
|
||||
engine: {
|
||||
name: 'Search Engine',
|
||||
verify: 'please select schema engine',
|
||||
placeholder: 'select schema engine',
|
||||
},
|
||||
default_field: {
|
||||
name: 'Default Field',
|
||||
created_at: 'Create time',
|
||||
updated_at: 'Update Time',
|
||||
creator: 'Creator',
|
||||
delete_at: 'SoftDelete',
|
||||
},
|
||||
comment: {
|
||||
name: 'Schema Comment',
|
||||
verify: 'please input schema comment',
|
||||
},
|
||||
|
||||
structure: {
|
||||
title: 'Create Schema Structure',
|
||||
field_name: {
|
||||
name: 'Field Name',
|
||||
verify: 'please input field name',
|
||||
},
|
||||
length: 'Length',
|
||||
type: {
|
||||
name: 'Field Type',
|
||||
placeholder: 'select field type',
|
||||
verify: 'please select field type',
|
||||
},
|
||||
form_label: 'Form Label',
|
||||
form_component: 'Component',
|
||||
list: 'List',
|
||||
form: 'Form',
|
||||
unique: 'Unique',
|
||||
search: 'Search',
|
||||
search_op: {
|
||||
name: 'Search Operate',
|
||||
placeholder: 'select search operate',
|
||||
},
|
||||
nullable: 'Nullable',
|
||||
default: 'Default',
|
||||
rules: {
|
||||
name: 'Verify Rules',
|
||||
placeholder: 'select verify rules',
|
||||
},
|
||||
operate: 'Operate',
|
||||
comment: 'Field Comment',
|
||||
},
|
||||
},
|
||||
code: {
|
||||
title: 'Code Gen',
|
||||
module: {
|
||||
name: 'module',
|
||||
placeholder: 'please select module',
|
||||
verify: 'please select module first',
|
||||
},
|
||||
controller: {
|
||||
name: 'Controller',
|
||||
placeholder: 'please input controller name',
|
||||
verify: 'please input Controller name first',
|
||||
},
|
||||
model: {
|
||||
name: 'Model',
|
||||
placeholder: 'please input model name',
|
||||
verify: 'please input model name first',
|
||||
},
|
||||
paginate: 'Paginate',
|
||||
},
|
||||
},
|
||||
|
||||
module: {
|
||||
create: 'Create Module',
|
||||
update: 'Update Module',
|
||||
form: {
|
||||
name: {
|
||||
title: 'Module Name',
|
||||
required: 'module name required',
|
||||
},
|
||||
|
||||
path: {
|
||||
title: 'Module Path',
|
||||
required: 'module Path required',
|
||||
},
|
||||
|
||||
desc: {
|
||||
title: 'Description',
|
||||
},
|
||||
|
||||
keywords: {
|
||||
title: 'Keywords',
|
||||
},
|
||||
|
||||
dirs: {
|
||||
title: 'Default Dirs',
|
||||
Controller: 'Controller',
|
||||
Model: 'Model',
|
||||
Database: 'Database',
|
||||
Request: 'Request',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default en
|
155
resources/admin/i18n/languages/zh.ts
Normal file
155
resources/admin/i18n/languages/zh.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
const zh = {
|
||||
system: {
|
||||
name: 'CatchAdmin 管理系统',
|
||||
chinese: '中文',
|
||||
english: '英文',
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
warning: '警告',
|
||||
next: '下一步',
|
||||
prev: '上一步',
|
||||
yes: '是',
|
||||
no: '否',
|
||||
add: '新增',
|
||||
edit: '编辑',
|
||||
finish: '完成',
|
||||
back: '返回',
|
||||
update: '更新',
|
||||
},
|
||||
|
||||
login: {
|
||||
email: '邮箱',
|
||||
password: '密码',
|
||||
sign_in: '登录',
|
||||
welcome: '👏欢迎回来',
|
||||
lost_password: '忘记密码?',
|
||||
remember: '记住我',
|
||||
verify: {
|
||||
email: {
|
||||
required: '请先输入邮箱',
|
||||
invalid: '邮箱地址无效',
|
||||
},
|
||||
|
||||
password: {
|
||||
required: '请先输入密码',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
register: {
|
||||
sign_up: '注册',
|
||||
},
|
||||
generate: {
|
||||
schema: {
|
||||
title: '创建数据表',
|
||||
name: '表名称',
|
||||
name_verify: '请输入表名称',
|
||||
engine: {
|
||||
name: '表引擎',
|
||||
verify: '请选择表引擎',
|
||||
placeholder: '选择表引擎',
|
||||
},
|
||||
default_field: {
|
||||
name: '默认字段',
|
||||
created_at: '创建时间',
|
||||
updated_at: '更新时间',
|
||||
creator: '创建人',
|
||||
delete_at: '软删除',
|
||||
},
|
||||
comment: {
|
||||
name: '表注释',
|
||||
verify: '请填写表注释/说明',
|
||||
},
|
||||
|
||||
structure: {
|
||||
title: '创建数据结构',
|
||||
field_name: {
|
||||
name: '字段名称',
|
||||
verify: '请填写字段名称',
|
||||
},
|
||||
length: '长度',
|
||||
type: {
|
||||
name: '类型',
|
||||
placeholder: '选择字段类型',
|
||||
verify: '请先选择字段类型',
|
||||
},
|
||||
form_label: '表单 Label',
|
||||
form_component: '表单组件',
|
||||
list: '列表',
|
||||
form: '表单',
|
||||
unique: '唯一',
|
||||
search: '查询',
|
||||
search_op: {
|
||||
name: '搜索操作符',
|
||||
placeholder: '选择搜索操作符',
|
||||
},
|
||||
nullable: 'nullable',
|
||||
default: '默认值',
|
||||
rules: {
|
||||
name: '验证规则',
|
||||
placeholder: '选择验证规则',
|
||||
},
|
||||
operate: '操作',
|
||||
comment: '字段注释',
|
||||
},
|
||||
},
|
||||
code: {
|
||||
title: '生成代码',
|
||||
module: {
|
||||
name: '模块',
|
||||
placeholder: '请选择模块',
|
||||
verify: '请选择模块',
|
||||
},
|
||||
controller: {
|
||||
name: '控制器',
|
||||
placeholder: '请输入控制器名称',
|
||||
verify: '请输入控制器名称',
|
||||
},
|
||||
model: {
|
||||
name: '模型',
|
||||
placeholder: '请输入模型名称',
|
||||
verify: '请输入模型名称',
|
||||
},
|
||||
paginate: '分页',
|
||||
menu: {
|
||||
name: '菜单名称',
|
||||
placeholder: '请输入菜单名称',
|
||||
verify: '请输入菜单名称',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
module: {
|
||||
create: '创建模块',
|
||||
update: '更新模块',
|
||||
form: {
|
||||
name: {
|
||||
title: '模块名称',
|
||||
required: '请输入模块名称',
|
||||
},
|
||||
|
||||
path: {
|
||||
title: '模块目录',
|
||||
required: '请输入模块目录',
|
||||
},
|
||||
|
||||
desc: {
|
||||
title: '模块描述',
|
||||
},
|
||||
|
||||
keywords: {
|
||||
title: '模块关键字',
|
||||
},
|
||||
|
||||
dirs: {
|
||||
title: '默认目录',
|
||||
Controller: 'Controller 目录',
|
||||
Model: 'Model 目录',
|
||||
Database: 'Database 目录',
|
||||
Request: 'Request 目录',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default zh
|
124
resources/admin/layout/components/Menu/index.vue
Normal file
124
resources/admin/layout/components/Menu/index.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<script lang="ts">
|
||||
import { h, defineComponent, VNode } from 'vue'
|
||||
import { usePermissionsStore } from '/admin/stores/modules/user/permissions'
|
||||
import MenuItem from './item.vue'
|
||||
import menus from './menus.vue'
|
||||
import { useUserStore } from '/admin/stores/modules/user'
|
||||
import { Menu } from '/admin/types/Menu'
|
||||
|
||||
/**
|
||||
* 递归渲染 Menu 节点
|
||||
*/
|
||||
function getVNodes(menus: Menu[] | undefined, _subMenuClass: string | undefined): VNode[] {
|
||||
const vnodes: VNode[] = []
|
||||
menus?.forEach(menu => {
|
||||
if (!menu.meta?.hidden) {
|
||||
let vnode: VNode
|
||||
const len = menu.children?.length
|
||||
if (len) {
|
||||
vnode = h(
|
||||
MenuItem,
|
||||
{
|
||||
subMenuClass: _subMenuClass,
|
||||
menu,
|
||||
},
|
||||
{
|
||||
default: () => getVNodes(menu.children, 'children-menu'),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
vnode = h(MenuItem, {
|
||||
subMenuClass: _subMenuClass,
|
||||
menu,
|
||||
})
|
||||
}
|
||||
vnodes.push(vnode)
|
||||
}
|
||||
})
|
||||
|
||||
return vnodes
|
||||
}
|
||||
|
||||
/**
|
||||
* filter menus
|
||||
*
|
||||
* @param menus
|
||||
*/
|
||||
function filterMenus(menus: Menu[] | undefined): Menu[] {
|
||||
const newMenus: Menu[] = []
|
||||
|
||||
menus?.forEach(m => {
|
||||
if (m.meta?.hidden) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isHasOnlyChild(m) && m.children?.length) {
|
||||
newMenus.push(
|
||||
Object.assign({
|
||||
path: m.children[0].path,
|
||||
meta: m.children[0].meta,
|
||||
name: m.name,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
newMenus.push(m)
|
||||
}
|
||||
})
|
||||
|
||||
return newMenus
|
||||
}
|
||||
|
||||
/**
|
||||
* is has only child
|
||||
*
|
||||
* @param menu
|
||||
*/
|
||||
function isHasOnlyChild(menu: Menu): boolean {
|
||||
if (menu.children === undefined) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (menu.children.length > 1 || !menu.children.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (menu.children[0].children?.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
subMenuClass: {
|
||||
type: String,
|
||||
require: true,
|
||||
},
|
||||
menuClass: {
|
||||
type: String,
|
||||
require: true,
|
||||
},
|
||||
},
|
||||
setup(props, ctx) {
|
||||
const permissionsStore = usePermissionsStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 后端的 permissions 返回 undefined,则认为该后端无权限系统
|
||||
const permissions = userStore.getPermissions === undefined ? [] : userStore.getPermissions
|
||||
const vnodes = getVNodes(filterMenus(permissionsStore.getMenusFrom(permissions)), props.subMenuClass)
|
||||
|
||||
return () => {
|
||||
return h(
|
||||
menus,
|
||||
{
|
||||
class: 'border-none side-menu ' + props.menuClass,
|
||||
},
|
||||
{
|
||||
default: () => vnodes,
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
58
resources/admin/layout/components/Menu/item.vue
Normal file
58
resources/admin/layout/components/Menu/item.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<el-sub-menu :index="menu?.path" :class="subMenuClass" v-if="menu?.children?.length">
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<Icon :name="menu?.meta?.icon" v-if="menu?.meta?.icon" class="text-sm"/>
|
||||
</el-icon>
|
||||
<span>{{ menu?.meta?.title }}</span>
|
||||
</template>
|
||||
<slot/>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-menu-item v-else class="ct-menu-item" :index="menu?.path" @click="isMiniScreen() && store.changeExpaned()">
|
||||
<el-icon>
|
||||
<Icon :name="menu?.meta?.icon" v-if="menu?.meta?.icon" class="text-sm"/>
|
||||
</el-icon>
|
||||
<span>{{ menu?.meta?.title }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="MenuItem" setup>
|
||||
import { Menu } from '/admin/types/Menu'
|
||||
import { PropType } from 'vue'
|
||||
import { useAppStore } from '/admin/stores/modules/app'
|
||||
import { isMiniScreen } from '/admin/support/Helper'
|
||||
|
||||
const store = useAppStore()
|
||||
|
||||
defineProps({
|
||||
subMenuClass: {
|
||||
type: String,
|
||||
require: true,
|
||||
default: '',
|
||||
},
|
||||
|
||||
menu: {
|
||||
type: Object as PropType<Menu>,
|
||||
require: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-menu) {
|
||||
background-color: var(--sider-sub-menu-bg-color);
|
||||
}
|
||||
|
||||
.ct-menu-item:hover {
|
||||
background-color: var(--sider-sub-menu-hover-bg-color) !important;
|
||||
}
|
||||
|
||||
:deep(.children-menu .el-sub-menu__title) {
|
||||
background-color: var(--sider-sub-menu-bg-color) !important;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item-group__title) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
9
resources/admin/layout/components/Menu/mask.vue
Normal file
9
resources/admin/layout/components/Menu/mask.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="block sm:hidden z-40 w-screen h-full absolute mask-bg left-0 top-0" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mask-bg {
|
||||
background-color: #00000080;
|
||||
}
|
||||
</style>
|
66
resources/admin/layout/components/Menu/menus.vue
Normal file
66
resources/admin/layout/components/Menu/menus.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<el-menu
|
||||
:default-active="appStore.getActiveMenu"
|
||||
background-color="var(--sider-menu-bg-color)"
|
||||
active-text-color="var(--sider-ment-active-text-color)"
|
||||
text-color="var(--sider-menu-text-color)"
|
||||
:collapse="!appStore.isExpand"
|
||||
:collapse-transition="false"
|
||||
:router="true"
|
||||
@select="selectMenu"
|
||||
:unique-opened="true"
|
||||
>
|
||||
<slot/>
|
||||
</el-menu>
|
||||
</template>
|
||||
<script lang="ts" setup name="menus">
|
||||
import { useAppStore } from '/admin/stores/modules/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const selectMenu = (index: string) => {
|
||||
if (index.startsWith('http') || index.startsWith('https')) {
|
||||
window.open(index)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
:deep(.el-menu--inline) {
|
||||
@apply pt-1 pb-2;
|
||||
}
|
||||
|
||||
:deep(.ct-menu-item) {
|
||||
@apply mt-1;
|
||||
}
|
||||
|
||||
:deep(.is-active) {
|
||||
background-color: var(--side-active-menu-bg-color) !important;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu__title) {
|
||||
padding-left: calc(calc(var(--el-menu-base-level-padding) + var(--el-menu-level) * var(--el-menu-level-padding)));
|
||||
|
||||
color: var(--sider-menu-text-color);
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu) {
|
||||
color: var(--sider-sub-menu-bg-color);
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu__title:hover) {
|
||||
background-color: var(--sider-menu-bg-color);
|
||||
}
|
||||
|
||||
:deep(.el-menu--popup .el-menu-item:hover) {
|
||||
background-color: var(--sider-menu-bg-color) !important;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item:hover) {
|
||||
background-color: var(--sider-sub-menu-hover-bg-color) !important;
|
||||
}
|
||||
</style>
|
30
resources/admin/layout/components/content.vue
Normal file
30
resources/admin/layout/components/content.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div :class="'w-full h-screen flex flex-col transition-spacing duration-300 ease-linear overflow-hidden ' + mlClass">
|
||||
<!-- Header -->
|
||||
<Header />
|
||||
<!-- Tag view -->
|
||||
<!--<div class=""></div>-->
|
||||
<!-- Container -->
|
||||
<div class="p-1 sm:p-4 max-w-full h-screen overflow-auto sm:overflow-x-hidden">
|
||||
<router-view />
|
||||
|
||||
<!--<div class="w-full text-center text-gray-400 h-10 leading-10 mt-2">CatchAdmin 管理系统 @copyright 2018 ~ {{ year }}</div>-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useAppStore } from '/admin/stores/modules/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const mlClass = computed(() => {
|
||||
return appStore.isExpand ? 'ml-0 sm:ml-56' : 'ml-0 sm:ml-16'
|
||||
})
|
||||
|
||||
const year = computed(() => {
|
||||
const date = new Date()
|
||||
|
||||
return date.getFullYear()
|
||||
})
|
||||
</script>
|
33
resources/admin/layout/components/header/index.vue
Normal file
33
resources/admin/layout/components/header/index.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="flex flex-row h-16 w-full drop-shadow border-l dark:border-l-0 border-gray-200" style="background-color: var(--header-bg-color)">
|
||||
<div class="flex flex-row justify-between w-full">
|
||||
<div class="flex flex-row min-w-[17rem]">
|
||||
<div class="h-full flex items-center w-8 ml-2 hover:cursor-pointer" @click="store.changeExpaned">
|
||||
<Icon name="list-bullet" class="w-6 h-8" />
|
||||
</div>
|
||||
<div class="w-96 flex items-center pl-3 sm:pl-0">
|
||||
<Breadcrumbs />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-52 sm:min-w-[18rem] flex-row item-center pl-1 sm:pl-0 justify-end sm:justify-between mr-4">
|
||||
<div class="w-3/5 hidden sm:flex">
|
||||
<!-- 搜索 -->
|
||||
<Search />
|
||||
<!-- 多语言 -->
|
||||
<Lang />
|
||||
<!-- 暗黑主题 -->
|
||||
<Theme />
|
||||
<Notification />
|
||||
</div>
|
||||
<Profile />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '/admin/stores/modules/app'
|
||||
import Notification from './notification.vue'
|
||||
|
||||
const store = useAppStore()
|
||||
</script>
|
38
resources/admin/layout/components/header/lang.vue
Normal file
38
resources/admin/layout/components/header/lang.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="w-10 h-10 grid place-items-center rounded-full mt-3 hover:cursor-pointer">
|
||||
<div class="flex hover:cursor-pointer pl-1 pr-1">
|
||||
<el-dropdown size="large" class="flex items-center justify-center hover:cursor-pointer w-full" @command="selectLanguage">
|
||||
<Icon name="language" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="lang in langs" :key="lang.value" :command="lang.value" :disabled="lang.value == defaultLang">
|
||||
{{ $t('system.' + lang.label) }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, computed } from 'vue'
|
||||
import { useAppStore } from '/admin/stores/modules/app'
|
||||
|
||||
const langs = reactive([
|
||||
{ label: 'chinese', value: 'zh' },
|
||||
{ label: 'english', value: 'en' },
|
||||
])
|
||||
|
||||
const appStore = useAppStore()
|
||||
// select default languages
|
||||
const defaultLang = computed(() => {
|
||||
return appStore.getLocale
|
||||
})
|
||||
|
||||
// select language
|
||||
const selectLanguage = (value: 'zh' | 'en') => {
|
||||
appStore.changeLocale(value)
|
||||
location.reload()
|
||||
}
|
||||
</script>
|
22
resources/admin/layout/components/header/logo.vue
Normal file
22
resources/admin/layout/components/header/logo.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="h-16 flex items-center justify-center logo-bg">
|
||||
<img :src="logo" class="h-9 w-9" />
|
||||
<div class="text-md logo-text pl-3" v-if="store.isExpand">CatchAdmin</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore } from '/admin/stores/modules/app'
|
||||
import logo from '/admin/assets/logo.png'
|
||||
|
||||
const store = useAppStore()
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.logo-bg {
|
||||
background-color: var(--header-logo-bg-color);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: var(--header-logo-text-color);
|
||||
}
|
||||
</style>
|
49
resources/admin/layout/components/header/notification.vue
Normal file
49
resources/admin/layout/components/header/notification.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<!-- 通知 -->
|
||||
<div class="w-10 h-10 grid place-items-center rounded-full mt-3 hover:cursor-pointer" ref="messageRef" v-click-outside="onClickOutside">
|
||||
<el-badge :value="3">
|
||||
<Icon name="bell" />
|
||||
</el-badge>
|
||||
<el-popover ref="popoverRef" :virtual-ref="messageRef" trigger="hover" virtual-triggering :width="300">
|
||||
<el-tabs model-value="message">
|
||||
<el-tab-pane label="消息(8)" name="message">
|
||||
<div>
|
||||
<div class="flex flex-row w-full border-b border-b-slate-300 dark:border-b-slate-500 mt-2" v-for="(message, key) in messages" :key="key">
|
||||
<div>
|
||||
<el-avatar src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" class="w-2 h-2" />
|
||||
</div>
|
||||
<div class="ml-2 h-10 mt-2">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="通知(1)" name="notice">
|
||||
<div>
|
||||
<div class="flex flex-row w-full border-b border-b-slate-300 dark:border-b-slate-500 mt-2" v-for="(message, key) in messages" :key="key">
|
||||
<div>
|
||||
<el-avatar src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" class="w-2 h-2" />
|
||||
</div>
|
||||
<div class="ml-2 h-10 mt-2">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, unref } from 'vue'
|
||||
import { ClickOutside as vClickOutside } from 'element-plus'
|
||||
const messageRef = ref()
|
||||
const popoverRef = ref()
|
||||
const onClickOutside = () => {
|
||||
unref(popoverRef).popperRef?.delayHide?.()
|
||||
}
|
||||
|
||||
const messages = ref()
|
||||
|
||||
messages.value = ['你收到 catchadmin 的好友申请', '你收到 catchadmin pro 的 license 授权', '你收到 catchadmin 通知']
|
||||
</script>
|
34
resources/admin/layout/components/header/profile.vue
Normal file
34
resources/admin/layout/components/header/profile.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="flex w-2/5 hover:cursor-pointer pl-1 pr-1">
|
||||
<el-dropdown size="large" placement="bottom-end" class="flex items-center justify-center hover:cursor-pointer w-full">
|
||||
<div class="flex lg:items-center">
|
||||
<img :src="userStore.getAvatar" class="w-7 h-7 rounded-full" />
|
||||
<div class="ml-2 hidden lg:block">{{ userStore.getNickname }}</div>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>Action 1</el-dropdown-item>
|
||||
<el-dropdown-item>Action 2</el-dropdown-item>
|
||||
<el-dropdown-item>Action 3</el-dropdown-item>
|
||||
<el-dropdown-item divided @click="logout">
|
||||
<Icon name="logout" class="mr-1" />
|
||||
退 出
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useUserStore } from '/admin/stores/modules/user'
|
||||
import Message from '/admin/support/message'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const logout = () => {
|
||||
Message.confirm('确定退出系统吗?', () => {
|
||||
userStore.logout()
|
||||
})
|
||||
}
|
||||
</script>
|
58
resources/admin/layout/components/header/search.vue
Normal file
58
resources/admin/layout/components/header/search.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="w-10 h-10 grid place-items-center rounded-full mt-3 hover:cursor-pointer">
|
||||
<div class="flex flex-row w-96">
|
||||
<Icon name="magnifying-glass" class="hidden sm:block" @click="serachMenuVisable = true" />
|
||||
|
||||
<Teleport to="body">
|
||||
<el-dialog v-model="serachMenuVisable" width="30%" draggable>
|
||||
<el-cascader :filterable="true" :options="options" @change="toWhere" placeholder="请输入菜单名称" clearable class="w-full" :show-all-levels="false" />
|
||||
</el-dialog>
|
||||
</Teleport>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { usePermissionsStore } from '/admin/stores/modules/user/permissions'
|
||||
import { Menu } from '/admin/types/Menu'
|
||||
import router from '/admin/router'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const serachMenuVisable = ref(false)
|
||||
|
||||
const permissionStore = usePermissionsStore()
|
||||
const options = computed(() => {
|
||||
return filterMenus(permissionStore.getMenus)
|
||||
})
|
||||
const toWhere = (value: string[]) => {
|
||||
if (value.length) {
|
||||
router.push({ path: value[value.length - 1] })
|
||||
}
|
||||
|
||||
serachMenuVisable.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* filter menus
|
||||
*
|
||||
* @param menus
|
||||
*/
|
||||
function filterMenus(menus: Menu[] | undefined): Object[] {
|
||||
const cascaderMenus: Object[] = []
|
||||
|
||||
menus?.forEach(menu => {
|
||||
if (menu.meta === undefined) {
|
||||
const child = menu.children?.pop()
|
||||
cascaderMenus.push(Object.assign({ label: child?.meta?.title, value: child?.path }))
|
||||
} else {
|
||||
const cascaderMenu = Object.assign({ label: menu.meta?.title, value: menu.path, children: [] })
|
||||
|
||||
cascaderMenu.children = filterMenus(menu.children)
|
||||
|
||||
cascaderMenus.push(cascaderMenu)
|
||||
}
|
||||
})
|
||||
|
||||
return cascaderMenus
|
||||
}
|
||||
</script>
|
22
resources/admin/layout/components/header/theme.vue
Normal file
22
resources/admin/layout/components/header/theme.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="w-10 h-10 grid place-items-center rounded-full mt-3 hover:cursor-pointer">
|
||||
<Icon name="moon" @click="changeTheme()" v-if="isDark" />
|
||||
<Icon name="sun" @click="changeTheme()" v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import { useAppStore } from '/admin/stores/modules/app'
|
||||
import { unref } from 'vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const isDark = useDark()
|
||||
|
||||
const toggleDark = useToggle(isDark)
|
||||
|
||||
function changeTheme() {
|
||||
appStore.setDarkMode(!unref(isDark))
|
||||
toggleDark()
|
||||
}
|
||||
</script>
|
60
resources/admin/layout/components/sider.vue
Normal file
60
resources/admin/layout/components/sider.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :class="sideClass + ' drop-shadow-md overflow-y'">
|
||||
<!--logo -->
|
||||
<Logo />
|
||||
<!-- menu item -->
|
||||
<Menu :menu-class="menuClass" />
|
||||
</div>
|
||||
<Mask v-if="isMobile && appStore.isExpand" @click="appStore.changeExpaned()" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore } from '/admin/stores/modules/app'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { isMiniScreen } from '/admin/support/Helper'
|
||||
|
||||
const isMobile = ref(isMiniScreen())
|
||||
const layoutSide = ' h-screen z-[1000] sm:z-0 absolute top-0 left-0 sm:fixed transition-width duration-300 ease-linear sider-bg'
|
||||
const layoutSideOpenClass = 'w-56' + layoutSide
|
||||
const layoutSideHiddenClass = 'w-0 sm:w-16' + layoutSide
|
||||
|
||||
// 是否是小屏幕
|
||||
const sideClass = computed(() => {
|
||||
return appStore.isExpand ? layoutSideOpenClass : layoutSideHiddenClass
|
||||
})
|
||||
|
||||
// menu class
|
||||
const menuClass = ref<string>()
|
||||
// 判断展开状态
|
||||
const appStore = useAppStore()
|
||||
watch(appStore.$state, state => {
|
||||
// 如果切换到小屏幕,并且是菜单是收缩状态
|
||||
menuClass.value = isExpandWhenInMobile()
|
||||
})
|
||||
|
||||
// 监控屏幕大小
|
||||
onMounted(() => {
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
// 如果切换到小屏幕,并且是菜单是收缩状态,则隐藏子菜单
|
||||
isMobile.value = isMiniScreen()
|
||||
menuClass.value = isExpandWhenInMobile()
|
||||
})()
|
||||
}
|
||||
|
||||
// 刷新或者 go back 的时候默认展开菜单
|
||||
appStore.isExpand = true
|
||||
})
|
||||
|
||||
function isExpandWhenInMobile(): string {
|
||||
return !appStore.isExpand && isMobile.value ? 'hidden' : ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sider-bg {
|
||||
background-color: var(--sider-menu-bg-color);
|
||||
}
|
||||
</style>
|
8
resources/admin/layout/index.vue
Normal file
8
resources/admin/layout/index.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="w-full flex" ref="el">
|
||||
<!--- Sider -->
|
||||
<Sider />
|
||||
<!--content-->
|
||||
<Content />
|
||||
</div>
|
||||
</template>
|
9
resources/admin/router/constantRoutes.ts
Normal file
9
resources/admin/router/constantRoutes.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { RouteRecordRaw } from 'vue-router'
|
||||
// @ts-ignore
|
||||
const modules = import.meta.glob('@/module/**/views/router.ts', { eager: true })
|
||||
let moduleRoutes: RouteRecordRaw[] = []
|
||||
|
||||
Object.keys(modules).forEach(routePath => {
|
||||
moduleRoutes = moduleRoutes.concat(modules[routePath].default)
|
||||
})
|
||||
export default moduleRoutes
|
68
resources/admin/router/guard/index.ts
Normal file
68
resources/admin/router/guard/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useUserStore } from '/admin/stores/modules/user'
|
||||
import { getAuthToken, removeAuthToken, setPageTitle } from '/admin/support/Helper'
|
||||
import progress from '/admin/support/progress'
|
||||
import { WhiteListPage } from '/admin/enum/app'
|
||||
import { Router, RouteRecordRaw } from 'vue-router'
|
||||
import { usePermissionsStore } from '/admin/stores/modules/user/permissions'
|
||||
import { Menu } from '/admin/types/Menu'
|
||||
|
||||
const guard = (router: Router) => {
|
||||
// white list
|
||||
const whiteList: string[] = [WhiteListPage.LOGIN_PATH, WhiteListPage.NOT_FOUND_PATH]
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
// set page title
|
||||
setPageTitle(to.meta.title as unknown as string)
|
||||
// page start
|
||||
progress.start()
|
||||
// 获取用户的 token
|
||||
const authToken = getAuthToken()
|
||||
// 如果 token 存在
|
||||
if (authToken) {
|
||||
// 如果进入 /login 页面,重定向到首页
|
||||
if (to.path === WhiteListPage.LOGIN_PATH) {
|
||||
next({ path: '/' })
|
||||
} else {
|
||||
const userStore = useUserStore()
|
||||
// 获取用户ID
|
||||
if (userStore.getId) {
|
||||
next()
|
||||
} else {
|
||||
try {
|
||||
// 阻塞获取用户信息
|
||||
await userStore.getUserInfo()
|
||||
// 如果后端没有返回 permissions,前台则只使用静态路由
|
||||
if (userStore.getPermissions !== undefined) {
|
||||
// 挂载路由(实际是从后端获取用户的权限)
|
||||
const permissionStore = usePermissionsStore()
|
||||
// 动态路由挂载
|
||||
const asyncRoutes = permissionStore.getAsyncMenusFrom(userStore.getPermissions)
|
||||
asyncRoutes.forEach((route: Menu) => {
|
||||
router.addRoute(route as unknown as RouteRecordRaw)
|
||||
})
|
||||
}
|
||||
next({ ...to, replace: true })
|
||||
} catch (e) {
|
||||
removeAuthToken()
|
||||
next({ path: `${WhiteListPage.LOGIN_PATH}?redirect=${to.path}` })
|
||||
}
|
||||
}
|
||||
}
|
||||
progress.done()
|
||||
} else {
|
||||
// 如果不在白名单
|
||||
if (whiteList.indexOf(to.path) !== -1) {
|
||||
next()
|
||||
} else {
|
||||
next({ path: WhiteListPage.LOGIN_PATH })
|
||||
}
|
||||
progress.done()
|
||||
}
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
progress.done()
|
||||
})
|
||||
}
|
||||
|
||||
export default guard
|
65
resources/admin/router/index.ts
Normal file
65
resources/admin/router/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
|
||||
import type { App } from 'vue'
|
||||
// module routers
|
||||
import moduleRoutes from './constantRoutes'
|
||||
|
||||
export const constantRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/dashboard',
|
||||
component: () => import('/admin/layout/index.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'Dashboard',
|
||||
meta: { title: 'Dashboard', icon: 'home', hidden: false },
|
||||
component: () => import('/admin/views/dashboard/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
// @ts-ignore
|
||||
.concat(moduleRoutes)
|
||||
|
||||
// default routes, it will not change to menus
|
||||
const defaultRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: '/',
|
||||
component: () => import('/admin/layout/index.vue'),
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: '/404',
|
||||
name: '404',
|
||||
meta: { title: '404' },
|
||||
component: () => import('/admin/components/404/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('/admin/views/login/index.vue'),
|
||||
},
|
||||
// 未定义路有重定向到 404
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/404',
|
||||
},
|
||||
]
|
||||
|
||||
const routes = constantRoutes.concat(defaultRoutes)
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
// 路由滚动
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
return savedPosition || { top: 0, behavior: 'smooth' }
|
||||
},
|
||||
})
|
||||
|
||||
export function bootstrapRouter(app: App) {
|
||||
app.use(router)
|
||||
}
|
||||
|
||||
export default router
|
10
resources/admin/stores/index.ts
Normal file
10
resources/admin/stores/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createPinia } from 'pinia'
|
||||
import type { App } from 'vue'
|
||||
|
||||
const store = createPinia()
|
||||
|
||||
export function bootstrapStore(app: App) : void {
|
||||
app.use(store)
|
||||
}
|
||||
|
||||
export default store
|
76
resources/admin/stores/modules/app/index.ts
Normal file
76
resources/admin/stores/modules/app/index.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import Cache from '/admin/support/cache'
|
||||
|
||||
/**
|
||||
* app
|
||||
*/
|
||||
type app = {
|
||||
size: 'small' | 'medium' | 'large'
|
||||
|
||||
isExpand: boolean
|
||||
|
||||
locale: 'zh' | 'en'
|
||||
|
||||
isMobile: boolean
|
||||
|
||||
isDarkMode: boolean
|
||||
|
||||
activeMenu: string
|
||||
}
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: (): app => ({
|
||||
size: 'small',
|
||||
isExpand: true,
|
||||
locale: Cache.get('language'),
|
||||
isMobile: false,
|
||||
isDarkMode: false,
|
||||
activeMenu: '/dashboard',
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getSize(): string {
|
||||
return this.size
|
||||
},
|
||||
|
||||
getLocale(): string {
|
||||
return this.locale
|
||||
},
|
||||
|
||||
getIsMobile(): boolean {
|
||||
return this.isMobile
|
||||
},
|
||||
|
||||
getIsDarkMode(): boolean {
|
||||
return this.isDarkMode
|
||||
},
|
||||
|
||||
getActiveMenu(): string {
|
||||
return this.activeMenu
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
changeSize(size: 'small' | 'medium' | 'large'): void {
|
||||
this.size = size
|
||||
},
|
||||
|
||||
changeLocale(locale: 'zh' | 'en'): void {
|
||||
Cache.set('language', locale)
|
||||
|
||||
this.locale = locale
|
||||
},
|
||||
|
||||
changeExpaned(): void {
|
||||
this.isExpand = !this.isExpand
|
||||
},
|
||||
|
||||
setDarkMode(isDarkMode: boolean): void {
|
||||
this.isDarkMode = isDarkMode
|
||||
},
|
||||
|
||||
setActiveMenu(activeMenu: string): void {
|
||||
this.activeMenu = activeMenu.startsWith('/') ? activeMenu : '/' + activeMenu
|
||||
},
|
||||
},
|
||||
})
|
152
resources/admin/stores/modules/user/index.ts
Normal file
152
resources/admin/stores/modules/user/index.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { User } from '/admin/types/user'
|
||||
import http from '/admin/support/http'
|
||||
import { rememberAuthToken, removeAuthToken } from '/admin/support/helper'
|
||||
import Message from '/admin/support/message'
|
||||
import router from '/admin/router'
|
||||
import { Permission } from '/admin/types/permission'
|
||||
|
||||
export const useUserStore = defineStore('UserStore', {
|
||||
state: (): User => {
|
||||
return {
|
||||
id: 0,
|
||||
|
||||
nickname: '',
|
||||
|
||||
avatar: '',
|
||||
|
||||
email: '',
|
||||
|
||||
remember_token: '',
|
||||
|
||||
status: 0,
|
||||
|
||||
permissions: [] as Permission[],
|
||||
|
||||
roles: [] as string[],
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
getId(): number {
|
||||
return this.id
|
||||
},
|
||||
getNickname(): string {
|
||||
return this.nickname
|
||||
},
|
||||
|
||||
getAvatar(): string {
|
||||
return this.avatar
|
||||
},
|
||||
|
||||
getRoles(): string[] | undefined {
|
||||
return this.roles
|
||||
},
|
||||
|
||||
getPermissions(): Permission[] | undefined {
|
||||
return this.permissions
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
isSuperAdmin(): boolean {
|
||||
return this.id === 1
|
||||
},
|
||||
setNickname(nickname: string) {
|
||||
this.nickname = nickname
|
||||
},
|
||||
|
||||
setId(id: number) {
|
||||
this.id = id
|
||||
},
|
||||
|
||||
setRememberToken(token: string) {
|
||||
this.remember_token = token
|
||||
},
|
||||
|
||||
setAvatar(avatar: string) {
|
||||
this.avatar = avatar
|
||||
},
|
||||
|
||||
setRoles(roles: string[]) {
|
||||
this.roles = roles
|
||||
},
|
||||
|
||||
setPermissions(permissions: Permission[]) {
|
||||
this.permissions = permissions
|
||||
},
|
||||
|
||||
setEmail(email: string) {
|
||||
this.email = email
|
||||
},
|
||||
|
||||
setStatus(status: number) {
|
||||
this.status = status
|
||||
},
|
||||
|
||||
/**
|
||||
* login
|
||||
*
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
login(params: Object) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
http
|
||||
.post('/login', params)
|
||||
.then(response => {
|
||||
const { token } = response.data.data
|
||||
rememberAuthToken(token)
|
||||
this.setRememberToken(token)
|
||||
resolve()
|
||||
})
|
||||
.catch(e => {
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* logout
|
||||
*/
|
||||
logout() {
|
||||
http
|
||||
.post('/logout')
|
||||
.then(() => {
|
||||
removeAuthToken()
|
||||
this.$reset()
|
||||
router.push({ path: '/login' })
|
||||
})
|
||||
.catch(e => {
|
||||
Message.error(e.message)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* user info
|
||||
*/
|
||||
getUserInfo() {
|
||||
return new Promise((resolve, reject) => {
|
||||
http
|
||||
.get('/user/info')
|
||||
.then(response => {
|
||||
const { id, nickname, email, avatar, permissions, roles, rememberToken, status } = response.data.data
|
||||
// set user info
|
||||
this.setId(id)
|
||||
this.setNickname(nickname)
|
||||
this.setEmail(email)
|
||||
this.setRoles(roles)
|
||||
this.setRememberToken(rememberToken)
|
||||
this.setStatus(status)
|
||||
this.setAvatar(avatar)
|
||||
this.setPermissions(permissions)
|
||||
|
||||
resolve(response.data.data)
|
||||
})
|
||||
.catch(e => {
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
193
resources/admin/stores/modules/user/permissions.ts
Normal file
193
resources/admin/stores/modules/user/permissions.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { Permission } from '/admin/types/permission'
|
||||
import { MenuType } from '/admin/enum/app'
|
||||
import { Menu } from '/admin/types/Menu'
|
||||
import { constantRoutes } from '/admin/router'
|
||||
import { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
interface Permissions {
|
||||
menus: Menu[]
|
||||
|
||||
asyncMenus: Menu[]
|
||||
|
||||
permissions: Permission[]
|
||||
|
||||
menuPathMap: Map<string, string>
|
||||
}
|
||||
|
||||
export const usePermissionsStore = defineStore('PermissionsStore', {
|
||||
state: (): Permissions => {
|
||||
return {
|
||||
menus: [],
|
||||
|
||||
asyncMenus: [],
|
||||
|
||||
permissions: [],
|
||||
|
||||
menuPathMap: new Map(),
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* get
|
||||
*/
|
||||
getters: {
|
||||
getMenus(): Menu[] {
|
||||
return this.menus
|
||||
},
|
||||
|
||||
getAsyncMenus(): Menu[] {
|
||||
return this.asyncMenus
|
||||
},
|
||||
|
||||
getPermissions(): Permission[] {
|
||||
return this.permissions
|
||||
},
|
||||
|
||||
getMenuPathMap(): Map<string, string> {
|
||||
return this.menuPathMap
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* actions
|
||||
*/
|
||||
actions: {
|
||||
/**
|
||||
* generate async menus
|
||||
* @param permissions
|
||||
* @param force
|
||||
* @returns
|
||||
*/
|
||||
getAsyncMenusFrom(permissions: Permission[], force: boolean = false): Menu[] {
|
||||
// 如果非强制获取并且 menu 有值,直接返回
|
||||
if (!force && this.asyncMenus.length > 0) {
|
||||
return this.asyncMenus
|
||||
}
|
||||
|
||||
const menus: Permission[] = []
|
||||
|
||||
permissions.forEach(permission => {
|
||||
if (permission.type === MenuType.PAGE_TYPE) {
|
||||
menus.push(permission)
|
||||
}
|
||||
|
||||
// set map
|
||||
this.menuPathMap.set(permission.route, permission.title)
|
||||
})
|
||||
|
||||
this.setAsyncMenus(this.getAsnycMenus(menus))
|
||||
|
||||
return this.asyncMenus
|
||||
},
|
||||
|
||||
/**
|
||||
* get menus
|
||||
* @param permissions
|
||||
* @param force
|
||||
* @returns
|
||||
*/
|
||||
getMenusFrom(permissions: Permission[], force: boolean = false): Menu[] {
|
||||
// 如果非强制获取并且 menu 有值,直接返回
|
||||
if (!force && this.menus.length > 0) {
|
||||
return this.menus
|
||||
}
|
||||
const asyncMenus = this.getAsyncMenusFrom(permissions, force)
|
||||
|
||||
this.setMenus(asyncMenus)
|
||||
|
||||
return this.menus
|
||||
},
|
||||
|
||||
/**
|
||||
* set menus
|
||||
*
|
||||
* @param menus
|
||||
*/
|
||||
setMenus(menus: Menu[]) {
|
||||
this.menus = this.transformRoutesToMenus(constantRoutes).concat(menus)
|
||||
},
|
||||
|
||||
setAsyncMenus(menus: Menu[]) {
|
||||
this.asyncMenus = menus
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成 Menus
|
||||
*
|
||||
* @param permissions
|
||||
* @param parentId
|
||||
* @param path
|
||||
* @returns
|
||||
*/
|
||||
getAsnycMenus(permissions: Permission[], parentId: number = 0, path: string = ''): Menu[] {
|
||||
const menus: Menu[] = []
|
||||
|
||||
permissions.forEach(permission => {
|
||||
if (permission.parent_id === parentId) {
|
||||
// menu
|
||||
const menu: Menu = Object.assign({
|
||||
path: this.resoulveRoutePath(permission.route, path),
|
||||
name: permission.module + '_' + permission.permission_mark,
|
||||
redirect: permission.redirect,
|
||||
meta: Object.assign({ title: permission.title, icon: permission.icon, hidden: permission.hidden, is_inner: permission.is_inner }),
|
||||
})
|
||||
|
||||
// child menu
|
||||
const children = this.getAsnycMenus(permissions, permission.id, menu.path)
|
||||
if (children.length > 0) {
|
||||
menu.children = children
|
||||
}
|
||||
menus.push(menu)
|
||||
}
|
||||
})
|
||||
|
||||
return menus
|
||||
},
|
||||
|
||||
/**
|
||||
* transform routes to menus
|
||||
* @param routes
|
||||
* @param path
|
||||
* @returns
|
||||
*/
|
||||
transformRoutesToMenus(routes: Menu[] | Array<RouteRecordRaw>, path: string = ''): Menu[] {
|
||||
const menus: Menu[] = []
|
||||
|
||||
routes.forEach(route => {
|
||||
if (route.meta?.hidden) {
|
||||
return false
|
||||
}
|
||||
|
||||
const menu: Menu = Object.assign({
|
||||
path: this.resoulveRoutePath(route.path, path),
|
||||
name: route.name,
|
||||
meta: route.meta,
|
||||
component: route.component,
|
||||
})
|
||||
|
||||
if (route.children?.length) {
|
||||
menu.children = this.transformRoutesToMenus(route.children, menu.path)
|
||||
}
|
||||
|
||||
menus.push(menu)
|
||||
})
|
||||
return menus
|
||||
},
|
||||
|
||||
/**
|
||||
* resoulve route path
|
||||
* @param route
|
||||
* @param path
|
||||
* @returns
|
||||
*/
|
||||
resoulveRoutePath(route: string, path: string): string {
|
||||
if (path.length) {
|
||||
return (path + (route.indexOf('/') === -1 ? '/' : '') + route).replace(/\/$/g, '')
|
||||
}
|
||||
|
||||
// 去除尾部的 /
|
||||
return route.replace(/\/$/g, '')
|
||||
},
|
||||
},
|
||||
})
|
61
resources/admin/styles/element.scss
Normal file
61
resources/admin/styles/element.scss
Normal file
@@ -0,0 +1,61 @@
|
||||
@forward 'element-plus/theme-chalk/src/common/var' with (
|
||||
// 基础色调
|
||||
$colors:
|
||||
(
|
||||
'primary': (
|
||||
'base': #4f46e5,
|
||||
),
|
||||
'success': (
|
||||
'base': #059669,
|
||||
),
|
||||
'warning': (
|
||||
'base': #fbbf24,
|
||||
),
|
||||
'danger': (
|
||||
'base': #f43f5e,
|
||||
),
|
||||
'error': (
|
||||
'base': #f43f5e,
|
||||
),
|
||||
'info': (
|
||||
'base': #909399,
|
||||
)
|
||||
),
|
||||
$input: ('border-radius': 8px)
|
||||
);
|
||||
|
||||
@use 'element-plus/theme-chalk/src/index' as *;
|
||||
|
||||
.el-table {
|
||||
border-radius: var(--el-table-border-radius);
|
||||
|
||||
.el-table__row {
|
||||
@apply h-14;
|
||||
}
|
||||
|
||||
.el-table__header {
|
||||
@apply h-14 bg-black;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs {
|
||||
border-radius: var(--el-table-border-radius);
|
||||
}
|
||||
|
||||
.el-card {
|
||||
border-radius: var(--el-card-border-radius) !important;
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
button {
|
||||
border-radius: var(--el-page-border-radius) !important;
|
||||
}
|
||||
.el-pager {
|
||||
.number {
|
||||
border-radius: var(--el-page-border-radius) !important;
|
||||
}
|
||||
.more {
|
||||
border-radius: var(--el-page-border-radius) !important;
|
||||
}
|
||||
}
|
||||
}
|
12
resources/admin/styles/index.scss
Normal file
12
resources/admin/styles/index.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
// tailwindcss
|
||||
@import 'tailwind.css';
|
||||
|
||||
// element style 必须在 tailwindcss 之后,不然样式会被 tailwindcss 覆盖
|
||||
// issue 在这里 https://github.com/tailwindlabs/tailwindcss/discussions/5969
|
||||
@import 'element';
|
||||
|
||||
// 后台管理定义的 css 变量
|
||||
@import 'var';
|
||||
|
||||
// theme
|
||||
@import 'theme/index';
|
22
resources/admin/styles/tailwind.css
Normal file
22
resources/admin/styles/tailwind.css
Normal file
@@ -0,0 +1,22 @@
|
||||
@tailwind base;
|
||||
|
||||
@layer base {
|
||||
.layout-sider {
|
||||
@apply z-0 absolute top-16 left-0 sm:static transition-width duration-300 ease-linear
|
||||
}
|
||||
|
||||
.layout-sider-open {
|
||||
@apply layout-sider w-64
|
||||
}
|
||||
|
||||
.layout-sider-hidden {
|
||||
@apply layout-sider w-0 lg:w-20
|
||||
}
|
||||
|
||||
.layout-sider-mask {
|
||||
@apply block lg:hidden z-40 w-full h-full absolute
|
||||
}
|
||||
}
|
||||
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
36
resources/admin/styles/theme/dark.scss
Normal file
36
resources/admin/styles/theme/dark.scss
Normal file
@@ -0,0 +1,36 @@
|
||||
@import 'element-plus/theme-chalk/dark/css-vars.css';
|
||||
|
||||
html.dark {
|
||||
background-color: #161d31;
|
||||
// 侧边栏背景色
|
||||
--sider-bg-color: #283046;
|
||||
// header logo 文字颜色
|
||||
--header-logo-text-color: #ffffff;
|
||||
// 侧边栏菜单的文字颜色
|
||||
--sider-menu-text-color: #ffffff;
|
||||
// sub menu bg color
|
||||
--sider-sub-menu-bg-color: #161d31;
|
||||
// 侧边栏子菜单 hover 的颜色
|
||||
--sider-sub-menu-hover-bg-color: #343d55;
|
||||
// 激活文字颜色
|
||||
--sider-ment-active-text-color: var(--el-color-primary);
|
||||
// 激活时背景色
|
||||
--side-active-menu-bg-color: rgba(255, 255, 255, 0.08);
|
||||
|
||||
/* 自定义深色背景颜色 */
|
||||
--el-bg-color: var(--sider-sub-menu-hover-bg-color);
|
||||
|
||||
--el-fill-color-blank: var(--sider-bg-color);
|
||||
|
||||
--el-bg-color-overlay: var(--sider-bg-color);
|
||||
|
||||
--header-bg-color: var(--sider-bg-color);
|
||||
|
||||
// border color
|
||||
--el-border-color-lighter: #3b4253;
|
||||
|
||||
--el-fill-color-light: #161d31;
|
||||
|
||||
// side sub menu margin
|
||||
--sider-sub-menu-bg-margin: 0px 0.05rem;
|
||||
}
|
3
resources/admin/styles/theme/index.scss
Normal file
3
resources/admin/styles/theme/index.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
@import "light";
|
||||
|
||||
@import "dark";
|
25
resources/admin/styles/theme/light.scss
Normal file
25
resources/admin/styles/theme/light.scss
Normal file
@@ -0,0 +1,25 @@
|
||||
html {
|
||||
background-color: rgb(241, 245, 249);
|
||||
// 侧边颜色
|
||||
--sider-bg-color: #ffffff;
|
||||
// header logo 背景颜色
|
||||
--header-logo-bg-color: var(--sider-bg-color);
|
||||
// 侧边栏菜单的文字颜色
|
||||
--sider-menu-text-color: #625f6e;
|
||||
// sub menu bg color
|
||||
--sider-sub-menu-bg-color: #ffffff;
|
||||
// 侧边栏子菜单 hover 的颜色
|
||||
--sider-sub-menu-hover-bg-color: #f6f6f6;
|
||||
// 菜单激活时文字的颜色
|
||||
--sider-ment-active-text-color: var(--el-color-primary);
|
||||
// 激活菜单背景
|
||||
--side-active-menu-bg-color: rgba(36, 153, 239, 0.06);
|
||||
// header logo 文字颜色
|
||||
--header-logo-text-color: #625f6e;
|
||||
// header bg color
|
||||
--header-bg-color: #ffffff;
|
||||
// 侧边栏菜单的背景色
|
||||
--sider-menu-bg-color: var(--sider-bg-color);
|
||||
|
||||
--el-table-tr-bg-color: black;
|
||||
}
|
12
resources/admin/styles/var.scss
Normal file
12
resources/admin/styles/var.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
:root {
|
||||
--el-menu-base-level-padding: 20px;
|
||||
// 后台自定义
|
||||
// el-table
|
||||
--el-table-border-radius: 8px;
|
||||
// el-tabs
|
||||
--el-tabs-border-radius: 8px;
|
||||
// el-card
|
||||
--el-card-border-radius: 8px;
|
||||
// -el-page
|
||||
--el-page-border-radius: 8px;
|
||||
}
|
32
resources/admin/support/cache.ts
Normal file
32
resources/admin/support/cache.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export default class Cache {
|
||||
private static readonly prefix:string = 'catchadmin_'
|
||||
/**
|
||||
* set
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
static set (key:string, value: any) : void {
|
||||
window.localStorage.setItem(Cache.prefix + key, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* get
|
||||
*
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
static get (key: string) : any {
|
||||
return window.localStorage.getItem(Cache.prefix + key)
|
||||
}
|
||||
|
||||
/**
|
||||
* delete
|
||||
*
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
static del (key: string) : void {
|
||||
window.localStorage.removeItem(Cache.prefix + key)
|
||||
}
|
||||
}
|
88
resources/admin/support/catchAdmin.ts
Normal file
88
resources/admin/support/catchAdmin.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { createApp } from 'vue'
|
||||
import type { App as app } from 'vue'
|
||||
import App from '/admin/App.vue'
|
||||
import router, { bootstrapRouter } from '/admin/router'
|
||||
import ElementPlus from 'element-plus'
|
||||
import zh from 'element-plus/es/locale/lang/zh-cn'
|
||||
import { bootstrapStore } from '/admin/stores'
|
||||
import Cache from './cache'
|
||||
import { bootstrapI18n } from '/admin/i18n'
|
||||
import guard from '/admin/router/guard'
|
||||
|
||||
/**
|
||||
* catchadmin
|
||||
*/
|
||||
export default class CatchAdmin {
|
||||
protected app: app
|
||||
|
||||
protected element: string
|
||||
|
||||
/**
|
||||
* construct
|
||||
*
|
||||
* @param ele
|
||||
*/
|
||||
constructor(ele: string = '#app') {
|
||||
this.app = createApp(App)
|
||||
this.element = ele
|
||||
}
|
||||
|
||||
/**
|
||||
* admin boot
|
||||
*/
|
||||
bootstrap(): void {
|
||||
this.useElementPlus().usePinia().useI18n().useRouter().mount()
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂载节点
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
protected mount(): void {
|
||||
this.app.mount(this.element)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载路由
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
protected useRouter(): CatchAdmin {
|
||||
guard(router)
|
||||
|
||||
bootstrapRouter(this.app)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* ui
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
protected useElementPlus(): CatchAdmin {
|
||||
this.app.use(ElementPlus, {
|
||||
locale: Cache.get('language') === 'zh' && zh,
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* use pinia
|
||||
*/
|
||||
protected usePinia(): CatchAdmin {
|
||||
bootstrapStore(this.app)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* use i18n
|
||||
*/
|
||||
protected useI18n(): CatchAdmin {
|
||||
bootstrapI18n(this.app)
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
89
resources/admin/support/helper.ts
Normal file
89
resources/admin/support/helper.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Helper 助教函数集合
|
||||
*/
|
||||
|
||||
import Cache from '/admin/support/cache'
|
||||
import i18n from '/admin/i18n'
|
||||
|
||||
const AUTH_TOKEN = 'auth_token'
|
||||
|
||||
/**
|
||||
* env get
|
||||
*
|
||||
* @param key
|
||||
*/
|
||||
export function env(key: string): any {
|
||||
const env = import.meta.env
|
||||
|
||||
return env[key]
|
||||
}
|
||||
|
||||
/**
|
||||
* remember token
|
||||
*
|
||||
* @param token
|
||||
*/
|
||||
export function rememberAuthToken(token: string): void {
|
||||
Cache.set(AUTH_TOKEN, token)
|
||||
}
|
||||
|
||||
/**
|
||||
* remove auth token
|
||||
*/
|
||||
export function removeAuthToken(): void {
|
||||
Cache.del(AUTH_TOKEN)
|
||||
}
|
||||
|
||||
/**
|
||||
* get auth token
|
||||
*
|
||||
*/
|
||||
export function getAuthToken(): string | null {
|
||||
return Cache.get(AUTH_TOKEN)
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是小屏幕
|
||||
* @return
|
||||
*/
|
||||
export function isMiniScreen(): boolean {
|
||||
return window.document.body.clientWidth < 500
|
||||
}
|
||||
|
||||
/**
|
||||
* translate
|
||||
*
|
||||
* @param translate
|
||||
* @returns
|
||||
*/
|
||||
export function t(translate: string) {
|
||||
return i18n.global.t(translate)
|
||||
}
|
||||
|
||||
/**
|
||||
* is undefined
|
||||
*
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
export function isUndefined(value: any): boolean {
|
||||
return value === undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* set page title
|
||||
*
|
||||
* @param title
|
||||
*/
|
||||
export function setPageTitle(title: string) {
|
||||
document.title = title
|
||||
}
|
||||
|
||||
/**
|
||||
* is function?
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
export function isFunction(value: any) {
|
||||
return typeof value === 'function'
|
||||
}
|
213
resources/admin/support/http.ts
Normal file
213
resources/admin/support/http.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { Code } from '/admin/enum/app'
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
|
||||
import { env, getAuthToken, removeAuthToken } from './helper'
|
||||
import Message from './message'
|
||||
import router from '/admin/router'
|
||||
import ResponseData from '/admin/types/responseData'
|
||||
|
||||
/**
|
||||
* http util
|
||||
*/
|
||||
class Http {
|
||||
/**
|
||||
* axios config
|
||||
* @protected
|
||||
*/
|
||||
protected config: AxiosRequestConfig = {}
|
||||
|
||||
/**
|
||||
* base url
|
||||
* @protected
|
||||
*/
|
||||
protected baseURL: string = ''
|
||||
|
||||
/**
|
||||
* http request timeout
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected timeout: number = 0
|
||||
|
||||
/**
|
||||
* http request headers
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected headers: { [k: string]: string } = {}
|
||||
|
||||
/**
|
||||
* axios instance
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected request: AxiosInstance
|
||||
|
||||
/**
|
||||
* instance
|
||||
*/
|
||||
constructor() {
|
||||
this.request = axios.create(this.getConfig())
|
||||
}
|
||||
|
||||
/**
|
||||
* get request
|
||||
*
|
||||
* @param path
|
||||
* @param params
|
||||
*/
|
||||
public get(path: string, params: object = {}) {
|
||||
return this.request.get(this.baseURL + path, {
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* post request
|
||||
*
|
||||
* @param path
|
||||
* @param data
|
||||
*/
|
||||
public post(path: string, data: object = {}) {
|
||||
return this.request.post(this.baseURL + path, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* put request
|
||||
*
|
||||
* @param path
|
||||
* @param data
|
||||
*/
|
||||
public put(path: string, data: object = {}) {
|
||||
return this.request.put(this.baseURL + path, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* delete request
|
||||
*
|
||||
* @param path
|
||||
*/
|
||||
public delete(path: string) {
|
||||
return this.request.delete(this.baseURL + path)
|
||||
}
|
||||
|
||||
/**
|
||||
* set timeout
|
||||
*
|
||||
* @param timeout
|
||||
* @returns
|
||||
*/
|
||||
public setTimeout(timeout: number): Http {
|
||||
this.timeout = timeout
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* set baseurl
|
||||
*
|
||||
* @param url
|
||||
* @returns
|
||||
*/
|
||||
public setBaseUrl(url: string): Http {
|
||||
this.baseURL = url
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* set headers
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
public setHeader(key: string, value: string): Http {
|
||||
this.headers.key = value
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* get axios 配置
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
protected getConfig(): AxiosRequestConfig {
|
||||
// set base url
|
||||
this.config.baseURL = this.baseURL ? this.baseURL : env('VITE_BASE_URL')
|
||||
|
||||
// set timeout
|
||||
this.config.timeout = this.timeout ? this.timeout : 5000
|
||||
|
||||
// set ajax request
|
||||
this.headers['X-Requested-With'] = 'XMLHttpRequest'
|
||||
this.config.headers = this.headers
|
||||
|
||||
return this.config
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加请求拦截器
|
||||
*
|
||||
*/
|
||||
public interceptorsOfRequest(): void {
|
||||
this.request.interceptors.request.use(function (config: AxiosRequestConfig) {
|
||||
const token = getAuthToken()
|
||||
if (token) {
|
||||
if (!config.headers) {
|
||||
config.headers = {}
|
||||
}
|
||||
|
||||
config.headers.authorization = 'Bearer ' + token
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加响应拦截器
|
||||
*
|
||||
*/
|
||||
public interceptorsOfResponse(): void {
|
||||
this.request.interceptors.response.use(
|
||||
response => {
|
||||
const r: ResponseData = response.data
|
||||
const code = r.code
|
||||
const message = r.message
|
||||
if (code === 1e4) {
|
||||
return response
|
||||
}
|
||||
|
||||
if (code === 10004) {
|
||||
Message.error(message || 'Error')
|
||||
} else if (code === Code.LOST_LOGIN || code === Code.LOGIN_EXPIRED) {
|
||||
// to re-login
|
||||
Message.confirm(message + ',需要重新登陆', function () {
|
||||
removeAuthToken()
|
||||
router.push('/login')
|
||||
})
|
||||
} else if (code === Code.LOGIN_BLACKLIST || code === Code.USER_FORBIDDEN) {
|
||||
Message.error(message || 'Error')
|
||||
removeAuthToken()
|
||||
// to login page
|
||||
router.push('/login')
|
||||
} else {
|
||||
Message.error(message || 'Error')
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(message || 'Error'))
|
||||
},
|
||||
error => {
|
||||
Message.error(error.message)
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const http = new Http()
|
||||
http.interceptorsOfRequest()
|
||||
|
||||
http.interceptorsOfResponse()
|
||||
export default http
|
59
resources/admin/support/message.ts
Normal file
59
resources/admin/support/message.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { t } from './helper'
|
||||
|
||||
export default class Message {
|
||||
/**
|
||||
* success
|
||||
*
|
||||
* @param message
|
||||
*/
|
||||
static success (message: string) : void {
|
||||
this.message(message, 'success')
|
||||
}
|
||||
|
||||
/**
|
||||
* error
|
||||
*
|
||||
* @param message
|
||||
*/
|
||||
static error (message: string) : void {
|
||||
this.message(message, 'error')
|
||||
}
|
||||
|
||||
/**
|
||||
* warning
|
||||
*
|
||||
* @param message
|
||||
*/
|
||||
static warning (message: string) : void {
|
||||
this.message(message, 'warning')
|
||||
}
|
||||
|
||||
/**
|
||||
* confirm
|
||||
*
|
||||
* @param message
|
||||
* @param callback
|
||||
*/
|
||||
static confirm (message: string, callback: any) : void {
|
||||
ElMessageBox.confirm(message, t('system.warning'), {
|
||||
confirmButtonText: t('system.confirm'),
|
||||
cancelButtonText: t('system.cancel'),
|
||||
type: 'warning'
|
||||
}).then(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* message
|
||||
*
|
||||
* @param message
|
||||
* @param type
|
||||
*/
|
||||
protected static message (message: string, type: any) {
|
||||
ElMessage({
|
||||
message,
|
||||
|
||||
type
|
||||
})
|
||||
}
|
||||
}
|
14
resources/admin/support/progress.ts
Normal file
14
resources/admin/support/progress.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import NProgress from 'nprogress'
|
||||
import 'nprogress/nprogress.css'
|
||||
|
||||
// 全局进度条的配置
|
||||
NProgress.configure({
|
||||
easing: 'ease', // 动画方式
|
||||
speed: 500, // 递增进度条的速度
|
||||
showSpinner: true, // 是否显示加载ico
|
||||
trickleSpeed: 200, // 自动递增间隔
|
||||
minimum: 0.3, // 更改启动时使用的最小百分比
|
||||
parent: 'body' // 指定进度条的父容器
|
||||
})
|
||||
|
||||
export default NProgress
|
27
resources/admin/types/Permission.ts
Normal file
27
resources/admin/types/Permission.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export interface Permission {
|
||||
id: number
|
||||
|
||||
parent_id: number
|
||||
|
||||
title: string
|
||||
|
||||
type: number
|
||||
|
||||
icon: string
|
||||
|
||||
component: string
|
||||
|
||||
module: string
|
||||
|
||||
permission_mark: string
|
||||
|
||||
route: string
|
||||
|
||||
redirect: string
|
||||
|
||||
keepAlive: boolean
|
||||
|
||||
hidden: boolean
|
||||
|
||||
is_inner: boolean
|
||||
}
|
1
resources/admin/types/Role.ts
Normal file
1
resources/admin/types/Role.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {}
|
22
resources/admin/types/User.ts
Normal file
22
resources/admin/types/User.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
// login user type
|
||||
|
||||
import { Permission } from './permission'
|
||||
|
||||
export interface User {
|
||||
id: number,
|
||||
|
||||
nickname: string,
|
||||
|
||||
avatar: string,
|
||||
|
||||
email: string,
|
||||
|
||||
status: number,
|
||||
|
||||
remember_token: string,
|
||||
|
||||
roles?: string[],
|
||||
|
||||
permissions?: Permission[]
|
||||
}
|
33
resources/admin/types/menu.ts
Normal file
33
resources/admin/types/menu.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Component } from 'vue'
|
||||
import { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export interface Meta {
|
||||
title: string
|
||||
|
||||
icon: string
|
||||
|
||||
roles?: string[]
|
||||
|
||||
cache?: boolean
|
||||
|
||||
hidden: boolean
|
||||
|
||||
keepalive?: boolean
|
||||
|
||||
is_inner?: boolean
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
export interface Menu extends Omit<RouteRecordRaw, 'meta'> {
|
||||
path: string
|
||||
|
||||
name: string
|
||||
|
||||
meta?: Meta
|
||||
|
||||
redirect?: string
|
||||
|
||||
component?: Component
|
||||
|
||||
children?: Menu[]
|
||||
}
|
7
resources/admin/types/responseData.ts
Normal file
7
resources/admin/types/responseData.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default interface ResponseData{
|
||||
code: number;
|
||||
|
||||
message: string;
|
||||
|
||||
data: any
|
||||
}
|
15
resources/admin/types/router.ts
Normal file
15
resources/admin/types/router.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
export interface RouterMeta {
|
||||
icon: string,
|
||||
title: string,
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
export interface RouterRecord {
|
||||
name: string;
|
||||
meta: RouterMeta;
|
||||
component?: string;
|
||||
children?: RouterRecord[];
|
||||
fullPath?: string;
|
||||
redirect: string
|
||||
}
|
36
resources/admin/views/dashboard/dependencies.vue
Normal file
36
resources/admin/views/dashboard/dependencies.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="flex flex-col bg-white dark:bg-regal-dark pl-5 pr-5 rounded">
|
||||
<span class="text-lg mt-5">项目依赖</span>
|
||||
<div class="flex mt-3">
|
||||
<el-table :data="dependencies" border width="200">
|
||||
<el-table-column prop="dependency" label="Dependency" />
|
||||
<el-table-column prop="version" label="Version" />
|
||||
</el-table>
|
||||
<el-table :data="devDependencies" border>
|
||||
<el-table-column prop="devDependency" label="DevDependency" />
|
||||
<el-table-column prop="version" label="Version" />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import packages from '/admin/../package.json'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const dependencies = computed(() => {
|
||||
const _dependencies = []
|
||||
for (const dependency in packages.dependencies) {
|
||||
_dependencies.push(Object.assign({ dependency, version: (packages.dependencies as any)[dependency] }))
|
||||
}
|
||||
|
||||
return _dependencies
|
||||
})
|
||||
const devDependencies = computed(() => {
|
||||
const _devDependencies = []
|
||||
for (const devDependency in packages.devDependencies) {
|
||||
_devDependencies.push(Object.assign({ devDependency, version: (packages.devDependencies as any)[devDependency] }))
|
||||
}
|
||||
|
||||
return _devDependencies
|
||||
})
|
||||
</script>
|
84
resources/admin/views/dashboard/index.vue
Normal file
84
resources/admin/views/dashboard/index.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg-white dark:bg-regal-dark rounded flex flex-col sm:flex-row justify-between">
|
||||
<div class="w-full sm:w-[40rem] flex flex-row p1-1 sm:pl-3 items-center justify-between min-h-28 border-b pb-2 sm:pb-0 sm:border-b-0 border-gray-400">
|
||||
<img :src="avatar" class="rounded-full w-16 h-16" />
|
||||
<div class="flex flex-col w-[34rem] pl-4 sm:pl-0 pt-2 sm:pt-3">
|
||||
<div class="text-lg text-zinc-800 dark:text-gray-200">很高兴见到你👋 ,{{ nickname }}!{{ itsTimeDo }}</div>
|
||||
<div class="text-sm text-gray-400 break-words pt-0 sm:pt-2">{{ context }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center h-28 w-full sm:w-[23rem] justify-between pl-2 pr-2 sm:pr-3">
|
||||
<div class="flex flex-col text-center">
|
||||
<div class="text-lg text-gray-600 dark:text-gray-400">项目数</div>
|
||||
<div class="text text-gray-400 dark:text-gray-200">1000</div>
|
||||
</div>
|
||||
|
||||
<el-divider direction="vertical" />
|
||||
<div class="flex flex-col text-center">
|
||||
<div class="text-lg text-gray-600 dark:text-gray-400">国内排名</div>
|
||||
<div class="text text-gray-400 dark:text-gray-200">1000</div>
|
||||
</div>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<div class="flex flex-col text-center">
|
||||
<div class="text-lg text-gray-600 dark:text-gray-400">团队成员</div>
|
||||
<div class="text text-gray-400 dark:text-gray-200">1000</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row mt-4 justify-between">
|
||||
<Introduce />
|
||||
<Project />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useUserStore } from '/admin/stores/modules/user'
|
||||
import Introduce from './introduce.vue'
|
||||
import Project from './project.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const nickname = computed(() => {
|
||||
return userStore.getNickname
|
||||
})
|
||||
|
||||
const avatar = computed(() => {
|
||||
return userStore.getAvatar
|
||||
})
|
||||
const itsTimeDo = computed(() => {
|
||||
const date = new Date()
|
||||
const now = date.getHours()
|
||||
if (isInRange(now, 2, 5)) {
|
||||
return '凌晨了,该休息了!注意身体!😪'
|
||||
} else if (isInRange(now, 5, 8)) {
|
||||
return '早晨,开始全新的一天!😊'
|
||||
} else if (isInRange(now, 8, 12)) {
|
||||
return '上午好,开始摸鱼的一天!😄'
|
||||
} else if (isInRange(now, 12, 18)) {
|
||||
return '下午好,快要下班了!再坚持下💪'
|
||||
} else if (isInRange(now, 18, 23)) {
|
||||
return '晚上了,请点击右上角关闭!👉'
|
||||
} else {
|
||||
return '深夜了,为什么还在打开该系统?💢'
|
||||
}
|
||||
})
|
||||
|
||||
const context = computed(() => {
|
||||
const contexts: string[] = [
|
||||
'资本主义社会里的民主是一种残缺不全的,贫乏和虚伪和民主,是只供富人,只供少数人享受的民主',
|
||||
'资本来到世间,从头到脚,每个毛孔都滴着血和肮脏的东西',
|
||||
'既然掠夺给少数人造成了天然的权利,那么多数人就只得积聚足够的力量,来取得夺回他们被夺去的一切的天然权利',
|
||||
'资本家有百分之五十的利润,就会铤而走险;有了百分之一百的利润就敢践踏人间一切法律;有了百分之三百的利润就敢冒上绞刑架的危险',
|
||||
]
|
||||
|
||||
return contexts[Math.floor(Math.random() * contexts.length)]
|
||||
})
|
||||
|
||||
function isInRange(compare: number, min: number, max: number) {
|
||||
return compare >= min && compare < max
|
||||
}
|
||||
</script>
|
121
resources/admin/views/dashboard/introduce.vue
Normal file
121
resources/admin/views/dashboard/introduce.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="w-full sm:w-[72%]">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex flex-row justify-between">
|
||||
<span class="text-lg">项目基于</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-1">
|
||||
<el-card shadow="hover">
|
||||
<div class="flex justify-between">
|
||||
<Vue class="w-10 h-10" />
|
||||
<div class="text-lg">
|
||||
<el-link href="https://cn.vuejs.org/" target="_blank" :underline="false"><el-button type="success" size="small">GO</el-button></el-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-400 pt-2">渐进式 JavaScript 框架 易学易用,性能出色,适用场景丰富的 Web 前端框架Go</div>
|
||||
</el-card>
|
||||
<el-card shadow="hover">
|
||||
<div class="flex justify-between">
|
||||
<Laravel class="w-10 h-10" />
|
||||
<div class="text-lg">
|
||||
<el-link href="https://learnku.com/docs/laravel" target="_blank" :underline="false"><el-button type="danger" size="small">GO</el-button></el-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-400 pt-2">Laravel 是一个具有表现力、优雅语法的 Web 应用程序框架</div>
|
||||
</el-card>
|
||||
<el-card shadow="hover">
|
||||
<div class="flex justify-between">
|
||||
<Element class="w-10 h-10" />
|
||||
<div class="text-lg">
|
||||
<el-link href="https://element-plus.org/" target="_blank" :underline="false"><el-button type="primary" size="small">GO</el-button></el-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-400 pt-2">Element Plus 基于 Vue3,面向设计师和开发者的组件库</div>
|
||||
</el-card>
|
||||
<el-card shadow="hover">
|
||||
<div class="flex justify-between">
|
||||
<Tailwindcss class="w-10 h-10" />
|
||||
<div class="text-lg">
|
||||
<el-link href="https://tailwindcss.com/" target="_blank" :underline="false"><el-button size="small">GO</el-button></el-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-400 pt-2">Tailwind CSS 是一个功能类优先的 CSS 框架,无需离开您的HTML,即可快速建立现代网站</div>
|
||||
</el-card>
|
||||
<el-card shadow="hover">
|
||||
<div class="flex justify-between">
|
||||
<Composer class="w-10 h-10" />
|
||||
<div class="text-lg">
|
||||
<el-link href="https://getcomposer.org/" target="_blank" :underline="false"><el-button type="info" size="small">GO</el-button></el-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-400 pt-2">Composer 是 PHP 中用于依赖管理的工具。它允许您声明项目所依赖的库,并将为您管理(安装/更新)它们。</div>
|
||||
</el-card>
|
||||
<el-card shadow="hover">
|
||||
<div class="flex justify-between">
|
||||
<Vite class="w-10 h-10" />
|
||||
<div class="text-lg">
|
||||
<el-link href="https://cn.vitejs.dev/" target="_blank" :underline="false"><el-button type="warning" size="small">Go</el-button></el-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-400 pt-2">Vite 下一代的前端工具链 为开发提供极速响应</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<div>
|
||||
<el-card shadow="never" class="mt-4">
|
||||
<template #header>
|
||||
<div class="flex flex-row justify-between">
|
||||
<span class="text-lg">项目动态</span>
|
||||
</div>
|
||||
</template>
|
||||
<ul>
|
||||
<li class="flex border-b border-b-slate-200 dark:border-b-slate-700 pt-2">
|
||||
<div><el-avatar src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" /></div>
|
||||
<div class="flex flex-col ml-4">
|
||||
<div class="text">JaguarJack 提交一个 PR</div>
|
||||
<div class="text-sm text-gray-400 mt-1 mb-2">1 小时前</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex border-b border-b-slate-200 dark:border-b-slate-700 pt-2">
|
||||
<div><el-avatar src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" /></div>
|
||||
<div class="flex flex-col ml-4 mt-1">
|
||||
<div>JaguarJack 关闭了一个 Issue</div>
|
||||
<div class="text-sm text-gray-400 mt-1 mb-2">2 小时前</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex border-b border-b-slate-200 dark:border-b-slate-700 pt-2">
|
||||
<div><el-avatar src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" /></div>
|
||||
<div class="flex flex-col ml-4 mt-1">
|
||||
<div>JaguarJack 提交新的 commit</div>
|
||||
<div class="text-sm text-gray-400 mt-1 mb-2">24 小时前</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex border-b border-b-slate-200 dark:border-b-slate-700 pt-2">
|
||||
<div><el-avatar src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" /></div>
|
||||
<div class="flex flex-col ml-4 mt-1">
|
||||
<div>JaguarJack 创建了一个 new project</div>
|
||||
<div class="text-sm text-gray-400 mt-1 mb-2">2021.10.26</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
// @ts-ignores
|
||||
import Laravel from '~icons/logos/laravel'
|
||||
// @ts-ignores
|
||||
import Vue from '~icons/logos/vue'
|
||||
// @ts-ignores
|
||||
import Vite from '~icons/logos/vitejs'
|
||||
// @ts-ignores
|
||||
import Element from '~icons/logos/element'
|
||||
// @ts-ignores
|
||||
import Composer from '~icons/logos/composer'
|
||||
// @ts-ignores
|
||||
import Tailwindcss from '~icons/logos/tailwindcss-icon'
|
||||
</script>
|
39
resources/admin/views/dashboard/project.vue
Normal file
39
resources/admin/views/dashboard/project.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="w-full sm:w-[27%]">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex flex-row justify-between">
|
||||
<span class="text-lg">项目信息</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-1 gap-1">
|
||||
<div>
|
||||
<div class="text-xl text-center">Catch-admin</div>
|
||||
<div class="text-base text-gray-400">
|
||||
是一个基于Vue3.0、Vite、 ElementPlus 、TypeScript 的后台解决方案,提供了丰富的功能组件,它可以帮助你快速搭建企业级中后台产品原型。
|
||||
<el-link href="https://catchadmin.com/" target="_blank">更多...</el-link>
|
||||
</div>
|
||||
<div class="flex justify-between pl-4 pr-4 mt-2 w-full sm:w-[75%] m-auto">
|
||||
<a href="https://github.com/JaguarJack/catch-admin/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/JaguarJack/catch-admin" /></a>
|
||||
<a href="https://github.com/JaguarJack/catch-admin/network"><img alt="GitHub forks" src="https://img.shields.io/github/forks/JaguarJack/catch-admin" /></a>
|
||||
<a href="https://github.com/JaguarJack/catch-admin/blob/master/LICENSE.txt"><img alt="GitHub license" src="https://img.shields.io/github/license/JaguarJack/catch-admin" /></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-1 mt-2">
|
||||
<el-card shadow="hover">
|
||||
<div class="text-center">易用</div>
|
||||
<div class="text-sm text-gray-400 pt-2">catchadmin 是从头开始设计的模块化框架,解耦化的设计让你轻松驾驭框架</div>
|
||||
</el-card>
|
||||
<el-card shadow="hover">
|
||||
<div class="text-center">专注</div>
|
||||
<div class="text-sm text-gray-400 pt-2">catchadmin 加入大量快速开发组件,让你专注于后台管理的开发,而无需重复无意义的工作</div>
|
||||
</el-card>
|
||||
<el-card shadow="hover">
|
||||
<div class="text-center">强大</div>
|
||||
<div class="text-sm text-gray-400 pt-2">catchadmin 基于 PHP 庞大的生态,使其稳定并且强大。使用 catchadmin 可以开发任意 web 应用</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
74
resources/admin/views/login/index.vue
Normal file
74
resources/admin/views/login/index.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="bg-gray-50 h-screen flex items-center justify-center">
|
||||
<div class="flex w-full sm:w-[32rem] shadow bg-white lg:rounded-lg">
|
||||
<!--<div class="w-1/2 hidden sm:block">
|
||||
<img src="@/assets/login-left.png" />
|
||||
</div>-->
|
||||
<div class="w-full mx-auto pt-6 pb-6 pl-4 pr-4">
|
||||
<div class="flex mt-2">
|
||||
<img :src="logo" class="mx-auto w-8" />
|
||||
</div>
|
||||
<div class="w-full text-center text-2xl mt-6 mb-8 text-indigo-700">Hi, {{ $t('login.welcome') }}</div>
|
||||
<el-divider>{{ $t('login.sign_in') }}</el-divider>
|
||||
<div>
|
||||
<el-form
|
||||
ref="form"
|
||||
:model="params"
|
||||
status-icon
|
||||
v-loading.fullscreen.lock="loading"
|
||||
:rules="rules"
|
||||
element-loading-background="rgba(0, 0, 0, 0.7)"
|
||||
label-width="70px"
|
||||
class="w-11/12 sm:w-4/5 pt-2 space-y-8 mx-auto"
|
||||
>
|
||||
<el-form-item prop="email">
|
||||
<el-input v-model="params.email" type="email" autocomplete="off" :placeholder="$t('login.email')" size="large" :prefix-icon="Message" class="h-12 text-base" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="params.password" type="password" autocomplete="off" size="large" :placeholder="$t('login.password')" show-password :prefix-icon="Lock" class="h-12 text-base" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="flex justify-between w-11/12 sm:w-4/5 mx-auto mt-3">
|
||||
<el-checkbox v-model="params.remember" class="top-2">
|
||||
{{ $t('login.remember') }}
|
||||
</el-checkbox>
|
||||
<div class="text-sm pt-3 text-indigo-600 cursor-pointer">
|
||||
{{ $t('login.lost_password') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-11/12 sm:w-4/5 mx-auto mt-5">
|
||||
<el-button type="primary" @click="submit(form)" size="large" class="w-full text-xl">
|
||||
{{ $t('login.sign_in') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="w-full text-center text-sm text-gray-400 mt-8 mb-10">{{ $t('system.name') }} @copyright 2018-{{ new Date().getFullYear() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Lock, Message } from '@element-plus/icons-vue'
|
||||
import { onMounted } from 'vue'
|
||||
import { useLogin } from './login'
|
||||
import logo from '/admin/assets/logo.png'
|
||||
|
||||
const { params, loading, submit, form, rules } = useLogin()
|
||||
|
||||
// set default color-theme light
|
||||
onMounted(() => {
|
||||
document.querySelector('html')?.setAttribute('class', 'light')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.el-divider__text) {
|
||||
@apply text-xl text-slate-400;
|
||||
}
|
||||
</style>
|
60
resources/admin/views/login/login.ts
Normal file
60
resources/admin/views/login/login.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { reactive, ref } from 'vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { useUserStore } from '/admin/stores/modules/user'
|
||||
import router from '/admin/router'
|
||||
import { t } from '/admin/support/helper'
|
||||
|
||||
export const useLogin = () => {
|
||||
const params = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const rules = reactive({
|
||||
email: [
|
||||
{ required: true, message: t('login.verify.email.required'), trigger: 'blur' },
|
||||
{ type: 'email', message: t('login.verify.email.invalid'), trigger: 'blur' },
|
||||
],
|
||||
password: [{ required: true, message: t('login.verify.password.required'), trigger: 'blur' }],
|
||||
})
|
||||
|
||||
const form = ref<FormInstance>()
|
||||
|
||||
const submit = (loginForm: FormInstance | undefined) => {
|
||||
if (!loginForm) return
|
||||
|
||||
loginForm.validate(valid => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
const store = useUserStore()
|
||||
|
||||
store
|
||||
.login(params)
|
||||
.then(() => {
|
||||
loading.value = false
|
||||
router.push({ path: '/' })
|
||||
})
|
||||
.catch(e => {
|
||||
loading.value = false
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
params,
|
||||
|
||||
rules,
|
||||
|
||||
loading,
|
||||
|
||||
submit,
|
||||
|
||||
form,
|
||||
}
|
||||
}
|
0
resources/views/welcome.blade.php
Normal file
0
resources/views/welcome.blade.php
Normal file
Reference in New Issue
Block a user