TypeScript 支持
@@ -1,89 +1,100 @@
|
||||
<script setup>
|
||||
<script lang="ts" setup>
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import eventBus from './utils/eventBus'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const buttonConfig = ref({
|
||||
autoInsertSpace: true,
|
||||
})
|
||||
|
||||
// 侧边栏主导航当前实际宽度
|
||||
const mainSidebarActualWidth = computed(() => {
|
||||
let actualWidth = getComputedStyle(document.documentElement).getPropertyValue('--g-main-sidebar-width')
|
||||
actualWidth = parseInt(actualWidth)
|
||||
if (['head', 'single'].includes(settingsStore.menu.menuMode)) {
|
||||
actualWidth = 0
|
||||
}
|
||||
return `${actualWidth}px`
|
||||
let actualWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-main-sidebar-width'))
|
||||
if (['head', 'single'].includes(settingsStore.menu.menuMode))
|
||||
actualWidth = 0
|
||||
|
||||
return `${actualWidth}px`
|
||||
})
|
||||
|
||||
// 侧边栏次导航当前实际宽度
|
||||
const subSidebarActualWidth = computed(() => {
|
||||
let actualWidth = getComputedStyle(document.documentElement).getPropertyValue('--g-sub-sidebar-width')
|
||||
actualWidth = parseInt(actualWidth)
|
||||
if (settingsStore.menu.subMenuCollapse) {
|
||||
actualWidth = 64
|
||||
}
|
||||
return `${actualWidth}px`
|
||||
let actualWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-sub-sidebar-width'))
|
||||
if (settingsStore.menu.subMenuCollapse)
|
||||
actualWidth = 64
|
||||
|
||||
return `${actualWidth}px`
|
||||
})
|
||||
|
||||
watch(() => settingsStore.app.colorScheme, () => {
|
||||
if (settingsStore.app.colorScheme === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
if (settingsStore.app.colorScheme === 'dark')
|
||||
document.documentElement.classList.add('dark')
|
||||
|
||||
else
|
||||
document.documentElement.classList.remove('dark')
|
||||
}, {
|
||||
immediate: true
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
watch(() => settingsStore.mode, () => {
|
||||
if (settingsStore.mode === 'pc') {
|
||||
settingsStore.$patch(state => {
|
||||
state.menu.subMenuCollapse = settingsStore.subMenuCollapseLastStatus
|
||||
})
|
||||
} else if (settingsStore.mode === 'mobile') {
|
||||
settingsStore.$patch(state => {
|
||||
state.menu.subMenuCollapse = true
|
||||
})
|
||||
}
|
||||
document.body.setAttribute('data-mode', settingsStore.mode)
|
||||
if (settingsStore.mode === 'pc') {
|
||||
settingsStore.$patch((state) => {
|
||||
state.menu.subMenuCollapse = settingsStore.subMenuCollapseLastStatus
|
||||
})
|
||||
}
|
||||
else if (settingsStore.mode === 'mobile') {
|
||||
settingsStore.$patch((state) => {
|
||||
state.menu.subMenuCollapse = true
|
||||
})
|
||||
}
|
||||
document.body.setAttribute('data-mode', settingsStore.mode)
|
||||
}, {
|
||||
immediate: true
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
watch(() => settingsStore.menu.menuMode, () => {
|
||||
document.body.setAttribute('data-menu-mode', settingsStore.menu.menuMode)
|
||||
document.body.setAttribute('data-menu-mode', settingsStore.menu.menuMode)
|
||||
}, {
|
||||
immediate: true
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
watch([
|
||||
() => settingsStore.app.enableDynamicTitle,
|
||||
() => settingsStore.title
|
||||
() => settingsStore.app.enableDynamicTitle,
|
||||
() => settingsStore.title,
|
||||
], () => {
|
||||
if (settingsStore.app.enableDynamicTitle && settingsStore.title) {
|
||||
let title = settingsStore.title
|
||||
document.title = `${title} - ${import.meta.env.VITE_APP_TITLE}`
|
||||
} else {
|
||||
document.title = import.meta.env.VITE_APP_TITLE
|
||||
}
|
||||
if (settingsStore.app.enableDynamicTitle && settingsStore.title) {
|
||||
const title = settingsStore.title
|
||||
document.title = `${title} - ${import.meta.env.VITE_APP_TITLE}`
|
||||
}
|
||||
else {
|
||||
document.title = import.meta.env.VITE_APP_TITLE
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.onresize = () => {
|
||||
settingsStore.setMode(document.documentElement.clientWidth)
|
||||
}
|
||||
window.onresize()
|
||||
settingsStore.setMode(document.documentElement.clientWidth)
|
||||
window.onresize = () => {
|
||||
settingsStore.setMode(document.documentElement.clientWidth)
|
||||
}
|
||||
hotkeys('alt+i', () => {
|
||||
eventBus.emit('global-system-info-toggle')
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-config-provider :locale="zhCn" :size="settingsStore.app.elementSize" :button="{autoInsertSpace: true}">
|
||||
<RouterView
|
||||
:style="{
|
||||
'--g-main-sidebar-actual-width': mainSidebarActualWidth,
|
||||
'--g-sub-sidebar-actual-width': subSidebarActualWidth
|
||||
}"
|
||||
/>
|
||||
</el-config-provider>
|
||||
<el-config-provider :locale="zhCn" :size="settingsStore.app.elementSize" :button="buttonConfig">
|
||||
<RouterView
|
||||
:style="{
|
||||
'--g-main-sidebar-actual-width': mainSidebarActualWidth,
|
||||
'--g-sub-sidebar-actual-width': subSidebarActualWidth,
|
||||
}"
|
||||
/>
|
||||
<system-info />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import axios from 'axios'
|
||||
// import qs from 'qs'
|
||||
import router from '@/router/index'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
const toLogin = () => {
|
||||
useUserStore().logout().then(() => {
|
||||
router.push({
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: router.currentRoute.value.path !== '/login' ? router.currentRoute.value.fullPath : undefined
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.DEV && import.meta.env.VITE_OPEN_PROXY === 'true' ? '/proxy/' : import.meta.env.VITE_APP_API_BASEURL,
|
||||
timeout: 10000,
|
||||
responseType: 'json'
|
||||
})
|
||||
|
||||
api.interceptors.request.use(
|
||||
request => {
|
||||
const userStore = useUserStore()
|
||||
/**
|
||||
* 全局拦截请求发送前提交的参数
|
||||
* 以下代码为示例,在请求头里带上 token 信息
|
||||
*/
|
||||
if (userStore.isLogin) {
|
||||
request.headers['Token'] = userStore.token
|
||||
}
|
||||
// 是否将 POST 请求参数进行字符串化处理
|
||||
if (request.method === 'post') {
|
||||
// request.data = qs.stringify(request.data, {
|
||||
// arrayFormat: 'brackets'
|
||||
// })
|
||||
}
|
||||
return request
|
||||
}
|
||||
)
|
||||
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
/**
|
||||
* 全局拦截请求发送后返回的数据,如果数据有报错则在这做全局的错误提示
|
||||
* 假设返回数据格式为:{ status: 1, error: '', data: '' }
|
||||
* 规则是当 status 为 1 时表示请求成功,为 0 时表示接口需要登录或者登录状态失效,需要重新登录
|
||||
* 请求出错时 error 会返回错误信息
|
||||
*/
|
||||
if (response.data.status === 1) {
|
||||
if (response.data.error === '') {
|
||||
// 请求成功并且没有报错
|
||||
return Promise.resolve(response.data)
|
||||
} else {
|
||||
// 这里做错误提示
|
||||
// ElMessage.error(options)
|
||||
return Promise.reject(response.data)
|
||||
}
|
||||
} else {
|
||||
toLogin()
|
||||
}
|
||||
},
|
||||
error => {
|
||||
let message = error.message
|
||||
if (message == 'Network Error') {
|
||||
message = '后端网络故障'
|
||||
} else if (message.includes('timeout')) {
|
||||
message = '接口请求超时'
|
||||
} else if (message.includes('Request failed with status code')) {
|
||||
message = '接口' + message.substr(message.length - 3) + '异常'
|
||||
}
|
||||
ElMessage({
|
||||
message,
|
||||
type: 'error'
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default api
|
||||
@@ -0,0 +1,83 @@
|
||||
import axios from 'axios'
|
||||
// import qs from 'qs'
|
||||
import router from '@/router/index'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
const toLogin = () => {
|
||||
router.push({
|
||||
path: '/login',
|
||||
query: {
|
||||
redirect: router.currentRoute.value.path !== '/login' ? router.currentRoute.value.fullPath : undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.DEV && import.meta.env.VITE_OPEN_PROXY === 'true' ? '/proxy/' : import.meta.env.VITE_APP_API_BASEURL,
|
||||
timeout: 10000,
|
||||
responseType: 'json',
|
||||
})
|
||||
|
||||
api.interceptors.request.use(
|
||||
(request) => {
|
||||
const userStore = useUserStore()
|
||||
/**
|
||||
* 全局拦截请求发送前提交的参数
|
||||
* 以下代码为示例,在请求头里带上 token 信息
|
||||
*/
|
||||
if (userStore.isLogin && request.headers)
|
||||
request.headers.Token = userStore.token
|
||||
|
||||
// 是否将 POST 请求参数进行字符串化处理
|
||||
if (request.method === 'post') {
|
||||
// request.data = qs.stringify(request.data, {
|
||||
// arrayFormat: 'brackets',
|
||||
// })
|
||||
}
|
||||
return request
|
||||
},
|
||||
)
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
/**
|
||||
* 全局拦截请求发送后返回的数据,如果数据有报错则在这做全局的错误提示
|
||||
* 假设返回数据格式为:{ status: 1, error: '', data: '' }
|
||||
* 规则是当 status 为 1 时表示请求成功,为 0 时表示接口需要登录或者登录状态失效,需要重新登录
|
||||
* 请求出错时 error 会返回错误信息
|
||||
*/
|
||||
if (response.data.status === 1) {
|
||||
if (response.data.error === '') {
|
||||
// 请求成功并且没有报错
|
||||
return Promise.resolve(response.data)
|
||||
}
|
||||
else {
|
||||
// 这里做错误提示,如果使用了 element plus 则可以使用 Message 进行提示
|
||||
// ElMessage.error(options)
|
||||
return Promise.reject(response.data)
|
||||
}
|
||||
}
|
||||
else {
|
||||
toLogin()
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
let message = error.message
|
||||
if (message === 'Network Error')
|
||||
message = '后端网络故障'
|
||||
|
||||
else if (message.includes('timeout'))
|
||||
message = '接口请求超时'
|
||||
|
||||
else if (message.includes('Request failed with status code'))
|
||||
message = `接口${message.substr(message.length - 3)}异常`
|
||||
|
||||
ElMessage({
|
||||
message,
|
||||
type: 'error',
|
||||
})
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
export default api
|
||||
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1623578868505" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5778" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M972.4928 217.4976a73.4208 73.4208 0 0 0-58.5728-64.0512 1415.7312 1415.7312 0 0 1-189.184-42.9568A706.1504 706.1504 0 0 1 555.52 14.4384a77.6704 77.6704 0 0 0-90.112 0 445.8496 445.8496 0 0 1-170.496 96.0512 639.0272 639.0272 0 0 1-183.8592 44.8512A67.8912 67.8912 0 0 0 53.1968 219.392S51.2 379.2384 51.2 520.448C51.2 776.4992 358.1952 1024 511.8464 1024c153.6 0 413.5936-174.592 455.0144-499.712 6.5536-101.888 7.68-204.0832 3.328-306.176H972.8l-0.3072-0.6144z m-167.168 197.9392l-320.512 297.216a43.6736 43.6736 0 0 1-53.248 5.12l-8.5504-7.0144L246.272 529.408a42.752 42.752 0 0 1 2.304-61.184 43.776 43.776 0 0 1 62.1568 2.56l145.7664 150.2208 288.8704-269.6704a44.4416 44.4416 0 0 1 62.5152 2.56c16.5376 17.7664 15.36 45.1072-2.6112 61.44z" p-id="5779"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1 +0,0 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>
|
||||
|
Before Width: | Height: | Size: 944 B |
@@ -1 +0,0 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>
|
||||
|
Before Width: | Height: | Size: 623 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1628158537840" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1342" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M954.391106 733.455997c-0.269471 42.495631-22.366122 51.954075-22.366122 51.954075s-380.897745 220.131141-405.662161 233.901127c-24.521892 10.53633-40.959644 0-40.959645 0S86.882008 788.15868 72.330555 777.99961c-14.551453-10.15907-14.901765-26.003985-14.901765-26.003985s0.404207-457.724025 0-478.93142c-0.404207-21.180448 26.057879-37.106204 26.057879-37.106204L481.711421 5.748632c24.521892-12.934625 48.370106 0 48.370106 0s351.821787 204.52875 390.787343 226.490665c38.237984 18.162369 33.522235 55.67278 33.522236 55.67278s0.215577 406.039421 0 445.54392z m-159.015041-459.421695a166166.674868 166166.674868 0 0 1-273.621202-158.233573s-18.755206-10.132123-37.99546 0L171.091803 295.942323s-20.776241 12.476523-20.479823 29.04901c0.323366 16.599435 0 374.83464 0 374.83464s0.269471 12.395682 11.695057 20.345087 324.362657 188.845518 324.362657 188.845517 12.88073 8.245823 32.147931 0c19.455831-10.778854 318.515129-183.051884 318.515129-183.051884s17.327007-7.410462 17.515637-40.690172c0.080841-9.566233 0.107789-46.807172 0.107789-93.560451L502.837974 805.054533v-81.62287c0-33.522235 25.95009-55.645833 25.950091-55.645833l311.454979-187.659844c11.748951-12.287893 14.174193-31.932354 14.659241-39.369763v-82.727703L502.837974 571.288142v-85.314628c0-33.549182 22.258333-48.235371 22.258333-48.23537l270.279758-163.730789z" p-id="1343"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M936.003 250.68C908.57 92.426 759.26-13.143 602.593 14.69 446.136 42.526 313.923 164.397 341.56 322.451c5.965 34.197 22.462 87.48 41.948 125.85L99.215 858.445c-10.537 15.109-16.501 42.347-13.322 60.636l12.33 70.18c-11.73-67.594 3.179 24.453 12.323 24.054l93.64 1.392c18.094-3.18 40.755-18.488 50.698-33.997L367.41 804.762l.988-1.193 98.217 7.756 131.41-205.967c43.146 7.358 104.97 4.772 141.753-1.587 155.863-27.838 224.061-195.036 196.224-353.09zm-759.87 635.904-24.591 15.483 5.729-27.817L424.079 492.4l18.453 12.918-266.399 381.266zM797.43 331.597c-43.141 62.034-87.076 18.294-148.511-25.247-61.435-43.54-117.3-70.175-74.158-132.408 43.141-62.024 127.836-77.338 189.266-33.797 61.435 43.735 76.544 129.423 33.403 191.452z"/></svg>
|
||||
|
Before Width: | Height: | Size: 844 B |
@@ -1 +0,0 @@
|
||||
<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>
|
||||
|
Before Width: | Height: | Size: 440 B |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
@@ -1,9 +1,10 @@
|
||||
// 暗黑模式
|
||||
@forward "element-plus/theme-chalk/src/dark/var.scss" with (
|
||||
$bg-color: (
|
||||
"page": #0a0a0a,
|
||||
"": #141414,
|
||||
"overlay": #1d1e1f,
|
||||
)
|
||||
$bg-color: (
|
||||
"page": #0a0a0a,
|
||||
"": #141414,
|
||||
"overlay": #1d1e1f,
|
||||
)
|
||||
);
|
||||
|
||||
@use "element-plus/theme-chalk/src/dark/css-vars.scss" as *;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// @forward "element-plus/theme-chalk/src/common/var.scss" with (
|
||||
// $colors: (
|
||||
// "primary": (
|
||||
// "base": green
|
||||
// )
|
||||
// $colors: (
|
||||
// "primary": (
|
||||
// "base": green
|
||||
// )
|
||||
// )
|
||||
// );
|
||||
|
||||
@use "element-plus/theme-chalk/src/index.scss" as *;
|
||||
@use "./dark.scss";
|
||||
|
||||
@@ -3,73 +3,82 @@
|
||||
|
||||
// 全局变量
|
||||
:root {
|
||||
// 头部宽度(默认自适应宽度,可固定宽度,固定宽度后为居中显示)
|
||||
--g-header-width: 100%;
|
||||
// 头部高度
|
||||
--g-header-height: 70px;
|
||||
// 侧边栏宽度
|
||||
--g-main-sidebar-width: 70px;
|
||||
--g-sub-sidebar-width: 220px;
|
||||
// 侧边栏Logo高度
|
||||
--g-sidebar-logo-height: 50px;
|
||||
// 顶栏高度
|
||||
--g-topbar-height: 50px;
|
||||
// 头部宽度(默认自适应宽度,可固定宽度,固定宽度后为居中显示)
|
||||
--g-header-width: 100%;
|
||||
// 头部高度
|
||||
--g-header-height: 70px;
|
||||
// 侧边栏宽度
|
||||
--g-main-sidebar-width: 70px;
|
||||
--g-sub-sidebar-width: 220px;
|
||||
// 侧边栏Logo高度
|
||||
--g-sidebar-logo-height: 50px;
|
||||
// 顶栏高度
|
||||
--g-topbar-height: 50px;
|
||||
}
|
||||
// 全局样式
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(0 0 0 / 40%);
|
||||
background-clip: padding-box;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 6px;
|
||||
background-color: rgb(0 0 0 / 40%);
|
||||
background-clip: padding-box;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgb(0 0 0 / 50%);
|
||||
background-color: rgb(0 0 0 / 50%);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: Lato, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
&.hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: Lato, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&.hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: inherit;
|
||||
box-sizing: inherit;
|
||||
}
|
||||
// 右侧内容区针对fixed元素,有横向铺满的需求,可在fixed元素上设置 [data-fixed-calc-width]
|
||||
[data-fixed-calc-width] {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
right: 0;
|
||||
width: calc(100% - var(--g-main-sidebar-actual-width) - var(--g-sub-sidebar-actual-width));
|
||||
transform: translateX(-50%) translateX(calc(var(--g-main-sidebar-actual-width) / 2)) translateX(calc(var(--g-sub-sidebar-actual-width) / 2));
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
right: 0;
|
||||
width: calc(100% - var(--g-main-sidebar-actual-width) - var(--g-sub-sidebar-actual-width));
|
||||
transform: translateX(-50%) translateX(calc(var(--g-main-sidebar-actual-width) / 2)) translateX(calc(var(--g-sub-sidebar-actual-width) / 2));
|
||||
}
|
||||
|
||||
[data-mode="mobile"] {
|
||||
[data-fixed-calc-width] {
|
||||
width: 100% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
}
|
||||
[data-fixed-calc-width] {
|
||||
width: 100% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
}
|
||||
}
|
||||
// textarea 字体跟随系统
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
// 列表页全局样式
|
||||
.el-table.list-table {
|
||||
margin: 20px 0;
|
||||
.el-button + .el-dropdown {
|
||||
margin-left: 10px;
|
||||
}
|
||||
margin: 20px 0;
|
||||
|
||||
.el-button + .el-dropdown {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +1,63 @@
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
.bar {
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--g-nprogress-color);
|
||||
}
|
||||
.peg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
opacity: 1;
|
||||
transform: rotate(3deg) translate(0, -4px);
|
||||
box-shadow: 0 0 10px var(--g-nprogress-color), 0 0 5px var(--g-nprogress-color);
|
||||
}
|
||||
.spinner {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 11px;
|
||||
right: 14px;
|
||||
.spinner-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
box-sizing: border-box;
|
||||
border: solid 2px transparent;
|
||||
border-radius: 50%;
|
||||
animation: nprogress-spinner 400ms linear infinite;
|
||||
border-top-color: var(--g-nprogress-color);
|
||||
border-left-color: var(--g-nprogress-color);
|
||||
}
|
||||
pointer-events: none;
|
||||
|
||||
.bar {
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--g-nprogress-color);
|
||||
}
|
||||
|
||||
.peg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
opacity: 1;
|
||||
transform: rotate(3deg) translate(0, -4px);
|
||||
box-shadow: 0 0 10px var(--g-nprogress-color), 0 0 5px var(--g-nprogress-color);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 11px;
|
||||
right: 14px;
|
||||
|
||||
.spinner-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
box-sizing: border-box;
|
||||
border: solid 2px transparent;
|
||||
border-radius: 50%;
|
||||
animation: nprogress-spinner 400ms linear infinite;
|
||||
border-top-color: var(--g-nprogress-color);
|
||||
border-left-color: var(--g-nprogress-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nprogress-custom-parent {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
#nprogress .spinner,
|
||||
#nprogress .bar {
|
||||
position: absolute;
|
||||
}
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
#nprogress .spinner,
|
||||
#nprogress .bar {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes nprogress-spinner {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes nprogress-spinner {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@@ -1,50 +1,55 @@
|
||||
// 文字超出隐藏,默认为单行超出隐藏,可设置多行
|
||||
@mixin text-overflow($line: 1, $fixed-width: true) {
|
||||
@if ($line == 1 and $fixed-width == true) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@else {
|
||||
/* stylelint-disable-next-line value-no-vendor-prefix */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: $line;
|
||||
overflow: hidden;
|
||||
}
|
||||
@if ($line == 1 and $fixed-width == true) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@else {
|
||||
/* stylelint-disable-next-line value-no-vendor-prefix */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: $line;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// 定位居中,默认水平居中,可选择垂直居中,或者水平垂直都居中
|
||||
@mixin position-center($type: x) {
|
||||
position: absolute;
|
||||
@if ($type == x) {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
@if ($type == y) {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
@if ($type == xy) {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
position: absolute;
|
||||
|
||||
@if ($type == x) {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
@if ($type == y) {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
@if ($type == xy) {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
// 文字两端对齐
|
||||
%justify-align {
|
||||
text-align: justify;
|
||||
text-align-last: justify;
|
||||
text-align: justify;
|
||||
text-align-last: justify;
|
||||
}
|
||||
|
||||
// 清除浮动
|
||||
%clearfix {
|
||||
zoom: 1;
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
zoom: 1;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,67 @@
|
||||
:root {
|
||||
--g-box-shadow-color: rgb(0 0 0 / 12%);
|
||||
--g-box-shadow-color: rgb(0 0 0 / 12%);
|
||||
|
||||
/* ----- 应用 ----- */
|
||||
// 应用背景色
|
||||
--g-app-bg: var(--el-bg-color);
|
||||
// 主区域背景色
|
||||
--g-main-bg: var(--el-bg-color-page);
|
||||
|
||||
/* ----- 顶部 ----- */
|
||||
// 背景色
|
||||
--g-header-bg: #222b45;
|
||||
// 文字颜色
|
||||
--g-header-color: #fff;
|
||||
// 导航文字颜色
|
||||
--g-header-menu-color: #fff;
|
||||
// 导航鼠标悬浮文字颜色
|
||||
--g-header-menu-hover-color: #fff;
|
||||
// 导航鼠标悬浮背景色
|
||||
--g-header-menu-hover-bg: #334067;
|
||||
// 导航选中文字颜色
|
||||
--g-header-menu-active-color: #fff;
|
||||
// 导航选中背景色
|
||||
--g-header-menu-active-bg: #334067;
|
||||
|
||||
/* ----- 主导航 ----- */
|
||||
// 背景色
|
||||
--g-main-sidebar-bg: #222b45;
|
||||
// 菜单文字颜色
|
||||
--g-main-sidebar-menu-color: #eee;
|
||||
// 菜单鼠标悬浮文字颜色
|
||||
--g-main-sidebar-menu-hover-color: #fff;
|
||||
// 菜单鼠标悬浮背景色
|
||||
--g-main-sidebar-menu-hover-bg: #334067;
|
||||
// 菜单选中文字颜色
|
||||
--g-main-sidebar-menu-active-color: #fff;
|
||||
// 菜单选中背景色
|
||||
--g-main-sidebar-menu-active-bg: #334067;
|
||||
|
||||
/* ----- 次导航 ----- */
|
||||
// 背景色
|
||||
--g-sub-sidebar-bg: #fff;
|
||||
// 菜单背景色
|
||||
--g-sub-sidebar-menu-bg: #e8e8e8;
|
||||
// 菜单文字颜色
|
||||
--g-sub-sidebar-menu-color: var(--el-text-color-primary);
|
||||
// 菜单鼠标悬浮文字颜色
|
||||
--g-sub-sidebar-menu-hover-color: var(--el-text-color-primary);
|
||||
// 菜单鼠标悬浮背景色
|
||||
--g-sub-sidebar-menu-hover-bg: #e1e1e1;
|
||||
// 菜单选中文字颜色
|
||||
--g-sub-sidebar-menu-active-color: #fff;
|
||||
// 菜单选中背景色
|
||||
--g-sub-sidebar-menu-active-bg: var(--el-color-primary);
|
||||
|
||||
/* ----- 顶栏 ----- */
|
||||
--g-toolbar-bg: var(--el-bg-color);
|
||||
|
||||
/* ----- 进度条 ----- */
|
||||
--g-nprogress-color: var(--el-color-primary);
|
||||
|
||||
/* ----- 暗黑模式 ----- */
|
||||
&.dark {
|
||||
--g-box-shadow-color: rgb(0 0 0 / 72%);
|
||||
|
||||
/* ----- 应用 ----- */
|
||||
// 应用背景色
|
||||
@@ -9,116 +71,54 @@
|
||||
|
||||
/* ----- 顶部 ----- */
|
||||
// 背景色
|
||||
--g-header-bg: #222b45;
|
||||
--g-header-bg: var(--el-bg-color);
|
||||
// 文字颜色
|
||||
--g-header-color: #fff;
|
||||
// 导航文字颜色
|
||||
--g-header-menu-color: #fff;
|
||||
--g-header-menu-color: #ccc;
|
||||
// 导航鼠标悬浮文字颜色
|
||||
--g-header-menu-hover-color: #fff;
|
||||
// 导航鼠标悬浮背景色
|
||||
--g-header-menu-hover-bg: #334067;
|
||||
--g-header-menu-hover-bg: var(--el-color-primary-light-5);
|
||||
// 导航选中文字颜色
|
||||
--g-header-menu-active-color: #fff;
|
||||
// 导航选中背景色
|
||||
--g-header-menu-active-bg: #334067;
|
||||
--g-header-menu-active-bg: var(--el-color-primary-light-3);
|
||||
|
||||
/* ----- 主导航 ----- */
|
||||
// 背景色
|
||||
--g-main-sidebar-bg: #222b45;
|
||||
--g-main-sidebar-bg: var(--el-bg-color);
|
||||
// 菜单文字颜色
|
||||
--g-main-sidebar-menu-color: #eee;
|
||||
--g-main-sidebar-menu-color: #ccc;
|
||||
// 菜单鼠标悬浮文字颜色
|
||||
--g-main-sidebar-menu-hover-color: #fff;
|
||||
// 菜单鼠标悬浮背景色
|
||||
--g-main-sidebar-menu-hover-bg: #334067;
|
||||
--g-main-sidebar-menu-hover-bg: var(--el-color-primary-light-5);
|
||||
// 菜单选中文字颜色
|
||||
--g-main-sidebar-menu-active-color: #fff;
|
||||
// 菜单选中背景色
|
||||
--g-main-sidebar-menu-active-bg: #334067;
|
||||
--g-main-sidebar-menu-active-bg: var(--el-color-primary-light-3);
|
||||
|
||||
/* ----- 次导航 ----- */
|
||||
// 背景色
|
||||
--g-sub-sidebar-bg: #fff;
|
||||
--g-sub-sidebar-bg: var(--el-bg-color-page);
|
||||
// 菜单背景色
|
||||
--g-sub-sidebar-menu-bg: #e8e8e8;
|
||||
--g-sub-sidebar-menu-bg: var(--el-bg-color);
|
||||
// 菜单文字颜色
|
||||
--g-sub-sidebar-menu-color: var(--el-text-color-primary);
|
||||
--g-sub-sidebar-menu-color: var(--el-text-color-secondary);
|
||||
// 菜单鼠标悬浮文字颜色
|
||||
--g-sub-sidebar-menu-hover-color: var(--el-text-color-primary);
|
||||
// 菜单鼠标悬浮背景色
|
||||
--g-sub-sidebar-menu-hover-bg: #e1e1e1;
|
||||
--g-sub-sidebar-menu-hover-bg: var(--el-color-primary-light-5);
|
||||
// 菜单选中文字颜色
|
||||
--g-sub-sidebar-menu-active-color: #fff;
|
||||
--g-sub-sidebar-menu-active-color: var(--el-text-color-primary);
|
||||
// 菜单选中背景色
|
||||
--g-sub-sidebar-menu-active-bg: var(--el-color-primary);
|
||||
--g-sub-sidebar-menu-active-bg: var(--el-color-primary-light-3);
|
||||
|
||||
/* ----- 顶栏 ----- */
|
||||
--g-toolbar-bg: var(--el-bg-color);
|
||||
|
||||
/* ----- 进度条 ----- */
|
||||
--g-nprogress-color: var(--el-color-primary);
|
||||
|
||||
/* ----- 暗黑模式 ----- */
|
||||
&.dark {
|
||||
--g-box-shadow-color: rgb(0 0 0 / 72%);
|
||||
|
||||
/* ----- 应用 ----- */
|
||||
// 应用背景色
|
||||
--g-app-bg: var(--el-bg-color);
|
||||
// 主区域背景色
|
||||
--g-main-bg: var(--el-bg-color-page);
|
||||
|
||||
/* ----- 顶部 ----- */
|
||||
// 背景色
|
||||
--g-header-bg: var(--el-bg-color);
|
||||
// 文字颜色
|
||||
--g-header-color: #fff;
|
||||
// 导航文字颜色
|
||||
--g-header-menu-color: #ccc;
|
||||
// 导航鼠标悬浮文字颜色
|
||||
--g-header-menu-hover-color: #fff;
|
||||
// 导航鼠标悬浮背景色
|
||||
--g-header-menu-hover-bg: var(--el-color-primary-light-5);
|
||||
// 导航选中文字颜色
|
||||
--g-header-menu-active-color: #fff;
|
||||
// 导航选中背景色
|
||||
--g-header-menu-active-bg: var(--el-color-primary-light-3);
|
||||
|
||||
/* ----- 主导航 ----- */
|
||||
// 背景色
|
||||
--g-main-sidebar-bg: var(--el-bg-color);
|
||||
// 菜单文字颜色
|
||||
--g-main-sidebar-menu-color: #ccc;
|
||||
// 菜单鼠标悬浮文字颜色
|
||||
--g-main-sidebar-menu-hover-color: #fff;
|
||||
// 菜单鼠标悬浮背景色
|
||||
--g-main-sidebar-menu-hover-bg: var(--el-color-primary-light-5);
|
||||
// 菜单选中文字颜色
|
||||
--g-main-sidebar-menu-active-color: #fff;
|
||||
// 菜单选中背景色
|
||||
--g-main-sidebar-menu-active-bg: var(--el-color-primary-light-3);
|
||||
|
||||
/* ----- 次导航 ----- */
|
||||
// 背景色
|
||||
--g-sub-sidebar-bg: var(--el-bg-color-page);
|
||||
// 菜单背景色
|
||||
--g-sub-sidebar-menu-bg: var(--el-bg-color);
|
||||
// 菜单文字颜色
|
||||
--g-sub-sidebar-menu-color: var(--el-text-color-secondary);
|
||||
// 菜单鼠标悬浮文字颜色
|
||||
--g-sub-sidebar-menu-hover-color: var(--el-text-color-primary);
|
||||
// 菜单鼠标悬浮背景色
|
||||
--g-sub-sidebar-menu-hover-bg: var(--el-color-primary-light-5);
|
||||
// 菜单选中文字颜色
|
||||
--g-sub-sidebar-menu-active-color: var(--el-text-color-primary);
|
||||
// 菜单选中背景色
|
||||
--g-sub-sidebar-menu-active-bg: var(--el-color-primary-light-3);
|
||||
|
||||
/* ----- 顶栏 ----- */
|
||||
--g-toolbar-bg: var(--el-bg-color);
|
||||
|
||||
/* ----- 进度条 ----- */
|
||||
--g-nprogress-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Generated by 'unplugin-auto-import'
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||
const ElNotification: typeof import('element-plus/es')['ElNotification']
|
||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createPinia: typeof import('pinia')['createPinia']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
const mapStores: typeof import('pinia')['mapStores']
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useAuth: typeof import('./utils/composables/useAuth')['default']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useCurrentInstance: typeof import('./utils/composables/useCurrentInstance')['default']
|
||||
const useMainPage: typeof import('./utils/composables/useMainPage')['default']
|
||||
const useMenu: typeof import('./utils/composables/useMenu')['default']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
export {}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
Auth: typeof import('./components/Auth/index.vue')['default']
|
||||
AuthAll: typeof import('./components/AuthAll/index.vue')['default']
|
||||
BatchActionBar: typeof import('./components/BatchActionBar/index.vue')['default']
|
||||
Copyright: typeof import('./components/Copyright/index.vue')['default']
|
||||
Editor: typeof import('./components/Editor/index.vue')['default']
|
||||
FileUpload: typeof import('./components/FileUpload/index.vue')['default']
|
||||
FixedActionBar: typeof import('./components/FixedActionBar/index.vue')['default']
|
||||
ImagePreview: typeof import('./components/ImagePreview/index.vue')['default']
|
||||
ImagesUpload: typeof import('./components/ImagesUpload/index.vue')['default']
|
||||
ImageUpload: typeof import('./components/ImageUpload/index.vue')['default']
|
||||
PageHeader: typeof import('./components/PageHeader/index.vue')['default']
|
||||
PageMain: typeof import('./components/PageMain/index.vue')['default']
|
||||
PcasCascader: typeof import('./components/PcasCascader/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SearchBar: typeof import('./components/SearchBar/index.vue')['default']
|
||||
SvgIcon: typeof import('./components/SvgIcon/index.vue')['default']
|
||||
SystemInfo: typeof import('./components/SystemInfo/index.vue')['default']
|
||||
Trend: typeof import('./components/Trend/index.vue')['default']
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,16 @@
|
||||
<script setup name="Auth">
|
||||
import { useAuth } from '@/utils/composables'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: [String, Array],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
<script lang="ts" setup name="Auth">
|
||||
const props = defineProps<{
|
||||
value: string | string[]
|
||||
}>()
|
||||
|
||||
function check() {
|
||||
return useAuth().auth(props.value)
|
||||
return useAuth().auth(props.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<slot v-if="check()" />
|
||||
<slot v-else name="no-auth" />
|
||||
</div>
|
||||
<div>
|
||||
<slot v-if="check()" />
|
||||
<slot v-else name="no-auth" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
<script setup name="AuthAll">
|
||||
import { useAuth } from '@/utils/composables'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
<script lang="ts" setup name="AuthAll">
|
||||
const props = defineProps<{
|
||||
value: string[]
|
||||
}>()
|
||||
|
||||
function check() {
|
||||
return useAuth().authAll(props.value)
|
||||
return useAuth().authAll(props.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<slot v-if="check()" />
|
||||
<slot v-else name="no-auth" />
|
||||
</div>
|
||||
<div>
|
||||
<slot v-if="check()" />
|
||||
<slot v-else name="no-auth" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,73 +1,82 @@
|
||||
<script setup name="BatchActionBar">
|
||||
<script lang="ts" setup name="BatchActionBar">
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectionData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectionData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['check-all', 'check-null'])
|
||||
const emit = defineEmits(['checkAll', 'checkNull'])
|
||||
|
||||
const checkAll = computed({
|
||||
get: function() {
|
||||
let flag = false
|
||||
if (props.data.length != 0 && props.data.length == props.selectionData.length) {
|
||||
flag = true
|
||||
}
|
||||
return flag
|
||||
},
|
||||
set: function(newVal) {
|
||||
newVal ? emit('check-all') : emit('check-null')
|
||||
}
|
||||
get() {
|
||||
let flag = false
|
||||
if (props.data.length !== 0 && props.data.length === props.selectionData.length)
|
||||
flag = true
|
||||
|
||||
return flag
|
||||
},
|
||||
set(newVal) {
|
||||
newVal ? emit('checkAll') : emit('checkNull')
|
||||
},
|
||||
})
|
||||
|
||||
const isIndeterminate = computed(() => {
|
||||
let flag = false
|
||||
if (props.selectionData.length > 0 && props.selectionData.length < props.data.length) {
|
||||
flag = true
|
||||
}
|
||||
return flag
|
||||
let flag = false
|
||||
if (props.selectionData.length > 0 && props.selectionData.length < props.data.length)
|
||||
flag = true
|
||||
|
||||
return flag
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="batch-action-bar">
|
||||
<el-checkbox v-model="checkAll" :indeterminate="isIndeterminate" :disabled="!data.length">当页全选</el-checkbox>
|
||||
<div v-if="selectionData.length" class="tips">已选 {{ selectionData.length }} 项</div>
|
||||
<el-form :disabled="!selectionData.length">
|
||||
<slot />
|
||||
</el-form>
|
||||
<div class="batch-action-bar">
|
||||
<el-checkbox v-model="checkAll" :indeterminate="isIndeterminate" :disabled="!data.length">
|
||||
当页全选
|
||||
</el-checkbox>
|
||||
<div v-if="selectionData.length" class="tips">
|
||||
已选 {{ selectionData.length }} 项
|
||||
</div>
|
||||
<el-form :disabled="!selectionData.length">
|
||||
<slot />
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.batch-action-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
padding: 0 10px;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.el-checkbox {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin-right: 20px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-form) {
|
||||
> .el-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.el-checkbox {
|
||||
margin-right: 20px;
|
||||
}
|
||||
.tips {
|
||||
margin-right: 20px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
:deep(.el-form) {
|
||||
> .el-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
> .el-button-group {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
> .el-button-group {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,44 +1,50 @@
|
||||
<script setup name="Copyright">
|
||||
<script lang="ts" setup name="Copyright">
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
const route = useRoute()
|
||||
const settingsStore = useSettingsStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="copyright">
|
||||
<span>Copyright</span>
|
||||
<span class="icon">©</span>
|
||||
<span v-if="settingsStore.copyright.dates">{{ settingsStore.copyright.dates }}</span>
|
||||
<template v-if="settingsStore.copyright.company">
|
||||
<a v-if="settingsStore.copyright.website" :href="settingsStore.copyright.website" target="_blank" rel="noopener">{{ settingsStore.copyright.company }}</a>
|
||||
<span v-else>{{ settingsStore.copyright.company }}</span>
|
||||
</template>
|
||||
<a v-if="settingsStore.copyright.beian" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">{{ settingsStore.copyright.beian }}</a>
|
||||
</footer>
|
||||
<footer v-if="route.meta.copyright ?? settingsStore.copyright.enable" class="copyright">
|
||||
<span>Copyright</span>
|
||||
<span class="icon">©</span>
|
||||
<span v-if="settingsStore.copyright.dates">{{ settingsStore.copyright.dates }}</span>
|
||||
<template v-if="settingsStore.copyright.company">
|
||||
<a v-if="settingsStore.copyright.website" :href="settingsStore.copyright.website" target="_blank" rel="noopener">{{ settingsStore.copyright.company }}</a>
|
||||
<span v-else>{{ settingsStore.copyright.company }}</span>
|
||||
</template>
|
||||
<a v-if="settingsStore.copyright.beian" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">{{ settingsStore.copyright.beian }}</a>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 40px 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 40px 0 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
|
||||
span,
|
||||
a {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
span,
|
||||
a {
|
||||
padding: 0 5px;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: var(--el-transition-color);
|
||||
&:hover {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
padding: 0;
|
||||
font-size: 18px;
|
||||
transition: var(--el-transition-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup name="Editor">
|
||||
<script lang="ts" setup name="Editor">
|
||||
import tinymce from 'tinymce/tinymce'
|
||||
import TinymceEditor from '@tinymce/tinymce-vue'
|
||||
import 'tinymce/themes/silver/theme'
|
||||
@@ -20,78 +20,78 @@ import 'tinymce/plugins/searchreplace'
|
||||
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
setting: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
setting: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const defaultSetting = ref({
|
||||
language_url: 'tinymce/langs/zh-Hans.js',
|
||||
language: 'zh-Hans',
|
||||
skin_url: settingsStore.app.colorScheme === 'light' ? 'tinymce/skins/ui/oxide' : 'tinymce/skins/ui/oxide-dark',
|
||||
content_css: settingsStore.app.colorScheme === 'light' ? 'tinymce/skins/content/default/content.min.css' : 'tinymce/skins/content/dark/content.min.css',
|
||||
min_height: 250,
|
||||
max_height: 600,
|
||||
selector: 'textarea',
|
||||
plugins: 'autolink autoresize fullscreen image insertdatetime link lists media preview table wordcount code searchreplace',
|
||||
toolbar: 'undo redo | blocks | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | forecolor backcolor removeformat | link image media table insertdatetime searchreplace | preview code fullscreen',
|
||||
branding: false,
|
||||
menubar: false,
|
||||
toolbar_mode: 'sliding',
|
||||
insertdatetime_formats: [
|
||||
'%Y年%m月%d日',
|
||||
'%H点%M分%S秒',
|
||||
'%Y-%m-%d',
|
||||
'%H:%M:%S'
|
||||
],
|
||||
// https://www.tiny.cloud/docs/tinymce/6/file-image-upload/#images_upload_handler
|
||||
images_upload_handler: blobInfo => new Promise(resolve => {
|
||||
const img = `data:image/jpeg;base64,${blobInfo.base64()}`
|
||||
resolve(img)
|
||||
})
|
||||
language_url: 'tinymce/langs/zh-Hans.js',
|
||||
language: 'zh-Hans',
|
||||
skin_url: settingsStore.app.colorScheme === 'light' ? 'tinymce/skins/ui/oxide' : 'tinymce/skins/ui/oxide-dark',
|
||||
content_css: settingsStore.app.colorScheme === 'light' ? 'tinymce/skins/content/default/content.min.css' : 'tinymce/skins/content/dark/content.min.css',
|
||||
min_height: 250,
|
||||
max_height: 600,
|
||||
selector: 'textarea',
|
||||
plugins: 'autolink autoresize fullscreen image insertdatetime link lists media preview table wordcount code searchreplace',
|
||||
toolbar: 'undo redo | blocks | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | forecolor backcolor removeformat | link image media table insertdatetime searchreplace | preview code fullscreen',
|
||||
branding: false,
|
||||
menubar: false,
|
||||
toolbar_mode: 'sliding',
|
||||
insertdatetime_formats: [
|
||||
'%Y年%m月%d日',
|
||||
'%H点%M分%S秒',
|
||||
'%Y-%m-%d',
|
||||
'%H:%M:%S',
|
||||
],
|
||||
// https://www.tiny.cloud/docs/tinymce/6/file-image-upload/#images_upload_handler
|
||||
images_upload_handler: (blobInfo: any, progress: any) => new Promise((resolve, reject) => {
|
||||
const img = `data:image/jpeg;base64,${blobInfo.base64()}`
|
||||
resolve(img)
|
||||
}),
|
||||
})
|
||||
|
||||
const myValue = ref(props.modelValue)
|
||||
|
||||
const completeSetting = computed(() => {
|
||||
return Object.assign(defaultSetting.value, props.setting)
|
||||
return Object.assign(defaultSetting.value, props.setting)
|
||||
})
|
||||
|
||||
watch(() => myValue.value, newValue => {
|
||||
emit('update:modelValue', newValue)
|
||||
watch(() => myValue.value, (newValue) => {
|
||||
emit('update:modelValue', newValue)
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, newValue => {
|
||||
myValue.value = newValue
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
myValue.value = newValue
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
tinymce.init({})
|
||||
tinymce.init({})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="editor">
|
||||
<TinymceEditor v-model="myValue" :init="completeSetting" :disabled="disabled" />
|
||||
</div>
|
||||
<div class="editor">
|
||||
<TinymceEditor v-model="myValue" :init="completeSetting" :disabled="disabled" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.tox-tinymce) {
|
||||
border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,107 +1,117 @@
|
||||
<script setup name="FileUpload">
|
||||
<script lang="ts" setup name="FileUpload">
|
||||
import { UploadUserFile } from 'element-plus'
|
||||
import type { UploadProps } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
action: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
headers: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file'
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
files: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
notip: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ext: {
|
||||
type: Array,
|
||||
default: () => ['zip', 'rar']
|
||||
}
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
headers: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file',
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
files: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
notip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
ext: {
|
||||
type: Array,
|
||||
default: () => ['zip', 'rar'],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['on-success'])
|
||||
const emit = defineEmits(['onSuccess'])
|
||||
|
||||
function beforeUpload(file) {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName[fileName.length - 1]
|
||||
const isTypeOk = props.ext.indexOf(fileExt) >= 0
|
||||
const isSizeOk = file.size / 1024 / 1024 < props.size
|
||||
if (!isTypeOk) {
|
||||
ElMessage.error(`上传文件只支持 ${ props.ext.join(' / ') } 格式!`)
|
||||
}
|
||||
if (!isSizeOk) {
|
||||
ElMessage.error(`上传文件大小不能超过 ${props.size}MB!`)
|
||||
}
|
||||
return isTypeOk && isSizeOk
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName.at(-1)
|
||||
const isTypeOk = props.ext.includes(fileExt)
|
||||
const isSizeOk = file.size / 1024 / 1024 < props.size
|
||||
if (!isTypeOk)
|
||||
ElMessage.error(`上传文件只支持 ${props.ext.join(' / ')} 格式!`)
|
||||
|
||||
if (!isSizeOk)
|
||||
ElMessage.error(`上传文件大小不能超过 ${props.size}MB!`)
|
||||
|
||||
return isTypeOk && isSizeOk
|
||||
}
|
||||
function onExceed() {
|
||||
ElMessage.warning('文件上传超过限制')
|
||||
|
||||
const onExceed: UploadProps['onExceed'] = () => {
|
||||
ElMessage.warning('文件上传超过限制')
|
||||
}
|
||||
function onSuccess(res, file, fileList) {
|
||||
emit('on-success', res, file, fileList)
|
||||
|
||||
const onSuccess: UploadProps['onSuccess'] = (res, file, fileList) => {
|
||||
emit('onSuccess', res, file, fileList)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-upload
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-exceed="onExceed"
|
||||
:on-success="onSuccess"
|
||||
:file-list="files"
|
||||
:limit="max"
|
||||
drag
|
||||
>
|
||||
<div class="slot">
|
||||
<el-icon class="el-icon--upload">
|
||||
<svg-icon name="ep:upload-filled" />
|
||||
</el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<el-upload
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-exceed="onExceed"
|
||||
:on-success="onSuccess"
|
||||
:file-list="files as UploadUserFile[]"
|
||||
:limit="max"
|
||||
drag
|
||||
>
|
||||
<div class="slot">
|
||||
<el-icon class="el-icon--upload">
|
||||
<svg-icon name="ep:upload-filled" />
|
||||
</el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div v-if="!notip" class="el-upload__tip">
|
||||
<div style="display: inline-block;">
|
||||
<el-alert :title="`上传文件支持 ${ext.join(' / ')} 格式,单个文件大小不超过 ${size}MB,且文件数量不超过 ${max} 个`" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
<template #tip>
|
||||
<div v-if="!notip" class="el-upload__tip">
|
||||
<div style="display: inline-block;">
|
||||
<el-alert :title="`上传文件支持 ${ ext.join(' / ') } 格式,单个文件大小不超过 ${ size }MB,且文件数量不超过 ${ max } 个`" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-upload.is-drag) {
|
||||
display: inline-block;
|
||||
.el-upload-dragger {
|
||||
padding: 0;
|
||||
}
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
.slot {
|
||||
width: 300px;
|
||||
padding: 40px 0;
|
||||
}
|
||||
display: inline-block;
|
||||
|
||||
.el-upload-dragger {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.slot {
|
||||
width: 300px;
|
||||
padding: 40px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,53 +1,49 @@
|
||||
<script setup name="FixedActionBar">
|
||||
const data = ref({
|
||||
isBottom: true
|
||||
})
|
||||
<script lang="ts" setup name="FixedActionBar">
|
||||
const isBottom = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
onScroll()
|
||||
window.addEventListener('scroll', onScroll)
|
||||
onScroll()
|
||||
window.addEventListener('scroll', onScroll)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
})
|
||||
|
||||
function onScroll() {
|
||||
// 变量scrollTop是滚动条滚动时,滚动条上端距离顶部的距离
|
||||
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop
|
||||
// 变量windowHeight是可视区的高度
|
||||
var windowHeight = document.documentElement.clientHeight || document.body.clientHeight
|
||||
// 变量scrollHeight是滚动条的总高度(当前可滚动的页面的总高度)
|
||||
var scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight
|
||||
// 滚动条到底部
|
||||
if (Math.ceil(scrollTop + windowHeight) >= scrollHeight) {
|
||||
data.value.isBottom = true
|
||||
} else {
|
||||
data.value.isBottom = false
|
||||
}
|
||||
// 变量scrollTop是滚动条滚动时,滚动条上端距离顶部的距离
|
||||
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
|
||||
// 变量windowHeight是可视区的高度
|
||||
const windowHeight = document.documentElement.clientHeight || document.body.clientHeight
|
||||
// 变量scrollHeight是滚动条的总高度(当前可滚动的页面的总高度)
|
||||
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight
|
||||
// 滚动条到底部
|
||||
isBottom.value = Math.ceil(scrollTop + windowHeight) >= scrollHeight
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="actionbar" :class="{'shadow': !data.isBottom}" data-fixed-calc-width>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="actionbar" :class="{ shadow: !isBottom }" data-fixed-calc-width>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actionbar {
|
||||
z-index: 4;
|
||||
bottom: 0;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background-color: var(--g-app-bg);
|
||||
box-shadow: 0 0 1px 0 var(--g-box-shadow-color);
|
||||
transition: all 0.3s, var(--el-transition-box-shadow);
|
||||
&.shadow {
|
||||
box-shadow: 0 -10px 10px -10px var(--g-box-shadow-color);
|
||||
}
|
||||
z-index: 4;
|
||||
bottom: 0;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background-color: var(--g-app-bg);
|
||||
box-shadow: 0 0 1px 0 var(--g-box-shadow-color);
|
||||
transition: all 0.3s, var(--el-transition-box-shadow);
|
||||
|
||||
&.shadow {
|
||||
box-shadow: 0 -10px 10px -10px var(--g-box-shadow-color);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,61 +1,64 @@
|
||||
<script setup name="ImagePreview">
|
||||
<script lang="ts" setup name="ImagePreview">
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
}
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: '',
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const realWidth = computed(() => {
|
||||
return typeof props.width == 'string' ? props.width : `${props.width}px`
|
||||
return typeof props.width === 'string' ? props.width : `${props.width}px`
|
||||
})
|
||||
|
||||
const realHeight = computed(() => {
|
||||
return typeof props.height == 'string' ? props.height : `${props.height}px`
|
||||
return typeof props.height === 'string' ? props.height : `${props.height}px`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-image :src="src" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="[src]">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<svg-icon name="image-load-fail" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<el-image :src="src" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="[src]">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<svg-icon name="image-load-fail" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-image {
|
||||
border-radius: 5px;
|
||||
background-color: var(--el-fill-color);
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
transition: background-color 0.3s, var(--el-transition-box-shadow);
|
||||
:deep(.el-image__inner) {
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
:deep(.image-slot) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
font-size: 30px;
|
||||
border-radius: 5px;
|
||||
background-color: var(--el-fill-color);
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
transition: background-color 0.3s, var(--el-transition-box-shadow);
|
||||
|
||||
:deep(.el-image__inner) {
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.image-slot) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,242 +1,263 @@
|
||||
<script setup name="ImageUpload">
|
||||
<script lang="ts" setup name="ImageUpload">
|
||||
import type { UploadProps } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
action: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
headers: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file'
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 150
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 150
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
notip: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ext: {
|
||||
type: Array,
|
||||
default: () => ['jpg', 'png', 'gif', 'bmp']
|
||||
}
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
headers: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file',
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 150,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 150,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
notip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
ext: {
|
||||
type: Array,
|
||||
default: () => ['jpg', 'png', 'gif', 'bmp'],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:url', 'on-success'])
|
||||
const emit = defineEmits(['update:url', 'onSuccess'])
|
||||
|
||||
const uploadData = ref({
|
||||
imageViewerVisible: false,
|
||||
progress: {
|
||||
preview: '',
|
||||
percent: 0
|
||||
}
|
||||
imageViewerVisible: false,
|
||||
progress: {
|
||||
preview: '',
|
||||
percent: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// 预览
|
||||
function preview() {
|
||||
uploadData.value.imageViewerVisible = true
|
||||
uploadData.value.imageViewerVisible = true
|
||||
}
|
||||
// 关闭预览
|
||||
function previewClose() {
|
||||
uploadData.value.imageViewerVisible = false
|
||||
uploadData.value.imageViewerVisible = false
|
||||
}
|
||||
// 移除
|
||||
function remove() {
|
||||
emit('update:url', '')
|
||||
emit('update:url', '')
|
||||
}
|
||||
function beforeUpload(file) {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName[fileName.length - 1]
|
||||
const isTypeOk = props.ext.indexOf(fileExt) >= 0
|
||||
const isSizeOk = file.size / 1024 / 1024 < props.size
|
||||
if (!isTypeOk) {
|
||||
ElMessage.error(`上传图片只支持 ${ props.ext.join(' / ') } 格式!`)
|
||||
}
|
||||
if (!isSizeOk) {
|
||||
ElMessage.error(`上传图片大小不能超过 ${props.size}MB!`)
|
||||
}
|
||||
if (isTypeOk && isSizeOk) {
|
||||
uploadData.value.progress.preview = URL.createObjectURL(file)
|
||||
}
|
||||
return isTypeOk && isSizeOk
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName.at(-1)
|
||||
const isTypeOk = props.ext.includes(fileExt)
|
||||
const isSizeOk = file.size / 1024 / 1024 < props.size
|
||||
if (!isTypeOk)
|
||||
ElMessage.error(`上传图片只支持 ${props.ext.join(' / ')} 格式!`)
|
||||
|
||||
if (!isSizeOk)
|
||||
ElMessage.error(`上传图片大小不能超过 ${props.size}MB!`)
|
||||
|
||||
if (isTypeOk && isSizeOk)
|
||||
uploadData.value.progress.preview = URL.createObjectURL(file)
|
||||
|
||||
return isTypeOk && isSizeOk
|
||||
}
|
||||
function onProgress(file) {
|
||||
uploadData.value.progress.percent = ~~file.percent
|
||||
const onProgress: UploadProps['onProgress'] = (file) => {
|
||||
uploadData.value.progress.percent = ~~file.percent
|
||||
}
|
||||
function onSuccess(res) {
|
||||
uploadData.value.progress.preview = ''
|
||||
uploadData.value.progress.percent = 0
|
||||
emit('on-success', res)
|
||||
const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
uploadData.value.progress.preview = ''
|
||||
uploadData.value.progress.percent = 0
|
||||
emit('onSuccess', res)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-progress="onProgress"
|
||||
:on-success="onSuccess"
|
||||
drag
|
||||
class="image-upload"
|
||||
>
|
||||
<el-image v-if="url === ''" :src="url === '' ? placeholder : url" :style="`width:${width}px;height:${height}px;`" fit="fill">
|
||||
<template #error>
|
||||
<div class="image-slot" :style="`width:${width}px;height:${height}px;`">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:plus" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="image">
|
||||
<el-image :src="url" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<div class="mask">
|
||||
<div class="actions">
|
||||
<span title="预览" @click.stop="preview">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:zoom-in" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span title="移除" @click.stop="remove">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:delete" />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="url === '' && uploadData.progress.percent" class="progress" :style="`width:${width}px;height:${height}px;`">
|
||||
<el-image :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<el-progress type="circle" :width="Math.min(width, height) * 0.8" :percentage="uploadData.progress.percent" />
|
||||
</div>
|
||||
</el-upload>
|
||||
<div v-if="!notip" class="el-upload__tip">
|
||||
<div style="display: inline-block;">
|
||||
<el-alert :title="`上传图片支持 ${ ext.join(' / ') } 格式,且图片大小不超过 ${ size }MB,建议图片尺寸为 ${width}*${height}`" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-progress="onProgress"
|
||||
:on-success="onSuccess"
|
||||
drag
|
||||
class="image-upload"
|
||||
>
|
||||
<el-image v-if="url === ''" :src="url === '' ? placeholder : url" :style="`width:${width}px;height:${height}px;`" fit="fill">
|
||||
<template #error>
|
||||
<div class="image-slot" :style="`width:${width}px;height:${height}px;`">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:plus" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="image">
|
||||
<el-image :src="url" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<div class="mask">
|
||||
<div class="actions">
|
||||
<span title="预览" @click.stop="preview">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:zoom-in" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span title="移除" @click.stop="remove">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:delete" />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-image-viewer v-if="uploadData.imageViewerVisible" :url-list="[url]" @close="previewClose" />
|
||||
</div>
|
||||
<div v-show="url === '' && uploadData.progress.percent" class="progress" :style="`width:${width}px;height:${height}px;`">
|
||||
<el-image :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<el-progress type="circle" :width="Math.min(width, height) * 0.8" :percentage="uploadData.progress.percent" />
|
||||
</div>
|
||||
</el-upload>
|
||||
<div v-if="!notip" class="el-upload__tip">
|
||||
<div style="display: inline-block;">
|
||||
<el-alert :title="`上传图片支持 ${ext.join(' / ')} 格式,且图片大小不超过 ${size}MB,建议图片尺寸为 ${width}*${height}`" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
<el-image-viewer v-if="uploadData.imageViewerVisible" :url-list="[url]" @close="previewClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload-container {
|
||||
line-height: initial;
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
.el-image {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image {
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
.mask {
|
||||
opacity: 0;
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
.mask {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
transition: opacity 0.3s;
|
||||
|
||||
.actions {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@include position-center(xy);
|
||||
|
||||
span {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: var(--el-color-white);
|
||||
transition: color 0.1s, transform 0.1s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .mask {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.image-upload {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
.el-upload-dragger {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--el-text-color-placeholder);
|
||||
background-color: transparent;
|
||||
|
||||
i {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
transition: opacity 0.3s;
|
||||
.actions {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include position-center(xy);
|
||||
span {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: var(--el-color-white);
|
||||
transition: color 0.1s, transform 0.1s;
|
||||
&:hover {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover .mask {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.image-upload {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
:deep(.el-upload) {
|
||||
.el-upload-dragger {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--el-text-color-placeholder);
|
||||
background-color: transparent;
|
||||
i {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
}
|
||||
.el-progress {
|
||||
z-index: 1;
|
||||
@include position-center(xy);
|
||||
.el-progress__text {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-progress {
|
||||
z-index: 1;
|
||||
|
||||
@include position-center(xy);
|
||||
|
||||
.el-progress__text {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,275 +1,298 @@
|
||||
<script setup name="ImagesUpload">
|
||||
<script lang="ts" setup name="ImagesUpload">
|
||||
import type { UploadProps } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
action: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
headers: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file'
|
||||
},
|
||||
url: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 150
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 150
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
notip: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ext: {
|
||||
type: Array,
|
||||
default: () => ['jpg', 'png', 'gif', 'bmp']
|
||||
}
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
headers: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file',
|
||||
},
|
||||
url: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 150,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 150,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
notip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
ext: {
|
||||
type: Array,
|
||||
default: () => ['jpg', 'png', 'gif', 'bmp'],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:url', 'on-success'])
|
||||
const emit = defineEmits(['update:url', 'onSuccess'])
|
||||
|
||||
const uploadData = ref({
|
||||
dialogImageIndex: 0,
|
||||
imageViewerVisible: false,
|
||||
progress: {
|
||||
preview: '',
|
||||
percent: 0
|
||||
}
|
||||
dialogImageIndex: 0,
|
||||
imageViewerVisible: false,
|
||||
progress: {
|
||||
preview: '',
|
||||
percent: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// 预览
|
||||
function preview(index) {
|
||||
uploadData.value.dialogImageIndex = index
|
||||
uploadData.value.imageViewerVisible = true
|
||||
function preview(index: number) {
|
||||
uploadData.value.dialogImageIndex = index
|
||||
uploadData.value.imageViewerVisible = true
|
||||
}
|
||||
// 关闭预览
|
||||
function previewClose() {
|
||||
uploadData.value.imageViewerVisible = false
|
||||
uploadData.value.imageViewerVisible = false
|
||||
}
|
||||
// 移除
|
||||
function remove(index) {
|
||||
let url = props.url
|
||||
url.splice(index, 1)
|
||||
emit('update:url', url)
|
||||
function remove(index: number) {
|
||||
const url = props.url
|
||||
url.splice(index, 1)
|
||||
emit('update:url', url)
|
||||
}
|
||||
// 移动
|
||||
function move(index, type) {
|
||||
let url = props.url
|
||||
if (type == 'left' && index != 0) {
|
||||
url[index] = url.splice(index - 1, 1, url[index])[0]
|
||||
}
|
||||
if (type == 'right' && index != url.length - 1) {
|
||||
url[index] = url.splice(index + 1, 1, url[index])[0]
|
||||
}
|
||||
emit('update:url', url)
|
||||
function move(index: number, type: 'left' | 'right') {
|
||||
const url = props.url
|
||||
if (type === 'left' && index !== 0)
|
||||
url[index] = url.splice(index - 1, 1, url[index])[0]
|
||||
|
||||
if (type === 'right' && index !== url.length - 1)
|
||||
url[index] = url.splice(index + 1, 1, url[index])[0]
|
||||
|
||||
emit('update:url', url)
|
||||
}
|
||||
function beforeUpload(file) {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName[fileName.length - 1]
|
||||
const isTypeOk = props.ext.indexOf(fileExt) >= 0
|
||||
const isSizeOk = file.size / 1024 / 1024 < props.size
|
||||
if (!isTypeOk) {
|
||||
ElMessage.error(`上传图片只支持 ${ props.ext.join(' / ') } 格式!`)
|
||||
}
|
||||
if (!isSizeOk) {
|
||||
ElMessage.error(`上传图片大小不能超过 ${props.size}MB!`)
|
||||
}
|
||||
if (isTypeOk && isSizeOk) {
|
||||
uploadData.value.progress.preview = URL.createObjectURL(file)
|
||||
}
|
||||
return isTypeOk && isSizeOk
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName.at(-1)
|
||||
const isTypeOk = props.ext.includes(fileExt)
|
||||
const isSizeOk = file.size / 1024 / 1024 < props.size
|
||||
if (!isTypeOk)
|
||||
ElMessage.error(`上传图片只支持 ${props.ext.join(' / ')} 格式!`)
|
||||
|
||||
if (!isSizeOk)
|
||||
ElMessage.error(`上传图片大小不能超过 ${props.size}MB!`)
|
||||
|
||||
if (isTypeOk && isSizeOk)
|
||||
uploadData.value.progress.preview = URL.createObjectURL(file)
|
||||
|
||||
return isTypeOk && isSizeOk
|
||||
}
|
||||
function onProgress(file) {
|
||||
uploadData.value.progress.percent = ~~file.percent
|
||||
const onProgress: UploadProps['onProgress'] = (file) => {
|
||||
uploadData.value.progress.percent = ~~file.percent
|
||||
}
|
||||
function onSuccess(res) {
|
||||
uploadData.value.progress.preview = ''
|
||||
uploadData.value.progress.percent = 0
|
||||
emit('on-success', res)
|
||||
const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
uploadData.value.progress.preview = ''
|
||||
uploadData.value.progress.percent = 0
|
||||
emit('onSuccess', res)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<div v-for="(item, index) in url" :key="index" class="images">
|
||||
<el-image v-if="index < max" :src="item" :style="`width:${width}px;height:${height}px;`" fit="cover" />
|
||||
<div class="mask">
|
||||
<div class="actions">
|
||||
<span title="预览" @click="preview(index)">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:zoom-in" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span title="移除" @click="remove(index)">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:delete" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-show="url.length > 1" title="左移" :class="{'disabled': index == 0}" @click="move(index, 'left')">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:back" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-show="url.length > 1" title="右移" :class="{'disabled': index == url.length - 1}" @click="move(index, 'right')">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:right" />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload-container">
|
||||
<div v-for="(item, index) in (url as string[])" :key="index" class="images">
|
||||
<el-image v-if="index < max" :src="item" :style="`width:${width}px;height:${height}px;`" fit="cover" />
|
||||
<div class="mask">
|
||||
<div class="actions">
|
||||
<span title="预览" @click="preview(index)">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:zoom-in" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span title="移除" @click="remove(index)">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:delete" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-show="url.length > 1" title="左移" :class="{ disabled: index === 0 }" @click="move(index, 'left')">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:back" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-show="url.length > 1" title="右移" :class="{ disabled: index === url.length - 1 }" @click="move(index, 'right')">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:right" />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
<el-upload
|
||||
v-show="url.length < max"
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-progress="onProgress"
|
||||
:on-success="onSuccess"
|
||||
drag
|
||||
class="images-upload"
|
||||
>
|
||||
<div class="image-slot" :style="`width:${width}px;height:${height}px;`">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:plus" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div v-show="uploadData.progress.percent" class="progress" :style="`width:${width}px;height:${height}px;`">
|
||||
<el-image :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<el-progress type="circle" :width="Math.min(width, height) * 0.8" :percentage="uploadData.progress.percent" />
|
||||
</div>
|
||||
</el-upload>
|
||||
<div v-if="!notip" class="el-upload__tip">
|
||||
<div style="display: inline-block;">
|
||||
<el-alert :title="`上传图片支持 ${ ext.join(' / ') } 格式,单张图片大小不超过 ${ size }MB,建议图片尺寸为 ${width}*${height},且图片数量不超过 ${ max } 张`" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
<el-image-viewer v-if="uploadData.imageViewerVisible" :url-list="url" :initial-index="uploadData.dialogImageIndex" @close="previewClose" />
|
||||
</div>
|
||||
</div>
|
||||
<el-upload
|
||||
v-show="url.length < max"
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-progress="onProgress"
|
||||
:on-success="onSuccess"
|
||||
drag
|
||||
class="images-upload"
|
||||
>
|
||||
<div class="image-slot" :style="`width:${width}px;height:${height}px;`">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:plus" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div v-show="uploadData.progress.percent" class="progress" :style="`width:${width}px;height:${height}px;`">
|
||||
<el-image :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<el-progress type="circle" :width="Math.min(width, height) * 0.8" :percentage="uploadData.progress.percent" />
|
||||
</div>
|
||||
</el-upload>
|
||||
<div v-if="!notip" class="el-upload__tip">
|
||||
<div style="display: inline-block;">
|
||||
<el-alert :title="`上传图片支持 ${ext.join(' / ')} 格式,单张图片大小不超过 ${size}MB,建议图片尺寸为 ${width}*${height},且图片数量不超过 ${max} 张`" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
<el-image-viewer v-if="uploadData.imageViewerVisible" :url-list="url as string[]" :initial-index="uploadData.dialogImageIndex" @close="previewClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload-container {
|
||||
line-height: initial;
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
.el-image {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.images {
|
||||
position: relative;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
.mask {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
transition: opacity 0.3s;
|
||||
|
||||
.actions {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@include position-center(xy);
|
||||
|
||||
span {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: var(--el-color-white);
|
||||
transition: color 0.1s, transform 0.1s;
|
||||
|
||||
&.disabled {
|
||||
color: var(--el-text-color-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .mask {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.images-upload {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
.el-upload-dragger {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
.mask {
|
||||
opacity: 0;
|
||||
padding: 0;
|
||||
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--el-text-color-placeholder);
|
||||
background-color: transparent;
|
||||
|
||||
i {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
transition: opacity 0.3s;
|
||||
.actions {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include position-center(xy);
|
||||
span {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: var(--el-color-white);
|
||||
transition: color 0.1s, transform 0.1s;
|
||||
&.disabled {
|
||||
color: var(--el-text-color-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&:hover:not(.disabled) {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover .mask {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.images-upload {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
:deep(.el-upload) {
|
||||
.el-upload-dragger {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--el-text-color-placeholder);
|
||||
background-color: transparent;
|
||||
i {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
}
|
||||
.el-progress {
|
||||
z-index: 1;
|
||||
@include position-center(xy);
|
||||
.el-progress__text {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-progress {
|
||||
z-index: 1;
|
||||
|
||||
@include position-center(xy);
|
||||
|
||||
.el-progress__text {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,59 +1,68 @@
|
||||
<script setup name="PageHeader">
|
||||
<script lang="ts" setup name="PageHeader">
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="header">
|
||||
<div class="main">
|
||||
<div class="title">{{ title }}</div>
|
||||
<div class="content">
|
||||
<slot name="content">{{ content }}</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="header">
|
||||
<div class="main">
|
||||
<div class="title">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot name="content">
|
||||
{{ content }}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px 20px;
|
||||
background-color: var(--g-app-bg);
|
||||
transition: background-color 0.3s;
|
||||
.main {
|
||||
flex: 1;
|
||||
margin-right: 20px;
|
||||
.title {
|
||||
font-size: 22px;
|
||||
color: var(--el-text-color-primary);
|
||||
transition: var(--el-transition-color);
|
||||
}
|
||||
.content {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: var(--el-transition-color);
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px 20px;
|
||||
background-color: var(--g-app-bg);
|
||||
transition: background-color 0.3s;
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
margin-right: 20px;
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
color: var(--el-text-color-primary);
|
||||
transition: var(--el-transition-color);
|
||||
}
|
||||
.sub {
|
||||
flex: none;
|
||||
|
||||
.content {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: var(--el-transition-color);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,84 +1,89 @@
|
||||
<script setup name="PageMain">
|
||||
<script lang="ts" setup name="PageMain">
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
collaspe: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
collaspe: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const titleSlot = !!useSlots().title
|
||||
|
||||
const collaspeData = ref(props.collaspe)
|
||||
function unCollaspe() {
|
||||
collaspeData.value = false
|
||||
collaspeData.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'page-main': true,
|
||||
'is-collaspe': collaspeData
|
||||
}" :style="{
|
||||
'height': collaspeData ? height : ''
|
||||
}"
|
||||
>
|
||||
<div v-if="titleSlot || title" class="title-container">
|
||||
<slot v-if="titleSlot" name="title" />
|
||||
<template v-else>{{ title }}</template>
|
||||
</div>
|
||||
<slot />
|
||||
<div v-if="collaspeData" class="collaspe" title="展开" @click="unCollaspe">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:arrow-down" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div
|
||||
class="page-main" :class="{
|
||||
'is-collaspe': collaspeData,
|
||||
}" :style="{
|
||||
height: collaspeData ? height : '',
|
||||
}"
|
||||
>
|
||||
<div v-if="titleSlot || title" class="title-container">
|
||||
<slot v-if="titleSlot" name="title" />
|
||||
<template v-else>
|
||||
{{ title }}
|
||||
</template>
|
||||
</div>
|
||||
<slot />
|
||||
<div v-if="collaspeData" class="collaspe" title="展开" @click="unCollaspe">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:arrow-down" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-main {
|
||||
position: relative;
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
background-color: var(--g-app-bg);
|
||||
transition: background-color 0.3s;
|
||||
&.is-collaspe {
|
||||
overflow: hidden;
|
||||
.collaspe {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 40px 0 10px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
color: var(--el-text-color-primary);
|
||||
text-shadow: 0 0 1px var(--el-text-color-primary);
|
||||
background: linear-gradient(to bottom, transparent, var(--g-app-bg));
|
||||
transition: background 0.3s, var(--el-transition-color);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
.title-container {
|
||||
width: calc(100% + 40px);
|
||||
padding: 14px 20px;
|
||||
margin-left: -20px;
|
||||
margin-top: -20px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
transition: var(--el-transition-border);
|
||||
position: relative;
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
background-color: var(--g-app-bg);
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&.is-collaspe {
|
||||
overflow: hidden;
|
||||
|
||||
.collaspe {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 40px 0 10px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
color: var(--el-text-color-primary);
|
||||
text-shadow: 0 0 1px var(--el-text-color-primary);
|
||||
background: linear-gradient(to bottom, transparent, var(--g-app-bg));
|
||||
transition: background 0.3s, var(--el-transition-color);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title-container {
|
||||
width: calc(100% + 40px);
|
||||
padding: 14px 20px;
|
||||
margin-left: -20px;
|
||||
margin-top: -20px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
transition: var(--el-transition-border);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,131 +1,137 @@
|
||||
<script setup name="PcasCascader">
|
||||
<script lang="ts" setup name="PcasCascader">
|
||||
// 行政区划数据来源于 https://github.com/modood/Administrative-divisions-of-China
|
||||
import pcasRaw from './pcas-code.json'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'pca'
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: 'code'
|
||||
}
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String as () => 'pc' | 'pca' | 'pcas',
|
||||
default: 'pca',
|
||||
},
|
||||
format: {
|
||||
type: String as () => 'code' | 'name' | 'both',
|
||||
default: 'code',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
interface pcasItem {
|
||||
code: string
|
||||
name: string
|
||||
children?: pcasItem[]
|
||||
}
|
||||
|
||||
const pcasData = computed(() => {
|
||||
const data = []
|
||||
// 省份
|
||||
pcasRaw.forEach(p => {
|
||||
const tempP = {
|
||||
code: p.code,
|
||||
name: p.name
|
||||
}
|
||||
const tempChildrenC = []
|
||||
// 城市
|
||||
p.children.forEach(c => {
|
||||
const tempC = {
|
||||
code: c.code,
|
||||
name: c.name
|
||||
}
|
||||
if (['pca', 'pcas'].includes(props.type)) {
|
||||
const tempChildrenA = []
|
||||
// 区县
|
||||
c.children.forEach(a => {
|
||||
const tempA = {
|
||||
code: a.code,
|
||||
name: a.name
|
||||
}
|
||||
if (props.type === 'pcas') {
|
||||
const tempChildrenS = []
|
||||
// 街道
|
||||
a.children.forEach(s => {
|
||||
const tempS = {
|
||||
code: s.code,
|
||||
name: s.name
|
||||
}
|
||||
tempChildrenS.push(tempS)
|
||||
})
|
||||
tempA.children = tempChildrenS
|
||||
}
|
||||
tempChildrenA.push(tempA)
|
||||
})
|
||||
tempC.children = tempChildrenA
|
||||
}
|
||||
tempChildrenC.push(tempC)
|
||||
const data: pcasItem[] = []
|
||||
// 省份
|
||||
pcasRaw.forEach((p) => {
|
||||
const tempP: pcasItem = {
|
||||
code: p.code,
|
||||
name: p.name,
|
||||
}
|
||||
const tempChildrenC: pcasItem[] = []
|
||||
// 城市
|
||||
p.children.forEach((c) => {
|
||||
const tempC: pcasItem = {
|
||||
code: c.code,
|
||||
name: c.name,
|
||||
}
|
||||
if (['pca', 'pcas'].includes(props.type)) {
|
||||
const tempChildrenA: pcasItem[] = []
|
||||
// 区县
|
||||
c.children.forEach((a) => {
|
||||
const tempA: pcasItem = {
|
||||
code: a.code,
|
||||
name: a.name,
|
||||
}
|
||||
if (props.type === 'pcas') {
|
||||
const tempChildrenS: pcasItem[] = []
|
||||
// 街道
|
||||
a.children.forEach((s) => {
|
||||
const tempS: pcasItem = {
|
||||
code: s.code,
|
||||
name: s.name,
|
||||
}
|
||||
tempChildrenS.push(tempS)
|
||||
})
|
||||
tempA.children = tempChildrenS
|
||||
}
|
||||
tempChildrenA.push(tempA)
|
||||
})
|
||||
tempP.children = tempChildrenC
|
||||
data.push(tempP)
|
||||
tempC.children = tempChildrenA
|
||||
}
|
||||
tempChildrenC.push(tempC)
|
||||
})
|
||||
return data
|
||||
tempP.children = tempChildrenC
|
||||
data.push(tempP)
|
||||
})
|
||||
return data
|
||||
})
|
||||
|
||||
const myValue = computed({
|
||||
// 将入参数据转成 code 码
|
||||
get: () => {
|
||||
return anyToCode(props.modelValue)
|
||||
},
|
||||
// 将 code 码转成出参数据
|
||||
set: value => {
|
||||
emit('update:modelValue', value ? codeToAny(value) : [])
|
||||
}
|
||||
// 将入参数据转成 code 码
|
||||
get: () => {
|
||||
return anyToCode(props.modelValue)
|
||||
},
|
||||
// 将 code 码转成出参数据
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value ? codeToAny(value) : [])
|
||||
},
|
||||
})
|
||||
|
||||
function anyToCode(value, dictionarie = pcasData.value) {
|
||||
const input = []
|
||||
if (value.length > 0) {
|
||||
const findItem = dictionarie.find(item => {
|
||||
if (props.format === 'code') {
|
||||
return item.code === value[0]
|
||||
} else if (props.format === 'name') {
|
||||
return item.name === value[0]
|
||||
} else {
|
||||
return item.name === value[0].name && item.code === value[0].code
|
||||
}
|
||||
})
|
||||
input.push(findItem.code)
|
||||
if (findItem.children) {
|
||||
input.push(...anyToCode(value.slice(1 - value.length), findItem.children))
|
||||
}
|
||||
}
|
||||
return input
|
||||
function anyToCode(value: any[], dictionarie: any[] = pcasData.value) {
|
||||
const input: string[] = []
|
||||
if (value.length > 0) {
|
||||
const findItem = dictionarie.find((item) => {
|
||||
if (props.format === 'code')
|
||||
return item.code === value[0]
|
||||
|
||||
else if (props.format === 'name')
|
||||
return item.name === value[0]
|
||||
|
||||
else
|
||||
return item.name === value[0].name && item.code === value[0].code
|
||||
})
|
||||
input.push(findItem.code)
|
||||
if (findItem.children)
|
||||
input.push(...anyToCode(value.slice(1 - value.length), findItem.children))
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
function codeToAny(codes, dictionarie = pcasData.value) {
|
||||
const output = []
|
||||
const findItem = dictionarie.find(item => item.code === codes[0])
|
||||
if (findItem) {
|
||||
switch (props.format) {
|
||||
case 'code':
|
||||
output.push(findItem.code)
|
||||
break
|
||||
case 'name':
|
||||
output.push(findItem.name)
|
||||
break
|
||||
case 'both':
|
||||
output.push({
|
||||
code: findItem.code,
|
||||
name: findItem.name
|
||||
})
|
||||
}
|
||||
const newCodes = codes.slice(1 - codes.length)
|
||||
if (newCodes.length > 0 && findItem.children) {
|
||||
output.push(...codeToAny(newCodes, findItem.children))
|
||||
}
|
||||
function codeToAny(codes: string[], dictionarie: any[] = pcasData.value): any {
|
||||
const output = []
|
||||
const findItem = dictionarie.find(item => item.code === codes[0])
|
||||
if (findItem) {
|
||||
switch (props.format) {
|
||||
case 'code':
|
||||
output.push(findItem.code)
|
||||
break
|
||||
case 'name':
|
||||
output.push(findItem.name)
|
||||
break
|
||||
case 'both':
|
||||
output.push({
|
||||
code: findItem.code,
|
||||
name: findItem.name,
|
||||
})
|
||||
}
|
||||
return output
|
||||
const newCodes = codes.slice(1 - codes.length)
|
||||
if (newCodes.length > 0 && findItem.children)
|
||||
output.push(...codeToAny(newCodes, findItem.children))
|
||||
}
|
||||
return output
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-cascader v-model="myValue" :options="pcasData" :props="{ value: 'code', label: 'name' }" :disabled="disabled" clearable filterable />
|
||||
<el-cascader v-model="myValue" :options="pcasData as any[]" :props="{ value: 'code', label: 'name' }" :disabled="disabled" clearable filterable />
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup name="SearchBar">
|
||||
<script lang="ts" setup name="SearchBar">
|
||||
const props = defineProps({
|
||||
showMore: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
unfold: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
showMore: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
unfold: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['toggle'])
|
||||
@@ -15,57 +15,63 @@ const emit = defineEmits(['toggle'])
|
||||
const isUnfold = ref(!props.unfold)
|
||||
|
||||
watch(() => props.unfold, () => toggle(), {
|
||||
immediate: true
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
function toggle() {
|
||||
isUnfold.value = !isUnfold.value
|
||||
emit('toggle', isUnfold.value)
|
||||
isUnfold.value = !isUnfold.value
|
||||
emit('toggle', isUnfold.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-container">
|
||||
<slot />
|
||||
<div v-if="showMore" class="more">
|
||||
<el-button text size="small" @click="toggle">
|
||||
<template #icon>
|
||||
<el-icon>
|
||||
<svg-icon :name="isUnfold ? 'ep:caret-top' : 'ep:caret-bottom'" />
|
||||
</el-icon>
|
||||
</template>
|
||||
{{ isUnfold ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="search-container">
|
||||
<slot />
|
||||
<div v-if="showMore" class="more">
|
||||
<el-button text size="small" @click="toggle">
|
||||
<template #icon>
|
||||
<el-icon>
|
||||
<svg-icon :name="isUnfold ? 'ep:caret-top' : 'ep:caret-bottom'" />
|
||||
</el-icon>
|
||||
</template>
|
||||
{{ isUnfold ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-container {
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-form) {
|
||||
margin-bottom: -10px;
|
||||
|
||||
.el-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-date-editor {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.more {
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background-color: var(--el-fill-color);
|
||||
transition: background-color 0.3s;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
:deep(.el-form) {
|
||||
margin-bottom: -10px;
|
||||
.el-select {
|
||||
width: 100%;
|
||||
}
|
||||
.el-date-editor {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.more {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
text-align: center;
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,53 +1,47 @@
|
||||
<script setup name="SvgIcon">
|
||||
<script lang="ts" setup name="SvgIcon">
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
flip: {
|
||||
type: String,
|
||||
validator(value) {
|
||||
return ['', 'horizontal', 'vertical', 'both'].includes(value)
|
||||
},
|
||||
default: ''
|
||||
},
|
||||
rotate: {
|
||||
type: Number,
|
||||
validator(value) {
|
||||
return value >= 0 && value <= 360
|
||||
},
|
||||
default: 0
|
||||
}
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
flip: {
|
||||
type: String as () => 'horizontal' | 'vertical' | 'both' | '',
|
||||
default: '',
|
||||
},
|
||||
rotate: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const transformStyle = computed(() => {
|
||||
let style = []
|
||||
if (props.flip != '') {
|
||||
switch (props.flip) {
|
||||
case 'horizontal':
|
||||
style.push('rotateY(180deg)')
|
||||
break
|
||||
case 'vertical':
|
||||
style.push('rotateX(180deg)')
|
||||
break
|
||||
case 'both':
|
||||
style.push('rotateX(180deg)')
|
||||
style.push('rotateY(180deg)')
|
||||
break
|
||||
}
|
||||
const style = []
|
||||
if (props.flip !== '') {
|
||||
switch (props.flip) {
|
||||
case 'horizontal':
|
||||
style.push('rotateY(180deg)')
|
||||
break
|
||||
case 'vertical':
|
||||
style.push('rotateX(180deg)')
|
||||
break
|
||||
case 'both':
|
||||
style.push('rotateX(180deg)')
|
||||
style.push('rotateY(180deg)')
|
||||
break
|
||||
}
|
||||
if (props.rotate != 0) {
|
||||
style.push(`rotate(${props.rotate}deg)`)
|
||||
}
|
||||
return `transform: ${style.join(' ')};`
|
||||
}
|
||||
if (props.rotate !== 0)
|
||||
style.push(`rotate(${props.rotate}deg)`)
|
||||
|
||||
return `transform: ${style.join(' ')};`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Icon v-if="name.indexOf('ep:') === 0" :icon="name" :style="transformStyle" />
|
||||
<svg v-else :style="transformStyle" aria-hidden="true">
|
||||
<use :xlink:href="`#icon-${name}`" />
|
||||
</svg>
|
||||
<Icon v-if="name.indexOf('ep:') === 0" :icon="name" :style="transformStyle" />
|
||||
<svg v-else :style="transformStyle" aria-hidden="true">
|
||||
<use :xlink:href="`#icon-${name}`" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
const isShow = ref(false)
|
||||
|
||||
const { pkg, lastBuildTime } = __SYSTEM_INFO__
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on('global-system-info-toggle', () => {
|
||||
isShow.value = !isShow.value
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer v-model="isShow" title="系统信息" direction="rtl" :size="360">
|
||||
<el-descriptions direction="vertical" :column="1" border>
|
||||
<el-descriptions-item label="版本号" align="center">
|
||||
{{ pkg.version }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最后编译时间" align="center">
|
||||
{{ lastBuildTime }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions title="生产环境依赖" :column="1" size="small" border>
|
||||
<el-descriptions-item v-for="(val, key) in (pkg.dependencies as object)" :key="key" :label="key">
|
||||
{{ val }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions title="开发环境依赖" :column="1" size="small" border>
|
||||
<el-descriptions-item v-for="(val, key) in (pkg.devDependencies as object)" :key="key" :label="key">
|
||||
{{ val }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: initial;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
transition: var(--el-transition-border);
|
||||
}
|
||||
|
||||
:deep(.el-descriptions) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,65 +1,68 @@
|
||||
<script setup name="Trend">
|
||||
<script lang="ts" setup name="Trend">
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
validator: val => ['up', 'down'].includes(val),
|
||||
default: 'up'
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
suffix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
reverse: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String as () => 'up' | 'down',
|
||||
default: 'up',
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
suffix: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
reverse: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const isUp = computed(() => {
|
||||
let isUp = props.type === 'up'
|
||||
if (props.reverse) {
|
||||
isUp = !isUp
|
||||
}
|
||||
return isUp
|
||||
let isUp = props.type === 'up'
|
||||
if (props.reverse)
|
||||
isUp = !isUp
|
||||
|
||||
return isUp
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="`trend ${isUp ? 'up' : 'down'}`">
|
||||
<span v-if="prefix" class="prefix">{{ prefix }}</span>
|
||||
<span class="text">{{ value }}</span>
|
||||
<span v-if="suffix" class="suffix">{{ suffix }}</span>
|
||||
<el-icon>
|
||||
<svg-icon name="ep:caret-top" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div :class="`trend ${isUp ? 'up' : 'down'}`">
|
||||
<span v-if="prefix" class="prefix">{{ prefix }}</span>
|
||||
<span class="text">{{ value }}</span>
|
||||
<span v-if="suffix" class="suffix">{{ suffix }}</span>
|
||||
<el-icon>
|
||||
<svg-icon name="ep:caret-top" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: var(--el-transition-color);
|
||||
&.up {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
&.down {
|
||||
color: var(--el-color-danger);
|
||||
.el-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: var(--el-transition-color);
|
||||
|
||||
&.up {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: var(--el-color-danger);
|
||||
|
||||
.el-icon {
|
||||
margin-left: 5px;
|
||||
transition: transform 0.3s;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-left: 5px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
import type { StyleValue } from "vue"
|
||||
import type { RouteRecordRaw, RouteMeta, NavigationGuard } from 'vue-router'
|
||||
|
||||
type RecursiveRequired<T> = {
|
||||
[P in keyof T]-?: RecursiveRequired<T[P]>
|
||||
}
|
||||
|
||||
declare namespace Settings {
|
||||
interface app {
|
||||
/**
|
||||
* 颜色方案
|
||||
* @默认值 `''` 跟随系统
|
||||
* @可选值 `'light'` 明亮模式
|
||||
* @可选值 `'dark'` 暗黑模式
|
||||
*/
|
||||
colorScheme?: '' | 'light' | 'dark'
|
||||
/**
|
||||
* Element Plus 组件默认尺寸
|
||||
* @默认值 `'default'` 默认
|
||||
* @可选值 `'large'` 大号
|
||||
* @可选值 `'small'` 小号
|
||||
*/
|
||||
elementSize?: 'default' | 'large' | 'small'
|
||||
/**
|
||||
* 是否将 iconify 图标集中的 Element Plus 图标下载到本地,以便在离线环境下使用
|
||||
* @默认值 `false`
|
||||
*/
|
||||
iconifyOfflineUse?: boolean
|
||||
/**
|
||||
* 是否开启权限功能
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enablePermission?: boolean
|
||||
/**
|
||||
* 是否开启载入进度条
|
||||
* @默认值 `true`
|
||||
*/
|
||||
enableProgress?: boolean
|
||||
/**
|
||||
* 是否开启动态标题
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableDynamicTitle?: boolean
|
||||
/**
|
||||
* 路由数据来源
|
||||
* @默认值 `'frontend'` 前端
|
||||
* @可选值 `'backend'` 后端
|
||||
* @可选值 `'filesystem'` 文件系统
|
||||
*/
|
||||
routeBaseOn?: 'frontend' | 'backend' | 'filesystem'
|
||||
}
|
||||
interface dashboard {
|
||||
/**
|
||||
* 是否开启控制台页面
|
||||
* @默认值 `true`
|
||||
*/
|
||||
enable?: boolean
|
||||
/**
|
||||
* 控制台名称
|
||||
* @默认值 `'控制台'`
|
||||
*/
|
||||
title?: string
|
||||
}
|
||||
interface layout {
|
||||
/**
|
||||
* 是否开启移动端适配,开启后当页面宽度小于 992px 时自动切换为移动端展示
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableMobileAdaptation?: boolean
|
||||
}
|
||||
interface menu {
|
||||
/**
|
||||
* 导航栏数据来源,当 `app.routeBaseOn: 'filesystem'` 时生效
|
||||
* @默认值 `'frontend'` 前端
|
||||
* @可选值 `'backend'` 后端
|
||||
*/
|
||||
baseOn?: 'frontend' | 'backend'
|
||||
/**
|
||||
* 导航栏模式
|
||||
* @默认值 `'side'` 侧边栏模式(有主导航)
|
||||
* @可选值 `'head'` 顶部模式
|
||||
* @可选值 `'single'` 侧边栏模式(无主导航)
|
||||
*/
|
||||
menuMode?: 'side' | 'head' | 'single'
|
||||
/**
|
||||
* 切换主导航是否跳转页面
|
||||
* @默认值 `false`
|
||||
*/
|
||||
switchMainMenuAndPageJump?: boolean
|
||||
/**
|
||||
* 次导航是否只保持一个子项的展开
|
||||
* @默认值 `true`
|
||||
*/
|
||||
subMenuUniqueOpened?: boolean
|
||||
/**
|
||||
* 次导航是否收起
|
||||
* @默认值 `false`
|
||||
*/
|
||||
subMenuCollapse?: boolean
|
||||
/**
|
||||
* 是否开启次导航的展开/收起按钮
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableSubMenuCollapseButton?: boolean
|
||||
/**
|
||||
* 是否开启主导航切换快捷键
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableHotkeys?: boolean
|
||||
}
|
||||
interface topbar {
|
||||
/**
|
||||
* 是否固定顶栏
|
||||
* @默认值 `true`
|
||||
*/
|
||||
fixed?: boolean
|
||||
}
|
||||
interface toolbar {
|
||||
/**
|
||||
* 是否开启全屏
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableFullscreen?: boolean
|
||||
/**
|
||||
* 是否开启页面刷新
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enablePageReload?: boolean
|
||||
/**
|
||||
* 是否开启颜色主题
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableColorScheme?: boolean
|
||||
/**
|
||||
* 是否开启应用配置,强烈建议在生产环境中关闭
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableAppSetting?: boolean
|
||||
}
|
||||
interface breadcrumb {
|
||||
/**
|
||||
* 是否开启面包屑导航
|
||||
* @默认值 `true`
|
||||
*/
|
||||
enable?: boolean
|
||||
}
|
||||
interface navSearch {
|
||||
/**
|
||||
* 是否开启导航搜索
|
||||
* @默认值 `true`
|
||||
*/
|
||||
enable?: boolean
|
||||
/**
|
||||
* 是否开启导航搜索快捷键
|
||||
* @默认值 `true`
|
||||
*/
|
||||
enableHotkeys?: boolean
|
||||
}
|
||||
interface copyright {
|
||||
/**
|
||||
* 是否开启底部版权,同时在路由 meta 对象里可以单独设置某个路由是否显示底部版权信息
|
||||
* @默认值 `true`
|
||||
*/
|
||||
enable?: boolean
|
||||
/**
|
||||
* 网站运行日期
|
||||
* @默认值 `'2020-2022'`
|
||||
*/
|
||||
dates?: string
|
||||
/**
|
||||
* 公司名称
|
||||
* @默认值 `'Fantastic-admin'`
|
||||
*/
|
||||
company?: string
|
||||
/**
|
||||
* 网站地址
|
||||
* @默认值 `'https://hooray.gitee.io/fantastic-admin/'`
|
||||
*/
|
||||
website?: string
|
||||
/**
|
||||
* 网站备案号
|
||||
* @默认值 `''`
|
||||
*/
|
||||
beian?: string
|
||||
}
|
||||
interface all {
|
||||
/** 应用设置 */
|
||||
app?: app
|
||||
/** 控制台设置 */
|
||||
dashboard?: dashboard
|
||||
/** 布局设置 */
|
||||
layout?: layout
|
||||
/** 导航栏设置 */
|
||||
menu?: menu
|
||||
/** 顶栏设置 */
|
||||
topbar?: topbar
|
||||
/** 工具栏设置 */
|
||||
toolbar?: toolbar
|
||||
/** 面包屑导航设置 */
|
||||
breadcrumb?: breadcrumb
|
||||
/** 导航搜索设置 */
|
||||
navSearch?: navSearch
|
||||
/** 底部版权设置 */
|
||||
copyright?: copyright
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
title: string | Function
|
||||
i18n?: string
|
||||
icon?: string
|
||||
activeIcon?: string
|
||||
defaultOpened?: boolean
|
||||
auth?: string | string[]
|
||||
sidebar?: boolean
|
||||
breadcrumb?: boolean
|
||||
activeMenu?: string
|
||||
cache?: boolean | string | string[]
|
||||
breadcrumbNeste?: Route.breadcrumb[]
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace Route {
|
||||
interface recordRaw extends Omit<RouteRecordRaw, 'meta' | 'children'> {
|
||||
meta: RouteMeta
|
||||
children?: recordRaw[]
|
||||
}
|
||||
interface recordMainRaw {
|
||||
meta: {
|
||||
title?: string | Function
|
||||
icon?: string
|
||||
auth?: string | string[]
|
||||
}
|
||||
children: recordRaw[]
|
||||
}
|
||||
interface breadcrumb {
|
||||
path: string
|
||||
title: string | Function
|
||||
hide: boolean
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace Menu {
|
||||
/** 原始 */
|
||||
interface recordRaw {
|
||||
path?: string
|
||||
meta: {
|
||||
title: string
|
||||
icon?: string
|
||||
defaultOpened?: boolean
|
||||
auth?: string | string[]
|
||||
sidebar?: boolean
|
||||
}
|
||||
children?: recordRaw[]
|
||||
}
|
||||
/** 主导航 */
|
||||
interface recordMainRaw {
|
||||
meta: {
|
||||
title?: string
|
||||
icon?: string
|
||||
auth?: string | string[]
|
||||
}
|
||||
children: recordRaw[]
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { addCollection } from '@iconify/vue'
|
||||
|
||||
export async function downloadAndInstall() {
|
||||
const data = Object.freeze(await fetch('./icons/icons.json').then(r => r.json()))
|
||||
addCollection(data)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { addCollection } from '@iconify/vue'
|
||||
|
||||
export async function downloadAndInstall() {
|
||||
const data = Object.freeze(await fetch('./icons/icons.json').then(r => r.json()))
|
||||
addCollection(data)
|
||||
}
|
||||
@@ -1,457 +0,0 @@
|
||||
<script setup name="AppSetting">
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import globalSettings from '@/settings'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const isShow = ref(false)
|
||||
|
||||
const settings = ref(globalSettings)
|
||||
|
||||
watch(() => settings, () => {
|
||||
settingsStore.updateThemeSetting(settings.value)
|
||||
menuStore.setActived(0)
|
||||
settings.value.menu.menuMode !== 'single' && menuStore.setActived(route.fullPath)
|
||||
}, {
|
||||
deep: true
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on('global-theme-toggle', () => {
|
||||
isShow.value = !isShow.value
|
||||
})
|
||||
})
|
||||
|
||||
const { copy, copied, isSupported } = useClipboard()
|
||||
|
||||
watch(copied, val => {
|
||||
if (val) {
|
||||
ElMessage.success('复制成功,请粘贴到 src/settings.custom.json 文件中!')
|
||||
}
|
||||
})
|
||||
|
||||
function handleCopy() {
|
||||
copy(JSON.stringify(settings.value, null, 4))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer v-model="isShow" title="应用配置" direction="rtl" :size="360">
|
||||
<el-alert title="应用配置可实时预览效果,但仅是临时生效,要想真正作用于项目,可以点击下方的“复制配置”按钮,将配置粘贴到 src/settings.custom.json 中即可,或者也可在 src/settings.js 中直接修改默认配置。同时建议在生产环境隐藏应用配置功能。" type="error" :closable="false" />
|
||||
<el-divider>颜色主题</el-divider>
|
||||
<div class="color-scheme">
|
||||
<div class="switch" :class="settings.app.colorScheme" @click="settings.app.colorScheme = settings.app.colorScheme === 'dark' ? 'light' : 'dark'">
|
||||
<el-icon class="icon">
|
||||
<svg-icon v-show="settings.app.colorScheme === 'light'" name="ep:sunny" />
|
||||
<svg-icon v-show="settings.app.colorScheme === 'dark'" name="ep:moon" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider v-if="settingsStore.mode === 'pc'">导航栏模式</el-divider>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="menu-mode">
|
||||
<el-tooltip content="侧边栏模式(含主导航)" placement="top" :show-after="500">
|
||||
<div class="mode mode-side" :class="{'active': settings.menu.menuMode === 'side'}" @click="settings.menu.menuMode = 'side'">
|
||||
<div class="mode-container" />
|
||||
<el-icon>
|
||||
<svg-icon name="ep:check" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="顶部模式" placement="top" :show-after="500">
|
||||
<div class="mode mode-head" :class="{'active': settings.menu.menuMode === 'head'}" @click="settings.menu.menuMode = 'head'">
|
||||
<div class="mode-container" />
|
||||
<el-icon>
|
||||
<svg-icon name="ep:check" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="侧边栏模式(不含主导航)" placement="top" :show-after="500">
|
||||
<div class="mode mode-single" :class="{'active': settings.menu.menuMode === 'single'}" @click="settings.menu.menuMode = 'single'">
|
||||
<div class="mode-container" />
|
||||
<el-icon>
|
||||
<svg-icon name="ep:check" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-divider>导航栏</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">是否折叠</div>
|
||||
<el-switch v-model="settings.menu.subMenuCollapse" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
切换跳转
|
||||
<el-tooltip content="开启该功能后,切换侧边栏时,页面自动跳转至该侧边栏导航下第一个路由地址" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.menu.switchMainMenuAndPageJump" :disabled="['single'].includes(settings.menu.menuMode)" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
保持展开一个
|
||||
<el-tooltip content="开启该功能后,侧边栏只保持一个子菜单的展开" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.menu.subMenuUniqueOpened" />
|
||||
</div>
|
||||
<el-divider>顶栏</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">是否固定</div>
|
||||
<el-switch v-model="settings.topbar.fixed" />
|
||||
</div>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
|
||||
<div class="label">折叠按钮</div>
|
||||
<el-switch v-model="settings.topbar.enableSidebarCollapse" />
|
||||
</div>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
|
||||
<div class="label">面包屑导航</div>
|
||||
<el-switch v-model="settings.topbar.enableBreadcrumb" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
导航栏搜索
|
||||
<el-tooltip content="对导航栏进行快捷搜索" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.topbar.enableNavSearch" />
|
||||
</div>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
|
||||
<div class="label">
|
||||
全屏
|
||||
<el-tooltip content="该功能使用场景极少,用户习惯于通过窗口“最大化”功能来扩大显示区域,以显示更多内容,并且使用 F11 键也可以进入全屏效果" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.topbar.enableFullscreen" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
页面刷新
|
||||
<el-tooltip content="开启时会阻止原生 F5 键刷新功能,并采用框架提供的刷新模式进行页面刷新" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.topbar.enablePageReload" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
颜色主题
|
||||
<el-tooltip content="开启后可在明亮/暗黑模式中切换" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.topbar.enableColorScheme" />
|
||||
</div>
|
||||
<el-divider>底部版权</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">是否启用</div>
|
||||
<el-switch v-model="settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">日期</div>
|
||||
<el-input v-model="settings.copyright.dates" size="small" :disabled="!settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">公司</div>
|
||||
<el-input v-model="settings.copyright.company" size="small" :disabled="!settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">网址</div>
|
||||
<el-input v-model="settings.copyright.website" size="small" :disabled="!settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">备案</div>
|
||||
<el-input v-model="settings.copyright.beian" size="small" :disabled="!settings.copyright.enable" />
|
||||
</div>
|
||||
<el-divider>控制台</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否开启
|
||||
<el-tooltip content="该功能开启时,登录成功默认进入控制台页面,反之则默认进入导航栏里第一个导航页面" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.dashboard.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">控制台名称</div>
|
||||
<el-input v-model="settings.dashboard.title" size="small" />
|
||||
</div>
|
||||
<el-divider>其它</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
组件尺寸
|
||||
<el-tooltip content="全局设置 Element Plus 组件的默认尺寸大小" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-radio-group v-model="settings.app.elementSize" size="small">
|
||||
<el-radio-button label="large">较大</el-radio-button>
|
||||
<el-radio-button label="default">默认</el-radio-button>
|
||||
<el-radio-button label="small">稍小</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">是否启用权限</div>
|
||||
<el-switch v-model="settings.app.enablePermission" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
载入进度条
|
||||
<el-tooltip content="该功能开启时,跳转路由会看到页面顶部有进度条" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.app.enableProgress" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
动态标题
|
||||
<el-tooltip content="该功能开启时,页面标题会显示当前路由标题,格式为“页面标题 - 网站名称”;关闭时则显示网站名称,网站名称在项目根目录下 .env.* 文件里配置" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.app.enableDynamicTitle" />
|
||||
</div>
|
||||
<template v-if="isSupported" #footer>
|
||||
<el-button type="primary" @click="handleCopy">
|
||||
<template #icon>
|
||||
<el-icon>
|
||||
<svg-icon name="ep:document-copy" />
|
||||
</el-icon>
|
||||
</template>
|
||||
复制配置
|
||||
</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: initial;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
:deep(.el-drawer__footer) {
|
||||
padding: 20px;
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
transition: var(--el-transition-border);
|
||||
.el-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
:deep(.el-divider) {
|
||||
margin: 36px 0 24px;
|
||||
}
|
||||
.color-scheme {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 10px;
|
||||
$width: 50px;
|
||||
.switch {
|
||||
width: $width;
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
cursor: pointer;
|
||||
background-color: var(--el-fill-color-darker);
|
||||
transition: background-color 0.3s;
|
||||
&.dark {
|
||||
.icon {
|
||||
margin-left: calc($width - 24px - 3px);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
margin: 3px;
|
||||
padding: 5px;
|
||||
font-size: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
transition: margin-left 0.3s, background-color 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
.menu-mode {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 10px;
|
||||
.mode {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 55px;
|
||||
margin: 10px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background-color: var(--g-app-bg);
|
||||
box-shadow: 0 0 5px 1px var(--el-border-color-lighter);
|
||||
transition: 0.2s;
|
||||
&:hover {
|
||||
box-shadow: 0 0 5px 1px var(--el-border-color-darker);
|
||||
}
|
||||
&.active {
|
||||
box-shadow: 0 0 0 2px var(--el-color-primary);
|
||||
}
|
||||
&::before,
|
||||
&::after,
|
||||
.mode-container {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.mode-container::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.2;
|
||||
}
|
||||
&-side {
|
||||
&::before {
|
||||
content: "";
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
bottom: 5px;
|
||||
width: 10px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
&::after {
|
||||
content: "";
|
||||
top: 5px;
|
||||
left: 20px;
|
||||
bottom: 5px;
|
||||
width: 15px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.mode-container {
|
||||
top: 5px;
|
||||
left: 40px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
}
|
||||
&-head {
|
||||
&::before {
|
||||
content: "";
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
height: 10px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
&::after {
|
||||
content: "";
|
||||
top: 20px;
|
||||
left: 5px;
|
||||
bottom: 5px;
|
||||
width: 15px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.mode-container {
|
||||
top: 20px;
|
||||
left: 25px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
}
|
||||
&-single {
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
bottom: 5px;
|
||||
width: 15px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.mode-container {
|
||||
top: 5px;
|
||||
left: 25px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
}
|
||||
i {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
display: none;
|
||||
}
|
||||
&.active i {
|
||||
display: block;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 5px 0;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background: var(--el-fill-color);
|
||||
}
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
i {
|
||||
margin-left: 4px;
|
||||
font-size: 17px;
|
||||
color: var(--el-color-warning);
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
.el-switch {
|
||||
height: auto;
|
||||
}
|
||||
.el-input {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,114 +0,0 @@
|
||||
<script setup name="BuyIt">
|
||||
const isActived = ref(true)
|
||||
setTimeout(() => {
|
||||
isActived.value = false
|
||||
}, 5000)
|
||||
|
||||
onMounted(() => {
|
||||
ElNotification({
|
||||
type: 'success',
|
||||
title: '温馨提示',
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: `
|
||||
<p>当前访问的是<b>基础版</b> (Vue3)</p>
|
||||
<p>你可以点<a href="https://hooray.gitee.io/fantastic-admin-pro-example/" target="_blank"><b>这里</b></a>访问专业版 (Vue3)</p>
|
||||
`,
|
||||
position: 'bottom-right',
|
||||
duration: 5000
|
||||
})
|
||||
})
|
||||
|
||||
function open(url) {
|
||||
window.open(url, 'top')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="buy-it" :class="{'actived': isActived}">
|
||||
<div class="item" @click="open(`https://hooray.gitee.io/fantastic-admin/buy.html`)">
|
||||
<el-icon>
|
||||
<svg-icon name="fixed-right-buy" />
|
||||
</el-icon>
|
||||
<span class="title">购买<br>专业版</span>
|
||||
</div>
|
||||
<div class="item" @click="open('https://gitee.com/hooray/fantastic-admin/')">
|
||||
<el-icon>
|
||||
<svg-icon name="fixed-right-code" />
|
||||
</el-icon>
|
||||
<span class="title">下载<br>基础版</span>
|
||||
</div>
|
||||
<div class="item" @click="open(`https://hooray.gitee.io/fantastic-admin/`)">
|
||||
<el-icon>
|
||||
<svg-icon name="fixed-right-doc" />
|
||||
</el-icon>
|
||||
<span class="title">开发<br>文档</span>
|
||||
</div>
|
||||
<div class="item" @click="open(`https://hooray.gitee.io/fantastic-admin/support.html`)">
|
||||
<el-icon>
|
||||
<svg-icon name="fixed-right-chat" />
|
||||
</el-icon>
|
||||
<span class="title">技术<br>支持</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.buy-it {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
right: -58px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: right 0.3s;
|
||||
&.actived,
|
||||
&:hover {
|
||||
right: 0;
|
||||
}
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 70px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
border-bottom: 1px solid #fff;
|
||||
transition: 0.3s;
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
&:first-child {
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
&:last-child {
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
&:nth-child(1) {
|
||||
background-color: #ff4200;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
background-color: #409eff;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
background-color: #0fcc1a;
|
||||
}
|
||||
&:nth-child(4) {
|
||||
background-color: #343b42;
|
||||
}
|
||||
.el-icon {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
font-size: 20px;
|
||||
}
|
||||
.title {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,170 +0,0 @@
|
||||
<script setup name="Header">
|
||||
import Logo from '../Logo/index.vue'
|
||||
import Tools from '../Tools/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import { useMenu } from '@/utils/composables'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const navRef = ref()
|
||||
|
||||
// 顶部模式鼠标滚动
|
||||
function handlerMouserScroll(event) {
|
||||
navRef.value.scrollBy({
|
||||
left: (event.deltaY || event.detail) > 0 ? 50 : -50
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="header">
|
||||
<header v-if="settingsStore.mode === 'pc' && settingsStore.menu.menuMode === 'head'">
|
||||
<div class="header-container">
|
||||
<div class="main">
|
||||
<Logo />
|
||||
<!-- 顶部模式 -->
|
||||
<div ref="navRef" class="nav" @wheel.prevent="handlerMouserScroll">
|
||||
<template v-for="(item, index) in menuStore.allMenus" :key="index">
|
||||
<div v-if="item.children && item.children.length !== 0" class="item-container" :class="{'active': index == menuStore.actived}">
|
||||
<div class="item" @click="useMenu().switchTo(index)">
|
||||
<el-icon>
|
||||
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span v-if="item.meta.title">{{ item.meta.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<Tools />
|
||||
</div>
|
||||
</header>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
header {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: var(--g-header-height);
|
||||
color: var(--g-header-color);
|
||||
background-color: var(--g-header-bg);
|
||||
transition: background-color 0.3s, var(--el-transition-color);
|
||||
.header-container {
|
||||
width: var(--g-header-width);
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: var(--g-header-width)) {
|
||||
.header-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
:deep(.title) {
|
||||
position: relative;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
padding: inherit;
|
||||
background-color: inherit;
|
||||
.logo {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
span {
|
||||
font-size: 24px;
|
||||
letter-spacing: 1px;
|
||||
color: var(--g-header-color);
|
||||
}
|
||||
}
|
||||
.nav {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
margin: 0 30px;
|
||||
padding: 0 20px;
|
||||
overflow-x: auto;
|
||||
mask-image: linear-gradient(to right, transparent, #000 20px, #000 calc(100% - 20px), transparent);
|
||||
-webkit-mask-image: linear-gradient(to right, transparent, #000 20px, #000 calc(100% - 20px), transparent);
|
||||
// firefox隐藏滚动条
|
||||
scrollbar-width: none;
|
||||
// chrome隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.item-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: initial;
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 0 5px;
|
||||
width: 80px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
color: var(--g-header-menu-color);
|
||||
background-color: var(--g-header-bg);
|
||||
transition: background-color 0.3s, var(--el-transition-color);
|
||||
&:hover {
|
||||
color: var(--g-header-menu-hover-color);
|
||||
background-color: var(--g-header-menu-hover-bg);
|
||||
}
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
span {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
word-break: break-all;
|
||||
@include text-overflow(1, false);
|
||||
}
|
||||
}
|
||||
&.active .item {
|
||||
color: var(--g-header-menu-active-color);
|
||||
background-color: var(--g-header-menu-active-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.tools) {
|
||||
padding: 0;
|
||||
.buttons .item .el-icon {
|
||||
color: var(--g-header-color);
|
||||
}
|
||||
.user-container {
|
||||
font-size: 16px;
|
||||
color: var(--g-header-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 头部动画
|
||||
.header-enter-active,
|
||||
.header-leave-active {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.header-enter-from,
|
||||
.header-leave-to {
|
||||
transform: translateY(calc(var(--g-header-height) * -1));
|
||||
}
|
||||
</style>
|
||||
@@ -1,68 +0,0 @@
|
||||
<script setup name="Logo">
|
||||
import imgLogo from '@/assets/images/logo.png'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
defineProps({
|
||||
showLogo: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showTitle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const title = ref(import.meta.env.VITE_APP_TITLE)
|
||||
const logo = ref(imgLogo)
|
||||
|
||||
const to = computed(() => {
|
||||
let rtn = {}
|
||||
if (settingsStore.dashboard.enable) {
|
||||
rtn.name = 'dashboard'
|
||||
}
|
||||
return rtn
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link :to="to" class="title" :class="{'is-link': settingsStore.dashboard.enable}" :title="title">
|
||||
<img v-if="showLogo" :src="logo" class="logo">
|
||||
<span v-if="showTitle">{{ title }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
width: inherit;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: var(--g-sidebar-logo-height);
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
&.is-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
.logo {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
& + span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
span {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
@include text-overflow;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,101 +0,0 @@
|
||||
<script setup name="MainSidebar">
|
||||
import Logo from '../Logo/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import { useMenu } from '@/utils/composables'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="main-sidebar">
|
||||
<div v-if="settingsStore.menu.menuMode === 'side' || (settingsStore.mode === 'mobile' && settingsStore.menu.menuMode !== 'single')" class="main-sidebar-container">
|
||||
<Logo :show-title="false" class="sidebar-logo" />
|
||||
<!-- 侧边栏模式(含主导航) -->
|
||||
<div class="nav">
|
||||
<template v-for="(item, index) in menuStore.allMenus">
|
||||
<div
|
||||
v-if="item.children && item.children.length !== 0" :key="index" :class="{
|
||||
'item': true,
|
||||
'active': index === menuStore.actived
|
||||
}" :title="item.meta.title" @click="useMenu().switchTo(index)"
|
||||
>
|
||||
<el-icon>
|
||||
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span>{{ item.meta.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-sidebar-container {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
// firefox隐藏滚动条
|
||||
scrollbar-width: none;
|
||||
// chrome隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: var(--g-main-sidebar-width);
|
||||
color: var(--g-main-sidebar-menu-color);
|
||||
background-color: var(--g-main-sidebar-bg);
|
||||
transition: background-color 0.3s, var(--el-transition-color);
|
||||
.sidebar-logo {
|
||||
transition: 0.3s;
|
||||
background-color: var(--g-main-sidebar-bg);
|
||||
}
|
||||
.nav {
|
||||
width: inherit;
|
||||
padding-top: var(--g-sidebar-logo-height);
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
height: 60px;
|
||||
padding: 0 5px;
|
||||
cursor: pointer;
|
||||
color: var(--g-main-sidebar-menu-color);
|
||||
background-color: var(--g-main-sidebar-bg);
|
||||
transition: background-color 0.3s, var(--el-transition-color);
|
||||
&:hover {
|
||||
color: var(--g-main-sidebar-menu-hover-color);
|
||||
background-color: var(--g-main-sidebar-menu-hover-bg);
|
||||
}
|
||||
&.active {
|
||||
color: var(--g-main-sidebar-menu-active-color);
|
||||
background-color: var(--g-main-sidebar-menu-active-bg);
|
||||
}
|
||||
.el-icon {
|
||||
margin: 0 auto;
|
||||
font-size: 24px;
|
||||
}
|
||||
span {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
@include text-overflow(1, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 主侧边栏动画
|
||||
.main-sidebar-enter-active,
|
||||
.main-sidebar-leave-active {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.main-sidebar-enter-from,
|
||||
.main-sidebar-leave-to {
|
||||
transform: translateX(calc(var(--g-main-sidebar-width) * -1));
|
||||
}
|
||||
</style>
|
||||
@@ -1,398 +0,0 @@
|
||||
<script setup name="Search">
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import { deepClone, isExternalLink } from '@/utils'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useRouteStore from '@/store/modules/route'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const routeStore = useRouteStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const isShow = ref(false)
|
||||
const searchInput = ref('')
|
||||
const sourceList = ref([])
|
||||
const actived = ref(-1)
|
||||
|
||||
const inputRef = ref()
|
||||
const resultRef = ref()
|
||||
const resultItemRef = ref([])
|
||||
const setResultItemRef = el => resultItemRef.value.push(el)
|
||||
onBeforeUpdate(() => {
|
||||
resultItemRef.value = []
|
||||
})
|
||||
|
||||
const resultList = computed(() => {
|
||||
let result = []
|
||||
result = sourceList.value.filter(item => {
|
||||
let flag = false
|
||||
if (item.title.indexOf(searchInput.value) >= 0) {
|
||||
flag = true
|
||||
}
|
||||
if (item.path.indexOf(searchInput.value) >= 0) {
|
||||
flag = true
|
||||
}
|
||||
if (item.breadcrumb.some(b => b.indexOf(searchInput.value) >= 0)) {
|
||||
flag = true
|
||||
}
|
||||
return flag
|
||||
})
|
||||
return result
|
||||
})
|
||||
|
||||
watch(() => isShow.value, val => {
|
||||
if (val) {
|
||||
document.querySelector('body').classList.add('hidden')
|
||||
resultRef.value.scrollTop = 0
|
||||
// 当搜索显示的时候绑定上、下、回车快捷键,隐藏的时候再解绑。另外当 input 处于 focus 状态时,采用 vue 来绑定键盘事件
|
||||
hotkeys('up', keyUp)
|
||||
hotkeys('down', keyDown)
|
||||
hotkeys('enter', keyEnter)
|
||||
setTimeout(() => {
|
||||
inputRef.value.focus()
|
||||
}, 500)
|
||||
} else {
|
||||
document.querySelector('body').classList.remove('hidden')
|
||||
hotkeys.unbind('up', keyUp)
|
||||
hotkeys.unbind('down', keyDown)
|
||||
hotkeys.unbind('enter', keyEnter)
|
||||
setTimeout(() => {
|
||||
searchInput.value = ''
|
||||
actived.value = -1
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
watch(() => resultList.value, () => {
|
||||
actived.value = -1
|
||||
handleScroll()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on('global-search-toggle', () => {
|
||||
isShow.value = !isShow.value
|
||||
})
|
||||
hotkeys('alt+s', e => {
|
||||
if (settingsStore.topbar.enableNavSearch) {
|
||||
e.preventDefault()
|
||||
isShow.value = true
|
||||
}
|
||||
})
|
||||
hotkeys('esc', e => {
|
||||
if (settingsStore.topbar.enableNavSearch) {
|
||||
e.preventDefault()
|
||||
isShow.value = false
|
||||
}
|
||||
})
|
||||
if (settingsStore.app.routeBaseOn !== 'filesystem') {
|
||||
routeStore.routes.map(item => {
|
||||
getSourceList(item.children)
|
||||
})
|
||||
} else {
|
||||
menuStore.menus.map(item => {
|
||||
getSourceList(item.children)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function hasChildren(item) {
|
||||
let flag = true
|
||||
if (item.children) {
|
||||
if (item.children.every(i => i.meta.sidebar === false)) {
|
||||
flag = false
|
||||
}
|
||||
} else {
|
||||
flag = false
|
||||
}
|
||||
return flag
|
||||
}
|
||||
function getSourceList(arr) {
|
||||
arr.map(item => {
|
||||
if (item.meta.sidebar !== false) {
|
||||
if (hasChildren(item)) {
|
||||
let baseBreadcrumb = item.meta.baseBreadcrumb ? deepClone(item.meta.baseBreadcrumb) : []
|
||||
baseBreadcrumb.push(item.meta.title)
|
||||
let child = deepClone(item.children)
|
||||
child.map(c => {
|
||||
c.meta.baseIcon = item.meta.icon || item.meta.baseIcon
|
||||
c.meta.baseBreadcrumb = baseBreadcrumb
|
||||
c.meta.basePath = item.meta.basePath ? [item.meta.basePath, item.path].join('/') : item.path
|
||||
})
|
||||
getSourceList(child)
|
||||
} else {
|
||||
let breadcrumb = []
|
||||
if (item.meta.baseBreadcrumb) {
|
||||
breadcrumb = deepClone(item.meta.baseBreadcrumb)
|
||||
}
|
||||
breadcrumb.push(item.meta.title)
|
||||
let path = ''
|
||||
if (isExternalLink(item.path)) {
|
||||
path = item.path
|
||||
} else {
|
||||
path = item.meta.basePath ? [item.meta.basePath, item.path].join('/') : item.path
|
||||
}
|
||||
sourceList.value.push({
|
||||
icon: item.meta.icon || item.meta.baseIcon,
|
||||
title: item.meta.title,
|
||||
i18n: item.meta.i18n,
|
||||
breadcrumb: breadcrumb,
|
||||
path: path
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
function keyUp() {
|
||||
if (resultList.value.length) {
|
||||
actived.value -= 1
|
||||
if (actived.value < 0) {
|
||||
actived.value = resultList.value.length - 1
|
||||
}
|
||||
handleScroll()
|
||||
}
|
||||
}
|
||||
function keyDown() {
|
||||
if (resultList.value.length) {
|
||||
actived.value += 1
|
||||
if (actived.value > resultList.value.length - 1) {
|
||||
actived.value = 0
|
||||
}
|
||||
handleScroll()
|
||||
}
|
||||
}
|
||||
function keyEnter() {
|
||||
if (actived.value !== -1) {
|
||||
resultItemRef.value[actived.value].click()
|
||||
}
|
||||
}
|
||||
function handleScroll() {
|
||||
let scrollTo = 0
|
||||
if (actived.value !== -1) {
|
||||
scrollTo = resultRef.value.scrollTop
|
||||
const activedOffsetTop = resultItemRef.value[actived.value].offsetTop
|
||||
const activedClientHeight = resultItemRef.value[actived.value].clientHeight
|
||||
const searchScrollTop = resultRef.value.scrollTop
|
||||
const searchClientHeight = resultRef.value.clientHeight
|
||||
if (activedOffsetTop + activedClientHeight > searchScrollTop + searchClientHeight) {
|
||||
scrollTo = activedOffsetTop + activedClientHeight - searchClientHeight
|
||||
} else if (activedOffsetTop <= searchScrollTop) {
|
||||
scrollTo = activedOffsetTop
|
||||
}
|
||||
}
|
||||
resultRef.value.scrollTo({
|
||||
top: scrollTo
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="search" :class="{'searching': isShow}" @click="isShow && eventBus.emit('global-search-toggle')">
|
||||
<div class="container">
|
||||
<div class="search-box" @click.stop>
|
||||
<el-input ref="inputRef" v-model="searchInput" placeholder="搜索页面,支持标题、URL模糊查询" clearable @keydown.esc="eventBus.emit('global-search-toggle')" @keydown.up.prevent="keyUp" @keydown.down.prevent="keyDown" @keydown.enter.prevent="keyEnter">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<svg-icon name="ep:search" />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="tips">
|
||||
<div class="tip">
|
||||
<span>Alt</span>+<span>S</span>
|
||||
唤醒搜索面板
|
||||
</div>
|
||||
<div class="tip">
|
||||
<span>
|
||||
<el-icon>
|
||||
<svg-icon name="search-up" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span>
|
||||
<el-icon>
|
||||
<svg-icon name="search-down" />
|
||||
</el-icon>
|
||||
</span>
|
||||
切换搜索结果
|
||||
</div>
|
||||
<div class="tip">
|
||||
<span>
|
||||
<el-icon>
|
||||
<svg-icon name="search-enter" />
|
||||
</el-icon>
|
||||
</span>
|
||||
访问页面
|
||||
</div>
|
||||
<div class="tip">
|
||||
<span>ESC</span>
|
||||
退出
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="resultRef" class="result" :class="{'mobile': settingsStore.mode === 'mobile'}">
|
||||
<router-link v-for="(item, index) in resultList" :key="item.path" v-slot="{ href, navigate }" custom :to="isShow ? item.path : ''">
|
||||
<a :ref="setResultItemRef" :href="isExternalLink(item.path) ? item.path : href" class="item" :class="{'actived': index === actived}" :target="isExternalLink(item.path) ? '_blank' : '_self'" @click="navigate" @mouseover="actived = index">
|
||||
<el-icon class="icon">
|
||||
<svg-icon v-if="item.icon" :name="item.icon" />
|
||||
</el-icon>
|
||||
<div class="info">
|
||||
<div class="title">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div class="breadcrumb">
|
||||
<span v-for="(bc, bcIndex) in item.breadcrumb" :key="bcIndex">
|
||||
{{ bc }}
|
||||
<el-icon>
|
||||
<svg-icon name="ep:arrow-right" />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
<div class="path">{{ item.path }}</div>
|
||||
</div>
|
||||
</a>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#search {
|
||||
position: fixed;
|
||||
z-index: 2000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.2s;
|
||||
transform: translateZ(0);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
&.searching {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
.container {
|
||||
transform: initial;
|
||||
filter: initial;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 800px;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
transition: all 0.2s;
|
||||
transform: scale(1.1);
|
||||
filter: blur(10px);
|
||||
.search-box {
|
||||
margin: 50px 20px 40px;
|
||||
:deep(.el-input__inner) {
|
||||
height: 52px;
|
||||
line-height: 52px;
|
||||
}
|
||||
:deep(.el-input__icon) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.tips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
margin-top: 20px;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
span {
|
||||
margin: 0 5px;
|
||||
padding: 3px 8px 5px;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: var(--el-text-color-primary);
|
||||
background-color: var(--el-fill-color);
|
||||
box-shadow: inset 0 -2px var(--el-fill-color-lighter), inset 0 0 1px 1px var(--el-fill-color-darker);
|
||||
}
|
||||
}
|
||||
}
|
||||
.result {
|
||||
position: relative;
|
||||
margin: 0 20px;
|
||||
max-height: calc(100% - 250px);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
background-color: var(--el-bg-color);
|
||||
box-shadow: 0 0 0 1px var(--el-border-color-darker);
|
||||
&.mobile {
|
||||
max-height: calc(100% - 200px);
|
||||
}
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&.actived {
|
||||
background-color: var(--el-bg-color-page);
|
||||
.icon {
|
||||
color: var(--el-color-primary);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
.info {
|
||||
border-left-color: var(--el-border-color);
|
||||
.title {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.breadcrumb,
|
||||
.path {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
flex: 0 0 66px;
|
||||
text-align: center;
|
||||
color: var(--el-color-info);
|
||||
font-size: 20px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.info {
|
||||
flex: 1;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
border-left: 1px solid var(--el-border-color-lighter);
|
||||
padding: 5px 10px 7px;
|
||||
transition: all 0.3s;
|
||||
@include text-overflow(1, true);
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--el-text-color-regular);
|
||||
@include text-overflow(1, true);
|
||||
}
|
||||
.breadcrumb,
|
||||
.path {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: all 0.3s;
|
||||
@include text-overflow(1, true);
|
||||
}
|
||||
.breadcrumb {
|
||||
span {
|
||||
margin-right: 5px;
|
||||
&:last-child i {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,147 +0,0 @@
|
||||
<script setup name="SidebarItem">
|
||||
import SidebarItem from './index.vue'
|
||||
import { isExternalLink, resolveRoutePath } from '@/utils'
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const hasChildren = computed(() => {
|
||||
let flag = true
|
||||
if (props.item.children) {
|
||||
if (props.item.children.every(item => item.meta.sidebar === false)) {
|
||||
flag = false
|
||||
}
|
||||
} else {
|
||||
flag = false
|
||||
}
|
||||
return flag
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar-item">
|
||||
<el-sub-menu v-if="item.path == undefined" :title="item.meta.title" :index="JSON.stringify(item)">
|
||||
<template #title>
|
||||
<el-icon v-if="item.meta.icon" class="title-icon">
|
||||
<svg-icon :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span class="title">{{ item.meta.title }}</span>
|
||||
</template>
|
||||
<template v-for="route in item.children">
|
||||
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path" :item="route" />
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
<router-link v-else-if="!hasChildren" v-slot="{ href, navigate, isActive, isExactActive }" custom :to="resolveRoutePath(basePath, item.path)">
|
||||
<a :href="isExternalLink(resolveRoutePath(basePath, item.path)) ? resolveRoutePath(basePath, item.path) : href" :class="[isActive && 'router-link-active', isExactActive && 'router-link-exact-active']" :target="isExternalLink(resolveRoutePath(basePath, item.path)) ? '_blank' : '_self'" @click="navigate">
|
||||
<el-menu-item :title="item.meta.title" :index="resolveRoutePath(basePath, item.path)">
|
||||
<el-icon v-if="item.meta.icon" class="title-icon">
|
||||
<svg-icon :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span class="title">{{ item.meta.title }}</span>
|
||||
</el-menu-item>
|
||||
</a>
|
||||
</router-link>
|
||||
<el-sub-menu v-else :title="item.meta.title" :index="resolveRoutePath(basePath, item.path)">
|
||||
<template #title>
|
||||
<el-icon v-if="item.meta.icon" class="title-icon">
|
||||
<svg-icon :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span class="title">{{ item.meta.title }}</span>
|
||||
</template>
|
||||
<template v-for="route in item.children">
|
||||
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path" :item="route" :base-path="resolveRoutePath(basePath, item.path)" />
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-menu-item span.title),
|
||||
:deep(.el-sub-menu__title),
|
||||
:deep(.el-sub-menu__title span.title) {
|
||||
flex: 1;
|
||||
vertical-align: inherit;
|
||||
@include text-overflow;
|
||||
}
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
:deep(.el-sub-menu),
|
||||
:deep(.el-menu-item) {
|
||||
.title-icon {
|
||||
width: 20px;
|
||||
font-size: 20px;
|
||||
vertical-align: -0.25em;
|
||||
transition: transform 0.3s;
|
||||
color: unset;
|
||||
}
|
||||
.title-icon + .title {
|
||||
margin-left: 10px;
|
||||
}
|
||||
&:hover > .title-icon,
|
||||
.el-sub-menu__title:hover > .title-icon {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
a {
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.el-sub-menu__title {
|
||||
> .badge {
|
||||
&-dot {
|
||||
right: 40px;
|
||||
}
|
||||
&-text {
|
||||
right: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.el-menu--inline {
|
||||
background-color: var(--g-sub-sidebar-menu-bg) !important;
|
||||
.el-menu-item,
|
||||
.el-sub-menu > .el-sub-menu__title {
|
||||
color: var(--g-sub-sidebar-menu-color);
|
||||
background-color: var(--g-sub-sidebar-menu-bg) !important;
|
||||
&:hover {
|
||||
color: var(--g-sub-sidebar-menu-hover-color) !important;
|
||||
background-color: var(--g-sub-sidebar-menu-hover-bg) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
color: var(--g-sub-sidebar-menu-color) !important;
|
||||
background: transparent !important;
|
||||
&:hover {
|
||||
color: var(--g-sub-sidebar-menu-hover-color) !important;
|
||||
background-color: var(--g-sub-sidebar-menu-hover-bg) !important;
|
||||
}
|
||||
}
|
||||
.el-menu-item.is-active,
|
||||
.el-menu--collapse .el-sub-menu.is-active > .el-sub-menu__title,
|
||||
.el-sub-menu .el-menu--inline .el-menu-item.is-active {
|
||||
color: var(--g-sub-sidebar-menu-active-color) !important;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg) !important;
|
||||
.icon,
|
||||
.el-sub-menu__icon-arrow {
|
||||
color: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,140 +0,0 @@
|
||||
<script setup name="SubSidebar">
|
||||
import Logo from '../Logo/index.vue'
|
||||
import SidebarItem from '../SidebarItem/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const sidebarScrollTop = ref(0)
|
||||
|
||||
function onSidebarScroll(e) {
|
||||
sidebarScrollTop.value = e.target.scrollTop
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="['side', 'head', 'single'].includes(settingsStore.menu.menuMode) || settingsStore.mode === 'mobile'" class="sub-sidebar-container" :class="{'is-collapse': settingsStore.mode === 'pc' && settingsStore.menu.subMenuCollapse}" @scroll="onSidebarScroll">
|
||||
<Logo
|
||||
:show-logo="settingsStore.menu.menuMode === 'single'" :class="{
|
||||
'sidebar-logo': true,
|
||||
'sidebar-logo-bg': settingsStore.menu.menuMode === 'single',
|
||||
'shadow': sidebarScrollTop
|
||||
}"
|
||||
/>
|
||||
<!-- 侧边栏模式(无主导航) -->
|
||||
<el-menu
|
||||
:unique-opened="settingsStore.menu.subMenuUniqueOpened" :default-openeds="menuStore.defaultOpenedPaths" :default-active="$route.meta.activeMenu || $route.path" :collapse="settingsStore.mode === 'pc' && settingsStore.menu.subMenuCollapse" :collapse-transition="false" :class="{
|
||||
'is-collapse-without-logo': settingsStore.menu.menuMode !== 'single' && settingsStore.menu.subMenuCollapse
|
||||
}"
|
||||
>
|
||||
<transition-group name="sub-sidebar">
|
||||
<template v-for="(route, index) in menuStore.sidebarMenus">
|
||||
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path || index" :item="route" :base-path="route.path" />
|
||||
</template>
|
||||
</transition-group>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sub-sidebar-container {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
// firefox隐藏滚动条
|
||||
scrollbar-width: none;
|
||||
// chrome隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
width: var(--g-sub-sidebar-width);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--g-sub-sidebar-bg);
|
||||
box-shadow: 10px 0 10px -10px var(--g-box-shadow-color);
|
||||
transition: background-color 0.3s, var(--el-transition-box-shadow), left 0.3s, width 0.3s;
|
||||
&.is-collapse {
|
||||
width: 64px;
|
||||
.sidebar-logo {
|
||||
&:not(.sidebar-logo-bg) {
|
||||
display: none;
|
||||
}
|
||||
:deep(span) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.sidebar-logo {
|
||||
transition: box-shadow 0.2s, background-color 0.3s, color 0.3s;
|
||||
background-color: var(--g-sub-sidebar-bg);
|
||||
&:not(.sidebar-logo-bg) {
|
||||
:deep(span) {
|
||||
color: var(--g-sub-sidebar-menu-color);
|
||||
}
|
||||
}
|
||||
&.sidebar-logo-bg {
|
||||
background-color: var(--g-main-sidebar-bg);
|
||||
}
|
||||
&.shadow {
|
||||
box-shadow: 0 10px 10px -10px var(--g-box-shadow-color);
|
||||
}
|
||||
}
|
||||
.el-menu {
|
||||
border-right: 0;
|
||||
padding-top: var(--g-sidebar-logo-height);
|
||||
transition: border-color 0.3s, background-color 0.3s, color 0.3s, padding-top 0.3s;
|
||||
background-color: var(--g-sub-sidebar-bg);
|
||||
&:not(.el-menu--collapse) {
|
||||
width: inherit;
|
||||
}
|
||||
&.is-collapse-without-logo {
|
||||
padding-top: 0;
|
||||
}
|
||||
&.el-menu--collapse {
|
||||
:deep(.title-icon) {
|
||||
margin-right: 0;
|
||||
}
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
span,
|
||||
.el-sub-menu__icon-arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.menu-radius:not(.el-menu--collapse) {
|
||||
.sidebar-item {
|
||||
padding: 0 10px;
|
||||
&:first-child {
|
||||
padding-top: 10px;
|
||||
}
|
||||
&:last-child {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
:deep(.el-menu--inline),
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 次侧边栏动画
|
||||
.sub-sidebar-enter-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
.sub-sidebar-enter-from,
|
||||
.sub-sidebar-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) skewY(10deg);
|
||||
}
|
||||
.sub-sidebar-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -1,168 +0,0 @@
|
||||
<script setup name="Tools">
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import { useMainPage } from '@/utils/composables'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const { isFullscreen, toggle } = useFullscreen()
|
||||
|
||||
function userCommand(command) {
|
||||
switch (command) {
|
||||
case 'dashboard':
|
||||
router.push({
|
||||
name: 'dashboard'
|
||||
})
|
||||
break
|
||||
case 'setting':
|
||||
router.push({
|
||||
name: 'personalSetting'
|
||||
})
|
||||
break
|
||||
case 'logout':
|
||||
userStore.logout().then(() => {
|
||||
router.push({
|
||||
name: 'login'
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
function pro() {
|
||||
window.open('https://hooray.gitee.io/fantastic-admin-pro-example/', 'top')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tools">
|
||||
<div class="buttons">
|
||||
<span v-if="settingsStore.mode == 'pc'" class="item item-pro" @click="pro">
|
||||
<el-icon>
|
||||
<svg-icon name="pro" />
|
||||
</el-icon>
|
||||
<span class="title">查看专业版</span>
|
||||
</span>
|
||||
<span v-if="settingsStore.topbar.enableNavSearch" class="item" @click="eventBus.emit('global-search-toggle')">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:search" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-if="settingsStore.mode === 'pc' && settingsStore.topbar.enableFullscreen" class="item" @click="toggle">
|
||||
<el-icon>
|
||||
<svg-icon :name="isFullscreen ? 'fullscreen-exit' : 'fullscreen'" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-if="settingsStore.topbar.enablePageReload" class="item" @click="useMainPage().reload()">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:refresh-right" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-if="settingsStore.topbar.enableColorScheme" class="item" @click="settingsStore.setColorScheme(settingsStore.app.colorScheme === 'dark' ? 'light' : 'dark')">
|
||||
<el-icon>
|
||||
<svg-icon v-show="settingsStore.app.colorScheme === 'light'" name="ep:sunny" />
|
||||
<svg-icon v-show="settingsStore.app.colorScheme === 'dark'" name="ep:moon" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-if="settingsStore.topbar.enableAppSetting" class="item" @click="eventBus.emit('global-theme-toggle')">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:setting" />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
<el-dropdown class="user-container" size="default" @command="userCommand">
|
||||
<div class="user-wrapper">
|
||||
<el-avatar size="small">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:user-filled" />
|
||||
</el-icon>
|
||||
</el-avatar>
|
||||
{{ userStore.account }}
|
||||
<el-icon>
|
||||
<svg-icon name="ep:caret-bottom" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="user-dropdown">
|
||||
<el-dropdown-item v-if="settingsStore.dashboard.enable" command="dashboard">控制台</el-dropdown-item>
|
||||
<el-dropdown-item command="setting">个人设置</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
white-space: nowrap;
|
||||
.buttons {
|
||||
margin-right: 20px;
|
||||
.item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 24px;
|
||||
width: 34px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
.el-icon {
|
||||
color: var(--el-text-color-primary);
|
||||
transition: var(--el-transition-color);
|
||||
}
|
||||
}
|
||||
.item-pro {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
padding: 0 10px;
|
||||
transform-origin: right center;
|
||||
animation: pro-text 3s ease-out infinite;
|
||||
@keyframes pro-text {
|
||||
0%,
|
||||
20% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50%,
|
||||
70% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.el-icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.title {
|
||||
padding-left: 5px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
background-image: linear-gradient(to right, #ffa237, #fc455d);
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.user-container) {
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
.user-wrapper {
|
||||
.el-avatar {
|
||||
vertical-align: middle;
|
||||
margin-top: -2px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,142 +0,0 @@
|
||||
<script setup name="Topbar">
|
||||
import { compile } from 'path-to-regexp'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import Tools from '../Tools/index.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const enableSidebarCollapse = computed(() => {
|
||||
return settingsStore.mode === 'mobile' || (
|
||||
['side', 'head', 'single'].includes(settingsStore.menu.menuMode) &&
|
||||
settingsStore.topbar.enableSidebarCollapse
|
||||
)
|
||||
})
|
||||
|
||||
const breadcrumbList = computed(() => {
|
||||
let breadcrumbList = []
|
||||
if (settingsStore.dashboard.enable) {
|
||||
breadcrumbList.push({
|
||||
path: '/dashboard',
|
||||
title: settingsStore.dashboard.title
|
||||
})
|
||||
}
|
||||
if (route.meta.breadcrumbNeste) {
|
||||
breadcrumbList.push(...route.meta.breadcrumbNeste.filter(item => item.hide === false))
|
||||
}
|
||||
return breadcrumbList
|
||||
})
|
||||
|
||||
const scrollTop = ref(0)
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', onScroll)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
})
|
||||
function onScroll() {
|
||||
scrollTop.value = document.documentElement.scrollTop || document.body.scrollTop
|
||||
}
|
||||
|
||||
function pathCompile(path) {
|
||||
let toPath = compile(path)
|
||||
return toPath(route.params)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="topbar-container" :class="{
|
||||
'fixed': settingsStore.topbar.fixed,
|
||||
'shadow': scrollTop
|
||||
}" data-fixed-calc-width
|
||||
>
|
||||
<div class="left-box">
|
||||
<div v-if="enableSidebarCollapse" class="sidebar-collapse" :class="{'is-collapse': settingsStore.menu.subMenuCollapse}" @click="settingsStore.toggleSidebarCollapse()">
|
||||
<el-icon>
|
||||
<svg-icon name="toolbar-collapse" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-breadcrumb v-if="settingsStore.topbar.enableBreadcrumb && settingsStore.mode === 'pc' && settingsStore.app.routeBaseOn !== 'filesystem'">
|
||||
<transition-group name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="item.path" :to="index < breadcrumbList.length - 1 ? pathCompile(item.path) : ''">
|
||||
{{ item.title }}
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<Tools />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.topbar-container {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: var(--g-topbar-height);
|
||||
background-color: var(--g-toolbar-bg);
|
||||
transition: width 0.3s, top 0.3s, transform 0.3s, background-color 0.3s, var(--el-transition-box-shadow);
|
||||
&.fixed {
|
||||
position: fixed;
|
||||
&.shadow {
|
||||
box-shadow: 0 10px 10px -10px var(--g-box-shadow-color);
|
||||
}
|
||||
}
|
||||
.left-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 50px;
|
||||
overflow: hidden;
|
||||
-webkit-mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 50px), transparent);
|
||||
mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 50px), transparent);
|
||||
.sidebar-collapse {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
.el-icon {
|
||||
color: var(--el-text-color-primary);
|
||||
transition: var(--el-transition-color), var(--el-transition-md-fade);
|
||||
}
|
||||
&:hover .el-icon {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
&.is-collapse .el-icon {
|
||||
transform: rotateZ(-180deg);
|
||||
}
|
||||
& + .el-breadcrumb {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
:deep(.el-breadcrumb) {
|
||||
margin-left: 20px;
|
||||
white-space: nowrap;
|
||||
.el-breadcrumb__item {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
span {
|
||||
font-weight: normal;
|
||||
}
|
||||
&:last-child span {
|
||||
color: #97a8be;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 面包屑动画
|
||||
.breadcrumb-enter-active {
|
||||
transition: all 0.25s;
|
||||
}
|
||||
.breadcrumb-enter-from,
|
||||
.breadcrumb-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(30px) skewX(-50deg);
|
||||
}
|
||||
</style>
|
||||
@@ -1,210 +0,0 @@
|
||||
<script setup name="Layout">
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import Header from './components/Header/index.vue'
|
||||
import MainSidebar from './components/MainSidebar/index.vue'
|
||||
import SubSidebar from './components/SubSidebar/index.vue'
|
||||
import Topbar from './components/Topbar/index.vue'
|
||||
import Search from './components/Search/index.vue'
|
||||
import AppSetting from './components/AppSetting/index.vue'
|
||||
import BuyIt from './components/BuyIt/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useKeepAliveStore from '@/store/modules/keepAlive'
|
||||
import { useMainPage } from '@/utils/composables'
|
||||
|
||||
const routeInfo = useRoute()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const keepAliveStore = useKeepAliveStore()
|
||||
|
||||
const showCopyright = computed(() => {
|
||||
return typeof routeInfo.meta.copyright !== 'undefined' ? routeInfo.meta.copyright : settingsStore.copyright.enable
|
||||
})
|
||||
|
||||
watch(() => settingsStore.menu.subMenuCollapse, val => {
|
||||
if (settingsStore.mode === 'mobile') {
|
||||
if (!val) {
|
||||
document.querySelector('body').classList.add('hidden')
|
||||
} else {
|
||||
document.querySelector('body').classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => routeInfo.path, () => {
|
||||
if (settingsStore.mode === 'mobile') {
|
||||
settingsStore.$patch(state => {
|
||||
state.menu.subMenuCollapse = true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
hotkeys('f5', e => {
|
||||
if (settingsStore.topbar.enablePageReload) {
|
||||
e.preventDefault()
|
||||
useMainPage().reload()
|
||||
}
|
||||
})
|
||||
})
|
||||
onUnmounted(() => {
|
||||
hotkeys.unbind('f5')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div id="app-main">
|
||||
<Header />
|
||||
<div class="wrapper">
|
||||
<div class="sidebar-container" :class="{'show': settingsStore.mode === 'mobile' && !settingsStore.menu.subMenuCollapse}">
|
||||
<MainSidebar />
|
||||
<SubSidebar />
|
||||
</div>
|
||||
<div class="sidebar-mask" :class="{'show': settingsStore.mode === 'mobile' && !settingsStore.menu.subMenuCollapse}" @click="settingsStore.toggleSidebarCollapse()" />
|
||||
<div class="main-container" :style="{'padding-bottom': $route.meta.paddingBottom}">
|
||||
<Topbar v-if="!(settingsStore.menu.menuMode === 'head' && !settingsStore.topbar.enableSidebarCollapse && !settingsStore.topbar.enableBreadcrumb)" />
|
||||
<div class="main">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="main" mode="out-in" appear>
|
||||
<keep-alive :include="keepAliveStore.list">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
<Copyright v-if="showCopyright" />
|
||||
</div>
|
||||
</div>
|
||||
<el-backtop :right="20" :bottom="20" title="回到顶部" />
|
||||
</div>
|
||||
<Search />
|
||||
<AppSetting />
|
||||
<BuyIt />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
[data-mode="mobile"] {
|
||||
.sidebar-container {
|
||||
transform: translateX(calc((var(--g-main-sidebar-width) + var(--g-sub-sidebar-width)) * -1));
|
||||
&.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
.main-container {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
&[data-menu-mode="single"] {
|
||||
.sidebar-container {
|
||||
transform: translateX(calc(var(--g-sub-sidebar-width) * -1));
|
||||
&.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.layout {
|
||||
height: 100%;
|
||||
}
|
||||
#app-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: -1px 0 0 0 var(--g-box-shadow-color);
|
||||
transition: padding-top 0.3s;
|
||||
.sidebar-container {
|
||||
position: fixed;
|
||||
z-index: 1010;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
transition: transform 0.3s, top 0.3s;
|
||||
width: calc(var(--g-main-sidebar-actual-width) + var(--g-sub-sidebar-actual-width));
|
||||
}
|
||||
.sidebar-mask {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgb(0 0 0 / 50%);
|
||||
backdrop-filter: blur(2px);
|
||||
transition: all 0.2s;
|
||||
transform: translateZ(0);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
&.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
.main-sidebar-container:not(.main-sidebar-leave-active) + .sub-sidebar-container {
|
||||
left: var(--g-main-sidebar-width);
|
||||
}
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
margin-left: calc(var(--g-main-sidebar-actual-width) + var(--g-sub-sidebar-actual-width));
|
||||
background-color: var(--g-main-bg);
|
||||
transition: margin-left 0.3s, background-color 0.3s;
|
||||
.topbar-container {
|
||||
top: 0;
|
||||
z-index: 998;
|
||||
}
|
||||
.main {
|
||||
height: 100%;
|
||||
flex: auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.topbar-container + .main {
|
||||
margin: var(--g-topbar-height) 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
header:not(.header-leave-active) + .wrapper {
|
||||
padding-top: var(--g-header-height);
|
||||
.sidebar-container {
|
||||
top: var(--g-header-height);
|
||||
:deep(.sidebar-logo) {
|
||||
display: none;
|
||||
}
|
||||
:deep(.el-menu) {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
.main-container {
|
||||
.topbar-container {
|
||||
top: var(--g-header-height);
|
||||
:deep(.tools) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主内容区动画
|
||||
.main-enter-active {
|
||||
transition: 0.2s;
|
||||
}
|
||||
.main-leave-active {
|
||||
transition: 0.15s;
|
||||
}
|
||||
.main-enter-from {
|
||||
opacity: 0;
|
||||
margin-left: -20px;
|
||||
}
|
||||
.main-leave-to {
|
||||
opacity: 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,532 @@
|
||||
<script lang="ts" setup name="AppSetting">
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import globalSettingsDefault from '@/settings.default'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const isShow = ref(false)
|
||||
|
||||
const settings = ref(globalSettingsDefault)
|
||||
|
||||
watch(() => settings, () => {
|
||||
settingsStore.updateSettings(settings.value)
|
||||
menuStore.setActived(0)
|
||||
settings.value.menu.menuMode !== 'single' && menuStore.setActived(route.fullPath)
|
||||
}, {
|
||||
deep: true,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on('global-theme-toggle', () => {
|
||||
isShow.value = !isShow.value
|
||||
})
|
||||
})
|
||||
|
||||
const { copy, copied, isSupported } = useClipboard()
|
||||
|
||||
watch(copied, (val) => {
|
||||
if (val)
|
||||
ElMessage.success('复制成功,请粘贴到 src/settings.ts 文件中!')
|
||||
})
|
||||
|
||||
function handleCopy() {
|
||||
copy(JSON.stringify(settings.value, null, 4))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer v-model="isShow" title="应用配置" direction="rtl" :size="360">
|
||||
<el-alert title="应用配置可实时预览效果,但只是临时生效,要想真正作用于项目,可以点击下方的“复制配置”按钮,并将配置粘贴到 src/settings.ts 文件中。同时建议在生产环境隐藏应用配置功能。" type="error" :closable="false" />
|
||||
<el-divider>颜色主题</el-divider>
|
||||
<div class="color-scheme">
|
||||
<div class="switch" :class="settings.app.colorScheme" @click="settings.app.colorScheme = settings.app.colorScheme === 'dark' ? 'light' : 'dark'">
|
||||
<el-icon class="icon">
|
||||
<svg-icon :name="settings.app.colorScheme === 'light' ? 'ep:sunny' : 'ep:moon'" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider v-if="settingsStore.mode === 'pc'">
|
||||
导航栏模式
|
||||
</el-divider>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="menu-mode">
|
||||
<el-tooltip content="侧边栏模式(含主导航)" placement="top" :show-after="500">
|
||||
<div class="mode mode-side" :class="{ active: settings.menu.menuMode === 'side' }" @click="settings.menu.menuMode = 'side'">
|
||||
<div class="mode-container" />
|
||||
<el-icon>
|
||||
<svg-icon name="ep:check" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="顶部模式" placement="top" :show-after="500">
|
||||
<div class="mode mode-head" :class="{ active: settings.menu.menuMode === 'head' }" @click="settings.menu.menuMode = 'head'">
|
||||
<div class="mode-container" />
|
||||
<el-icon>
|
||||
<svg-icon name="ep:check" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="侧边栏模式(不含主导航)" placement="top" :show-after="500">
|
||||
<div class="mode mode-single" :class="{ active: settings.menu.menuMode === 'single' }" @click="settings.menu.menuMode = 'single'">
|
||||
<div class="mode-container" />
|
||||
<el-icon>
|
||||
<svg-icon name="ep:check" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-divider>导航栏</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
主导航切换跳转
|
||||
<el-tooltip content="开启该功能后,切换主导航时,页面自动跳转至该主导航下,次导航里第一个导航" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.menu.switchMainMenuAndPageJump" :disabled="['single'].includes(settings.menu.menuMode)" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
次导航保持展开一个
|
||||
<el-tooltip content="开启该功能后,次导航只保持单个菜单的展开" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.menu.subMenuUniqueOpened" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
次导航是否折叠
|
||||
</div>
|
||||
<el-switch v-model="settings.menu.subMenuCollapse" />
|
||||
</div>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
|
||||
<div class="label">
|
||||
显示次导航折叠按钮
|
||||
</div>
|
||||
<el-switch v-model="settings.menu.enableSubMenuCollapseButton" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用快捷键
|
||||
</div>
|
||||
<el-switch v-model="settings.menu.enableHotkeys" :disabled="['single'].includes(settingsStore.menu.menuMode)" />
|
||||
</div>
|
||||
<el-divider>顶栏</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否固定
|
||||
</div>
|
||||
<el-switch v-model="settings.topbar.fixed" />
|
||||
</div>
|
||||
<el-divider>工具栏</el-divider>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
|
||||
<div class="label">
|
||||
全屏
|
||||
<el-tooltip content="该功能使用场景极少,用户习惯于通过窗口“最大化”功能来扩大显示区域,以显示更多内容,并且使用 F11 键也可以进入全屏效果" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.toolbar.enableFullscreen" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
页面刷新
|
||||
<el-tooltip content="开启时会阻止原生 F5 键刷新功能,并采用框架提供的刷新模式进行页面刷新" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.toolbar.enablePageReload" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
颜色主题
|
||||
<el-tooltip content="开启后可在明亮/暗黑模式中切换" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.toolbar.enableColorScheme" />
|
||||
</div>
|
||||
<el-divider v-if="settingsStore.mode === 'pc'">
|
||||
面包屑导航
|
||||
</el-divider>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
|
||||
<div class="label">
|
||||
是否启用
|
||||
</div>
|
||||
<el-switch v-model="settings.breadcrumb.enable" />
|
||||
</div>
|
||||
<el-divider>导航搜索</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用
|
||||
<el-tooltip content="对导航进行快捷搜索" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.navSearch.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用快捷键
|
||||
</div>
|
||||
<el-switch v-model="settings.navSearch.enableHotkeys" :disabled="!settings.navSearch.enable" />
|
||||
</div>
|
||||
<el-divider>底部版权</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用
|
||||
</div>
|
||||
<el-switch v-model="settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
日期
|
||||
</div>
|
||||
<el-input v-model="settings.copyright.dates" size="small" :disabled="!settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
公司
|
||||
</div>
|
||||
<el-input v-model="settings.copyright.company" size="small" :disabled="!settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
网址
|
||||
</div>
|
||||
<el-input v-model="settings.copyright.website" size="small" :disabled="!settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
备案
|
||||
</div>
|
||||
<el-input v-model="settings.copyright.beian" size="small" :disabled="!settings.copyright.enable" />
|
||||
</div>
|
||||
<el-divider>控制台</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否开启
|
||||
<el-tooltip content="该功能开启时,登录成功默认进入控制台页面,反之则默认进入导航栏里第一个导航页面" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.dashboard.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
控制台名称
|
||||
</div>
|
||||
<el-input v-model="settings.dashboard.title" size="small" />
|
||||
</div>
|
||||
<el-divider>其它</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
组件尺寸
|
||||
<el-tooltip content="全局设置 Element Plus 组件的默认尺寸大小" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-radio-group v-model="settings.app.elementSize" size="small">
|
||||
<el-radio-button label="large">
|
||||
较大
|
||||
</el-radio-button>
|
||||
<el-radio-button label="default">
|
||||
默认
|
||||
</el-radio-button>
|
||||
<el-radio-button label="small">
|
||||
稍小
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用权限
|
||||
</div>
|
||||
<el-switch v-model="settings.app.enablePermission" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
载入进度条
|
||||
<el-tooltip content="该功能开启时,跳转路由会看到页面顶部有进度条" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.app.enableProgress" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
动态标题
|
||||
<el-tooltip content="该功能开启时,页面标题会显示当前路由标题,格式为“页面标题 - 网站名称”;关闭时则显示网站名称,网站名称在项目根目录下 .env.* 文件里配置" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.app.enableDynamicTitle" />
|
||||
</div>
|
||||
<template v-if="isSupported" #footer>
|
||||
<el-button type="primary" @click="handleCopy">
|
||||
<template #icon>
|
||||
<el-icon>
|
||||
<svg-icon name="ep:document-copy" />
|
||||
</el-icon>
|
||||
</template>
|
||||
复制配置
|
||||
</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: initial;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
:deep(.el-drawer__footer) {
|
||||
padding: 20px;
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
transition: var(--el-transition-border);
|
||||
|
||||
.el-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-divider) {
|
||||
margin: 36px 0 24px;
|
||||
}
|
||||
|
||||
.color-scheme {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 10px;
|
||||
|
||||
$width: 50px;
|
||||
|
||||
.switch {
|
||||
width: $width;
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
cursor: pointer;
|
||||
background-color: var(--el-fill-color-darker);
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&.dark {
|
||||
.icon {
|
||||
margin-left: calc($width - 24px - 3px);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 3px;
|
||||
padding: 5px;
|
||||
font-size: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
transition: margin-left 0.3s, background-color 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-mode {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.mode {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 55px;
|
||||
margin: 10px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background-color: var(--g-app-bg);
|
||||
box-shadow: 0 0 5px 1px var(--el-border-color-lighter);
|
||||
transition: 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 5px 1px var(--el-border-color-darker);
|
||||
}
|
||||
|
||||
&.active {
|
||||
box-shadow: 0 0 0 2px var(--el-color-primary);
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after,
|
||||
.mode-container {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mode-container::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
&-side {
|
||||
&::before {
|
||||
content: "";
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
bottom: 5px;
|
||||
width: 10px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
top: 5px;
|
||||
left: 20px;
|
||||
bottom: 5px;
|
||||
width: 15px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.mode-container {
|
||||
top: 5px;
|
||||
left: 40px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&-head {
|
||||
&::before {
|
||||
content: "";
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
height: 10px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
top: 20px;
|
||||
left: 5px;
|
||||
bottom: 5px;
|
||||
width: 15px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.mode-container {
|
||||
top: 20px;
|
||||
left: 25px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&-single {
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
bottom: 5px;
|
||||
width: 15px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.mode-container {
|
||||
top: 5px;
|
||||
left: 25px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.active i {
|
||||
display: block;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 5px 0;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-left: 4px;
|
||||
font-size: 17px;
|
||||
color: var(--el-color-warning);
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
|
||||
.el-switch {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<script lang="ts" setup name="BuyIt">
|
||||
const isActived = ref(true)
|
||||
setTimeout(() => {
|
||||
isActived.value = false
|
||||
}, 5000)
|
||||
|
||||
onMounted(() => {
|
||||
ElNotification({
|
||||
type: 'success',
|
||||
title: '温馨提示',
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: `
|
||||
<p>当前访问的是<b>基础版</b> (Vue3)</p>
|
||||
<p>你可以点<a href="https://hooray.gitee.io/fantastic-admin-pro-example/" target="_blank"><b>这里</b></a>访问专业版 (Vue3)</p>
|
||||
`,
|
||||
position: 'bottom-right',
|
||||
duration: 5000,
|
||||
})
|
||||
})
|
||||
|
||||
function open(url: string) {
|
||||
window.open(url, 'top')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="buy-it" :class="{ actived: isActived }">
|
||||
<div class="item" @click="open(`https://hooray.gitee.io/fantastic-admin/buy.html`)">
|
||||
<el-icon>
|
||||
<svg-icon name="fixed-right-buy" />
|
||||
</el-icon>
|
||||
<span class="title">购买<br>专业版</span>
|
||||
</div>
|
||||
<div class="item" @click="open('https://gitee.com/hooray/fantastic-admin/')">
|
||||
<el-icon>
|
||||
<svg-icon name="fixed-right-code" />
|
||||
</el-icon>
|
||||
<span class="title">下载<br>基础版</span>
|
||||
</div>
|
||||
<div class="item" @click="open(`https://hooray.gitee.io/fantastic-admin/`)">
|
||||
<el-icon>
|
||||
<svg-icon name="fixed-right-doc" />
|
||||
</el-icon>
|
||||
<span class="title">开发<br>文档</span>
|
||||
</div>
|
||||
<div class="item" @click="open(`https://hooray.gitee.io/fantastic-admin/support.html`)">
|
||||
<el-icon>
|
||||
<svg-icon name="fixed-right-chat" />
|
||||
</el-icon>
|
||||
<span class="title">技术<br>支持</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.buy-it {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
right: -58px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: right 0.3s;
|
||||
|
||||
&.actived,
|
||||
&:hover {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 70px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
border-bottom: 1px solid #fff;
|
||||
transition: 0.3s;
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
background-color: #ff4200;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
background-color: #0fcc1a;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
background-color: #343b42;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,191 @@
|
||||
<script lang="ts" setup name="Header">
|
||||
import Logo from '../Logo/index.vue'
|
||||
import Tools from '../Tools/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const { switchTo } = useMenu()
|
||||
|
||||
const navRef = ref()
|
||||
|
||||
// 顶部模式鼠标滚动
|
||||
function handlerMouserScroll(event: WheelEvent) {
|
||||
navRef.value.scrollBy({
|
||||
left: (event.deltaY || event.detail) > 0 ? 50 : -50,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="header">
|
||||
<header v-if="settingsStore.mode === 'pc' && settingsStore.menu.menuMode === 'head'">
|
||||
<div class="header-container">
|
||||
<div class="main">
|
||||
<Logo />
|
||||
<!-- 顶部模式 -->
|
||||
<div ref="navRef" class="nav" @wheel.prevent="handlerMouserScroll">
|
||||
<template v-for="(item, index) in menuStore.allMenus" :key="index">
|
||||
<div v-if="item.children && item.children.length !== 0" class="item-container" :class="{ active: index === menuStore.actived }">
|
||||
<div class="item" @click="switchTo(index)">
|
||||
<el-icon>
|
||||
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span v-if="item.meta.title">{{ item.meta.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<Tools />
|
||||
</div>
|
||||
</header>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
header {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: var(--g-header-height);
|
||||
color: var(--g-header-color);
|
||||
background-color: var(--g-header-bg);
|
||||
transition: background-color 0.3s, var(--el-transition-color);
|
||||
|
||||
.header-container {
|
||||
width: var(--g-header-width);
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: var(--g-header-width)) {
|
||||
.header-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.title) {
|
||||
position: relative;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
padding: inherit;
|
||||
background-color: inherit;
|
||||
|
||||
.logo {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 24px;
|
||||
letter-spacing: 1px;
|
||||
color: var(--g-header-color);
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
margin: 0 30px;
|
||||
padding: 0 20px;
|
||||
overflow-x: auto;
|
||||
mask-image: linear-gradient(to right, transparent, #000 20px, #000 calc(100% - 20px), transparent);
|
||||
|
||||
// firefox隐藏滚动条
|
||||
scrollbar-width: none;
|
||||
|
||||
// chrome隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: initial;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 0 5px;
|
||||
width: 80px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
color: var(--g-header-menu-color);
|
||||
background-color: var(--g-header-bg);
|
||||
transition: background-color 0.3s, var(--el-transition-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--g-header-menu-hover-color);
|
||||
background-color: var(--g-header-menu-hover-bg);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
span {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
word-break: break-all;
|
||||
|
||||
@include text-overflow(1, false);
|
||||
}
|
||||
}
|
||||
|
||||
&.active .item {
|
||||
color: var(--g-header-menu-active-color);
|
||||
background-color: var(--g-header-menu-active-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.tools) {
|
||||
padding: 0;
|
||||
|
||||
.buttons .item .el-icon {
|
||||
color: var(--g-header-color);
|
||||
}
|
||||
|
||||
.user-container {
|
||||
font-size: 16px;
|
||||
color: var(--g-header-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 头部动画
|
||||
.header-enter-active,
|
||||
.header-leave-active {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.header-enter-from,
|
||||
.header-leave-to {
|
||||
transform: translateY(calc(var(--g-header-height) * -1));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
const isShow = ref(false)
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on('global-hotkeys-intro-toggle', () => {
|
||||
isShow.value = !isShow.value
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer v-model="isShow" title="快捷键介绍" direction="rtl" :size="360">
|
||||
<el-descriptions title="全局" :column="1" border>
|
||||
<el-descriptions-item label="查看系统信息">
|
||||
Alt + I
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="settingsStore.navSearch.enable && settingsStore.navSearch.enableHotkeys" label="唤起导航搜索">
|
||||
Alt + S
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions v-if="settingsStore.menu.enableHotkeys && ['side', 'head'].includes(settingsStore.menu.menuMode)" title="主导航" :column="1" border>
|
||||
<el-descriptions-item label="激活下一个主导航">
|
||||
Alt + `
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: initial;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
transition: var(--el-transition-border);
|
||||
}
|
||||
|
||||
:deep(.el-descriptions) {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.el-descriptions__label {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script lang="ts" setup name="Logo">
|
||||
import imgLogo from '@/assets/images/logo.png'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
defineProps({
|
||||
showLogo: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showTitle: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const title = ref(import.meta.env.VITE_APP_TITLE)
|
||||
const logo = ref(imgLogo)
|
||||
|
||||
const to = computed(() => {
|
||||
const rtn: {
|
||||
name?: string
|
||||
} = {}
|
||||
if (settingsStore.dashboard.enable)
|
||||
rtn.name = 'dashboard'
|
||||
|
||||
return rtn
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link :to="to" class="title" :class="{ 'is-link': settingsStore.dashboard.enable }" :title="title">
|
||||
<img v-if="showLogo" :src="logo" class="logo">
|
||||
<span v-if="showTitle">{{ title }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
width: inherit;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: var(--g-sidebar-logo-height);
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
|
||||
&.is-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
|
||||
& + span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
|
||||
@include text-overflow;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<script lang="ts" setup name="MainSidebar">
|
||||
import Logo from '../Logo/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const { switchTo } = useMenu()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="main-sidebar">
|
||||
<div v-if="settingsStore.menu.menuMode === 'side' || (settingsStore.mode === 'mobile' && settingsStore.menu.menuMode !== 'single')" class="main-sidebar-container">
|
||||
<Logo :show-title="false" class="sidebar-logo" />
|
||||
<!-- 侧边栏模式(含主导航) -->
|
||||
<div class="nav">
|
||||
<template v-for="(item, index) in menuStore.allMenus">
|
||||
<div
|
||||
v-if="item.children && item.children.length !== 0" :key="index" class="item" :class="{
|
||||
active: index === menuStore.actived,
|
||||
}" :title="item.meta.title" @click="switchTo(index)"
|
||||
>
|
||||
<el-icon>
|
||||
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span>{{ item.meta.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-sidebar-container {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
// firefox隐藏滚动条
|
||||
scrollbar-width: none;
|
||||
|
||||
// chrome隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: var(--g-main-sidebar-width);
|
||||
color: var(--g-main-sidebar-menu-color);
|
||||
background-color: var(--g-main-sidebar-bg);
|
||||
transition: background-color 0.3s, var(--el-transition-color);
|
||||
|
||||
.sidebar-logo {
|
||||
transition: 0.3s;
|
||||
background-color: var(--g-main-sidebar-bg);
|
||||
}
|
||||
|
||||
.nav {
|
||||
width: inherit;
|
||||
padding-top: var(--g-sidebar-logo-height);
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
height: 60px;
|
||||
padding: 0 5px;
|
||||
cursor: pointer;
|
||||
color: var(--g-main-sidebar-menu-color);
|
||||
background-color: var(--g-main-sidebar-bg);
|
||||
transition: background-color 0.3s, var(--el-transition-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--g-main-sidebar-menu-hover-color);
|
||||
background-color: var(--g-main-sidebar-menu-hover-bg);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--g-main-sidebar-menu-active-color);
|
||||
background-color: var(--g-main-sidebar-menu-active-bg);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin: 0 auto;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
span {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
|
||||
@include text-overflow(1, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主侧边栏动画
|
||||
.main-sidebar-enter-active,
|
||||
.main-sidebar-leave-active {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.main-sidebar-enter-from,
|
||||
.main-sidebar-leave-to {
|
||||
transform: translateX(calc(var(--g-main-sidebar-width) * -1));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,448 @@
|
||||
<script lang="ts" setup name="Search">
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import { isExternalLink } from '@/utils'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useRouteStore from '@/store/modules/route'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import type { Menu, Route } from '@/global'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const routeStore = useRouteStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
interface listTypes {
|
||||
icon?: string
|
||||
title: string
|
||||
breadcrumb: string[]
|
||||
path: string
|
||||
}
|
||||
|
||||
const isShow = ref(false)
|
||||
const searchInput = ref('')
|
||||
const sourceList = ref<listTypes[]>([])
|
||||
const actived = ref(-1)
|
||||
|
||||
const searchInputRef = ref()
|
||||
const searchResultRef = ref()
|
||||
const searchResultItemRef = ref<any>([])
|
||||
const setSearchResultItemRef = (el: any) => searchResultItemRef.value.push(el)
|
||||
onBeforeUpdate(() => {
|
||||
searchResultItemRef.value = []
|
||||
})
|
||||
|
||||
const resultList = computed(() => {
|
||||
let result = []
|
||||
result = sourceList.value.filter((item) => {
|
||||
let flag = false
|
||||
if (item.title.includes(searchInput.value))
|
||||
flag = true
|
||||
|
||||
if (item.path.includes(searchInput.value))
|
||||
flag = true
|
||||
|
||||
if (item.breadcrumb.some(b => b.includes(searchInput.value)))
|
||||
flag = true
|
||||
|
||||
return flag
|
||||
})
|
||||
return result
|
||||
})
|
||||
|
||||
watch(() => isShow.value, (val) => {
|
||||
if (val) {
|
||||
document.body.classList.add('hidden')
|
||||
searchResultRef.value.scrollTop = 0
|
||||
// 当搜索显示的时候绑定上、下、回车快捷键,隐藏的时候再解绑。另外当 input 处于 focus 状态时,采用 vue 来绑定键盘事件
|
||||
hotkeys('up', keyUp)
|
||||
hotkeys('down', keyDown)
|
||||
hotkeys('enter', keyEnter)
|
||||
setTimeout(() => {
|
||||
searchInputRef.value.focus()
|
||||
}, 500)
|
||||
}
|
||||
else {
|
||||
document.body.classList.remove('hidden')
|
||||
hotkeys.unbind('up', keyUp)
|
||||
hotkeys.unbind('down', keyDown)
|
||||
hotkeys.unbind('enter', keyEnter)
|
||||
setTimeout(() => {
|
||||
searchInput.value = ''
|
||||
actived.value = -1
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
watch(() => resultList.value, () => {
|
||||
actived.value = -1
|
||||
searchResultItemRef.value = []
|
||||
handleScroll()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on('global-search-toggle', () => {
|
||||
isShow.value = !isShow.value
|
||||
})
|
||||
hotkeys('alt+s', (e) => {
|
||||
if (settingsStore.navSearch.enable && settingsStore.navSearch.enableHotkeys) {
|
||||
e.preventDefault()
|
||||
isShow.value = true
|
||||
}
|
||||
})
|
||||
hotkeys('esc', (e) => {
|
||||
if (settingsStore.navSearch.enable && settingsStore.navSearch.enableHotkeys) {
|
||||
e.preventDefault()
|
||||
isShow.value = false
|
||||
}
|
||||
})
|
||||
if (settingsStore.app.routeBaseOn !== 'filesystem') {
|
||||
routeStore.routes.forEach((item) => {
|
||||
item.children && getSourceList(item.children)
|
||||
})
|
||||
}
|
||||
else {
|
||||
menuStore.menus.forEach((item) => {
|
||||
getSourceListByMenus(item.children)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function hasChildren(item: Route.recordRaw) {
|
||||
let flag = true
|
||||
if (item.children && item.children.every(i => i.meta.sidebar === false))
|
||||
flag = false
|
||||
|
||||
return flag
|
||||
}
|
||||
function getSourceList(arr: Route.recordRaw[], basePath?: string, icon?: string, breadcrumb?: string[]) {
|
||||
arr.forEach((item) => {
|
||||
if (item.meta.sidebar !== false) {
|
||||
const breadcrumbTemp = cloneDeep(breadcrumb) || []
|
||||
if (item.children && hasChildren(item)) {
|
||||
breadcrumbTemp.push(typeof item.meta.title === 'function' ? item.meta.title() : item.meta.title)
|
||||
getSourceList(item.children, basePath ? [basePath, item.path].join('/') : item.path, item.meta.icon || icon, breadcrumbTemp)
|
||||
}
|
||||
else {
|
||||
breadcrumbTemp.push(typeof item.meta.title === 'function' ? item.meta.title() : item.meta.title)
|
||||
let path = ''
|
||||
if (isExternalLink(item.path))
|
||||
path = item.path
|
||||
|
||||
else
|
||||
path = basePath ? [basePath, item.path].join('/') : item.path
|
||||
|
||||
sourceList.value.push({
|
||||
icon: item.meta.icon || icon,
|
||||
title: typeof item.meta.title === 'function' ? item.meta.title() : item.meta.title,
|
||||
breadcrumb: breadcrumbTemp,
|
||||
path,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
function getSourceListByMenus(arr: Menu.recordRaw[], icon?: string, breadcrumb?: string[]) {
|
||||
arr.forEach((item) => {
|
||||
const breadcrumbTemp = cloneDeep(breadcrumb) || []
|
||||
if (item.children && item.children.length > 0) {
|
||||
breadcrumbTemp.push(item.meta.title)
|
||||
getSourceListByMenus(item.children, item.meta.icon, breadcrumbTemp)
|
||||
}
|
||||
else {
|
||||
breadcrumbTemp.push(item.meta.title)
|
||||
sourceList.value.push({
|
||||
icon: item.meta.icon || icon,
|
||||
title: item.meta.title,
|
||||
path: item.path as string,
|
||||
breadcrumb: breadcrumbTemp,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function keyUp() {
|
||||
if (resultList.value.length) {
|
||||
actived.value -= 1
|
||||
if (actived.value < 0)
|
||||
actived.value = resultList.value.length - 1
|
||||
|
||||
handleScroll()
|
||||
}
|
||||
}
|
||||
function keyDown() {
|
||||
if (resultList.value.length) {
|
||||
actived.value += 1
|
||||
if (actived.value > resultList.value.length - 1)
|
||||
actived.value = 0
|
||||
|
||||
handleScroll()
|
||||
}
|
||||
}
|
||||
function keyEnter() {
|
||||
if (actived.value !== -1)
|
||||
searchResultItemRef.value[actived.value].click()
|
||||
}
|
||||
function handleScroll() {
|
||||
let scrollTo = 0
|
||||
if (actived.value !== -1) {
|
||||
scrollTo = searchResultRef.value.scrollTop
|
||||
const activedOffsetTop = searchResultItemRef.value[actived.value].offsetTop
|
||||
const activedClientHeight = searchResultItemRef.value[actived.value].clientHeight
|
||||
const searchScrollTop = searchResultRef.value.scrollTop
|
||||
const searchClientHeight = searchResultRef.value.clientHeight
|
||||
if (activedOffsetTop + activedClientHeight > searchScrollTop + searchClientHeight)
|
||||
scrollTo = activedOffsetTop + activedClientHeight - searchClientHeight
|
||||
|
||||
else if (activedOffsetTop <= searchScrollTop)
|
||||
scrollTo = activedOffsetTop
|
||||
}
|
||||
searchResultRef.value.scrollTo({
|
||||
top: scrollTo,
|
||||
})
|
||||
}
|
||||
|
||||
function pageJump(url: string) {
|
||||
if (isExternalLink(url))
|
||||
window.open(url, 'top')
|
||||
|
||||
else
|
||||
router.push(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="search" :class="{ searching: isShow }" @click="isShow && eventBus.emit('global-search-toggle')">
|
||||
<div class="container">
|
||||
<div class="search-box" @click.stop>
|
||||
<el-input ref="searchInputRef" v-model="searchInput" placeholder="搜索页面,支持标题、URL模糊查询" clearable @keydown.esc="eventBus.emit('global-search-toggle')" @keydown.up.prevent="keyUp" @keydown.down.prevent="keyDown" @keydown.enter.prevent="keyEnter">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<svg-icon name="ep:search" />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="tips">
|
||||
<div class="tip">
|
||||
<span>Alt</span>+<span>S</span>
|
||||
唤醒搜索面板
|
||||
</div>
|
||||
<div class="tip">
|
||||
<span>
|
||||
<el-icon>
|
||||
<svg-icon name="search-up" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span>
|
||||
<el-icon>
|
||||
<svg-icon name="search-down" />
|
||||
</el-icon>
|
||||
</span>
|
||||
切换搜索结果
|
||||
</div>
|
||||
<div class="tip">
|
||||
<span>
|
||||
<el-icon>
|
||||
<svg-icon name="search-enter" />
|
||||
</el-icon>
|
||||
</span>
|
||||
访问页面
|
||||
</div>
|
||||
<div class="tip">
|
||||
<span>ESC</span>
|
||||
退出
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="searchResultRef" class="result" :class="{ mobile: settingsStore.mode === 'mobile' }">
|
||||
<a v-for="(item, index) in resultList" :key="item.path" :ref="setSearchResultItemRef" class="item" :class="{ actived: index === actived }" @click="pageJump(item.path)" @mouseover="actived = index">
|
||||
<el-icon class="icon">
|
||||
<svg-icon v-if="item.icon" :name="item.icon" />
|
||||
</el-icon>
|
||||
<div class="info">
|
||||
<div class="title">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div class="breadcrumb">
|
||||
<span v-for="(bc, bcIndex) in item.breadcrumb" :key="bcIndex">
|
||||
{{ bc }}
|
||||
<el-icon>
|
||||
<svg-icon name="ep:arrow-right" />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
<div class="path">{{ item.path }}</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#search {
|
||||
position: fixed;
|
||||
z-index: 2000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.2s;
|
||||
transform: translateZ(0);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
&.searching {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
|
||||
.container {
|
||||
transform: initial;
|
||||
filter: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 800px;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
transition: all 0.2s;
|
||||
transform: scale(1.1);
|
||||
filter: blur(10px);
|
||||
|
||||
.search-box {
|
||||
margin: 50px 20px 40px;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 52px;
|
||||
line-height: 52px;
|
||||
}
|
||||
|
||||
:deep(.el-input__icon) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
margin-top: 20px;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
|
||||
span {
|
||||
margin: 0 5px;
|
||||
padding: 3px 8px 5px;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: var(--el-text-color-primary);
|
||||
background-color: var(--el-fill-color);
|
||||
box-shadow: inset 0 -2px var(--el-fill-color-lighter), inset 0 0 1px 1px var(--el-fill-color-darker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result {
|
||||
position: relative;
|
||||
margin: 0 20px;
|
||||
max-height: calc(100% - 250px);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
background-color: var(--el-bg-color);
|
||||
box-shadow: 0 0 0 1px var(--el-border-color-darker);
|
||||
|
||||
&.mobile {
|
||||
max-height: calc(100% - 200px);
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.actived {
|
||||
background-color: var(--el-bg-color-page);
|
||||
|
||||
.icon {
|
||||
color: var(--el-color-primary);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.info {
|
||||
border-left-color: var(--el-border-color);
|
||||
|
||||
.title {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.breadcrumb,
|
||||
.path {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: 0 0 66px;
|
||||
text-align: center;
|
||||
color: var(--el-color-info);
|
||||
font-size: 20px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
border-left: 1px solid var(--el-border-color-lighter);
|
||||
padding: 5px 10px 7px;
|
||||
transition: all 0.3s;
|
||||
|
||||
@include text-overflow(1, true);
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
@include text-overflow(1, true);
|
||||
}
|
||||
|
||||
.breadcrumb,
|
||||
.path {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: all 0.3s;
|
||||
|
||||
@include text-overflow(1, true);
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
span {
|
||||
margin-right: 5px;
|
||||
|
||||
&:last-child i {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,151 @@
|
||||
<script lang="ts" setup name="SidebarItem">
|
||||
// import SidebarItem from './index.vue'
|
||||
import { isExternalLink, resolveRoutePath } from '@/utils'
|
||||
import type { Menu } from '@/global'
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object as () => Menu.recordRaw,
|
||||
required: true,
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const hasChildren = computed(() => {
|
||||
let flag = true
|
||||
if (props.item.children) {
|
||||
if (props.item.children.every(item => item.meta.sidebar === false))
|
||||
flag = false
|
||||
}
|
||||
else {
|
||||
flag = false
|
||||
}
|
||||
return flag
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar-item">
|
||||
<router-link v-if="!hasChildren" v-slot="{ href, navigate, isActive, isExactActive }" custom :to="resolveRoutePath(basePath, item.path || '')">
|
||||
<a :href="isExternalLink(resolveRoutePath(basePath, item.path || '')) ? resolveRoutePath(basePath, item.path || '') : href" :class="[isActive && 'router-link-active', isExactActive && 'router-link-exact-active']" :target="isExternalLink(resolveRoutePath(basePath, item.path || '')) ? '_blank' : '_self'" @click="navigate">
|
||||
<el-menu-item :title="item.meta.title" :index="resolveRoutePath(basePath, item.path || '')">
|
||||
<el-icon v-if="item.meta.icon" class="title-icon">
|
||||
<svg-icon :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span class="title">{{ item.meta.title }}</span>
|
||||
</el-menu-item>
|
||||
</a>
|
||||
</router-link>
|
||||
<el-sub-menu v-else :title="item.meta.title" :index="resolveRoutePath(basePath, item.path || '')">
|
||||
<template #title>
|
||||
<el-icon v-if="item.meta.icon" class="title-icon">
|
||||
<svg-icon :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span class="title">{{ item.meta.title }}</span>
|
||||
</template>
|
||||
<template v-for="route in item.children">
|
||||
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path" :item="route" :base-path="resolveRoutePath(basePath, item.path || '')" />
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-menu-item span.title),
|
||||
:deep(.el-sub-menu__title),
|
||||
:deep(.el-sub-menu__title span.title) {
|
||||
flex: 1;
|
||||
vertical-align: inherit;
|
||||
|
||||
@include text-overflow;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu),
|
||||
:deep(.el-menu-item) {
|
||||
.title-icon {
|
||||
width: 20px;
|
||||
font-size: 20px;
|
||||
vertical-align: -0.25em;
|
||||
transition: transform 0.3s;
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.title-icon + .title {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&:hover > .title-icon,
|
||||
.el-sub-menu__title:hover > .title-icon {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.el-sub-menu__title {
|
||||
> .badge {
|
||||
&-dot {
|
||||
right: 40px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
right: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.el-menu--inline {
|
||||
background-color: var(--g-sub-sidebar-menu-bg) !important;
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu > .el-sub-menu__title {
|
||||
color: var(--g-sub-sidebar-menu-color);
|
||||
background-color: var(--g-sub-sidebar-menu-bg) !important;
|
||||
|
||||
&:hover {
|
||||
color: var(--g-sub-sidebar-menu-hover-color) !important;
|
||||
background-color: var(--g-sub-sidebar-menu-hover-bg) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
color: var(--g-sub-sidebar-menu-color) !important;
|
||||
background: transparent !important;
|
||||
|
||||
&:hover {
|
||||
color: var(--g-sub-sidebar-menu-hover-color) !important;
|
||||
background-color: var(--g-sub-sidebar-menu-hover-bg) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item.is-active,
|
||||
.el-menu--collapse .el-sub-menu.is-active > .el-sub-menu__title,
|
||||
.el-sub-menu .el-menu--inline .el-menu-item.is-active {
|
||||
color: var(--g-sub-sidebar-menu-active-color) !important;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg) !important;
|
||||
|
||||
.icon,
|
||||
.el-sub-menu__icon-arrow {
|
||||
color: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<script lang="ts" setup name="SubSidebar">
|
||||
import Logo from '../Logo/index.vue'
|
||||
import SidebarItem from '../SidebarItem/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const sidebarScrollTop = ref(0)
|
||||
|
||||
function onSidebarScroll(e: Event) {
|
||||
sidebarScrollTop.value = (e.target as HTMLElement).scrollTop
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="['side', 'head', 'single'].includes(settingsStore.menu.menuMode) || settingsStore.mode === 'mobile'" class="sub-sidebar-container" :class="{ 'is-collapse': settingsStore.mode === 'pc' && settingsStore.menu.subMenuCollapse }" @scroll="onSidebarScroll">
|
||||
<Logo
|
||||
:show-logo="settingsStore.menu.menuMode === 'single'" class="sidebar-logo" :class="{
|
||||
'sidebar-logo-bg': settingsStore.menu.menuMode === 'single',
|
||||
'shadow': sidebarScrollTop,
|
||||
}"
|
||||
/>
|
||||
<!-- 侧边栏模式(无主导航) -->
|
||||
<el-menu
|
||||
:unique-opened="settingsStore.menu.subMenuUniqueOpened" :default-openeds="menuStore.defaultOpenedPaths" :default-active="$route.meta.activeMenu || $route.path" :collapse="settingsStore.mode === 'pc' && settingsStore.menu.subMenuCollapse" :collapse-transition="false" :class="{
|
||||
'is-collapse-without-logo': settingsStore.menu.menuMode !== 'single' && settingsStore.menu.subMenuCollapse,
|
||||
}"
|
||||
>
|
||||
<transition-group name="sub-sidebar">
|
||||
<template v-for="(route, index) in menuStore.sidebarMenus">
|
||||
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path || index" :item="route" :base-path="route.path" />
|
||||
</template>
|
||||
</transition-group>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sub-sidebar-container {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
// firefox隐藏滚动条
|
||||
scrollbar-width: none;
|
||||
|
||||
// chrome隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
width: var(--g-sub-sidebar-width);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--g-sub-sidebar-bg);
|
||||
box-shadow: 10px 0 10px -10px var(--g-box-shadow-color);
|
||||
transition: background-color 0.3s, var(--el-transition-box-shadow), left 0.3s, width 0.3s;
|
||||
|
||||
&.is-collapse {
|
||||
width: 64px;
|
||||
|
||||
.sidebar-logo {
|
||||
&:not(.sidebar-logo-bg) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(span) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
transition: box-shadow 0.2s, background-color 0.3s, color 0.3s;
|
||||
background-color: var(--g-sub-sidebar-bg);
|
||||
|
||||
&:not(.sidebar-logo-bg) {
|
||||
:deep(span) {
|
||||
color: var(--g-sub-sidebar-menu-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.sidebar-logo-bg {
|
||||
background-color: var(--g-main-sidebar-bg);
|
||||
}
|
||||
|
||||
&.shadow {
|
||||
box-shadow: 0 10px 10px -10px var(--g-box-shadow-color);
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border-right: 0;
|
||||
padding-top: var(--g-sidebar-logo-height);
|
||||
transition: border-color 0.3s, background-color 0.3s, color 0.3s, padding-top 0.3s;
|
||||
background-color: var(--g-sub-sidebar-bg);
|
||||
|
||||
&:not(.el-menu--collapse) {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
&.is-collapse-without-logo {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
&.el-menu--collapse {
|
||||
:deep(.title-icon) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
span,
|
||||
.el-sub-menu__icon-arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.menu-radius:not(.el-menu--collapse) {
|
||||
.sidebar-item {
|
||||
padding: 0 10px;
|
||||
|
||||
&:first-child {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-menu--inline),
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 次侧边栏动画
|
||||
.sub-sidebar-enter-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
|
||||
.sub-sidebar-enter-from,
|
||||
.sub-sidebar-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) skewY(10deg);
|
||||
}
|
||||
|
||||
.sub-sidebar-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,191 @@
|
||||
<script lang="ts" setup name="Tools">
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const mainPage = useMainPage()
|
||||
const { isFullscreen, toggle } = useFullscreen()
|
||||
|
||||
function userCommand(command: 'dashboard' | 'setting' | 'hotkeys' | 'logout') {
|
||||
switch (command) {
|
||||
case 'dashboard':
|
||||
router.push({
|
||||
name: 'dashboard',
|
||||
})
|
||||
break
|
||||
case 'setting':
|
||||
router.push({
|
||||
name: 'personalSetting',
|
||||
})
|
||||
break
|
||||
case 'hotkeys':
|
||||
eventBus.emit('global-hotkeys-intro-toggle')
|
||||
break
|
||||
case 'logout':
|
||||
userStore.logout().then(() => {
|
||||
router.push({
|
||||
name: 'login',
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
function pro() {
|
||||
window.open('https://hooray.gitee.io/fantastic-admin-pro-example/', 'top')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tools">
|
||||
<div class="buttons">
|
||||
<span v-if="settingsStore.mode === 'pc'" class="item item-pro" @click="pro">
|
||||
<el-icon>
|
||||
<svg-icon name="pro" />
|
||||
</el-icon>
|
||||
<span class="title">查看专业版</span>
|
||||
</span>
|
||||
<span v-if="settingsStore.navSearch.enable" class="item" @click="eventBus.emit('global-search-toggle')">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:search" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-if="settingsStore.mode === 'pc' && settingsStore.toolbar.enableFullscreen" class="item" @click="toggle">
|
||||
<el-icon>
|
||||
<svg-icon :name="isFullscreen ? 'fullscreen-exit' : 'fullscreen'" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-if="settingsStore.toolbar.enablePageReload" class="item" @click="mainPage.reload()">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:refresh-right" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-if="settingsStore.toolbar.enableColorScheme" class="item" @click="settingsStore.setColorScheme(settingsStore.app.colorScheme === 'dark' ? 'light' : 'dark')">
|
||||
<el-icon>
|
||||
<svg-icon v-show="settingsStore.app.colorScheme === 'light'" name="ep:sunny" />
|
||||
<svg-icon v-show="settingsStore.app.colorScheme === 'dark'" name="ep:moon" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-if="settingsStore.toolbar.enableAppSetting" class="item" @click="eventBus.emit('global-theme-toggle')">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:setting" />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
<el-dropdown class="user-container" size="default" @command="userCommand">
|
||||
<div class="user-wrapper">
|
||||
<el-avatar size="small">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:user-filled" />
|
||||
</el-icon>
|
||||
</el-avatar>
|
||||
{{ userStore.account }}
|
||||
<el-icon>
|
||||
<svg-icon name="ep:caret-bottom" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="user-dropdown">
|
||||
<el-dropdown-item v-if="settingsStore.dashboard.enable" command="dashboard">
|
||||
控制台
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="setting">
|
||||
个人设置
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="settingsStore.mode === 'pc'" divided command="hotkeys">
|
||||
快捷键介绍
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">
|
||||
退出登录
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
white-space: nowrap;
|
||||
|
||||
.buttons {
|
||||
margin-right: 20px;
|
||||
|
||||
.item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 24px;
|
||||
width: 34px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-text-color-primary);
|
||||
transition: var(--el-transition-color);
|
||||
}
|
||||
}
|
||||
|
||||
.item-pro {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
padding: 0 10px;
|
||||
transform-origin: right center;
|
||||
animation: pro-text 3s ease-out infinite;
|
||||
|
||||
@keyframes pro-text {
|
||||
0%,
|
||||
20% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50%,
|
||||
70% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding-left: 5px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
background-image: linear-gradient(to right, #ffa237, #fc455d);
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.user-container) {
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
|
||||
.user-wrapper {
|
||||
.el-avatar {
|
||||
vertical-align: middle;
|
||||
margin-top: -2px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,155 @@
|
||||
<script lang="ts" setup name="Topbar">
|
||||
import { compile } from 'path-to-regexp'
|
||||
import Tools from '../Tools/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const enableSubMenuCollapseButton = computed(() => {
|
||||
return settingsStore.mode === 'mobile' || (
|
||||
['side', 'head', 'single'].includes(settingsStore.menu.menuMode)
|
||||
&& settingsStore.menu.enableSubMenuCollapseButton
|
||||
)
|
||||
})
|
||||
|
||||
const breadcrumbList = computed(() => {
|
||||
const breadcrumbList = []
|
||||
if (settingsStore.dashboard.enable) {
|
||||
breadcrumbList.push({
|
||||
path: '/dashboard',
|
||||
title: settingsStore.dashboard.title,
|
||||
})
|
||||
}
|
||||
if (route.meta.breadcrumbNeste)
|
||||
breadcrumbList.push(...route.meta.breadcrumbNeste.filter(item => item.hide === false))
|
||||
|
||||
return breadcrumbList
|
||||
})
|
||||
|
||||
const scrollTop = ref(0)
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', onScroll)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
})
|
||||
function onScroll() {
|
||||
scrollTop.value = document.documentElement.scrollTop || document.body.scrollTop
|
||||
}
|
||||
|
||||
function pathCompile(path: string) {
|
||||
const toPath = compile(path)
|
||||
return toPath(route.params)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="topbar-container" :class="{
|
||||
fixed: settingsStore.topbar.fixed,
|
||||
shadow: scrollTop,
|
||||
}" data-fixed-calc-width
|
||||
>
|
||||
<div class="left-box">
|
||||
<div v-if="enableSubMenuCollapseButton" class="sidebar-collapse" :class="{ 'is-collapse': settingsStore.menu.subMenuCollapse }" @click="settingsStore.toggleSidebarCollapse()">
|
||||
<el-icon>
|
||||
<svg-icon name="toolbar-collapse" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-breadcrumb v-if="settingsStore.breadcrumb.enable && settingsStore.mode === 'pc' && settingsStore.app.routeBaseOn !== 'filesystem'">
|
||||
<transition-group name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="item.path" :to="index < breadcrumbList.length - 1 ? pathCompile(item.path) : ''">
|
||||
{{ item.title }}
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<Tools />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.topbar-container {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: var(--g-topbar-height);
|
||||
background-color: var(--g-toolbar-bg);
|
||||
transition: width 0.3s, top 0.3s, transform 0.3s, background-color 0.3s, var(--el-transition-box-shadow);
|
||||
|
||||
&.fixed {
|
||||
position: fixed;
|
||||
|
||||
&.shadow {
|
||||
box-shadow: 0 10px 10px -10px var(--g-box-shadow-color);
|
||||
}
|
||||
}
|
||||
|
||||
.left-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 50px;
|
||||
overflow: hidden;
|
||||
mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 50px), transparent);
|
||||
|
||||
.sidebar-collapse {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-text-color-primary);
|
||||
transition: var(--el-transition-color), var(--el-transition-md-fade);
|
||||
}
|
||||
|
||||
&:hover .el-icon {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.is-collapse .el-icon {
|
||||
transform: rotateZ(-180deg);
|
||||
}
|
||||
|
||||
& + .el-breadcrumb {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb) {
|
||||
margin-left: 20px;
|
||||
white-space: nowrap;
|
||||
|
||||
.el-breadcrumb__item {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
|
||||
span {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&:last-child span {
|
||||
color: #97a8be;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 面包屑动画
|
||||
.breadcrumb-enter-active {
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
.breadcrumb-enter-from,
|
||||
.breadcrumb-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(30px) skewX(-50deg);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,240 @@
|
||||
<script lang="ts" setup name="Layout">
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import Header from './components/Header/index.vue'
|
||||
import MainSidebar from './components/MainSidebar/index.vue'
|
||||
import SubSidebar from './components/SubSidebar/index.vue'
|
||||
import Topbar from './components/Topbar/index.vue'
|
||||
import Search from './components/Search/index.vue'
|
||||
import AppSetting from './components/AppSetting/index.vue'
|
||||
import HotkeysIntro from './components/HotkeysIntro/index.vue'
|
||||
import BuyIt from './components/BuyIt/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useKeepAliveStore from '@/store/modules/keepAlive'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
|
||||
const routeInfo = useRoute()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const keepAliveStore = useKeepAliveStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
watch(() => settingsStore.menu.subMenuCollapse, (val) => {
|
||||
if (settingsStore.mode === 'mobile') {
|
||||
if (!val)
|
||||
document.body.classList.add('hidden')
|
||||
|
||||
else
|
||||
document.body.classList.remove('hidden')
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => routeInfo.path, () => {
|
||||
if (settingsStore.mode === 'mobile') {
|
||||
settingsStore.$patch((state) => {
|
||||
state.menu.subMenuCollapse = true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
hotkeys('f5', (e) => {
|
||||
if (settingsStore.toolbar.enablePageReload) {
|
||||
e.preventDefault()
|
||||
useMainPage().reload()
|
||||
}
|
||||
})
|
||||
hotkeys('alt+`', (e) => {
|
||||
if (settingsStore.menu.enableHotkeys) {
|
||||
e.preventDefault()
|
||||
useMenu().switchTo(menuStore.actived + 1 < menuStore.allMenus.length ? menuStore.actived + 1 : 0)
|
||||
}
|
||||
})
|
||||
})
|
||||
onUnmounted(() => {
|
||||
hotkeys.unbind('f5')
|
||||
hotkeys.unbind('alt+`')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div id="app-main">
|
||||
<Header />
|
||||
<div class="wrapper">
|
||||
<div class="sidebar-container" :class="{ show: settingsStore.mode === 'mobile' && !settingsStore.menu.subMenuCollapse }">
|
||||
<MainSidebar />
|
||||
<SubSidebar />
|
||||
</div>
|
||||
<div class="sidebar-mask" :class="{ show: settingsStore.mode === 'mobile' && !settingsStore.menu.subMenuCollapse }" @click="settingsStore.toggleSidebarCollapse()" />
|
||||
<div class="main-container" :style="{ 'padding-bottom': $route.meta.paddingBottom } as any">
|
||||
<Topbar v-if="!(settingsStore.menu.menuMode === 'head' && !settingsStore.menu.enableSubMenuCollapseButton && !settingsStore.breadcrumb.enable)" />
|
||||
<div class="main">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="main" mode="out-in" appear>
|
||||
<keep-alive :include="keepAliveStore.list">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
<Copyright />
|
||||
</div>
|
||||
</div>
|
||||
<el-backtop :right="20" :bottom="20" title="回到顶部" />
|
||||
</div>
|
||||
<Search />
|
||||
<AppSetting />
|
||||
<HotkeysIntro />
|
||||
<BuyIt />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
[data-mode="mobile"] {
|
||||
.sidebar-container {
|
||||
transform: translateX(calc((var(--g-main-sidebar-width) + var(--g-sub-sidebar-width)) * -1));
|
||||
|
||||
&.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.main-container {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
&[data-menu-mode="single"] {
|
||||
.sidebar-container {
|
||||
transform: translateX(calc(var(--g-sub-sidebar-width) * -1));
|
||||
|
||||
&.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#app-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: -1px 0 0 0 var(--g-box-shadow-color);
|
||||
transition: padding-top 0.3s;
|
||||
|
||||
.sidebar-container {
|
||||
position: fixed;
|
||||
z-index: 1010;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
transition: transform 0.3s, top 0.3s;
|
||||
width: calc(var(--g-main-sidebar-actual-width) + var(--g-sub-sidebar-actual-width));
|
||||
}
|
||||
|
||||
.sidebar-mask {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgb(0 0 0 / 50%);
|
||||
backdrop-filter: blur(2px);
|
||||
transition: all 0.2s;
|
||||
transform: translateZ(0);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
&.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.main-sidebar-container:not(.main-sidebar-leave-active) + .sub-sidebar-container {
|
||||
left: var(--g-main-sidebar-width);
|
||||
}
|
||||
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
margin-left: calc(var(--g-main-sidebar-actual-width) + var(--g-sub-sidebar-actual-width));
|
||||
background-color: var(--g-main-bg);
|
||||
transition: margin-left 0.3s, background-color 0.3s;
|
||||
|
||||
.topbar-container {
|
||||
top: 0;
|
||||
z-index: 998;
|
||||
}
|
||||
|
||||
.main {
|
||||
height: 100%;
|
||||
flex: auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.topbar-container + .main {
|
||||
margin: var(--g-topbar-height) 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header:not(.header-leave-active) + .wrapper {
|
||||
padding-top: var(--g-header-height);
|
||||
|
||||
.sidebar-container {
|
||||
top: var(--g-header-height);
|
||||
|
||||
:deep(.sidebar-logo) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.el-menu) {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.main-container {
|
||||
.topbar-container {
|
||||
top: var(--g-header-height);
|
||||
|
||||
:deep(.tools) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主内容区动画
|
||||
.main-enter-active {
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.main-leave-active {
|
||||
transition: 0.15s;
|
||||
}
|
||||
|
||||
.main-enter-from {
|
||||
opacity: 0;
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
.main-leave-to {
|
||||
opacity: 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +1,25 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import App from './App.vue'
|
||||
const app = createApp(App)
|
||||
|
||||
import pinia from './store'
|
||||
app.use(pinia)
|
||||
|
||||
import router from './router'
|
||||
app.use(router)
|
||||
|
||||
/* importElementPlusPlaceholder */
|
||||
import useSettingsStore from './store/modules/settings'
|
||||
|
||||
// 自定义指令
|
||||
import directive from '@/utils/directive'
|
||||
directive(app)
|
||||
|
||||
// 加载 svg 图标
|
||||
import 'virtual:svg-icons-register'
|
||||
|
||||
// 加载 iconify 图标(element plus)
|
||||
import { downloadAndInstall } from '@/iconify-ep'
|
||||
import useSettingsStore from './store/modules/settings'
|
||||
if (useSettingsStore().app.iconifyOfflineUse) {
|
||||
downloadAndInstall()
|
||||
}
|
||||
|
||||
// 全局样式
|
||||
import '@/assets/styles/globals.scss'
|
||||
// 加载 iconify 图标(element plus)
|
||||
import { downloadAndInstall } from '@/iconify-ep'
|
||||
if (useSettingsStore().app.iconifyOfflineUse)
|
||||
downloadAndInstall()
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(ElementPlus)
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
directive(app)
|
||||
|
||||
app.mount('#app')
|
||||
@@ -1,15 +0,0 @@
|
||||
import MultilevelMenuExample from './modules/multilevel.menu.example'
|
||||
|
||||
const menu = [
|
||||
{
|
||||
meta: {
|
||||
title: '演示',
|
||||
icon: 'sidebar-default'
|
||||
},
|
||||
children: [
|
||||
MultilevelMenuExample
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export default menu
|
||||
@@ -0,0 +1,17 @@
|
||||
import MultilevelMenuExample from './modules/multilevel.menu.example'
|
||||
|
||||
import type { Menu } from '@/global'
|
||||
|
||||
const menu: Menu.recordMainRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
title: '演示',
|
||||
icon: 'sidebar-default',
|
||||
},
|
||||
children: [
|
||||
MultilevelMenuExample,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export default menu
|
||||
@@ -1,46 +0,0 @@
|
||||
export default {
|
||||
meta: {
|
||||
title: '多级导航',
|
||||
icon: 'sidebar-menu'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/page',
|
||||
meta: {
|
||||
title: '导航1'
|
||||
}
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: '导航2'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/page',
|
||||
meta: {
|
||||
title: '导航2-1'
|
||||
}
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: '导航2-2'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/level3/page1',
|
||||
meta: {
|
||||
title: '导航2-2-1'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/level3/page2',
|
||||
meta: {
|
||||
title: '导航2-2-2'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Menu } from '@/global'
|
||||
|
||||
const menus: Menu.recordRaw = {
|
||||
meta: {
|
||||
title: '多级导航',
|
||||
icon: 'heroicons-solid:menu-alt-3',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/page',
|
||||
meta: {
|
||||
title: '导航1',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: '导航2',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/page',
|
||||
meta: {
|
||||
title: '导航2-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: '导航2-2',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/level3/page1',
|
||||
meta: {
|
||||
title: '导航2-2-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/level3/page2',
|
||||
meta: {
|
||||
title: '导航2-2-2',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default menus
|
||||
@@ -1,54 +0,0 @@
|
||||
export default [
|
||||
{
|
||||
url: '/mock/member/login',
|
||||
method: 'post',
|
||||
response: option => {
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
account: option.body.account,
|
||||
token: '@string',
|
||||
failure_time: Math.ceil(new Date().getTime() / 1000) + 24 * 60 * 60
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/mock/member/permission',
|
||||
method: 'get',
|
||||
response: option => {
|
||||
let permissions = []
|
||||
if (option.query.account == 'admin') {
|
||||
permissions = [
|
||||
'permission.browse',
|
||||
'permission.create',
|
||||
'permission.edit',
|
||||
'permission.remove'
|
||||
]
|
||||
} else if (option.query.account == 'test') {
|
||||
permissions = [
|
||||
'permission.browse'
|
||||
]
|
||||
}
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
permissions
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/mock/member/edit/password',
|
||||
method: 'post',
|
||||
response: {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
isSuccess: true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,55 @@
|
||||
export default [
|
||||
{
|
||||
url: '/mock/member/login',
|
||||
method: 'post',
|
||||
response: (option: any) => {
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
account: option.body.account,
|
||||
token: '@string',
|
||||
failure_time: Math.ceil(new Date().getTime() / 1000) + 24 * 60 * 60,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
url: '/mock/member/permission',
|
||||
method: 'get',
|
||||
response: (option: any) => {
|
||||
let permissions: string[] = []
|
||||
if (option.query.account === 'admin') {
|
||||
permissions = [
|
||||
'permission.browse',
|
||||
'permission.create',
|
||||
'permission.edit',
|
||||
'permission.remove',
|
||||
]
|
||||
}
|
||||
else if (option.query.account === 'test') {
|
||||
permissions = [
|
||||
'permission.browse',
|
||||
]
|
||||
}
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
permissions,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
url: '/mock/member/edit/password',
|
||||
method: 'post',
|
||||
response: {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
isSuccess: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -1,68 +0,0 @@
|
||||
export default [
|
||||
{
|
||||
url: '/mock/menu/list',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: [
|
||||
{
|
||||
meta: {
|
||||
title: '演示',
|
||||
icon: 'sidebar-default'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
meta: {
|
||||
title: '多级导航',
|
||||
icon: 'sidebar-menu'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/page',
|
||||
meta: {
|
||||
title: '导航1'
|
||||
}
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: '导航2'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/page',
|
||||
meta: {
|
||||
title: '导航2-1'
|
||||
}
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: '导航2-2'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/level3/page1',
|
||||
meta: {
|
||||
title: '导航2-2-1'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/level3/page2',
|
||||
meta: {
|
||||
title: '导航2-2-2'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,68 @@
|
||||
export default [
|
||||
{
|
||||
url: '/mock/menu/list',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: [
|
||||
{
|
||||
meta: {
|
||||
title: '演示',
|
||||
icon: 'sidebar-default',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
meta: {
|
||||
title: '多级导航',
|
||||
icon: 'sidebar-menu',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/page',
|
||||
meta: {
|
||||
title: '导航1',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: '导航2',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/page',
|
||||
meta: {
|
||||
title: '导航2-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: '导航2-2',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/level3/page1',
|
||||
meta: {
|
||||
title: '导航2-2-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/level3/page2',
|
||||
meta: {
|
||||
title: '导航2-2-2',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -1,86 +0,0 @@
|
||||
export default [
|
||||
{
|
||||
url: '/mock/route/list',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: [
|
||||
{
|
||||
meta: {
|
||||
title: '演示',
|
||||
icon: 'sidebar-default'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example',
|
||||
component: 'Layout',
|
||||
redirect: '/multilevel_menu_example/page',
|
||||
name: 'multilevelMenuExample',
|
||||
meta: {
|
||||
title: '多级导航',
|
||||
icon: 'sidebar-menu'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'page',
|
||||
name: 'multilevelMenuExample1',
|
||||
component: 'multilevel_menu_example/page.vue',
|
||||
meta: {
|
||||
title: '导航1'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'level2',
|
||||
name: 'multilevelMenuExample2',
|
||||
redirect: '/multilevel_menu_example/level2/page',
|
||||
meta: {
|
||||
title: '导航2'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'page',
|
||||
name: 'multilevelMenuExample2-1',
|
||||
component: 'multilevel_menu_example/level2/page.vue',
|
||||
meta: {
|
||||
title: '导航2-1'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'level3',
|
||||
name: 'multilevelMenuExample2-2',
|
||||
redirect: '/multilevel_menu_example/level2/level3/page1',
|
||||
meta: {
|
||||
title: '导航2-2'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'page1',
|
||||
name: 'multilevelMenuExample2-2-1',
|
||||
component: 'multilevel_menu_example/level2/level3/page1.vue',
|
||||
meta: {
|
||||
title: '导航2-2-1'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'page2',
|
||||
name: 'multilevelMenuExample2-2-2',
|
||||
component: 'multilevel_menu_example/level2/level3/page2.vue',
|
||||
meta: {
|
||||
title: '导航2-2-2'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,86 @@
|
||||
export default [
|
||||
{
|
||||
url: '/mock/route/list',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: [
|
||||
{
|
||||
meta: {
|
||||
title: '演示',
|
||||
icon: 'sidebar-default',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example',
|
||||
component: 'Layout',
|
||||
redirect: '/multilevel_menu_example/page',
|
||||
name: 'multilevelMenuExample',
|
||||
meta: {
|
||||
title: '多级导航',
|
||||
icon: 'sidebar-menu',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'page',
|
||||
name: 'multilevelMenuExample1',
|
||||
component: 'multilevel_menu_example/page.vue',
|
||||
meta: {
|
||||
title: '导航1',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'level2',
|
||||
name: 'multilevelMenuExample2',
|
||||
redirect: '/multilevel_menu_example/level2/page',
|
||||
meta: {
|
||||
title: '导航2',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'page',
|
||||
name: 'multilevelMenuExample2-1',
|
||||
component: 'multilevel_menu_example/level2/page.vue',
|
||||
meta: {
|
||||
title: '导航2-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'level3',
|
||||
name: 'multilevelMenuExample2-2',
|
||||
redirect: '/multilevel_menu_example/level2/level3/page1',
|
||||
meta: {
|
||||
title: '导航2-2',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'page1',
|
||||
name: 'multilevelMenuExample2-2-1',
|
||||
component: 'multilevel_menu_example/level2/level3/page1.vue',
|
||||
meta: {
|
||||
title: '导航2-2-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'page2',
|
||||
name: 'multilevelMenuExample2-2-2',
|
||||
component: 'multilevel_menu_example/level2/level3/page2.vue',
|
||||
meta: {
|
||||
title: '导航2-2-2',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -1,11 +0,0 @@
|
||||
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
|
||||
|
||||
const mocks = []
|
||||
const mockContext = import.meta.globEager('./mock/*.js')
|
||||
Object.keys(mockContext).forEach(v => {
|
||||
mocks.push(...mockContext[v].default)
|
||||
})
|
||||
|
||||
export function setupProdMockServer() {
|
||||
createProdMockServer(mocks)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
|
||||
|
||||
const mocks: any[] = []
|
||||
const mockContext: any = import.meta.glob('./mock/*.js', { eager: true })
|
||||
Object.keys(mockContext).forEach((v) => {
|
||||
mocks.push(...mockContext[v].default)
|
||||
})
|
||||
|
||||
export function setupProdMockServer() {
|
||||
createProdMockServer(mocks)
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useKeepAliveStore from '@/store/modules/keepAlive'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useRouteStore from '@/store/modules/route'
|
||||
|
||||
import '@/assets/styles/nprogress.scss'
|
||||
import { useNProgress } from '@vueuse/integrations/useNProgress'
|
||||
const { isLoading } = useNProgress()
|
||||
|
||||
// 路由相关数据
|
||||
import { constantRoutes, asyncRoutes } from './routes'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: constantRoutes
|
||||
})
|
||||
|
||||
router.beforeEach(async(to, from, next) => {
|
||||
const settingsStore = useSettingsStore()
|
||||
const userStore = useUserStore()
|
||||
const menuStore = useMenuStore()
|
||||
const routeStore = useRouteStore()
|
||||
settingsStore.app.enableProgress && (isLoading.value = true)
|
||||
// 是否已登录
|
||||
if (userStore.isLogin) {
|
||||
// 是否已根据权限动态生成并挂载路由
|
||||
if (routeStore.isGenerate) {
|
||||
// 导航栏如果不是 single 模式,则需要根据 path 定位主导航的选中状态
|
||||
settingsStore.menu.menuMode !== 'single' && menuStore.setActived(to.path)
|
||||
if (to.name) {
|
||||
if (to.matched.length !== 0) {
|
||||
// 如果已登录状态下,进入登录页会强制跳转到控制台页面
|
||||
if (to.name == 'login') {
|
||||
next({
|
||||
name: 'dashboard',
|
||||
replace: true
|
||||
})
|
||||
} else if (!settingsStore.dashboard.enable && to.name == 'dashboard') {
|
||||
// 如果未开启控制台页面,则默认进入侧边栏导航第一个模块
|
||||
if (menuStore.sidebarMenus.length > 0) {
|
||||
next({
|
||||
path: menuStore.sidebarMenusFirstDeepestPath,
|
||||
replace: true
|
||||
})
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
// 如果是通过 name 跳转,并且 name 对应的路由没有权限时,需要做这步处理,手动指向到 404 页面
|
||||
next({
|
||||
path: '/404'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
switch (settingsStore.app.routeBaseOn) {
|
||||
case 'frontend':
|
||||
await routeStore.generateRoutesAtFront(asyncRoutes)
|
||||
break
|
||||
case 'backend':
|
||||
await routeStore.generateRoutesAtBack()
|
||||
break
|
||||
case 'filesystem':
|
||||
await routeStore.generateRoutesAtFilesystem(asyncRoutes)
|
||||
switch (settingsStore.menu.baseOn) {
|
||||
case 'frontend':
|
||||
await menuStore.generateMenusAtFront()
|
||||
break
|
||||
case 'backend':
|
||||
await menuStore.generateMenusAtBack()
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
let removeRoutes = []
|
||||
routeStore.flatRoutes.forEach(route => {
|
||||
if (!/^(https?:|mailto:|tel:)/.test(route.path)) {
|
||||
removeRoutes.push(router.addRoute(route))
|
||||
}
|
||||
})
|
||||
if (settingsStore.app.routeBaseOn !== 'filesystem') {
|
||||
routeStore.flatSystemRoutes.forEach(route => {
|
||||
removeRoutes.push(router.addRoute(route))
|
||||
})
|
||||
}
|
||||
// 记录的 accessRoutes 路由数据,在登出时会使用到,不使用 router.removeRoute 是考虑配置的路由可能不一定有设置 name ,则通过调用 router.addRoute() 返回的回调进行删除
|
||||
routeStore.setCurrentRemoveRoutes(removeRoutes)
|
||||
next({
|
||||
path: to.fullPath,
|
||||
query: to.query,
|
||||
replace: true
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (to.name != 'login') {
|
||||
next({
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: to.fullPath
|
||||
}
|
||||
})
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
const settingsStore = useSettingsStore()
|
||||
const keepAliveStore = useKeepAliveStore()
|
||||
settingsStore.app.enableProgress && (isLoading.value = false)
|
||||
// 设置页面 title
|
||||
to.meta.title && settingsStore.setTitle(typeof to.meta.title === 'function' ? to.meta.title() : to.meta.title)
|
||||
// 判断当前页面是否开启缓存,如果开启,则将当前页面的 name 信息存入 keep-alive 全局状态
|
||||
if (to.meta.cache) {
|
||||
let componentName = to.matched[to.matched.length - 1].components.default.name
|
||||
if (componentName) {
|
||||
keepAliveStore.add(componentName)
|
||||
} else {
|
||||
console.warn('该页面组件未设置组件名,会导致缓存失效,请检查')
|
||||
}
|
||||
}
|
||||
// 判断离开页面是否开启缓存,如果开启,则根据缓存规则判断是否需要清空 keep-alive 全局状态里离开页面的 name 信息
|
||||
if (from.meta.cache) {
|
||||
let componentName = from.matched[from.matched.length - 1].components.default.name
|
||||
// 通过 meta.cache 判断针对哪些页面进行缓存
|
||||
switch (typeof from.meta.cache) {
|
||||
case 'string':
|
||||
if (from.meta.cache != to.name) {
|
||||
keepAliveStore.remove(componentName)
|
||||
}
|
||||
break
|
||||
case 'object':
|
||||
if (!from.meta.cache.includes(to.name)) {
|
||||
keepAliveStore.remove(componentName)
|
||||
}
|
||||
break
|
||||
}
|
||||
// 如果进入的是 reload 页面,则也将离开页面的缓存清空
|
||||
if (to.name == 'reload') {
|
||||
keepAliveStore.remove(componentName)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,164 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { useNProgress } from '@vueuse/integrations/useNProgress'
|
||||
import '@/assets/styles/nprogress.scss'
|
||||
// 路由相关数据
|
||||
import { asyncRoutes, asyncRoutesByFilesystem, constantRoutes, constantRoutesByFilesystem } from './routes'
|
||||
import pinia from '@/store'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useKeepAliveStore from '@/store/modules/keepAlive'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useRouteStore from '@/store/modules/route'
|
||||
|
||||
const { isLoading } = useNProgress()
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: useSettingsStore(pinia).app.routeBaseOn === 'filesystem' ? constantRoutesByFilesystem : constantRoutes as RouteRecordRaw[],
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const settingsStore = useSettingsStore()
|
||||
const userStore = useUserStore()
|
||||
const menuStore = useMenuStore()
|
||||
const routeStore = useRouteStore()
|
||||
settingsStore.app.enableProgress && (isLoading.value = true)
|
||||
// 是否已登录
|
||||
if (userStore.isLogin) {
|
||||
// 是否已根据权限动态生成并挂载路由
|
||||
if (routeStore.isGenerate) {
|
||||
// 导航栏如果不是 single 模式,则需要根据 path 定位主导航的选中状态
|
||||
settingsStore.menu.menuMode !== 'single' && menuStore.setActived(to.path)
|
||||
if (to.name) {
|
||||
if (to.matched.length !== 0) {
|
||||
// 如果已登录状态下,进入登录页会强制跳转到控制台页面
|
||||
if (to.name === 'login') {
|
||||
next({
|
||||
name: 'dashboard',
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
else if (!settingsStore.dashboard.enable && to.name === 'dashboard') {
|
||||
// 如果未开启控制台页面,则默认进入侧边栏导航第一个模块
|
||||
if (menuStore.sidebarMenus.length > 0) {
|
||||
next({
|
||||
path: menuStore.sidebarMenusFirstDeepestPath,
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 如果是通过 name 跳转,并且 name 对应的路由没有权限时,需要做这步处理,手动指向到 404 页面
|
||||
next({
|
||||
path: '/404',
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch (settingsStore.app.routeBaseOn) {
|
||||
case 'frontend':
|
||||
await routeStore.generateRoutesAtFront(asyncRoutes)
|
||||
break
|
||||
case 'backend':
|
||||
await routeStore.generateRoutesAtBack()
|
||||
break
|
||||
case 'filesystem':
|
||||
await routeStore.generateRoutesAtFilesystem(asyncRoutesByFilesystem)
|
||||
switch (settingsStore.menu.baseOn) {
|
||||
case 'frontend':
|
||||
await menuStore.generateMenusAtFront()
|
||||
break
|
||||
case 'backend':
|
||||
await menuStore.generateMenusAtBack()
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
const removeRoutes: Function[] = []
|
||||
routeStore.flatRoutes.forEach((route) => {
|
||||
if (!/^(https?:|mailto:|tel:)/.test(route.path))
|
||||
removeRoutes.push(router.addRoute(route as RouteRecordRaw))
|
||||
})
|
||||
if (settingsStore.app.routeBaseOn !== 'filesystem') {
|
||||
routeStore.flatSystemRoutes.forEach((route) => {
|
||||
removeRoutes.push(router.addRoute(route as RouteRecordRaw))
|
||||
})
|
||||
}
|
||||
// 记录路由数据,在登出时会使用到,不使用 router.removeRoute 是考虑配置的路由可能不一定有设置 name ,则通过调用 router.addRoute() 返回的回调进行删除
|
||||
routeStore.setCurrentRemoveRoutes(removeRoutes)
|
||||
next({
|
||||
path: to.path,
|
||||
query: to.query,
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (to.name !== 'login') {
|
||||
next({
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: to.fullPath,
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
const settingsStore = useSettingsStore()
|
||||
const keepAliveStore = useKeepAliveStore()
|
||||
settingsStore.app.enableProgress && (isLoading.value = false)
|
||||
// 设置页面 title
|
||||
to.meta.title && settingsStore.setTitle(typeof to.meta.title === 'function' ? to.meta.title() : to.meta.title)
|
||||
// 判断当前页面是否开启缓存,如果开启,则将当前页面的 name 信息存入 keep-alive 全局状态
|
||||
if (to.meta.cache) {
|
||||
const componentName = to.matched.at(-1)?.components?.default.name
|
||||
if (componentName)
|
||||
keepAliveStore.add(componentName)
|
||||
|
||||
else
|
||||
console.warn('该页面组件未设置组件名,会导致缓存失效,请检查')
|
||||
}
|
||||
// 判断离开页面是否开启缓存,如果开启,则根据缓存规则判断是否需要清空 keep-alive 全局状态里离开页面的 name 信息
|
||||
if (from.meta.cache) {
|
||||
const componentName = from.matched.at(-1)?.components?.default.name
|
||||
if (componentName) {
|
||||
// 通过 meta.cache 判断针对哪些页面进行缓存
|
||||
switch (typeof from.meta.cache) {
|
||||
case 'string':
|
||||
if (from.meta.cache !== to.name)
|
||||
keepAliveStore.remove(componentName)
|
||||
|
||||
break
|
||||
case 'object':
|
||||
if (!from.meta.cache.includes(to.name as string))
|
||||
keepAliveStore.remove(componentName)
|
||||
|
||||
break
|
||||
}
|
||||
// 如果进入的是 reload 页面,则也将离开页面的缓存清空
|
||||
if (to.name === 'reload')
|
||||
keepAliveStore.remove(componentName)
|
||||
}
|
||||
}
|
||||
document.body.scrollTop = 0
|
||||
document.documentElement.scrollTop = 0
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -1,61 +0,0 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
path: '/breadcrumb_example',
|
||||
component: Layout,
|
||||
redirect: '/breadcrumb_example/list1',
|
||||
name: 'breadcrumbExample',
|
||||
meta: {
|
||||
title: '面包屑导航',
|
||||
icon: 'sidebar-breadcrumb'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'list1',
|
||||
name: 'breadcrumbExampleList1',
|
||||
component: () => import('@/views/breadcrumb_example/list1.vue'),
|
||||
meta: {
|
||||
title: '列表1(平级模式)'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'detail1',
|
||||
name: 'breadcrumbExampleDetail1',
|
||||
component: () => import('@/views/breadcrumb_example/detail1.vue'),
|
||||
meta: {
|
||||
title: '详情1',
|
||||
sidebar: false,
|
||||
activeMenu: '/breadcrumb_example/list1'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'list2',
|
||||
name: 'breadcrumbExampleList2',
|
||||
redirect: '/breadcrumb_example/list2',
|
||||
meta: {
|
||||
title: '列表2(层级模式)'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: () => import('@/views/breadcrumb_example/list2.vue'),
|
||||
meta: {
|
||||
title: '列表2(层级模式)',
|
||||
sidebar: false,
|
||||
breadcrumb: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'detail2',
|
||||
name: 'breadcrumbExampleDetail2',
|
||||
component: () => import('@/views/breadcrumb_example/detail2.vue'),
|
||||
meta: {
|
||||
title: '详情2',
|
||||
sidebar: false,
|
||||
activeMenu: '/breadcrumb_example/list2'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { Route } from '@/global'
|
||||
|
||||
const Layout = () => import('@/layouts/index.vue')
|
||||
|
||||
const routes: Route.recordRaw = {
|
||||
path: '/breadcrumb_example',
|
||||
component: Layout,
|
||||
redirect: '/breadcrumb_example/list1',
|
||||
name: 'breadcrumbExample',
|
||||
meta: {
|
||||
title: '面包屑导航',
|
||||
icon: 'sidebar-breadcrumb',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'list1',
|
||||
name: 'breadcrumbExampleList1',
|
||||
component: () => import('@/views/breadcrumb_example/list1.vue'),
|
||||
meta: {
|
||||
title: '列表1(平级模式)',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'detail1',
|
||||
name: 'breadcrumbExampleDetail1',
|
||||
component: () => import('@/views/breadcrumb_example/detail1.vue'),
|
||||
meta: {
|
||||
title: '详情1',
|
||||
sidebar: false,
|
||||
activeMenu: '/breadcrumb_example/list1',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'list2',
|
||||
name: 'breadcrumbExampleList2',
|
||||
redirect: '/breadcrumb_example/list2',
|
||||
meta: {
|
||||
title: '列表2(层级模式)',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: () => import('@/views/breadcrumb_example/list2.vue'),
|
||||
meta: {
|
||||
title: '列表2(层级模式)',
|
||||
sidebar: false,
|
||||
breadcrumb: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'detail2',
|
||||
name: 'breadcrumbExampleDetail2',
|
||||
component: () => import('@/views/breadcrumb_example/detail2.vue'),
|
||||
meta: {
|
||||
title: '详情2',
|
||||
sidebar: false,
|
||||
activeMenu: '/breadcrumb_example/list2',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default routes
|
||||
@@ -1,126 +0,0 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
path: '/component_basic_example',
|
||||
component: Layout,
|
||||
redirect: '/component_basic_example/icon',
|
||||
name: 'componentBasicExample',
|
||||
meta: {
|
||||
title: '基础组件(部分)',
|
||||
icon: 'sidebar-element'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'icon',
|
||||
name: 'componentBasicExampleIcon',
|
||||
component: () => import('@/views/component_basic_example/icon.vue'),
|
||||
meta: {
|
||||
title: '图标'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'button',
|
||||
name: 'componentBasicExampleButton',
|
||||
component: () => import('@/views/component_basic_example/button.vue'),
|
||||
meta: {
|
||||
title: '按钮'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'link',
|
||||
name: 'componentBasicExampleLink',
|
||||
component: () => import('@/views/component_basic_example/link.vue'),
|
||||
meta: {
|
||||
title: '文字链接'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'radio',
|
||||
name: 'componentBasicExampleRadio',
|
||||
component: () => import('@/views/component_basic_example/radio.vue'),
|
||||
meta: {
|
||||
title: '单选框'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'checkbox',
|
||||
name: 'componentBasicExampleCheckbox',
|
||||
component: () => import('@/views/component_basic_example/checkbox.vue'),
|
||||
meta: {
|
||||
title: '多选框'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'input',
|
||||
name: 'componentBasicExampleInput',
|
||||
component: () => import('@/views/component_basic_example/input.vue'),
|
||||
meta: {
|
||||
title: '输入框'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'inputnumber',
|
||||
name: 'componentBasicExampleInputnumber',
|
||||
component: () => import('@/views/component_basic_example/inputnumber.vue'),
|
||||
meta: {
|
||||
title: '计数器'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'select',
|
||||
name: 'componentBasicExampleSelect',
|
||||
component: () => import('@/views/component_basic_example/select.vue'),
|
||||
meta: {
|
||||
title: '选择器'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'switch',
|
||||
name: 'componentBasicExampleSwitch',
|
||||
component: () => import('@/views/component_basic_example/switch.vue'),
|
||||
meta: {
|
||||
title: '开关'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'slider',
|
||||
name: 'componentBasicExampleSlider',
|
||||
component: () => import('@/views/component_basic_example/slider.vue'),
|
||||
meta: {
|
||||
title: '滑块'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'timepicker',
|
||||
name: 'componentBasicExampleTimepicker',
|
||||
component: () => import('@/views/component_basic_example/timepicker.vue'),
|
||||
meta: {
|
||||
title: '时间选择器'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'datepicker',
|
||||
name: 'componentBasicExampleDatepicker',
|
||||
component: () => import('@/views/component_basic_example/datepicker.vue'),
|
||||
meta: {
|
||||
title: '日期选择器'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'datetimepicker',
|
||||
name: 'componentBasicExampleDatetimepicker',
|
||||
component: () => import('@/views/component_basic_example/datetimepicker.vue'),
|
||||
meta: {
|
||||
title: '日期时间选择器'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'rate',
|
||||
name: 'componentBasicExampleRate',
|
||||
component: () => import('@/views/component_basic_example/rate.vue'),
|
||||
meta: {
|
||||
title: '评分'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import type { Route } from '@/global'
|
||||
|
||||
const Layout = () => import('@/layouts/index.vue')
|
||||
|
||||
const routes: Route.recordRaw = {
|
||||
path: '/component_basic_example',
|
||||
component: Layout,
|
||||
redirect: '/component_basic_example/icon',
|
||||
name: 'componentBasicExample',
|
||||
meta: {
|
||||
title: '基础组件(部分)',
|
||||
icon: 'ep:element-plus',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'button',
|
||||
name: 'componentBasicExampleButton',
|
||||
component: () => import('@/views/component_basic_example/button.vue'),
|
||||
meta: {
|
||||
title: '按钮',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'link',
|
||||
name: 'componentBasicExampleLink',
|
||||
component: () => import('@/views/component_basic_example/link.vue'),
|
||||
meta: {
|
||||
title: '文字链接',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'radio',
|
||||
name: 'componentBasicExampleRadio',
|
||||
component: () => import('@/views/component_basic_example/radio.vue'),
|
||||
meta: {
|
||||
title: '单选框',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'checkbox',
|
||||
name: 'componentBasicExampleCheckbox',
|
||||
component: () => import('@/views/component_basic_example/checkbox.vue'),
|
||||
meta: {
|
||||
title: '多选框',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'input',
|
||||
name: 'componentBasicExampleInput',
|
||||
component: () => import('@/views/component_basic_example/input.vue'),
|
||||
meta: {
|
||||
title: '输入框',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'inputnumber',
|
||||
name: 'componentBasicExampleInputnumber',
|
||||
component: () => import('@/views/component_basic_example/inputnumber.vue'),
|
||||
meta: {
|
||||
title: '计数器',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'select',
|
||||
name: 'componentBasicExampleSelect',
|
||||
component: () => import('@/views/component_basic_example/select.vue'),
|
||||
meta: {
|
||||
title: '选择器',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'switch',
|
||||
name: 'componentBasicExampleSwitch',
|
||||
component: () => import('@/views/component_basic_example/switch.vue'),
|
||||
meta: {
|
||||
title: '开关',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'slider',
|
||||
name: 'componentBasicExampleSlider',
|
||||
component: () => import('@/views/component_basic_example/slider.vue'),
|
||||
meta: {
|
||||
title: '滑块',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'timepicker',
|
||||
name: 'componentBasicExampleTimepicker',
|
||||
component: () => import('@/views/component_basic_example/timepicker.vue'),
|
||||
meta: {
|
||||
title: '时间选择器',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'datepicker',
|
||||
name: 'componentBasicExampleDatepicker',
|
||||
component: () => import('@/views/component_basic_example/datepicker.vue'),
|
||||
meta: {
|
||||
title: '日期选择器',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'datetimepicker',
|
||||
name: 'componentBasicExampleDatetimepicker',
|
||||
component: () => import('@/views/component_basic_example/datetimepicker.vue'),
|
||||
meta: {
|
||||
title: '日期时间选择器',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'rate',
|
||||
name: 'componentBasicExampleRate',
|
||||
component: () => import('@/views/component_basic_example/rate.vue'),
|
||||
meta: {
|
||||
title: '评分',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default routes
|
||||
@@ -1,110 +0,0 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
path: '/component_extend_example',
|
||||
component: Layout,
|
||||
redirect: '/component_extend_example/pageheader',
|
||||
name: 'componentExtendExample',
|
||||
meta: {
|
||||
title: '扩展组件',
|
||||
icon: 'sidebar-component'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'svg',
|
||||
name: 'componentExtendExampleSvg',
|
||||
component: () => import('@/views/component_extend_example/svg.demo.vue'),
|
||||
meta: {
|
||||
title: 'SVG Icon'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'pageheader',
|
||||
name: 'componentExtendExamplePageheader',
|
||||
component: () => import('@/views/component_extend_example/pageheader.demo.vue'),
|
||||
meta: {
|
||||
title: '页头'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'pagemain',
|
||||
name: 'componentExtendExamplePagemain',
|
||||
component: () => import('@/views/component_extend_example/pagemain.demo.vue'),
|
||||
meta: {
|
||||
title: '内容块'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'actionbar',
|
||||
name: 'componentExtendExampleActionbar',
|
||||
component: () => import('@/views/component_extend_example/actionbar.demo.vue'),
|
||||
meta: {
|
||||
title: '固定底部操作栏'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'searchbar',
|
||||
name: 'componentExtendExampleSearchbar',
|
||||
component: () => import('@/views/component_extend_example/searchbar.demo.vue'),
|
||||
meta: {
|
||||
title: '搜索面板'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'batchactionbar',
|
||||
name: 'componentExtendExampleBatchactionbar',
|
||||
component: () => import('@/views/component_extend_example/batchactionbar.demo.vue'),
|
||||
meta: {
|
||||
title: '批量操作栏'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tableautoheight',
|
||||
name: 'componentExtendExampleTableautoheight',
|
||||
component: () => import('@/views/component_extend_example/table.autoheight.demo.vue'),
|
||||
meta: {
|
||||
title: '表格高度自适应'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'upload',
|
||||
name: 'componentExtendExampleUpload',
|
||||
component: () => import('@/views/component_extend_example/upload.demo.vue'),
|
||||
meta: {
|
||||
title: '上传'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'imagepreview',
|
||||
name: 'componentExtendExampleImagePreview',
|
||||
component: () => import('@/views/component_extend_example/image.preview.demo.vue'),
|
||||
meta: {
|
||||
title: '图片预览'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'trend',
|
||||
name: 'componentExtendExampleTrend',
|
||||
component: () => import('@/views/component_extend_example/trend.demo.vue'),
|
||||
meta: {
|
||||
title: '趋势标记'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'pcas',
|
||||
name: 'componentExtendExamplePcas',
|
||||
component: () => import('@/views/component_extend_example/pcas.demo.vue'),
|
||||
meta: {
|
||||
title: '省市区街道联动'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'editor',
|
||||
name: 'componentExtendExampleEditor',
|
||||
component: () => import('@/views/component_extend_example/editor.demo.vue'),
|
||||
meta: {
|
||||
title: '编辑器'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import type { Route } from '@/global'
|
||||
|
||||
const Layout = () => import('@/layouts/index.vue')
|
||||
|
||||
const routes: Route.recordRaw = {
|
||||
path: '/component_extend_example',
|
||||
component: Layout,
|
||||
redirect: '/component_extend_example/pageheader',
|
||||
name: 'componentExtendExample',
|
||||
meta: {
|
||||
title: '扩展组件',
|
||||
icon: 'sidebar-component',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'svg',
|
||||
name: 'componentExtendExampleSvg',
|
||||
component: () => import('@/views/component_extend_example/svg.demo.vue'),
|
||||
meta: {
|
||||
title: 'SVG Icon',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'pageheader',
|
||||
name: 'componentExtendExamplePageheader',
|
||||
component: () => import('@/views/component_extend_example/pageheader.demo.vue'),
|
||||
meta: {
|
||||
title: '页头',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'pagemain',
|
||||
name: 'componentExtendExamplePagemain',
|
||||
component: () => import('@/views/component_extend_example/pagemain.demo.vue'),
|
||||
meta: {
|
||||
title: '内容块',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'actionbar',
|
||||
name: 'componentExtendExampleActionbar',
|
||||
component: () => import('@/views/component_extend_example/actionbar.demo.vue'),
|
||||
meta: {
|
||||
title: '固定底部操作栏',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'searchbar',
|
||||
name: 'componentExtendExampleSearchbar',
|
||||
component: () => import('@/views/component_extend_example/searchbar.demo.vue'),
|
||||
meta: {
|
||||
title: '搜索面板',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'batchactionbar',
|
||||
name: 'componentExtendExampleBatchactionbar',
|
||||
component: () => import('@/views/component_extend_example/batchactionbar.demo.vue'),
|
||||
meta: {
|
||||
title: '批量操作栏',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'tableautoheight',
|
||||
name: 'componentExtendExampleTableautoheight',
|
||||
component: () => import('@/views/component_extend_example/table.autoheight.demo.vue'),
|
||||
meta: {
|
||||
title: '表格高度自适应',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'upload',
|
||||
name: 'componentExtendExampleUpload',
|
||||
component: () => import('@/views/component_extend_example/upload.demo.vue'),
|
||||
meta: {
|
||||
title: '上传',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'imagepreview',
|
||||
name: 'componentExtendExampleImagePreview',
|
||||
component: () => import('@/views/component_extend_example/image.preview.demo.vue'),
|
||||
meta: {
|
||||
title: '图片预览',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'trend',
|
||||
name: 'componentExtendExampleTrend',
|
||||
component: () => import('@/views/component_extend_example/trend.demo.vue'),
|
||||
meta: {
|
||||
title: '趋势标记',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'pcas',
|
||||
name: 'componentExtendExamplePcas',
|
||||
component: () => import('@/views/component_extend_example/pcas.demo.vue'),
|
||||
meta: {
|
||||
title: '省市区街道联动',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'editor',
|
||||
name: 'componentExtendExampleEditor',
|
||||
component: () => import('@/views/component_extend_example/editor.demo.vue'),
|
||||
meta: {
|
||||
title: '编辑器',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default routes
|
||||
@@ -1,66 +0,0 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/official',
|
||||
meta: {
|
||||
title: '官方周边',
|
||||
icon: 'ep:grid'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'https://hooray.gitee.io/fantastic-startkit/',
|
||||
meta: {
|
||||
title: 'Fantastic-startkit 项目启动套件'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'https://hooray.gitee.io/one-step-admin/',
|
||||
meta: {
|
||||
title: 'One-step-admin 干啥都快人一步的中后台框架'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/cooperation',
|
||||
meta: {
|
||||
title: '战略合作',
|
||||
icon: 'sidebar-cooperation'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'https://www.vform666.com',
|
||||
meta: {
|
||||
title: 'VForm 低代码表单'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'https://gitee.com/sciqtw/quickadmin',
|
||||
meta: {
|
||||
title: 'QuickAdmin 快速开发管理框架'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/videos/coming/soon',
|
||||
component: Layout,
|
||||
meta: {
|
||||
title: '视频教程',
|
||||
icon: 'sidebar-videos'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'videosComingSoon',
|
||||
component: () => import('@/views/videos_example/coming.soon.vue'),
|
||||
meta: {
|
||||
title: '视频教程',
|
||||
sidebar: false,
|
||||
breadcrumb: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { Route } from '@/global'
|
||||
|
||||
const Layout = () => import('@/layouts/index.vue')
|
||||
|
||||
const routes: Route.recordRaw[] = [
|
||||
{
|
||||
path: '/official',
|
||||
meta: {
|
||||
title: '官方周边',
|
||||
icon: 'ep:grid',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'https://hooray.gitee.io/fantastic-startkit/',
|
||||
meta: {
|
||||
title: 'Fantastic-startkit 项目启动套件',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'https://hooray.gitee.io/one-step-admin/',
|
||||
meta: {
|
||||
title: 'One-step-admin 干啥都快人一步的中后台框架',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/cooperation',
|
||||
meta: {
|
||||
title: '战略合作',
|
||||
icon: 'sidebar-cooperation',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'https://www.vform666.com',
|
||||
meta: {
|
||||
title: 'VForm 低代码表单',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'https://gitee.com/sciqtw/quickadmin',
|
||||
meta: {
|
||||
title: 'QuickAdmin 快速开发管理框架',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/video/coming/soon',
|
||||
component: Layout,
|
||||
meta: {
|
||||
title: '视频教程',
|
||||
icon: 'sidebar-videos',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'videosComingSoon',
|
||||
component: () => import('@/views/videos_example/coming.soon.vue'),
|
||||
meta: {
|
||||
title: '视频教程',
|
||||
sidebar: false,
|
||||
breadcrumb: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export default routes
|
||||
@@ -1,26 +0,0 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
path: '/link',
|
||||
component: Layout,
|
||||
redirect: '/link/iframe',
|
||||
name: 'externalLinkExample',
|
||||
meta: {
|
||||
title: '外链',
|
||||
icon: 'sidebar-external-link'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'https://gitee.com/hooray/fantastic-admin',
|
||||
meta: {
|
||||
title: 'Gitee 仓库'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'https://github.com/hooray/fantastic-admin',
|
||||
meta: {
|
||||
title: 'Github 仓库'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Route } from '@/global'
|
||||
|
||||
const Layout = () => import('@/layouts/index.vue')
|
||||
|
||||
const routes: Route.recordRaw = {
|
||||
path: '/link',
|
||||
component: Layout,
|
||||
name: 'externalLinkExample',
|
||||
meta: {
|
||||
title: '外链',
|
||||
icon: 'sidebar-external-link',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'https://gitee.com/hooray/fantastic-admin',
|
||||
meta: {
|
||||
title: 'Gitee 仓库',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'https://github.com/hooray/fantastic-admin',
|
||||
meta: {
|
||||
title: 'Github 仓库',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default routes
|
||||
@@ -1,25 +0,0 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
path: '/jsx_example',
|
||||
component: Layout,
|
||||
redirect: '/jsx_example/index',
|
||||
name: 'jsxExample',
|
||||
meta: {
|
||||
title: 'JSX',
|
||||
icon: 'sidebar-jsx'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'jsxExampleIndex',
|
||||
component: () => import('@/views/jsx_example/index.jsx'),
|
||||
meta: {
|
||||
title: 'JSX',
|
||||
sidebar: false,
|
||||
breadcrumb: false,
|
||||
activeMenu: '/jsx_example'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Route } from '@/global'
|
||||
|
||||
const Layout = () => import('@/layouts/index.vue')
|
||||
|
||||
const routes: Route.recordRaw = {
|
||||
path: '/jsx_example',
|
||||
component: Layout,
|
||||
redirect: '/jsx_example/index',
|
||||
name: 'jsxExample',
|
||||
meta: {
|
||||
title: 'JSX',
|
||||
icon: 'sidebar-jsx',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'jsxExampleIndex',
|
||||
component: () => import('@/views/jsx_example/index.jsx'),
|
||||
meta: {
|
||||
title: 'JSX',
|
||||
sidebar: false,
|
||||
breadcrumb: false,
|
||||
activeMenu: '/jsx_example',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default routes
|
||||
@@ -1,81 +0,0 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
path: '/keep_alive_example',
|
||||
component: Layout,
|
||||
redirect: '/keep_alive_example/page',
|
||||
name: 'keepAliveExample',
|
||||
meta: {
|
||||
title: '页面缓存',
|
||||
icon: 'sidebar-keep-alive'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'page',
|
||||
name: 'keepAliveExamplePage',
|
||||
component: () => import('@/views/keep_alive_example/page.vue'),
|
||||
meta: {
|
||||
title: '页面缓存',
|
||||
breadcrumb: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'detail',
|
||||
name: 'keepAliveExampleDetail',
|
||||
component: () => import('@/views/keep_alive_example/detail.vue'),
|
||||
meta: {
|
||||
title: '平级路由',
|
||||
sidebar: false,
|
||||
activeMenu: '/keep_alive_example/page'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'nested',
|
||||
redirect: '/keep_alive_example/detail2',
|
||||
meta: {
|
||||
title: '下级路由',
|
||||
sidebar: false
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'detail',
|
||||
name: 'keepAliveExampleNestedDetail',
|
||||
component: () => import('@/views/keep_alive_example/detail.vue'),
|
||||
meta: {
|
||||
title: '测试页面',
|
||||
activeMenu: '/keep_alive_example/page'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'nested',
|
||||
name: 'keepAliveExampleNested1',
|
||||
component: () => import('@/views/keep_alive_example/nested/nested.vue'),
|
||||
meta: {
|
||||
title: '路由多级缓存(1)'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'nested',
|
||||
name: 'keepAliveExampleNested2',
|
||||
component: () => import('@/views/keep_alive_example/nested/nested/nested.vue'),
|
||||
meta: {
|
||||
title: '路由多级缓存(1-1)'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'test',
|
||||
name: 'keepAliveExampleNestedTest',
|
||||
component: () => import('@/views/keep_alive_example/nested/nested/nested/index.vue'),
|
||||
meta: {
|
||||
title: '测试页面(1-1-1)',
|
||||
cache: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { Route } from '@/global'
|
||||
|
||||
const Layout = () => import('@/layouts/index.vue')
|
||||
|
||||
const routes: Route.recordRaw = {
|
||||
path: '/keep_alive_example',
|
||||
component: Layout,
|
||||
redirect: '/keep_alive_example/page',
|
||||
name: 'keepAliveExample',
|
||||
meta: {
|
||||
title: '页面缓存',
|
||||
icon: 'sidebar-keep-alive',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'page',
|
||||
name: 'keepAliveExamplePage',
|
||||
component: () => import('@/views/keep_alive_example/page.vue'),
|
||||
meta: {
|
||||
title: '页面缓存',
|
||||
breadcrumb: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'detail',
|
||||
name: 'keepAliveExampleDetail',
|
||||
component: () => import('@/views/keep_alive_example/detail.vue'),
|
||||
meta: {
|
||||
title: '平级路由',
|
||||
sidebar: false,
|
||||
activeMenu: '/keep_alive_example/page',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'nested',
|
||||
redirect: '/keep_alive_example/nested/detail',
|
||||
meta: {
|
||||
title: '嵌套路由',
|
||||
sidebar: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'detail',
|
||||
name: 'keepAliveExampleNestedDetail',
|
||||
component: () => import('@/views/keep_alive_example/detail.vue'),
|
||||
meta: {
|
||||
title: '嵌套路由',
|
||||
activeMenu: '/keep_alive_example/page',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'nested1',
|
||||
name: 'keepAliveExampleNested1',
|
||||
component: () => import('@/views/keep_alive_example/nested/nested.vue'),
|
||||
meta: {
|
||||
title: '路由多级缓存1',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'nested2',
|
||||
name: 'keepAliveExampleNested2',
|
||||
component: () => import('@/views/keep_alive_example/nested/nested/nested.vue'),
|
||||
meta: {
|
||||
title: '路由多级缓存1-1',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'keepAliveExampleNestedIndex',
|
||||
component: () => import('@/views/keep_alive_example/nested/nested/nested/index.vue'),
|
||||
meta: {
|
||||
title: '路由多级缓存1-1-1',
|
||||
cache: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default routes
|
||||
@@ -1,25 +0,0 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
path: '/mock_example',
|
||||
component: Layout,
|
||||
redirect: '/mock_example/index',
|
||||
name: 'mockExample',
|
||||
meta: {
|
||||
title: 'Mock',
|
||||
icon: 'sidebar-mock'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'mockExampleIndex',
|
||||
component: () => import('@/views/mock_example/index.vue'),
|
||||
meta: {
|
||||
title: 'Mock',
|
||||
sidebar: false,
|
||||
breadcrumb: false,
|
||||
activeMenu: '/mock_example'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Route } from '@/global'
|
||||
|
||||
const Layout = () => import('@/layouts/index.vue')
|
||||
|
||||
const routes: Route.recordRaw = {
|
||||
path: '/mock_example',
|
||||
component: Layout,
|
||||
redirect: '/mock_example/index',
|
||||
name: 'mockExample',
|
||||
meta: {
|
||||
title: 'Mock',
|
||||
icon: 'sidebar-mock',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'mockExampleIndex',
|
||||
component: () => import('@/views/mock_example/index.vue'),
|
||||
meta: {
|
||||
title: 'Mock',
|
||||
sidebar: false,
|
||||
breadcrumb: false,
|
||||
activeMenu: '/mock_example',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default routes
|
||||
@@ -1,66 +0,0 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
path: '/multilevel_menu_example',
|
||||
component: Layout,
|
||||
redirect: '/multilevel_menu_example/page',
|
||||
name: 'multilevelMenuExample',
|
||||
meta: {
|
||||
title: '多级导航',
|
||||
icon: 'sidebar-menu'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'page',
|
||||
name: 'multilevelMenuExample1',
|
||||
component: () => import('@/views/multilevel_menu_example/page.vue'),
|
||||
meta: {
|
||||
title: '导航1'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'level2',
|
||||
name: 'multilevelMenuExample2',
|
||||
redirect: '/multilevel_menu_example/level2/page',
|
||||
meta: {
|
||||
title: '导航2'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'page',
|
||||
name: 'multilevelMenuExample2-1',
|
||||
component: () => import('@/views/multilevel_menu_example/level2/page.vue'),
|
||||
meta: {
|
||||
title: '导航2-1'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'level3',
|
||||
name: 'multilevelMenuExample2-2',
|
||||
redirect: '/multilevel_menu_example/level2/level3/page1',
|
||||
meta: {
|
||||
title: '导航2-2'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'page1',
|
||||
name: 'multilevelMenuExample2-2-1',
|
||||
component: () => import('@/views/multilevel_menu_example/level2/level3/page1.vue'),
|
||||
meta: {
|
||||
title: '导航2-2-1'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'page2',
|
||||
name: 'multilevelMenuExample2-2-2',
|
||||
component: () => import('@/views/multilevel_menu_example/level2/level3/page2.vue'),
|
||||
meta: {
|
||||
title: '导航2-2-2'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { Route } from '@/global'
|
||||
|
||||
const Layout = () => import('@/layouts/index.vue')
|
||||
|
||||
const routes: Route.recordRaw = {
|
||||
path: '/multilevel_menu_example',
|
||||
component: Layout,
|
||||
redirect: '/multilevel_menu_example/page',
|
||||
name: 'multilevelMenuExample',
|
||||
meta: {
|
||||
title: '多级导航',
|
||||
icon: 'sidebar-menu',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'page',
|
||||
name: 'multilevelMenuExample1',
|
||||
component: () => import('@/views/multilevel_menu_example/page.vue'),
|
||||
meta: {
|
||||
title: '导航1',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'level2',
|
||||
name: 'multilevelMenuExample2',
|
||||
redirect: '/multilevel_menu_example/level2/page',
|
||||
meta: {
|
||||
title: '导航2',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'page',
|
||||
name: 'multilevelMenuExample2-1',
|
||||
component: () => import('@/views/multilevel_menu_example/level2/page.vue'),
|
||||
meta: {
|
||||
title: '导航2-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'level3',
|
||||
name: 'multilevelMenuExample2-2',
|
||||
redirect: '/multilevel_menu_example/level2/level3/page1',
|
||||
meta: {
|
||||
title: '导航2-2',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'page1',
|
||||
name: 'multilevelMenuExample2-2-1',
|
||||
component: () => import('@/views/multilevel_menu_example/level2/level3/page1.vue'),
|
||||
meta: {
|
||||
title: '导航2-2-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'page2',
|
||||
name: 'multilevelMenuExample2-2-2',
|
||||
component: () => import('@/views/multilevel_menu_example/level2/level3/page2.vue'),
|
||||
meta: {
|
||||
title: '导航2-2-2',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default routes
|
||||
@@ -1,26 +0,0 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
path: '/permission_example',
|
||||
component: Layout,
|
||||
redirect: '/permission_example/index',
|
||||
name: 'permissionExample',
|
||||
meta: {
|
||||
title: '权限验证',
|
||||
icon: 'sidebar-permission'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'permissionExampleIndex',
|
||||
component: () => import('@/views/permission_example/index.vue'),
|
||||
meta: {
|
||||
title: '权限验证',
|
||||
auth: ['permission.browse'],
|
||||
sidebar: false,
|
||||
breadcrumb: false,
|
||||
activeMenu: '/permission_example'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { Route } from '@/global'
|
||||
|
||||
const Layout = () => import('@/layouts/index.vue')
|
||||
|
||||
const routes: Route.recordRaw = {
|
||||
path: '/permission_example',
|
||||
component: Layout,
|
||||
redirect: '/permission_example/index',
|
||||
name: 'permissionExample',
|
||||
meta: {
|
||||
title: '权限验证',
|
||||
icon: 'ep:key',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'permissionExampleIndex',
|
||||
component: () => import('@/views/permission_example/index.vue'),
|
||||
meta: {
|
||||
title: '权限验证',
|
||||
auth: ['permission.browse'],
|
||||
sidebar: false,
|
||||
breadcrumb: false,
|
||||
activeMenu: '/permission_example',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default routes
|
||||