刘力
3 years ago
16 changed files with 1265 additions and 104 deletions
-
160src/components/ThemePicker/index.vue
-
60src/layout/components/AppMain.vue
-
265src/layout/components/Navbar.vue
-
31src/layout/components/Sidebar/FixiOSBug.js
-
36src/layout/components/Sidebar/Link.vue
-
90src/layout/components/Sidebar/SidebarItem.vue
-
44src/layout/components/Sidebar/TopMenu.vue
-
45src/layout/components/Sidebar/TopMenus.vue
-
81src/layout/components/TagsView/ScrollPane.vue
-
275src/layout/components/TagsView/index.vue
-
5src/layout/components/index.js
-
74src/layout/index.vue
-
6src/main.js
-
27src/router/routers.js
-
166src/utils/validate.js
-
4vue.config.js
@ -0,0 +1,160 @@ |
|||||
|
<template> |
||||
|
<el-color-picker v-model="theme" :predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d']" class="theme-picker" popper-class="theme-picker-dropdown" /> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
const version = require('element-ui/package.json').version // element-ui version from node_modules |
||||
|
const ORIGINAL_THEME = '#409EFF' // default color |
||||
|
import Cookies from 'js-cookie' |
||||
|
export default { |
||||
|
data() { |
||||
|
return { |
||||
|
chalk: '', // content of theme-chalk css |
||||
|
theme: '' |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
defaultTheme() { |
||||
|
return this.$store.state.settings.theme |
||||
|
} |
||||
|
}, |
||||
|
watch: { |
||||
|
defaultTheme: { |
||||
|
handler: function(val, oldVal) { |
||||
|
this.theme = val |
||||
|
}, |
||||
|
immediate: true |
||||
|
}, |
||||
|
async theme(val) { |
||||
|
Cookies.set('theme', val, { expires: 365 }) |
||||
|
const oldVal = this.chalk ? this.theme : Cookies.get('theme') ? Cookies.get('theme') : ORIGINAL_THEME |
||||
|
if (typeof val !== 'string') return |
||||
|
const themeCluster = this.getThemeCluster(val.replace('#', '')) |
||||
|
const originalCluster = this.getThemeCluster(oldVal.replace('#', '')) |
||||
|
|
||||
|
const getHandler = (variable, id) => { |
||||
|
return () => { |
||||
|
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', '')) |
||||
|
const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster) |
||||
|
|
||||
|
let styleTag = document.getElementById(id) |
||||
|
if (!styleTag) { |
||||
|
styleTag = document.createElement('style') |
||||
|
styleTag.setAttribute('id', id) |
||||
|
document.head.appendChild(styleTag) |
||||
|
} |
||||
|
styleTag.innerText = newStyle |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!this.chalk) { |
||||
|
const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css` |
||||
|
await this.getCSSString(url, 'chalk') |
||||
|
} |
||||
|
|
||||
|
const chalkHandler = getHandler('chalk', 'chalk-style') |
||||
|
|
||||
|
chalkHandler() |
||||
|
|
||||
|
const styles = [].slice.call(document.querySelectorAll('style')) |
||||
|
.filter(style => { |
||||
|
const text = style.innerText |
||||
|
return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text) |
||||
|
}) |
||||
|
styles.forEach(style => { |
||||
|
const { innerText } = style |
||||
|
if (typeof innerText !== 'string') return |
||||
|
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster) |
||||
|
}) |
||||
|
|
||||
|
this.$emit('change', val) |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
updateStyle(style, oldCluster, newCluster) { |
||||
|
let newStyle = style |
||||
|
oldCluster.forEach((color, index) => { |
||||
|
newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index]) |
||||
|
}) |
||||
|
return newStyle |
||||
|
}, |
||||
|
|
||||
|
getCSSString(url, variable) { |
||||
|
return new Promise(resolve => { |
||||
|
const xhr = new XMLHttpRequest() |
||||
|
xhr.onreadystatechange = () => { |
||||
|
if (xhr.readyState === 4 && xhr.status === 200) { |
||||
|
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '') |
||||
|
resolve() |
||||
|
} |
||||
|
} |
||||
|
xhr.open('GET', url) |
||||
|
xhr.send() |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
getThemeCluster(theme) { |
||||
|
const tintColor = (color, tint) => { |
||||
|
let red = parseInt(color.slice(0, 2), 16) |
||||
|
let green = parseInt(color.slice(2, 4), 16) |
||||
|
let blue = parseInt(color.slice(4, 6), 16) |
||||
|
|
||||
|
if (tint === 0) { // when primary color is in its rgb space |
||||
|
return [red, green, blue].join(',') |
||||
|
} else { |
||||
|
red += Math.round(tint * (255 - red)) |
||||
|
green += Math.round(tint * (255 - green)) |
||||
|
blue += Math.round(tint * (255 - blue)) |
||||
|
|
||||
|
red = red.toString(16) |
||||
|
green = green.toString(16) |
||||
|
blue = blue.toString(16) |
||||
|
|
||||
|
return `#${red}${green}${blue}` |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const shadeColor = (color, shade) => { |
||||
|
let red = parseInt(color.slice(0, 2), 16) |
||||
|
let green = parseInt(color.slice(2, 4), 16) |
||||
|
let blue = parseInt(color.slice(4, 6), 16) |
||||
|
|
||||
|
red = Math.round((1 - shade) * red) |
||||
|
green = Math.round((1 - shade) * green) |
||||
|
blue = Math.round((1 - shade) * blue) |
||||
|
|
||||
|
red = red.toString(16) |
||||
|
green = green.toString(16) |
||||
|
blue = blue.toString(16) |
||||
|
|
||||
|
return `#${red}${green}${blue}` |
||||
|
} |
||||
|
|
||||
|
const clusters = [theme] |
||||
|
for (let i = 0; i <= 9; i++) { |
||||
|
clusters.push(tintColor(theme, Number((i / 10).toFixed(2)))) |
||||
|
} |
||||
|
clusters.push(shadeColor(theme, 0.1)) |
||||
|
return clusters |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
.theme-message, |
||||
|
.theme-picker-dropdown { |
||||
|
z-index: 99999 !important; |
||||
|
} |
||||
|
|
||||
|
.theme-picker .el-color-picker__trigger { |
||||
|
height: 26px !important; |
||||
|
width: 26px !important; |
||||
|
padding: 2px; |
||||
|
} |
||||
|
|
||||
|
.theme-picker-dropdown .el-color-dropdown__link-btn { |
||||
|
display: none; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,60 @@ |
|||||
|
<template> |
||||
|
<section class="app-main"> |
||||
|
<transition name="fade-transform" mode="out-in"> |
||||
|
<keep-alive :include="cachedViews"> |
||||
|
<router-view :key="key" /> |
||||
|
</keep-alive> |
||||
|
</transition> |
||||
|
<div v-if="$store.state.settings.showFooter" id="el-main-footer"> |
||||
|
<span v-html="$store.state.settings.footerTxt" /> |
||||
|
<span> ⋅ </span> |
||||
|
<a href="https://beian.miit.gov.cn/#/Integrated/index" target="_blank">{{ $store.state.settings.caseNumber }}</a> |
||||
|
</div> |
||||
|
</section> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: 'AppMain', |
||||
|
computed: { |
||||
|
cachedViews() { |
||||
|
return this.$store.state.tagsView.cachedViews |
||||
|
}, |
||||
|
key() { |
||||
|
return this.$route.path |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.app-main { |
||||
|
/* 50= navbar 50 */ |
||||
|
min-height: calc(100vh - 50px); |
||||
|
width: 100%; |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.fixed-header + .app-main { |
||||
|
padding-top: 50px; |
||||
|
} |
||||
|
|
||||
|
.hasTagsView { |
||||
|
.app-main { |
||||
|
min-height: calc(100vh - 84px); |
||||
|
} |
||||
|
|
||||
|
.fixed-header + .app-main { |
||||
|
padding-top: 84px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<style lang="scss"> |
||||
|
.el-popup-parent--hidden { |
||||
|
.fixed-header { |
||||
|
padding-right: 15px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,265 @@ |
|||||
|
<template> |
||||
|
<div class="navbar"> |
||||
|
|
||||
|
<hamburger id="hamburger-container" class="hamburger-container" @toggleClick="toggleSideBar" /> |
||||
|
<!-- 移除面包屑效果 --> |
||||
|
<!-- <breadcrumb id="breadcrumb-container" class="breadcrumb-container" /> --> |
||||
|
|
||||
|
<!-- 顶部菜单 start--> |
||||
|
<div class="navmenu"> |
||||
|
<template> |
||||
|
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal" @select="handleSelect"> |
||||
|
<el-menu-item :index="resolvePath(onlyOneChild.path)"> |
||||
|
</el-menu-item> |
||||
|
</el-menu> |
||||
|
</template> |
||||
|
</div> |
||||
|
<!-- 顶部菜单 end--> |
||||
|
<div class="right-menu"> |
||||
|
<template> |
||||
|
<search id="header-search" class="right-menu-item" /> |
||||
|
<el-tooltip content="项目文档" effect="dark" placement="bottom"> |
||||
|
<Doc class="right-menu-item hover-effect" /> |
||||
|
</el-tooltip> |
||||
|
<el-tooltip content="全屏缩放" effect="dark" placement="bottom"> |
||||
|
<screenfull id="screenfull" class="right-menu-item hover-effect" /> |
||||
|
</el-tooltip> |
||||
|
<el-tooltip content="布局设置" effect="dark" placement="bottom"> |
||||
|
<size-select id="size-select" class="right-menu-item hover-effect" /> |
||||
|
</el-tooltip> |
||||
|
</template> |
||||
|
|
||||
|
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click"> |
||||
|
<div class="avatar-wrapper"> |
||||
|
<img class="user-avatar"> |
||||
|
<i class="el-icon-caret-bottom" /> |
||||
|
</div> |
||||
|
<el-dropdown-menu slot="dropdown"> |
||||
|
<span style="display:block;" @click="show = true"> |
||||
|
<el-dropdown-item> |
||||
|
布局设置 |
||||
|
</el-dropdown-item> |
||||
|
</span> |
||||
|
<router-link to="/user/center"> |
||||
|
<el-dropdown-item> |
||||
|
个人中心 |
||||
|
</el-dropdown-item> |
||||
|
</router-link> |
||||
|
<span style="display:block;" @click="open"> |
||||
|
<el-dropdown-item divided> |
||||
|
退出登录 |
||||
|
</el-dropdown-item> |
||||
|
</span> |
||||
|
</el-dropdown-menu> |
||||
|
</el-dropdown> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { mapGetters } from "vuex"; |
||||
|
//import Breadcrumb from "@/components/Breadcrumb"; |
||||
|
//import Hamburger from "@/components/Hamburger"; |
||||
|
//import Doc from "@/components/Doc"; |
||||
|
//import Screenfull from "@/components/Screenfull"; |
||||
|
//import SizeSelect from "@/components/SizeSelect"; |
||||
|
//import Search from "@/components/HeaderSearch"; |
||||
|
import Avatar from "@/assets/images/avatar.png"; |
||||
|
import path from "path"; |
||||
|
import { isExternal } from "@/utils/validate"; |
||||
|
//import Item from './Sidebar/Item' |
||||
|
import AppLink from './Sidebar/Link' |
||||
|
|
||||
|
export default { |
||||
|
components: { |
||||
|
//Breadcrumb, 移除面包屑 |
||||
|
// Hamburger, |
||||
|
//Screenfull, |
||||
|
// SizeSelect, |
||||
|
//Search, |
||||
|
//Doc, |
||||
|
// Item, |
||||
|
AppLink |
||||
|
}, |
||||
|
props: { |
||||
|
// item: { |
||||
|
// type: Object, |
||||
|
// required: true |
||||
|
// }, |
||||
|
isNest: { |
||||
|
type: Boolean, |
||||
|
default: false |
||||
|
}, |
||||
|
basePath: { |
||||
|
type: String, |
||||
|
default: '' |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
this.onlyOneChild=null |
||||
|
return { |
||||
|
Avatar: Avatar, |
||||
|
dialogVisible: false, |
||||
|
activeIndex: "1", |
||||
|
activeIndex2: "1", |
||||
|
}; |
||||
|
}, |
||||
|
computed: { |
||||
|
//...mapGetters(["sidebar", "device", "user", "baseApi"]), |
||||
|
show: { |
||||
|
get() { |
||||
|
return this.$store.state.settings.showSettings; |
||||
|
}, |
||||
|
set(val) { |
||||
|
this.$store.dispatch("settings/changeSetting", { |
||||
|
key: "showSettings", |
||||
|
value: val, |
||||
|
}); |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
methods: { |
||||
|
toggleSideBar() { |
||||
|
this.$store.dispatch("app/toggleSideBar"); |
||||
|
}, |
||||
|
open() { |
||||
|
this.$confirm("确定注销并退出系统吗?", "提示", { |
||||
|
confirmButtonText: "确定", |
||||
|
cancelButtonText: "取消", |
||||
|
type: "warning", |
||||
|
}).then(() => { |
||||
|
this.logout(); |
||||
|
}); |
||||
|
}, |
||||
|
handleSelect(key, keyPath) { |
||||
|
console.log(key, keyPath); |
||||
|
}, |
||||
|
hasOneShowingChild(children = [], parent) { |
||||
|
// const showingChildren = children.filter((item) => { |
||||
|
// if (item.hidden) { |
||||
|
// return false; |
||||
|
// } else { |
||||
|
// // Temp set(will be used if only has one showing child) |
||||
|
// this.onlyOneChild = item; |
||||
|
// return true; |
||||
|
// } |
||||
|
// }); |
||||
|
|
||||
|
// When there is only one child router, the child router is displayed by default |
||||
|
// if (showingChildren.length === 1) { |
||||
|
// return true; |
||||
|
// } |
||||
|
|
||||
|
// // Show parent if there are no child router to display |
||||
|
// if (showingChildren.length === 0) { |
||||
|
// this.onlyOneChild = { ...parent, path: "", noShowingChildren: true }; |
||||
|
// return true; |
||||
|
// } |
||||
|
|
||||
|
// return false; |
||||
|
}, |
||||
|
resolvePath(routePath) { |
||||
|
if (isExternal(routePath)) { |
||||
|
return routePath; |
||||
|
} |
||||
|
if (isExternal(this.basePath)) { |
||||
|
return this.basePath; |
||||
|
} |
||||
|
return path.resolve(this.basePath, routePath); |
||||
|
}, |
||||
|
logout() { |
||||
|
this.$store.dispatch("LogOut").then(() => { |
||||
|
location.reload(); |
||||
|
}); |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.navbar { |
||||
|
height: 60px; |
||||
|
overflow: hidden; |
||||
|
position: relative; |
||||
|
background: #fff; |
||||
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); |
||||
|
|
||||
|
.navmenu { |
||||
|
float: left; |
||||
|
} |
||||
|
|
||||
|
.hamburger-container { |
||||
|
line-height: 46px; |
||||
|
height: 100%; |
||||
|
float: left; |
||||
|
cursor: pointer; |
||||
|
transition: background 0.3s; |
||||
|
-webkit-tap-highlight-color: transparent; |
||||
|
|
||||
|
&:hover { |
||||
|
background: rgba(0, 0, 0, 0.025); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.breadcrumb-container { |
||||
|
float: left; |
||||
|
} |
||||
|
|
||||
|
.errLog-container { |
||||
|
display: inline-block; |
||||
|
vertical-align: top; |
||||
|
} |
||||
|
|
||||
|
.right-menu { |
||||
|
float: right; |
||||
|
height: 100%; |
||||
|
line-height: 50px; |
||||
|
|
||||
|
&:focus { |
||||
|
outline: none; |
||||
|
} |
||||
|
|
||||
|
.right-menu-item { |
||||
|
display: inline-block; |
||||
|
padding: 0 8px; |
||||
|
height: 100%; |
||||
|
font-size: 18px; |
||||
|
color: #5a5e66; |
||||
|
vertical-align: text-bottom; |
||||
|
|
||||
|
&.hover-effect { |
||||
|
cursor: pointer; |
||||
|
transition: background 0.3s; |
||||
|
|
||||
|
&:hover { |
||||
|
background: rgba(0, 0, 0, 0.025); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.avatar-container { |
||||
|
margin-right: 30px; |
||||
|
|
||||
|
.avatar-wrapper { |
||||
|
margin-top: 5px; |
||||
|
position: relative; |
||||
|
|
||||
|
.user-avatar { |
||||
|
cursor: pointer; |
||||
|
width: 40px; |
||||
|
height: 40px; |
||||
|
border-radius: 10px; |
||||
|
} |
||||
|
|
||||
|
.el-icon-caret-bottom { |
||||
|
cursor: pointer; |
||||
|
position: absolute; |
||||
|
right: -20px; |
||||
|
top: 25px; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -1,7 +1,24 @@ |
|||||
// export default{
|
|
||||
// computed:{
|
|
||||
// device(){
|
|
||||
// return this.$store.state.app
|
|
||||
// }
|
|
||||
// }
|
|
||||
// }
|
|
||||
|
export default { |
||||
|
computed: { |
||||
|
device() { |
||||
|
return this.$store.state.app.device |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.fixBugIniOS() |
||||
|
}, |
||||
|
methods: { |
||||
|
fixBugIniOS() { |
||||
|
const $subMenu = this.$refs.subMenu |
||||
|
if ($subMenu) { |
||||
|
const handleMouseleave = $subMenu.handleMouseleave |
||||
|
$subMenu.handleMouseleave = (e) => { |
||||
|
if (this.device === 'mobile') { |
||||
|
return |
||||
|
} |
||||
|
handleMouseleave(e) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,36 @@ |
|||||
|
|
||||
|
<template> |
||||
|
|
||||
|
<component v-bind:is="linkProps(to)"> |
||||
|
<slot /> |
||||
|
</component> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { isExternal } from '@/utils/validate' |
||||
|
|
||||
|
export default { |
||||
|
props: { |
||||
|
to: { |
||||
|
type: String, |
||||
|
required: true |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
linkProps(url) { |
||||
|
if (isExternal(url)) { |
||||
|
return { |
||||
|
is: 'a', |
||||
|
href: url, |
||||
|
target: '_blank', |
||||
|
rel: 'noopener' |
||||
|
} |
||||
|
} |
||||
|
return { |
||||
|
is: 'router-link', |
||||
|
to: url |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,90 @@ |
|||||
|
<template> |
||||
|
<div v-if="!item.hidden" class="menu-wrapper"> |
||||
|
<template v-if=" |
||||
|
hasOneShowingChild(item.children, item) && |
||||
|
(!onlyOneChild.children || onlyOneChild.noShowingChildren) && |
||||
|
!item.alwaysShow |
||||
|
"> |
||||
|
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)"> |
||||
|
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }"> |
||||
|
<item :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" :title="onlyOneChild.meta.title" /> |
||||
|
</el-menu-item> |
||||
|
</app-link> |
||||
|
</template> |
||||
|
|
||||
|
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body> |
||||
|
<template slot="title"> |
||||
|
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" /> |
||||
|
</template> |
||||
|
<sidebar-item v-for="child in item.children" :key="child.path" :is-nest="true" :item="child" :base-path="resolvePath(child.path)" class="nest-menu" /> |
||||
|
</el-submenu> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import path from 'path' |
||||
|
import { isExternal } from '@/utils/validate' |
||||
|
import Item from './Item' |
||||
|
import AppLink from './Link' |
||||
|
import FixiOSBug from './FixiOSBug' |
||||
|
|
||||
|
export default { |
||||
|
name: 'SidebarItem', |
||||
|
components: { Item, AppLink }, |
||||
|
mixins: [FixiOSBug], |
||||
|
props: { |
||||
|
// route object |
||||
|
item: { |
||||
|
type: Object, |
||||
|
required: true |
||||
|
}, |
||||
|
isNest: { |
||||
|
type: Boolean, |
||||
|
default: false |
||||
|
}, |
||||
|
basePath: { |
||||
|
type: String, |
||||
|
default: '' |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
this.onlyOneChild = null |
||||
|
return {} |
||||
|
}, |
||||
|
methods: { |
||||
|
hasOneShowingChild(children = [], parent) { |
||||
|
const showingChildren = children.filter(item => { |
||||
|
if (item.hidden) { |
||||
|
return false |
||||
|
} else { |
||||
|
// Temp set(will be used if only has one showing child) |
||||
|
this.onlyOneChild = item |
||||
|
return true |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
// When there is only one child router, the child router is displayed by default |
||||
|
if (showingChildren.length === 1) { |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// Show parent if there are no child router to display |
||||
|
if (showingChildren.length === 0) { |
||||
|
this.onlyOneChild = { ...parent, path: '', noShowingChildren: true } |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
return false |
||||
|
}, |
||||
|
resolvePath(routePath) { |
||||
|
if (isExternal(routePath)) { |
||||
|
return routePath |
||||
|
} |
||||
|
if (isExternal(this.basePath)) { |
||||
|
return this.basePath |
||||
|
} |
||||
|
return path.resolve(this.basePath, routePath) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
@ -1,44 +0,0 @@ |
|||||
<!-- |
|
||||
* @Author: Liu_li |
|
||||
* @Descripttion: |
|
||||
* @Date: 2021-09-26 11:03:50 |
|
||||
--> |
|
||||
<template> |
|
||||
<div> |
|
||||
<template> |
|
||||
<app-link> |
|
||||
<el-menu-item> |
|
||||
<item :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" :title="onlyOneChild.meta.title" /> |
|
||||
</el-menu-item> |
|
||||
</app-link> |
|
||||
</template> |
|
||||
</div> |
|
||||
</template> |
|
||||
|
|
||||
<script> |
|
||||
import path from 'path' |
|
||||
import {isExternal} from '@/utils/validate' |
|
||||
|
|
||||
export default { |
|
||||
name:'SidebarItem', |
|
||||
props:{ |
|
||||
item:{ |
|
||||
type:Object, |
|
||||
required:true |
|
||||
}, |
|
||||
isNest:{ |
|
||||
type:Boolean, |
|
||||
default:false |
|
||||
}, |
|
||||
basePath:{ |
|
||||
type:String, |
|
||||
default:'' |
|
||||
} |
|
||||
}, |
|
||||
data() { |
|
||||
this.onlyOneChild=null |
|
||||
return { |
|
||||
} |
|
||||
}, |
|
||||
} |
|
||||
</script> |
|
@ -0,0 +1,45 @@ |
|||||
|
<!-- |
||||
|
* @Author: Liu_li |
||||
|
* @Descripttion: 顶部菜单 |
||||
|
* @Date: 2021-09-26 11:03:50 |
||||
|
--> |
||||
|
<template> |
||||
|
<div> |
||||
|
<el-menu :default-active="activeMenu" mode="horizontal" @select="handleSelect"> |
||||
|
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)"> |
||||
|
</app-link> |
||||
|
</el-menu> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import path from 'path' |
||||
|
import { isExternal } from '@/utils/validate' |
||||
|
import Item from './Item' |
||||
|
|
||||
|
export default { |
||||
|
name: 'TopMenus', |
||||
|
comments: { Item }, |
||||
|
props: { |
||||
|
item: { |
||||
|
type: Object, |
||||
|
required: true |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
this.onlyOneChild = null |
||||
|
return { |
||||
|
activeIndex: '1', |
||||
|
activeIndex2: '1' |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
hasOneShowingChild(children = [], parent) { |
||||
|
const showingChildren = children.filter(item => { |
||||
|
this.onlyOneChild = item |
||||
|
return true |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,81 @@ |
|||||
|
<template> |
||||
|
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll"> |
||||
|
<slot /> |
||||
|
</el-scrollbar> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
const tagAndTagSpacing = 4 |
||||
|
|
||||
|
export default { |
||||
|
name: 'ScrollPane', |
||||
|
data() { |
||||
|
return { |
||||
|
left: 0 |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
scrollWrapper() { |
||||
|
return this.$refs.scrollContainer.$refs.wrap |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
handleScroll(e) { |
||||
|
const eventDelta = e.wheelDelta || -e.deltaY * 40 |
||||
|
const $scrollWrapper = this.scrollWrapper |
||||
|
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4 |
||||
|
}, |
||||
|
moveToTarget(currentTag) { |
||||
|
const $container = this.$refs.scrollContainer.$el |
||||
|
const $containerWidth = $container.offsetWidth |
||||
|
const $scrollWrapper = this.scrollWrapper |
||||
|
const tagList = this.$parent.$refs.tag |
||||
|
|
||||
|
let firstTag = null |
||||
|
let lastTag = null |
||||
|
|
||||
|
if (tagList.length > 0) { |
||||
|
firstTag = tagList[0] |
||||
|
lastTag = tagList[tagList.length - 1] |
||||
|
} |
||||
|
|
||||
|
if (firstTag === currentTag) { |
||||
|
$scrollWrapper.scrollLeft = 0 |
||||
|
} else if (lastTag === currentTag) { |
||||
|
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth |
||||
|
} else { |
||||
|
const currentIndex = tagList.findIndex(item => item === currentTag) |
||||
|
const prevTag = tagList[currentIndex - 1] |
||||
|
const nextTag = tagList[currentIndex + 1] |
||||
|
|
||||
|
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing |
||||
|
|
||||
|
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing |
||||
|
|
||||
|
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) { |
||||
|
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth |
||||
|
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) { |
||||
|
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.scroll-container { |
||||
|
white-space: nowrap; |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
width: 100%; |
||||
|
::v-deep { |
||||
|
.el-scrollbar__bar { |
||||
|
bottom: 0px; |
||||
|
} |
||||
|
.el-scrollbar__wrap { |
||||
|
height: 49px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,275 @@ |
|||||
|
<template> |
||||
|
<div id="tags-view-container" class="tags-view-container"> |
||||
|
<scroll-pane ref="scrollPane" class="tags-view-wrapper"> |
||||
|
<router-link v-for="tag in visitedViews" ref="tag" :key="tag.path" :class="isActive(tag)?'active':''" :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" tag="span" class="tags-view-item" @click.middle.native="closeSelectedTag(tag)" @contextmenu.prevent.native="openMenu(tag,$event)"> |
||||
|
{{ tag.title }} |
||||
|
<span v-if="!tag.meta.affix" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" /> |
||||
|
</router-link> |
||||
|
</scroll-pane> |
||||
|
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu"> |
||||
|
<li @click="refreshSelectedTag(selectedTag)">刷新</li> |
||||
|
<li v-if="!(selectedTag.meta&&selectedTag.meta.affix)" @click="closeSelectedTag(selectedTag)">关闭</li> |
||||
|
<li @click="closeOthersTags">关闭其他</li> |
||||
|
<li @click="closeAllTags(selectedTag)">关闭全部</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import ScrollPane from './ScrollPane' |
||||
|
import path from 'path' |
||||
|
|
||||
|
export default { |
||||
|
components: { ScrollPane }, |
||||
|
data() { |
||||
|
return { |
||||
|
visible: false, |
||||
|
top: 0, |
||||
|
left: 0, |
||||
|
selectedTag: {}, |
||||
|
affixTags: [] |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
visitedViews() { |
||||
|
return this.$store.state.tagsView.visitedViews |
||||
|
}, |
||||
|
routes() { |
||||
|
return this.$store.state.permission.routers |
||||
|
} |
||||
|
}, |
||||
|
watch: { |
||||
|
$route() { |
||||
|
this.addTags() |
||||
|
this.moveToCurrentTag() |
||||
|
}, |
||||
|
visible(value) { |
||||
|
if (value) { |
||||
|
document.body.addEventListener('click', this.closeMenu) |
||||
|
} else { |
||||
|
document.body.removeEventListener('click', this.closeMenu) |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.initTags() |
||||
|
this.addTags() |
||||
|
}, |
||||
|
methods: { |
||||
|
isActive(route) { |
||||
|
return route.path === this.$route.path |
||||
|
}, |
||||
|
filterAffixTags(routes, basePath = '/') { |
||||
|
let tags = [] |
||||
|
routes.forEach(route => { |
||||
|
if (route.meta && route.meta.affix) { |
||||
|
const tagPath = path.resolve(basePath, route.path) |
||||
|
tags.push({ |
||||
|
fullPath: tagPath, |
||||
|
path: tagPath, |
||||
|
name: route.name, |
||||
|
meta: { ...route.meta } |
||||
|
}) |
||||
|
} |
||||
|
if (route.children) { |
||||
|
const tempTags = this.filterAffixTags(route.children, route.path) |
||||
|
if (tempTags.length >= 1) { |
||||
|
tags = [...tags, ...tempTags] |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
return tags |
||||
|
}, |
||||
|
initTags() { |
||||
|
const affixTags = this.affixTags = this.filterAffixTags(this.routes) |
||||
|
for (const tag of affixTags) { |
||||
|
// Must have tag name |
||||
|
if (tag.name) { |
||||
|
this.$store.dispatch('tagsView/addVisitedView', tag) |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
addTags() { |
||||
|
const { name } = this.$route |
||||
|
if (name) { |
||||
|
this.$store.dispatch('tagsView/addView', this.$route) |
||||
|
} |
||||
|
return false |
||||
|
}, |
||||
|
moveToCurrentTag() { |
||||
|
const tags = this.$refs.tag |
||||
|
this.$nextTick(() => { |
||||
|
for (const tag of tags) { |
||||
|
if (tag.to.path === this.$route.path) { |
||||
|
this.$refs.scrollPane.moveToTarget(tag) |
||||
|
if (tag.to.fullPath !== this.$route.fullPath) { |
||||
|
this.$store.dispatch('tagsView/updateVisitedView', this.$route) |
||||
|
} |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
refreshSelectedTag(view) { |
||||
|
this.$store.dispatch('tagsView/delCachedView', view).then(() => { |
||||
|
const { fullPath } = view |
||||
|
this.$nextTick(() => { |
||||
|
this.$router.replace({ |
||||
|
path: '/redirect' + fullPath |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}, |
||||
|
closeSelectedTag(view) { |
||||
|
this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => { |
||||
|
if (this.isActive(view)) { |
||||
|
this.toLastView(visitedViews, view) |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
closeOthersTags() { |
||||
|
this.$router.push(this.selectedTag) |
||||
|
this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => { |
||||
|
this.moveToCurrentTag() |
||||
|
}) |
||||
|
}, |
||||
|
closeAllTags(view) { |
||||
|
this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => { |
||||
|
if (this.affixTags.some(tag => tag.path === view.path)) { |
||||
|
return |
||||
|
} |
||||
|
this.toLastView(visitedViews, view) |
||||
|
}) |
||||
|
}, |
||||
|
toLastView(visitedViews, view) { |
||||
|
const latestView = visitedViews.slice(-1)[0] |
||||
|
if (latestView) { |
||||
|
this.$router.push(latestView) |
||||
|
} else { |
||||
|
// now the default is to redirect to the home page if there is no tags-view, |
||||
|
// you can adjust it according to your needs. |
||||
|
if (view.name === 'Dashboard') { |
||||
|
// to reload home page |
||||
|
this.$router.replace({ path: '/redirect' + view.fullPath }) |
||||
|
} else { |
||||
|
this.$router.push('/') |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
openMenu(tag, e) { |
||||
|
const menuMinWidth = 105 |
||||
|
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left |
||||
|
const offsetWidth = this.$el.offsetWidth // container width |
||||
|
const maxLeft = offsetWidth - menuMinWidth // left boundary |
||||
|
const left = e.clientX - offsetLeft + 15 // 15: margin right |
||||
|
|
||||
|
if (left > maxLeft) { |
||||
|
this.left = maxLeft |
||||
|
} else { |
||||
|
this.left = left |
||||
|
} |
||||
|
|
||||
|
this.top = e.clientY |
||||
|
this.visible = true |
||||
|
this.selectedTag = tag |
||||
|
}, |
||||
|
closeMenu() { |
||||
|
this.visible = false |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.tags-view-container { |
||||
|
height: 34px; |
||||
|
width: 100%; |
||||
|
background: #fff; |
||||
|
border-bottom: 1px solid #d8dce5; |
||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04); |
||||
|
.tags-view-wrapper { |
||||
|
.tags-view-item { |
||||
|
display: inline-block; |
||||
|
position: relative; |
||||
|
cursor: pointer; |
||||
|
height: 26px; |
||||
|
line-height: 26px; |
||||
|
border: 1px solid #d8dce5; |
||||
|
color: #495060; |
||||
|
background: #fff; |
||||
|
padding: 0 8px; |
||||
|
font-size: 12px; |
||||
|
margin-left: 5px; |
||||
|
margin-top: 4px; |
||||
|
&:first-of-type { |
||||
|
margin-left: 15px; |
||||
|
} |
||||
|
&:last-of-type { |
||||
|
margin-right: 15px; |
||||
|
} |
||||
|
&.active { |
||||
|
background-color: #42b983; |
||||
|
color: #fff; |
||||
|
border-color: #42b983; |
||||
|
&::before { |
||||
|
content: ''; |
||||
|
background: #fff; |
||||
|
display: inline-block; |
||||
|
width: 8px; |
||||
|
height: 8px; |
||||
|
border-radius: 50%; |
||||
|
position: relative; |
||||
|
margin-right: 2px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.contextmenu { |
||||
|
margin: 0; |
||||
|
background: #fff; |
||||
|
z-index: 3000; |
||||
|
position: absolute; |
||||
|
list-style-type: none; |
||||
|
padding: 5px 0; |
||||
|
border-radius: 4px; |
||||
|
font-size: 12px; |
||||
|
font-weight: 400; |
||||
|
color: #333; |
||||
|
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3); |
||||
|
li { |
||||
|
margin: 0; |
||||
|
padding: 7px 16px; |
||||
|
cursor: pointer; |
||||
|
&:hover { |
||||
|
background: #eee; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<style lang="scss"> |
||||
|
//reset element css of el-icon-close |
||||
|
.tags-view-wrapper { |
||||
|
.tags-view-item { |
||||
|
.el-icon-close { |
||||
|
width: 16px; |
||||
|
height: 16px; |
||||
|
vertical-align: 2px; |
||||
|
border-radius: 50%; |
||||
|
text-align: center; |
||||
|
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); |
||||
|
transform-origin: 100% 50%; |
||||
|
&:before { |
||||
|
transform: scale(0.6); |
||||
|
display: inline-block; |
||||
|
vertical-align: -3px; |
||||
|
} |
||||
|
&:hover { |
||||
|
background-color: #b4bccc; |
||||
|
color: #fff; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,5 @@ |
|||||
|
export { default as AppMain } from './AppMain' |
||||
|
export { default as Navbar } from './Navbar' |
||||
|
export { default as Settings } from './Settings' |
||||
|
export { default as Sidebar } from './Sidebar/index.vue' |
||||
|
export { default as TagsView } from './TagsView/index.vue' |
@ -0,0 +1,166 @@ |
|||||
|
|
||||
|
/** |
||||
|
* 是否合法URL |
||||
|
* @param {string} path |
||||
|
* @returns {Boolean} |
||||
|
*/ |
||||
|
export function isExternal(path) { |
||||
|
return /^(https?:|mailto:|tel:)/.test(path) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {string} str |
||||
|
* @returns {Boolean} |
||||
|
*/ |
||||
|
export function validUsername(str) { |
||||
|
const valid_map = ['admin', 'editor'] |
||||
|
return valid_map.indexOf(str.trim()) >= 0 |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {string} url |
||||
|
* @returns {Boolean} |
||||
|
*/ |
||||
|
export function validURL(url) { |
||||
|
const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ |
||||
|
return reg.test(url) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {string} str |
||||
|
* @returns {Boolean} |
||||
|
*/ |
||||
|
export function validLowerCase(str) { |
||||
|
const reg = /^[a-z]+$/ |
||||
|
return reg.test(str) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {string} str |
||||
|
* @returns {Boolean} |
||||
|
*/ |
||||
|
export function validUpperCase(str) { |
||||
|
const reg = /^[A-Z]+$/ |
||||
|
return reg.test(str) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {string} str |
||||
|
* @returns {Boolean} |
||||
|
*/ |
||||
|
export function validAlphabets(str) { |
||||
|
const reg = /^[A-Za-z]+$/ |
||||
|
return reg.test(str) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {string} email |
||||
|
* @returns {Boolean} |
||||
|
*/ |
||||
|
export function validEmail(email) { |
||||
|
const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ |
||||
|
return reg.test(email) |
||||
|
} |
||||
|
|
||||
|
export function isvalidPhone(phone) { |
||||
|
const reg = /^1[3|4|5|7|8][0-9]\d{8}$/ |
||||
|
return reg.test(phone) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {string} str |
||||
|
* @returns {Boolean} |
||||
|
*/ |
||||
|
export function isString(str) { |
||||
|
if (typeof str === 'string' || str instanceof String) { |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {Array} arg |
||||
|
* @returns {Boolean} |
||||
|
*/ |
||||
|
export function isArray(arg) { |
||||
|
if (typeof Array.isArray === 'undefined') { |
||||
|
return Object.prototype.toString.call(arg) === '[object Array]' |
||||
|
} |
||||
|
return Array.isArray(arg) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 是否合法IP地址 |
||||
|
* @param rule |
||||
|
* @param value |
||||
|
* @param callback |
||||
|
*/ |
||||
|
export function validateIP(rule, value, callback) { |
||||
|
if (value === '' || value === undefined || value == null) { |
||||
|
callback() |
||||
|
} else { |
||||
|
const reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/ |
||||
|
if ((!reg.test(value)) && value !== '') { |
||||
|
callback(new Error('请输入正确的IP地址')) |
||||
|
} else { |
||||
|
callback() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 是否手机号码或者固话*/ |
||||
|
export function validatePhoneTwo(rule, value, callback) { |
||||
|
const reg = /^((0\d{2,3}-\d{7,8})|(1[34578]\d{9}))$/ |
||||
|
if (value === '' || value === undefined || value == null) { |
||||
|
callback() |
||||
|
} else { |
||||
|
if ((!reg.test(value)) && value !== '') { |
||||
|
callback(new Error('请输入正确的电话号码或者固话号码')) |
||||
|
} else { |
||||
|
callback() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 是否固话*/ |
||||
|
export function validateTelephone(rule, value, callback) { |
||||
|
const reg = /0\d{2}-\d{7,8}/ |
||||
|
if (value === '' || value === undefined || value == null) { |
||||
|
callback() |
||||
|
} else { |
||||
|
if ((!reg.test(value)) && value !== '') { |
||||
|
callback(new Error('请输入正确的固话(格式:区号+号码,如010-1234567)')) |
||||
|
} else { |
||||
|
callback() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 是否手机号码*/ |
||||
|
export function validatePhone(rule, value, callback) { |
||||
|
const reg = /^[1][3,4,5,7,8][0-9]{9}$/ |
||||
|
if (value === '' || value === undefined || value == null) { |
||||
|
callback() |
||||
|
} else { |
||||
|
if ((!reg.test(value)) && value !== '') { |
||||
|
callback(new Error('请输入正确的电话号码')) |
||||
|
} else { |
||||
|
callback() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 是否身份证号码*/ |
||||
|
export function validateIdNo(rule, value, callback) { |
||||
|
const reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/ |
||||
|
if (value === '' || value === undefined || value == null) { |
||||
|
callback() |
||||
|
} else { |
||||
|
if ((!reg.test(value)) && value !== '') { |
||||
|
callback(new Error('请输入正确的身份证号码')) |
||||
|
} else { |
||||
|
callback() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue