init
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
watch: {
|
||||
'$store.state.keepAlive.list'(val) {
|
||||
process.env.NODE_ENV == 'development' && console.log(`[ keepAliveList ] ${val}`)
|
||||
},
|
||||
'$store.state.settings.mode': {
|
||||
handler() {
|
||||
if (this.$store.state.settings.mode == 'pc') {
|
||||
this.$store.commit('settings/updateThemeSetting', {
|
||||
'sidebarCollapse': this.$store.state.settings.sidebarCollapseLastStatus
|
||||
})
|
||||
} else if (this.$store.state.settings.mode == 'mobile') {
|
||||
this.$store.commit('settings/updateThemeSetting', {
|
||||
'sidebarCollapse': true
|
||||
})
|
||||
}
|
||||
document.body.setAttribute('data-mode', this.$store.state.settings.mode)
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.onresize = () => {
|
||||
this.$store.commit('settings/setMode', document.body.clientWidth)
|
||||
}
|
||||
window.onresize()
|
||||
},
|
||||
methods: {},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$store.state.settings.enableDynamicTitle && this.$store.state.settings.title,
|
||||
titleTemplate: title => {
|
||||
return title ? `${title} - ${process.env.VUE_APP_TITLE}` : process.env.VUE_APP_TITLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
import axios from 'axios'
|
||||
// import Qs from 'qs'
|
||||
import router from '@/router/index'
|
||||
import store from '@/store/index'
|
||||
import { Message } from 'element-ui'
|
||||
|
||||
const toLogin = () => {
|
||||
router.push({
|
||||
path: '/login',
|
||||
query: {
|
||||
redirect: router.currentRoute.fullPath
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: process.env.VUE_APP_API_ROOT,
|
||||
timeout: 10000,
|
||||
responseType: 'json'
|
||||
// withCredentials: true
|
||||
})
|
||||
|
||||
api.interceptors.request.use(
|
||||
request => {
|
||||
if (request.method == 'post') {
|
||||
if (request.data instanceof FormData) {
|
||||
if (store.getters['user/isLogin']) {
|
||||
// 如果是 FormData 类型(上传图片)
|
||||
request.data.append('token', store.state.user.token)
|
||||
}
|
||||
} else {
|
||||
// 带上 token
|
||||
if (request.data == undefined) {
|
||||
request.data = {}
|
||||
}
|
||||
if (store.getters['user/isLogin']) {
|
||||
request.data.token = store.state.user.token
|
||||
}
|
||||
// request.data = Qs.stringify(request.data)
|
||||
}
|
||||
} else {
|
||||
// 带上 token
|
||||
if (request.params == undefined) {
|
||||
request.params = {}
|
||||
}
|
||||
if (store.getters['user/isLogin']) {
|
||||
request.params.token = store.state.user.token
|
||||
}
|
||||
}
|
||||
return request
|
||||
}
|
||||
)
|
||||
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
if (response.data.error != '') {
|
||||
// 如果接口请求时发现 token 失效,则立马跳转到登录页
|
||||
if (response.data.status == 0) {
|
||||
toLogin()
|
||||
}
|
||||
Message.error(response.data.error)
|
||||
return Promise.reject(response.data)
|
||||
}
|
||||
return Promise.resolve(response.data)
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default api
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1028 1024" xmlns="http://www.w3.org/2000/svg" width="200.781" height="200"><path d="M989.867 234.667H499.2c-17.067 0-34.133-21.334-34.133-42.667 0-25.6 12.8-42.667 34.133-42.667h490.667c17.066 0 34.133 17.067 34.133 42.667 0 21.333-12.8 42.667-34.133 42.667zm-473.6 128h465.066c25.6 0 46.934 21.333 46.934 42.666 0 25.6-21.334 42.667-46.934 42.667H516.267c-25.6 0-46.934-17.067-46.934-42.667s21.334-42.666 46.934-42.666zm0 298.666c-25.6 0-46.934-21.333-46.934-42.666 0-25.6 21.334-42.667 46.934-42.667h465.066c25.6 0 46.934 17.067 46.934 42.667s-21.334 42.666-46.934 42.666H516.267zm4.266 128H972.8c29.867 0 51.2 17.067 51.2 42.667s-21.333 42.667-51.2 42.667H520.533c-29.866 0-51.2-17.067-51.2-42.667s21.334-42.667 51.2-42.667zm-192 25.6c-17.066 17.067-46.933 17.067-64 0L12.8 541.867c-17.067-17.067-17.067-51.2 0-68.267l251.733-273.067c17.067-17.066 46.934-17.066 64 0s17.067 51.2 0 68.267L106.667 507.733l221.866 238.934c17.067 21.333 17.067 51.2 0 68.266z"/></svg>
|
||||
|
After Width: | Height: | Size: 998 B |
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 944 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M512 102.4A409.6 409.6 0 1 0 921.6 512 409.6 409.6 0 0 0 512 102.4zm0 760.448A350.848 350.848 0 1 1 862.848 512 351.36 351.36 0 0 1 512 862.848z"/><path d="M690.304 300.928a29.184 29.184 0 0 0-40.96 5.888L515.712 485.12h-7.424L374.528 307.2a29.312 29.312 0 0 0-46.72 35.2L435.2 485.12h-54.784a29.184 29.184 0 1 0 0 58.368h98.688l3.712 4.864v38.4h-102.4a29.312 29.312 0 0 0 0 58.496h102.4v102.4a29.184 29.184 0 1 0 58.368 0v-102.4h102.4a29.312 29.312 0 0 0 0-58.496h-102.4v-38.4l3.712-4.864h98.688a29.184 29.184 0 0 0 0-58.368H588.8l107.52-143.232a29.312 29.312 0 0 0-6.016-40.96z"/></svg>
|
||||
|
After Width: | Height: | Size: 697 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1113 1024" xmlns="http://www.w3.org/2000/svg" width="217.383" height="200"><path d="M639.918 471.178c-24.795.045-44.88 20.144-44.903 44.939 0 24.85 20.143 45.061 44.903 45.061 24.759 0 44.902-20.211 44.902-45.059-.022-24.798-20.107-44.897-44.898-44.941zm399.873-346.139c0-34.621-28.116-62.775-62.668-62.775H302.71c-34.552 0-62.657 28.154-62.657 62.766v62.775h54.322v-71.12h691.082v443.348h-70.993v54.421h62.657c34.553 0 62.668-28.164 62.668-62.766V125.029zm-577.35 346.139c-24.8.038-44.89 20.14-44.912 44.939 0 24.85 20.152 45.061 44.912 45.061s44.902-20.211 44.902-45.059c-.022-24.798-20.108-44.897-44.898-44.941z"/><path d="M862.316 302.818c0-34.611-28.116-62.766-62.668-62.766H125.235c-34.553 0-62.657 28.154-62.657 62.766v426.65c0 34.611 28.105 62.766 62.657 62.766h79.318v166.05l165.697-166.05h429.398c34.552 0 62.657-28.154 62.657-62.766v-426.65zM804.5 734.329H346.332l-83.975 84.111v-84.111H120.373V297.956H804.51v436.382z"/><path d="M284.966 471.178c-24.8.038-44.89 20.14-44.912 44.939 0 24.85 20.152 45.061 44.903 45.061 24.77 0 44.922-20.211 44.922-45.059-.022-24.801-20.113-44.902-44.909-44.941z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="m541.141 268.864 61.718 16.939-132.395 482.474-61.717-16.938L541.14 268.864zm-212.138 29.803 44.885 45.61-175.36 172.587 175.04 167.573-44.267 46.23-222.634-213.163 222.336-218.837zm355.882 0 222.336 218.837-222.634 213.163-44.267-46.23 175.019-167.573L640 344.277l44.885-45.61z"/></svg>
|
||||
|
After Width: | Height: | Size: 396 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M766.46 172.7H610.53c-17.51 0-34.29 5.42-48.5 15.67l-29.79 21.49a27.623 27.623 0 0 1-32.34 0l-29.79-21.49c-14.21-10.25-30.98-15.67-48.5-15.67H257.54c-45.71 0-82.9 37.19-82.9 82.9V726c0 45.71 37.19 82.9 82.9 82.9h164.07c5.85 0 11.43 1.8 16.17 5.22l29.79 21.49a82.783 82.783 0 0 0 48.5 15.69c17 0 34-5.24 48.5-15.69l29.81-21.5c4.72-3.41 10.31-5.21 16.15-5.21h155.93c45.71 0 82.9-37.19 82.9-82.9V255.6c0-45.72-37.19-82.9-82.9-82.9zm27.63 553.29c0 15.24-12.4 27.63-27.63 27.63H610.53c-17.54 0-34.31 5.42-48.5 15.67l-29.79 21.49a27.623 27.623 0 0 1-32.34 0l-29.78-21.48c-14.2-10.27-30.97-15.68-48.51-15.68H257.54c-15.24 0-27.63-12.39-27.63-27.63V255.6c0-15.24 12.4-27.63 27.63-27.63h164.07c5.84 0 11.43 1.8 16.17 5.22l29.79 21.49c5.28 3.81 10.97 6.68 16.8 9.11v239.83c0 15.26 12.37 27.63 27.63 27.63s27.63-12.37 27.63-27.63V266.83c8.76-2.6 17.21-6.58 24.93-12.15l29.79-21.49a27.484 27.484 0 0 1 16.17-5.22h155.93c15.24 0 27.63 12.39 27.63 27.63v470.39z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M878.84 489.558c-29.743-47-42.633-97.607-50.35-151.176C800.417 143.543 608.343 17.635 422.354 75.3 299.54 113.381 224.75 199.036 199.868 325.796c-11.624 59.218-22.74 117-56.89 169.79-34.466 53.28-43.74 115.843-39.244 179.3 2.598 36.665 13.79 70.055 40.995 97.233 8.184 8.174 16.292 21.674 15.855 32.386-3.144 77.23 42.516 118.05 107.2 140.595 71.654 24.975 144.968 23.878 215.783-5.914 19.299-8.12 34.811-11.058 55.074-1.221 58.244 28.272 120.867 28.719 182.709 17.476 98.704-17.944 142.118-71.041 142.442-169.584 30.923-22.389 50.298-53.787 54.758-93.077 8.156-71.806-.712-141.6-39.71-203.222zm-40.041 168.231c-.549 18.204-7.914 36.202-15.062 66.66l-54.693-46.324c-17.599 26.982-34.342 52.65-52.53 80.537 10.998 6.033 23.923 10.175 32.673 18.636 12.176 11.773 27.23 25.605 30.188 40.686 4.279 21.827-15.523 34.544-33.882 42.758-58.292 26.08-118.836 29-175.529.451-40.527-20.408-73.857-21.777-114.39-.441-53.108 27.95-110.563 24.686-166.789 4.917-6.126-2.154-12.068-5.221-17.572-8.692-36.836-23.223-37.796-52.719-2.018-76.576 11.974-7.985 25.356-13.857 38.788-21.068-17.693-27.946-34.155-53.953-51.922-82.017l-45.513 38.839c-28.297-21.745-26.875-48.763-25.337-74.01 3.245-53.263 17.492-103.337 53.944-144.31 13.664-15.36 18.348-31.765 21.015-52.399 5.81-44.936 14.763-89.923 27.454-133.424 30.692-105.216 129.925-174.186 237.442-168.36 105.725 5.73 195.386 82.577 217.572 187.954 9.343 44.375 15.066 89.525 24.912 133.772 2.943 13.225 13.255 25.37 21.945 36.658 35.44 46.039 51.026 98.507 49.304 155.753z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M768 298.667h170.667V384h-256V128H768v170.667zM341.333 384h-256v-85.333H256V128h85.333v256zM768 725.333V896h-85.333V640h256v85.333H768zM341.333 640v256H256V725.333H85.333V640h256z"/></svg>
|
||||
|
After Width: | Height: | Size: 299 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M682.667 128h256v256h-85.334V213.333H682.667V128zm-597.334 0h256v85.333H170.667V384H85.333V128zm768 682.667V640h85.334v256h-256v-85.333h170.666zm-682.666 0h170.666V896h-256V640h85.334v170.667z"/></svg>
|
||||
|
After Width: | Height: | Size: 312 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M704 328a72 72 0 1 0 144 0 72 72 0 1 0-144 0z"/><path d="M999.904 116.608a32 32 0 0 0-21.952-10.912L521.76 73.792a31.552 31.552 0 0 0-27.2 11.904l-92.192 114.848a32 32 0 0 0 .672 40.896l146.144 169.952-147.456 194.656 36.48-173.376a32 32 0 0 0-11.136-31.424L235.616 245.504l79.616-125.696a32 32 0 0 0-29.28-49.024L45.76 87.552a32 32 0 0 0-29.696 34.176l55.808 798.016a32.064 32.064 0 0 0 34.304 29.696l176.512-13.184c17.632-1.312 30.848-16.672 29.504-34.272s-16.576-31.04-34.304-29.536L133.44 883.232l-6.432-92.512 125.312-12.576a32 32 0 0 0 28.672-35.04 32.16 32.16 0 0 0-35.04-28.672L122.56 726.848 82.144 149.184l145.152-10.144-60.96 96.224a32 32 0 0 0 6.848 41.952l198.4 161.344-58.752 279.296a30.912 30.912 0 0 0 .736 14.752 31.68 31.68 0 0 0 1.408 11.04l51.52 154.56a31.968 31.968 0 0 0 27.456 21.76l523.104 47.552a32.064 32.064 0 0 0 34.848-29.632l55.776-798.048a32.064 32.064 0 0 0-7.776-23.232zm-98.912 630.848-412.576-39.648a31.52 31.52 0 0 0-34.912 28.768 32 32 0 0 0 28.8 34.912l414.24 39.808-6.272 89.536-469.728-42.72-39.584-118.72 234.816-310.016a31.936 31.936 0 0 0-1.248-40.192L468.896 219.84l65.088-81.056 407.584 28.48-40.576 580.192z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 623 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M798.663 952.212h-565.04a165.7 165.7 0 0 1-165.701-165.7v-426.68a55.234 55.234 0 0 1 82.85-47.225l156.311 94.173 165.701-175.92A53.024 53.024 0 0 1 512 213.187a55.234 55.234 0 0 1 40.32 18.227l157.416 174.538 152.998-93.344a55.234 55.234 0 0 1 82.85 47.224V693.72a55.234 55.234 0 0 1-110.467 0V458.147l-107.982 66.004a55.234 55.234 0 0 1-69.87-10.218L512 350.165 357.346 515.866a55.234 55.234 0 0 1-68.766 9.39l-110.467-66.833v326.983a55.234 55.234 0 0 0 55.233 55.234h565.317a55.234 55.234 0 0 1 0 110.467z" fill="#FC455D"/><path d="M118.694 239.43900000000002a55.234 55.234 0 1 0 78.112-78.112 55.234 55.234 0 1 0-78.112 78.112zM472.83099999999996 137.748a55.234 55.234 0 1 0 78.113-78.113 55.234 55.234 0 1 0-78.113 78.113zM818.258 239.404a55.234 55.234 0 1 0 78.113-78.112 55.234 55.234 0 1 0-78.113 78.112z" fill="#FF9B57"/><path d="M517.247 795.349a41.425 41.425 0 0 1-32.864-17.123l-93.62-128.142a41.425 41.425 0 0 1 66.003-48.606l57.167 78.985 49.434-77.051a41.425 41.425 0 1 1 69.595 44.739l-82.85 128.142a41.425 41.425 0 0 1-33.693 19.056z" fill="#FD8F9E"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M960 416V192l-73.056 73.056a447.712 447.712 0 0 0-373.6-201.088C265.92 63.968 65.312 264.544 65.312 512S265.92 960.032 513.344 960.032a448.064 448.064 0 0 0 415.232-279.488 38.368 38.368 0 1 0-71.136-28.896 371.36 371.36 0 0 1-344.096 231.584c-205.024 0-371.232-166.208-371.232-371.232S308.32 140.768 513.344 140.768c132.448 0 251.936 70.08 318.016 179.84L736 416h224z"/></svg>
|
||||
|
After Width: | Height: | Size: 488 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="m936.833 853.8-164.3-164.3c52.5-65.9 83.9-149.3 83.9-240.1 0-212.9-172.6-385.6-385.6-385.6s-385.6 172.7-385.6 385.6 172.6 385.6 385.6 385.6c90.9 0 174.5-31.5 240.4-84.1l164.3 164.3c16.7 16.7 44 16.7 60.7 0l.6-.6c16.7-16.8 16.7-44.1 0-60.8zm-764.8-404.4c0-165 133.8-298.8 298.8-298.8s298.8 133.8 298.8 298.8-133.8 298.9-298.8 298.9-298.8-133.8-298.8-298.9z"/></svg>
|
||||
|
After Width: | Height: | Size: 475 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M165.415 827.077c-11.815 0-19.692-7.877-19.692-19.692V214.646c0-11.815 7.877-19.692 19.692-19.692h159.508c7.877 0 17.723 3.938 23.63 9.846l228.432 287.508c7.877 9.846 7.877 25.6 0 37.415l-230.4 287.508c-5.908 7.877-15.754 11.815-25.6 11.815l-155.57-1.97zm706.954-334.77L641.97 206.77c-9.846-11.815-27.569-15.754-41.354-3.938l-45.292 37.415c-13.785 9.846-15.754 29.539-3.938 41.354L738.462 512 551.385 744.37c-9.847 11.815-7.877 31.507 3.938 41.353l45.292 37.415c13.785 9.847 29.539 7.877 41.354-3.938l230.4-287.508c7.877-15.754 7.877-29.538 0-39.384z"/></svg>
|
||||
|
After Width: | Height: | Size: 670 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M914.5 653.5c-5.5 0-11 1.1-16 3.3l-.2.1h-.2L510.2 822.2 122.2 657h-.2l-.2-.1c-5-2.1-10.3-3.3-16-3.3-23.1 0-41.8 19.3-41.8 43.1 0 18 10.7 33.3 25.8 39.8l403.9 172.1.4.1c10.2 4.4 21.8 4.4 32 0l.2-.1c.1 0 .1-.1.2-.1l403.9-172.1c15.1-6.5 25.8-21.8 25.8-39.8.1-23.8-18.6-43.1-41.7-43.1zm0-186.5c-7.9-.2-16 3.2-16 3.2L510.2 635.6 121.8 470.2s-10.3-3.2-16-3.2C82.7 467 64 486.2 64 510c0 17.9 10.7 33.3 25.8 39.7l403.9 172c.1 0 .1.1.2.1l.1.1c5 2.1 10.3 3.3 16 3.3 5.7 0 11.1-1.2 16-3.3l.2-.1c.1 0 .1 0 .2-.1l403.9-172c15.1-6.4 25.8-21.8 25.9-39.7.1-23.8-18.6-43-41.7-43zM89.8 363.2l403.9 172.1c.1 0 .1 0 .2.1l.1.1c5 2.1 10.3 3.2 16 3.2 5.5 0 10.9-1.1 16-3.2l.2-.1.2-.1 403.9-172c15.1-6.5 25.8-21.8 25.9-39.7 0-18-10.7-33.3-25.8-39.8L526.5 111.6c-.1 0-.1 0-.2-.1l-.2-.1c-10.2-4.4-21.8-4.4-32 0l-.1.1L89.8 283.7C74.7 290.1 64 305.5 64 323.5c0 17.9 10.7 33.2 25.8 39.7z"/></svg>
|
||||
|
After Width: | Height: | Size: 978 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M487 71.425a75 75 0 0 1 72.575 0l335.7 185.5a75 75 0 0 1 38.75 65.65V690.85a75 75 0 0 1-38.75 65.625l-335.7 185.55a75 75 0 0 1-72.55 0l-335.7-185.5a75 75 0 0 1-38.75-65.675V322.6a75 75 0 0 1 38.75-65.6L487 71.4v.025zM859 322.6 523.275 137.1l-335.7 185.5v368.25l335.7 185.5 335.75-185.5V322.6zm-601.75 37.1A37.5 37.5 0 0 1 308.2 345l215.1 118.875L738.45 345a37.5 37.5 0 0 1 36.25 65.625l-213.9 118.25V764.55a37.5 37.5 0 0 1-75 0v-235.7l-213.9-118.2a37.5 37.5 0 0 1-14.65-51v.05z"/></svg>
|
||||
|
After Width: | Height: | Size: 597 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M804.571 530.286v182.857q0 68-48.285 116.286T640 877.714H164.571q-68 0-116.285-48.285T0 713.143V237.714q0-68 48.286-116.285T164.57 73.143h402.286q8 0 13.143 5.143t5.143 13.143V128q0 8-5.143 13.143t-13.143 5.143H164.571q-37.714 0-64.571 26.857t-26.857 64.571v475.429q0 37.714 26.857 64.571t64.571 26.857H640q37.714 0 64.571-26.857t26.858-64.571V530.286q0-8 5.142-13.143T749.714 512h36.572q8 0 13.143 5.143t5.142 13.143zM1024 36.57v292.572q0 14.857-10.857 25.714t-25.714 10.857-25.715-10.857L861.143 254.286 488.57 626.857q-5.714 5.714-13.142 5.714t-13.143-5.714l-65.143-65.143q-5.714-5.714-5.714-13.143t5.714-13.142l372.571-372.572L669.143 62.286q-10.857-10.857-10.857-25.715t10.857-25.714T694.857 0H987.43q14.857 0 25.714 10.857T1024 36.571z"/></svg>
|
||||
|
After Width: | Height: | Size: 861 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M440.064 761.856A319.36 319.36 0 0 1 320 512a319.36 319.36 0 0 1 120.064-249.856 256 256 0 1 0 0 499.712zM512 218.624a320 320 0 1 1 0 586.752 320 320 0 1 1 0-586.752zm63.168 376.96L469.76 497.6l-43.52 46.848 150.592 139.968 277.76-277.76-45.248-45.312-234.176 234.24z"/></svg>
|
||||
|
After Width: | Height: | Size: 387 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M781.692 208.636H242.367c-9.309 0-17.229 3.307-23.844 9.868-6.575 6.624-9.877 14.544-9.877 23.854v101.126h606.748V242.363c0-9.31-3.307-17.23-9.878-23.855-6.625-6.56-14.545-9.867-23.844-9.867h.02zm33.702 573.046V410.879H444.591v404.505h337.081c9.3 0 17.224-3.307 23.844-9.878 6.576-6.61 9.878-14.544 9.878-23.844v.02zm-438.208 33.702V410.88H208.65v370.803c0 9.314 3.303 17.264 9.873 23.83 6.615 6.62 14.535 9.892 23.844 9.892h134.82v-.02zM242.348 141.211h539.324c27.893 0 51.747 9.878 71.503 29.624 19.746 19.756 29.614 43.58 29.614 71.488v539.34c0 27.892-9.868 51.737-29.614 71.492-19.756 19.756-43.61 29.634-71.503 29.634H242.348c-27.913 0-51.748-9.878-71.503-29.634-19.756-19.755-29.634-43.605-29.634-71.493V242.328c0-27.913 9.878-51.737 29.634-71.493 19.755-19.746 43.59-29.624 71.503-29.624z"/></svg>
|
||||
|
After Width: | Height: | Size: 915 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M913.6 135.2H380.2c-26.7 0-44.4 17.8-44.4 44.4V224c0 26.7 17.8 44.4 44.4 44.4h533.3c26.7 0 44.4-17.8 44.4-44.4v-44.4c.1-26.6-17.7-44.4-44.3-44.4zm-755.6 0h-44.4c-22.2 0-44.4 22.2-44.4 44.4V224c0 22.2 22.2 44.4 44.4 44.4H158c22.2 0 44.4-22.2 44.4-44.4v-44.4c.1-22.2-22.1-44.4-44.4-44.4zm0 311.1h-44.4c-22.2 0-44.4 22.2-44.4 44.4v44.4c0 22.2 22.2 44.4 44.4 44.4H158c22.2 0 44.4-22.2 44.4-44.4v-44.4c.1-22.2-22.1-44.4-44.4-44.4zm755.6 0H380.2c-26.7 0-44.4 17.8-44.4 44.4v44.4c0 26.7 17.8 44.4 44.4 44.4h533.3c26.7 0 44.4-17.8 44.4-44.4v-44.4c.1-26.6-17.7-44.4-44.3-44.4zM158 757.4h-44.4c-22.2 0-44.4 22.2-44.4 44.4v44.4c0 22.2 22.2 44.4 44.4 44.4H158c22.2 0 44.4-22.2 44.4-44.4v-44.4c.1-22.2-22.1-44.4-44.4-44.4zm755.6 0H380.2c-26.7 0-44.4 17.8-44.4 44.4v44.4c0 26.7 17.8 44.4 44.4 44.4h533.3c26.7 0 44.4-17.8 44.4-44.4v-44.4c.1-26.6-17.7-44.4-44.3-44.4z"/></svg>
|
||||
|
After Width: | Height: | Size: 971 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M512 384c-204.8 0-426.667-55.467-426.667-170.667S307.2 42.667 512 42.667s426.667 55.466 426.667 170.666S716.8 384 512 384zm0-256c-221.867 0-341.333 64-341.333 85.333S290.133 298.667 512 298.667s341.333-64 341.333-85.334S733.867 128 512 128zm0 554.667C307.2 682.667 85.333 627.2 85.333 512c0-25.6 17.067-42.667 42.667-42.667S170.667 486.4 170.667 512c0 21.333 106.666 85.333 341.333 85.333s341.333-64 341.333-85.333c0-25.6 17.067-42.667 42.667-42.667S938.667 486.4 938.667 512c0 115.2-221.867 170.667-426.667 170.667z"/><path d="M512 981.333c-204.8 0-426.667-55.466-426.667-170.666V213.333c0-25.6 17.067-42.666 42.667-42.666s42.667 17.066 42.667 42.666v597.334C170.667 832 277.333 896 512 896s341.333-64 341.333-85.333V213.333c0-25.6 17.067-42.666 42.667-42.666s42.667 17.066 42.667 42.666v597.334c0 115.2-221.867 170.666-426.667 170.666z"/></svg>
|
||||
|
After Width: | Height: | Size: 955 B |
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 844 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M84.48 724.48h852.48V128H84.48v596.48zM0 128c0-46.08 38.4-84.48 84.48-84.48h852.48c48.64 0 87.04 38.4 87.04 84.48v596.48c0 46.08-38.4 84.48-84.48 84.48H84.48C38.4 808.96 0 770.56 0 724.48V128z"/><path d="M468.48 724.48h84.48v212.48h-84.48V724.48z"/><path d="M299.52 896h427.52v84.48H299.52V896zm89.6-588.8v230.4l245.76-122.88z"/></svg>
|
||||
|
After Width: | Height: | Size: 446 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M524.48 965.12a456 456 0 0 1-147.52-24.64 32 32 0 0 1-19.52-40.64A32 32 0 0 1 398.4 880a362.24 362.24 0 0 0 126.08 21.12 384 384 0 0 0 125.76-21.44 184.64 184.64 0 0 1-7.36-57.92 192 192 0 0 1 243.2-177.92 384 384 0 0 0-19.84-299.52 32 32 0 1 1 56.96-29.12 448 448 0 0 1 18.24 363.2 51.52 51.52 0 0 1-65.92 29.44 121.6 121.6 0 0 0-45.76-7.36 128 128 0 0 0-122.88 122.88 114.56 114.56 0 0 0 7.36 45.76 51.52 51.52 0 0 1-29.44 65.92 437.12 437.12 0 0 1-160.32 30.08zm137.28-89.6zM130.24 684.8a32 32 0 0 1-30.08-21.44 441.28 441.28 0 0 1-24.64-146.24 448 448 0 0 1 448-448 441.92 441.92 0 0 1 201.92 48.32 32 32 0 0 1-29.12 56.96 375.68 375.68 0 0 0-172.8-41.28A384 384 0 0 0 160 642.24a32 32 0 0 1-19.52 40.96 40.32 40.32 0 0 1-10.24 1.6z"/><path d="M399.04 756.8a56 56 0 0 1-40.64-17.6l-89.28-97.6a55.04 55.04 0 0 1 8-81.28L849.28 136a48.96 48.96 0 0 1 68.48 0 49.6 49.6 0 0 1-5.76 69.12L440.32 738.24a54.72 54.72 0 0 1-40.96 18.56zm6.72-61.12zm-82.56-89.92 75.84 82.56 358.08-404.48z"/><path d="M236.48 930.88h-7.68a169.92 169.92 0 0 1-117.44-54.72 91.84 91.84 0 0 1-30.08-71.04l2.24-16.32 66.56-34.56-4.48-41.28v-2.24a83.52 83.52 0 0 1 23.68-59.84c92.8-74.88 155.52-43.52 192-3.84A169.28 169.28 0 0 1 236.8 930.88zm-84.16-105.6A61.44 61.44 0 0 0 160 832a104.64 104.64 0 0 0 148.8 6.4 105.28 105.28 0 0 0 6.4-148.8c-19.84-21.44-46.08-35.52-101.76 8.32a56.32 56.32 0 0 0-2.56 12.8l4.8 42.24a55.04 55.04 0 0 1-29.44 54.72zm60.16-128zM196.48 454.08a39.68 39.68 0 1 0 79.36 0 39.68 39.68 0 1 0-79.36 0zM256 325.44a29.76 29.76 0 1 0 59.52 0 29.76 29.76 0 1 0-59.52 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 440 B |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,3 @@
|
||||
// 勿删
|
||||
// sass-resources-loader@2.2.1 版本开始,resources 如果指向的资源目录不存在文件,运行会报错
|
||||
// 所以当精灵图目录为空的时候,保留一个默认的 scss 文件
|
||||
@@ -0,0 +1,41 @@
|
||||
::-webkit-scrollbar {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
background-clip: padding-box;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 7px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-track:hover {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
background-color: $g-app-bg;
|
||||
box-sizing: border-box;
|
||||
font-family: Lato, PingFang SC, Microsoft YaHei, sans-serif;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
&.hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
* {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
// textarea 字体跟随系统
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// @mixin 通过 @include 调用使用
|
||||
// % 通过 @extend 调用使用
|
||||
|
||||
// 文字超出隐藏,默认为单行超出隐藏,可设置多行
|
||||
@mixin text-overflow($line: 1, $fixed-width: true) {
|
||||
@if ($line==1 and $fixed-width==true) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@else {
|
||||
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%);
|
||||
}
|
||||
}
|
||||
|
||||
// 文字两端对齐
|
||||
%justify-align {
|
||||
text-align: justify;
|
||||
text-align-last: justify;
|
||||
}
|
||||
|
||||
// 清除浮动
|
||||
%clearfix {
|
||||
zoom: 1;
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// 全局变量
|
||||
|
||||
// 头部宽度(默认自适应宽度,可固定宽度,固定宽度后为居中显示)
|
||||
$g-header-width: 100%;
|
||||
// 头部高度
|
||||
$g-header-height: 70px;
|
||||
// 侧边栏宽度
|
||||
$g-main-sidebar-width: 60px;
|
||||
$g-sub-sidebar-width: 220px;
|
||||
$g-sidebar-width: var(--real-sidebar-width);
|
||||
// 侧边栏Logo高度
|
||||
$g-sidebar-logo-height: 50px;
|
||||
// 顶部导航栏高度
|
||||
$g-topbar-height: 50px;
|
||||
|
||||
// 应用背景色
|
||||
$g-app-bg: #fff;
|
||||
// 主区域背景色
|
||||
$g-main-bg: #f5f7f9;
|
||||
|
||||
// 头部背景色
|
||||
$g-header-bg: #222b45;
|
||||
// 头部导航文字颜色
|
||||
$g-header-menu-color: #fff;
|
||||
// 头部导航选中颜色
|
||||
$g-header-menu-active-bg: lighten($g-header-bg, 10);
|
||||
|
||||
// 主侧边栏背景色
|
||||
$g-main-sidebar-bg: #222b45;
|
||||
// 主侧边栏文字颜色
|
||||
$g-main-sidebar-menu-color: #fff;
|
||||
// 主侧边栏菜单选中背景色
|
||||
$g-main-sidebar-menu-active-bg: lighten($g-main-sidebar-bg, 10);
|
||||
|
||||
// 次侧边栏背景色
|
||||
$g-sub-sidebar-bg: #fafafa;
|
||||
// 次侧边栏菜单文字颜色
|
||||
$g-sub-sidebar-menu-color: #37414b;
|
||||
// 次侧边栏菜单文字选中颜色
|
||||
$g-sub-sidebar-menu-active-color: #e7f4ff;
|
||||
// 次侧边栏菜单选中背景色
|
||||
$g-sub-sidebar-menu-active-bg: #5482ee;
|
||||
|
||||
// 输出给js使用
|
||||
:export {
|
||||
g_main_sidebar_width: $g-main-sidebar-width;
|
||||
g_sub_sidebar_width: $g-sub-sidebar-width;
|
||||
g_sidebar_width: $g-main-sidebar-width + $g-sub-sidebar-width;
|
||||
g_app_bg: $g-app-bg;
|
||||
g_main_bg: $g-main-bg;
|
||||
g_header_bg: $g-header-bg;
|
||||
g_header_menu_color: $g-header-menu-color;
|
||||
g_header_menu_active_bg: $g-header-menu-active-bg;
|
||||
g_main_sidebar_bg: $g-main-sidebar-bg;
|
||||
g_main_sidebar_menu_color: $g-main-sidebar-menu-color;
|
||||
g_main_sidebar_menu_active_bg: $g-main-sidebar-menu-active-bg;
|
||||
g_sub_sidebar_bg: $g-sub-sidebar-bg;
|
||||
g_sub_sidebar_menu_color: $g-sub-sidebar-menu-color;
|
||||
g_sub_sidebar_menu_active_color: $g-sub-sidebar-menu-active-color;
|
||||
g_sub_sidebar_menu_active_bg: $g-sub-sidebar-menu-active-bg;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="absolute-container">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AbsoluteContainer',
|
||||
props: {},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
mounted() {},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.absolute-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: calc(100% - #{$g-topbar-height});
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<slot v-if="check()" />
|
||||
<slot v-else name="no-auth" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { auth } from '@/util'
|
||||
|
||||
export default {
|
||||
name: 'Auth',
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Array],
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
check() {
|
||||
return auth(this.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<slot v-if="check()" />
|
||||
<slot v-else name="no-auth" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { authAll } from '@/util'
|
||||
|
||||
export default {
|
||||
name: 'AuthAll',
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
check() {
|
||||
return authAll(this.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,80 @@
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BatchActionBar',
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectionData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
checkAll: {
|
||||
get: function() {
|
||||
let flag = false
|
||||
if (this.data.length != 0 && this.data.length == this.selectionData.length) {
|
||||
flag = true
|
||||
}
|
||||
return flag
|
||||
},
|
||||
set: function(newVal) {
|
||||
newVal ? this.$emit('check-all') : this.$emit('check-null')
|
||||
}
|
||||
},
|
||||
isIndeterminate() {
|
||||
let flag = false
|
||||
if (this.selectionData.length > 0 && this.selectionData.length < this.data.length) {
|
||||
flag = true
|
||||
}
|
||||
return flag
|
||||
}
|
||||
},
|
||||
created() {},
|
||||
mounted() {},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.batch-action-bar {
|
||||
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;
|
||||
}
|
||||
::v-deep .el-form {
|
||||
> .el-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
> .el-button-group {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<el-cascader v-model="myValue" :options="options" :props="props" :size="size" :disabled="disabled" clearable filterable />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import area from '@/util/area'
|
||||
|
||||
export default {
|
||||
name: 'CascaderArea',
|
||||
props: {
|
||||
// 传入数据为中文数组,例如:['河北省', '唐山市', '市辖区']
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
options: area,
|
||||
props: {
|
||||
value: 'code',
|
||||
label: 'name'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
myValue: {
|
||||
// 将中文转成 code 码
|
||||
get: function() {
|
||||
const myValue = []
|
||||
if (this.value.length === 3) {
|
||||
const [pName, cName, aName] = this.value
|
||||
area.map(p => {
|
||||
if (p.name === pName) {
|
||||
myValue.push(p.code)
|
||||
p.children.map(c => {
|
||||
if (c.name === cName) {
|
||||
myValue.push(c.code)
|
||||
c.children.map(a => {
|
||||
if (a.name === aName) {
|
||||
myValue.push(a.code)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return myValue
|
||||
},
|
||||
// 将 code 码转成中文
|
||||
set: function(value) {
|
||||
let [pCode, cCode, aCode] = value
|
||||
area.map(p => {
|
||||
if (p.code === pCode) {
|
||||
pCode = p.name
|
||||
p.children.map(c => {
|
||||
if (c.code === cCode) {
|
||||
cCode = c.name
|
||||
c.children.map(a => {
|
||||
if (a.code === aCode) {
|
||||
aCode = a.name
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
this.$emit('input', [pCode, cCode, aCode])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-cascader {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<footer class="copyright">
|
||||
Copyright © {{ $store.state.settings.copyrightDates }}
|
||||
<!-- eslint-disable-next-line vue/require-component-is -->
|
||||
<component v-bind="linkProps()">{{ $store.state.settings.copyrightCompany }}</component>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Copyright',
|
||||
methods: {
|
||||
linkProps() {
|
||||
if (this.$store.state.settings.copyrightWebsite) {
|
||||
return {
|
||||
is: 'a',
|
||||
href: this.$store.state.settings.copyrightWebsite,
|
||||
target: '_blank',
|
||||
rel: 'noopener'
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
is: 'span'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
footer {
|
||||
margin: 40px 0 20px 0;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
color: #808695;
|
||||
font-size: 14px;
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #808695;
|
||||
&:hover {
|
||||
color: #3d4047;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="editor">
|
||||
<TinymceEditor v-model="myValue" :init="completeSetting" :disabled="disabled" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tinymce from 'tinymce/tinymce'
|
||||
import TinymceEditor from '@tinymce/tinymce-vue'
|
||||
import 'tinymce/themes/silver/theme'
|
||||
import 'tinymce/icons/default/icons'
|
||||
import 'tinymce/plugins/autolink'
|
||||
import 'tinymce/plugins/autoresize'
|
||||
import 'tinymce/plugins/colorpicker'
|
||||
import 'tinymce/plugins/contextmenu'
|
||||
import 'tinymce/plugins/fullscreen'
|
||||
import 'tinymce/plugins/hr'
|
||||
import 'tinymce/plugins/image'
|
||||
import 'tinymce/plugins/imagetools'
|
||||
import 'tinymce/plugins/insertdatetime'
|
||||
import 'tinymce/plugins/link'
|
||||
import 'tinymce/plugins/lists'
|
||||
import 'tinymce/plugins/media'
|
||||
import 'tinymce/plugins/preview'
|
||||
import 'tinymce/plugins/table'
|
||||
import 'tinymce/plugins/textcolor'
|
||||
import 'tinymce/plugins/wordcount'
|
||||
import 'tinymce/plugins/code'
|
||||
import 'tinymce/plugins/searchreplace'
|
||||
|
||||
export default {
|
||||
name: 'Editor',
|
||||
components: {
|
||||
TinymceEditor
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
setting: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaultSetting: {
|
||||
language_url: 'tinymce/langs/zh_CN.js',
|
||||
language: 'zh_CN',
|
||||
skin_url: 'tinymce/skins/ui/oxide',
|
||||
min_height: 250,
|
||||
max_height: 600,
|
||||
selector: 'textarea',
|
||||
plugins: 'autolink autoresize contextmenu fullscreen hr image imagetools insertdatetime link lists media preview table textcolor wordcount code searchreplace',
|
||||
toolbar: 'undo redo | formatselect | bold italic strikethrough forecolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | hr link image media table insertdatetime searchreplace removeformat | preview code fullscreen',
|
||||
branding: false,
|
||||
menubar: false,
|
||||
toolbar_mode: 'sliding',
|
||||
insertdatetime_formats: [
|
||||
'%Y年%m月%d日',
|
||||
'%H点%M分%S秒',
|
||||
'%Y-%m-%d',
|
||||
'%H:%M:%S'
|
||||
],
|
||||
images_upload_handler: (blobInfo, success) => {
|
||||
const img = 'data:image/jpeg;base64,' + blobInfo.base64()
|
||||
success(img)
|
||||
}
|
||||
},
|
||||
myValue: this.value
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
completeSetting() {
|
||||
return Object.assign(this.defaultSetting, this.setting)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
myValue(newValue) {
|
||||
this.$emit('input', newValue)
|
||||
},
|
||||
value(newValue) {
|
||||
this.myValue = newValue
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
tinymce.init({})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .tox-tinymce {
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
const component = require('./main.vue').default
|
||||
const constructor = Vue.extend(component)
|
||||
|
||||
const exampleNotice = options => {
|
||||
options = options || {}
|
||||
let instance = new constructor({
|
||||
data: options
|
||||
})
|
||||
instance.vm = instance.$mount()
|
||||
instance.dom = instance.vm.$el
|
||||
document.body.appendChild(instance.dom)
|
||||
return instance.vm
|
||||
}
|
||||
|
||||
export default {
|
||||
install: Vue => {
|
||||
Vue.prototype[`$${component.name}`] = exampleNotice
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<transition name="notice">
|
||||
<div v-if="show" class="notice">
|
||||
{{ content }}
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ExampleNotice',
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
content: ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.show = true
|
||||
setTimeout(() => {
|
||||
this.show = false
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notice {
|
||||
padding: 10px;
|
||||
background-color: #eee;
|
||||
border-radius: 10px;
|
||||
@include position-center(xy);
|
||||
}
|
||||
.notice-leave-active,
|
||||
.notice-enter-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.notice-enter,
|
||||
.notice-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<el-upload
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-exceed="onExceed"
|
||||
:on-success="onSuccess"
|
||||
:file-list="files"
|
||||
:limit="max"
|
||||
drag
|
||||
>
|
||||
<div class="slot">
|
||||
<i class="el-icon-upload" />
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</div>
|
||||
<div v-if="!notip" slot="tip" 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>
|
||||
</el-upload>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FileUpload',
|
||||
props: {
|
||||
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']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
beforeUpload(file) {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName[fileName.length - 1]
|
||||
const isTypeOk = this.ext.indexOf(fileExt) >= 0
|
||||
const isSizeOk = file.size / 1024 / 1024 < this.size
|
||||
if (!isTypeOk) {
|
||||
this.$message.error(`上传文件只支持 ${ this.ext.join(' / ') } 格式!`)
|
||||
}
|
||||
if (!isSizeOk) {
|
||||
this.$message.error(`上传文件大小不能超过 ${this.size}MB!`)
|
||||
}
|
||||
return isTypeOk && isSizeOk
|
||||
},
|
||||
onExceed() {
|
||||
this.$message.warning('文件上传超过限制')
|
||||
},
|
||||
onSuccess(res, file) {
|
||||
this.$emit('on-success', res, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .el-upload-dragger {
|
||||
width: auto;
|
||||
height: auto;
|
||||
overflow: unset;
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
}
|
||||
.slot {
|
||||
width: 300px;
|
||||
height: 160px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div :class="{
|
||||
'actionbar': true,
|
||||
'shadow': !isBottom
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FixedActionBar',
|
||||
data() {
|
||||
return {
|
||||
isBottom: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.onScroll()
|
||||
window.addEventListener('scroll', this.onScroll)
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('scroll', this.onScroll)
|
||||
},
|
||||
methods: {
|
||||
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) {
|
||||
this.isBottom = true
|
||||
} else {
|
||||
this.isBottom = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
[data-mode=mobile] {
|
||||
.actionbar {
|
||||
width: 100%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
.actionbar {
|
||||
position: fixed;
|
||||
z-index: 4;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: calc(100% - #{$g_sidebar_width});
|
||||
transform: translateX(calc(-50% + #{$g_sidebar_width} * 0.5));
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
transition: all 0.3s, box-shadow 0.5s;
|
||||
box-shadow: 0 0 1px 0 #ccc;
|
||||
&.shadow {
|
||||
box-shadow: 0 -10px 10px -10px #ccc;
|
||||
}
|
||||
}
|
||||
::v-deep .el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<el-image :src="src" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="[src]">
|
||||
<div slot="error" class="image-slot">
|
||||
<svg-icon name="image-load-fail" />
|
||||
</div>
|
||||
</el-image>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ImagePreview',
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
realWidth() {
|
||||
return typeof this.width == 'string' ? this.width : `${this.width}px`
|
||||
},
|
||||
realHeight() {
|
||||
return typeof this.height == 'string' ? this.height : `${this.height}px`
|
||||
}
|
||||
},
|
||||
created() {},
|
||||
mounted() {},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-image {
|
||||
border-radius: 5px;
|
||||
background-color: #ebeef5;
|
||||
box-shadow: 0 0 5px 1px #ccc;
|
||||
::v-deep .el-image__inner {
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
::v-deep .image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,228 @@
|
||||
<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
|
||||
>
|
||||
<el-image v-if="url === ''" :src="url === '' ? placeholder : url" :style="`width:${width}px;height:${height}px;`" fit="fill">
|
||||
<div slot="error" class="image-slot">
|
||||
<i class="el-icon-plus" />
|
||||
</div>
|
||||
</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="dialogVisible = true">
|
||||
<i class="el-icon-zoom-in" />
|
||||
</span>
|
||||
<span title="移除" @click.stop="remove">
|
||||
<i class="el-icon-delete" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="progress.percent" class="progress" :style="`width:${width}px;height:${height}px;`">
|
||||
<el-image :src="progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<el-progress type="circle" :width="Math.min(width, height) * 0.8" :percentage="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-dialog :visible.sync="dialogVisible" title="预览" width="800px">
|
||||
<img :src="url" style="display: block; max-width: 100%; margin: 0 auto;">
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ImageUpload',
|
||||
props: {
|
||||
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']
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
progress: {
|
||||
preview: '',
|
||||
percent: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 移除
|
||||
remove() {
|
||||
this.$emit('update:url', '')
|
||||
},
|
||||
beforeUpload(file) {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName[fileName.length - 1]
|
||||
const isTypeOk = this.ext.indexOf(fileExt) >= 0
|
||||
const isSizeOk = file.size / 1024 / 1024 < this.size
|
||||
if (!isTypeOk) {
|
||||
this.$message.error(`上传图片只支持 ${ this.ext.join(' / ') } 格式!`)
|
||||
}
|
||||
if (!isSizeOk) {
|
||||
this.$message.error(`上传图片大小不能超过 ${this.size}MB!`)
|
||||
}
|
||||
if (isTypeOk && isSizeOk) {
|
||||
this.progress.preview = URL.createObjectURL(file)
|
||||
}
|
||||
return isTypeOk && isSizeOk
|
||||
},
|
||||
onProgress(file) {
|
||||
this.progress.percent = ~~file.percent
|
||||
if (this.progress.percent == 100) {
|
||||
setTimeout(() => {
|
||||
this.progress.preview = ''
|
||||
this.progress.percent = 0
|
||||
}, 1000)
|
||||
}
|
||||
},
|
||||
onSuccess(res) {
|
||||
this.$emit('on-success', res)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload-container {
|
||||
line-height: initial;
|
||||
}
|
||||
.image {
|
||||
position: relative;
|
||||
.mask {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 24px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
transition: all 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;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.1s;
|
||||
&.disabled {
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&:hover:not(.disabled) {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover .mask {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
::v-deep .el-upload {
|
||||
.el-upload-dragger {
|
||||
width: auto;
|
||||
height: auto;
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
.el-image {
|
||||
display: block;
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
font-size: 30px;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.el-progress {
|
||||
z-index: 1;
|
||||
@include position-center(xy);
|
||||
.el-progress__text {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,266 @@
|
||||
<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)">
|
||||
<i class="el-icon-zoom-in" />
|
||||
</span>
|
||||
<span title="移除" @click="remove(index)">
|
||||
<i class="el-icon-delete" />
|
||||
</span>
|
||||
<span v-show="url.length > 1" title="左移" :class="{'disabled': index == 0}" @click="move(index, 'left')">
|
||||
<i class="el-icon-back" />
|
||||
</span>
|
||||
<span v-show="url.length > 1" title="右移" :class="{'disabled': index == url.length - 1}" @click="move(index, 'right')">
|
||||
<i class="el-icon-right" />
|
||||
</span>
|
||||
</div>
|
||||
</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;`">
|
||||
<i class="el-icon-plus" />
|
||||
</div>
|
||||
<div v-show="progress.percent" class="progress" :style="`width:${width}px;height:${height}px;`">
|
||||
<el-image :src="progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<el-progress type="circle" :width="Math.min(width, height) * 0.8" :percentage="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="imageViewerVisible" :on-close="() => {imageViewerVisible = false}" :url-list="[dialogImageUrl]" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ElImageViewer from 'element-ui/packages/image/src/image-viewer'
|
||||
|
||||
export default {
|
||||
name: 'ImagesUpload',
|
||||
components: {
|
||||
ElImageViewer
|
||||
},
|
||||
props: {
|
||||
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']
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogImageUrl: '',
|
||||
imageViewerVisible: false,
|
||||
progress: {
|
||||
preview: '',
|
||||
percent: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 预览
|
||||
preview(index) {
|
||||
this.dialogImageUrl = this.url[index]
|
||||
this.imageViewerVisible = true
|
||||
},
|
||||
// 移除
|
||||
remove(index) {
|
||||
let url = this.url
|
||||
url.splice(index, 1)
|
||||
this.$emit('update:url', url)
|
||||
},
|
||||
// 移动
|
||||
move(index, type) {
|
||||
let url = this.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]
|
||||
}
|
||||
this.$emit('update:url', url)
|
||||
},
|
||||
beforeUpload(file) {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName[fileName.length - 1]
|
||||
const isTypeOk = this.ext.indexOf(fileExt) >= 0
|
||||
const isSizeOk = file.size / 1024 / 1024 < this.size
|
||||
if (!isTypeOk) {
|
||||
this.$message.error(`上传图片只支持 ${ this.ext.join(' / ') } 格式!`)
|
||||
}
|
||||
if (!isSizeOk) {
|
||||
this.$message.error(`上传图片大小不能超过 ${this.size}MB!`)
|
||||
}
|
||||
if (isTypeOk && isSizeOk) {
|
||||
this.progress.preview = URL.createObjectURL(file)
|
||||
}
|
||||
return isTypeOk && isSizeOk
|
||||
},
|
||||
onProgress(file) {
|
||||
this.progress.percent = ~~file.percent
|
||||
if (this.progress.percent == 100) {
|
||||
this.progress.preview = ''
|
||||
this.progress.percent = 0
|
||||
}
|
||||
},
|
||||
onSuccess(res) {
|
||||
this.$emit('on-success', res)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload-container {
|
||||
line-height: initial;
|
||||
}
|
||||
.images {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
.el-image {
|
||||
display: block;
|
||||
}
|
||||
.mask {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 24px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
transition: all 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;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.1s;
|
||||
&.disabled {
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&:hover:not(.disabled) {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover .mask {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.images-upload {
|
||||
display: inline-block;
|
||||
}
|
||||
::v-deep .el-upload {
|
||||
.el-upload-dragger {
|
||||
width: auto;
|
||||
height: auto;
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
font-size: 30px;
|
||||
background-color: transparent;
|
||||
}
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.el-progress {
|
||||
z-index: 1;
|
||||
@include position-center(xy);
|
||||
.el-progress__text {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="header">
|
||||
<div>
|
||||
<div class="title">{{ title }}</div>
|
||||
<div class="content">
|
||||
<slot name="content">{{ content }}</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PageHeader',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
.title {
|
||||
font-size: 22px;
|
||||
}
|
||||
.content {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="page-main">
|
||||
<div v-if="title" class="title-container">{{ title }}</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PageMain',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-main {
|
||||
position: relative;
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
.title-container {
|
||||
width: calc(100% + 40px);
|
||||
padding: 14px 20px;
|
||||
margin-left: -20px;
|
||||
margin-top: -20px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="result">
|
||||
<div v-if="type === 'success'" class="icon icon-success">
|
||||
<i class="el-icon-success" />
|
||||
</div>
|
||||
<div v-else-if="type === 'warning'" class="icon icon-warning">
|
||||
<i class="el-icon-warning" />
|
||||
</div>
|
||||
<div v-else class="icon icon-error">
|
||||
<i class="el-icon-error" />
|
||||
</div>
|
||||
<h1>{{ title }}</h1>
|
||||
<div v-if="desc" class="desc">{{ desc }}</div>
|
||||
<div v-if="$slots.extra" class="extra">
|
||||
<slot name="extra" />
|
||||
</div>
|
||||
<div v-if="$slots.default" class="actions">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Result',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
validator: val => ['success', 'warning', 'error'].includes(val),
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
desc: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$success: #67c23a;
|
||||
$warning: #e6a23c;
|
||||
$error: #f56c6c;
|
||||
.result {
|
||||
width: 72%;
|
||||
margin: 20px auto 0;
|
||||
text-align: center;
|
||||
.icon {
|
||||
i {
|
||||
font-size: 80px;
|
||||
}
|
||||
&-success i {
|
||||
color: $success;
|
||||
}
|
||||
&-warning i {
|
||||
color: $warning;
|
||||
}
|
||||
&-error i {
|
||||
color: $error;
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
margin: 20px 0;
|
||||
font-size: 24px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.desc {
|
||||
color: #909399;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.extra {
|
||||
margin: 50px 0;
|
||||
padding: 24px 40px;
|
||||
text-align: left;
|
||||
color: #606266;
|
||||
background: #f8f8f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.actions {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="search-container">
|
||||
<slot />
|
||||
<div v-if="showMore" class="more">
|
||||
<el-button type="text" size="small" :icon="isOpen ? 'el-icon-caret-top' : 'el-icon-caret-bottom'" @click="toggle">{{ isOpen ? '收起' : '展开' }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SearchBar',
|
||||
props: {
|
||||
showMore: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOpen: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggle() {
|
||||
this.isOpen = !this.isOpen
|
||||
this.$emit('toggle', this.isOpen)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-container {
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background-color: #f7f8fa;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
::v-deep .el-form {
|
||||
margin-bottom: -18px;
|
||||
.el-select {
|
||||
width: 100%;
|
||||
}
|
||||
.el-date-editor {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.more {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<svg class="svg-icon" aria-hidden="true" v-on="$listeners">
|
||||
<use :xlink:href="`#icon-${name}`" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SvgIcon',
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<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>
|
||||
<i :class="`${isUp ? 'el-icon-caret-top' : 'el-icon-caret-bottom'}`" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Trend',
|
||||
props: {
|
||||
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
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isUp() {
|
||||
let isUp = this.type === 'up'
|
||||
if (this.reverse) {
|
||||
isUp = !isUp
|
||||
}
|
||||
return isUp
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.trend {
|
||||
display: inline-block;
|
||||
&.up {
|
||||
color: #67c23a;
|
||||
}
|
||||
&.down {
|
||||
color: #f56c6c;
|
||||
}
|
||||
i {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 全局组件自动注册
|
||||
*
|
||||
* 全局组件各个组件按文件夹区分,文件夹名称与组件名无关联,但建议与组件名保持一致
|
||||
* 文件夹内至少保留一个文件名为 index 的组件入口,例如 index.vue
|
||||
* 普通组件必须设置 name 并保证其唯一,自动注册会将组件的 name 设为组件名,可参考 SvgIcon 组件写法
|
||||
* 如果组件是通过 js 进行调用,则确保组件入口文件为 index.js,可参考 ExampleNotice 组件
|
||||
*/
|
||||
|
||||
import Vue from 'vue'
|
||||
|
||||
const componentsContext = require.context('./', true, /index.(vue|js)$/)
|
||||
componentsContext.keys().forEach(file_name => {
|
||||
// 获取文件中的 default 模块
|
||||
const componentConfig = componentsContext(file_name).default
|
||||
if (/.vue$/.test(file_name)) {
|
||||
Vue.component(componentConfig.name, componentConfig)
|
||||
} else {
|
||||
Vue.use(componentConfig)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<router-link v-slot="{ navigate }" custom :to="to" :class="{
|
||||
'title': true,
|
||||
'is-link': $store.state.settings.enableDashboard
|
||||
}" :title="title" tag="div"
|
||||
>
|
||||
<div @click="navigate">
|
||||
<img v-if="showLogo" :src="logo" class="logo">
|
||||
<span v-if="showTitle">{{ title }}</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Logo',
|
||||
props: {
|
||||
showLogo: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showTitle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: process.env.VUE_APP_TITLE,
|
||||
logo: require('@/assets/images/logo.png')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
to() {
|
||||
let rtn = {}
|
||||
if (this.$store.state.settings.enableDashboard) {
|
||||
rtn.name = 'dashboard'
|
||||
}
|
||||
return rtn
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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: $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,270 @@
|
||||
<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="input" v-model="search" prefix-icon="el-icon-search" placeholder="搜索页面" clearable @keydown.esc.native="$eventBus.$emit('global-search-toggle')" />
|
||||
<div class="tips">你可以使用快捷键<span>alt</span>+<span>s</span>唤醒搜索面板,按<span>esc</span>退出</div>
|
||||
</div>
|
||||
<div ref="search" class="result">
|
||||
<router-link v-for="item in resultList" :key="item.path" v-slot="{ href, navigate }" custom :to="isShow ? item.path : ''">
|
||||
<a :href="isExternal(item.path) ? item.path : href" class="item" :target="isExternal(item.path) ? '_blank' : '_self'" @click="navigate">
|
||||
<div class="icon">
|
||||
<svg-icon v-if="item.icon" :name="item.icon" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="title">
|
||||
{{ item.title }}
|
||||
<svg-icon v-if="item.isExternal" name="external-link" />
|
||||
</div>
|
||||
<div class="breadcrumb">
|
||||
<span v-for="(bc, index) in item.breadcrumb" :key="index">
|
||||
{{ bc }}
|
||||
<i class="el-icon-arrow-right" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="path">{{ item.path }}</div>
|
||||
</div>
|
||||
</a>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { deepClone } from '@/util'
|
||||
|
||||
export default {
|
||||
name: 'Search',
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
isShow: false,
|
||||
search: '',
|
||||
sourceList: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
resultList() {
|
||||
let result = []
|
||||
result = this.sourceList.filter(item => {
|
||||
let flag = false
|
||||
if (item.title.indexOf(this.search) >= 0) {
|
||||
flag = true
|
||||
}
|
||||
if (item.path.indexOf(this.search) >= 0) {
|
||||
flag = true
|
||||
}
|
||||
if (item.breadcrumb.some(b => b.indexOf(this.search) >= 0)) {
|
||||
flag = true
|
||||
}
|
||||
return flag
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isShow(val) {
|
||||
if (val) {
|
||||
document.querySelector('body').classList.add('hidden')
|
||||
this.$refs.search.scrollTop = 0
|
||||
setTimeout(() => {
|
||||
this.$refs.input.$el.children[0].focus()
|
||||
}, 100)
|
||||
} else {
|
||||
document.querySelector('body').classList.remove('hidden')
|
||||
setTimeout(() => {
|
||||
this.search = ''
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {},
|
||||
mounted() {
|
||||
this.$eventBus.$on('global-search-toggle', () => {
|
||||
this.isShow = !this.isShow
|
||||
})
|
||||
this.$store.state.menu.routes.map(item => {
|
||||
this.getSourceList(item.children)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
isExternal(path) {
|
||||
return /^(https?:|mailto:|tel:)/.test(path)
|
||||
},
|
||||
hasChildren(item) {
|
||||
let flag = true
|
||||
if (item.children) {
|
||||
if (item.children.every(i => i.meta.sidebar === false)) {
|
||||
flag = false
|
||||
}
|
||||
} else {
|
||||
flag = false
|
||||
}
|
||||
return flag
|
||||
},
|
||||
getSourceList(arr) {
|
||||
arr.map(item => {
|
||||
if (item.meta.sidebar !== false) {
|
||||
if (this.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
|
||||
})
|
||||
this.getSourceList(child)
|
||||
} else {
|
||||
let breadcrumb = []
|
||||
if (item.meta.baseBreadcrumb) {
|
||||
breadcrumb = deepClone(item.meta.baseBreadcrumb)
|
||||
}
|
||||
breadcrumb.push(item.meta.title)
|
||||
let path = ''
|
||||
if (this.isExternal(item.path)) {
|
||||
path = item.path
|
||||
} else {
|
||||
path = item.meta.basePath ? [item.meta.basePath, item.path].join('/') : item.path
|
||||
}
|
||||
this.sourceList.push({
|
||||
icon: item.meta.icon || item.meta.baseIcon,
|
||||
title: item.meta.title,
|
||||
breadcrumb: breadcrumb,
|
||||
path: path,
|
||||
isExternal: this.isExternal(item.path)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#search {
|
||||
position: fixed;
|
||||
z-index: 2000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba($color: #000, $alpha: 0.5);
|
||||
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 0;
|
||||
::v-deep .el-input__inner {
|
||||
height: 52px;
|
||||
line-height: 52px;
|
||||
}
|
||||
.tips {
|
||||
margin: 20px 0 40px;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
span {
|
||||
margin: 0 5px;
|
||||
padding: 1px 5px 2px;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
background-color: rgba($color: #000, $alpha: 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
.result {
|
||||
margin: 0 20px;
|
||||
max-height: calc(100% - 250px);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
background-color: #fff;
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
transition: all 0.3s;
|
||||
background-color: #f5f7fa;
|
||||
.icon .svg-icon {
|
||||
color: #409eff;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
.info {
|
||||
.title {
|
||||
color: #333;
|
||||
}
|
||||
.breadcrumb,
|
||||
.path {
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
flex: 0 0 66px;
|
||||
text-align: center;
|
||||
.svg-icon {
|
||||
color: #999;
|
||||
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 #ebeef5;
|
||||
padding: 5px 10px 7px;
|
||||
@include text-overflow(1, true);
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
@include text-overflow(1, true);
|
||||
.svg-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.breadcrumb,
|
||||
.path {
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
transition: all 0.3s;
|
||||
@include text-overflow(1, true);
|
||||
}
|
||||
.breadcrumb {
|
||||
span:last-child i {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div v-if="item.meta.sidebar !== false">
|
||||
<router-link v-if="!hasChildren" v-slot="{ href, navigate, isActive, isExactActive }" custom :to="resolvePath(item.path)">
|
||||
<a :href="isExternal(resolvePath(item.path)) ? resolvePath(item.path) : href" :class="[isActive && 'router-link-active', isExactActive && 'router-link-exact-active']" :target="isExternal(resolvePath(item.path)) ? '_blank' : '_self'" @click="navigate">
|
||||
<el-menu-item :title="item.meta.title" :index="resolvePath(item.path)">
|
||||
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
|
||||
<span>{{ item.meta.title }}</span>
|
||||
</el-menu-item>
|
||||
</a>
|
||||
</router-link>
|
||||
<el-submenu v-else :title="item.meta.title" :index="resolvePath(item.path)">
|
||||
<template slot="title">
|
||||
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
|
||||
<span>{{ item.meta.title }}</span>
|
||||
</template>
|
||||
<SidebarItem v-for="route in item.children" :key="route.path" :item="route" :base-path="resolvePath(item.path)" />
|
||||
</el-submenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import path from 'path'
|
||||
|
||||
export default {
|
||||
name: 'SidebarItem',
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
hasChildren() {
|
||||
let flag = true
|
||||
if (this.item.children) {
|
||||
if (this.item.children.every(item => item.meta.sidebar === false)) {
|
||||
flag = false
|
||||
}
|
||||
} else {
|
||||
flag = false
|
||||
}
|
||||
return flag
|
||||
}
|
||||
},
|
||||
created() {},
|
||||
mounted() {},
|
||||
methods: {
|
||||
isExternal(path) {
|
||||
return /^(https?:|mailto:|tel:)/.test(path)
|
||||
},
|
||||
resolvePath(routePath) {
|
||||
if (this.isExternal(routePath)) {
|
||||
return routePath
|
||||
}
|
||||
if (this.isExternal(this.basePath)) {
|
||||
return this.basePath
|
||||
}
|
||||
return path.resolve(this.basePath, routePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .el-menu-item,
|
||||
::v-deep .el-menu-item span,
|
||||
::v-deep .el-submenu__title,
|
||||
::v-deep .el-submenu__title span {
|
||||
vertical-align: inherit;
|
||||
@include text-overflow;
|
||||
}
|
||||
::v-deep .el-menu-item,
|
||||
::v-deep .el-submenu__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
::v-deep .el-submenu {
|
||||
.el-menu--inline {
|
||||
.el-submenu > .el-submenu__title,
|
||||
.el-menu-item {
|
||||
background-color: darken($g-sub-sidebar-bg, 7) !important;
|
||||
&:hover {
|
||||
background-color: darken($g-sub-sidebar-bg, 10) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
::v-deep .el-menu-item.is-active,
|
||||
::v-deep .el-submenu .el-menu--inline .el-menu-item.is-active {
|
||||
background-color: $g-sub-sidebar-menu-active-bg !important;
|
||||
}
|
||||
::v-deep .el-submenu,
|
||||
::v-deep .el-menu-item {
|
||||
.svg-icon {
|
||||
font-size: 20px;
|
||||
margin-right: 10px;
|
||||
vertical-align: -0.25em;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
&:hover > .svg-icon,
|
||||
.el-submenu__title:hover > .svg-icon {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
a {
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.el-menu--collapse .el-submenu.is-active > .el-submenu__title {
|
||||
color: $g-sub-sidebar-menu-active-color !important;
|
||||
background-color: $g-sub-sidebar-menu-active-bg !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer title="主题配置" :visible.sync="isShow" direction="rtl" :size="$store.state.settings.mode == 'pc' ? '500px' : '300px'">
|
||||
<el-alert title="主题配置可实时预览效果,更多设置请在 src/setting.js 中进行设置,建议在生产环境隐藏主题配置功能" type="error" :closable="false" />
|
||||
<el-form ref="form" :label-position="$store.state.settings.mode == 'pc' ? 'right' : 'top'" label-width="100px" size="small">
|
||||
<el-form-item v-if="$store.state.settings.mode == 'pc'" label="头部">
|
||||
<el-radio-group v-model="showHeader">
|
||||
<el-radio-button :label="true">显示</el-radio-button>
|
||||
<el-radio-button :label="false">隐藏</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="$store.state.settings.mode == 'pc'" label="侧边栏切换">
|
||||
<el-radio-group v-model="enableSidebarCollapse">
|
||||
<el-radio-button :label="true">启用</el-radio-button>
|
||||
<el-radio-button :label="false">关闭</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="侧边栏导航">
|
||||
<el-radio-group v-model="sidebarCollapse">
|
||||
<el-radio-button :label="true">收起</el-radio-button>
|
||||
<el-radio-button :label="false">展开</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="底部版权">
|
||||
<el-radio-group v-model="showCopyright">
|
||||
<el-radio-button :label="true">显示</el-radio-button>
|
||||
<el-radio-button :label="false">隐藏</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="导航搜索">
|
||||
<el-radio-group v-model="enableNavSearch">
|
||||
<el-radio-button :label="true">开启</el-radio-button>
|
||||
<el-radio-button :label="false">关闭</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-alert title="该功能为页面右上角的搜索按钮,可对侧边栏导航进行快捷搜索" type="info" :closable="false" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="$store.state.settings.mode == 'pc'" label="全屏">
|
||||
<el-radio-group v-model="enableFullscreen">
|
||||
<el-radio-button :label="true">开启</el-radio-button>
|
||||
<el-radio-button :label="false">关闭</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-alert title="该功能为页面右上角的全屏按钮" type="info" :closable="false" />
|
||||
<el-alert title="不建议开启,该功能使用场景极少,用户习惯于通过窗口“最大化”功能来扩大显示区域,以显示更多内容,并且使用 F11 键也可以进入全屏效果" type="warning" :closable="false" />
|
||||
</el-form-item>
|
||||
<el-form-item label="页面刷新">
|
||||
<el-radio-group v-model="enablePageReload">
|
||||
<el-radio-button :label="true">开启</el-radio-button>
|
||||
<el-radio-button :label="false">关闭</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-alert title="该功能为页面右上角的刷新按钮,开启时会阻止 F5 键原刷新功能,并采用框架提供的刷新模式进行页面刷新" type="info" :closable="false" />
|
||||
</el-form-item>
|
||||
<el-form-item label="加载进度条">
|
||||
<el-radio-group v-model="enableProgress">
|
||||
<el-radio-button :label="true">开启</el-radio-button>
|
||||
<el-radio-button :label="false">关闭</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-alert title="该功能开启时,跳转路由会看到页面顶部有条蓝色的进度条" type="info" :closable="false" />
|
||||
</el-form-item>
|
||||
<el-form-item label="动态标题">
|
||||
<el-radio-group v-model="enableDynamicTitle">
|
||||
<el-radio-button :label="true">开启</el-radio-button>
|
||||
<el-radio-button :label="false">关闭</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-alert title="该功能开启时,页面标题会显示当前路由标题,格式为“页面标题 - 网站名称”;关闭时则显示网站名称,网站名称在项目根目录下 .env.* 文件里配置" type="info" :closable="false" />
|
||||
</el-form-item>
|
||||
<el-form-item label="控制台">
|
||||
<el-radio-group v-model="enableDashboard">
|
||||
<el-radio-button :label="true">开启</el-radio-button>
|
||||
<el-radio-button :label="false">关闭</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-alert title="控制台即欢迎页,该功能开启时,登录成功默认进入控制台;关闭时则默认进入侧边栏导航第一个导航页面" type="info" :closable="false" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ThemeSetting',
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
isShow: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showHeader: {
|
||||
get: function() {
|
||||
return this.$store.state.settings.showHeader
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.$store.commit('settings/updateThemeSetting', {
|
||||
'showHeader': newValue
|
||||
})
|
||||
}
|
||||
},
|
||||
enableSidebarCollapse: {
|
||||
get: function() {
|
||||
return this.$store.state.settings.enableSidebarCollapse
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.$store.commit('settings/updateThemeSetting', {
|
||||
'enableSidebarCollapse': newValue
|
||||
})
|
||||
}
|
||||
},
|
||||
sidebarCollapse: {
|
||||
get: function() {
|
||||
return this.$store.state.settings.sidebarCollapse
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.$store.commit('settings/updateThemeSetting', {
|
||||
'sidebarCollapse': newValue
|
||||
})
|
||||
}
|
||||
},
|
||||
showCopyright: {
|
||||
get: function() {
|
||||
return this.$store.state.settings.showCopyright
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.$store.commit('settings/updateThemeSetting', {
|
||||
'showCopyright': newValue
|
||||
})
|
||||
}
|
||||
},
|
||||
enableNavSearch: {
|
||||
get: function() {
|
||||
return this.$store.state.settings.enableNavSearch
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.$store.commit('settings/updateThemeSetting', {
|
||||
'enableNavSearch': newValue
|
||||
})
|
||||
}
|
||||
},
|
||||
enableFullscreen: {
|
||||
get: function() {
|
||||
return this.$store.state.settings.enableFullscreen
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.$store.commit('settings/updateThemeSetting', {
|
||||
'enableFullscreen': newValue
|
||||
})
|
||||
}
|
||||
},
|
||||
enablePageReload: {
|
||||
get: function() {
|
||||
return this.$store.state.settings.enablePageReload
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.$store.commit('settings/updateThemeSetting', {
|
||||
'enablePageReload': newValue
|
||||
})
|
||||
}
|
||||
},
|
||||
enableProgress: {
|
||||
get: function() {
|
||||
return this.$store.state.settings.enableProgress
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.$store.commit('settings/updateThemeSetting', {
|
||||
'enableProgress': newValue
|
||||
})
|
||||
}
|
||||
},
|
||||
enableDynamicTitle: {
|
||||
get: function() {
|
||||
return this.$store.state.settings.enableDynamicTitle
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.$store.commit('settings/updateThemeSetting', {
|
||||
'enableDynamicTitle': newValue
|
||||
})
|
||||
}
|
||||
},
|
||||
enableDashboard: {
|
||||
get: function() {
|
||||
return this.$store.state.settings.enableDashboard
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.$store.commit('settings/updateThemeSetting', {
|
||||
'enableDashboard': newValue
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$eventBus.$on('global-theme-toggle', () => {
|
||||
this.isShow = !this.isShow
|
||||
})
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .el-drawer__wrapper,
|
||||
::v-deep .el-drawer__wrapper * {
|
||||
outline: none !important;
|
||||
}
|
||||
::v-deep .el-drawer__body {
|
||||
padding: 0 20px 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
.el-form {
|
||||
margin-top: 20px;
|
||||
.el-alert {
|
||||
margin-top: 10px;
|
||||
line-height: initial;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="topbar-container">
|
||||
<div class="left-box">
|
||||
<div v-if="$store.state.settings.mode == 'mobile' || $store.state.settings.enableSidebarCollapse" :class="{
|
||||
'sidebar-collapse': true,
|
||||
'is-collapse': $store.state.settings.sidebarCollapse
|
||||
}" @click="$store.commit('settings/toggleSidebarCollapse')"
|
||||
>
|
||||
<svg-icon name="collapse" />
|
||||
</div>
|
||||
<el-breadcrumb v-if="$store.state.settings.mode == 'pc'" separator-class="el-icon-arrow-right">
|
||||
<transition-group name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="item in breadcrumbList" :key="item.path" :to="pathCompile(item.path)">{{ item.title }}</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<UserMenu />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { compile } from 'path-to-regexp'
|
||||
import { deepClone } from '@/util'
|
||||
import UserMenu from '../UserMenu'
|
||||
|
||||
export default {
|
||||
name: 'Breadcrumb',
|
||||
components: {
|
||||
UserMenu
|
||||
},
|
||||
computed: {
|
||||
breadcrumbList() {
|
||||
let breadcrumbList = []
|
||||
if (this.$store.state.settings.enableDashboard) {
|
||||
breadcrumbList.push({
|
||||
path: '/dashboard',
|
||||
title: this.$store.state.settings.dashboardTitle
|
||||
})
|
||||
}
|
||||
if (this.$store.state.settings.enableFlatRoutes) {
|
||||
if (this.$route.meta.breadcrumbNeste) {
|
||||
this.$route.meta.breadcrumbNeste.map((item, index) => {
|
||||
let tmpItem = deepClone(item)
|
||||
if (index != 0) {
|
||||
tmpItem.path = `${this.$route.meta.breadcrumbNeste[0].path}/${item.path}`
|
||||
}
|
||||
breadcrumbList.push(tmpItem)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.$route.matched.map(item => {
|
||||
if (item.meta && item.meta.title && item.meta.breadcrumb !== false && item.path != '/dashboard') {
|
||||
breadcrumbList.push({
|
||||
path: item.path,
|
||||
title: item.meta.title
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return breadcrumbList
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pathCompile(path) {
|
||||
var toPath = compile(path)
|
||||
return toPath(this.$route.params)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
[data-mode=mobile] {
|
||||
.topbar-container {
|
||||
width: 100%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
.topbar-container {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 50%;
|
||||
width: calc(100% - #{$g_sidebar_width});
|
||||
transform: translateX(calc(-50% + #{$g_sidebar_width} * 0.5));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: $g-topbar-height;
|
||||
background-color: #fff;
|
||||
transition: 0.3s, box-shadow 0.2s;
|
||||
box-shadow: 0 0 1px 0 #ccc;
|
||||
&.shadow {
|
||||
box-shadow: 0 10px 10px -10px #ccc;
|
||||
}
|
||||
.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;
|
||||
transition: 0.3s;
|
||||
&:hover {
|
||||
background-image: linear-gradient(to right, #ddd, transparent);
|
||||
}
|
||||
.svg-icon {
|
||||
transition: 0.3s;
|
||||
}
|
||||
&.is-collapse .svg-icon {
|
||||
transform: rotateZ(-180deg);
|
||||
}
|
||||
& + .el-breadcrumb {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
::v-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,
|
||||
.breadcrumb-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(30px) skewX(-50deg);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="user">
|
||||
<div class="tools">
|
||||
<el-tooltip v-if="$store.state.settings.enableNavSearch" effect="dark" content="搜索页面" placement="bottom">
|
||||
<span class="item" @click="$eventBus.$emit('global-search-toggle')">
|
||||
<svg-icon name="search" />
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="$store.state.settings.mode == 'pc' && isFullscreenEnable && $store.state.settings.enableFullscreen" effect="dark" content="全屏" placement="bottom">
|
||||
<span class="item" @click="fullscreen">
|
||||
<svg-icon :name="isFullscreen ? 'fullscreen-exit' : 'fullscreen'" />
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="$store.state.settings.enablePageReload" effect="dark" content="刷新页面" placement="bottom">
|
||||
<span class="item" @click="reload(2)">
|
||||
<svg-icon name="reload" />
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="$store.state.settings.enableThemeSetting" effect="dark" content="主题配置" placement="bottom">
|
||||
<span class="item" @click="$eventBus.$emit('global-theme-toggle')">
|
||||
<svg-icon name="theme" />
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-dropdown class="user-container" @command="handleCommand">
|
||||
<div class="user-wrapper">
|
||||
<el-avatar size="medium">
|
||||
<i class="el-icon-user-solid" />
|
||||
</el-avatar>
|
||||
{{ $store.state.user.account }}
|
||||
<i class="el-icon-caret-bottom" />
|
||||
</div>
|
||||
<el-dropdown-menu slot="dropdown" class="user-dropdown">
|
||||
<el-dropdown-item v-if="$store.state.settings.enableDashboard" 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>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import screenfull from 'screenfull'
|
||||
|
||||
export default {
|
||||
name: 'UserMenu',
|
||||
inject: ['reload'],
|
||||
data() {
|
||||
return {
|
||||
isFullscreenEnable: screenfull.isEnabled,
|
||||
isFullscreen: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.on('change', this.fullscreenChange)
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.off('change', this.fullscreenChange)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fullscreen() {
|
||||
screenfull.toggle()
|
||||
},
|
||||
fullscreenChange() {
|
||||
this.isFullscreen = screenfull.isFullscreen
|
||||
},
|
||||
handleCommand(command) {
|
||||
switch (command) {
|
||||
case 'dashboard':
|
||||
this.$router.push({
|
||||
name: 'dashboard'
|
||||
})
|
||||
break
|
||||
case 'setting':
|
||||
this.$router.push({
|
||||
name: 'personalSetting'
|
||||
})
|
||||
break
|
||||
case 'logout':
|
||||
this.$store.dispatch('user/logout').then(() => {
|
||||
this.$router.push({
|
||||
name: 'login'
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tools {
|
||||
margin-right: 20px;
|
||||
.item {
|
||||
margin-left: 5px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
.user-container {
|
||||
display: inline-block;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
cursor: pointer;
|
||||
.user-wrapper {
|
||||
.el-avatar {
|
||||
vertical-align: middle;
|
||||
margin-top: -2px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EmptyLayout',
|
||||
props: {},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
created() {},
|
||||
mounted() {},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,507 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div id="app-main" :style="{
|
||||
'--real-sidebar-width': realSidebarWidth
|
||||
}"
|
||||
>
|
||||
<header v-if="$store.state.settings.mode == 'pc' && $store.state.settings.showHeader">
|
||||
<div class="header-container">
|
||||
<div class="main">
|
||||
<Logo />
|
||||
<!-- 当头部导航大于 1 个的时候才会显示 -->
|
||||
<div v-if="$store.state.menu.routes.length > 1" class="nav">
|
||||
<template v-for="(item, index) in $store.state.menu.routes">
|
||||
<div v-if="item.children && item.children.length !== 0" :key="index" :class="{
|
||||
'item': true,
|
||||
'active': index == $store.state.menu.headerActived
|
||||
}" @click="$store.commit('menu/switchHeaderActived', index)"
|
||||
>
|
||||
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
|
||||
<span v-if="item.meta.title">{{ item.meta.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<UserMenu />
|
||||
</div>
|
||||
</header>
|
||||
<div class="wrapper">
|
||||
<div :class="{
|
||||
'sidebar-container': true,
|
||||
'show': $store.state.settings.mode == 'mobile' && !$store.state.settings.sidebarCollapse
|
||||
}"
|
||||
>
|
||||
<div v-if="(!$store.state.settings.showHeader || $store.state.settings.mode == 'mobile') && ($store.state.menu.routes.length > 1 || $store.state.settings.alwaysShowMainSidebar)" class="main-sidebar-container">
|
||||
<Logo :show-title="false" class="sidebar-logo" />
|
||||
<div class="nav">
|
||||
<template v-for="(item, index) in $store.state.menu.routes">
|
||||
<div v-if="item.children && item.children.length !== 0" :key="index" :class="{
|
||||
'item': true,
|
||||
'active': index == $store.state.menu.headerActived
|
||||
}" :title="item.meta.title" @click="$store.commit('menu/switchHeaderActived', index)"
|
||||
>
|
||||
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
|
||||
<span>{{ item.meta.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="{
|
||||
'sub-sidebar-container': true,
|
||||
'is-collapse': $store.state.settings.mode == 'pc' && $store.state.settings.sidebarCollapse
|
||||
}" @scroll="onSidebarScroll"
|
||||
>
|
||||
<Logo :show-logo="$store.state.menu.routes.length <= 1 && !$store.state.settings.alwaysShowMainSidebar" :class="{
|
||||
'sidebar-logo': true,
|
||||
'sidebar-logo-bg': $store.state.menu.routes.length <= 1 && !$store.state.settings.alwaysShowMainSidebar,
|
||||
'shadow': sidebarScrollTop
|
||||
}"
|
||||
/>
|
||||
<el-menu :background-color="variables.g_sub_sidebar_bg" :text-color="variables.g_sub_sidebar_menu_color" :active-text-color="variables.g_sub_sidebar_menu_active_color" unique-opened :default-active="$route.meta.activeMenu || $route.path" :collapse="$store.state.settings.mode == 'pc' && $store.state.settings.sidebarCollapse" :collapse-transition="false" :class="{
|
||||
'is-collapse-without-logo': $store.state.menu.routes.length > 1 && $store.state.settings.mode == 'pc' && $store.state.settings.sidebarCollapse
|
||||
}"
|
||||
>
|
||||
<transition-group name="sidebar">
|
||||
<template v-for="route in $store.getters['menu/sidebarRoutes']">
|
||||
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path" :item="route" :base-path="route.path" />
|
||||
</template>
|
||||
</transition-group>
|
||||
</el-menu>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="{
|
||||
'sidebar-mask': true,
|
||||
'show': $store.state.settings.mode == 'mobile' && !$store.state.settings.sidebarCollapse
|
||||
}" @click="$store.commit('settings/toggleSidebarCollapse')"
|
||||
/>
|
||||
<div class="main-container">
|
||||
<Topbar :class="{'shadow': scrollTop}" />
|
||||
<div class="main">
|
||||
<transition name="main" mode="out-in">
|
||||
<keep-alive v-if="isRouterAlive" :include="$store.state.keepAlive.list">
|
||||
<RouterView :key="$route.path" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</div>
|
||||
<Copyright v-if="$store.state.settings.showCopyright" />
|
||||
</div>
|
||||
</div>
|
||||
<el-backtop :right="20" :bottom="20" title="回到顶部" />
|
||||
</div>
|
||||
<Search />
|
||||
<ThemeSetting />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import variables from '@/assets/styles/resources/variables.scss'
|
||||
import Logo from './components/Logo'
|
||||
import UserMenu from './components/UserMenu'
|
||||
import SidebarItem from './components/SidebarItem'
|
||||
import Topbar from './components/Topbar'
|
||||
import Search from './components/Search'
|
||||
import ThemeSetting from './components/ThemeSetting'
|
||||
|
||||
export default {
|
||||
name: 'Layout',
|
||||
components: {
|
||||
Logo,
|
||||
Search,
|
||||
SidebarItem,
|
||||
Topbar,
|
||||
ThemeSetting,
|
||||
UserMenu
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
reload: this.reload
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isRouterAlive: true,
|
||||
routePath: '',
|
||||
sidebarScrollTop: 0,
|
||||
scrollTop: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
variables() {
|
||||
return variables
|
||||
},
|
||||
realSidebarWidth() {
|
||||
let realSidebarWidth = 0
|
||||
if (this.$store.state.settings.mode == 'pc') {
|
||||
if (!this.$store.state.settings.showHeader && (this.$store.state.menu.routes.length > 1 || this.$store.state.settings.alwaysShowMainSidebar)) {
|
||||
realSidebarWidth = parseInt(variables.g_main_sidebar_width)
|
||||
}
|
||||
if (this.$store.state.settings.sidebarCollapse) {
|
||||
realSidebarWidth += 64
|
||||
} else {
|
||||
realSidebarWidth += parseInt(variables.g_sub_sidebar_width)
|
||||
}
|
||||
} else {
|
||||
realSidebarWidth = parseInt(variables.g_main_sidebar_width) + parseInt(variables.g_sub_sidebar_width)
|
||||
}
|
||||
return `${realSidebarWidth}px`
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route: 'routeChange',
|
||||
'$store.state.settings.sidebarCollapse'(val) {
|
||||
if (this.$store.state.settings.mode == 'mobile') {
|
||||
if (!val) {
|
||||
document.querySelector('body').classList.add('hidden')
|
||||
} else {
|
||||
document.querySelector('body').classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$hotkeys('alt+s', e => {
|
||||
if (this.$store.state.settings.enableNavSearch) {
|
||||
e.preventDefault()
|
||||
this.$eventBus.$emit('global-search-toggle')
|
||||
}
|
||||
})
|
||||
this.$hotkeys('f5', e => {
|
||||
if (this.$store.state.settings.enablePageReload) {
|
||||
e.preventDefault()
|
||||
this.reload(2)
|
||||
}
|
||||
})
|
||||
window.addEventListener('scroll', this.onScroll)
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('scroll', this.onScroll)
|
||||
},
|
||||
methods: {
|
||||
reload(type = 1) {
|
||||
if (type == 1) {
|
||||
this.isRouterAlive = false
|
||||
this.$nextTick(() => (this.isRouterAlive = true))
|
||||
} else {
|
||||
this.$router.push({
|
||||
name: 'reload'
|
||||
})
|
||||
}
|
||||
},
|
||||
routeChange(newVal, oldVal) {
|
||||
if (newVal.name == oldVal.name) {
|
||||
this.reload()
|
||||
}
|
||||
},
|
||||
onSidebarScroll(e) {
|
||||
this.sidebarScrollTop = e.target.scrollTop
|
||||
},
|
||||
onScroll() {
|
||||
this.scrollTop = document.documentElement.scrollTop || document.body.scrollTop
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
[data-mode=mobile] {
|
||||
.sidebar-container {
|
||||
transition: 0.3s;
|
||||
transform: translateX(calc(-1 * #{$g-sidebar-width}));
|
||||
&.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
.main-container {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
.layout {
|
||||
height: 100%;
|
||||
}
|
||||
#app-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
header {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: $g-header-height;
|
||||
background-color: $g-header-bg;
|
||||
color: $g-header-menu-color;
|
||||
.header-container {
|
||||
width: $g-header-width;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: $g-header-width) {
|
||||
.header-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
::v-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;
|
||||
}
|
||||
}
|
||||
.nav {
|
||||
display: flex;
|
||||
margin-left: 50px;
|
||||
.item {
|
||||
margin: 0 10px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&.active,
|
||||
&:hover {
|
||||
background-color: $g-header-menu-active-bg;
|
||||
}
|
||||
.svg-icon {
|
||||
font-size: 20px;
|
||||
& + span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
::v-deep .user {
|
||||
padding: 0;
|
||||
.user-container {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.sidebar-container {
|
||||
position: fixed;
|
||||
z-index: 1010;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
width: var(--real-sidebar-width);
|
||||
}
|
||||
.sidebar-mask {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba($color: #000, $alpha: 0.5);
|
||||
backdrop-filter: blur(2px);
|
||||
transition: all 0.2s;
|
||||
transform: translateZ(0);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
&.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
.main-sidebar-container,
|
||||
.sub-sidebar-container {
|
||||
transition: 0.3s;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
// firefox隐藏滚动条
|
||||
scrollbar-width: none;
|
||||
// chrome隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.main-sidebar-container {
|
||||
width: $g-main-sidebar-width;
|
||||
background-color: $g-main-sidebar-bg;
|
||||
color: #fff;
|
||||
.sidebar-logo {
|
||||
transition: 0.3s;
|
||||
background-color: $g-main-sidebar-bg;
|
||||
}
|
||||
.nav {
|
||||
width: inherit;
|
||||
padding-top: $g-sidebar-logo-height;
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 60px;
|
||||
padding: 0 5px;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
&.active,
|
||||
&:hover {
|
||||
background-color: $g-main-sidebar-menu-active-bg;
|
||||
}
|
||||
.svg-icon {
|
||||
margin: 0 auto;
|
||||
font-size: 20px;
|
||||
}
|
||||
span {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
@include text-overflow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sub-sidebar-container {
|
||||
width: $g-sub-sidebar-width;
|
||||
background-color: $g-sub-sidebar-bg;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
box-shadow: 10px 0 10px -10px darken($g-sub-sidebar-bg, 20);
|
||||
&.is-collapse {
|
||||
width: 64px;
|
||||
.sidebar-logo {
|
||||
&:not(.sidebar-logo-bg) {
|
||||
display: none;
|
||||
}
|
||||
::v-deep span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.sidebar-logo {
|
||||
background: $g-sub-sidebar-bg;
|
||||
transition: box-shadow 0.2s, background-color 0.3s, color 0.3s;
|
||||
&:not(.sidebar-logo-bg) {
|
||||
::v-deep span {
|
||||
color: $g-sub-sidebar-menu-color;
|
||||
}
|
||||
}
|
||||
&.sidebar-logo-bg {
|
||||
background: $g-main-sidebar-bg;
|
||||
}
|
||||
&.shadow {
|
||||
box-shadow: 0 10px 10px -10px darken($g-sub-sidebar-bg, 20);
|
||||
}
|
||||
}
|
||||
.el-menu {
|
||||
border-right: 0;
|
||||
padding-top: $g-sidebar-logo-height;
|
||||
transition: border-color 0.3s, background-color 0.3s, color 0.3s;
|
||||
&:not(.el-menu--collapse) {
|
||||
width: inherit;
|
||||
}
|
||||
&.is-collapse-without-logo {
|
||||
padding-top: 0;
|
||||
}
|
||||
&.el-menu--collapse {
|
||||
::v-deep .svg-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
::v-deep .el-menu-item,
|
||||
::v-deep .el-submenu__title {
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
i {
|
||||
right: 7px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.main-sidebar-container + .sub-sidebar-container {
|
||||
left: $g-main-sidebar-width;
|
||||
}
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
margin-left: $g-sidebar-width;
|
||||
background-color: #f5f7f9;
|
||||
box-shadow: 1px 0 0 0 darken($g-main-bg, 10);
|
||||
transition: 0.3s;
|
||||
.main {
|
||||
height: 100%;
|
||||
flex: auto;
|
||||
position: relative;
|
||||
padding: $g-topbar-height 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
header + .wrapper {
|
||||
padding-top: $g-header-height;
|
||||
.sidebar-container {
|
||||
top: $g-header-height;
|
||||
.sidebar-logo {
|
||||
display: none;
|
||||
}
|
||||
.el-menu {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
.main-container {
|
||||
.topbar-container {
|
||||
top: $g-header-height;
|
||||
::v-deep .user {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 侧边栏动画
|
||||
.sidebar-enter-active {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.sidebar-enter,
|
||||
.sidebar-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
.sidebar-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
// 主内容区动画
|
||||
.main-enter-active {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.main-leave-active {
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.main-enter {
|
||||
opacity: 0;
|
||||
margin-left: -20px;
|
||||
}
|
||||
.main-leave-to {
|
||||
opacity: 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router/index'
|
||||
import store from './store/index'
|
||||
|
||||
import api from './api'
|
||||
Vue.prototype.$api = api
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
Vue.prototype.$dayjs = dayjs
|
||||
|
||||
import auth from './util/auth'
|
||||
Vue.use(auth)
|
||||
|
||||
import cookies from 'vue-cookies'
|
||||
Vue.use(cookies)
|
||||
|
||||
import VueMeta from 'vue-meta'
|
||||
Vue.use(VueMeta)
|
||||
|
||||
import ElementUI from 'element-ui'
|
||||
import 'element-ui/lib/theme-chalk/index.css'
|
||||
Vue.use(ElementUI)
|
||||
|
||||
import hotkeys from 'hotkeys-js'
|
||||
Vue.prototype.$hotkeys = hotkeys
|
||||
|
||||
// 全局组件自动注册
|
||||
import './components/autoRegister'
|
||||
|
||||
// 自动加载 svg 图标
|
||||
const req = require.context('./assets/icons', false, /\.svg$/)
|
||||
const requireAll = requireContext => requireContext.keys().map(requireContext)
|
||||
requireAll(req)
|
||||
|
||||
import './assets/styles/reset.scss'
|
||||
|
||||
import './mock'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
Vue.prototype.$eventBus = new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
@@ -0,0 +1,49 @@
|
||||
const Mock = require('mockjs')
|
||||
|
||||
export function param2Obj(url) {
|
||||
const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
|
||||
if (!search) {
|
||||
return {}
|
||||
}
|
||||
const obj = {}
|
||||
const searchArr = search.split('&')
|
||||
searchArr.forEach(v => {
|
||||
const index = v.indexOf('=')
|
||||
if (index !== -1) {
|
||||
const name = v.substring(0, index)
|
||||
const val = v.substring(index + 1, v.length)
|
||||
obj[name] = val
|
||||
}
|
||||
})
|
||||
return obj
|
||||
}
|
||||
|
||||
function XHR2ExpressReqWrap(respond) {
|
||||
return function(options) {
|
||||
let result = null
|
||||
if (respond instanceof Function) {
|
||||
const { body, type, url } = options
|
||||
// https://expressjs.com/en/4x/api.html#req
|
||||
result = respond({
|
||||
method: type,
|
||||
body: JSON.parse(body),
|
||||
query: param2Obj(url)
|
||||
})
|
||||
} else {
|
||||
result = respond
|
||||
}
|
||||
return Mock.mock(result)
|
||||
}
|
||||
}
|
||||
const mocksContext = require.context('./modules/', true, /.js$/)
|
||||
mocksContext.keys().forEach(file_name => {
|
||||
// 获取文件中的 default 模块
|
||||
const mocks = mocksContext(file_name)
|
||||
for (const mock of mocks) {
|
||||
Mock.mock(
|
||||
new RegExp(`${process.env.VUE_APP_API_ROOT}mock/${mock.url}`),
|
||||
mock.type || 'get',
|
||||
XHR2ExpressReqWrap(mock.result)
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
module.exports = [
|
||||
{
|
||||
url: 'member/login',
|
||||
type: 'post',
|
||||
result: option => {
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
account: option.body.account,
|
||||
token: '@string',
|
||||
failure_time: Date.parse(new Date()) / 1000 + 24 * 60 * 60
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
url: 'member/permission',
|
||||
type: 'get',
|
||||
result: 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
const Mock = require('mockjs')
|
||||
|
||||
export default {
|
||||
'POST /mock/member/login': (req, res) => {
|
||||
return res.json(Mock.mock({
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
account: req.body.account,
|
||||
token: '@string',
|
||||
failure_time: Date.parse(new Date()) / 1000 + 24 * 60 * 60
|
||||
}
|
||||
}))
|
||||
},
|
||||
'GET /mock/member/permission': (req, res) => {
|
||||
let permissions = []
|
||||
if (req.query.account == 'admin') {
|
||||
permissions = [
|
||||
'permission.browse',
|
||||
'permission.create',
|
||||
'permission.edit',
|
||||
'permission.remove'
|
||||
]
|
||||
} else if (req.query.account == 'test') {
|
||||
permissions = [
|
||||
'permission.browse'
|
||||
]
|
||||
}
|
||||
return res.json({
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
permissions
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
let mockMap = {}
|
||||
|
||||
const mocksContext = require.context('./server-modules/', false, /.js$/)
|
||||
mocksContext.keys().forEach(file_name => {
|
||||
mockMap = Object.assign(mockMap, mocksContext(file_name).default)
|
||||
})
|
||||
|
||||
export default mockMap
|
||||
@@ -0,0 +1,177 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import store from '@/store/index'
|
||||
import NProgress from 'nprogress'
|
||||
import 'nprogress/nprogress.css' // progress bar style
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
import Layout from '@/layout'
|
||||
import EmptyLayout from '@/layout/empty'
|
||||
|
||||
const constantRoutes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('@/views/login'),
|
||||
meta: {
|
||||
title: '登录'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'dashboard',
|
||||
component: () => import('@/views/index'),
|
||||
meta: {
|
||||
title: store.state.settings.dashboardTitle
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'personal',
|
||||
component: EmptyLayout,
|
||||
redirect: '/personal/setting',
|
||||
meta: {
|
||||
title: '个人中心',
|
||||
breadcrumb: false
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'setting',
|
||||
name: 'personalSetting',
|
||||
component: () => import('@/views/personal/setting'),
|
||||
meta: {
|
||||
title: '个人设置'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'edit/password',
|
||||
name: 'personalEditPassword',
|
||||
component: () => import('@/views/personal/edit.password'),
|
||||
meta: {
|
||||
title: '修改密码'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'reload',
|
||||
name: 'reload',
|
||||
component: () => import('@/views/reload')
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
import MultilevelMenuExample from './modules/multilevel.menu.example'
|
||||
import BreadcrumbExample from './modules/breadcrumb.example'
|
||||
|
||||
// 当 children 不为空的主导航只有一项时,则隐藏
|
||||
let asyncRoutes = [
|
||||
{
|
||||
meta: {
|
||||
title: '演示',
|
||||
icon: 'sidebar-default'
|
||||
},
|
||||
children: [
|
||||
MultilevelMenuExample,
|
||||
BreadcrumbExample
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const lastRoute = [{
|
||||
path: '*',
|
||||
component: () => import('@/views/404'),
|
||||
meta: {
|
||||
title: '404',
|
||||
sidebar: false
|
||||
}
|
||||
}]
|
||||
|
||||
const router = new VueRouter({
|
||||
routes: constantRoutes
|
||||
})
|
||||
|
||||
// 解决路由在 push/replace 了相同地址报错的问题
|
||||
const originalPush = VueRouter.prototype.push
|
||||
VueRouter.prototype.push = function push(location) {
|
||||
return originalPush.call(this, location).catch(err => err)
|
||||
}
|
||||
const originalReplace = VueRouter.prototype.replace
|
||||
VueRouter.prototype.replace = function replace(location) {
|
||||
return originalReplace.call(this, location).catch(err => err)
|
||||
}
|
||||
|
||||
router.beforeEach(async(to, from, next) => {
|
||||
store.state.settings.enableProgress && NProgress.start()
|
||||
// 已经登录,但还没根据权限动态挂载路由
|
||||
if (store.getters['user/isLogin'] && !store.state.menu.isGenerate) {
|
||||
/**
|
||||
* 重置 matcher
|
||||
* https://blog.csdn.net/baidu_28647571/article/details/101711682
|
||||
*/
|
||||
router.matcher = new VueRouter({
|
||||
routes: constantRoutes
|
||||
}).matcher
|
||||
const accessRoutes = await store.dispatch('menu/generateRoutes', {
|
||||
asyncRoutes,
|
||||
currentPath: to.path
|
||||
})
|
||||
accessRoutes.push(...lastRoute)
|
||||
accessRoutes.forEach(route => {
|
||||
router.addRoute(route)
|
||||
})
|
||||
next({ ...to, replace: true })
|
||||
}
|
||||
if (store.state.menu.isGenerate) {
|
||||
store.commit('menu/setHeaderActived', to.path)
|
||||
}
|
||||
to.meta.title && store.commit('settings/setTitle', to.meta.title)
|
||||
if (store.getters['user/isLogin']) {
|
||||
if (to.name) {
|
||||
if (to.matched.length !== 0) {
|
||||
// 如果已登录状态下,进入登录页会强制跳转到控制台页面
|
||||
if (to.name == 'login') {
|
||||
next({
|
||||
name: 'dashboard',
|
||||
replace: true
|
||||
})
|
||||
} else if (!store.state.settings.enableDashboard && to.name == 'dashboard') {
|
||||
// 如果未开启控制台页面,则默认进入侧边栏导航第一个模块,但如果侧边栏导航没有模块,则还是进入控制台页面
|
||||
if (store.getters['menu/sidebarRoutes'].length > 0) {
|
||||
next({
|
||||
name: store.getters['menu/sidebarRoutes'][0].name,
|
||||
replace: true
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果是通过 name 跳转,并且 name 对应的路由没有权限时,需要做这步处理,手动指向到 404 页面
|
||||
next({
|
||||
path: '/404'
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (to.name != 'login') {
|
||||
next({
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: to.fullPath
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
store.state.settings.enableProgress && NProgress.done()
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,63 @@
|
||||
import Layout from '@/layout'
|
||||
import EmptyLayout from '@/layout/empty'
|
||||
|
||||
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(/* webpackChunkName: 'breadcrumb_example' */ '@/views/breadcrumb_example/list1'),
|
||||
meta: {
|
||||
title: '列表1(平级模式)'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'detail1',
|
||||
name: 'breadcrumbExampleDetail1',
|
||||
component: () => import(/* webpackChunkName: 'breadcrumb_example' */ '@/views/breadcrumb_example/detail1'),
|
||||
meta: {
|
||||
title: '详情1',
|
||||
sidebar: false,
|
||||
activeMenu: '/breadcrumb_example/list1'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'list2',
|
||||
name: 'breadcrumbExampleList2',
|
||||
component: EmptyLayout,
|
||||
redirect: '/breadcrumb_example/list2',
|
||||
meta: {
|
||||
title: '列表2(层级模式)'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: () => import(/* webpackChunkName: 'breadcrumb_example' */ '@/views/breadcrumb_example/list2'),
|
||||
meta: {
|
||||
title: '列表2(层级模式)',
|
||||
sidebar: false,
|
||||
breadcrumb: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'detail2',
|
||||
name: 'breadcrumbExampleDetail2',
|
||||
component: () => import(/* webpackChunkName: 'breadcrumb_example' */ '@/views/breadcrumb_example/detail2'),
|
||||
meta: {
|
||||
title: '详情2',
|
||||
sidebar: false,
|
||||
activeMenu: '/breadcrumb_example/list2'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import Layout from '@/layout'
|
||||
import EmptyLayout from '@/layout/empty'
|
||||
|
||||
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(/* webpackChunkName: 'multilevel_menu_example' */ '@/views/multilevel_menu_example/page'),
|
||||
meta: {
|
||||
title: '导航1'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'level2',
|
||||
name: 'multilevelMenuExample2',
|
||||
component: EmptyLayout,
|
||||
redirect: '/multilevel_menu_example/level2/page',
|
||||
meta: {
|
||||
title: '导航2'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'page',
|
||||
name: 'multilevelMenuExample2-1',
|
||||
component: () => import(/* webpackChunkName: 'multilevel_menu_example' */ '@/views/multilevel_menu_example/level2/page'),
|
||||
meta: {
|
||||
title: '导航2-1'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'level3',
|
||||
name: 'multilevelMenuExample2-2',
|
||||
component: EmptyLayout,
|
||||
redirect: '/multilevel_menu_example/level2/level3/page1',
|
||||
meta: {
|
||||
title: '导航2-2'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'page1',
|
||||
name: 'multilevelMenuExample2-2-1',
|
||||
component: () => import(/* webpackChunkName: 'multilevel_menu_example' */ '@/views/multilevel_menu_example/level2/level3/page1'),
|
||||
meta: {
|
||||
title: '导航2-2-1'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'page2',
|
||||
name: 'multilevelMenuExample2-2-2',
|
||||
component: () => import(/* webpackChunkName: 'multilevel_menu_example' */ '@/views/multilevel_menu_example/level2/level3/page2'),
|
||||
meta: {
|
||||
title: '导航2-2-2'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
let globalSettings = {
|
||||
/**
|
||||
* 是否开启权限功能,权限功能提供以下鉴权支持:
|
||||
* 1、路由鉴权
|
||||
* 2、鉴权组件:<Auth></Auth>、<AuthAll></AuthAll>
|
||||
* 3、鉴权指令:v-auth、v-auth-all
|
||||
* 4、鉴权函数:this.$auth()、this.$authAll()
|
||||
*/
|
||||
openPermission: false,
|
||||
// 是否显示头部
|
||||
showHeader: false,
|
||||
// 是否始终显示主侧边栏,设置为 false 且主侧边栏里只有一个导航时,主侧边栏会自动隐藏
|
||||
alwaysShowMainSidebar: false,
|
||||
// 是否开启侧边栏展开收起按钮
|
||||
enableSidebarCollapse: false,
|
||||
// 侧边栏是否收起
|
||||
sidebarCollapse: false,
|
||||
// 是否显示底部版权信息,同时在路由 meta 对象里可以单独设置某个路由是否显示底部版权信息
|
||||
showCopyright: true,
|
||||
// 版权信息配置,格式为:Copyright © [dates] <company>
|
||||
copyrightDates: '2021',
|
||||
copyrightCompany: 'Fantastic-admin',
|
||||
copyrightWebsite: 'https://hooray.github.io/fantastic-admin',
|
||||
// 是否开启导航搜索
|
||||
enableNavSearch: true,
|
||||
// 是否开启全屏
|
||||
enableFullscreen: false,
|
||||
// 是否开启页面刷新
|
||||
enablePageReload: false,
|
||||
// 是否开启载入进度条
|
||||
enableProgress: true,
|
||||
// 是否开启动态标题
|
||||
enableDynamicTitle: false,
|
||||
// 是否开启控制台
|
||||
enableDashboard: true,
|
||||
// 是否开启扁平化路由,开启后三级以及三级以上的嵌套路由均为被处理成二级,但侧边栏导航的层级效果不变
|
||||
enableFlatRoutes: false,
|
||||
// 控制台名称
|
||||
dashboardTitle: '控制台',
|
||||
// 是否开启主题配置(建议在生产环境关闭)
|
||||
enableThemeSetting: true
|
||||
}
|
||||
|
||||
export default globalSettings
|
||||
@@ -0,0 +1,15 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
const modules = {}
|
||||
const require_module = require.context('./modules', false, /.js$/)
|
||||
require_module.keys().forEach(file_name => {
|
||||
modules[file_name.slice(2, -3)] = require_module(file_name).default
|
||||
})
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: modules,
|
||||
strict: process.env.NODE_ENV !== 'production'
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
const state = {
|
||||
list: []
|
||||
}
|
||||
|
||||
const getters = {}
|
||||
|
||||
const actions = {}
|
||||
|
||||
const mutations = {
|
||||
add(state, name) {
|
||||
!state.list.includes(name) && state.list.push(name)
|
||||
},
|
||||
remove(state, name) {
|
||||
state.list = state.list.filter(v => {
|
||||
return v != name
|
||||
})
|
||||
},
|
||||
clean(state) {
|
||||
state.list = []
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
actions,
|
||||
getters,
|
||||
mutations
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { deepClone } from '@/util'
|
||||
|
||||
function hasPermission(permissions, route) {
|
||||
let isAuth = false
|
||||
if (route.meta && route.meta.auth) {
|
||||
isAuth = permissions.some(auth => {
|
||||
if (typeof route.meta.auth == 'string') {
|
||||
return route.meta.auth === auth
|
||||
} else {
|
||||
return route.meta.auth.some(routeAuth => {
|
||||
return routeAuth === auth
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
isAuth = true
|
||||
}
|
||||
return isAuth
|
||||
}
|
||||
|
||||
function filterAsyncRoutes(routes, permissions) {
|
||||
const res = []
|
||||
routes.forEach(route => {
|
||||
const tmp = { ...route }
|
||||
if (hasPermission(permissions, tmp)) {
|
||||
if (tmp.children) {
|
||||
tmp.children = filterAsyncRoutes(tmp.children, permissions)
|
||||
tmp.children.length && res.push(tmp)
|
||||
} else {
|
||||
res.push(tmp)
|
||||
}
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
// 将多层嵌套路由处理成平级
|
||||
function flatAsyncRoutes(routes, breadcrumb, baseUrl = '') {
|
||||
let res = []
|
||||
routes.forEach(route => {
|
||||
const tmp = { ...route }
|
||||
if (tmp.children) {
|
||||
let childrenBaseUrl = ''
|
||||
if (baseUrl == '') {
|
||||
childrenBaseUrl = tmp.path
|
||||
} else if (tmp.path != '') {
|
||||
childrenBaseUrl = `${baseUrl}/${tmp.path}`
|
||||
}
|
||||
let childrenBreadcrumb = deepClone(breadcrumb)
|
||||
if (route.meta.breadcrumb !== false) {
|
||||
childrenBreadcrumb.push({
|
||||
path: childrenBaseUrl,
|
||||
title: route.meta.title
|
||||
})
|
||||
}
|
||||
let tmpRoute = deepClone(route)
|
||||
tmpRoute.path = childrenBaseUrl
|
||||
tmpRoute.meta.breadcrumbNeste = childrenBreadcrumb
|
||||
delete tmpRoute.children
|
||||
res.push(tmpRoute)
|
||||
let childrenRoutes = flatAsyncRoutes(tmp.children, childrenBreadcrumb, childrenBaseUrl)
|
||||
childrenRoutes.map(item => {
|
||||
// 如果 path 一样则覆盖,因为子路由的 path 可能设置为空,导致和父路由一样,直接注册会提示路由重复
|
||||
if (res.some(v => v.path == item.path)) {
|
||||
res.forEach((v, i) => {
|
||||
if (v.path == item.path) {
|
||||
res[i] = item
|
||||
}
|
||||
})
|
||||
} else {
|
||||
res.push(item)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (baseUrl != '') {
|
||||
if (tmp.path != '') {
|
||||
tmp.path = `${baseUrl}/${tmp.path}`
|
||||
} else {
|
||||
tmp.path = baseUrl
|
||||
}
|
||||
}
|
||||
// 处理面包屑导航
|
||||
let tmpBreadcrumb = deepClone(breadcrumb)
|
||||
if (tmp.meta.breadcrumb !== false) {
|
||||
tmpBreadcrumb.push({
|
||||
path: tmp.path,
|
||||
title: tmp.meta.title
|
||||
})
|
||||
}
|
||||
tmp.meta.breadcrumbNeste = tmpBreadcrumb
|
||||
res.push(tmp)
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const state = {
|
||||
isGenerate: false,
|
||||
routes: [],
|
||||
headerActived: 0
|
||||
}
|
||||
|
||||
const getters = {
|
||||
sidebarRoutes: state => {
|
||||
return state.routes.length > 0 ? state.routes[state.headerActived].children : []
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
// 根据权限动态生成路由
|
||||
generateRoutes({rootState, dispatch, commit}, data) {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async resolve => {
|
||||
let accessedRoutes
|
||||
// 判断权限功能是否开启
|
||||
if (rootState.settings.openPermission) {
|
||||
const permissions = await dispatch('user/getPermissions', null, { root: true })
|
||||
accessedRoutes = filterAsyncRoutes(data.asyncRoutes, permissions)
|
||||
} else {
|
||||
accessedRoutes = data.asyncRoutes
|
||||
}
|
||||
commit('setRoutes', accessedRoutes)
|
||||
commit('setHeaderActived', data.currentPath)
|
||||
let routes = []
|
||||
accessedRoutes.map(item => {
|
||||
routes.push(...item.children)
|
||||
})
|
||||
if (rootState.settings.enableFlatRoutes) {
|
||||
routes.map(item => {
|
||||
if (item.children) {
|
||||
item.children = flatAsyncRoutes(item.children, [{
|
||||
path: item.path,
|
||||
title: item.meta.title
|
||||
}])
|
||||
}
|
||||
})
|
||||
}
|
||||
resolve(routes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
invalidRoutes(state) {
|
||||
state.isGenerate = false
|
||||
state.headerActived = 0
|
||||
},
|
||||
setRoutes(state, routes) {
|
||||
state.isGenerate = true
|
||||
let newRoutes = deepClone(routes)
|
||||
state.routes = newRoutes.filter(item => {
|
||||
return item.children.length != 0
|
||||
})
|
||||
},
|
||||
// 根据路由判断属于哪个头部导航
|
||||
setHeaderActived(state, path) {
|
||||
state.routes.map((item, index) => {
|
||||
if (
|
||||
item.children.some(r => {
|
||||
return path.indexOf(r.path + '/') === 0 || path == r.path
|
||||
})
|
||||
) {
|
||||
state.headerActived = index
|
||||
}
|
||||
})
|
||||
},
|
||||
// 切换头部导航
|
||||
switchHeaderActived(state, index) {
|
||||
state.headerActived = index
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
actions,
|
||||
getters,
|
||||
mutations
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 存放全局公用状态
|
||||
*/
|
||||
import setting from '@/setting'
|
||||
|
||||
const state = {
|
||||
...setting,
|
||||
// 侧边栏是否收起(用于记录 pc 模式下最后的状态)
|
||||
sidebarCollapseLastStatus: setting.sidebarCollapse,
|
||||
// 显示模式,支持:mobile、pc
|
||||
mode: 'pc',
|
||||
// 页面标题
|
||||
title: ''
|
||||
}
|
||||
|
||||
const getters = {}
|
||||
|
||||
const actions = {}
|
||||
|
||||
const mutations = {
|
||||
// 设置访问模式,页面宽度小于 992px 时切换为移动端展示
|
||||
setMode(state, width) {
|
||||
if (width < 992) {
|
||||
state.mode = 'mobile'
|
||||
} else {
|
||||
state.mode = 'pc'
|
||||
}
|
||||
},
|
||||
// 设置网页标题
|
||||
setTitle(state, title) {
|
||||
state.title = title
|
||||
},
|
||||
// 切换侧边栏导航展开/收起
|
||||
toggleSidebarCollapse(state) {
|
||||
state.sidebarCollapse = !state.sidebarCollapse
|
||||
if (state.mode == 'pc') {
|
||||
state.sidebarCollapseLastStatus = !state.sidebarCollapseLastStatus
|
||||
}
|
||||
},
|
||||
// 更新主题配置
|
||||
updateThemeSetting(state, data) {
|
||||
Object.assign(state, data)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
actions,
|
||||
getters,
|
||||
mutations
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import api from '@/api'
|
||||
|
||||
const state = {
|
||||
account: localStorage.account || '',
|
||||
token: localStorage.token || '',
|
||||
failure_time: localStorage.failure_time || '',
|
||||
permissions: []
|
||||
}
|
||||
|
||||
const getters = {
|
||||
isLogin: state => {
|
||||
let retn = false
|
||||
if (state.token) {
|
||||
let unix = Date.parse(new Date())
|
||||
if (unix < state.failure_time * 1000) {
|
||||
retn = true
|
||||
}
|
||||
}
|
||||
return retn
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
login({commit}, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 通过 mock 进行登录
|
||||
api.post('mock/member/login', data).then(res => {
|
||||
commit('setUserData', res.data)
|
||||
resolve()
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
logout({commit}) {
|
||||
commit('removeUserData')
|
||||
commit('menu/invalidRoutes', null, {root: true})
|
||||
},
|
||||
// 获取我的权限
|
||||
getPermissions({state, commit}) {
|
||||
return new Promise(resolve => {
|
||||
// 通过 mock 获取权限
|
||||
api.get('mock/member/permission', {
|
||||
params: {
|
||||
account: state.account
|
||||
}
|
||||
}).then(res => {
|
||||
commit('setPermissions', res.data.permissions)
|
||||
resolve(res.data.permissions)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
setUserData(state, data) {
|
||||
localStorage.setItem('account', data.account)
|
||||
localStorage.setItem('token', data.token)
|
||||
localStorage.setItem('failure_time', data.failure_time)
|
||||
state.account = data.account
|
||||
state.token = data.token
|
||||
state.failure_time = data.failure_time
|
||||
},
|
||||
removeUserData(state) {
|
||||
localStorage.removeItem('account')
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('failure_time')
|
||||
state.account = ''
|
||||
state.token = ''
|
||||
state.failure_time = ''
|
||||
},
|
||||
setPermissions(state, permissions) {
|
||||
state.permissions = permissions
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
actions,
|
||||
getters,
|
||||
mutations
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { auth, authAll } from '@/util'
|
||||
|
||||
export default {
|
||||
install(Vue) {
|
||||
Vue.prototype.$auth = auth
|
||||
Vue.prototype.$authAll = authAll
|
||||
// 注册 v-auth 和 v-auth-all 指令
|
||||
Vue.directive('auth', {
|
||||
inserted: (el, binding) => {
|
||||
if (!auth(binding.value)) {
|
||||
el.remove()
|
||||
}
|
||||
}
|
||||
})
|
||||
Vue.directive('auth-all', {
|
||||
inserted: (el, binding) => {
|
||||
if (!authAll(binding.value)) {
|
||||
el.remove()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import store from '@/store/index'
|
||||
|
||||
export function deepClone(target) {
|
||||
// 定义一个变量
|
||||
let result
|
||||
// 如果当前需要深拷贝的是一个对象的话
|
||||
if (typeof target === 'object') {
|
||||
// 如果是一个数组的话
|
||||
if (Array.isArray(target)) {
|
||||
result = [] // 将result赋值为一个数组,并且执行遍历
|
||||
for (let i in target) {
|
||||
// 递归克隆数组中的每一项
|
||||
result.push(deepClone(target[i]))
|
||||
}
|
||||
// 判断如果当前的值是null的话;直接赋值为null
|
||||
} else if (target === null) {
|
||||
result = null
|
||||
// 判断如果当前的值是一个RegExp对象的话,直接赋值
|
||||
} else if (target.constructor === RegExp) {
|
||||
result = target
|
||||
} else {
|
||||
// 否则是普通对象,直接for in循环,递归赋值对象的所有值
|
||||
result = {}
|
||||
for (let i in target) {
|
||||
result[i] = deepClone(target[i])
|
||||
}
|
||||
}
|
||||
// 如果不是对象的话,就是基本数据类型,那么直接赋值
|
||||
} else {
|
||||
result = target
|
||||
}
|
||||
// 返回最终结果
|
||||
return result
|
||||
}
|
||||
|
||||
function hasPermission(permission) {
|
||||
if (store.state.settings.openPermission) {
|
||||
return store.state.user.permissions.some(v => {
|
||||
return v === permission
|
||||
})
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export function auth(value) {
|
||||
let auth
|
||||
if (typeof value === 'string') {
|
||||
auth = hasPermission(value)
|
||||
} else {
|
||||
auth = value.some(item => {
|
||||
return hasPermission(item)
|
||||
})
|
||||
}
|
||||
return auth
|
||||
}
|
||||
|
||||
export function authAll(value) {
|
||||
const auth = value.every(item => {
|
||||
return hasPermission(item)
|
||||
})
|
||||
return auth
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="notfound">
|
||||
<svg-icon name="404" />
|
||||
<div class="content">
|
||||
<h1>404</h1>
|
||||
<div class="desc">抱歉,你访问的页面不存在</div>
|
||||
<el-button type="primary" @click="goBack">{{ countdown }}秒后,返回首页</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
beforeRouteLeave(to, from, next) {
|
||||
clearInterval(this.inter)
|
||||
next()
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inter: null,
|
||||
countdown: 5
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.inter = setInterval(() => {
|
||||
this.countdown--
|
||||
if (this.countdown == 0) {
|
||||
clearInterval(this.inter)
|
||||
this.goBack()
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
this.$router.push('/')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notfound {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 700px;
|
||||
@include position-center(xy);
|
||||
.svg-icon {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
}
|
||||
.content {
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 72px;
|
||||
color: #303133;
|
||||
}
|
||||
.desc {
|
||||
margin: 20px 0 30px;
|
||||
font-size: 20px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-main>
|
||||
注意看面包屑导航的变化
|
||||
</page-main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-main>
|
||||
注意看面包屑导航的变化
|
||||
</page-main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-main>
|
||||
<router-link :to="{name: 'breadcrumbExampleDetail1'}">查看详情页</router-link>
|
||||
</page-main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-main>
|
||||
<router-link :to="{name: 'breadcrumbExampleDetail2'}">查看详情页</router-link>
|
||||
</page-main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-header title="欢迎使用 Fantastic-admin(基础版)">
|
||||
<template #content>
|
||||
<div>
|
||||
<div style="margin-bottom: 5px;">这是一款<b style="text-emphasis-style: '❤';">开箱即用</b>的中后台框架,同时它也经历过数十个真实项目的技术沉淀,确保框架在开发中可落地、可使用、可维护</div>
|
||||
<div>注:在作者就职的公司,本框架已在电商、直播、OA、ERP等多个不同领域的中后台系统中应用并稳定运行</div>
|
||||
</div>
|
||||
</template>
|
||||
</page-header>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg-banner" />
|
||||
<div id="login-box">
|
||||
<div class="login-banner" />
|
||||
<el-form ref="form" :model="form" :rules="rules" class="login-form" auto-complete="on" label-position="left">
|
||||
<div class="title-container">
|
||||
<h3 class="title">{{ title }}管理后台</h3>
|
||||
</div>
|
||||
<div>
|
||||
<el-form-item prop="account">
|
||||
<el-input ref="name" v-model="form.account" placeholder="用户名" type="text" tabindex="1" auto-complete="on">
|
||||
<svg-icon slot="prefix" name="user" />
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input ref="password" v-model="form.password" :type="passwordType" placeholder="密码" tabindex="2" auto-complete="on" @keyup.enter.native="handleLogin">
|
||||
<svg-icon slot="prefix" name="password" />
|
||||
<svg-icon slot="suffix" :name="passwordType === 'password' ? 'eye' : 'eye-open'" @click="showPassword" />
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-checkbox v-model="form.remember">记住我</el-checkbox>
|
||||
<el-button :loading="loading" type="primary" style="width: 100%;" @click.native.prevent="handleLogin">登 录</el-button>
|
||||
<div style="margin-top: 20px; margin-bottom: -10px; color: #666; font-size: 14px; text-align: center; font-weight: bold;">
|
||||
<span style="margin-right: 5px;">演示帐号一键登录:</span>
|
||||
<el-button type="danger" size="mini" @click="testAccount('admin')">admin</el-button>
|
||||
<el-button type="danger" size="mini" plain @click="testAccount('test')">test</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
<Copyright v-if="$store.state.settings.showCopyright" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Login',
|
||||
data() {
|
||||
return {
|
||||
title: process.env.VUE_APP_TITLE,
|
||||
form: {
|
||||
account: localStorage.login_account || '',
|
||||
password: '',
|
||||
remember: !!localStorage.login_account
|
||||
},
|
||||
rules: {
|
||||
account: [
|
||||
{ required: true, trigger: 'blur', message: '请输入用户名' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, trigger: 'blur', message: '请输入密码' },
|
||||
{ min: 6, max: 18, trigger: 'blur', message: '密码长度为6到18位' }
|
||||
]
|
||||
},
|
||||
loading: false,
|
||||
passwordType: 'password',
|
||||
redirect: undefined
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route: {
|
||||
handler: function(route) {
|
||||
this.redirect = route.query && route.query.redirect
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showPassword() {
|
||||
this.passwordType = this.passwordType === 'password' ? '' : 'password'
|
||||
this.$nextTick(() => {
|
||||
this.$refs.password.focus()
|
||||
})
|
||||
},
|
||||
handleLogin() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (valid) {
|
||||
this.loading = true
|
||||
this.$store.dispatch('user/login', this.form).then(() => {
|
||||
this.loading = false
|
||||
this.form.remember && localStorage.setItem('login_account', this.form.account)
|
||||
this.$router.push({ path: this.redirect || '/' })
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
testAccount(account) {
|
||||
this.form.account = account
|
||||
this.form.password = '123456'
|
||||
this.handleLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
[data-mode=mobile] {
|
||||
#login-box {
|
||||
max-width: 80%;
|
||||
.login-banner {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
::v-deep input[type=password]::-ms-reveal {
|
||||
display: none;
|
||||
}
|
||||
.bg-banner {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url(../assets/images/login-bg.jpg);
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
}
|
||||
#login-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 5px #999;
|
||||
.login-banner {
|
||||
width: 300px;
|
||||
background-image: url(../assets/images/login-banner.jpg);
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
}
|
||||
.login-form {
|
||||
width: 450px;
|
||||
padding: 50px 35px 30px;
|
||||
overflow: hidden;
|
||||
.svg-icon {
|
||||
vertical-align: -0.35em;
|
||||
}
|
||||
.title-container {
|
||||
position: relative;
|
||||
.title {
|
||||
font-size: 22px;
|
||||
color: #666;
|
||||
margin: 0 auto 30px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
@include text-overflow;
|
||||
}
|
||||
}
|
||||
}
|
||||
::v-deep .el-input {
|
||||
display: inline-block;
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
input {
|
||||
height: 48px;
|
||||
}
|
||||
.el-input__prefix {
|
||||
left: 10px;
|
||||
}
|
||||
.el-input__suffix {
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
.el-checkbox {
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
.copyright {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
mix-blend-mode: difference;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-main>
|
||||
多级导航2-2-1
|
||||
</page-main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-main>
|
||||
多级导航2-2-2
|
||||
</page-main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-main>
|
||||
多级导航2-1
|
||||
</page-main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-main>
|
||||
多级导航1
|
||||
</page-main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-header title="修改密码" content="定期修改密码可以提高帐号安全性噢~" />
|
||||
<page-main>
|
||||
<el-row>
|
||||
<el-col :md="24" :lg="12">
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="原密码" prop="password">
|
||||
<el-input v-model="form.password" type="password" placeholder="请输入原密码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码" prop="newpassword">
|
||||
<el-input v-model="form.newpassword" type="password" placeholder="请输入原密码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="确认新密码" prop="checkpassword">
|
||||
<el-input v-model="form.checkpassword" type="password" placeholder="请输入原密码" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</page-main>
|
||||
<fixed-action-bar>
|
||||
<el-button type="primary" @click="onSubmit">提交</el-button>
|
||||
</fixed-action-bar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
const validatePassword = (rule, value, callback) => {
|
||||
if (value !== this.form.newpassword) {
|
||||
callback(new Error('请确认新密码'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
return {
|
||||
form: {
|
||||
password: '',
|
||||
newpassword: '',
|
||||
checkpassword: ''
|
||||
},
|
||||
rules: {
|
||||
password: [
|
||||
{ required: true, message: '请输入原密码', trigger: 'blur' }
|
||||
],
|
||||
newpassword: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 6, max: 18, trigger: 'blur', message: '密码长度为6到18位' }
|
||||
],
|
||||
checkpassword: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ validator: validatePassword }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.$refs['form'].validate(valid => {
|
||||
if (valid) {
|
||||
this.$store.dispatch('user/editPassword', this.form).then(() => {
|
||||
this.$message({
|
||||
type: 'success',
|
||||
message: '修改成功,请重新登录'
|
||||
})
|
||||
this.$store.dispatch('user/logout').then(() => {
|
||||
this.$router.push('/login')
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 页面:Setting -->
|
||||
<page-main>
|
||||
<el-tabs tab-position="left" style="height: 600px;">
|
||||
<el-tab-pane label="基本设置" class="basic">
|
||||
<h2>基本设置</h2>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="16">
|
||||
<el-form ref="form" :model="form" label-width="120px" label-suffix=":">
|
||||
<el-form-item label="名 称">
|
||||
<el-input v-model="form.name" placeholder="请输入你的名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="form.mobile" placeholder="请输入你的手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="QQ 号">
|
||||
<el-input v-model="form.qq" placeholder="请输入你的 QQ 号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="微信号">
|
||||
<el-input v-model="form.wechat" placeholder="请输入你的微信号" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary">保存</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<image-upload :url.sync="form.headimg" action="http://scrm.1daas.com/api/upload/upload" name="image" :data="{'token':'TKD628431923530324'}" notip class="headimg-upload" @onSuccess="handleSuccess" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="安全设置" class="security">
|
||||
<h2>安全设置</h2>
|
||||
<div class="setting-list">
|
||||
<div class="item">
|
||||
<div class="content">
|
||||
<div class="title">账户密码</div>
|
||||
<div class="desc">当前密码强度:强</div>
|
||||
</div>
|
||||
<div class="action">
|
||||
<el-button type="text" @click="editPassword">修改</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="content">
|
||||
<div class="title">密保手机</div>
|
||||
<div class="desc">已绑定手机:187****3441</div>
|
||||
</div>
|
||||
<div class="action">
|
||||
<el-button type="text">修改</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="content">
|
||||
<div class="title">备用邮箱</div>
|
||||
<div class="desc">当前未绑定备用邮箱</div>
|
||||
</div>
|
||||
<div class="action">
|
||||
<el-button type="text">绑定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</page-main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PersonalSetting',
|
||||
beforeRouteLeave(to, from, next) {
|
||||
if (['personalEditPassword'].includes(to.name)) {
|
||||
this.$store.commit('keepAlive/add', 'PersonalSetting')
|
||||
}
|
||||
next()
|
||||
},
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
headimg: '',
|
||||
mobile: '',
|
||||
name: '',
|
||||
qq: '',
|
||||
wechat: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {},
|
||||
mounted() {},
|
||||
methods: {
|
||||
handleSuccess(res) {
|
||||
if (res.error == '') {
|
||||
this.form.headimg = res.data.path
|
||||
} else {
|
||||
this.$message.warning(res.error)
|
||||
}
|
||||
},
|
||||
editPassword() {
|
||||
this.$router.push({
|
||||
name: 'personalEditPassword'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .el-tabs {
|
||||
.el-tabs__header .el-tabs__nav {
|
||||
.el-tabs__active-bar {
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
background-color: #e1f0ff;
|
||||
border-right: 2px solid #409eff;
|
||||
}
|
||||
.el-tabs__item {
|
||||
text-align: left;
|
||||
padding-right: 100px;
|
||||
}
|
||||
}
|
||||
.el-tab-pane {
|
||||
padding: 0 20px 0 30px;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
margin-bottom: 30px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.basic {
|
||||
::v-deep .headimg-upload {
|
||||
> div {
|
||||
text-align: center;
|
||||
}
|
||||
.el-upload-dragger {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.security {
|
||||
.setting-list {
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
.content {
|
||||
.title {
|
||||
margin-bottom: 5px;
|
||||
color: #666;
|
||||
}
|
||||
.desc {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
mounted() {
|
||||
this.$router.go(-1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||