This commit is contained in:
kk
2026-07-08 18:00:33 +08:00
commit e66b4a3680
312 changed files with 54718 additions and 0 deletions

111
src/page/index/index.vue Normal file
View File

@@ -0,0 +1,111 @@
<template>
<el-watermark :content="watermark" style="height: 100%">
<div class="avue-contail" :class="{ 'avue--collapse': isCollapse }">
<div class="avue-layout" :class="{ 'avue-layout--horizontal': isHorizontal }">
<div class="avue-sidebar" v-show="validSidebar">
<!-- 左侧导航栏 -->
<logo />
<sidebar />
</div>
<div class="avue-main">
<!-- 顶部导航栏 -->
<top ref="top" />
<!-- 顶部标签卡 -->
<tags />
<search class="avue-view" v-show="isSearch"></search>
<!-- 主体视图层 -->
<div id="avue-view" v-show="!isSearch" v-if="isRefresh">
<router-view #="{ Component }">
<keep-alive :include="$store.getters.tagsKeep">
<component :is="tabView($route, Component)" />
</keep-alive>
</router-view>
</div>
</div>
</div>
<!-- <wechat></wechat> -->
</div>
</el-watermark>
</template>
<script>
import index from '@/mixins/index';
import wechat from './wechat.vue';
//import { validatenull } from 'utils/validate';
import { mapGetters } from 'vuex';
import tags from './tags.vue';
import search from './search.vue';
import logo from './logo.vue';
import top from './top/index.vue';
import sidebar from './sidebar/index.vue';
import website from '@/config/website';
import { validatenull } from '@/utils/validate';
import { getMainMenu } from '@/api/system/menu';
import { tabView } from '@/router/tab';
export default {
mixins: [index],
components: {
top,
logo,
tags,
search,
sidebar,
wechat,
},
name: 'index',
provide() {
return {
index: this,
};
},
computed: {
...mapGetters([
'isHorizontal',
'isRefresh',
'isLock',
'isCollapse',
'isSearch',
'menu',
'setting',
]),
validSidebar() {
return !(
(this.$route.meta || {}).menu === false || (this.$route.query || {}).menu === 'false'
);
},
watermark() {
return website.watermark.mode ? website.watermark.text : '';
},
},
props: [],
methods: {
tabView,
//打开菜单
openMenu(item = {}, skipMainMenu = false) {
const doOpen = menuItem => {
this.$store.dispatch('GetMenu', menuItem.id).then(data => {
if (data.length !== 0) {
this.$router.$avueRouter.formatRoutes(data, true);
if (!validatenull(menuItem.path)) {
this.$router.push({ path: menuItem.path });
}
}
});
};
if (!item.id && !skipMainMenu) {
getMainMenu()
.then(res => {
const mainMenu = res.data.data;
doOpen(mainMenu && mainMenu.id ? { id: mainMenu.id } : item);
})
.catch(() => {
doOpen(item);
});
} else {
doOpen(item);
}
},
},
};
</script>

View File

@@ -0,0 +1,7 @@
<template>
<router-view #="{ Component }">
<keep-alive :include="$store.getters.tagsKeep">
<component :is="Component" />
</keep-alive>
</router-view>
</template>

44
src/page/index/logo.vue Normal file
View File

@@ -0,0 +1,44 @@
<template>
<div class="avue-logo">
<transition name="fade">
<span v-if="getScreen(isCollapse)" class="avue-logo_subtitle" key="0">
<img class="logo-img" src="/img/logo.png" />
</span>
</transition>
<transition-group name="fade">
<template v-if="getScreen(!isCollapse)">
<span class="logo-title" key="1">{{ website.indexTitle }} </span>
</template>
</transition-group>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
name: 'logo',
data() {
return {};
},
created() {},
computed: {
...mapGetters(['isCollapse']),
},
methods: {},
};
</script>
<style scoped>
.logo-title {
font-size: 20px;
background-image: linear-gradient(120deg, #54b6d0 16%, #3f8bdb, #2c77f1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 700;
padding-left: 30px;
}
.logo-img {
width: 40px;
margin-top: 5px;
}
</style>

178
src/page/index/search.vue Normal file
View File

@@ -0,0 +1,178 @@
<template>
<div class="avue-searchs" @click.self="handleEsc">
<div class="avue-searchs__title">菜单搜索</div>
<div class="avue-searchs__content">
<div class="avue-searchs__form">
<el-input :placeholder="$t('search')" v-model="value" @keydown.esc="handleEsc">
<template #append>
<el-button icon="el-icon-search"></el-button>
</template>
</el-input>
<p>
<el-tag>你可以使用快捷键esc 关闭</el-tag>
</p>
</div>
<div class="avue-searchs__list">
<el-scrollbar class="avue-searchs__scrollbar">
<div
class="avue-searchs__item"
v-for="(item, index) in menus"
:key="index"
@click="handleSelect(item)"
>
<i :class="[item[iconKey], 'avue-searchs__item-icon']"></i>
<span class="avue-searchs__item-title">{{ item[labelKey] }}</span>
<div class="avue-searchs__item-path">
{{ item[pathKey] }}
</div>
</div>
</el-scrollbar>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
data() {
return {
value: '',
menus: [],
menuList: [],
};
},
created() {
this.getMenuList();
},
watch: {
value() {
this.querySearch();
},
menu() {
this.getMenuList();
},
},
computed: {
labelKey() {
return this.website.menu.label;
},
pathKey() {
return this.website.menu.path;
},
iconKey() {
return this.website.menu.icon;
},
childrenKey() {
return this.website.menu.children;
},
...mapGetters(['menu']),
},
methods: {
handleEsc() {
this.$store.commit('SET_IS_SEARCH', false);
},
getMenuList() {
const findMenu = list => {
for (let i = 0; i < list.length; i++) {
const ele = Object.assign({}, list[i]);
if (this.validatenull(ele[this.childrenKey])) {
this.menuList.push(ele);
} else {
findMenu(ele[this.childrenKey]);
}
}
};
this.menuList = [];
findMenu(this.menu);
this.menus = this.menuList;
},
querySearch() {
var restaurants = this.menuList;
var queryString = this.value;
this.menus = queryString ? this.menuList.filter(this.createFilter(queryString)) : restaurants;
},
createFilter(queryString) {
return restaurant => {
return restaurant[this.labelKey].toLowerCase().indexOf(queryString.toLowerCase()) === 0;
};
},
handleSelect(item) {
this.value = '';
this.$router.push({
path: item[this.pathKey],
query: item.query,
});
},
},
};
</script>
<style lang="scss" scoped>
.avue-searchs {
padding-top: 50px;
width: 100%;
height: 100%;
background-color: #fff;
z-index: 1024;
&__title {
margin-bottom: 60px;
text-align: center;
font-size: 42px;
font-weight: bold;
letter-spacing: 2px;
text-indent: 2px;
}
&__form {
margin: 0 auto 50px auto;
width: 50%;
text-align: center;
p {
margin-top: 20px;
}
}
&__scrollbar {
height: 400px;
}
&__list {
box-sizing: border-box;
padding: 20px 30px;
margin: 0 auto;
width: 70%;
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
overflow: hidden;
color: #303133;
transition: 0.3s;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
&__item {
padding: 5px 0;
border-bottom: 1px dashed #eee;
&-icon {
margin-right: 5px;
font-size: 18px;
}
&-title {
font-size: 20px;
font-weight: 500;
color: #333;
}
&-path {
line-height: 30px;
color: #666;
}
}
}
</style>

173
src/page/index/setting.vue Normal file
View File

@@ -0,0 +1,173 @@
<template>
<!-- <el-button
@click="show = true"
class="setting-icon"
type="primary"
icon="el-icon-setting"
circle
></el-button>-->
<el-drawer append-to-body :with-header="false" v-model="show" size="30%">
<div class="setting">
<h5>导航模式</h5>
<div class="setting-checkbox">
<el-tooltip class="item" effect="dark" content="侧边菜单布局" placement="top">
<div
@click="setting.sidebar = 'vertical'"
class="setting-checkbox-item setting-checkbox-item--side"
></div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="顶部菜单布局" placement="top">
<div
@click="setting.sidebar = 'horizontal'"
class="setting-checkbox-item setting-checkbox-item--top"
></div>
</el-tooltip>
</div>
<h5>页面布局</h5>
<div class="setting-checkbox">
<div class="setting-item" v-for="(item, index) in list1" :key="index">
{{ item.label }}:
<el-switch v-model="setting[item.value]"></el-switch>
</div>
</div>
<h5>功能调试</h5>
<div class="setting-checkbox">
<div class="setting-item" v-for="(item, index) in list2" :key="index">
{{ item.label }}:
<el-switch v-model="setting[item.value]"></el-switch>
</div>
</div>
</div>
</el-drawer>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
data() {
return {
show: false,
list1: [
{
label: '导航标签',
value: 'tag',
},
{
label: '菜单折叠',
value: 'collapse',
},
{
label: '菜单搜索',
value: 'search',
},
{
label: '屏幕全屏',
value: 'fullscreen',
},
{
label: '主题选择',
value: 'theme',
},
{
label: '顶部菜单',
value: 'menu',
},
],
list2: [
{
label: '日志调试',
value: 'debug',
},
{
label: '屏幕锁定',
value: 'lock',
},
],
};
},
computed: {
...mapGetters(['isHorizontal', 'setting']),
},
};
</script>
<style lang="scss">
.setting {
&-icon {
color: #666;
position: fixed;
bottom: 200px;
right: 20px;
z-index: 2048;
}
&-item {
display: flex;
justify-content: space-between;
font-size: 14px;
margin-bottom: 8px;
}
&-checkbox {
&--check {
position: absolute;
color: var(--el-color-primary);
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
&-item {
display: inline-block;
position: relative;
width: 44px;
height: 36px;
margin-right: 16px;
overflow: hidden;
background-color: #f0f2f5;
border-radius: 4px;
box-shadow: 0 1px 2.5px 0 rgba(0, 0, 0, 0.18);
cursor: pointer;
&:before {
position: absolute;
top: 0;
left: 0;
width: 33%;
height: 100%;
background-color: #fff;
content: '';
}
&:after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 25%;
background-color: #fff;
content: '';
}
&--side {
&:before {
z-index: 1;
background-color: #001529;
content: '';
}
&:after {
background-color: #fff;
}
}
&--top {
&:after {
background-color: #001529;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<el-scrollbar class="avue-menu">
<div v-if="menu && menu.length == 0 && !isHorizontal" class="avue-sidebar--tip">
{{ $t('menuTip') }}
</div>
<el-menu
unique-opened
:default-active="activeMenu"
:mode="setting.sidebar"
:collapse="getScreen(isCollapse)"
>
<sidebar-item :menu="menu"></sidebar-item>
</el-menu>
</el-scrollbar>
</template>
<script>
import { mapGetters } from 'vuex';
import website from '@/config/website';
import sidebarItem from './sidebarItem.vue';
export default {
name: 'sidebar',
components: { sidebarItem },
inject: ['index'],
created() {
this.index.openMenu();
},
computed: {
...mapGetters(['isHorizontal', 'setting', 'menu', 'tag', 'isCollapse', 'menuId']),
// 带参菜单的索引集合,用于激活态的精确匹配
menuIndexes() {
const props = website.menu;
const indexes = new Set();
const collect = list => {
(list || []).forEach(item => {
const query = item[props.query];
if (query && Object.keys(query).length) {
indexes.add(this.$router.resolve({ path: item[props.path], query }).fullPath);
}
collect(item[props.children]);
});
};
collect(this.menu);
return indexes;
},
activeMenu() {
const route = this.$route;
const { meta, path } = route;
if (meta.activeMenu) {
return meta.activeMenu;
}
// 完整地址命中带参菜单索引时精确高亮,其余场景按路径高亮
const fullPath = route.fullPath;
if (fullPath !== path && this.menuIndexes.has(fullPath)) {
return fullPath;
}
return path;
},
},
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,105 @@
<template>
<template v-for="item in menu">
<el-menu-item
v-if="validatenull(item[childrenKey]) && validRoles(item)"
:index="getIndex(item)"
@click="open(item)"
:key="item[labelKey]"
>
<i :class="item[iconKey]"></i>
<template #title>
<span :alt="item[pathKey]">{{ getTitle(item) }}</span>
</template>
</el-menu-item>
<el-sub-menu
v-else-if="!validatenull(item[childrenKey]) && validRoles(item)"
:index="getPath(item)"
:key="item[labelKey]"
>
<template #title>
<i :class="item[iconKey]"></i>
<span>{{ getTitle(item) }}</span>
</template>
<template v-for="(child, cindex) in item[childrenKey]" :key="child[labelKey]">
<el-menu-item
:index="getIndex(child)"
@click="open(child)"
v-if="validatenull(child[childrenKey])"
>
<i :class="child[iconKey]"></i>
<template #title>
<span>{{ getTitle(child) }}</span>
</template>
</el-menu-item>
<sidebar-item v-else :menu="[child]" :key="cindex"></sidebar-item>
</template>
</el-sub-menu>
</template>
</template>
<script>
import { mapGetters } from 'vuex';
import { validatenull } from 'utils/validate';
import website from '@/config/website';
import { generateIframePath, generateIframeQuery, isIframeRoute } from '@/router/router';
export default {
name: 'sidebarItem',
data() {
return {
props: website.menu,
};
},
props: {
menu: Array,
},
computed: {
...mapGetters(['roles']),
labelKey() {
return this.props.label;
},
pathKey() {
return this.props.path;
},
queryKey() {
return this.props.query;
},
iconKey() {
return this.props.icon;
},
childrenKey() {
return this.props.children;
},
},
methods: {
validatenull,
getPath(item) {
return generateIframePath(item, this.pathKey);
},
getIndex(item) {
// 带参菜单以完整地址为索引,保证同路径多菜单的激活态互不干扰
const { path, query } = this.menuRoute(item);
if (!query || !Object.keys(query).length) return path;
return this.$router.resolve({ path, query }).fullPath;
},
menuRoute(item) {
const path = this.getPath(item);
const originalPath = item[this.pathKey];
const isIframe = isIframeRoute(path, originalPath);
const query = isIframe
? generateIframeQuery(item, this.pathKey, this.queryKey)
: item[this.queryKey];
return { path, query };
},
getTitle(item) {
return this.$router.$avueRouter.generateTitle(item, this.props);
},
validRoles(item) {
item.meta = item.meta || {};
return item.meta.roles ? item.meta.roles.includes(this.roles) : true;
},
open(item) {
this.$router.push(this.menuRoute(item));
},
},
};
</script>

192
src/page/index/tags.vue Normal file
View File

@@ -0,0 +1,192 @@
<template>
<div class="avue-tags" v-if="setting.tag" @click="contextmenuFlag = false">
<!-- tag盒子 -->
<div
v-if="contextmenuFlag"
class="avue-tags__contentmenu"
:style="{ left: contentmenuX + 'px', top: contentmenuY + 'px' }"
>
<div class="item" @click="closeOthersTags">{{ $t('tagsView.closeOthers') }}</div>
<div class="item" @click="closeAllTags">{{ $t('tagsView.closeAll') }}</div>
<div class="item" @click="clearCacheTags">{{ $t('tagsView.clearCache') }}</div>
</div>
<div class="avue-tags__box">
<el-tabs
v-model="active"
type="card"
@contextmenu="handleContextmenu"
:closable="tagLen !== 1"
@tab-click="openTag"
@edit="menuTag"
>
<el-tab-pane
v-for="(item, index) in tagList"
:key="index"
:label="generateTitle(item)"
:name="item.fullPath"
>
<template #label>
<span>
{{ generateTitle(item) }}
<i
class="el-icon-refresh"
:class="{ turn: refresh }"
@click="handleRefresh"
v-if="active === item.fullPath"
></i>
</span>
</template>
</el-tab-pane>
</el-tabs>
<el-dropdown class="avue-tags__menu">
<el-button type="primary">
{{ $t('tagsView.menu') }}
<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openSearch"
><i class="icon-fangda" /> {{ $t('tagsView.search') }}</el-dropdown-item
>
<el-dropdown-item @click="closeOthersTags"
><i class="icon-fangkuai1" /> {{ $t('tagsView.closeOthers') }}
</el-dropdown-item>
<el-dropdown-item @click="closeAllTags"
><i class="icon-cuowukongxin" /> {{ $t('tagsView.closeAll') }}</el-dropdown-item
>
<el-dropdown-item @click="clearCacheTags"
><i class="icon-dingwei" /> {{ $t('tagsView.clearCache') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { clearCache } from '@/api/user';
export default {
name: 'tags',
data() {
return {
refresh: false,
active: '',
contentmenuX: '',
contentmenuY: '',
contextmenuFlag: false,
};
},
watch: {
tag: {
handler(val) {
this.active = val.fullPath;
},
immediate: true,
},
contextmenuFlag() {
window.addEventListener('mousedown', this.watchContextmenu);
},
},
computed: {
...mapGetters(['tagWel', 'tagList', 'tag', 'setting']),
tagLen() {
return this.tagList.length || 0;
},
},
methods: {
openSearch() {
this.$store.commit('SET_IS_SEARCH', true);
},
handleRefresh() {
this.refresh = true;
this.$store.commit('SET_IS_REFRESH', false);
setTimeout(() => {
this.$store.commit('SET_IS_REFRESH', true);
}, 100);
setTimeout(() => {
this.refresh = false;
}, 500);
},
generateTitle(item) {
return this.$router.$avueRouter.generateTitle({
...item,
...{
label: item.name,
},
});
},
watchContextmenu(event) {
if (!this.$el.contains(event.target) || event.button !== 0) {
this.contextmenuFlag = false;
}
window.removeEventListener('mousedown', this.watchContextmenu);
},
handleContextmenu(event) {
let target = event.target;
let flag = false;
if (target.className.indexOf('el-tabs__item') > -1) flag = true;
else if (target.parentNode.className.indexOf('el-tabs__item') > -1) {
target = target.parentNode;
flag = true;
}
if (flag) {
event.preventDefault();
event.stopPropagation();
this.contentmenuX = event.clientX;
this.contentmenuY = event.clientY;
this.tagName = target.getAttribute('aria-controls').slice(5);
this.contextmenuFlag = true;
}
},
menuTag(value, action) {
if (action === 'remove') {
let { tag, key } = this.findTag(value);
this.$store.commit('DEL_TAG', tag);
if (tag.fullPath === this.tag.fullPath) {
tag = this.tagList[key - 1];
this.$router.push({
path: tag.path,
query: tag.query,
});
}
}
},
openTag(item) {
let value = item.props.name;
let { tag } = this.findTag(value);
this.$router.push({
path: tag.path,
query: tag.query,
});
},
findTag(fullPath) {
let tag = this.tagList.find(item => item.fullPath === fullPath);
let key = this.tagList.findIndex(item => item.fullPath === fullPath);
return { tag, key };
},
closeOthersTags() {
this.contextmenuFlag = false;
this.$store.commit('DEL_TAG_OTHER');
},
closeAllTags() {
this.contextmenuFlag = false;
this.$store.commit('DEL_ALL_TAG');
this.$router.push(this.tagWel);
},
clearCacheTags() {
this.$confirm('是否需要清除缓存?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
clearCache().then(() => {
this.contextmenuFlag = false;
this.$message.success('清除完毕');
});
});
},
},
};
</script>

View File

@@ -0,0 +1,364 @@
<template>
<div class="avue-top">
<div class="top-bar__left">
<div
class="avue-breadcrumb"
:class="[{ 'avue-breadcrumb--active': isCollapse }]"
v-if="setting.collapse && !isHorizontal"
>
<i class="icon-navicon" @click="setCollapse"></i>
</div>
</div>
<div class="top-bar__title">
<top-menu ref="topMenu" v-if="setting.menu"></top-menu>
<top-search class="top-bar__item" v-if="setting.search"></top-search>
</div>
<div class="top-bar__right">
<div v-if="setting.color" class="top-bar__item">
<top-color></top-color>
</div>
<div v-if="setting.theme" class="top-bar__item">
<top-theme></top-theme>
</div>
<div v-if="setting.lock" class="top-bar__item">
<top-lock></top-lock>
</div>
<div class="top-bar__item">
<top-lang></top-lang>
</div>
<div class="top-bar__item" v-if="setting.fullscreen">
<top-full></top-full>
</div>
<div class="top-bar__item" v-if="setting.debug">
<top-logs></top-logs>
</div>
<div class="top-user">
<img class="top-bar__img" :src="userInfo.avatar" />
<el-dropdown :hide-on-click="false">
<span class="el-dropdown-link">
{{ userInfo.real_name }}
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="toDashboard">
<i class="icon-zuzhixiaxia" /> {{ $t('navbar.dashboard') }}
</el-dropdown-item>
<el-dropdown-item @click="toAllMenu">
<i class="icon-liebiao" /> {{ $t('navbar.allMenu') }}
</el-dropdown-item>
<el-dropdown-item @click="toInfo">
<i class="icon-yonghu" />{{ $t('navbar.userinfo') }}
</el-dropdown-item>
<el-dropdown-item divided>
<i class="icon-hezuohuobanmiyueguanli" />
<el-tooltip :content="roleName" placement="top" trigger="click">
<strong>
{{
func.contains(roleId, ',')
? $t('navbar.roleName')
: $t('navbar.currentRoleName')
}}: {{ func.truncateString(roleName) }}
</strong>
</el-tooltip>
</el-dropdown-item>
<el-dropdown-item
v-if="Object.keys(roleObj).length < 5"
v-for="(name, id) in roleObj"
:key="id"
@click="toRole(id, name)"
>
<i class="icon-changjingguanli" />
{{ $t('navbar.switchRoleTo') }}: {{ name }}
</el-dropdown-item>
<el-dropdown-item v-if="Object.keys(roleObj).length >= 5" @click="switchRole">
<i class="icon-changjingguanli" />
{{ $t('navbar.switchRole') }}
</el-dropdown-item>
<el-dropdown-item divided>
<i class="iconfont iconicon_group" />
<el-tooltip :content="deptName" placement="top" trigger="click">
<strong>
{{
func.contains(deptId, ',')
? $t('navbar.deptName')
: $t('navbar.currentDeptName')
}}: {{ func.truncateString(deptName) }}
</strong>
</el-tooltip>
</el-dropdown-item>
<el-dropdown-item
v-if="Object.keys(deptObj).length < 5"
v-for="(name, id) in deptObj"
:key="id"
@click="toDept(id, name)"
>
<i class="icon-guanlianshebei" />
{{ $t('navbar.switchDeptTo') }}: {{ name }}
</el-dropdown-item>
<el-dropdown-item v-if="Object.keys(deptObj).length >= 5" @click="switchDept">
<i class="icon-guanlianshebei" />
{{ $t('navbar.switchDept') }}
</el-dropdown-item>
<el-dropdown-item @click="logout" divided>
<i class="icon-tuichu" /> {{ $t('navbar.logOut') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<top-setting></top-setting>
<el-dialog title="请选择身份信息后切换" append-to-body v-model="userBox" width="400px">
<avue-form :option="userOption" v-model="userForm" @submit="toAll" />
</el-dialog>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import topLock from './top-lock.vue';
import topMenu from './top-menu.vue';
import topSearch from './top-search.vue';
import topTheme from './top-theme.vue';
import topLogs from './top-logs.vue';
import topColor from './top-color.vue';
import topLang from './top-lang.vue';
import topFull from './top-full.vue';
import topSetting from '../setting.vue';
import { getUserInfo as getUerDetailInfo } from '@/api/system/user';
import { getUserInfo as getUerOauthInfo } from '@/api/user';
import { validatenull } from '@/utils/validate';
import func from '@/utils/func';
export default {
components: {
topLock,
topMenu,
topSearch,
topTheme,
topLogs,
topColor,
topLang,
topFull,
topSetting,
},
name: 'top',
inject: ['index'],
data() {
return {
userId: '',
roleId: '',
roleName: '',
roleObj: {},
deptId: '',
deptName: '',
deptObj: {},
userBox: false,
userForm: {
deptId: '',
roleId: '',
},
userOption: {
labelWidth: 70,
submitBtn: true,
emptyBtn: false,
submitText: '切 换',
column: [
{
label: '部门',
prop: 'deptId',
type: 'select',
props: {
label: 'deptName',
value: 'id',
},
dicUrl: '/blade-system/dept/select',
filterable: true,
span: 24,
display: false,
rules: [
{
required: true,
message: '请选择部门',
trigger: 'blur',
},
],
},
{
label: '角色',
prop: 'roleId',
type: 'select',
props: {
label: 'roleName',
value: 'id',
},
dicUrl: '/blade-system/role/select',
filterable: true,
span: 24,
display: false,
rules: [
{
required: true,
message: '请选择角色',
trigger: 'blur',
},
],
},
],
},
};
},
filters: {},
created() {
this.init();
},
computed: {
func() {
return func;
},
...mapGetters([
'setting',
'userInfo',
'tagWel',
'tagList',
'isCollapse',
'tag',
'logsLen',
'logsFlag',
'isHorizontal',
]),
},
methods: {
init() {
getUerDetailInfo().then(res => {
// 获取绑定信息
const roleId = res.data.data.roleId;
const roleName = res.data.data.roleName;
const roleIds = roleId.split(',');
const roleNames = roleName.split(',');
const deptId = res.data.data.deptId;
const deptName = res.data.data.deptName;
const deptIds = deptId.split(',');
const deptNames = deptName.split(',');
roleIds.forEach((id, index) => {
if (id === this.roleId) {
this.roleName = roleNames[index];
} else {
this.roleObj[id] = roleNames[index];
}
});
deptIds.forEach((id, index) => {
if (id === this.deptId) {
this.deptName = deptNames[index];
} else {
this.deptObj[id] = deptNames[index];
}
});
if (validatenull(this.roleName)) {
this.roleName = roleName;
}
if (validatenull(this.deptName)) {
this.deptName = deptName;
}
});
getUerOauthInfo().then(res => {
this.userId = res.data.userId;
this.roleId = res.data.roleId;
this.deptId = res.data.deptId;
});
},
toDashboard() {
this.$router.push(this.tagWel);
},
toAllMenu() {
this.index.openMenu({}, true);
this.$router.push(this.tagWel);
},
toInfo() {
this.$router.push({ path: '/info/index' });
},
toRole(roleId, name) {
const userInfo = { roleId, deptId: this.deptId };
this.switchTo(userInfo, name);
},
toDept(deptId, name) {
const userInfo = { roleId: this.roleId, deptId };
this.switchTo(userInfo, name);
},
toAll(form, done) {
let userInfo;
if (!validatenull(form.deptId)) {
userInfo = { roleId: this.roleId, deptId: form.deptId };
}
if (!validatenull(form.roleId)) {
userInfo = { roleId: form.roleId, deptId: this.deptId };
}
this.switchTo(userInfo);
done();
},
switchRole() {
const columnRole = this.findColumn(this.userOption.column, 'roleId');
const columnDept = this.findColumn(this.userOption.column, 'deptId');
columnRole.dicUrl = `/blade-system/role/select?userId=${this.userId}`;
columnRole.display = true;
columnDept.dicUrl = '';
columnDept.display = false;
this.userBox = true;
},
switchDept() {
const columnDept = this.findColumn(this.userOption.column, 'deptId');
const columnRole = this.findColumn(this.userOption.column, 'roleId');
columnDept.dicUrl = `/blade-system/dept/select?userId=${this.userId}`;
columnDept.display = true;
columnRole.dicUrl = '';
columnRole.display = false;
this.userBox = true;
},
switchTo(userInfo, name) {
const tipMessage = validatenull(name)
? this.$t('navbar.switchTip') + ' ? '
: this.$t('navbar.switchTip') + ` [${name}] ?`;
this.$confirm(tipMessage, this.$t('confirmTip'), {
confirmButtonText: this.$t('submitText'),
cancelButtonText: this.$t('cancelText'),
type: 'warning',
}).then(() => {
this.$store
.dispatch('RefreshToken', userInfo)
.then(() => {
this.$message.success(this.$t('navbar.switchSuccess'));
location.reload();
})
.catch(err => {
console.log(err);
});
});
},
setCollapse() {
this.$store.commit('SET_COLLAPSE');
},
logout() {
this.$confirm(this.$t('logoutTip'), this.$t('confirmTip'), {
confirmButtonText: this.$t('submitText'),
cancelButtonText: this.$t('cancelText'),
type: 'warning',
}).then(() => {
this.$store.dispatch('LogOut').then(() => {
if (this.website.oauth2.ssoMode) {
window.location.href =
this.website.oauth2.ssoBaseUrl +
this.website.oauth2.ssoLogoutUrl +
this.website.oauth2.redirectUri;
} else {
this.$router.push({ path: '/login' });
}
});
});
},
},
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,75 @@
<template>
<el-color-picker
size="small"
class="theme-picker"
popper-class="theme-picker-dropdown"
v-model="themeVal"
></el-color-picker>
</template>
<script>
import { mapGetters } from 'vuex'; // default color
export default {
name: 'topColor',
data() {
return {
themeVal: '',
};
},
created() {
this.themeVal = this.colorName || '#2C77F1';
},
watch: {
themeVal(val, oldVal) {
this.$store.commit('SET_COLOR_NAME', val);
this.updateTheme(val, oldVal);
},
},
computed: {
...mapGetters(['colorName']),
},
methods: {
hexToRgb(str) {
let hexs = '';
str = str.replace('#', '');
hexs = str.match(/../g);
for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16);
return hexs;
},
// r 代表红色 | g 代表绿色 | b 代表蓝色
rgbToHex(r, g, b) {
let hexs = [r.toString(16), g.toString(16), b.toString(16)];
for (let i = 0; i < 3; i++) if (hexs[i].length == 1) hexs[i] = `0${hexs[i]}`;
return `#${hexs.join('')}`;
},
getDarkColor(color, level) {
let rgb = this.hexToRgb(color);
for (let i = 0; i < 3; i++) rgb[i] = Math.floor(rgb[i] * (1 - level));
return this.rgbToHex(rgb[0], rgb[1], rgb[2]);
},
// color 颜色值字符串 | level 加深的程度限0-1之间
getLightColor(color, level) {
let rgb = this.hexToRgb(color);
for (let i = 0; i < 3; i++) rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i]);
return this.rgbToHex(rgb[0], rgb[1], rgb[2]);
},
updateTheme(e) {
if (!e) return;
// e就是选择了的颜色
const pre = '--el-color-primary';
const el = document.documentElement;
el.style.setProperty(pre, e);
// 这里是覆盖原有颜色的核心代码
for (let i = 1; i < 10; i += 1) {
document.documentElement.style.setProperty(
`${pre}-light-${i}`,
`${this.getLightColor(e, i / 10)}`
);
}
},
},
};
</script>

View File

@@ -0,0 +1,24 @@
<template>
<i :class="isFullScren ? 'icon-tuichuquanping' : 'icon-quanping'" @click="handleScreen"></i>
</template>
<script>
import { mapGetters } from 'vuex';
import { fullscreenToggel, listenfullscreen } from 'utils/util';
export default {
computed: {
...mapGetters(['isFullScren']),
},
mounted() {
listenfullscreen(this.setScreen);
},
methods: {
setScreen() {
this.$store.commit('SET_FULLSCREN');
},
handleScreen() {
fullscreenToggel();
},
},
};
</script>

View File

@@ -0,0 +1,42 @@
<template>
<el-dropdown trigger="click" @command="handleSetLanguage">
<i class="icon-zhongyingwen"></i>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :disabled="language === 'zh-cn'" command="zh-cn">中文</el-dropdown-item>
<el-dropdown-item :disabled="language === 'en-us'" command="en-us"
>English</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
name: 'top-lang',
data() {
return {};
},
created() {},
mounted() {},
computed: {
...mapGetters(['language', 'tag']),
},
props: [],
methods: {
handleSetLanguage(lang) {
this.$i18n.locale = lang;
this.$store.commit('SET_LANGUAGE', lang);
let tag = this.tag;
let title = this.$router.$avueRouter.generateTitle(tag);
//根据当前的标签也获取label的值动态设置浏览器标题
this.$router.$avueRouter.setTitle(title);
},
},
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,67 @@
<template>
<span v-if="text" @click="handleLock">{{ text }}</span>
<i v-else class="icon-suoping" @click="handleLock"></i>
<el-dialog title="设置锁屏密码" v-model="box" width="30%" append-to-body>
<el-form :model="form" ref="form" label-width="80px">
<el-form-item
label="锁屏密码"
prop="passwd"
:rules="[{ required: true, message: '锁屏密码不能为空' }]"
>
<el-input v-model="form.passwd" placeholder="请输入锁屏密码">
<template #append>
<el-button @click="handleSetLock" icon="el-icon-lock"></el-button>
</template>
</el-input>
</el-form-item>
</el-form>
</el-dialog>
</template>
<script>
import { validatenull } from 'utils/validate';
import { mapGetters } from 'vuex';
export default {
name: 'top-lock',
data() {
return {
box: false,
form: {
passwd: '',
},
};
},
created() {},
mounted() {},
computed: {
...mapGetters(['lockPasswd']),
},
props: {
text: String,
},
methods: {
handleSetLock() {
this.$refs['form'].validate(valid => {
if (valid) {
this.$store.commit('SET_LOCK_PASSWD', this.form.passwd);
this.handleLock();
}
});
},
handleLock() {
if (validatenull(this.lockPasswd)) {
this.box = true;
return;
}
this.$store.commit('SET_LOCK');
setTimeout(() => {
this.$router.push({ path: '/lock' });
}, 100);
},
},
components: {},
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,86 @@
<template>
<span @click="logsFlag ? '' : handleOpen()">
<el-badge :value="logsFlag ? '' : logsLen" :max="99">
<i class="icon-rizhi1"></i>
</el-badge>
<el-dialog title="日志" v-model="box" width="60%" append-to-body>
<el-button type="primary" icon="el-icon-upload" @click="send">上传服务器</el-button>
<el-button type="danger" icon="el-icon-delete" @click="clear">清空本地日志</el-button>
<el-table :data="logsList">
<el-table-column prop="type" label="类型" width="50px"> </el-table-column>
<el-table-column prop="url" label="地址" show-overflow-tooltip width="180">
</el-table-column>
<el-table-column prop="message" show-overflow-tooltip label="内容"> </el-table-column>
<el-table-column prop="stack" show-overflow-tooltip label="错误堆栈"> </el-table-column>
<el-table-column prop="time" label="时间"> </el-table-column>
</el-table>
</el-dialog>
</span>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
name: 'top-logs',
data() {
return {
box: false,
};
},
created() {},
mounted() {},
computed: {
...mapGetters(['logsList', 'logsFlag', 'logsLen']),
},
props: [],
methods: {
handleOpen() {
this.box = true;
},
send() {
this.$confirm('确定上传本地日志到服务器?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.$store.dispatch('SendLogs').then(() => {
this.box = false;
this.$message({
type: 'success',
message: '发送成功!',
});
});
})
.catch(() => {});
},
clear() {
this.$confirm('确定清空本地日志记录?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.$store.commit('CLEAR_LOGS');
this.box = false;
this.$message({
type: 'success',
message: '清空成功!',
});
})
.catch(() => {});
},
},
};
</script>
<style lang="scss" scoped>
.code {
font-size: 12px;
display: block;
font-family: monospace;
white-space: pre;
margin: 1em 0px;
}
</style>

View File

@@ -0,0 +1,58 @@
<template>
<el-menu class="top-menu" :default-active="activeIndex" mode="horizontal" text-color="#333">
<el-menu-item index="0" @click="openHome">
<template #title>
<i :class="itemHome.source" style="padding-right: 5px"></i>
<span>{{ itemHome.name }}</span>
</template>
</el-menu-item>
<template v-for="(item, index) in items" :key="index">
<el-menu-item :index="item.id + ''" @click="openMenu(item)">
<template #title>
<i :class="item.source" style="padding-right: 5px"></i>
<span>{{ item.name }}</span>
</template>
</el-menu-item>
</template>
</el-menu>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
name: 'top-menu',
data() {
return {
itemHome: {
name: '首页',
source: 'iconfont iconicon_work',
},
activeIndex: '0',
items: [],
};
},
inject: ['index'],
created() {
this.getMenu();
},
computed: {
...mapGetters(['tagCurrent', 'menu', 'tagWel']),
},
methods: {
openMenu(item) {
this.index.openMenu(item);
},
openHome() {
this.index.openMenu();
this.$router.push(this.tagWel);
},
getMenu() {
this.$store.dispatch('GetTopMenu').then(res => {
this.items = res.filter(item => item.isMain !== 1);
});
},
},
};
</script>

View File

@@ -0,0 +1,120 @@
<template>
<el-autocomplete
class="top-search"
popper-class="my-autocomplete"
v-model="value"
:fetch-suggestions="querySearch"
:placeholder="$t('search')"
@select="handleSelect"
>
<template #="{ item }">
<i :class="[item[iconKey], 'icon']"></i>
<div class="name">{{ item[labelKey] }}</div>
<div class="addr">{{ item[pathKey] }}</div>
</template>
</el-autocomplete>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
data() {
return {
value: '',
menuList: [],
};
},
created() {
this.getMenuList();
},
watch: {
menu() {
this.getMenuList();
},
},
computed: {
labelKey() {
return this.website.menu.label;
},
pathKey() {
return this.website.menu.path;
},
iconKey() {
return this.website.menu.icon;
},
childrenKey() {
return this.website.menu.children;
},
...mapGetters(['menu']),
},
methods: {
getMenuList() {
const findMenu = list => {
for (let i = 0; i < list.length; i++) {
const ele = Object.assign({}, list[i]);
if (this.validatenull(ele[this.childrenKey])) {
this.menuList.push(ele);
} else {
findMenu(ele[this.childrenKey]);
}
}
};
this.menuList = [];
findMenu(this.menu);
},
querySearch(queryString, cb) {
var restaurants = this.menuList;
var results = queryString ? restaurants.filter(this.createFilter(queryString)) : restaurants;
// 调用 callback 返回建议列表的数据
cb(results);
},
createFilter(queryString) {
return restaurant => {
return restaurant[this.labelKey].toLowerCase().indexOf(queryString.toLowerCase()) === 0;
};
},
handleSelect(item) {
this.value = '';
this.$router.push({
path: item[this.pathKey],
query: item.query,
});
},
},
};
</script>
<style lang="scss">
.my-autocomplete {
li {
line-height: normal !important;
padding: 7px !important;
.icon {
margin-right: 5px;
display: inline-block;
vertical-align: middle;
}
.name {
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
vertical-align: middle;
}
.addr {
padding-top: 5px;
width: 100%;
font-size: 12px;
color: #b4b4b4;
}
.highlighted .addr {
color: #ddd;
}
}
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<div>
<el-dialog title="选择" v-model="box" width="50%">
<el-radio-group v-model="text" class="list">
<el-row :span="24">
<el-col v-for="(item, index) in list" :key="index" :md="4" :xs="12" :sm="4">
<el-radio :label="item.value">{{ item.name }}</el-radio>
</el-col>
</el-row>
</el-radio-group>
</el-dialog>
<span>
<i class="icon-zhuti" @click="open"></i>
</span>
</div>
</template>
<script>
import { setTheme } from 'utils/util';
import { mapGetters } from 'vuex';
export default {
data() {
return {
box: false,
text: '',
list: [
{
name: '默认主题',
value: 'default',
},
{
name: '白色主题',
value: 'theme-white',
},
{
name: '黑色主题',
value: 'theme-dark',
},
{
name: 'go主题',
value: 'theme-go',
},
{
name: 'hey主题',
value: 'theme-hey',
},
{
name: '炫彩主题',
value: 'theme-star',
},
{
name: 'vip主题',
value: 'theme-vip',
},
{
name: '智能工厂主题',
value: 'theme-bule',
},
{
name: 'iview主题',
value: 'theme-iview',
},
{
name: 'cool主题',
value: 'theme-cool',
},
{
name: 'd2主题',
value: 'theme-d2',
},
{
name: 'lte主题',
value: 'theme-lte',
},
{
name: 'beautiful主题',
value: 'theme-beautiful',
},
{
name: 'Mac OS主题',
value: 'mac-os',
},
],
};
},
watch: {
text: function (val) {
this.$store.commit('SET_THEME_NAME', val);
setTheme(val);
if (this.$store.getters.isMacOs) {
this.$router.push(this.tagWel);
setTimeout(() => location.reload());
}
},
},
computed: {
...mapGetters(['themeName', 'tagWel']),
},
mounted() {
this.text = this.themeName;
if (!this.text) {
this.text = '';
}
},
methods: {
open() {
this.box = true;
},
},
};
</script>
<style lang="scss" scoped>
.list {
width: 100%;
}
</style>

59
src/page/index/wechat.vue Normal file
View File

@@ -0,0 +1,59 @@
<template>
<el-dialog
center
:show-close="false"
:close-on-press-escape="false"
:close-on-click-modal="false"
append-to-body
v-model="dialogVisible"
title="人机识别"
width="400px"
>
<center>
<span>
扫码下方二维码回复<b>验证码</b><br />
<span style="color: red">获得验证码 + 交流群(一起摸🐟)</span>
</span>
<br />
<br />
<img width="200" src="https://avuejs.com/images/icon/wechat.png" />
<br />
<br />
<el-input v-model="value" placeholder="请输入验证码"></el-input>
</center>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="submit"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script>
export default {
data() {
return {
value: '',
dialogVisible: false,
};
},
created() {
if (window.localStorage.getItem('avue_lock')) {
return;
}
this.dialogVisible = true;
},
methods: {
submit() {
if (this.value == '') {
this.$message.error('验证码不能为空');
return;
} else if (this.value != 'avue') {
this.$message.error('验证码不正确');
return;
}
this.dialogVisible = false;
window.localStorage.setItem('avue_lock', true);
},
},
};
</script>

108
src/page/lock/index.vue Normal file
View File

@@ -0,0 +1,108 @@
<template>
<div class="login-container" ref="login" @keyup.enter="handleLogin">
<div class="login-time">
{{ time }}
</div>
<div class="login-weaper">
<div class="login-left animate__animated animate__fadeInLeft">
<img class="img" src="/img/logo.png" alt="" />
<p class="title">{{ $t('login.info') }}</p>
</div>
<div class="login-border animate__animated animate__fadeInRight">
<div class="login-main">
<div class="lock-form animate__animated animate__bounceInDown">
<div
class="animate__animated"
:class="{ shake: passwdError, animate__bounceOut: pass }"
>
<h3 style="color: #333">{{ userInfo.username }}</h3>
<el-input
placeholder="请输入登录密码"
type="password"
class="input-with-select animated"
v-model="passwd"
@keyup.enter="handleLogin"
>
<template #append>
<i class="icon-bofangqi-suoping" @click="handleLogin"></i>
&nbsp; &nbsp;
<i class="icon-tuichu" @click="handleLogout"></i>
</template>
</el-input>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { removeRefreshToken, removeToken } from '@/utils/auth';
export default {
name: 'lock',
data() {
return {
time: '',
passwd: '',
passwdError: false,
pass: false,
};
},
created() {
this.getTime();
setInterval(() => {
this.getTime();
}, 1000);
},
mounted() {},
computed: {
...mapGetters(['userInfo', 'tag', 'lockPasswd']),
},
props: [],
methods: {
getTime() {
this.time = this.$dayjs().format('YYYY年MM月DD日 HH:mm:ss');
},
handleLogout() {
this.$confirm('是否退出系统, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.$store.dispatch('LogOut').then(() => {
// 清除token信息
removeToken();
removeRefreshToken();
this.$router.push({ path: '/login' });
});
});
},
handleLogin() {
if (this.passwd !== this.lockPasswd) {
this.passwd = '';
this.$message({
message: '解锁密码错误,请重新输入',
type: 'error',
});
this.passwdError = true;
setTimeout(() => {
this.passwdError = false;
}, 1000);
return;
}
this.pass = true;
setTimeout(() => {
this.$store.commit('CLEAR_LOCK');
this.$router.push({
path: this.tag.path,
});
}, 1000);
},
},
components: {},
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,18 @@
<template>
<div></div>
</template>
<script>
export default {
name: 'authredirect',
created() {
window.close();
const params = this.$route.query;
const state = params.state;
const code = params.code;
window.opener.location.href = `${window.location.origin}/#/login?state=${state}&code=${code}`;
},
};
</script>
<style></style>

View File

@@ -0,0 +1,193 @@
<template>
<el-form
class="login-form"
status-icon
:rules="loginRules"
ref="loginForm"
:model="loginForm"
label-width="0"
>
<el-form-item v-if="tenantMode" prop="tenantId">
<el-input
@keyup.enter="handleLogin"
v-model="loginForm.tenantId"
auto-complete="off"
:placeholder="$t('login.tenantId')"
>
<template #prefix>
<i class="icon-quanxian" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="phone">
<el-input
@keyup.enter="handleLogin"
v-model="loginForm.phone"
auto-complete="off"
:placeholder="$t('login.phone')"
>
<template #prefix>
<i class="icon-shouji" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-input
@keyup.enter="handleLogin"
v-model="loginForm.codeValue"
auto-complete="off"
:placeholder="$t('login.code')"
>
<template #prefix>
<i class="icon-yanzhengma"></i>
</template>
<template #append>
<span @click="handleSend" class="msg-text" :class="[{ display: msgKey }]">{{
msgText
}}</span>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click.prevent="handleLogin" class="login-submit">{{
$t('login.submit')
}}</el-button>
</el-form-item>
</el-form>
</template>
<script>
import { isvalidatemobile } from '@/utils/validate';
import { mapGetters } from 'vuex';
import { sendSms } from '@/api/user';
import { getTopUrl } from '@/utils/util';
import { info } from '@/api/system/tenant';
import { encrypt } from '@/utils/sm2';
export default {
name: 'codelogin',
data() {
const validatePhone = (rule, value, callback) => {
if (isvalidatemobile(value)[0]) {
callback(new Error(isvalidatemobile(value)[1]));
} else {
callback();
}
};
return {
tenantMode: this.website.tenantMode,
msgText: '',
msgTime: '',
msgKey: false,
loginForm: {
tenantId: '000000',
phone: '',
codeValue: '',
codeId: '',
},
loginRules: {
phone: [{ required: true, trigger: 'blur', validator: validatePhone }],
codeValue: [{ required: true, trigger: 'blur' }],
},
};
},
created() {
this.getTenant();
this.getMsg();
},
mounted() {},
computed: {
...mapGetters(['tagWel']),
config() {
return {
MSGINIT: this.$t('login.msgText'),
MSGSCUCCESS: this.$t('login.msgSuccess'),
MSGTIME: 60,
};
},
},
props: [],
methods: {
handleSend() {
this.$refs.loginForm.validate(valid => {
if (valid) {
if (this.msgKey) return;
this.msgText = this.msgTime + this.config.MSGSCUCCESS;
this.msgKey = true;
const time = setInterval(() => {
this.msgTime--;
this.msgText = this.msgTime + this.config.MSGSCUCCESS;
if (this.msgTime === 0) {
this.msgTime = this.config.MSGTIME;
this.msgText = this.config.MSGINIT;
this.msgKey = false;
clearInterval(time);
}
}, 1000);
sendSms(this.loginForm.tenantId, encrypt(this.loginForm.phone)).then(res => {
const data = res.data;
if (data.success) {
this.loginForm.codeId = data.data.id;
this.$message.success(data.msg);
} else {
this.$message.error(data.msg);
}
});
}
});
},
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
const loading = this.$loading({
lock: true,
text: '登录中,请稍后',
background: 'rgba(0, 0, 0, 0.7)',
});
this.$store
.dispatch('LoginByPhone', this.loginForm)
.then(() => {
loading.close();
this.$router.push(this.tagWel);
})
.catch(err => {
console.log(err);
loading.close();
});
}
});
},
getMsg() {
this.msgText = this.config.MSGINIT;
this.msgTime = this.config.MSGTIME;
},
getTenant() {
let domain = getTopUrl();
// 临时指定域名,方便测试
//domain = "https://bladex.cn";
info(domain).then(res => {
const data = res.data;
if (data.success && data.data.tenantId) {
this.tenantMode = false;
this.loginForm.tenantId = data.data.tenantId;
this.$parent.$refs.login.style.backgroundImage = `url(${data.data.backgroundUrl})`;
}
});
},
},
};
</script>
<style>
.msg-text {
display: block;
width: 60px;
font-size: 12px;
text-align: center;
cursor: pointer;
}
.msg-text.display {
color: #ccc;
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<basic-video ref="video" :width="350"></basic-video>
</template>
<script>
import { mapGetters } from 'vuex';
import basicVideo from '@/components/basic-video/main.vue';
export default {
components: {
basicVideo,
},
data() {
return {
loginForm: {
username: 'admin',
password: '123456',
},
};
},
created() {
setTimeout(() => {
this.handleLogin();
}, 6000);
},
computed: {
...mapGetters(['tagWel']),
},
methods: {
handleLogin() {
this.$store.dispatch('LoginByUsername', this.loginForm).then(() => {
this.$router.push(this.tagWel);
});
},
},
};
</script>
<style></style>

160
src/page/login/index.vue Normal file
View File

@@ -0,0 +1,160 @@
<template>
<div class="login-container" ref="login" @keyup.enter="handleLogin">
<div class="login-time">
{{ time }}
</div>
<div class="login-weaper">
<div class="login-left animate__animated animate__fadeInLeft">
<img class="img" src="/img/logo.png" alt="" />
<p class="title">{{ $t('login.info') }}</p>
</div>
<div class="login-border animate__animated animate__fadeInRight">
<div class="login-main">
<p class="login-title">
{{ $t('login.title') }}{{ website.title }}
<top-lang></top-lang>
</p>
<userLogin v-if="activeName === 'user'"></userLogin>
<codeLogin v-else-if="activeName === 'code'"></codeLogin>
<thirdLogin v-else-if="activeName === 'third'"></thirdLogin>
<registerLogin v-else-if="activeName === 'register'"></registerLogin>
<div class="login-menu">
<el-link href="#" @click.stop="activeName = 'user'">{{
$t('login.userLogin')
}}</el-link>
<el-link href="#" @click.stop="activeName = 'code'">{{
$t('login.phoneLogin')
}}</el-link>
<el-link href="#" @click.stop="activeName = 'third'">{{
$t('login.thirdLogin')
}}</el-link>
<el-link
:href="
website.oauth2.ssoBaseUrl + website.oauth2.ssoAuthUrl + website.oauth2.redirectUri
"
>{{ $t('login.ssoLogin') }}</el-link
>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import userLogin from './userlogin.vue';
import registerLogin from './registerlogin.vue';
import codeLogin from './codelogin.vue';
import thirdLogin from './thirdlogin.vue';
import { mapGetters } from 'vuex';
import { validatenull } from '@/utils/validate';
import topLang from '@/page/index/top/top-lang.vue';
import { getQueryString, getTopUrl } from '@/utils/util';
import website from '@/config/website';
export default {
name: 'login',
components: {
userLogin,
registerLogin,
codeLogin,
thirdLogin,
topLang,
},
data() {
return {
website: website,
time: '',
activeName: 'user',
socialForm: {
tenantId: '000000',
source: '',
code: '',
state: '',
},
};
},
watch: {
$route() {
this.handleLogin();
},
},
created() {
this.handleLogin();
this.getTime();
},
mounted() {},
computed: {
...mapGetters(['tagWel']),
},
props: [],
methods: {
getTime() {
setInterval(() => {
this.time = this.$dayjs().format('YYYY年MM月DD日 HH:mm:ss');
}, 1000);
},
handleLogin() {
const topUrl = getTopUrl();
const redirectUrl = '/oauth/redirect/';
const ssoCode = '?code=';
this.socialForm.source = getQueryString('source');
this.socialForm.code = getQueryString('code');
this.socialForm.state = getQueryString('state');
if (validatenull(this.socialForm.source) && topUrl.includes(redirectUrl)) {
let source = topUrl.split('?')[0];
source = source.split(redirectUrl)[1];
this.socialForm.source = source;
}
if (
topUrl.includes(redirectUrl) &&
!validatenull(this.socialForm.source) &&
!validatenull(this.socialForm.code) &&
!validatenull(this.socialForm.state)
) {
const loading = this.$loading({
lock: true,
text: '第三方系统登录中,请稍后',
background: 'rgba(0, 0, 0, 0.7)',
});
this.$store
.dispatch('LoginBySocial', this.socialForm)
.then(() => {
window.location.href = topUrl.split(redirectUrl)[0];
//加载工作流路由集
this.loadFlowRoutes();
this.$router.push(this.tagWel);
loading.close();
})
.catch(() => {
loading.close();
});
} else if (
!topUrl.includes(redirectUrl) &&
!validatenull(this.socialForm.code) &&
!validatenull(this.socialForm.state)
) {
const loading = this.$loading({
lock: true,
text: '单点系统登录中,请稍后',
background: 'rgba(0, 0, 0, 0.7)',
});
this.$store
.dispatch('LoginBySso', this.socialForm)
.then(() => {
window.location.href = topUrl.split(ssoCode)[0];
//加载工作流路由集
this.loadFlowRoutes();
this.$router.push(this.tagWel);
loading.close();
})
.catch(() => {
loading.close();
});
}
},
loadFlowRoutes() {
this.$store.dispatch('FlowRoutes').then(() => {});
},
},
};
</script>

View File

@@ -0,0 +1,216 @@
<template>
<el-form
class="login-form"
status-icon
:rules="loginRules"
ref="loginForm"
:model="loginForm"
label-width="0"
>
<el-form-item v-if="tenantMode" prop="tenantId">
<el-input
@keyup.enter="handleRegister"
v-model="loginForm.tenantId"
auto-complete="off"
:placeholder="$t('login.tenantId')"
>
<template #prefix>
<i class="icon-quanxian" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="name">
<el-input
@keyup.enter="handleRegister"
v-model="loginForm.name"
auto-complete="off"
:placeholder="$t('login.name')"
>
<template #prefix>
<i class="icon-zhanghaoquanxianguanli" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="account">
<el-input
@keyup.enter="handleRegister"
v-model="loginForm.account"
auto-complete="off"
:placeholder="$t('login.username')"
>
<template #prefix>
<i class="icon-yonghu" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="phone">
<el-input
@keyup.enter="handleRegister"
v-model="loginForm.phone"
auto-complete="off"
:placeholder="$t('login.phone')"
>
<template #prefix>
<i class="icon-shouji" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="email">
<el-input
@keyup.enter="handleRegister"
v-model="loginForm.email"
auto-complete="off"
:placeholder="$t('login.email')"
>
<template #prefix>
<i class="icon-xiaoxitongzhi" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
@keyup.enter="handleRegister"
type="password"
show-password
v-model="loginForm.password"
auto-complete="off"
:placeholder="$t('login.password')"
>
<template #prefix>
<i class="icon-mima" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="password1">
<el-input
@keyup.enter="handleRegister"
type="password"
show-password
v-model="loginForm.password1"
auto-complete="off"
:placeholder="$t('login.password1')"
>
<template #prefix>
<i class="icon-mima" />
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click.prevent="handleRegister" class="login-submit"
>{{ $t('login.register') }}
</el-button>
<el-button @click.prevent="handleBack" class="register-submit"
>{{ $t('login.back') }}
</el-button>
</el-form-item>
</el-form>
</template>
<script>
import { mapGetters } from 'vuex';
import { info } from '@/api/system/tenant';
import { getTopUrl } from '@/utils/util';
export default {
name: 'userlogin',
data() {
return {
tenantMode: this.website.tenantMode,
captchaMode: this.website.captchaMode,
registerMode: this.website.oauth2.registerMode,
loginForm: {
//租户ID
tenantId: '',
//部门ID
deptId: '',
//角色ID
roleId: '',
//用户名
account: '',
//手机号
phone: '',
//邮箱
email: '',
//密码
password: '',
//确认密码
password1: '',
//账号类型
type: 'account',
//预加载白色背景
image: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
},
loginRules: {
tenantId: [{ required: true, message: '请输入租户ID', trigger: 'blur' }],
name: [{ required: true, message: '请输入密码', trigger: 'blur' }],
account: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
email: [{ required: true, message: '请输入邮箱', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
password1: [{ required: true, message: '请输入确认密码', trigger: 'blur' }],
},
passwordType: 'password',
};
},
created() {
this.getTenant();
},
mounted() {
this.$nextTick(() => {});
},
watch: {},
computed: {
...mapGetters(['tagWel', 'userInfo']),
},
props: [],
methods: {
handleRegister() {
if (this.loginForm.password !== this.loginForm.password1) {
this.$message.error('两次密码输入不一致');
return;
}
this.$refs.loginForm.validate(valid => {
if (valid) {
const loading = this.$loading({
lock: true,
text: '注册中,请稍后',
background: 'rgba(0, 0, 0, 0.7)',
});
this.$store
.dispatch('RegisterUser', this.loginForm)
.then(() => {
this.$alert('注册成功,请耐心等待管理员审核后分配权限', '注册成功', {
confirmButtonText: '确定',
callback: () => {
this.$parent.activeName = 'user';
},
});
})
.catch(err => {
console.log(err);
});
loading.close();
}
});
},
handleBack() {
this.$parent.activeName = 'user';
},
getTenant() {
let domain = getTopUrl();
// 临时指定域名,方便测试
//domain = "https://bladex.cn";
info(domain).then(res => {
const data = res.data;
if (data.success && data.data.tenantId) {
this.tenantMode = false;
this.loginForm.tenantId = data.data.tenantId;
this.$parent.$refs.login.style.backgroundImage = `url(${data.data.backgroundUrl})`;
}
});
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,67 @@
<template>
<div class="social-container">
<div @click="handleClick('github')">
<span class="container" :style="{ backgroundColor: '#61676D' }">
<i icon-class="github" class="iconfont icongithub"></i>
</span>
</div>
<div @click="handleClick('gitee')">
<span class="container" :style="{ backgroundColor: '#c35152' }">
<i icon-class="gitee" class="iconfont icongitee2"></i>
</span>
</div>
<div @click="handleClick('wechat_open')">
<span class="container" :style="{ backgroundColor: '#8dc349' }">
<i icon-class="wechat" class="iconfont icon-weixin" />
</span>
</div>
<div @click="handleClick('qq')">
<span class="container" :style="{ backgroundColor: '#6ba2d6' }">
<i icon-class="qq" class="iconfont icon-qq" />
</span>
</div>
</div>
</template>
<script>
import website from '@/config/website';
export default {
name: 'thirdLogin',
methods: {
handleClick(source) {
window.location.href = `${website.oauth2.authUrl}/${source}`;
},
},
};
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.social-container {
margin: 20px 0;
display: flex;
align-items: center;
justify-content: space-around;
.iconfont {
color: #fff;
font-size: 30px;
}
.container {
$height: 50px;
cursor: pointer;
display: inline-block;
width: $height;
height: $height;
line-height: $height;
text-align: center;
border-radius: 4px;
margin-bottom: 10px;
}
.title {
text-align: center;
}
}
</style>

View File

@@ -0,0 +1,218 @@
<template>
<el-form
class="login-form"
status-icon
:rules="loginRules"
ref="loginForm"
:model="loginForm"
label-width="0"
>
<el-form-item v-if="tenantMode" prop="tenantId">
<el-input
@keyup.enter="handleLogin"
v-model="loginForm.tenantId"
auto-complete="off"
:placeholder="$t('login.tenantId')"
>
<template #prefix>
<i class="icon-quanxian" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="username">
<el-input
@keyup.enter="handleLogin"
v-model="loginForm.username"
auto-complete="off"
:placeholder="$t('login.username')"
>
<template #prefix>
<i class="icon-yonghu" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
@keyup.enter="handleLogin"
type="password"
show-password
v-model="loginForm.password"
auto-complete="off"
:placeholder="$t('login.password')"
>
<template #prefix>
<i class="icon-mima" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="code" class="login-code" v-if="captchaMode && captchaType === 'image'">
<el-input
@keyup.enter="handleLogin"
v-model="loginForm.code"
auto-complete="off"
:placeholder="$t('login.code')"
>
<template #prefix>
<i class="icon-yanzhengma"></i>
</template>
<template #append>
<div class="login-code-box">
<img :src="loginForm.image" class="login-code-img" @click="refreshCode" />
</div>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click.prevent="handleLogin" class="login-submit"
>{{ $t('login.submit') }}
</el-button>
<el-button
v-if="this.registerMode"
type="danger"
@click.prevent="handleRegister"
class="register-submit"
>{{ $t('login.register') }}
</el-button>
</el-form-item>
<behavior-captcha ref="behaviorCaptcha" @success="handleBehaviorSuccess"></behavior-captcha>
</el-form>
</template>
<script>
import { mapGetters } from 'vuex';
import { info } from '@/api/system/tenant';
import { getCaptcha } from '@/api/user';
import { getTopUrl } from '@/utils/util';
import { validatenull } from '@/utils/validate';
import BehaviorCaptcha from '@/components/behavior-captcha/index.vue';
export default {
name: 'userlogin',
components: {
BehaviorCaptcha,
},
data() {
return {
tenantMode: this.website.tenantMode,
captchaMode: this.website.captchaMode,
captchaType: this.website.captchaType || 'image',
registerMode: this.website.oauth2.registerMode,
loginForm: {
//租户ID
tenantId: '000000',
//部门ID
deptId: '',
//角色ID
roleId: '',
//用户名
username: 'admin',
//密码
password: 'admin',
//账号类型
type: 'account',
//验证码的值
code: '',
//验证码的索引
key: '',
//预加载白色背景
image: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
},
loginRules: {
tenantId: [{ required: false, message: '请输入租户ID', trigger: 'blur' }],
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 1, message: '密码长度最少为6位', trigger: 'blur' },
],
},
passwordType: 'password',
};
},
created() {
this.getTenant();
this.refreshCode();
},
mounted() {
this.$nextTick(() => {});
},
computed: {
...mapGetters(['tagWel', 'userInfo']),
},
props: [],
methods: {
refreshCode() {
if (this.website.captchaMode && this.captchaType === 'image') {
getCaptcha().then(res => {
const data = res.data;
this.loginForm.key = data.key;
this.loginForm.image = data.image;
});
}
},
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
// 行为验证模式下先完成人机验证,通过后携带一次性票据登录
if (this.captchaMode && this.captchaType === 'behavior') {
this.$refs.behaviorCaptcha.open();
return;
}
this.doLogin();
}
});
},
handleBehaviorSuccess(result) {
this.loginForm.key = result.key;
this.loginForm.code = result.ticket;
this.doLogin();
},
doLogin() {
const loading = this.$loading({
lock: true,
text: '登录中,请稍后',
background: 'rgba(0, 0, 0, 0.7)',
});
this.$store
.dispatch('LoginByUsername', this.loginForm)
.then(() => {
loading.close();
//加载工作流路由集
this.loadFlowRoutes();
this.$router.push(this.tagWel);
})
.catch(err => {
console.log(err);
loading.close();
this.refreshCode();
});
},
handleRegister() {
this.$parent.activeName = 'register';
},
loadFlowRoutes() {
this.$store.dispatch('FlowRoutes').then(() => {});
},
getTenant() {
let domain = getTopUrl();
// 临时指定域名,方便测试
//domain = "https://bladex.cn";
info(domain).then(res => {
const data = res.data;
if (data.success && data.data.tenantId) {
this.tenantMode = false;
this.loginForm.tenantId = data.data.tenantId;
if (!validatenull(data.data.backgroundUrl)) {
const loginEl = this.$parent.$refs.login;
loginEl.style.backgroundImage = `url(${data.data.backgroundUrl})`;
loginEl.style.backgroundSize = '100% 100%';
loginEl.style.backgroundRepeat = 'no-repeat';
loginEl.style.backgroundPosition = 'center center';
}
}
});
},
},
};
</script>
<style></style>