提交 4ae629ab 编写于 作者: 郝先瑞

refactor: pinia整合优化重构

上级 7bd9d704
......@@ -7,17 +7,19 @@
<script setup lang="ts">
import {computed, onMounted, ref, watch} from "vue";
import {useAppStoreHook} from "@/store/modules/app";
import {ElConfigProvider} from 'element-plus'
import {localStorage} from "@/utils/storage";
import useStore from "@/store";
//官方文档: https://element-plus.gitee.io/zh-CN/guide/i18n.html
// 导入 Element Plus 语言包
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/es/locale/lang/en'
const language = computed(() => useAppStoreHook().language)
const {app} =useStore()
const language = computed(() => app.language)
const locale = ref()
watch(language, (value) => {
......
......@@ -19,9 +19,10 @@
<script setup lang="ts">
import {computed} from "vue";
import {useAppStoreHook} from "@/store/modules/app";
import useStore from "@/store";
const language = computed(() => useAppStoreHook().language)
const {app}=useStore()
const language = computed(() => app.language)
import {useI18n} from 'vue-i18n'
import {ElMessage} from 'element-plus'
......@@ -31,7 +32,7 @@ const {locale} = useI18n()
function handleSetLanguage(lang: string) {
locale.value = lang
useAppStoreHook().setLanguage(lang)
app.setLanguage(lang)
if (lang == 'en') {
ElMessage.success('Switch Language Successful!')
} else {
......
......@@ -17,12 +17,15 @@
import {computed, onBeforeUnmount, onMounted, ref, watch} from "vue";
import {addClass, removeClass} from '@/utils/index'
import {useSettingStoreHook} from "@/store/modules/settings";
import useStore from "@/store";
// 图标依赖
import {Close, Setting} from '@element-plus/icons-vue'
import {ElColorPicker} from "element-plus";
const {setting} =useStore()
const props = defineProps({
buttonTop: {
default: 250,
......@@ -30,7 +33,7 @@ const props = defineProps({
}
})
const theme = computed(() => useSettingStoreHook().theme)
const theme = computed(() =>setting.theme)
const show = ref(false)
......
<template>
<el-dropdown class="size-select" trigger="click" @command="handleSetSize">
<div class="size-select__icon">
<svg-icon class-name="size-icon" icon-class="size"/>
<svg-icon class-name="size-icon" icon-class="size" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item of sizeOptions"
:key="item.value"
:disabled="(size||'default')==item.value"
:command="item.value">
v-for="item of sizeOptions"
:key="item.value"
:disabled="(size || 'default') == item.value"
:command="item.value"
>
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
......@@ -18,29 +19,28 @@
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import {ref, computed} from "vue";
import {useRoute, useRouter} from "vue-router"
import { ElMessage } from "element-plus";
import {ElMessage} from 'element-plus'
import useStore from "@/store";
import SvgIcon from "@/components/SvgIcon/index.vue";
import {useAppStoreHook} from '@/store/modules/app'
import SvgIcon from '@/components/SvgIcon/index.vue'
const size = computed(() => useAppStoreHook().size)
const { app } = useStore();
const size = computed(() => app.size);
const sizeOptions = ref([
{label: '默认', value: 'default'},
{label: '大型', value: 'large'},
{label: '小型', value: 'small'}
])
{ label: "默认", value: "default" },
{ label: "大型", value: "large" },
{ label: "小型", value: "small" },
]);
function handleSetSize(size: string) {
useAppStoreHook().setSize(size)
window.location.reload()
ElMessage.success('切换布局大小成功')
app.setSize(size);
window.location.reload();
ElMessage.success("切换布局大小成功");
}
</script>
<style lang='scss' scoped>
......
......@@ -9,8 +9,7 @@
<script setup lang="ts">
import {computed, nextTick, watch} from "vue";
import {useSettingStoreHook} from "@/store/modules/settings";
import {useTagsViewStoreHook} from "@/store/modules/tagsView";
import useStore from "@/store";
import {useRoute, useRouter} from "vue-router";
import {localStorage} from "@/utils/storage";
......@@ -23,7 +22,8 @@ const mixBlack = "#000000";
const node = document.documentElement;
const theme = computed(() => useSettingStoreHook().theme)
const {setting} =useStore()
const theme = computed(() => setting.theme)
watch(theme, (color: string) => {
node.style.setProperty("--el-color-primary", color);
......
import {useUserStoreHook} from "@/store/modules/user";
import {Directive, DirectiveBinding} from "vue";
import useStore from "@/store";
import { Directive, DirectiveBinding } from "vue";
/**
* 按钮权限校验
......@@ -7,16 +8,17 @@ import {Directive, DirectiveBinding} from "vue";
export const hasPerm: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
// 「超级管理员」拥有所有的按钮权限
const roles = useUserStoreHook().roles;
const { user } = useStore()
const roles = user.roles;
if (roles.includes('ROOT')) {
return true
}
// 「其他角色」按钮权限校验
const {value} = binding;
const { value } = binding;
if (value) {
const requiredPerms = value; // DOM绑定需要的按钮权限标识
const hasPerm = useUserStoreHook().perms.some(perm => {
const hasPerm = user.perms.some(perm => {
return requiredPerms.includes(perm)
})
......@@ -34,11 +36,12 @@ export const hasPerm: Directive = {
*/
export const hasRole: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const {value} = binding;
const { value } = binding;
if (value) {
const requiredRoles = value; // DOM绑定需要的角色编码
const hasRole = useUserStoreHook().roles.some(perm => {
const { user } = useStore()
const hasRole = user.roles.some(perm => {
return requiredRoles.includes(perm)
})
......
<template>
<section class="app-main">
<router-view v-slot="{ Component ,route}">
<router-view v-slot="{ Component, route }">
<transition name="router-fade" mode="out-in">
<keep-alive :include="cachedViews">
<component :is="Component" :key="route.path"/>
<component :is="Component" :key="route.path" />
</keep-alive>
</transition>
</router-view>
......@@ -12,11 +12,12 @@
<script setup lang="ts">
import {computed} from "vue";
import {useTagsViewStoreHook} from '@/store/modules/tagsView'
import { computed } from "vue";
import useStore from "@/store";
const cachedViews = computed(() => useTagsViewStoreHook().cachedViews);
const { tagsView } = useStore();
const cachedViews = computed(() => tagsView.cachedViews);
</script>
<style lang="scss" scoped>
......@@ -28,7 +29,7 @@ const cachedViews = computed(() => useTagsViewStoreHook().cachedViews);
overflow: hidden;
}
.fixed-header+.app-main {
.fixed-header + .app-main {
padding-top: 50px;
}
......@@ -38,7 +39,7 @@ const cachedViews = computed(() => useTagsViewStoreHook().cachedViews);
min-height: calc(100vh - 84px);
}
.fixed-header+.app-main {
.fixed-header + .app-main {
padding-top: 84px;
}
}
......
<template>
<div class="navbar">
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container"
@toggleClick="toggleSideBar"/>
<hamburger
id="hamburger-container"
:is-active="sidebar.opened"
class="hamburger-container"
@toggleClick="toggleSideBar"
/>
<breadcrumb id="breadcrumb-container" class="breadcrumb-container"/>
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" />
<div class="right-menu">
<template v-if="device!=='mobile'">
<template v-if="device !== 'mobile'">
<!-- <search id="header-search" class="right-menu-item" />
<error-log class="errLog-container right-menu-item hover-effect" />-->
<screenfull id="screenfull" class="right-menu-item hover-effect"/>
<screenfull id="screenfull" class="right-menu-item hover-effect" />
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect"/>
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>
<lang-select class="right-menu-item hover-effect" />
</template>
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
<el-dropdown
class="avatar-container right-menu-item hover-effect"
trigger="click"
>
<div class="avatar-wrapper">
<img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
<CaretBottom style="width: .6em; height: .6em;margin-left: 5px"/>
<img :src="avatar + '?imageView2/1/w/80/h/80'" class="user-avatar" />
<CaretBottom style="width: 0.6em; height: 0.6em; margin-left: 5px" />
</div>
<template #dropdown>
<el-dropdown-menu>
<router-link to="/">
<el-dropdown-item>{{$t('navbar.dashboard')}}</el-dropdown-item>
<el-dropdown-item>{{ $t("navbar.dashboard") }}</el-dropdown-item>
</router-link>
<a target="_blank" href="https://github.com/hxrui">
<el-dropdown-item>Github</el-dropdown-item>
</a>
<a target="_blank" href="https://gitee.com/haoxr">
<el-dropdown-item>{{$t('navbar.gitee')}}</el-dropdown-item>
<el-dropdown-item>{{ $t("navbar.gitee") }}</el-dropdown-item>
</a>
<a target="_blank" href="https://www.cnblogs.com/haoxianrui/">
<el-dropdown-item>{{$t('navbar.document')}}</el-dropdown-item>
<el-dropdown-item>{{ $t("navbar.document") }}</el-dropdown-item>
</a>
<el-dropdown-item divided @click="logout">
{{$t('navbar.logout')}}
{{ $t("navbar.logout") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup lang="ts">
import {computed} from "vue"
import {useRoute, useRouter} from "vue-router"
import {ElMessageBox} from 'element-plus'
import { computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ElMessageBox } from "element-plus";
import {useAppStoreHook} from '@/store/modules/app'
import {useUserStoreHook} from '@/store/modules/user'
import useStore from "@/store";
// 组件依赖
import Breadcrumb from '@/components/Breadcrumb/index.vue'
import Hamburger from '@/components/Hamburger/index.vue'
import Screenfull from '@/components/Screenfull/index.vue'
import SizeSelect from '@/components/SizeSelect/index.vue'
import LangSelect from '@/components/LangSelect/index.vue'
import SvgIcon from '@/components/SvgIcon/index.vue'
import Breadcrumb from "@/components/Breadcrumb/index.vue";
import Hamburger from "@/components/Hamburger/index.vue";
import Screenfull from "@/components/Screenfull/index.vue";
import SizeSelect from "@/components/SizeSelect/index.vue";
import LangSelect from "@/components/LangSelect/index.vue";
import SvgIcon from "@/components/SvgIcon/index.vue";
// 图标依赖
import {CaretBottom} from '@element-plus/icons-vue'
import { CaretBottom } from "@element-plus/icons-vue";
const { app, user } = useStore();
const route = useRoute()
const router = useRouter()
const route = useRoute();
const router = useRouter();
const sidebar = computed(() => useAppStoreHook().sidebar)
const device = computed(() => useAppStoreHook().device)
const avatar = computed(() => useUserStoreHook().avatar)
const sidebar = computed(() => app.sidebar);
const device = computed(() => app.device);
const avatar = computed(() => user.avatar);
function toggleSideBar() {
useAppStoreHook().toggleSidebar()
app.toggleSidebar();
}
function logout() {
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
useUserStoreHook().logout().then(() => {
router.push(`/login?redirect=${route.fullPath}`)
})
})
user.logout().then(() => {
router.push(`/login?redirect=${route.fullPath}`);
});
});
}
</script>
<style lang="scss" scoped>
ul { list-style: none; margin: 0; padding: 0; }
ul {
list-style: none;
margin: 0;
padding: 0;
}
.navbar {
height: 50px;
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background .3s;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
&:hover {
background: rgba(0, 0, 0, .025)
background: rgba(0, 0, 0, 0.025);
}
}
......@@ -134,10 +145,10 @@ ul { list-style: none; margin: 0; padding: 0; }
&.hover-effect {
cursor: pointer;
transition: background .3s;
transition: background 0.3s;
&:hover {
background: rgba(0, 0, 0, .025)
background: rgba(0, 0, 0, 0.025);
}
}
}
......
<template>
<div class="drawer-container">
<h3 class="drawer-title">系统布局配置</h3>
<div class="drawer-item">
<span>主题颜色</span>
<div style="float: right;height: 26px;margin: -3px 8px 0 0;">
<theme-picker @change="themeChange"/>
</div>
</div>
<div class="drawer-item">
<span>开启 Tags-View</span>
<el-switch v-model="tagsView" class="drawer-switch"/>
</div>
<div class="drawer-item">
<span>固定 Header</span>
<el-switch v-model="fixedHeader" class="drawer-switch"/>
</div>
<div class="drawer-item">
<span>侧边栏 Logo</span>
<el-switch v-model="sidebarLogo" class="drawer-switch"/>
<h3 class="drawer-title">系统布局配置</h3>
<div class="drawer-item">
<span>主题颜色</span>
<div style="float: right; height: 26px; margin: -3px 8px 0 0">
<theme-picker @change="themeChange" />
</div>
</div>
<div class="drawer-item">
<span>开启 Tags-View</span>
<el-switch v-model="tagsView" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>固定 Header</span>
<el-switch v-model="fixedHeader" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>侧边栏 Logo</span>
<el-switch v-model="sidebarLogo" class="drawer-switch" />
</div>
</div>
</template>
<script setup lang="ts">
import {reactive, toRefs, watch} from "vue";
import {useSettingStoreHook} from "@/store/modules/settings";
import ThemePicker from '@/components/ThemePicker/index.vue';
import { reactive, toRefs, watch } from "vue";
import ThemePicker from "@/components/ThemePicker/index.vue";
import useStore from "@/store";
const { setting } = useStore();
const state = reactive({
fixedHeader: useSettingStoreHook().fixedHeader,
tagsView: useSettingStoreHook().tagsView,
sidebarLogo: useSettingStoreHook().sidebarLogo
})
fixedHeader: setting.fixedHeader,
tagsView: setting.tagsView,
sidebarLogo: setting.sidebarLogo,
});
const {fixedHeader, tagsView, sidebarLogo} = toRefs(state)
const { fixedHeader, tagsView, sidebarLogo } = toRefs(state);
function themeChange(val: any) {
useSettingStoreHook().changeSetting({key: 'theme', value: val})
setting.changeSetting({ key: "theme", value: val });
}
watch(() => state.fixedHeader, (value) => {
useSettingStoreHook().changeSetting({key: 'fixedHeader', value: value})
})
watch(() => state.tagsView, (value) => {
useSettingStoreHook().changeSetting({key: 'tagsView', value: value})
})
watch(
() => state.fixedHeader,
(value) => {
setting.changeSetting({ key: "fixedHeader", value: value });
}
);
watch(() => state.sidebarLogo, (value) => {
useSettingStoreHook().changeSetting({key: 'sidebarLogo', value: value})
})
watch(
() => state.tagsView,
(value) => {
setting.changeSetting({ key: "tagsView", value: value });
}
);
watch(
() => state.sidebarLogo,
(value) => {
setting.changeSetting({ key: "sidebarLogo", value: value });
}
);
</script>
<style lang="scss" scoped>
......@@ -65,19 +77,19 @@ watch(() => state.sidebarLogo, (value) => {
.drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, .85);
color: rgba(0, 0, 0, 0.85);
font-size: 14px;
line-height: 22px;
}
.drawer-item {
color: rgba(0, 0, 0, .65);
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
padding: 12px 0;
}
.drawer-switch {
float: right
float: right;
}
.job-link {
......
......@@ -19,10 +19,13 @@
import {computed, defineComponent} from 'vue'
import { isExternal } from '@/utils/validate'
import { useRouter } from 'vue-router'
import {useAppStoreHook} from "@/store/modules/app";
const sidebar = computed(() => useAppStoreHook().sidebar);
const device = computed(() => useAppStoreHook().device);
import useStore from "@/store";
const {app}=useStore()
const sidebar = computed(() => app.sidebar);
const device = computed(() => app.device);
export default defineComponent({
props: {
......@@ -35,7 +38,7 @@ export default defineComponent({
const router = useRouter()
const push = () => {
if (device.value === 'mobile' && sidebar.value.opened == true) {
useAppStoreHook().closeSideBar(false)
app.closeSideBar(false)
}
router.push(props.to).catch((err) => {
console.log(err)
......
......@@ -39,8 +39,8 @@ import {isExternal} from '@/utils/validate'
import AppLink from './Link.vue'
import {RouteRecordRaw} from "vue-router";
import SvgIcon from '@/components/SvgIcon/index.vue';
import { generateTitle } from '@/utils/i18n'
import SvgIcon from '@/components/SvgIcon/index.vue';
const props = defineProps({
item: {
......
......@@ -24,20 +24,22 @@
</template>
<script setup lang="ts">
import {computed, defineComponent} from "vue";
import {useRoute} from 'vue-router'
import SidebarItem from './SidebarItem.vue'
import Logo from './Logo.vue'
import variables from '@/styles/variables.module.scss'
import { useSettingStoreHook } from "@/store/modules/settings";
import { useAppStoreHook } from "@/store/modules/app";
import { usePermissionStoreHook } from "@/store/modules/permission";
import {useRoute} from 'vue-router'
import useStore from "@/store";
const {permission,setting,app} =useStore();
const route =useRoute()
const routes =computed(() => usePermissionStoreHook().routes)
const showLogo = computed(() => useSettingStoreHook().sidebarLogo)
const isCollapse = computed(() => !useAppStoreHook().sidebar.opened)
const routes =computed(() => permission.routes)
const showLogo = computed(() => setting.sidebarLogo)
const isCollapse = computed(() => !app.sidebar.opened)
const activeMenu = computed(() => {
const {meta, path} = route
......
<template>
<el-scrollbar
ref="scrollContainerRef"
:vertical="false"
class="scroll-container"
@wheel.prevent="handleScroll">
<slot/>
ref="scrollContainerRef"
:vertical="false"
class="scroll-container"
@wheel.prevent="handleScroll"
>
<slot />
</el-scrollbar>
</template>
<script setup lang="ts">
import {ref, computed, onMounted, onBeforeUnmount, getCurrentInstance} from "vue";
import {useTagsViewStoreHook} from "@/store/modules/tagsView"
import {TagView} from "@/store/interface";
const emits = defineEmits()
import {
ref,
computed,
onMounted,
onBeforeUnmount,
getCurrentInstance,
} from "vue";
import { TagView } from "@/store/interface";
import useStore from "@/store";
const tagAndTagSpacing = ref(4)
const scrollContainerRef = ref(null)
const visitedViews = computed(() => useTagsViewStoreHook().visitedViews)
const emits = defineEmits();
const tagAndTagSpacing = ref(4);
const scrollContainerRef = ref(null);
const { tagsView } = useStore();
const visitedViews = computed(() => tagsView.visitedViews);
const emitScroll = () => {
(emits as any)('scroll')
}
(emits as any)("scroll");
};
const {ctx} = getCurrentInstance() as any
const { ctx } = getCurrentInstance() as any;
const scrollWrapper = computed(() => {
return (scrollContainerRef.value as any).$refs.wrap as HTMLElement
})
return (scrollContainerRef.value as any).$refs.wrap as HTMLElement;
});
onMounted(() => {
//scrollWrapper.value.addEventListener('scroll', emitScroll, true);
})
});
onBeforeUnmount(() => {
// scrollWrapper.value.removeEventListener('scroll', emitScroll);
})
});
function handleScroll(e: WheelEvent) {
const eventDelta = (e as any).wheelDelta || -e.deltaY * 40
scrollWrapper.value.scrollLeft = scrollWrapper.value.scrollLeft + eventDelta / 4
const eventDelta = (e as any).wheelDelta || -e.deltaY * 40;
scrollWrapper.value.scrollLeft =
scrollWrapper.value.scrollLeft + eventDelta / 4;
}
function moveToTarget(currentTag:TagView) {
const $container = ctx.$refs.scrollContainer.$el
const $containerWidth = $container.offsetWidth
function moveToTarget(currentTag: TagView) {
const $container = ctx.$refs.scrollContainer.$el;
const $containerWidth = $container.offsetWidth;
const $scrollWrapper = scrollWrapper.value;
let firstTag = null
let lastTag = null
let firstTag = null;
let lastTag = null;
// find first tag and last tag
if (visitedViews.value.length > 0) {
firstTag = visitedViews.value[0]
lastTag = visitedViews.value[visitedViews.value.length - 1]
firstTag = visitedViews.value[0];
lastTag = visitedViews.value[visitedViews.value.length - 1];
}
if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = 0
$scrollWrapper.scrollLeft = 0;
} else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
} else {
const tagListDom = document.getElementsByClassName('tags-view-item');
const currentIndex = visitedViews.value.findIndex(item => item === currentTag)
let prevTag = null
let nextTag = null
const tagListDom = document.getElementsByClassName("tags-view-item");
const currentIndex = visitedViews.value.findIndex(
(item) => item === currentTag
);
let prevTag = null;
let nextTag = null;
for (const k in tagListDom) {
if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
if ((tagListDom[k] as any).dataset.path === visitedViews.value[currentIndex - 1].path) {
prevTag = tagListDom[k] ;
if (k !== "length" && Object.hasOwnProperty.call(tagListDom, k)) {
if (
(tagListDom[k] as any).dataset.path ===
visitedViews.value[currentIndex - 1].path
) {
prevTag = tagListDom[k];
}
if ((tagListDom[k] as any).dataset.path === visitedViews.value[currentIndex + 1].path) {
if (
(tagListDom[k] as any).dataset.path ===
visitedViews.value[currentIndex + 1].path
) {
nextTag = tagListDom[k];
}
}
}
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft = (nextTag as any).offsetLeft + (nextTag as any).offsetWidth + tagAndTagSpacing.value
const afterNextTagOffsetLeft =
(nextTag as any).offsetLeft +
(nextTag as any).offsetWidth +
tagAndTagSpacing.value;
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = (prevTag as any).offsetLeft - tagAndTagSpacing.value
const beforePrevTagOffsetLeft =
(prevTag as any).offsetLeft - tagAndTagSpacing.value;
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth;
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft;
}
}
}
defineExpose({
moveToTarget
})
moveToTarget,
});
</script>
<style lang="scss" scoped>
......
<template>
<div id="tags-view-container" class="tags-view-container">
<scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
<scroll-pane
ref="scrollPaneRef"
class="tags-view-wrapper"
@scroll="handleScroll"
>
<router-link
v-for="tag in visitedViews"
:key="tag.path"
:class="isActive(tag)?'active':''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
class="tags-view-item"
@click.middle="!isAffix(tag)?closeSelectedTag(tag):''"
@contextmenu.prevent="openMenu(tag,$event)"
v-for="tag in visitedViews"
:key="tag.path"
:class="isActive(tag) ? 'active' : ''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
class="tags-view-item"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent="openMenu(tag, $event)"
>
{{ generateTitle(tag.meta.title) }}
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)">
<close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;"/>
<span
v-if="!isAffix(tag)"
class="el-icon-close"
@click.prevent.stop="closeSelectedTag(tag)"
>
<close
class="el-icon-close"
style="width: 1em; height: 1em; vertical-align: middle"
/>
</span>
</router-link>
</scroll-pane>
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
<ul
v-show="visible"
:style="{ left: left + 'px', top: top + 'px' }"
class="contextmenu"
>
<li @click="refreshSelectedTag(selectedTag)">
<refresh-right style="width: 1em; height: 1em;"/>
<refresh-right style="width: 1em; height: 1em" />
刷新
</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
<close style="width: 1em; height: 1em;"/>
<close style="width: 1em; height: 1em" />
关闭
</li>
<li @click="closeOtherTags">
<circle-close style="width: 1em; height: 1em;"/>
<circle-close style="width: 1em; height: 1em" />
关闭其它
</li>
<li v-if="!isFirstView()" @click="closeLeftTags">
<back style="width: 1em; height: 1em;"/>
<back style="width: 1em; height: 1em" />
关闭左侧
</li>
<li v-if="!isLastView()" @click="closeRightTags">
<right style="width: 1em; height: 1em;"/>
<right style="width: 1em; height: 1em" />
关闭右侧
</li>
<li @click="closeAllTags(selectedTag)">
<circle-close style="width: 1em; height: 1em;"/>
<circle-close style="width: 1em; height: 1em" />
关闭所有
</li>
</ul>
......@@ -46,31 +61,39 @@
</template>
<script setup lang="ts" >
import {useTagsViewStoreHook} from '@/store/modules/tagsView'
import {usePermissionStoreHook} from '@/store/modules/permission'
import path from 'path-browserify'
import {
computed,
getCurrentInstance,
nextTick,
ref,
watch,
onMounted
onMounted,
} from "vue";
import {RouteRecordRaw, useRoute, useRouter} from 'vue-router'
import {TagView} from "@/store/interface";
import ScrollPane from './ScrollPane.vue'
import {Close,RefreshRight,CircleClose,Back,Right} from '@element-plus/icons-vue'
import { generateTitle } from '@/utils/i18n'
import path from "path-browserify";
import { RouteRecordRaw, useRoute, useRouter } from "vue-router";
import { TagView } from "@/store/interface";
const {ctx} = getCurrentInstance() as any
const router = useRouter()
import ScrollPane from "./ScrollPane.vue";
import {
Close,
RefreshRight,
CircleClose,
Back,
Right,
} from "@element-plus/icons-vue";
import { generateTitle } from "@/utils/i18n";
import useStore from "@/store";
const { tagsView, permission } = useStore();
const { ctx } = getCurrentInstance() as any;
const router = useRouter();
const route = useRoute();
const visitedViews = computed<any>(() => useTagsViewStoreHook().visitedViews)
const routes = computed<any>(() => usePermissionStoreHook().routes)
const visitedViews = computed<any>(() => tagsView.visitedViews);
const routes = computed<any>(() => permission.routes);
const affixTags = ref([]);
const visible = ref(false);
......@@ -80,197 +103,207 @@ const left = ref(0);
const top = ref(0);
watch(route, () => {
addTags()
moveToCurrentTag()
})
addTags();
moveToCurrentTag();
});
watch(visible, (value) => {
if (value) {
document.body.addEventListener('click', closeMenu)
document.body.addEventListener("click", closeMenu);
} else {
document.body.removeEventListener('click', closeMenu)
document.body.removeEventListener("click", closeMenu);
}
})
});
function filterAffixTags(routes: RouteRecordRaw[], basePath = '/') {
let tags: TagView[] = []
function filterAffixTags(routes: RouteRecordRaw[], basePath = "/") {
let tags: TagView[] = [];
routes.forEach(route => {
routes.forEach((route) => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path)
const tagPath = path.resolve(basePath, route.path);
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: {...route.meta}
})
meta: { ...route.meta },
});
}
if (route.children) {
const childTags = filterAffixTags(route.children, route.path)
const childTags = filterAffixTags(route.children, route.path);
if (childTags.length >= 1) {
tags = tags.concat(childTags)
tags = tags.concat(childTags);
}
}
})
return tags
});
return tags;
}
function initTags() {
const res = filterAffixTags(routes.value) as []
affixTags.value = res
const res = filterAffixTags(routes.value) as [];
affixTags.value = res;
for (const tag of res) {
// Must have tag name
if ((tag as TagView).name) {
useTagsViewStoreHook().addVisitedView(tag)
tagsView.addVisitedView(tag);
}
}
}
function addTags() {
if (route.name) {
useTagsViewStoreHook().addView(route)
tagsView.addView(route);
}
return false
return false;
}
function moveToCurrentTag() {
const tags = getCurrentInstance()?.refs.tag as any[]
const tags = getCurrentInstance()?.refs.tag as any[];
nextTick(() => {
if (tags === null || tags === undefined || !Array.isArray(tags)) {
return
return;
}
for (const tag of tags) {
if ((tag.to as TagView).path === route.path) {
(scrollPaneRef.value as any).value.moveToTarget(tag)
(scrollPaneRef.value as any).value.moveToTarget(tag);
// when query is different then update
if ((tag.to as TagView).fullPath !== route.fullPath) {
useTagsViewStoreHook().updateVisitedView(route)
tagsView.updateVisitedView(route);
}
}
}
})
});
}
function isActive(tag: TagView) {
return tag.path === route.path
return tag.path === route.path;
}
function isAffix(tag: TagView) {
return tag.meta && tag.meta.affix
return tag.meta && tag.meta.affix;
}
function isFirstView() {
try {
return (selectedTag.value as TagView).fullPath === visitedViews.value[1].fullPath || (selectedTag.value as TagView).fullPath === '/index'
return (
(selectedTag.value as TagView).fullPath ===
visitedViews.value[1].fullPath ||
(selectedTag.value as TagView).fullPath === "/index"
);
} catch (err) {
return false
return false;
}
}
function isLastView() {
try {
return (selectedTag.value as TagView).fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath
return (
(selectedTag.value as TagView).fullPath ===
visitedViews.value[visitedViews.value.length - 1].fullPath
);
} catch (err) {
return false
return false;
}
}
function refreshSelectedTag(view: TagView) {
useTagsViewStoreHook().delCachedView(view)
const {fullPath} = view
tagsView.delCachedView(view);
const { fullPath } = view;
nextTick(() => {
router.replace({path: '/redirect' + fullPath}).catch(err => {
console.warn(err)
})
})
router.replace({ path: "/redirect" + fullPath }).catch((err) => {
console.warn(err);
});
});
}
function toLastView(visitedViews: TagView[], view?: any) {
const latestView = visitedViews.slice(-1)[0]
const latestView = visitedViews.slice(-1)[0];
if (latestView && latestView.fullPath) {
router.push(latestView.fullPath)
router.push(latestView.fullPath);
} 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') {
if (view.name === "Dashboard") {
// to reload home page
router.replace({path: '/redirect' + view.fullPath})
router.replace({ path: "/redirect" + view.fullPath });
} else {
router.push('/')
router.push("/");
}
}
}
function closeSelectedTag(view: TagView) {
useTagsViewStoreHook().delView(view).then((res: any) => {
tagsView.delView(view).then((res: any) => {
if (isActive(view)) {
toLastView(res.visitedViews, view)
toLastView(res.visitedViews, view);
}
})
});
}
function closeLeftTags() {
useTagsViewStoreHook().delLeftViews(selectedTag.value).then((res: any) => {
if (!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)) {
toLastView(res.visitedViews)
tagsView.delLeftViews(selectedTag.value).then((res: any) => {
if (
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
) {
toLastView(res.visitedViews);
}
})
});
}
function closeRightTags() {
useTagsViewStoreHook().delRightViews(selectedTag.value).then((res:any) => {
if (!res.visitedViews.find((item:any) => item.fullPath === route.fullPath)) {
toLastView(res.visitedViews)
tagsView.delRightViews(selectedTag.value).then((res: any) => {
if (
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
) {
toLastView(res.visitedViews);
}
})
});
}
function closeOtherTags() {
useTagsViewStoreHook().delOtherViews(selectedTag.value).then(() => {
moveToCurrentTag()
})
tagsView.delOtherViews(selectedTag.value).then(() => {
moveToCurrentTag();
});
}
function closeAllTags(view: TagView) {
useTagsViewStoreHook().delRightViews(selectedTag.value).then((res:any) => {
if (affixTags.value.some((tag:any) => tag.path === route.path)) {
return
tagsView.delRightViews(selectedTag.value).then((res: any) => {
if (affixTags.value.some((tag: any) => tag.path === route.path)) {
return;
}
toLastView(res.visitedViews, view)
})
toLastView(res.visitedViews, view);
});
}
function openMenu(tag: TagView, e: MouseEvent) {
const menuMinWidth = 105
const offsetLeft = ctx.$el.getBoundingClientRect().left // container margin left
const offsetWidth = ctx.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const l = e.clientX - offsetLeft + 15 // 15: margin right
const menuMinWidth = 105;
const offsetLeft = ctx.$el.getBoundingClientRect().left; // container margin left
const offsetWidth = ctx.$el.offsetWidth; // container width
const maxLeft = offsetWidth - menuMinWidth; // left boundary
const l = e.clientX - offsetLeft + 15; // 15: margin right
if (l > maxLeft) {
left.value = maxLeft
left.value = maxLeft;
} else {
left.value = l
left.value = l;
}
top.value = e.clientY
visible.value = true
selectedTag.value = tag
top.value = e.clientY;
visible.value = true;
selectedTag.value = tag;
}
function closeMenu() {
visible.value = false
visible.value = false;
}
function handleScroll() {
closeMenu()
closeMenu();
}
onMounted(() => {
initTags()
addTags()
})
initTags();
addTags();
});
</script>
<style lang='scss' scoped>
......
<template>
<div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<sidebar class="sidebar-container"/>
<div :class="{hasTagsView:needTagsView}" class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<navbar/>
<tags-view v-if="needTagsView"/>
<div
v-if="device === 'mobile' && sidebar.opened"
class="drawer-bg"
@click="handleClickOutside"
/>
<sidebar class="sidebar-container" />
<div :class="{ hasTagsView: needTagsView }" class="main-container">
<div :class="{ 'fixed-header': fixedHeader }">
<navbar />
<tags-view v-if="needTagsView" />
</div>
<app-main/>
<app-main />
<right-panel v-if="showSettings">
<settings/>
<settings />
</right-panel>
</div>
</div>
</template>
<script setup lang="ts">
import {computed, watchEffect} from "vue"
import {useWindowSize} from '@vueuse/core'
import {AppMain, Navbar, Settings, TagsView} from './components/index'
import Sidebar from './components/Sidebar/index.vue'
import RightPanel from '@/components/RightPanel/index.vue'
import { computed, watchEffect } from "vue";
import { useWindowSize } from "@vueuse/core";
import { AppMain, Navbar, Settings, TagsView } from "./components/index";
import Sidebar from "./components/Sidebar/index.vue";
import RightPanel from "@/components/RightPanel/index.vue";
import {useAppStoreHook} from "@/store/modules/app"
import {useSettingStoreHook} from "@/store/modules/settings"
import useStore from "@/store";
const {width, height} = useWindowSize();
const WIDTH = 992
const { width, height } = useWindowSize();
const WIDTH = 992;
const sidebar = computed(() => useAppStoreHook().sidebar);
const device = computed(() => useAppStoreHook().device);
const needTagsView = computed(() => useSettingStoreHook().tagsView);
const fixedHeader = computed(() => useSettingStoreHook().fixedHeader);
const showSettings = computed(() => useSettingStoreHook().showSettings);
const { app, setting } = useStore();
const sidebar = computed(() => app.sidebar);
const device = computed(() => app.device);
const needTagsView = computed(() => setting.tagsView);
const fixedHeader = computed(() => setting.fixedHeader);
const showSettings = computed(() => setting.showSettings);
const classObj = computed(() => ({
hideSidebar: !sidebar.value.opened,
openSidebar: sidebar.value.opened,
withoutAnimation: sidebar.value.withoutAnimation,
mobile: device.value === 'mobile'
}))
mobile: device.value === "mobile",
}));
watchEffect(() => {
if (width.value < WIDTH) {
useAppStoreHook().toggleDevice("mobile")
useAppStoreHook().closeSideBar(true)
app.toggleDevice("mobile");
app.closeSideBar(true);
} else {
useAppStoreHook().toggleDevice("desktop")
app.toggleDevice("desktop");
}
})
});
function handleClickOutside() {
useAppStoreHook().closeSideBar(false)
app.closeSideBar(false);
}
</script>
......@@ -91,7 +96,7 @@ function handleClickOutside() {
}
.hideSidebar .fixed-header {
width: calc(100% - 54px)
width: calc(100% - 54px);
}
.mobile .fixed-header {
......
......@@ -2,7 +2,7 @@ import {createApp, Directive} from 'vue'
import App from './App.vue'
import router from "@/router";
import {store} from "@/store";
import { createPinia } from "pinia"
import Pagination from '@/components/Pagination/index.vue'
import {localStorage} from "@/utils/storage";
......@@ -34,7 +34,7 @@ app.config.globalProperties.$listDictsByCode = listDictsByCode
// 注册全局组件
app.component('Pagination', Pagination)
.use(store)
.use(createPinia())
.use(router)
.use(ElementPlus, {size: localStorage.get('size') || 'default'})
.use(i18n)
......
import router from "@/router";
import {ElMessage} from "element-plus";
import { usePermissionStoreHook } from "@/store/modules/permission";
import { useUserStoreHook } from "@/store/modules/user";
import { ElMessage } from "element-plus";
import useStore from "@/store";
import NProgress from 'nprogress';
import 'nprogress/nprogress.css'
NProgress.configure({showSpinner: false}) // 进度环显示/隐藏
NProgress.configure({ showSpinner: false }) // 进度环显示/隐藏
// 白名单
const whiteList = ['/login', '/auth-redirect']
router.beforeEach(async (to, form, next) => {
NProgress.start()
const hasToken =useUserStoreHook().token
const { user, permission } = useStore()
const hasToken = user.token
if (hasToken) {
// 如果登录成功,跳转到首页
if (to.path === '/login') {
next({path: '/'})
next({ path: '/' })
NProgress.done()
} else {
const hasGetUserInfo =useUserStoreHook().roles.length > 0
const hasGetUserInfo = user.roles.length > 0
if (hasGetUserInfo) {
next()
} else {
try {
await useUserStoreHook().getUserInfo()
const roles =useUserStoreHook().roles
const accessRoutes:any = await usePermissionStoreHook().generateRoutes(roles)
await user.getUserInfo()
const roles = user.roles
const accessRoutes: any = await permission.generateRoutes(roles)
accessRoutes.forEach((route: any) => {
router.addRoute(route)
})
next({...to, replace: true})
next({ ...to, replace: true })
} catch (error) {
// remove token and go to login page to re-login
await useUserStoreHook().resetToken()
await user.resetToken()
ElMessage.error(error as any || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
......
import { createPinia } from "pinia";
const store = createPinia();
export { store };
// 导入首页模块
import useUserStore from './modules/user'
import useAppStore from './modules/app'
import usePermissionStore from './modules/permission'
import useSettingStore from './modules/settings'
import useTagsViewStore from './modules/tagsView'
const useStore = () => ({
user: useUserStore(),
app: useAppStore(),
permission: usePermissionStore(),
setting: useSettingStore(),
tagsView: useTagsViewStore()
})
export default useStore
\ No newline at end of file
import {AppState} from "@/store/interface";
import {localStorage} from "@/utils/storage";
import {store} from "@/store";
import {defineStore} from "pinia";
import { getLanguage } from '@/lang/index'
export const useAppStore = defineStore({
const useAppStore = defineStore({
id: "app",
state: (): AppState => ({
device: 'desktop',
......@@ -44,6 +43,4 @@ export const useAppStore = defineStore({
}
})
export function useAppStoreHook() {
return useAppStore(store);
}
export default useAppStore;
\ No newline at end of file
......@@ -3,7 +3,6 @@ import {RouteRecordRaw} from 'vue-router'
import {constantRoutes} from '@/router'
import {listRoutes} from "@/api/system/menu";
import {defineStore} from "pinia";
import {store} from "@/store";
const modules = import.meta.glob("../../views/**/**.vue");
export const Layout = () => import( '@/layout/index.vue')
......@@ -48,7 +47,7 @@ export const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) =>
}
export const usePermissionStore = defineStore({
const usePermissionStore = defineStore({
id: "permission",
state: (): PermissionState => ({
routes: [],
......@@ -74,6 +73,6 @@ export const usePermissionStore = defineStore({
}
})
export function usePermissionStoreHook() {
return usePermissionStore(store);
}
export default usePermissionStore;
import {defineStore} from "pinia";
import {store} from "@/store";
import {SettingState} from "@/store/interface";
import defaultSettings from '../../settings'
import {localStorage} from "@/utils/storage";
......@@ -43,7 +42,4 @@ export const useSettingStore = defineStore({
}
})
export function useSettingStoreHook() {
return useSettingStore(store);
}
export default useSettingStore;
import {defineStore} from "pinia";
import {store} from "@/store";
import {TagsViewState} from "@/store/interface";
import { defineStore } from "pinia";
import { TagsViewState } from "@/store/interface";
const useTagsViewStore = defineStore({
id: "tagsView",
......@@ -112,7 +111,7 @@ const useTagsViewStore = defineStore({
return true
}
const cacheIndex = this.cachedViews.indexOf(item.name as string)
const cacheIndex = this.cachedViews.indexOf(item.name as string)
if (cacheIndex > -1) {
this.cachedViews.splice(cacheIndex, 1)
}
......@@ -173,8 +172,4 @@ const useTagsViewStore = defineStore({
}
})
export function useTagsViewStoreHook() {
return useTagsViewStore(store);
}
export default useTagsViewStore;
import { defineStore } from "pinia";
import { store } from "@/store";
import {UserState} from "@/store/interface";
import {localStorage} from "@/utils/storage";
import {getUserInfo, login, logout} from "@/api/login";
import {resetRouter} from "@/router";
const getDefaultState = () => {
return {
token: localStorage.get('token'),
nickname: '',
avatar: '',
roles: [],
perms: []
}
}
export const useUserStore = defineStore({
const useUserStore = defineStore({
id:"user",
state: ():UserState=>({
token: localStorage.get('token') || '',
......@@ -113,7 +102,4 @@ export const useUserStore = defineStore({
}
})
export function useUserStoreHook() {
return useUserStore(store);
}
export default useUserStore;
\ No newline at end of file
import axios from "axios";
import {ElMessage, ElMessageBox} from "element-plus";
import {localStorage} from "@/utils/storage";
import {useUserStoreHook} from "@/store/modules/user";
import { ElMessage, ElMessageBox } from "element-plus";
import { localStorage } from "@/utils/storage";
import useStore from "@/store";
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 50000,
headers: {'Content-Type': 'application/json;charset=utf-8'}
headers: { 'Content-Type': 'application/json;charset=utf-8' }
})
// 请求拦截器
......@@ -16,7 +18,8 @@ service.interceptors.request.use(
if (!config?.headers) {
throw new Error(`Expected 'config' and 'config.headers' not to be undefined`);
}
if (useUserStoreHook().token) {
const { user } = useStore()
if (user.token) {
config.headers.Authorization = `${localStorage.get('token')}`;
}
return config
......@@ -27,8 +30,8 @@ service.interceptors.request.use(
// 响应拦截器
service.interceptors.response.use(
({data}) => {
const {code, msg} = data;
({ data }) => {
const { code, msg } = data;
if (code === '00000') {
return data;
} else {
......@@ -40,7 +43,7 @@ service.interceptors.response.use(
}
},
(error) => {
const {code, msg} = error.response.data
const { code, msg } = error.response.data
if (code === 'A0230') { // token 过期
localStorage.clear(); // 清除浏览器全部缓存
window.location.href = '/'; // 跳转登录页
......
<template>
<div class="dashboard-container">
<github-corner class="github-corner"/>
<github-corner class="github-corner" />
<!-- 数据 -->
<el-row :gutter="40" class="card-panel-col">
<!-- <el-col :xs="24" :lg="6" class="card-panel-col">
<!-- <el-col :xs="24" :lg="6" class="card-panel-col">
<div class="card-panel">
<div class="card-panel-icon-wrapper" style="margin-top: -10px">
<el-image style="width:200px; height: 100px"
......@@ -22,15 +22,13 @@
</div>
</el-col>-->
<el-col :xs="24" :sm="12" :lg="6" class="card-panel-col">
<el-col :xs="24" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel">
<div class="card-panel-icon-wrapper icon-people">
<svg-icon icon-class="peoples" class-name="card-panel-icon"/>
<svg-icon icon-class="peoples" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
访问数
</div>
<div class="card-panel-text">访问数</div>
<div class="card-panel-num">1000</div>
</div>
</div>
......@@ -42,9 +40,7 @@
<svg-icon icon-class="message" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
消息数
</div>
<div class="card-panel-text">消息数</div>
<div class="card-panel-num">1000</div>
</div>
</div>
......@@ -53,25 +49,21 @@
<el-col :xs="24" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel">
<div class="card-panel-icon-wrapper icon-money">
<svg-icon icon-class="money" class-name="card-panel-icon"/>
<svg-icon icon-class="money" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
支付金额
</div>
<div class="card-panel-text">支付金额</div>
<div class="card-panel-num">1000</div>
</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :lg="6" class="card-panel-col">
<el-col :xs="24" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel">
<div class="card-panel-icon-wrapper icon-shopping">
<svg-icon icon-class="shopping" class-name="card-panel-icon"/>
<svg-icon icon-class="shopping" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
订单数
</div>
<div class="card-panel-text">订单数</div>
<div class="card-panel-num">1000</div>
</div>
</div>
......@@ -80,47 +72,65 @@
<!-- 项目 + 团队成员介绍 -->
<el-row :gutter="40">
<!-- 项目介绍 -->
<el-col :md="12" :lg="12" class="card-panel-col">
<Project/>
<Project />
</el-col>
<!-- 团队介绍 -->
<el-col :md="12" :lg="12" class="card-panel-col">
<Team/>
<Team />
</el-col>
</el-row>
<!-- Echarts 图表 -->
<el-row :gutter="40" style="margin-top: 20px">
<el-col :sm="24" :lg="8" class="card-panel-col">
<BarChart id="barChart" height="400px" width="100%" class="chart-container"/>
<BarChart
id="barChart"
height="400px"
width="100%"
class="chart-container"
/>
</el-col>
<el-col :xs="24" :sm="12" :lg="8" class="card-panel-col">
<PieChart id="pieChart" height="400px" width="100%" class="chart-container"/>
<PieChart
id="pieChart"
height="400px"
width="100%"
class="chart-container"
/>
<!--订单漏斗图-->
<!--<FunnelChart id="funnelChart" height="400px" width="100%" class="chart-container"/>-->
</el-col>
<el-col :xs="24" :sm="12" :lg="8" class="card-panel-col">
<RadarChart id="radarChart" height="400px" width="100%" class="chart-container"/>
<RadarChart
id="radarChart"
height="400px"
width="100%"
class="chart-container"
/>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
// Vue引用
import {computed, nextTick, onMounted, reactive, toRefs, watchEffect} from "vue";
import {
computed,
nextTick,
onMounted,
reactive,
toRefs,
watchEffect,
} from "vue";
// 组件引用
import GithubCorner from '@/components/GithubCorner/index.vue'
import SvgIcon from '@/components/SvgIcon/index.vue'
import GithubCorner from "@/components/GithubCorner/index.vue";
import SvgIcon from "@/components/SvgIcon/index.vue";
import BarChart from "./components/Chart/BarChart.vue";
import PieChart from "./components/Chart/PieChart.vue";
import RadarChart from "./components/Chart/RadarChart.vue";
......@@ -129,20 +139,19 @@ import FunnelChart from "./components/Chart/FunnelChart.vue";
import Project from "./components/Project/index.vue";
import Team from "./components/Team/index.vue";
import BScroll from 'better-scroll'
import {useUserStoreHook} from "@/store/modules/user"
import BScroll from "better-scroll";
const roles = computed(() => useUserStoreHook().roles);
const avatar = computed(() => useUserStoreHook().avatar);
const nickname = computed(() => useUserStoreHook().nickname);
import useStore from "@/store";
const { user } = useStore();
const roles = computed(() => user.roles);
const avatar = computed(() => user.avatar);
const nickname = computed(() => user.nickname);
</script>
<style lang="scss" scoped>
.dashboard-container {
padding: 24px;
background-color: rgb(240, 242, 245);
......@@ -200,8 +209,8 @@ const nickname = computed(() => useUserStoreHook().nickname);
overflow: hidden;
color: #666;
background: #fff;
box-shadow: 4px 4px 40px rgba(0, 0, 0, .05);
border-color: rgba(0, 0, 0, .05);
box-shadow: 4px 4px 40px rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.05);
&:hover {
.card-panel-icon-wrapper {
......@@ -225,7 +234,7 @@ const nickname = computed(() => useUserStoreHook().nickname);
}
.icon-shopping {
background: #34bfa3
background: #34bfa3;
}
}
......@@ -291,12 +300,8 @@ const nickname = computed(() => useUserStoreHook().nickname);
}
}
.chart-container {
background: #ffffff;
}
}
</style>
<template>
<div class="login-container">
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
class="login-form"
auto-complete="on"
label-position="left"
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
class="login-form"
auto-complete="on"
label-position="left"
>
<div class="title-container">
<h3 class="title">{{ $t('login.title') }}</h3>
<lang-select class="set-language"/>
<h3 class="title">{{ $t("login.title") }}</h3>
<lang-select class="set-language" />
</div>
<el-form-item prop="username">
<span class="svg-container">
<svg-icon icon-class="user"/>
<svg-icon icon-class="user" />
</span>
<el-input
ref="username"
v-model="loginForm.username"
:placeholder="$t('login.username')"
name="username"
type="text"
tabindex="1"
auto-complete="on"
ref="username"
v-model="loginForm.username"
:placeholder="$t('login.username')"
name="username"
type="text"
tabindex="1"
auto-complete="on"
/>
</el-form-item>
<el-tooltip
:disabled="capslockTooltipDisabled"
content="Caps lock is On"
placement="right"
:disabled="capslockTooltipDisabled"
content="Caps lock is On"
placement="right"
>
<el-form-item prop="password">
<span class="svg-container">
<svg-icon icon-class="password"/>
</span>
<span class="svg-container">
<svg-icon icon-class="password" />
</span>
<el-input
:key="passwordType"
ref="passwordRef"
v-model="loginForm.password"
:type="passwordType"
placeholder="Password"
name="password"
tabindex="2"
auto-complete="on"
@keyup.native="checkCapslock"
@blur="capslockTooltipDisabled = true"
@keyup.enter.native="handleLogin"
:key="passwordType"
ref="passwordRef"
v-model="loginForm.password"
:type="passwordType"
placeholder="Password"
name="password"
tabindex="2"
auto-complete="on"
@keyup.native="checkCapslock"
@blur="capslockTooltipDisabled = true"
@keyup.enter.native="handleLogin"
/>
<span class="show-pwd" @click="showPwd">
<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'"/>
</span>
<svg-icon
:icon-class="passwordType === 'password' ? 'eye' : 'eye-open'"
/>
</span>
</el-form-item>
</el-tooltip>
<!-- 验证码 -->
<el-form-item prop="code">
<span class="svg-container">
<svg-icon icon-class="validCode"/>
</span>
<span class="svg-container">
<svg-icon icon-class="validCode" />
</span>
<el-input
v-model="loginForm.code"
auto-complete="off"
:placeholder="$t('login.code')"
style="width: 65%"
@keyup.enter.native="handleLogin"
v-model="loginForm.code"
auto-complete="off"
:placeholder="$t('login.code')"
style="width: 65%"
@keyup.enter.native="handleLogin"
/>
<div class="captcha">
<img :src="captchaBase64" @click="handleCaptchaGenerate" height="38px"/>
<img
:src="captchaBase64"
@click="handleCaptchaGenerate"
height="38px"
/>
</div>
</el-form-item>
<el-button size="default" :loading="loading" type="primary" style="width:100%;margin-bottom:30px;"
@click.native.prevent="handleLogin">{{ $t('login.login') }}
<el-button
size="default"
:loading="loading"
type="primary"
style="width: 100%; margin-bottom: 30px"
@click.native.prevent="handleLogin"
>{{ $t("login.login") }}
</el-button>
<div class="tips">
<span style="margin-right:20px;">{{ $t('login.username') }}: admin</span>
<span> {{ $t('login.password') }}: 123456</span>
<span style="margin-right: 20px"
>{{ $t("login.username") }}: admin</span
>
<span> {{ $t("login.password") }}: 123456</span>
</div>
</el-form>
<div v-if="showCopyright==true" class="copyright">
<p>{{ $t('login.copyright') }}</p>
<p>{{ $t('login.icp') }}</p>
<div v-if="showCopyright == true" class="copyright">
<p>{{ $t("login.copyright") }}</p>
<p>{{ $t("login.icp") }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import {onMounted, reactive, ref, toRefs, watch, nextTick} from "vue";
import { onMounted, reactive, ref, toRefs, watch, nextTick } from "vue";
// 组件依赖
import {ElForm, ElInput} from "element-plus";
import router from '@/router'
import LangSelect from '@/components/LangSelect/index.vue';
import SvgIcon from '@/components/SvgIcon/index.vue';
import { ElForm, ElInput } from "element-plus";
import router from "@/router";
import LangSelect from "@/components/LangSelect/index.vue";
import SvgIcon from "@/components/SvgIcon/index.vue";
// 状态管理依赖
import {useUserStoreHook} from "@/store/modules/user";
import {useAppStoreHook} from "@/store/modules/app";
import useStore from "@/store";
// API依赖
import {getCaptcha} from "@/api/login";
import {useRoute} from "vue-router";
import { getCaptcha } from "@/api/login";
import { useRoute } from "vue-router";
const { user } = useStore();
const route = useRoute();
const loginFormRef = ref(ElForm)
const passwordRef = ref(ElInput)
const loginFormRef = ref(ElForm);
const passwordRef = ref(ElInput);
const state = reactive({
loginForm: {
username: 'admin',
password: '123456',
code: '',
uuid: ''
username: "admin",
password: "123456",
code: "",
uuid: "",
},
loginRules: {
username: [{required: true, trigger: 'blur'}],
password: [{required: true, trigger: 'blur', validator: validatePassword}]
username: [{ required: true, trigger: "blur" }],
password: [
{ required: true, trigger: "blur", validator: validatePassword },
],
},
loading: false,
passwordType: 'password',
redirect: '',
captchaBase64: '',
passwordType: "password",
redirect: "",
captchaBase64: "",
// 大写提示禁用
capslockTooltipDisabled: true,
otherQuery: {},
clientHeight: document.documentElement.clientHeight,
showCopyright: true
})
showCopyright: true,
});
function validatePassword(rule: any, value: any, callback: any) {
if (value.length < 6) {
callback(new Error('The password can not be less than 6 digits'))
callback(new Error("The password can not be less than 6 digits"));
} else {
callback()
callback();
}
}
......@@ -151,83 +166,88 @@ const {
redirect,
captchaBase64,
capslockTooltipDisabled,
showCopyright
} = toRefs(state)
showCopyright,
} = toRefs(state);
function checkCapslock(e: any) {
const {key} = e
state.capslockTooltipDisabled = key && key.length === 1 && (key >= 'A' && key <= 'Z')
const { key } = e;
state.capslockTooltipDisabled =
key && key.length === 1 && key >= "A" && key <= "Z";
}
function showPwd() {
if (state.passwordType === 'password') {
state.passwordType = ''
if (state.passwordType === "password") {
state.passwordType = "";
} else {
state.passwordType = 'password'
state.passwordType = "password";
}
nextTick(() => {
passwordRef.value.focus()
})
passwordRef.value.focus();
});
}
function handleLogin() {
loginFormRef.value.validate((valid: boolean) => {
if (valid) {
state.loading = true
useUserStoreHook().login(state.loginForm).then(() => {
router.push({path: state.redirect || '/', query: state.otherQuery})
state.loading = false
}).catch(() => {
state.loading = false
handleCaptchaGenerate()
})
state.loading = true;
user
.login(state.loginForm)
.then(() => {
router.push({ path: state.redirect || "/", query: state.otherQuery });
state.loading = false;
})
.catch(() => {
state.loading = false;
handleCaptchaGenerate();
});
} else {
return false
return false;
}
})
});
}
// 获取验证码
function handleCaptchaGenerate() {
getCaptcha().then(response => {
const {img, uuid} = response.data
state.captchaBase64 = "data:image/gif;base64," + img
getCaptcha().then((response) => {
const { img, uuid } = response.data;
state.captchaBase64 = "data:image/gif;base64," + img;
state.loginForm.uuid = uuid;
})
});
}
watch(route, () => {
const query = route.query
if (query) {
state.redirect = query.redirect as string
state.otherQuery = getOtherQuery(query)
}
},
{
immediate: true
watch(
route,
() => {
const query = route.query;
if (query) {
state.redirect = query.redirect as string;
state.otherQuery = getOtherQuery(query);
}
)
},
{
immediate: true,
}
);
function getOtherQuery(query: any) {
return Object.keys(query).reduce((acc: any, cur: any) => {
if (cur !== 'redirect') {
acc[cur] = query[cur]
if (cur !== "redirect") {
acc[cur] = query[cur];
}
return acc
}, {})
return acc;
}, {});
}
onMounted(() => {
handleCaptchaGenerate()
handleCaptchaGenerate();
window.onresize = () => {
if (state.clientHeight > document.documentElement.clientHeight) {
state.showCopyright = false
state.showCopyright = false;
} else {
state.showCopyright = true
state.showCopyright = true;
}
}
})
};
});
</script>
<style lang="scss">
......@@ -240,7 +260,6 @@ $cursor: #fff;
/* reset element-ui css */
.login-container {
.title-container {
position: relative;
......@@ -262,7 +281,6 @@ $cursor: #fff;
}
}
.el-input {
display: inline-block;
height: 47px;
......@@ -285,12 +303,12 @@ $cursor: #fff;
}
}
.el-input__inner{
&:hover{
border-color: var(--el-input-hover-border,var(--el-border-color-hover));
box-shadow:none;
.el-input__inner {
&:hover {
border-color: var(--el-input-hover-border, var(--el-border-color-hover));
box-shadow: none;
}
box-shadow:none;
box-shadow: none;
}
.el-form-item {
......@@ -384,6 +402,5 @@ $light_gray: #eee;
vertical-align: middle;
}
}
}
</style>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册