mirror of
https://gitee.com/niucloud-team/niucloud.git
synced 2026-01-26 04:28:10 +00:00
update
This commit is contained in:
parent
d7fa4d55d9
commit
14585292f9
@ -21,6 +21,7 @@ const systemStore = useSystemStore()
|
||||
const locale = computed(() => (systemStore.lang === 'zh-cn' ? zhCn : en))
|
||||
// 查询website信息
|
||||
systemStore.getWebsiteInfo()
|
||||
systemStore.getWebsiteLayout()
|
||||
|
||||
const toggleDark = useToggle(useDark())
|
||||
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
"cancel": "取消",
|
||||
"search": "搜索",
|
||||
"reset": "重置",
|
||||
"export": "导出列表",
|
||||
"exportPlaceholder": "确定要导出数据吗?",
|
||||
"exportTip": "批量导出数据",
|
||||
"exportConfirm": "确定并导出",
|
||||
"warning": "提示",
|
||||
"isShow": "是否显示",
|
||||
"show": "显示",
|
||||
@ -147,7 +151,9 @@
|
||||
"localBuild": "手动编译",
|
||||
"cloudBuild": "云编译",
|
||||
"showDialogCloseTips": "升级任务尚未完成,关闭将取消升级,是否要继续关闭?",
|
||||
"upgradeCompleteTips": "升级完成后还需要编译admin wap web端可选择云编译或者是手动编译"
|
||||
"upgradeCompleteTips": "升级完成后还需要编译admin wap web端可选择云编译或者是手动编译",
|
||||
"upgradeTips": "应用和插件升级时,系统会自动备份当前程序及数据库。升级功能不会造成您当前程序的损坏或者数据的丢失,请放心使用,但是升级过程可能会因为兼容性等各种原因出现意外的升级错误,当出现错误时,请参考链接<a href='https://www.kancloud.cn/niushop/niushop_v6/3228611' target='_blank' class='text-primary'> https://www.kancloud.cn/niushop/niushop_v6/3228611 </a>手动回退上一版本!",
|
||||
"knownToKnow": "我已知晓,不需要再次提示"
|
||||
},
|
||||
"cloudbuild": {
|
||||
"title": "云编译",
|
||||
|
||||
@ -2,11 +2,13 @@
|
||||
<div :class="['layout-aside ease-in duration-200 flex h-full', { 'bright': !dark }]">
|
||||
<div class="flex flex-col h-full border-0 border-r-[1px] border-solid border-[#eee] box-border bg-[#f5f6f8]">
|
||||
<div class="w-full h-[64px] flex justify-center items-center w-[65px]flex-shrink-0">
|
||||
<el-image style="width: 40px; height: 40px" :src="img(logoUrl)" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-[40px]"><img class="max-w-[40px]" src="@/app/assets/images/site_login_logo.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div class="w-[40px] h-[40px] rounded-[50%] overflow-hidden">
|
||||
<el-image style="width: 100%; height: 100%" :src="img(logoUrl)" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-[40px]"><img class="max-w-[40px]" src="@/app/assets/images/site_login_logo.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
<el-scrollbar class="flex-1 w-[65px] one-menu">
|
||||
<div class="flex flex-col items-center">
|
||||
@ -180,4 +182,20 @@ watch(route, () => {
|
||||
font-size: var(--el-font-size-base);
|
||||
}
|
||||
}
|
||||
|
||||
.layout-aside .el-scrollbar__wrap--hidden-default, .layout-aside .el-scrollbar{
|
||||
overflow: inherit !important;
|
||||
}
|
||||
.layout-aside .menu-item.is-active{
|
||||
position: relative;
|
||||
&:after{
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: var(--el-color-primary);
|
||||
right: -1px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
<el-container class="w-100" :class="[{ 'sidebar-dark-mode': systemStore.sidebar == 'twoType' }, { 'sidebar-brightness-mode': systemStore.sidebar == 'oneType' }]">
|
||||
<el-main class="menu-wrap">
|
||||
<el-scrollbar>
|
||||
<el-menu :default-active="menuActive" :router="true" class="aside-menu h-full" unique-opened="true" :collapse="systemStore.menuIsCollapse">
|
||||
<menu-item v-for="(route, index) in userStore.routers[0].children" :routes="route" :route-path="'setting/'+ route.path" :key="index" />
|
||||
<el-menu :default-active="menuActive" :router="true" class="aside-menu h-full" :unique-opened="true" :collapse="systemStore.menuIsCollapse">
|
||||
<menu-item v-for="(route, index) in userStore.routers[0].children" :routes="route" :key="index" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dialog v-model="changePasswordDialog" width="450px" title="修改密码" :before-close="handleClose">
|
||||
<el-dialog v-model="changePasswordDialog" width="450px" title="修改密码">
|
||||
<div>
|
||||
<el-form :model="saveInfo" label-width="90px" ref="formRef" :rules="formRules" class="page-form">
|
||||
<el-form-item :label="t('originalPassword')" prop="original_password">
|
||||
|
||||
34
admin/src/layout/bussiness/components/aside/index.vue
Normal file
34
admin/src/layout/bussiness/components/aside/index.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<el-aside class="h-screen layout-aside w-auto">
|
||||
<side class="hidden-xs-only" />
|
||||
</el-aside>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import side from './side.vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const route = useRoute()
|
||||
watch(route, () => {
|
||||
systemStore.$patch(state => {
|
||||
state.menuDrawer = false
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.layout-aside {
|
||||
background-color: var(--side-dark-color, var(--el-bg-color));
|
||||
border-right: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.aside-drawer {
|
||||
.el-drawer__body {
|
||||
padding: 0px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
67
admin/src/layout/bussiness/components/aside/menu-item.vue
Normal file
67
admin/src/layout/bussiness/components/aside/menu-item.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<template v-if="meta.show">
|
||||
<el-sub-menu v-if="routes.children" :index="String(routes.name)">
|
||||
<template #title>
|
||||
<span :class="['ml-[10px]']">{{ meta.title }}</span>
|
||||
</template>
|
||||
<menu-item v-for="(route, index) in routes.children" :routes="route" :key="index" />
|
||||
</el-sub-menu>
|
||||
<template v-else>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-if="meta.addon && meta.parent_route && meta.parent_route.addon == ''">
|
||||
<template #title>
|
||||
<el-tooltip placement="right" effect="light">
|
||||
<template #content>
|
||||
该功能仅限{{ addons[meta.addon].title }}使用
|
||||
</template>
|
||||
<span :class="[{'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}, {'ml-[10px]': routes.meta.class == 2, 'ml-[15px]': routes.meta.class == 3}]">{{ meta.title }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-else>
|
||||
<template #title>
|
||||
<span :class="[{'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}, {'ml-[10px]': routes.meta.class == 2, 'ml-[15px]': routes.meta.class == 3}]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
<div v-if="routes.is_border" class="!border-0 !border-t-[1px] border-solid mx-[25px] bg-[#f7f7f7] my-[5px]"></div>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
import { img } from '@/utils/common'
|
||||
import menuItem from './menu-item.vue'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
|
||||
const router = useRouter()
|
||||
const props = defineProps({
|
||||
routes: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const userStore = useUserStore()
|
||||
const siteInfo = userStore.siteInfo
|
||||
const meta = computed(() => props.routes.meta)
|
||||
|
||||
const addons = computed(() => {
|
||||
const addons:Record<string, any> = {}
|
||||
siteInfo?.apps.forEach((item: any) => { addons[item.key] = item })
|
||||
siteInfo?.site_addons.forEach((item: any) => { addons[item.key] = item })
|
||||
return addons
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-sub-menu{
|
||||
.el-icon{
|
||||
width: auto;
|
||||
}
|
||||
li{
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
242
admin/src/layout/bussiness/components/aside/side.vue
Normal file
242
admin/src/layout/bussiness/components/aside/side.vue
Normal file
@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<el-container class="w-100 h-screen">
|
||||
<el-main class="p-0 flex">
|
||||
<div class="w-[64px] bg-[#282c34] h-screen one-menu">
|
||||
<el-header class="logo-wrap">
|
||||
<div class="logo flex items-center m-auto h-[64px]" v-if="!systemStore.menuIsCollapse">
|
||||
<el-image style="width: 40px; height: 40px" :src="img(logoUrl)" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-[40px]"><img class="max-w-[40px]" src="@/app/assets/images/icon-addon.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="logo flex items-center justify-center h-[64px]" v-else>
|
||||
<i class="text-3xl iconfont iconyunkongjian"></i>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-scrollbar class="h-[calc( 100vh - 64px )]">
|
||||
<el-menu :default-active="oneMenuActive" :router="true" class="aside-menu" :unique-opened="true">
|
||||
<template v-for="(item, index) in oneMenuData" :key="index">
|
||||
<el-menu-item :index="item.original_name" @click="router.push({ name: item.name })" v-if="item.meta.show">
|
||||
<div v-if="item.meta.icon" class="w-[16px] h-[16px] relative flex justify-center">
|
||||
<icon :name="item.meta.icon" class="absolute top-[50%] -translate-y-[50%]" />
|
||||
</div>
|
||||
<template #title>
|
||||
<div class="relative">
|
||||
<span class="ml-[10px] text-[15px]">{{ item.meta.short_title || item.meta.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<el-scrollbar v-if="twoMenuData.length" class="two-menu w-[190px]">
|
||||
<div class="w-[190px] h-[64px] flex items-center justify-center text-[16px] border-0 border-b-[1px] border-solid border-[#eee]">{{ route.matched[1].meta.title }}</div>
|
||||
<el-menu :default-active="route.name" :router="true" class="aside-menu" :collapse="systemStore.menuIsCollapse">
|
||||
<menu-item v-for="(route, index) in twoMenuData" :routes="route" :key="index" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch,computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import menuItem from './menu-item.vue'
|
||||
import { img, isUrl } from '@/utils/common'
|
||||
import { findFirstValidRoute } from '@/router/routers'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const siteInfo = userStore.siteInfo
|
||||
const routers = userStore.routers
|
||||
const addonIndexRoute = userStore.addonIndexRoute
|
||||
|
||||
const oneMenuData = ref<Record<string, any>[]>([])
|
||||
const twoMenuData = ref<Record<string, any>[]>([])
|
||||
const addonRouters: Record<string, any> = {}
|
||||
const logoUrl = computed(() => {
|
||||
return userStore.siteInfo.icon ? userStore.siteInfo.icon : systemStore.website.icon
|
||||
})
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon == '') {
|
||||
if (item.children && item.children.length) {
|
||||
item.name = findFirstValidRoute(item.children)
|
||||
}
|
||||
oneMenuData.value.push(item)
|
||||
} else if (item.meta.addon != '' && siteInfo?.apps.length <= 1 && siteInfo?.apps[0].key == item.meta.addon) {
|
||||
if (item.children) {
|
||||
item.children.forEach((citem: Record<string, any>) => {
|
||||
citem.original_name = citem.name
|
||||
if (citem.children && citem.children.length) {
|
||||
citem.name = findFirstValidRoute(citem.children)
|
||||
}
|
||||
})
|
||||
oneMenuData.value.unshift(...item.children)
|
||||
} else {
|
||||
oneMenuData.value.unshift(item)
|
||||
}
|
||||
} else {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
})
|
||||
|
||||
// 多应用时将应用插入菜单
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
const routers:Record<string, any>[] = []
|
||||
siteInfo?.apps.forEach((item: Record<string, any>) => {
|
||||
if (addonRouters[item.key]) {
|
||||
addonRouters[item.key].name = addonIndexRoute[item.key]
|
||||
routers.push(addonRouters[item.key])
|
||||
}
|
||||
})
|
||||
oneMenuData.value.unshift(...routers)
|
||||
}
|
||||
|
||||
const oneMenuActive = ref(route.matched[1].name)
|
||||
|
||||
watch(route, () => {
|
||||
// 多应用
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
twoMenuData.value = route.matched[1].children
|
||||
oneMenuActive.value = route.matched[1].name
|
||||
} else {
|
||||
// 单应用
|
||||
if (route.meta.addon == '') {
|
||||
oneMenuActive.value = route.matched[1].name
|
||||
twoMenuData.value = route.matched[1].children ?? []
|
||||
} else if (route.meta.addon && route.meta.addon != siteInfo?.apps[0].key) {
|
||||
oneMenuActive.value = '/site/app'
|
||||
twoMenuData.value = route.matched[1].children ?? []
|
||||
} else {
|
||||
oneMenuActive.value = route.matched[2].name
|
||||
twoMenuData.value = route.matched[2].children ?? []
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.one-menu{
|
||||
.aside-menu:not(.el-menu--collapse) {
|
||||
background-color: transparent;
|
||||
.el-menu-item{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 5px;
|
||||
height: 54px;
|
||||
width: 54px;
|
||||
color: #b9b9bf;
|
||||
font-size: 14px;
|
||||
border-radius: 5px;
|
||||
line-height: 1;
|
||||
&>div{
|
||||
i{
|
||||
position: static;
|
||||
transform: inherit;
|
||||
}
|
||||
span{
|
||||
display: block;
|
||||
margin: 0;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
&:hover{
|
||||
background-color: transparent;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
&.is-active{
|
||||
background-color: var(--el-color-primary) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
span{
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-menu{
|
||||
border: 0;
|
||||
}
|
||||
.el-scrollbar{
|
||||
height: calc(100vh - 65px);
|
||||
}
|
||||
}
|
||||
.two-menu{
|
||||
.aside-menu:not(.el-menu--collapse) {
|
||||
width: 190px;
|
||||
border: 0;
|
||||
padding-top: 10px;
|
||||
.el-menu-item{
|
||||
height: 40px;
|
||||
margin: 0 8px 2px;
|
||||
padding: 0 10px !important;
|
||||
border-radius: 2px;
|
||||
span{
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
&.is-active{
|
||||
background-color: #f3f3f3 !important;
|
||||
color: #333;
|
||||
}
|
||||
&:hover{
|
||||
background-color: transparent;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.el-sub-menu{
|
||||
.el-sub-menu__title{
|
||||
margin: 0 8px 2px;
|
||||
height: 40px;
|
||||
padding-left: 8px;
|
||||
border-radius: 2px;
|
||||
span{
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
&:hover{
|
||||
background-color: transparent;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.el-menu-item{
|
||||
padding-left: 20px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-wrap {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
|
||||
.logo {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-size: var(--el-font-size-base);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
186
admin/src/layout/bussiness/components/header/index.vue
Normal file
186
admin/src/layout/bussiness/components/header/index.vue
Normal file
@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<el-container :class="['h-full px-[10px]',{'layout-header border-b border-color': !dark}]" >
|
||||
<el-row class="w-100 h-full w-full">
|
||||
<el-col :span="12">
|
||||
<div class="left-panel h-full flex items-center">
|
||||
<!-- 左侧菜单折叠 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
|
||||
<icon name="element-Expand" v-if="systemStore.menuIsCollapse" />
|
||||
<icon name="element-Fold" v-else />
|
||||
</div> -->
|
||||
<!-- 刷新当前页 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="refreshRouter">
|
||||
<icon name="element-Refresh" />
|
||||
</div>
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="flex items-center h-full pl-[10px] hidden-xs-only">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(route, index) in breadcrumb" :key="index">{{route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="right-panel h-full flex items-center justify-end">
|
||||
<!-- 预览 只有站点时展示-->
|
||||
<i class="iconfont iconicon_huojian1 cursor-pointer px-[8px]" :title="t('visitWap')" @click="toPreview"></i>
|
||||
<i class="iconfont iconlingdang-xianxing cursor-pointer px-[8px]" :title="t('newInfo')" v-if="appType == 'site'"></i>
|
||||
<!-- 切换语言 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<switch-lang />
|
||||
</div> -->
|
||||
<!-- 切换全屏 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleFullscreen">
|
||||
<icon name="iconfont-icontuichuquanping" v-if="isFullscreen" />
|
||||
<icon name="iconfont-iconquanping" v-else />
|
||||
</div> -->
|
||||
<!-- 布局设置 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<layout-setting />
|
||||
</div>
|
||||
<!-- 用户信息 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<user-info />
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<input type="hidden" v-model="comparisonToken">
|
||||
<input type="hidden" v-model="comparisonSiteId">
|
||||
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
|
||||
<span>{{ t('layout.detectionLoginContent') }}</span>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detectionLoginFn">{{ t('layout.detectionLoginOperation') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import layoutSetting from './layout-setting.vue'
|
||||
import userInfo from './user-info.vue'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import { useRoute,useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const appType = storage.get('app_type')
|
||||
const { toggle: toggleFullscreen, isFullscreen } = useFullscreen()
|
||||
const systemStore = useSystemStore()
|
||||
const appStore = useAppStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const screenWidth = ref(window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth)
|
||||
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
|
||||
// 检测登录 start
|
||||
const detectionLoginDialog = ref(false)
|
||||
const comparisonToken = ref('')
|
||||
const comparisonSiteId = ref('')
|
||||
if (storage.get('comparisonTokenStorage')) {
|
||||
comparisonToken.value = storage.get('comparisonTokenStorage')
|
||||
// storage.remove(['comparisonTokenStorage']);
|
||||
}
|
||||
if (storage.get('comparisonSiteIdStorage')) {
|
||||
comparisonSiteId.value = storage.get('comparisonSiteIdStorage')
|
||||
// storage.remove(['comparisonSiteIdStorage']);
|
||||
}
|
||||
// 监听标签页面切换
|
||||
document.addEventListener('visibilitychange', e => {
|
||||
if (document.visibilityState === 'visible' && (comparisonSiteId.value != storage.get('siteId') || comparisonToken.value != storage.get('token'))) {
|
||||
detectionLoginDialog.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const detectionLoginFn = () => {
|
||||
detectionLoginDialog.value = false
|
||||
location.href = `${location.origin}/site/`
|
||||
}
|
||||
// 检测登录 end
|
||||
|
||||
onMounted(() => {
|
||||
// 监听窗体宽度变化
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
screenWidth.value = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
|
||||
})()
|
||||
}
|
||||
})
|
||||
|
||||
// watch(screenWidth, () => {
|
||||
// if (screenWidth.value < 992) {
|
||||
// if (!systemStore.menuIsCollapse) systemStore.toggleMenuCollapse(true)
|
||||
// } else {
|
||||
// if (systemStore.menuIsCollapse) systemStore.toggleMenuCollapse(false)
|
||||
// }
|
||||
// })
|
||||
|
||||
// 菜单栏展开折叠
|
||||
// const toggleMenuCollapse = () => {
|
||||
// systemStore.$patch((state) => {
|
||||
// if (screenWidth.value < 768) {
|
||||
// state.menuDrawer = true
|
||||
// state.menuIsCollapse = false
|
||||
// } else {
|
||||
// systemStore.toggleMenuCollapse(!systemStore.menuIsCollapse)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// 刷新路由
|
||||
const refreshRouter = () => {
|
||||
if (!appStore.routeRefreshTag) return
|
||||
appStore.refreshRouterView()
|
||||
}
|
||||
|
||||
// 面包屑导航
|
||||
const breadcrumb = computed(() => {
|
||||
const matched = route.matched.filter(item => { return item.meta.title })
|
||||
if (matched[0] && matched[0].path == '/') matched.splice(0, 1)
|
||||
return matched
|
||||
})
|
||||
|
||||
// 跳转去预览
|
||||
const toPreview = () => {
|
||||
const url = router.resolve({
|
||||
path: '/preview/wap',
|
||||
query: {
|
||||
page:'/'
|
||||
}
|
||||
})
|
||||
window.open(url.href)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-header{
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
box-shadow: 0px 0px 4px 0px rgba(0,145,255,0.1);
|
||||
}
|
||||
.navbar-item {
|
||||
padding: 0 8px;
|
||||
&:hover {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
}
|
||||
.index-item {
|
||||
border: 1px solid;
|
||||
border-color: var(--el-color-primary);
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<icon name="element-Setting" @click="drawer = true" />
|
||||
|
||||
<el-drawer v-model="drawer" :title="t('layout.layoutSetting')" size="300px">
|
||||
<el-scrollbar>
|
||||
<!-- 黑暗模式 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.darkMode') }}</div>
|
||||
<div>
|
||||
<el-switch v-model="dark" :active-value="true" :inactive-value="false" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主题颜色 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.themeColor') }}</div>
|
||||
<div>
|
||||
<el-color-picker v-model="theme" />
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import { setThemeColor } from '@/utils/common'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const drawer = ref(false)
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const isDark = useDark()
|
||||
const toggleDark = useToggle(isDark)
|
||||
|
||||
const dark = computed({
|
||||
get () {
|
||||
return systemStore.dark
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('dark', val)
|
||||
toggleDark(val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const theme = computed({
|
||||
get () {
|
||||
return systemStore.theme
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('theme', val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.layout-style {
|
||||
&>div:nth-child(2n+2) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
32
admin/src/layout/bussiness/components/header/switch-lang.vue
Normal file
32
admin/src/layout/bussiness/components/header/switch-lang.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-dropdown @command="switchLang" :tabindex="1">
|
||||
<icon name="iconfont-iconfanyi" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="zh-cn" :disabled="systemStore.lang == 'zh-cn'">简体中文</el-dropdown-item>
|
||||
<el-dropdown-item command="en" :disabled="systemStore.lang == 'en'">English</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { language } from '@/lang'
|
||||
import { useRoute } from 'vue-router'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const route = useRoute()
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const switchLang = (command: string) => {
|
||||
systemStore.$patch((state) => {
|
||||
state.lang = command
|
||||
storage.set({ key: 'lang', data: command })
|
||||
})
|
||||
language.loadLocaleMessages(route.path, systemStore.lang)
|
||||
location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
147
admin/src/layout/bussiness/components/header/user-info.vue
Normal file
147
admin/src/layout/bussiness/components/header/user-info.vue
Normal file
@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dropdown @command="clickEvent" :tabindex="1">
|
||||
<div class="userinfo flex h-full items-center">
|
||||
<el-avatar :size="25" :icon="UserFilled" />
|
||||
<div class="user-name pl-[8px]">{{ userStore.userInfo.username }}</div>
|
||||
<icon name="element-ArrowDown" class="ml-[5px]" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="toLink('/home/index')">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconqiehuan ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">切换站点</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="getUserInfoFn">
|
||||
<!-- <router-link to="/user/center"> -->
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconshezhi1 ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">账号设置</span>
|
||||
</div>
|
||||
<!-- </router-link> -->
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="changePasswordDialog=true">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconxiugai ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">修改密码</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="logout">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont icontuichudenglu ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">退出登录</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dialog v-model="changePasswordDialog" width="450px" title="修改密码">
|
||||
<div>
|
||||
<el-form :model="saveInfo" label-width="90px" ref="formRef" :rules="formRules" class="page-form">
|
||||
<el-form-item :label="t('originalPassword')" prop="original_password">
|
||||
<el-input v-model="saveInfo.original_password" type="password" :placeholder="t('originalPasswordPlaceholder')" clearable class="input-width" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('newPassword')" prop="password">
|
||||
<el-input v-model="saveInfo.password" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" />
|
||||
<div class="form-tip">{{t('passwordTip')}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('passwordCopy')" prop="password_copy">
|
||||
<el-input v-model="saveInfo.password_copy" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="changePasswordDialog = false">{{t('cancel')}}</el-button>
|
||||
<el-button type="primary" @click="submitForm(formRef)">{{t('save')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<user-info-edit ref="userInfoEditRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { UserFilled } from '@element-plus/icons-vue'
|
||||
import { computed, reactive, ref, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { FormInstance, FormRules, ElNotification } from 'element-plus'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { setUserInfo } from '@/app/api/personal'
|
||||
import { t } from '@/lang'
|
||||
import userInfoEdit from '@/app/components/user-info-edit/index.vue'
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const clickEvent = (command: string) => {
|
||||
switch (command) {
|
||||
case 'logout':
|
||||
userStore.logout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
userStore.logout();
|
||||
}
|
||||
const toLink = (link) => {
|
||||
router.push(link)
|
||||
}
|
||||
const userInfoEditRef = ref(null)
|
||||
const getUserInfoFn = ()=>{
|
||||
userInfoEditRef.value?.open()
|
||||
}
|
||||
// 修改密码 --- start
|
||||
let changePasswordDialog = ref(false)
|
||||
const formRef = ref<FormInstance>();
|
||||
// 提交信息
|
||||
let saveInfo = reactive({
|
||||
original_password: '',
|
||||
password: '',
|
||||
password_copy: ''
|
||||
});
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
original_password: [
|
||||
{ required: true, message: t("originalPasswordPlaceholder"), trigger: "blur" },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: t("passwordPlaceholder"), trigger: "blur" },
|
||||
],
|
||||
password_copy: [
|
||||
{ required: true, message: t("passwordPlaceholder"), trigger: "blur" },
|
||||
]
|
||||
});
|
||||
const submitForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
let msg = "";
|
||||
if (saveInfo.password && !saveInfo.original_password) msg = t('originalPasswordHint');
|
||||
if (saveInfo.password && saveInfo.original_password && !saveInfo.password_copy) msg = t('newPasswordHint');
|
||||
if (saveInfo.password && saveInfo.original_password && saveInfo.password_copy && saveInfo.password != saveInfo.password_copy) msg = t('doubleCipherHint');
|
||||
if (msg) {
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: msg,
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
setUserInfo(saveInfo).then((res: any) => {
|
||||
changePasswordDialog.value = false;
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
});
|
||||
}
|
||||
// 修改密码 --- end
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-popper .el-dropdown-menu{
|
||||
width: 150px;
|
||||
}
|
||||
</style>
|
||||
137
admin/src/layout/bussiness/components/tabs.vue
Normal file
137
admin/src/layout/bussiness/components/tabs.vue
Normal file
@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div class="tab-wrap w-full px-[16px]">
|
||||
<el-tabs :closable="tabbarStore.tabLength > 1" :model-value="route.path" @tab-click="tabClick"
|
||||
@tab-remove="removeTab">
|
||||
<el-tab-pane v-for="(tab, key, index) in tabbarStore.tabs" :name="tab.path" :key="index">
|
||||
<template #label>
|
||||
<el-dropdown trigger="contextmenu" placement="bottom-start">
|
||||
<span :class="{ 'text-primary': route.path == tab.path }" class="tab-name">{{ tab.title }}</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Back" :disabled="index == 0" @click="closeLeft(tab.path)">{{t('tabs.closeLeft') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Right" :disabled="index == (tabbarStore.tabLength - 1)" @click="closeRight(tab.path)">{{t('tabs.closeRight') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Close" :disabled="tabbarStore.tabLength == 1" @click="closeOther(tab.path)">{{t('tabs.closeOther') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, onMounted } from 'vue'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const tabbarStore = useTabbarStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
tabbarStore.addTab(route)
|
||||
})
|
||||
|
||||
watch(route, (nval: any) => {
|
||||
tabbarStore.addTab(nval)
|
||||
})
|
||||
|
||||
/**
|
||||
* 添加tab
|
||||
* @param content
|
||||
*/
|
||||
const tabClick = (content: any) => {
|
||||
const tabRoute = tabbarStore.tabs[content.props.name]
|
||||
router.push({ path: tabRoute.path, query: tabRoute.query })
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除tab
|
||||
* @param content
|
||||
*/
|
||||
const removeTab = (content: any) => {
|
||||
if (route.path == content) {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
router.push({ path: tabs[tabs.indexOf(content) - 1] })
|
||||
}
|
||||
tabbarStore.removeTab(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭左侧
|
||||
* @param path
|
||||
*/
|
||||
const closeLeft = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(path) - 1; i >= 0; i--) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ path })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭右侧
|
||||
* @param path
|
||||
*/
|
||||
const closeRight = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(path) + 1; i < tabs.length; i++) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ path })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭其他
|
||||
* @param path
|
||||
*/
|
||||
const closeOther = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
tabs.forEach((key: string) => { key != path && delete tabbarStore.tabs[key] })
|
||||
router.push({ path })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tabs) {
|
||||
.el-tabs--border-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap {
|
||||
margin-bottom: 0;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
display: inline-flex !important;
|
||||
padding: 0 20px !important;
|
||||
align-items: center;
|
||||
|
||||
.tab-name:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
44
admin/src/layout/bussiness/index.vue
Normal file
44
admin/src/layout/bussiness/index.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="common-layout min-w-[1200px]" >
|
||||
<el-container class="w-100 h-screen">
|
||||
<layout-aside></layout-aside>
|
||||
|
||||
<el-container>
|
||||
<el-header>
|
||||
<layout-header></layout-header>
|
||||
</el-header>
|
||||
|
||||
<el-main :class="['main-wrap h-full p-0 bg-page']">
|
||||
<el-scrollbar>
|
||||
<div class="p-[10px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
|
||||
<keep-alive :include="tabbarStore.tabNames">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import layoutHeader from './components/header/index.vue'
|
||||
import layoutAside from './components/aside/index.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const tabbarStore = useTabbarStore()
|
||||
const systemStore = useSystemStore()
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
4
admin/src/layout/bussiness/layout.json
Normal file
4
admin/src/layout/bussiness/layout.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"layout": "bussiness",
|
||||
"cover": "/app/assets/images/layout_bussiness.png"
|
||||
}
|
||||
71
admin/src/layout/darkside/components/aside/index.vue
Normal file
71
admin/src/layout/darkside/components/aside/index.vue
Normal file
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<el-aside class="layout-aside dark w-auto">
|
||||
<side class="hidden-xs-only slide" />
|
||||
</el-aside>
|
||||
|
||||
<el-drawer v-model="systemStore.menuDrawer" direction="ltr" :with-header="false" custom-class="aside-drawer" size="210px">
|
||||
<template #default>
|
||||
<side />
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import side from './side.vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
watch(route, () => {
|
||||
systemStore.$patch(state => {
|
||||
state.menuDrawer = false
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.layout-aside {
|
||||
--side-dark-color: #141414;
|
||||
background-color: var(--side-dark-color, var(--el-bg-color));
|
||||
|
||||
&.bright {
|
||||
background-color: #F5F7F9;
|
||||
|
||||
li {
|
||||
background-color: #F5F7F9;
|
||||
|
||||
&.is-active:not(.is-opened) {
|
||||
position: relative;
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
background-color: var(--el-menu-active-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slide {
|
||||
border-right: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
}
|
||||
|
||||
.aside-drawer {
|
||||
.el-drawer__body {
|
||||
padding: 0px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
108
admin/src/layout/darkside/components/aside/menu-item.vue
Normal file
108
admin/src/layout/darkside/components/aside/menu-item.vue
Normal file
@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<template v-if="meta.show">
|
||||
<el-sub-menu v-if="routes.children" :index="String(routes.name)">
|
||||
<template #title>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
</template>
|
||||
<menu-item v-for="(route, index) in routes.children" :routes="route" :key="index" :level="props.level + 1" />
|
||||
<template v-if="routes.name == 'addon_list'">
|
||||
<template v-if="addonsMenus">
|
||||
<menu-item :routes="addonsMenus" :key="index" :level="props.level + 1"/>
|
||||
</template>
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
<template v-else>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-if="meta.addon && meta.parent_route && meta.parent_route.addon == ''">
|
||||
<template #title>
|
||||
<el-tooltip placement="right" effect="light">
|
||||
<template #content>
|
||||
该功能仅限{{ addons[meta.addon].title }}使用
|
||||
</template>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-else>
|
||||
<template #title>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
<div v-if="routes.is_border" class="!border-0 !border-t-[1px] border-solid mx-[25px] bg-[#f7f7f7] my-[5px]"></div>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { img } from '@/utils/common'
|
||||
import menuItem from './menu-item.vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const routers = useUserStore().routers
|
||||
const props = defineProps({
|
||||
routes: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
})
|
||||
const systemStore = useSystemStore()
|
||||
const meta = computed(() => props.routes.meta)
|
||||
|
||||
const addons = computed(() => {
|
||||
const addons:Record<string, any> = {}
|
||||
userStore.siteInfo?.apps.forEach((item: any) => { addons[item.key] = item })
|
||||
userStore.siteInfo?.site_addons.forEach((item: any) => { addons[item.key] = item })
|
||||
return addons
|
||||
})
|
||||
|
||||
const systemAddonKeys = computed(() => {
|
||||
return userStore.siteInfo?.site_addons.map((item: any) => item.key)
|
||||
})
|
||||
|
||||
const addonRouters: Record<string, any> = {}
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon) {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
})
|
||||
|
||||
const addonsMenus = ref(null)
|
||||
|
||||
watch(route, () => {
|
||||
if (props.routes.name != 'addon_list') return
|
||||
|
||||
if (systemAddonKeys.value.includes(route.meta.addon) && addonRouters[route.meta.addon]) {
|
||||
addonsMenus.value = addonRouters[route.meta.addon]
|
||||
} else {
|
||||
addonsMenus.value = null
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-sub-menu{
|
||||
.el-icon{
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
101
admin/src/layout/darkside/components/aside/side.vue
Normal file
101
admin/src/layout/darkside/components/aside/side.vue
Normal file
@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<el-container class="w-[200px] h-screen flex flex-col">
|
||||
<el-header class="logo-wrap flex items-center justify-center h-[64px]">
|
||||
<div class="logo flex items-center m-auto h-[64px]" v-if="!systemStore.menuIsCollapse">
|
||||
<el-image style="width: 40px; height: 40px" :src="img(logoUrl)" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-[40px]"><img class="max-w-[40px]" src="@/app/assets/images/icon-addon.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="logo flex items-center justify-center h-[64px]" v-else>
|
||||
<i class="text-3xl iconfont iconyunkongjian"></i>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main class="menu-wrap">
|
||||
<el-scrollbar>
|
||||
<el-menu :default-active="route.name" :router="true" background-color="--side-dark-color" text-color="#fff" :unique-opened="true" :collapse="systemStore.menuIsCollapse" >
|
||||
<menu-item v-for="(route, index) in menuData" :routes="route" :key="index" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch,computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import menuItem from './menu-item.vue'
|
||||
import { img, isUrl } from '@/utils/common'
|
||||
import { findFirstValidRoute } from '@/router/routers'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const siteInfo = userStore.siteInfo
|
||||
const routers = userStore.routers
|
||||
const addonIndexRoute = userStore.addonIndexRoute
|
||||
const menuData = ref<Record<string, any>[]>([])
|
||||
const addonRouters: Record<string, any> = {}
|
||||
const logoUrl = computed(() => {
|
||||
return userStore.siteInfo.icon ? userStore.siteInfo.icon : systemStore.website.icon
|
||||
})
|
||||
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon == '') {
|
||||
if (item.children && item.children.length) {
|
||||
item.name = findFirstValidRoute(item.children)
|
||||
}
|
||||
menuData.value.push(item)
|
||||
} else if (item.meta.addon != '' && siteInfo?.apps.length == 1 && siteInfo?.apps[0].key == item.meta.addon) {
|
||||
if (item.children) {
|
||||
item.children.forEach((citem: Record<string, any>) => {
|
||||
citem.original_name = citem.name
|
||||
if (citem.children && citem.children.length) {
|
||||
citem.name = findFirstValidRoute(citem.children)
|
||||
}
|
||||
})
|
||||
menuData.value.unshift(...item.children)
|
||||
} else {
|
||||
menuData.value.unshift(item)
|
||||
}
|
||||
} else {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
})
|
||||
|
||||
// 多应用时将应用插入菜单
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
const routers:Record<string, any>[] = []
|
||||
siteInfo?.apps.forEach((item: Record<string, any>) => {
|
||||
if (addonRouters[item.key]) {
|
||||
addonRouters[item.key].name = addonIndexRoute[item.key]
|
||||
routers.push(addonRouters[item.key])
|
||||
}
|
||||
})
|
||||
menuData.value.unshift(...routers)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.menu-wrap {
|
||||
padding: 0!important;
|
||||
|
||||
.el-menu {
|
||||
border-right: 0!important;
|
||||
|
||||
.el-menu-item, .el-sub-menu__title {
|
||||
--el-menu-item-height: 40px;
|
||||
}
|
||||
|
||||
.el-sub-menu .el-menu-item {
|
||||
--el-menu-sub-item-height: 40px;
|
||||
--el-menu-sub-item-height: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
190
admin/src/layout/darkside/components/header/index.vue
Normal file
190
admin/src/layout/darkside/components/header/index.vue
Normal file
@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<el-container :class="['h-full px-[10px]',{'layout-header border-b border-color': !dark}]" >
|
||||
<el-row class="w-100 h-full w-full">
|
||||
<el-col :span="12">
|
||||
<div class="left-panel h-full flex items-center">
|
||||
<!-- 左侧菜单折叠 -->
|
||||
<div class="hidden-sm-and-up navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
|
||||
<icon name="element-Expand" v-if="systemStore.menuIsCollapse" />
|
||||
<icon name="element-Fold" v-else />
|
||||
</div>
|
||||
<!-- 刷新当前页 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="refreshRouter">
|
||||
<icon name="element-Refresh" />
|
||||
</div>
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="flex items-center h-full pl-[10px]">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(route, index) in breadcrumb" :key="index">{{route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="right-panel h-full flex items-center justify-end">
|
||||
<!-- 预览 只有站点时展示-->
|
||||
<i class="iconfont iconicon_huojian1 cursor-pointer px-[8px]" :title="t('visitWap')" @click="toPreview"></i>
|
||||
<i class="iconfont iconlingdang-xianxing cursor-pointer px-[8px]" :title="t('newInfo')" v-if="appType == 'site'"></i>
|
||||
<!-- 切换语言 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<switch-lang />
|
||||
</div> -->
|
||||
<!-- 切换全屏 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleFullscreen">
|
||||
<icon name="iconfont-icontuichuquanping" v-if="isFullscreen" />
|
||||
<icon name="iconfont-iconquanping" v-else />
|
||||
</div> -->
|
||||
<!-- 布局设置 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<layout-setting />
|
||||
</div>
|
||||
<!-- 用户信息 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<user-info />
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<input type="hidden" v-model="comparisonToken">
|
||||
<input type="hidden" v-model="comparisonSiteId">
|
||||
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
|
||||
<span>{{ t('layout.detectionLoginContent') }}</span>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detectionLoginFn">{{ t('layout.detectionLoginOperation') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import layoutSetting from './layout-setting.vue'
|
||||
import userInfo from './user-info.vue'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import { useRoute,useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const appType = storage.get('app_type')
|
||||
const { toggle: toggleFullscreen } = useFullscreen()
|
||||
const systemStore = useSystemStore()
|
||||
const appStore = useAppStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const screenWidth = ref(window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth)
|
||||
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
|
||||
// 检测登录 start
|
||||
const detectionLoginDialog = ref(false)
|
||||
const comparisonToken = ref('')
|
||||
const comparisonSiteId = ref('')
|
||||
if (storage.get('comparisonTokenStorage')) {
|
||||
comparisonToken.value = storage.get('comparisonTokenStorage')
|
||||
// storage.remove(['comparisonTokenStorage']);
|
||||
}
|
||||
if (storage.get('comparisonSiteIdStorage')) {
|
||||
comparisonSiteId.value = storage.get('comparisonSiteIdStorage')
|
||||
// storage.remove(['comparisonSiteIdStorage']);
|
||||
}
|
||||
// 监听标签页面切换
|
||||
document.addEventListener('visibilitychange', e => {
|
||||
if (document.visibilityState === 'visible' && (comparisonSiteId.value != storage.get('siteId') || comparisonToken.value != storage.get('token'))) {
|
||||
detectionLoginDialog.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const detectionLoginFn = () => {
|
||||
detectionLoginDialog.value = false
|
||||
location.href = `${location.origin}/site/`
|
||||
}
|
||||
// 检测登录 end
|
||||
|
||||
onMounted(() => {
|
||||
// 监听窗体宽度变化
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
screenWidth.value = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
|
||||
})()
|
||||
}
|
||||
})
|
||||
|
||||
// watch(screenWidth, () => {
|
||||
// if (screenWidth.value < 992) {
|
||||
// if (!systemStore.menuIsCollapse) systemStore.toggleMenuCollapse(true)
|
||||
// } else {
|
||||
// if (systemStore.menuIsCollapse) systemStore.toggleMenuCollapse(false)
|
||||
// }
|
||||
// })
|
||||
|
||||
// 菜单栏展开折叠
|
||||
const toggleMenuCollapse = () => {
|
||||
systemStore.$patch((state) => {
|
||||
if (screenWidth.value < 768) {
|
||||
state.menuDrawer = true
|
||||
state.menuIsCollapse = false
|
||||
} else {
|
||||
systemStore.toggleMenuCollapse(!systemStore.menuIsCollapse)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新路由
|
||||
const refreshRouter = () => {
|
||||
if (!appStore.routeRefreshTag) return
|
||||
appStore.refreshRouterView()
|
||||
}
|
||||
|
||||
// 面包屑导航
|
||||
const breadcrumb = computed(() => {
|
||||
const matched = route.matched.filter(item => { return item.meta.title })
|
||||
if (matched[0] && matched[0].path == '/') matched.splice(0, 1)
|
||||
return matched
|
||||
})
|
||||
|
||||
// 跳转去预览
|
||||
const toPreview = () => {
|
||||
const url = router.resolve({
|
||||
path: '/preview/wap',
|
||||
query: {
|
||||
page:'/'
|
||||
}
|
||||
})
|
||||
window.open(url.href)
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
// const backFn = () => {
|
||||
// router.go(-1)
|
||||
// }
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-header{
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
box-shadow: 0px 0px 4px 0px rgba(0,145,255,0.1);
|
||||
}
|
||||
.navbar-item {
|
||||
padding: 0 8px;
|
||||
&:hover {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
}
|
||||
.index-item {
|
||||
border: 1px solid;
|
||||
border-color: var(--el-color-primary);
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<icon name="element-Setting" @click="drawer = true" />
|
||||
|
||||
<el-drawer v-model="drawer" :title="t('layout.layoutSetting')" size="300px">
|
||||
<el-scrollbar>
|
||||
<!-- 黑暗模式 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.darkMode') }}</div>
|
||||
<div>
|
||||
<el-switch v-model="dark" :active-value="true" :inactive-value="false" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主题颜色 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.themeColor') }}</div>
|
||||
<div>
|
||||
<el-color-picker v-model="theme" />
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import { setThemeColor } from '@/utils/common'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const drawer = ref(false)
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const isDark = useDark()
|
||||
const toggleDark = useToggle(isDark)
|
||||
|
||||
const dark = computed({
|
||||
get () {
|
||||
return systemStore.dark
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('dark', val)
|
||||
toggleDark(val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const theme = computed({
|
||||
get () {
|
||||
return systemStore.theme
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('theme', val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.layout-style {
|
||||
&>div:nth-child(2n+2) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
32
admin/src/layout/darkside/components/header/switch-lang.vue
Normal file
32
admin/src/layout/darkside/components/header/switch-lang.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-dropdown @command="switchLang" :tabindex="1">
|
||||
<icon name="iconfont-iconfanyi" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="zh-cn" :disabled="systemStore.lang == 'zh-cn'">简体中文</el-dropdown-item>
|
||||
<el-dropdown-item command="en" :disabled="systemStore.lang == 'en'">English</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { language } from '@/lang'
|
||||
import { useRoute } from 'vue-router'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const route = useRoute()
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const switchLang = (command: string) => {
|
||||
systemStore.$patch((state) => {
|
||||
state.lang = command
|
||||
storage.set({ key: 'lang', data: command })
|
||||
})
|
||||
language.loadLocaleMessages(route.path, systemStore.lang)
|
||||
location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
147
admin/src/layout/darkside/components/header/user-info.vue
Normal file
147
admin/src/layout/darkside/components/header/user-info.vue
Normal file
@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dropdown @command="clickEvent" :tabindex="1">
|
||||
<div class="userinfo flex h-full items-center">
|
||||
<el-avatar :size="25" :icon="UserFilled" />
|
||||
<div class="user-name pl-[8px]">{{ userStore.userInfo.username }}</div>
|
||||
<icon name="element-ArrowDown" class="ml-[5px]" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="toLink('/home/index')">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconqiehuan ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">切换站点</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="getUserInfoFn">
|
||||
<!-- <router-link to="/user/center"> -->
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconshezhi1 ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">账号设置</span>
|
||||
</div>
|
||||
<!-- </router-link> -->
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="changePasswordDialog=true">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconxiugai ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">修改密码</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="logout">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont icontuichudenglu ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">退出登录</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dialog v-model="changePasswordDialog" width="450px" title="修改密码">
|
||||
<div>
|
||||
<el-form :model="saveInfo" label-width="90px" ref="formRef" :rules="formRules" class="page-form">
|
||||
<el-form-item :label="t('originalPassword')" prop="original_password">
|
||||
<el-input v-model="saveInfo.original_password" type="password" :placeholder="t('originalPasswordPlaceholder')" clearable class="input-width" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('newPassword')" prop="password">
|
||||
<el-input v-model="saveInfo.password" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" />
|
||||
<div class="form-tip">{{t('passwordTip')}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('passwordCopy')" prop="password_copy">
|
||||
<el-input v-model="saveInfo.password_copy" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="changePasswordDialog = false">{{t('cancel')}}</el-button>
|
||||
<el-button type="primary" @click="submitForm(formRef)">{{t('save')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<user-info-edit ref="userInfoEditRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { UserFilled } from '@element-plus/icons-vue'
|
||||
import { computed, reactive, ref, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { FormInstance, FormRules, ElNotification } from 'element-plus'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { setUserInfo } from '@/app/api/personal'
|
||||
import { t } from '@/lang'
|
||||
import userInfoEdit from '@/app/components/user-info-edit/index.vue'
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const clickEvent = (command: string) => {
|
||||
switch (command) {
|
||||
case 'logout':
|
||||
userStore.logout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
userStore.logout();
|
||||
}
|
||||
const toLink = (link) => {
|
||||
router.push(link)
|
||||
}
|
||||
const userInfoEditRef = ref(null)
|
||||
const getUserInfoFn = ()=>{
|
||||
userInfoEditRef.value?.open()
|
||||
}
|
||||
// 修改密码 --- start
|
||||
let changePasswordDialog = ref(false)
|
||||
const formRef = ref<FormInstance>();
|
||||
// 提交信息
|
||||
let saveInfo = reactive({
|
||||
original_password: '',
|
||||
password: '',
|
||||
password_copy: ''
|
||||
});
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
original_password: [
|
||||
{ required: true, message: t("originalPasswordPlaceholder"), trigger: "blur" },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: t("passwordPlaceholder"), trigger: "blur" },
|
||||
],
|
||||
password_copy: [
|
||||
{ required: true, message: t("passwordPlaceholder"), trigger: "blur" },
|
||||
]
|
||||
});
|
||||
const submitForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
let msg = "";
|
||||
if (saveInfo.password && !saveInfo.original_password) msg = t('originalPasswordHint');
|
||||
if (saveInfo.password && saveInfo.original_password && !saveInfo.password_copy) msg = t('newPasswordHint');
|
||||
if (saveInfo.password && saveInfo.original_password && saveInfo.password_copy && saveInfo.password != saveInfo.password_copy) msg = t('doubleCipherHint');
|
||||
if (msg) {
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: msg,
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
setUserInfo(saveInfo).then((res: any) => {
|
||||
changePasswordDialog.value = false;
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
});
|
||||
}
|
||||
// 修改密码 --- end
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-popper .el-dropdown-menu{
|
||||
width: 150px;
|
||||
}
|
||||
</style>
|
||||
137
admin/src/layout/darkside/components/tabs.vue
Normal file
137
admin/src/layout/darkside/components/tabs.vue
Normal file
@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div class="tab-wrap w-full px-[16px]">
|
||||
<el-tabs :closable="tabbarStore.tabLength > 1" :model-value="route.path" @tab-click="tabClick"
|
||||
@tab-remove="removeTab">
|
||||
<el-tab-pane v-for="(tab, key, index) in tabbarStore.tabs" :name="tab.path" :key="index">
|
||||
<template #label>
|
||||
<el-dropdown trigger="contextmenu" placement="bottom-start">
|
||||
<span :class="{ 'text-primary': route.path == tab.path }" class="tab-name">{{ tab.title }}</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Back" :disabled="index == 0" @click="closeLeft(tab.path)">{{t('tabs.closeLeft') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Right" :disabled="index == (tabbarStore.tabLength - 1)" @click="closeRight(tab.path)">{{t('tabs.closeRight') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Close" :disabled="tabbarStore.tabLength == 1" @click="closeOther(tab.path)">{{t('tabs.closeOther') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, onMounted } from 'vue'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const tabbarStore = useTabbarStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
tabbarStore.addTab(route)
|
||||
})
|
||||
|
||||
watch(route, (nval: any) => {
|
||||
tabbarStore.addTab(nval)
|
||||
})
|
||||
|
||||
/**
|
||||
* 添加tab
|
||||
* @param content
|
||||
*/
|
||||
const tabClick = (content: any) => {
|
||||
const tabRoute = tabbarStore.tabs[content.props.name]
|
||||
router.push({ path: tabRoute.path, query: tabRoute.query })
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除tab
|
||||
* @param content
|
||||
*/
|
||||
const removeTab = (content: any) => {
|
||||
if (route.path == content) {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
router.push({ path: tabs[tabs.indexOf(content) - 1] })
|
||||
}
|
||||
tabbarStore.removeTab(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭左侧
|
||||
* @param path
|
||||
*/
|
||||
const closeLeft = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(path) - 1; i >= 0; i--) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ path })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭右侧
|
||||
* @param path
|
||||
*/
|
||||
const closeRight = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(path) + 1; i < tabs.length; i++) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ path })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭其他
|
||||
* @param path
|
||||
*/
|
||||
const closeOther = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
tabs.forEach((key: string) => { key != path && delete tabbarStore.tabs[key] })
|
||||
router.push({ path })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tabs) {
|
||||
.el-tabs--border-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap {
|
||||
margin-bottom: 0;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
display: inline-flex !important;
|
||||
padding: 0 20px !important;
|
||||
align-items: center;
|
||||
|
||||
.tab-name:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
47
admin/src/layout/darkside/index.vue
Normal file
47
admin/src/layout/darkside/index.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="flex w-full h-screen">
|
||||
<!-- 左侧边栏 -->
|
||||
<layout-aside></layout-aside>
|
||||
<!-- 左侧边栏 end -->
|
||||
|
||||
<el-container>
|
||||
<!-- 顶部 -->
|
||||
<el-header>
|
||||
<layout-header></layout-header>
|
||||
</el-header>
|
||||
<!-- 顶部 end -->
|
||||
|
||||
<!-- 主体 -->
|
||||
<el-main class="h-full p-0 bg-page">
|
||||
<el-scrollbar>
|
||||
<div class="p-[15px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
|
||||
<keep-alive :include="tabbarStore.tabNames">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
<!-- 主体 end -->
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import layoutHeader from './components/header/index.vue'
|
||||
import layoutAside from './components/aside/index.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const tabbarStore = useTabbarStore()
|
||||
const systemStore = useSystemStore()
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
4
admin/src/layout/darkside/layout.json
Normal file
4
admin/src/layout/darkside/layout.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"layout": "darkside",
|
||||
"cover": "/app/assets/images/layout_darkside.png"
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<el-aside :class="['h-screen layout-aside w-auto', { 'bright': !dark }]">
|
||||
<side class="hidden-xs-only" />
|
||||
<el-aside class="layout-aside w-auto">
|
||||
<side class="hidden-xs-only slide" />
|
||||
</el-aside>
|
||||
|
||||
<el-drawer v-model="systemStore.menuDrawer" direction="ltr" :with-header="false" custom-class="aside-drawer" size="210px">
|
||||
@ -15,6 +15,7 @@ import { watch, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import side from './side.vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
@ -30,21 +31,33 @@ watch(route, () => {
|
||||
|
||||
<style lang="scss">
|
||||
.layout-aside {
|
||||
background-color: var(--side-dark-color, var(--el-bg-color));
|
||||
border-right: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
&.bright {
|
||||
// background-color: #F5F7F9;
|
||||
background-color: #F5F7F9;
|
||||
|
||||
li {
|
||||
// background-color: #F5F7F9;
|
||||
background-color: #F5F7F9;
|
||||
|
||||
&.is-active:not(.is-opened) {
|
||||
position: relative;
|
||||
color: var(--el-color-primary);
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
background-color: var(--el-menu-active-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slide {
|
||||
border-right: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
}
|
||||
|
||||
.aside-drawer {
|
||||
|
||||
@ -2,9 +2,17 @@
|
||||
<template v-if="meta.show">
|
||||
<el-sub-menu v-if="routes.children" :index="String(routes.name)">
|
||||
<template #title>
|
||||
<span :class="['ml-[10px]']">{{ meta.title }}</span>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
</template>
|
||||
<menu-item v-for="(route, index) in routes.children" :routes="route" :key="index" :level="props.level + 1" />
|
||||
<template v-if="routes.name == 'addon_list'">
|
||||
<template v-if="addonsMenus">
|
||||
<menu-item :routes="addonsMenus" :key="index" :level="props.level + 1"/>
|
||||
</template>
|
||||
</template>
|
||||
<menu-item v-for="(route, index) in routes.children" :routes="route" :key="index" />
|
||||
</el-sub-menu>
|
||||
<template v-else>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-if="meta.addon && meta.parent_route && meta.parent_route.addon == ''">
|
||||
@ -13,13 +21,19 @@
|
||||
<template #content>
|
||||
该功能仅限{{ addons[meta.addon].title }}使用
|
||||
</template>
|
||||
<span :class="[{'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}, {'ml-[10px]': routes.meta.class == 2, 'ml-[15px]': routes.meta.class == 3}]">{{ meta.title }}</span>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-else>
|
||||
<template #title>
|
||||
<span :class="[{'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}, {'ml-[10px]': routes.meta.class == 2, 'ml-[15px]': routes.meta.class == 3}]">{{ meta.title }}</span>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
@ -29,30 +43,60 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { img } from '@/utils/common'
|
||||
import menuItem from './menu-item.vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const routers = useUserStore().routers
|
||||
const props = defineProps({
|
||||
routes: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
})
|
||||
const userStore = useUserStore()
|
||||
const siteInfo = userStore.siteInfo
|
||||
const systemStore = useSystemStore()
|
||||
const meta = computed(() => props.routes.meta)
|
||||
|
||||
const addons = computed(() => {
|
||||
const addons:Record<string, any> = {}
|
||||
siteInfo?.apps.forEach((item: any) => { addons[item.key] = item })
|
||||
siteInfo?.site_addons.forEach((item: any) => { addons[item.key] = item })
|
||||
userStore.siteInfo?.apps.forEach((item: any) => { addons[item.key] = item })
|
||||
userStore.siteInfo?.site_addons.forEach((item: any) => { addons[item.key] = item })
|
||||
return addons
|
||||
})
|
||||
|
||||
const systemAddonKeys = computed(() => {
|
||||
return userStore.siteInfo?.site_addons.map((item: any) => item.key)
|
||||
})
|
||||
|
||||
const addonRouters: Record<string, any> = {}
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon) {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
})
|
||||
|
||||
const addonsMenus = ref(null)
|
||||
|
||||
watch(route, () => {
|
||||
if (props.routes.name != 'addon_list') return
|
||||
|
||||
if (systemAddonKeys.value.includes(route.meta.addon) && addonRouters[route.meta.addon]) {
|
||||
addonsMenus.value = addonRouters[route.meta.addon]
|
||||
} else {
|
||||
addonsMenus.value = null
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@ -60,8 +104,5 @@ const addons = computed(() => {
|
||||
.el-icon{
|
||||
width: auto;
|
||||
}
|
||||
li{
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,43 +1,21 @@
|
||||
<template>
|
||||
<el-container class="w-100 h-screen">
|
||||
<el-main class="p-0 flex">
|
||||
<div class="w-[124px] px-[8px] bg-[#282c34] h-screen one-menu">
|
||||
<el-header class="logo-wrap">
|
||||
<div class="logo flex items-center m-auto h-[64px]" v-if="!systemStore.menuIsCollapse">
|
||||
<el-image style="width: 40px; height: 40px" :src="img(logoUrl)" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-[40px]"><img class="max-w-[40px]" src="@/app/assets/images/icon-addon.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="logo flex items-center justify-center h-[64px]" v-else>
|
||||
<i class="text-3xl iconfont iconyunkongjian"></i>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-scrollbar class="h-[calc( 100vh - 64px )]">
|
||||
<el-menu :default-active="oneMenuActive" :router="true" class="aside-menu" unique-opened="true" :collapse="systemStore.menuIsCollapse">
|
||||
<template v-for="(item, index) in oneMenuData" :key="index">
|
||||
<el-menu-item :index="item.original_name" @click="router.push({ name: item.name })" v-if="item.meta.show">
|
||||
<div v-if="item.meta.icon" class="w-[16px] h-[16px] relative flex justify-center">
|
||||
<el-image class="w-[16px] h-[16px] rounded-[50%] overflow-hidden" :src="item.meta.icon" fit="fill" v-if="isUrl(item.meta.icon)"/>
|
||||
<icon :name="item.meta.icon" class="absolute top-[50%] -translate-y-[50%]" v-else />
|
||||
</div>
|
||||
<div v-else class="w-[16px] h-[16px]"></div>
|
||||
<template #title>
|
||||
<div class="relative flex-1 w-0">
|
||||
<span class="ml-[10px] w-full truncate">{{ item.meta.short_title || item.meta.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
<el-container class="w-[200px] h-screen layout-aside flex flex-col">
|
||||
<el-header class="logo-wrap flex items-center justify-center h-[64px]">
|
||||
<div class="logo flex items-center m-auto h-[64px]" v-if="!systemStore.menuIsCollapse">
|
||||
<el-image style="width: 40px; height: 40px" :src="img(logoUrl)" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-[40px]"><img class="max-w-[40px]" src="@/app/assets/images/icon-addon.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<el-scrollbar v-if="twoMenuData.length" class="two-menu w-[140px]">
|
||||
<div class="w-[140px] h-[64px] flex items-center justify-center text-[16px] border-0 border-b-[1px] border-solid border-[#eee]">{{ route.matched[1].meta.title }}</div>
|
||||
<el-menu :default-active="route.name" :router="true" class="aside-menu" :collapse="systemStore.menuIsCollapse">
|
||||
<menu-item v-for="(route, index) in twoMenuData" :routes="route" :key="index" />
|
||||
<div class="logo flex items-center justify-center h-[64px]" v-else>
|
||||
<i class="text-3xl iconfont iconyunkongjian"></i>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main class="menu-wrap">
|
||||
<el-scrollbar>
|
||||
<el-menu :default-active="route.name" :router="true" class="aside-menu h-full" :unique-opened="true" :collapse="systemStore.menuIsCollapse" >
|
||||
<menu-item v-for="(route, index) in menuData" :routes="route" :key="index" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
@ -57,13 +35,10 @@ import { findFirstValidRoute } from '@/router/routers'
|
||||
const systemStore = useSystemStore()
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const siteInfo = userStore.siteInfo
|
||||
const routers = userStore.routers
|
||||
const addonIndexRoute = userStore.addonIndexRoute
|
||||
|
||||
const oneMenuData = ref<Record<string, any>[]>([])
|
||||
const twoMenuData = ref<Record<string, any>[]>([])
|
||||
const menuData = ref<Record<string, any>[]>([])
|
||||
const addonRouters: Record<string, any> = {}
|
||||
const logoUrl = computed(() => {
|
||||
return userStore.siteInfo.icon ? userStore.siteInfo.icon : systemStore.website.icon
|
||||
@ -75,8 +50,8 @@ routers.forEach(item => {
|
||||
if (item.children && item.children.length) {
|
||||
item.name = findFirstValidRoute(item.children)
|
||||
}
|
||||
oneMenuData.value.push(item)
|
||||
} else if (item.meta.addon != '' && siteInfo?.apps.length <= 1 && siteInfo?.apps[0].key == item.meta.addon) {
|
||||
menuData.value.push(item)
|
||||
} else if (item.meta.addon != '' && siteInfo?.apps.length == 1 && siteInfo?.apps[0].key == item.meta.addon) {
|
||||
if (item.children) {
|
||||
item.children.forEach((citem: Record<string, any>) => {
|
||||
citem.original_name = citem.name
|
||||
@ -84,9 +59,9 @@ routers.forEach(item => {
|
||||
citem.name = findFirstValidRoute(citem.children)
|
||||
}
|
||||
})
|
||||
oneMenuData.value.unshift(...item.children)
|
||||
menuData.value.unshift(...item.children)
|
||||
} else {
|
||||
oneMenuData.value.unshift(item)
|
||||
menuData.value.unshift(item)
|
||||
}
|
||||
} else {
|
||||
addonRouters[item.meta.addon] = item
|
||||
@ -97,144 +72,30 @@ routers.forEach(item => {
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
const routers:Record<string, any>[] = []
|
||||
siteInfo?.apps.forEach((item: Record<string, any>) => {
|
||||
routers.push({
|
||||
path: addonRouters[item.key] ? addonRouters[item.key].path : '',
|
||||
meta: {
|
||||
icon: addonRouters[item.key]?.meta.icon || 'element-Setting',
|
||||
addon: item.key,
|
||||
title: item.title,
|
||||
app: item.app,
|
||||
show: true
|
||||
},
|
||||
original_name: item.key,
|
||||
name: addonIndexRoute[item.key]
|
||||
})
|
||||
})
|
||||
oneMenuData.value.unshift(...routers)
|
||||
}
|
||||
|
||||
const oneMenuActive = ref(route.matched[1].name)
|
||||
|
||||
watch(route, () => {
|
||||
// 多应用
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
twoMenuData.value = route.matched[1].children
|
||||
oneMenuActive.value = route.matched[1].name
|
||||
} else {
|
||||
// 单应用
|
||||
if (route.meta.addon == '') {
|
||||
oneMenuActive.value = route.matched[1].name
|
||||
twoMenuData.value = route.matched[1].children ?? []
|
||||
} else if (route.meta.addon && route.meta.addon != siteInfo?.apps[0].key) {
|
||||
oneMenuActive.value = '/site/app'
|
||||
twoMenuData.value = route.matched[1].children ?? []
|
||||
} else {
|
||||
oneMenuActive.value = route.matched[2].name
|
||||
twoMenuData.value = route.matched[2].children ?? []
|
||||
if (addonRouters[item.key]) {
|
||||
addonRouters[item.key].name = addonIndexRoute[item.key]
|
||||
routers.push(addonRouters[item.key])
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
})
|
||||
menuData.value.unshift(...routers)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.one-menu{
|
||||
.aside-menu:not(.el-menu--collapse) {
|
||||
background-color: transparent;
|
||||
.el-menu-item{
|
||||
margin-bottom: 4px;
|
||||
height: 40px;
|
||||
padding-left: 12px !important;
|
||||
color: rgba(255,255,255,.7);
|
||||
font-size: 14px;
|
||||
border-radius: 2px;
|
||||
&:hover{
|
||||
background-color: transparent;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
&.is-active{
|
||||
background-color: var(--el-color-primary) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
span{
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-menu{
|
||||
border: 0;
|
||||
}
|
||||
.el-scrollbar{
|
||||
height: calc(100vh - 65px);
|
||||
}
|
||||
}
|
||||
.two-menu{
|
||||
.aside-menu:not(.el-menu--collapse) {
|
||||
width: 140px;
|
||||
border: 0;
|
||||
padding-top: 16px;
|
||||
.el-menu-item{
|
||||
height: 36px;
|
||||
margin: 0 8px 4px;
|
||||
padding: 0 8px !important;
|
||||
border-radius: 2px;
|
||||
span{
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
&.is-active{
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
&:hover{
|
||||
background-color: #f7f7f7;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.el-sub-menu{
|
||||
margin-bottom: 8px;
|
||||
.el-sub-menu__title{
|
||||
margin: 0 8px 4px;
|
||||
height: 36px;
|
||||
padding-left: 8px;
|
||||
border-radius: 2px;
|
||||
span{
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
&:hover{
|
||||
background-color: #f7f7f7;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.el-icon.el-sub-menu__icon-arrow{
|
||||
right: 5px;
|
||||
}
|
||||
}
|
||||
.el-menu-item{
|
||||
padding-left: 20px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.menu-wrap {
|
||||
padding: 0!important;
|
||||
|
||||
.logo-wrap {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
.el-menu {
|
||||
border-right: 0!important;
|
||||
|
||||
.logo {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.el-menu-item, .el-sub-menu__title {
|
||||
--el-menu-item-height: 40px;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-size: var(--el-font-size-base);
|
||||
.el-sub-menu .el-menu-item {
|
||||
--el-menu-sub-item-height: 40px;
|
||||
--el-menu-sub-item-height: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -4,16 +4,16 @@
|
||||
<el-col :span="12">
|
||||
<div class="left-panel h-full flex items-center">
|
||||
<!-- 左侧菜单折叠 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
|
||||
<div class="hidden-sm-and-up navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
|
||||
<icon name="element-Expand" v-if="systemStore.menuIsCollapse" />
|
||||
<icon name="element-Fold" v-else />
|
||||
</div> -->
|
||||
</div>
|
||||
<!-- 刷新当前页 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="refreshRouter">
|
||||
<icon name="element-Refresh" />
|
||||
</div>
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="flex items-center h-full pl-[10px] hidden-xs-only">
|
||||
<div class="flex items-center h-full pl-[10px]">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(route, index) in breadcrumb" :key="index">{{route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
@ -125,16 +125,16 @@ onMounted(() => {
|
||||
// })
|
||||
|
||||
// 菜单栏展开折叠
|
||||
// const toggleMenuCollapse = () => {
|
||||
// systemStore.$patch((state) => {
|
||||
// if (screenWidth.value < 768) {
|
||||
// state.menuDrawer = true
|
||||
// state.menuIsCollapse = false
|
||||
// } else {
|
||||
// systemStore.toggleMenuCollapse(!systemStore.menuIsCollapse)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
const toggleMenuCollapse = () => {
|
||||
systemStore.$patch((state) => {
|
||||
if (screenWidth.value < 768) {
|
||||
state.menuDrawer = true
|
||||
state.menuIsCollapse = false
|
||||
} else {
|
||||
systemStore.toggleMenuCollapse(!systemStore.menuIsCollapse)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新路由
|
||||
const refreshRouter = () => {
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dialog v-model="changePasswordDialog" width="450px" title="修改密码" :before-close="handleClose">
|
||||
<el-dialog v-model="changePasswordDialog" width="450px" title="修改密码">
|
||||
<div>
|
||||
<el-form :model="saveInfo" label-width="90px" ref="formRef" :rules="formRules" class="page-form">
|
||||
<el-form-item :label="t('originalPassword')" prop="original_password">
|
||||
|
||||
@ -1,26 +1,29 @@
|
||||
<template>
|
||||
<div class="common-layout min-w-[1200px]" >
|
||||
<el-container class="w-100 h-screen">
|
||||
<layout-aside></layout-aside>
|
||||
<div class="flex w-full h-screen">
|
||||
<!-- 左侧边栏 -->
|
||||
<layout-aside></layout-aside>
|
||||
<!-- 左侧边栏 end -->
|
||||
|
||||
<el-container>
|
||||
<el-header>
|
||||
<layout-header></layout-header>
|
||||
</el-header>
|
||||
<el-container>
|
||||
<!-- 顶部 -->
|
||||
<el-header>
|
||||
<layout-header></layout-header>
|
||||
</el-header>
|
||||
<!-- 顶部 end -->
|
||||
|
||||
<el-main :class="['main-wrap h-full p-0 bg-page']">
|
||||
<el-scrollbar>
|
||||
<div class="p-[10px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
|
||||
<keep-alive :include="tabbarStore.tabNames">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
|
||||
</el-container>
|
||||
<!-- 主体 -->
|
||||
<el-main class="h-full p-0 bg-page">
|
||||
<el-scrollbar>
|
||||
<div class="p-[15px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
|
||||
<keep-alive :include="tabbarStore.tabNames">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
<!-- 主体 end -->
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
4
admin/src/layout/default/layout.json
Normal file
4
admin/src/layout/default/layout.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"layout": "default",
|
||||
"cover": "/app/assets/images/layout_default.png"
|
||||
}
|
||||
@ -7,7 +7,7 @@
|
||||
</div>
|
||||
<span :class="['ml-[10px]', {'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}]">{{ meta.title }}</span>
|
||||
</template>
|
||||
<menu-item v-for="(route, index) in routes.children" :routes="route" :route-path="resolvePath(route.path)" :key="index" />
|
||||
<menu-item v-for="(route, index) in routes.children" :routes="route" :key="index" />
|
||||
</el-sub-menu>
|
||||
<el-menu-item v-else-if="routes.meta.class == 1" :index="String(routes.name)" :route="routePath">
|
||||
<div v-if="meta.icon" class="w-[16px] h-[16px] relative flex justify-center">
|
||||
@ -68,10 +68,6 @@ const props = defineProps({
|
||||
|
||||
const meta = computed(() => props.routes.meta)
|
||||
|
||||
const resolvePath = (path: string) => {
|
||||
return `${props.routePath}/${path}`
|
||||
}
|
||||
|
||||
const indexList = ref();
|
||||
const showDialog = ref(false)
|
||||
const checkIndexList = () => {
|
||||
|
||||
@ -12,8 +12,8 @@
|
||||
|
||||
<el-main class="menu-wrap">
|
||||
<el-scrollbar>
|
||||
<el-menu :default-active="menuActive" :router="true" class="aside-menu h-full" unique-opened="true" :collapse="systemStore.menuIsCollapse">
|
||||
<menu-item v-for="(route, index) in userStore.routers" :routes="route" :route-path="route.path" :key="index" />
|
||||
<el-menu :default-active="menuActive" :router="true" class="aside-menu h-full" :unique-opened="true" :collapse="systemStore.menuIsCollapse">
|
||||
<menu-item v-for="(route, index) in userStore.routers" :routes="route" :key="index" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
import { ref, markRaw, defineAsyncComponent, provide } from 'vue'
|
||||
import { getAppType } from '@/utils/common'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
|
||||
const sysLayout = import.meta.glob('./*/index.vue')
|
||||
const addonLayout = import.meta.glob('@/addon/**/layout/index.vue')
|
||||
@ -21,13 +22,20 @@ switch (getAppType()) {
|
||||
break
|
||||
default:
|
||||
const siteInfo = useUserStore().siteInfo
|
||||
if (siteInfo && siteInfo.apps && siteInfo.apps.length == 1) siteLayout = siteInfo.apps[0].key
|
||||
if (siteInfo && siteInfo.apps) {
|
||||
const layouts = useSystemStore().layoutConfig
|
||||
if (siteInfo.apps.length == 1) {
|
||||
layouts[siteInfo.apps[0].key] != undefined && (siteLayout = layouts[siteInfo.apps[0].key])
|
||||
} else {
|
||||
layouts.system != undefined && (siteLayout = layouts.system)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const layout = ref<any>(null)
|
||||
|
||||
Object.keys(modules).forEach(key => {
|
||||
key.indexOf(siteLayout) !== -1 && (layout.value = markRaw(defineAsyncComponent(modules[key])))
|
||||
key.indexOf(`/${siteLayout}/`) !== -1 && (layout.value = markRaw(defineAsyncComponent(modules[key])))
|
||||
})
|
||||
|
||||
!layout.value && (layout.value = markRaw(defineAsyncComponent(modules['./default/index.vue'])))
|
||||
@ -39,7 +47,7 @@ provide('setLayout', (name: any) => {
|
||||
if (siteLayout == name) return
|
||||
siteLayout = name
|
||||
Object.keys(modules).forEach(key => {
|
||||
key.indexOf(name) !== -1 && (layout.value = markRaw(defineAsyncComponent(modules[key])))
|
||||
key.indexOf(`/${name}/`) !== -1 && (layout.value = markRaw(defineAsyncComponent(modules[key])))
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
55
admin/src/layout/profession/components/aside/index.vue
Normal file
55
admin/src/layout/profession/components/aside/index.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<el-aside :class="['h-screen layout-aside w-auto', { 'bright': !dark }]">
|
||||
<side class="hidden-xs-only" />
|
||||
</el-aside>
|
||||
|
||||
<el-drawer v-model="systemStore.menuDrawer" direction="ltr" :with-header="false" custom-class="aside-drawer" size="210px">
|
||||
<template #default>
|
||||
<side />
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import side from './side.vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
const systemStore = useSystemStore()
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
watch(route, () => {
|
||||
systemStore.$patch(state => {
|
||||
state.menuDrawer = false
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.layout-aside {
|
||||
background-color: var(--side-dark-color, var(--el-bg-color));
|
||||
border-right: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
&.bright {
|
||||
// background-color: #F5F7F9;
|
||||
|
||||
li {
|
||||
// background-color: #F5F7F9;
|
||||
|
||||
&.is-active:not(.is-opened) {
|
||||
position: relative;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aside-drawer {
|
||||
.el-drawer__body {
|
||||
padding: 0px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
67
admin/src/layout/profession/components/aside/menu-item.vue
Normal file
67
admin/src/layout/profession/components/aside/menu-item.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<template v-if="meta.show">
|
||||
<el-sub-menu v-if="routes.children" :index="String(routes.name)">
|
||||
<template #title>
|
||||
<span :class="['ml-[10px]']">{{ meta.title }}</span>
|
||||
</template>
|
||||
<menu-item v-for="(route, index) in routes.children" :routes="route" :key="index" />
|
||||
</el-sub-menu>
|
||||
<template v-else>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-if="meta.addon && meta.parent_route && meta.parent_route.addon == ''">
|
||||
<template #title>
|
||||
<el-tooltip placement="right" effect="light">
|
||||
<template #content>
|
||||
该功能仅限{{ addons[meta.addon].title }}使用
|
||||
</template>
|
||||
<span :class="[{'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}, {'ml-[10px]': routes.meta.class == 2, 'ml-[15px]': routes.meta.class == 3}]">{{ meta.title }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-else>
|
||||
<template #title>
|
||||
<span :class="[{'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}, {'ml-[10px]': routes.meta.class == 2, 'ml-[15px]': routes.meta.class == 3}]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
<div v-if="routes.is_border" class="!border-0 !border-t-[1px] border-solid mx-[25px] bg-[#f7f7f7] my-[5px]"></div>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
import { img } from '@/utils/common'
|
||||
import menuItem from './menu-item.vue'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
|
||||
const router = useRouter()
|
||||
const props = defineProps({
|
||||
routes: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const userStore = useUserStore()
|
||||
const siteInfo = userStore.siteInfo
|
||||
const meta = computed(() => props.routes.meta)
|
||||
|
||||
const addons = computed(() => {
|
||||
const addons:Record<string, any> = {}
|
||||
siteInfo?.apps.forEach((item: any) => { addons[item.key] = item })
|
||||
siteInfo?.site_addons.forEach((item: any) => { addons[item.key] = item })
|
||||
return addons
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-sub-menu{
|
||||
.el-icon{
|
||||
width: auto;
|
||||
}
|
||||
li{
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
244
admin/src/layout/profession/components/aside/side.vue
Normal file
244
admin/src/layout/profession/components/aside/side.vue
Normal file
@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<el-container class="w-100 h-screen">
|
||||
<el-main class="p-0 flex">
|
||||
<div class="w-[124px] px-[8px] bg-[#282c34] h-screen one-menu">
|
||||
<el-header class="logo-wrap">
|
||||
<div class="logo flex items-center m-auto h-[64px]" v-if="!systemStore.menuIsCollapse">
|
||||
<el-image style="width: 40px; height: 40px" :src="img(logoUrl)" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-[40px]"><img class="max-w-[40px]" src="@/app/assets/images/icon-addon.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="logo flex items-center justify-center h-[64px]" v-else>
|
||||
<i class="text-3xl iconfont iconyunkongjian"></i>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-scrollbar class="h-[calc( 100vh - 64px )]">
|
||||
<el-menu :default-active="oneMenuActive" :router="true" class="aside-menu" :unique-opened="true" :collapse="systemStore.menuIsCollapse">
|
||||
<template v-for="(item, index) in oneMenuData" :key="index">
|
||||
<el-menu-item :index="item.original_name" @click="router.push({ name: item.name })" v-if="item.meta.show">
|
||||
<div v-if="item.meta.icon" class="w-[16px] h-[16px] relative flex justify-center">
|
||||
<el-image class="w-[16px] h-[16px] rounded-[50%] overflow-hidden" :src="item.meta.icon" fit="fill" v-if="isUrl(item.meta.icon)"/>
|
||||
<icon :name="item.meta.icon" class="absolute top-[50%] -translate-y-[50%]" v-else />
|
||||
</div>
|
||||
<div v-else class="w-[16px] h-[16px]"></div>
|
||||
<template #title>
|
||||
<div class="relative flex-1 w-0">
|
||||
<span class="ml-[10px] w-full truncate">{{ item.meta.short_title || item.meta.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<el-scrollbar v-if="twoMenuData.length" class="two-menu w-[140px]">
|
||||
<div class="w-[140px] h-[64px] flex items-center justify-center text-[16px] border-0 border-b-[1px] border-solid border-[#eee]">{{ route.matched[1].meta.title }}</div>
|
||||
<el-menu :default-active="route.name" :default-openeds="menuOption" :router="true" class="aside-menu" :collapse="systemStore.menuIsCollapse">
|
||||
<menu-item v-for="(route, index) in twoMenuData" :routes="route" :key="index" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch,computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import menuItem from './menu-item.vue'
|
||||
import { img, isUrl } from '@/utils/common'
|
||||
import { findFirstValidRoute } from '@/router/routers'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const siteInfo = userStore.siteInfo
|
||||
const routers = userStore.routers
|
||||
const addonIndexRoute = userStore.addonIndexRoute
|
||||
|
||||
const oneMenuData = ref<Record<string, any>[]>([])
|
||||
const twoMenuData = ref<Record<string, any>[]>([])
|
||||
const addonRouters: Record<string, any> = {}
|
||||
const logoUrl = computed(() => {
|
||||
return userStore.siteInfo.icon ? userStore.siteInfo.icon : systemStore.website.icon
|
||||
})
|
||||
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon == '') {
|
||||
if (item.children && item.children.length) {
|
||||
item.name = findFirstValidRoute(item.children)
|
||||
}
|
||||
oneMenuData.value.push(item)
|
||||
} else if (item.meta.addon != '' && siteInfo?.apps.length <= 1 && siteInfo?.apps[0].key == item.meta.addon) {
|
||||
if (item.children) {
|
||||
item.children.forEach((citem: Record<string, any>) => {
|
||||
citem.original_name = citem.name
|
||||
if (citem.children && citem.children.length) {
|
||||
citem.name = findFirstValidRoute(citem.children)
|
||||
}
|
||||
})
|
||||
oneMenuData.value.unshift(...item.children)
|
||||
} else {
|
||||
oneMenuData.value.unshift(item)
|
||||
}
|
||||
} else {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
})
|
||||
|
||||
// 多应用时将应用插入菜单
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
const routers:Record<string, any>[] = []
|
||||
siteInfo?.apps.forEach((item: Record<string, any>) => {
|
||||
if (addonRouters[item.key]) {
|
||||
addonRouters[item.key].name = addonIndexRoute[item.key]
|
||||
routers.push(addonRouters[item.key])
|
||||
}
|
||||
})
|
||||
oneMenuData.value.unshift(...routers)
|
||||
}
|
||||
|
||||
const oneMenuActive = ref(route.matched[1].name)
|
||||
|
||||
watch(route, () => {
|
||||
// 多应用
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
twoMenuData.value = route.matched[1].children
|
||||
oneMenuActive.value = route.matched[1].name
|
||||
} else {
|
||||
// 单应用
|
||||
if (route.meta.addon == '') {
|
||||
oneMenuActive.value = route.matched[1].name
|
||||
twoMenuData.value = route.matched[1].children ?? []
|
||||
} else if (route.meta.addon && route.meta.addon != siteInfo?.apps[0].key) {
|
||||
oneMenuActive.value = '/site/app'
|
||||
twoMenuData.value = route.matched[1].children ?? []
|
||||
} else {
|
||||
oneMenuActive.value = route.matched[2].name
|
||||
twoMenuData.value = route.matched[2].children ?? []
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 让二级菜单默认展开
|
||||
let menuOption = ref([])
|
||||
watch(twoMenuData.value, () => {
|
||||
menuOption.value = [];
|
||||
if(twoMenuData.value && Object.values(twoMenuData.value).length){
|
||||
let data = JSON.parse(JSON.stringify(twoMenuData.value));
|
||||
for(let key in data){
|
||||
menuOption.value.push(data[key].name);
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.one-menu{
|
||||
.aside-menu:not(.el-menu--collapse) {
|
||||
background-color: transparent;
|
||||
.el-menu-item{
|
||||
margin-bottom: 4px;
|
||||
height: 40px;
|
||||
padding-left: 12px !important;
|
||||
color: rgba(255,255,255,.7);
|
||||
font-size: 14px;
|
||||
border-radius: 2px;
|
||||
&:hover{
|
||||
background-color: transparent;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
&.is-active{
|
||||
background-color: var(--el-color-primary) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
span{
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-menu{
|
||||
border: 0;
|
||||
}
|
||||
.el-scrollbar{
|
||||
height: calc(100vh - 65px);
|
||||
}
|
||||
}
|
||||
.two-menu{
|
||||
.aside-menu:not(.el-menu--collapse) {
|
||||
width: 140px;
|
||||
border: 0;
|
||||
padding-top: 16px;
|
||||
.el-menu-item{
|
||||
height: 36px;
|
||||
margin: 0 8px 4px;
|
||||
padding: 0 8px !important;
|
||||
border-radius: 2px;
|
||||
span{
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
&.is-active{
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
&:hover{
|
||||
background-color: #f7f7f7;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.el-sub-menu{
|
||||
margin-bottom: 8px;
|
||||
.el-sub-menu__title{
|
||||
margin: 0 8px 4px;
|
||||
height: 36px;
|
||||
padding-left: 8px;
|
||||
border-radius: 2px;
|
||||
span{
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
&:hover{
|
||||
background-color: #f7f7f7;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.el-icon.el-sub-menu__icon-arrow{
|
||||
right: 5px;
|
||||
}
|
||||
}
|
||||
.el-menu-item{
|
||||
padding-left: 20px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-wrap {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
|
||||
.logo {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-size: var(--el-font-size-base);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
190
admin/src/layout/profession/components/header/index.vue
Normal file
190
admin/src/layout/profession/components/header/index.vue
Normal file
@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<el-container :class="['h-full px-[10px]',{'layout-header border-b border-color': !dark}]" >
|
||||
<el-row class="w-100 h-full w-full">
|
||||
<el-col :span="12">
|
||||
<div class="left-panel h-full flex items-center">
|
||||
<!-- 左侧菜单折叠 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
|
||||
<icon name="element-Expand" v-if="systemStore.menuIsCollapse" />
|
||||
<icon name="element-Fold" v-else />
|
||||
</div> -->
|
||||
<!-- 刷新当前页 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="refreshRouter">
|
||||
<icon name="element-Refresh" />
|
||||
</div>
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="flex items-center h-full pl-[10px] hidden-xs-only">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(route, index) in breadcrumb" :key="index">{{route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="right-panel h-full flex items-center justify-end">
|
||||
<!-- 预览 只有站点时展示-->
|
||||
<i class="iconfont iconicon_huojian1 cursor-pointer px-[8px]" :title="t('visitWap')" @click="toPreview"></i>
|
||||
<i class="iconfont iconlingdang-xianxing cursor-pointer px-[8px]" :title="t('newInfo')" v-if="appType == 'site'"></i>
|
||||
<!-- 切换语言 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<switch-lang />
|
||||
</div> -->
|
||||
<!-- 切换全屏 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleFullscreen">
|
||||
<icon name="iconfont-icontuichuquanping" v-if="isFullscreen" />
|
||||
<icon name="iconfont-iconquanping" v-else />
|
||||
</div> -->
|
||||
<!-- 布局设置 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<layout-setting />
|
||||
</div>
|
||||
<!-- 用户信息 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<user-info />
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<input type="hidden" v-model="comparisonToken">
|
||||
<input type="hidden" v-model="comparisonSiteId">
|
||||
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
|
||||
<span>{{ t('layout.detectionLoginContent') }}</span>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detectionLoginFn">{{ t('layout.detectionLoginOperation') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import layoutSetting from './layout-setting.vue'
|
||||
import userInfo from './user-info.vue'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import { useRoute,useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const appType = storage.get('app_type')
|
||||
const { toggle: toggleFullscreen } = useFullscreen()
|
||||
const systemStore = useSystemStore()
|
||||
const appStore = useAppStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const screenWidth = ref(window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth)
|
||||
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
|
||||
// 检测登录 start
|
||||
const detectionLoginDialog = ref(false)
|
||||
const comparisonToken = ref('')
|
||||
const comparisonSiteId = ref('')
|
||||
if (storage.get('comparisonTokenStorage')) {
|
||||
comparisonToken.value = storage.get('comparisonTokenStorage')
|
||||
// storage.remove(['comparisonTokenStorage']);
|
||||
}
|
||||
if (storage.get('comparisonSiteIdStorage')) {
|
||||
comparisonSiteId.value = storage.get('comparisonSiteIdStorage')
|
||||
// storage.remove(['comparisonSiteIdStorage']);
|
||||
}
|
||||
// 监听标签页面切换
|
||||
document.addEventListener('visibilitychange', e => {
|
||||
if (document.visibilityState === 'visible' && (comparisonSiteId.value != storage.get('siteId') || comparisonToken.value != storage.get('token'))) {
|
||||
detectionLoginDialog.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const detectionLoginFn = () => {
|
||||
detectionLoginDialog.value = false
|
||||
location.href = `${location.origin}/site/`
|
||||
}
|
||||
// 检测登录 end
|
||||
|
||||
onMounted(() => {
|
||||
// 监听窗体宽度变化
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
screenWidth.value = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
|
||||
})()
|
||||
}
|
||||
})
|
||||
|
||||
// watch(screenWidth, () => {
|
||||
// if (screenWidth.value < 992) {
|
||||
// if (!systemStore.menuIsCollapse) systemStore.toggleMenuCollapse(true)
|
||||
// } else {
|
||||
// if (systemStore.menuIsCollapse) systemStore.toggleMenuCollapse(false)
|
||||
// }
|
||||
// })
|
||||
|
||||
// 菜单栏展开折叠
|
||||
// const toggleMenuCollapse = () => {
|
||||
// systemStore.$patch((state) => {
|
||||
// if (screenWidth.value < 768) {
|
||||
// state.menuDrawer = true
|
||||
// state.menuIsCollapse = false
|
||||
// } else {
|
||||
// systemStore.toggleMenuCollapse(!systemStore.menuIsCollapse)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// 刷新路由
|
||||
const refreshRouter = () => {
|
||||
if (!appStore.routeRefreshTag) return
|
||||
appStore.refreshRouterView()
|
||||
}
|
||||
|
||||
// 面包屑导航
|
||||
const breadcrumb = computed(() => {
|
||||
const matched = route.matched.filter(item => { return item.meta.title })
|
||||
if (matched[0] && matched[0].path == '/') matched.splice(0, 1)
|
||||
return matched
|
||||
})
|
||||
|
||||
// 跳转去预览
|
||||
const toPreview = () => {
|
||||
const url = router.resolve({
|
||||
path: '/preview/wap',
|
||||
query: {
|
||||
page:'/'
|
||||
}
|
||||
})
|
||||
window.open(url.href)
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
// const backFn = () => {
|
||||
// router.go(-1)
|
||||
// }
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-header{
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
box-shadow: 0px 0px 4px 0px rgba(0,145,255,0.1);
|
||||
}
|
||||
.navbar-item {
|
||||
padding: 0 8px;
|
||||
&:hover {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
}
|
||||
.index-item {
|
||||
border: 1px solid;
|
||||
border-color: var(--el-color-primary);
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<icon name="element-Setting" @click="drawer = true" />
|
||||
|
||||
<el-drawer v-model="drawer" :title="t('layout.layoutSetting')" size="300px">
|
||||
<el-scrollbar>
|
||||
<!-- 黑暗模式 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.darkMode') }}</div>
|
||||
<div>
|
||||
<el-switch v-model="dark" :active-value="true" :inactive-value="false" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主题颜色 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.themeColor') }}</div>
|
||||
<div>
|
||||
<el-color-picker v-model="theme" />
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import { setThemeColor } from '@/utils/common'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const drawer = ref(false)
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const isDark = useDark()
|
||||
const toggleDark = useToggle(isDark)
|
||||
|
||||
const dark = computed({
|
||||
get () {
|
||||
return systemStore.dark
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('dark', val)
|
||||
toggleDark(val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const theme = computed({
|
||||
get () {
|
||||
return systemStore.theme
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('theme', val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.layout-style {
|
||||
&>div:nth-child(2n+2) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-dropdown @command="switchLang" :tabindex="1">
|
||||
<icon name="iconfont-iconfanyi" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="zh-cn" :disabled="systemStore.lang == 'zh-cn'">简体中文</el-dropdown-item>
|
||||
<el-dropdown-item command="en" :disabled="systemStore.lang == 'en'">English</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { language } from '@/lang'
|
||||
import { useRoute } from 'vue-router'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const route = useRoute()
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const switchLang = (command: string) => {
|
||||
systemStore.$patch((state) => {
|
||||
state.lang = command
|
||||
storage.set({ key: 'lang', data: command })
|
||||
})
|
||||
language.loadLocaleMessages(route.path, systemStore.lang)
|
||||
location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
147
admin/src/layout/profession/components/header/user-info.vue
Normal file
147
admin/src/layout/profession/components/header/user-info.vue
Normal file
@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dropdown @command="clickEvent" :tabindex="1">
|
||||
<div class="userinfo flex h-full items-center">
|
||||
<el-avatar :size="25" :icon="UserFilled" />
|
||||
<div class="user-name pl-[8px]">{{ userStore.userInfo.username }}</div>
|
||||
<icon name="element-ArrowDown" class="ml-[5px]" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="toLink('/home/index')">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconqiehuan ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">切换站点</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="getUserInfoFn">
|
||||
<!-- <router-link to="/user/center"> -->
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconshezhi1 ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">账号设置</span>
|
||||
</div>
|
||||
<!-- </router-link> -->
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="changePasswordDialog=true">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconxiugai ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">修改密码</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="logout">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont icontuichudenglu ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">退出登录</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dialog v-model="changePasswordDialog" width="450px" title="修改密码">
|
||||
<div>
|
||||
<el-form :model="saveInfo" label-width="90px" ref="formRef" :rules="formRules" class="page-form">
|
||||
<el-form-item :label="t('originalPassword')" prop="original_password">
|
||||
<el-input v-model="saveInfo.original_password" type="password" :placeholder="t('originalPasswordPlaceholder')" clearable class="input-width" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('newPassword')" prop="password">
|
||||
<el-input v-model="saveInfo.password" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" />
|
||||
<div class="form-tip">{{t('passwordTip')}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('passwordCopy')" prop="password_copy">
|
||||
<el-input v-model="saveInfo.password_copy" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="changePasswordDialog = false">{{t('cancel')}}</el-button>
|
||||
<el-button type="primary" @click="submitForm(formRef)">{{t('save')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<user-info-edit ref="userInfoEditRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { UserFilled } from '@element-plus/icons-vue'
|
||||
import { computed, reactive, ref, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { FormInstance, FormRules, ElNotification } from 'element-plus'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { setUserInfo } from '@/app/api/personal'
|
||||
import { t } from '@/lang'
|
||||
import userInfoEdit from '@/app/components/user-info-edit/index.vue'
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const clickEvent = (command: string) => {
|
||||
switch (command) {
|
||||
case 'logout':
|
||||
userStore.logout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
userStore.logout();
|
||||
}
|
||||
const toLink = (link) => {
|
||||
router.push(link)
|
||||
}
|
||||
const userInfoEditRef = ref(null)
|
||||
const getUserInfoFn = ()=>{
|
||||
userInfoEditRef.value?.open()
|
||||
}
|
||||
// 修改密码 --- start
|
||||
let changePasswordDialog = ref(false)
|
||||
const formRef = ref<FormInstance>();
|
||||
// 提交信息
|
||||
let saveInfo = reactive({
|
||||
original_password: '',
|
||||
password: '',
|
||||
password_copy: ''
|
||||
});
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
original_password: [
|
||||
{ required: true, message: t("originalPasswordPlaceholder"), trigger: "blur" },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: t("passwordPlaceholder"), trigger: "blur" },
|
||||
],
|
||||
password_copy: [
|
||||
{ required: true, message: t("passwordPlaceholder"), trigger: "blur" },
|
||||
]
|
||||
});
|
||||
const submitForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
let msg = "";
|
||||
if (saveInfo.password && !saveInfo.original_password) msg = t('originalPasswordHint');
|
||||
if (saveInfo.password && saveInfo.original_password && !saveInfo.password_copy) msg = t('newPasswordHint');
|
||||
if (saveInfo.password && saveInfo.original_password && saveInfo.password_copy && saveInfo.password != saveInfo.password_copy) msg = t('doubleCipherHint');
|
||||
if (msg) {
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: msg,
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
setUserInfo(saveInfo).then((res: any) => {
|
||||
changePasswordDialog.value = false;
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
});
|
||||
}
|
||||
// 修改密码 --- end
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-popper .el-dropdown-menu{
|
||||
width: 150px;
|
||||
}
|
||||
</style>
|
||||
137
admin/src/layout/profession/components/tabs.vue
Normal file
137
admin/src/layout/profession/components/tabs.vue
Normal file
@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div class="tab-wrap w-full px-[16px]">
|
||||
<el-tabs :closable="tabbarStore.tabLength > 1" :model-value="route.path" @tab-click="tabClick"
|
||||
@tab-remove="removeTab">
|
||||
<el-tab-pane v-for="(tab, key, index) in tabbarStore.tabs" :name="tab.path" :key="index">
|
||||
<template #label>
|
||||
<el-dropdown trigger="contextmenu" placement="bottom-start">
|
||||
<span :class="{ 'text-primary': route.path == tab.path }" class="tab-name">{{ tab.title }}</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Back" :disabled="index == 0" @click="closeLeft(tab.path)">{{t('tabs.closeLeft') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Right" :disabled="index == (tabbarStore.tabLength - 1)" @click="closeRight(tab.path)">{{t('tabs.closeRight') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Close" :disabled="tabbarStore.tabLength == 1" @click="closeOther(tab.path)">{{t('tabs.closeOther') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, onMounted } from 'vue'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const tabbarStore = useTabbarStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
tabbarStore.addTab(route)
|
||||
})
|
||||
|
||||
watch(route, (nval: any) => {
|
||||
tabbarStore.addTab(nval)
|
||||
})
|
||||
|
||||
/**
|
||||
* 添加tab
|
||||
* @param content
|
||||
*/
|
||||
const tabClick = (content: any) => {
|
||||
const tabRoute = tabbarStore.tabs[content.props.name]
|
||||
router.push({ path: tabRoute.path, query: tabRoute.query })
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除tab
|
||||
* @param content
|
||||
*/
|
||||
const removeTab = (content: any) => {
|
||||
if (route.path == content) {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
router.push({ path: tabs[tabs.indexOf(content) - 1] })
|
||||
}
|
||||
tabbarStore.removeTab(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭左侧
|
||||
* @param path
|
||||
*/
|
||||
const closeLeft = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(path) - 1; i >= 0; i--) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ path })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭右侧
|
||||
* @param path
|
||||
*/
|
||||
const closeRight = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(path) + 1; i < tabs.length; i++) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ path })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭其他
|
||||
* @param path
|
||||
*/
|
||||
const closeOther = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
tabs.forEach((key: string) => { key != path && delete tabbarStore.tabs[key] })
|
||||
router.push({ path })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tabs) {
|
||||
.el-tabs--border-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap {
|
||||
margin-bottom: 0;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
display: inline-flex !important;
|
||||
padding: 0 20px !important;
|
||||
align-items: center;
|
||||
|
||||
.tab-name:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
44
admin/src/layout/profession/index.vue
Normal file
44
admin/src/layout/profession/index.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="common-layout min-w-[1200px]" >
|
||||
<el-container class="w-100 h-screen">
|
||||
<layout-aside></layout-aside>
|
||||
|
||||
<el-container>
|
||||
<el-header>
|
||||
<layout-header></layout-header>
|
||||
</el-header>
|
||||
|
||||
<el-main :class="['main-wrap h-full p-0 bg-page']">
|
||||
<el-scrollbar>
|
||||
<div class="p-[10px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
|
||||
<keep-alive :include="tabbarStore.tabNames">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import layoutHeader from './components/header/index.vue'
|
||||
import layoutAside from './components/aside/index.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const tabbarStore = useTabbarStore()
|
||||
const systemStore = useSystemStore()
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
4
admin/src/layout/profession/layout.json
Normal file
4
admin/src/layout/profession/layout.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"layout": "profession",
|
||||
"cover": "/app/assets/images/layout_profession.png"
|
||||
}
|
||||
@ -9,6 +9,7 @@ import { useElementIcon } from './utils/common'
|
||||
import 'highlight.js/styles/stackoverflow-light.css';
|
||||
import hljs from 'highlight.js/lib/common'
|
||||
import hljsVuePlugin from '@highlightjs/vue-plugin'
|
||||
import VueUeditorWrap from 'vue-ueditor-wrap'
|
||||
|
||||
window.hl = hljs
|
||||
|
||||
@ -19,6 +20,7 @@ async function run() {
|
||||
app.use(roter)
|
||||
app.use(ElementPlus)
|
||||
app.use(hljsVuePlugin)
|
||||
app.use(VueUeditorWrap)
|
||||
useElementIcon(app)
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
@ -8,9 +8,18 @@ import useUserStore from '@/stores/modules/user'
|
||||
import { setWindowTitle, getAppType, urlToRouteRaw } from '@/utils/common'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
// 加载插件中定义的router
|
||||
const ADDON_ROUTE = []
|
||||
const addonRoutes = import.meta.globEager('@/addon/**/router/index.ts')
|
||||
for (const key in addonRoutes) {
|
||||
const addon = addonRoutes[key]
|
||||
addon.ROUTE && ADDON_ROUTE.push(...addon.ROUTE)
|
||||
addon.NO_LOGIN_ROUTES && NO_LOGIN_ROUTES.push(...addon.NO_LOGIN_ROUTES)
|
||||
}
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [ADMIN_ROUTE, HOME_ROUTE, SITE_ROUTE, ...STATIC_ROUTES]
|
||||
routes: [ADMIN_ROUTE, HOME_ROUTE, SITE_ROUTE, ...STATIC_ROUTES, ...ADDON_ROUTE]
|
||||
})
|
||||
|
||||
/**
|
||||
@ -102,7 +111,13 @@ router.beforeEach(async (to, from, next) => {
|
||||
// 设置首页路由
|
||||
let firstRoute: symbol | string | undefined = findFirstValidRoute(userStore.routers)
|
||||
if (getAppType() != 'admin') {
|
||||
firstRoute = userStore.addonIndexRoute[ userStore.siteInfo?.apps[0].key ]
|
||||
for (let i = 0; i < userStore.siteInfo?.apps.length; i++) {
|
||||
const item = userStore.siteInfo?.apps[i]
|
||||
if (userStore.addonIndexRoute[item.key]) {
|
||||
firstRoute = userStore.addonIndexRoute[item.key]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ROOT_ROUTER.redirect = { name: firstRoute }
|
||||
|
||||
@ -35,6 +35,10 @@ export const ADMIN_ROUTE: RouteRecordRaw = {
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
meta: {
|
||||
type: 1,
|
||||
title: '用户登录'
|
||||
},
|
||||
component: () => import('@/app/views/login/index.vue')
|
||||
}
|
||||
]
|
||||
@ -74,6 +78,10 @@ export const SITE_ROUTE: RouteRecordRaw = {
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
meta: {
|
||||
type: 1,
|
||||
title: '用户登录'
|
||||
},
|
||||
component: () => import('@/app/views/login/index.vue')
|
||||
}
|
||||
]
|
||||
|
||||
@ -13,6 +13,7 @@ const useDiyStore = defineStore('diy', {
|
||||
currentComponent: 'edit-page', // 当前正在编辑的组件名称
|
||||
pageMode: 'diy',
|
||||
editTab: 'content',// 编辑页面
|
||||
pageTitle: '', // 页面名称(用于后台展示)
|
||||
name: '', // 页面标识
|
||||
type: '', // 页面模板
|
||||
typeName: '', // 页面模板名称
|
||||
@ -41,7 +42,7 @@ const useDiyStore = defineStore('diy', {
|
||||
components: [], // 组件集合
|
||||
position: ['top_fixed','right_fixed','bottom_fixed','left_fixed','fixed'],
|
||||
global: {
|
||||
title: "页面", // 页面标题
|
||||
title: "页面", // 页面标题(用于前台展示)
|
||||
|
||||
pageStartBgColor: "", // 页面背景颜色(开始)
|
||||
pageEndBgColor: "", // 页面背景颜色(结束)
|
||||
@ -51,14 +52,20 @@ const useDiyStore = defineStore('diy', {
|
||||
imgWidth: '', // 页面背景图片宽度
|
||||
imgHeight: '', // 页面背景图片高度
|
||||
|
||||
// 顶部状态栏
|
||||
// 顶部导航栏
|
||||
topStatusBar: {
|
||||
isShow: true, // 是否显示
|
||||
bgColor: "#ffffff", // 背景颜色
|
||||
isTransparent: false, // 是否透明
|
||||
isShow: true, // 是否显示
|
||||
style: 'style-1', // 风格样式
|
||||
textColor: "#333333", // 文字颜色
|
||||
style: 'style-1', // 导航栏风格样式(style-1:文字,style-2:图片+文字,style-3:图片+搜索,style-4:定位)
|
||||
styleName: '风格1',
|
||||
textColor: "#333333", // 文字颜色()
|
||||
textAlign: 'center', // 文字对齐方式
|
||||
inputPlaceholder : '请输入搜索关键词',
|
||||
imgUrl:'', // 图片
|
||||
link: { // 跳转链接
|
||||
name: ""
|
||||
}
|
||||
},
|
||||
|
||||
bottomTabBarSwitch: true, // 底部导航开关
|
||||
@ -129,14 +136,20 @@ const useDiyStore = defineStore('diy', {
|
||||
imgWidth: '', // 页面背景图片宽度
|
||||
imgHeight: '', // 页面背景图片高度
|
||||
|
||||
// 顶部状态栏
|
||||
// 顶部导航栏
|
||||
topStatusBar: {
|
||||
isShow: true, // 是否显示
|
||||
bgColor: "#ffffff", // 背景颜色
|
||||
isTransparent: false, // 是否透明
|
||||
isShow: true, // 是否显示
|
||||
style: 'style-1', // 风格样式
|
||||
textColor: "#333333", // 文字颜色
|
||||
style: 'style-1', // 导航栏风格样式(style-1:文字,style-2:图片+文字,style-3:图片+搜索,style-4:定位)
|
||||
styleName: '风格1',
|
||||
textColor: "#333333", // 文字颜色()
|
||||
textAlign: 'center', // 文字对齐方式
|
||||
inputPlaceholder : '请输入搜索关键词',
|
||||
imgUrl:'', // 图片
|
||||
link: { // 跳转链接
|
||||
name: ""
|
||||
}
|
||||
},
|
||||
|
||||
bottomTabBarSwitch: true, // 底部导航开关
|
||||
@ -206,7 +219,7 @@ const useDiyStore = defineStore('diy', {
|
||||
Object.assign(component, template);
|
||||
|
||||
if(component.template){
|
||||
// 按照组件初始的属性加载覆盖
|
||||
// 按照组件初始的属性覆盖默认值
|
||||
Object.assign(component, component.template);
|
||||
delete component.template;
|
||||
}
|
||||
@ -418,15 +431,24 @@ const useDiyStore = defineStore('diy', {
|
||||
},
|
||||
// 组件验证
|
||||
verify() {
|
||||
if (this.global.title === "") {
|
||||
if (this.pageTitle === "") {
|
||||
ElMessage({
|
||||
message: t('pageNamePlaceholder'),
|
||||
message: t('diyPageTitlePlaceholder'),
|
||||
type: 'warning'
|
||||
})
|
||||
this.changeCurrentIndex(-99);
|
||||
return false;
|
||||
}
|
||||
|
||||
// if (this.global.title === "") {
|
||||
// ElMessage({
|
||||
// message: t('diyTitlePlaceholder'),
|
||||
// type: 'warning'
|
||||
// })
|
||||
// this.changeCurrentIndex(-99);
|
||||
// return false;
|
||||
// }
|
||||
|
||||
for (var i = 0; i < this.value.length; i++) {
|
||||
try {
|
||||
if (this.value[i].verify) {
|
||||
|
||||
803
admin/src/stores/modules/poster.ts
Normal file
803
admin/src/stores/modules/poster.ts
Normal file
@ -0,0 +1,803 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {t} from '@/lang'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import {cloneDeep} from 'lodash-es'
|
||||
import {img} from '@/utils/common'
|
||||
|
||||
const usePosterStore = defineStore('poster', {
|
||||
state: () => {
|
||||
return {
|
||||
|
||||
contentBoxWidth: 360, // 360*2=720
|
||||
contentBoxHeight: 640, // 640*2=1280
|
||||
|
||||
id: 0,
|
||||
name: '', // 页面名称
|
||||
type: '', // 海报类型
|
||||
typeName: '',
|
||||
channel: '', // 海报支持的渠道
|
||||
status: 1, // 是否启用
|
||||
isDefault: 0, // 是否默认
|
||||
addon: '', // 海报所属插件
|
||||
|
||||
currentIndex: -99, // 当前正在编辑的组件下标
|
||||
currentComponent: 'edit-page', // 当前正在编辑的组件名称
|
||||
predefineColors: [
|
||||
'#F4391c',
|
||||
'#ff4500',
|
||||
'#ff8c00',
|
||||
'#FFD009',
|
||||
'#ffd700',
|
||||
'#19C650',
|
||||
'#90ee90',
|
||||
'#00ced1',
|
||||
'#1e90ff',
|
||||
'#c71585',
|
||||
'#FF407E',
|
||||
'#CFAF70',
|
||||
'#A253FF',
|
||||
'rgba(255, 69, 0, 0.68)',
|
||||
'rgb(255, 120, 0)',
|
||||
'hsl(181, 100%, 37%)',
|
||||
'hsla(209, 100%, 56%, 0.73)',
|
||||
'#c7158577'
|
||||
],
|
||||
components: <any>[], // 组件集合
|
||||
global: {
|
||||
width: 720, // 海报宽度
|
||||
height: 1280, // 海报高度
|
||||
bgType: 'url',
|
||||
bgColor: "#ffffff", // 背景颜色
|
||||
bgUrl: '', // 背景图片
|
||||
},
|
||||
// 组件类型,文本:text,image:图片,qrcode:二维码
|
||||
template: {
|
||||
width: 100, // 宽度
|
||||
height: 100, // 高度
|
||||
minWidth: 30, // 最小宽度
|
||||
minHeight: 30, // 最小高度
|
||||
x: 0, // 横向坐标 →
|
||||
y: 0, // 纵向坐标 ↑
|
||||
angle: 0, // 旋转角度 0~360
|
||||
zIndex: 0 // 层级
|
||||
},
|
||||
// 各组件类型的默认值
|
||||
templateType: {
|
||||
text: {
|
||||
height: 30,
|
||||
minWidth: 60,
|
||||
minHeight: 22,
|
||||
fontFamily: '',
|
||||
fontSize: 20,
|
||||
fontColor: '#303133'
|
||||
},
|
||||
image: {},
|
||||
qrcode: {},
|
||||
// 绘画
|
||||
draw: {
|
||||
draw_type: 'Polygon',
|
||||
points: [[0, 1210], [720, 1210], [720, 1280], [0, 1280]],
|
||||
bgColor: '#eeeeee'
|
||||
}
|
||||
},
|
||||
// 组件集合
|
||||
value: <any>[]
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
editComponent: (state) => {
|
||||
if (state.currentIndex == -99) {
|
||||
return state.global;
|
||||
} else {
|
||||
return state.value[state.currentIndex];
|
||||
}
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
// 初始化数据
|
||||
init() {
|
||||
this.global = {
|
||||
width: 720, // 海报宽度
|
||||
height: 1280, // 海报高度
|
||||
bgType: 'url',
|
||||
bgColor: "#ffffff", // 页面背景颜色(开始)
|
||||
bgUrl: '' // 页面背景图片
|
||||
};
|
||||
this.value = [];
|
||||
},
|
||||
// 添加组件
|
||||
addComponent(key: string, data: any) {
|
||||
|
||||
// 删除不用的字段
|
||||
let component = cloneDeep(data);
|
||||
|
||||
component.id = this.generateRandom();
|
||||
component.componentName = key;
|
||||
component.componentTitle = component.title;
|
||||
|
||||
delete component.title;
|
||||
delete component.icon;
|
||||
|
||||
// 继承默认属性
|
||||
let template: any = cloneDeep(this.template);
|
||||
Object.assign(component, template);
|
||||
|
||||
let templateType: any = cloneDeep(this.templateType);
|
||||
Object.assign(component, templateType[component.type]);
|
||||
|
||||
if (component.template) {
|
||||
// 按照组件初始的属性覆盖默认值
|
||||
Object.assign(component, component.template);
|
||||
delete component.template;
|
||||
}
|
||||
|
||||
if (!this.checkComponentIsAdd(component)) {
|
||||
// 组件最多只能添加n个
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: `${component.componentTitle}${t('componentCanOnlyAdd')}${component.uses}${t('piece')}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
component.zIndex = this.value.length + 1;
|
||||
this.value.push(component);
|
||||
// 添加组件后(不是编辑调用的),选择最后一个
|
||||
this.currentIndex = this.value.length - 1;
|
||||
|
||||
this.currentComponent = 'edit-' + component.path;
|
||||
},
|
||||
// 生成随机数
|
||||
generateRandom(len: number = 5) {
|
||||
return Number(Math.random().toString().substr(3, len) + Date.now()).toString(36);
|
||||
},
|
||||
// 选中正在编辑的组件
|
||||
changeCurrentIndex(index: number, component: any = null) {
|
||||
this.currentIndex = index;
|
||||
if (this.currentIndex == -99) {
|
||||
this.currentComponent = 'edit-page';
|
||||
} else if (component) {
|
||||
this.currentComponent = 'edit-' + component.path;
|
||||
}
|
||||
},
|
||||
// 删除组件
|
||||
delComponent() {
|
||||
if (this.currentIndex == -99) return;
|
||||
|
||||
ElMessageBox.confirm(
|
||||
t('delComponentTips'),
|
||||
t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning',
|
||||
autofocus: false
|
||||
}
|
||||
).then(() => {
|
||||
this.value.splice(this.currentIndex, 1);
|
||||
|
||||
// 如果组件全部删除,则选中页面设置
|
||||
if (this.value.length === 0) {
|
||||
this.currentIndex = -99;
|
||||
}
|
||||
|
||||
// 如果当前选中的组件不存在,则选择上一个
|
||||
if (this.currentIndex === this.value.length) {
|
||||
this.currentIndex--;
|
||||
}
|
||||
|
||||
let component = cloneDeep(this.value[this.currentIndex]);
|
||||
this.changeCurrentIndex(this.currentIndex, component)
|
||||
|
||||
}).catch(() => {
|
||||
})
|
||||
|
||||
},
|
||||
// 上移一层组件
|
||||
moveUpComponent() {
|
||||
if (this.currentIndex < -1) return; // 从0开始
|
||||
|
||||
this.value[this.currentIndex].zIndex++;
|
||||
if (this.value[this.currentIndex].zIndex >= this.value.length) {
|
||||
this.value[this.currentIndex].zIndex = this.value.length;
|
||||
}
|
||||
|
||||
},
|
||||
// 下移一层组件
|
||||
moveDownComponent() {
|
||||
if (this.currentIndex < -1) return; // 从0开始
|
||||
|
||||
this.value[this.currentIndex].zIndex--;
|
||||
|
||||
if (this.value[this.currentIndex].zIndex < 0) {
|
||||
this.value[this.currentIndex].zIndex = 0;
|
||||
}
|
||||
},
|
||||
// 复制组件
|
||||
copyComponent() {
|
||||
if (this.currentIndex < 0) return; // 从0开始
|
||||
|
||||
let component = cloneDeep(this.value[this.currentIndex]); // 当前选中组件
|
||||
|
||||
component.id = this.generateRandom(); // 更新id,刷新组件数据
|
||||
component.x = 0; // 重置坐标
|
||||
component.y = 0; // 重置坐标
|
||||
|
||||
// 暂不复制宽高
|
||||
// let box: any = document.getElementById(this.value[this.currentIndex].id)
|
||||
// component.width = box.offsetWidth
|
||||
// component.height = box.offsetHeight
|
||||
// component.auto = false;
|
||||
|
||||
if (!this.checkComponentIsAdd(component)) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: `${t('notCopy')},${component.componentTitle}${t('componentCanOnlyAdd')}${component.uses}${t('piece')}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var index = this.currentIndex + 1;
|
||||
this.value.splice(index, 0, component);
|
||||
|
||||
this.changeCurrentIndex(index, component);
|
||||
},
|
||||
// 检测组件是否允许添加,true:允许 false:不允许
|
||||
checkComponentIsAdd(component: any) {
|
||||
|
||||
//为0时不处理
|
||||
if (component.uses === 0) return true;
|
||||
|
||||
var count = 0;
|
||||
|
||||
//遍历已添加的自定义组件,检测是否超出数量
|
||||
for (var i in this.value) if (this.value[i].componentName === component.componentName) count++;
|
||||
|
||||
if (count >= component.uses) return false;
|
||||
else return true;
|
||||
},
|
||||
// 重置当前组件数据
|
||||
resetComponent() {
|
||||
if (this.currentIndex < 0) return; // 从0开始
|
||||
|
||||
ElMessageBox.confirm(
|
||||
t('resetComponentTips'),
|
||||
t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning',
|
||||
autofocus: false
|
||||
}
|
||||
).then(() => {
|
||||
// 重置当前选中的组件数据
|
||||
for (let i = 0; i < this.components.length; i++) {
|
||||
if (this.components[i].componentName == this.editComponent.componentName) {
|
||||
Object.assign(this.editComponent, this.components[i]);
|
||||
|
||||
let templateType: any = cloneDeep(this.templateType);
|
||||
Object.assign(this.editComponent, templateType[this.editComponent.type]);
|
||||
this.editComponent.angle = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}).catch(() => {
|
||||
})
|
||||
|
||||
},
|
||||
// 组件验证
|
||||
verify() {
|
||||
if (this.name === "") {
|
||||
ElMessage({
|
||||
message: t('posterNamePlaceholder'),
|
||||
type: 'warning'
|
||||
});
|
||||
this.changeCurrentIndex(-99);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.value.length == 0) {
|
||||
ElMessage({
|
||||
message: t('diyPosterValueEmptyTips'),
|
||||
type: 'warning'
|
||||
});
|
||||
this.changeCurrentIndex(-99);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < this.value.length; i++) {
|
||||
try {
|
||||
if (this.value[i].verify) {
|
||||
var res = this.value[i].verify(i);
|
||||
if (!res.code) {
|
||||
this.changeCurrentIndex(i, this.value[i]);
|
||||
ElMessage({
|
||||
message: res.message,
|
||||
type: 'warning'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("verify Error:", e, i, this.value[i]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 移动事件
|
||||
mouseDown(e: any, id: any, index: any) {
|
||||
const box: any = document.getElementById(id);
|
||||
const disX = e.clientX - box.offsetLeft;
|
||||
const disY = e.clientY - box.offsetTop;
|
||||
|
||||
// 鼠标移动时
|
||||
document.onmousemove = (e) => {
|
||||
if (this.contentBoxWidth == box.offsetWidth) {
|
||||
box.style.left = 0
|
||||
} else {
|
||||
box.style.left = e.clientX - disX + 'px'
|
||||
}
|
||||
box.style.top = e.clientY - disY + 'px';
|
||||
|
||||
// 边界判断
|
||||
if (e.clientX - disX < 0) {
|
||||
box.style.left = 0
|
||||
}
|
||||
|
||||
if (e.clientX - disX > this.contentBoxWidth - box.offsetWidth) {
|
||||
box.style.left = this.contentBoxWidth - box.offsetWidth + 'px'
|
||||
}
|
||||
|
||||
|
||||
if (e.clientY - disY < 0) {
|
||||
box.style.top = 0
|
||||
}
|
||||
|
||||
if (e.clientY - disY > this.contentBoxHeight - box.offsetHeight) {
|
||||
box.style.top = this.contentBoxHeight - box.offsetHeight + 'px'
|
||||
}
|
||||
|
||||
this.value[index].x = box.offsetLeft;
|
||||
this.value[index].y = box.offsetTop
|
||||
|
||||
};
|
||||
|
||||
// 鼠标抬起时
|
||||
document.onmouseup = (e) => {
|
||||
document.onmousemove = null
|
||||
}
|
||||
},
|
||||
// 拖拽缩放事件
|
||||
resizeMouseDown(e: any, item: any, index: any) {
|
||||
const oEv = e;
|
||||
oEv.stopPropagation();
|
||||
const box: any = document.getElementById(item.id);
|
||||
const className = e.target.className;
|
||||
|
||||
// 获取移动前盒子的宽高,
|
||||
const oldWidth = box.offsetWidth;
|
||||
const oldHeight = box.offsetHeight;
|
||||
|
||||
// 获取鼠标距离屏幕的left和top值
|
||||
const oldX = oEv.clientX;
|
||||
const oldY = oEv.clientY;
|
||||
|
||||
// 元素相对于最近的父级定位
|
||||
const oldLeft = box.offsetLeft;
|
||||
const oldTop = box.offsetTop;
|
||||
|
||||
// 设置最小的宽度
|
||||
let minWidth = 100;
|
||||
let minHeight = 100;
|
||||
|
||||
if (item.type == 'text') {
|
||||
// 文本类型
|
||||
minWidth = 60;
|
||||
minHeight = 22;
|
||||
} else if (item.type == 'image' || item.type == 'qrcode') {
|
||||
// 图片类型
|
||||
minWidth = 30;
|
||||
minHeight = 30;
|
||||
} else if (item.type == 'draw') {
|
||||
// 绘画类型
|
||||
minWidth = 20;
|
||||
minHeight = 20;
|
||||
}
|
||||
|
||||
document.onmousemove = (e) => {
|
||||
const oEv = e;
|
||||
// console.log('move', "width:" + oldWidth,
|
||||
// ',oldLeft: ' + oldLeft, ',oldTop: ' + oldTop,
|
||||
// ',oldX:clientX-- ' + oldX + ':' + oEv.clientX,
|
||||
// ',oldY:clientY-- ' + oldY + ':' + oEv.clientY,
|
||||
// )
|
||||
|
||||
// 左上角
|
||||
if (className == 'box1') {
|
||||
let width = oldWidth - (oEv.clientX - oldX);
|
||||
const maxWidth = this.contentBoxWidth;
|
||||
|
||||
let height = oldHeight - (oEv.clientY - oldY);
|
||||
const maxHeight = this.contentBoxHeight - oldTop;
|
||||
|
||||
let left = oldLeft + (oEv.clientX - oldX);
|
||||
let top = oldTop + (oEv.clientY - oldY);
|
||||
|
||||
if (width < minWidth) {
|
||||
width = minWidth
|
||||
}
|
||||
if (width > maxWidth) {
|
||||
width = maxWidth
|
||||
}
|
||||
|
||||
if (height < minHeight) {
|
||||
height = minHeight
|
||||
}
|
||||
if (height > maxHeight) {
|
||||
height = maxHeight
|
||||
}
|
||||
|
||||
if (oldLeft == 0 && oldTop == 0) {
|
||||
// 坐标:left = 0,top = 0
|
||||
|
||||
if (width == minWidth && height == minHeight) {
|
||||
// 宽高 = 最小值,left = 最小宽度,top = 最小高度
|
||||
left = minWidth;
|
||||
top = minHeight;
|
||||
} else if (width == minWidth && height > minHeight) {
|
||||
// 宽 = 最小值,高 > 最小值,left = 最小宽度,top = 不予处理
|
||||
left = minWidth;
|
||||
} else if (width > minWidth && height == minHeight) {
|
||||
// 宽 > 最小值,高 = 最小值,left = 不予处理,top = 最小高度
|
||||
top = minHeight;
|
||||
} else if (width > minWidth && height > minHeight) {
|
||||
// 宽 > 最小值,高 > 最小值,left = 不予处理,top = 不予处理
|
||||
}
|
||||
} else if (oldLeft == 0 && oldTop > 0) {
|
||||
// 坐标:left = 0,top > 0
|
||||
|
||||
if (width == minWidth && height == minHeight) {
|
||||
// 宽高 = 最小值,left = 最小宽度,top = 元素上偏移位置
|
||||
left = minWidth;
|
||||
top = box.offsetTop;
|
||||
} else if (width == minWidth && height > minHeight) {
|
||||
// 宽 = 最小值,高 > 最小值,left = 最小宽度,top = 元素上偏移位置
|
||||
left = minWidth;
|
||||
top = box.offsetTop;
|
||||
} else if (width > minWidth && height == minHeight) {
|
||||
// 宽 > 最小值,高 = 最小值,left = 不予处理,top = 元素上偏移位置
|
||||
top = box.offsetTop;
|
||||
} else if (width > minWidth && height > minHeight) {
|
||||
// 宽 > 最小值,高 > 最小值,left = 不予处理,top = 不予处理
|
||||
}
|
||||
} else if (oldLeft > 0 && oldTop == 0) {
|
||||
// 坐标:left > 0,top = 0
|
||||
|
||||
if (width == minWidth && height == minHeight) {
|
||||
// 宽高 = 最小值,left = 元素左偏移位置,top = 元素上偏移位置
|
||||
left = box.offsetLeft;
|
||||
top = box.offsetTop;
|
||||
} else if (width == minWidth && height > minHeight) {
|
||||
// 宽 = 最小值,高 > 最小值,left = 元素左偏移位置,top = 0
|
||||
left = box.offsetLeft;
|
||||
top = 0;
|
||||
} else if (width > minWidth && height == minHeight) {
|
||||
// 宽 > 最小值,高 = 最小值,left = 不予处理,top = 元素上偏移位置
|
||||
top = box.offsetTop;
|
||||
} else if (width > minWidth && height > minHeight) {
|
||||
// 宽 > 最小值,高 > 最小值,left = 不予处理,top = 不予处理
|
||||
}
|
||||
} else if (oldLeft > 0 && oldTop > 0) {
|
||||
// 坐标:left > 0,top > 0
|
||||
|
||||
if (width == minWidth && height == minHeight) {
|
||||
// 宽高 = 最小值,left = 元素左偏移位置,top = 元素上偏移位置
|
||||
left = box.offsetLeft;
|
||||
top = box.offsetTop;
|
||||
} else if (width == minWidth && height > minHeight) {
|
||||
// 宽 = 最小值,高 > 最小值,left = 元素左偏移位置,top = 元素上偏移位置
|
||||
left = box.offsetLeft;
|
||||
top = box.offsetTop;
|
||||
} else if (width > minWidth && height == minHeight) {
|
||||
// 宽 > 最小值,高 = 最小值,left = 不予处理,top = 元素上偏移位置
|
||||
top = box.offsetTop;
|
||||
} else if (width > minWidth && height > minHeight) {
|
||||
// 宽 > 最小值,高 > 最小值,left = 不予处理,top = 不予处理
|
||||
}
|
||||
}
|
||||
|
||||
// 左上宽
|
||||
if (left < 0) {
|
||||
left = 0;
|
||||
width = oldWidth - (oEv.clientX - oldX) + (oldLeft + (oEv.clientX - oldX));
|
||||
}
|
||||
|
||||
// 左上 高
|
||||
if (top < 0) {
|
||||
top = 0;
|
||||
height = oldTop + (oEv.clientY - oldY) + (oldHeight - (oEv.clientY - oldY));
|
||||
}
|
||||
|
||||
box.children[0].style.width = width + 'px';
|
||||
|
||||
// 文本设置高度,图片自适应 无需设置
|
||||
if (item.type == 'text' || item.type == 'draw') {
|
||||
box.children[0].style.height = height + 'px';
|
||||
}
|
||||
box.style.left = left + 'px';
|
||||
box.style.top = top + 'px';
|
||||
} else if (className == 'box2') {
|
||||
// 右上角
|
||||
|
||||
let width = oldWidth + (oEv.clientX - oldX);
|
||||
const maxWidth = this.contentBoxWidth - oldLeft;
|
||||
|
||||
let height = oldHeight - (oEv.clientY - oldY);
|
||||
const maxHeight = this.contentBoxHeight - oldTop;
|
||||
|
||||
let top = oldTop + (oEv.clientY - oldY);
|
||||
|
||||
if (width < minWidth) {
|
||||
width = minWidth
|
||||
}
|
||||
if (width > maxWidth) {
|
||||
width = maxWidth
|
||||
}
|
||||
|
||||
if (height < minHeight) {
|
||||
height = minHeight
|
||||
}
|
||||
if (height > maxHeight) {
|
||||
height = maxHeight
|
||||
}
|
||||
|
||||
if (oldLeft == 0 && oldTop == 0) {
|
||||
// 坐标:left = 0,top = 0
|
||||
|
||||
if (width == minWidth && height == minHeight) {
|
||||
// 宽高 = 最小值,top = 最小高度
|
||||
top = minHeight
|
||||
} else if (width == minWidth && height > minHeight) {
|
||||
// 宽 = 最小值,高 > 最小值,不予处理
|
||||
} else if (width > minWidth && height == minHeight) {
|
||||
// 宽 > 最小值,高 = 最小值,top = 最小高度
|
||||
top = minHeight
|
||||
} else if (width > minWidth && height > minHeight) {
|
||||
// 宽 > 最小值,高 > 最小值,不予处理
|
||||
}
|
||||
} else if (oldLeft == 0 && oldTop > 0) {
|
||||
// 坐标:left = 0,top > 0
|
||||
|
||||
if (width == minWidth && height == minHeight) {
|
||||
// 宽高 = 最小值,top = 元素上偏移位置
|
||||
top = box.offsetTop
|
||||
} else if (width == minWidth && height > minHeight) {
|
||||
// 宽 = 最小值,高 > 最小值,top = 元素上偏移位置
|
||||
top = box.offsetTop
|
||||
} else if (width > minWidth && height == minHeight) {
|
||||
// 宽 > 最小值,高 = 最小值,top = 元素上偏移位置
|
||||
top = box.offsetTop
|
||||
} else if (width > minWidth && height > minHeight) {
|
||||
// 宽 > 最小值,高 > 最小值,不予处理
|
||||
}
|
||||
} else if (oldLeft > 0 && oldTop == 0) {
|
||||
// 坐标:left = 0,top = 0
|
||||
|
||||
if (width == minWidth && height == minHeight) {
|
||||
// 宽高 = 最小值,top = 元素上偏移位置
|
||||
top = box.offsetTop
|
||||
} else if (width == minWidth && height > minHeight) {
|
||||
// 宽 = 最小值,高 > 最小值,top = 0
|
||||
top = 0
|
||||
} else if (width > minWidth && height == minHeight) {
|
||||
// 宽 > 最小值,高 = 最小值,top = 元素上偏移位置
|
||||
top = box.offsetTop
|
||||
} else if (width > minWidth && height > minHeight) {
|
||||
// 宽 > 最小值,高 > 最小值,不予处理
|
||||
}
|
||||
} else if (oldLeft > 0 && oldTop > 0) {
|
||||
// 坐标:left > 0,top > 0
|
||||
|
||||
if (width == minWidth && height == minHeight) {
|
||||
// 宽高 = 最小值,top = 元素上偏移位置
|
||||
top = box.offsetTop
|
||||
} else if (width == minWidth && height > minHeight) {
|
||||
// 宽 = 最小值,高 > 最小值,top = 元素上偏移位置
|
||||
top = box.offsetTop
|
||||
} else if (width > minWidth && height == minHeight) {
|
||||
// 宽 > 最小值,高 = 最小值,top = 元素上偏移位置
|
||||
top = box.offsetTop
|
||||
} else if (width > minWidth && height > minHeight) {
|
||||
// 宽 > 最小值,高 > 最小值,不予处理
|
||||
}
|
||||
}
|
||||
|
||||
// 右上高
|
||||
if (top < 0) {
|
||||
top = 0;
|
||||
height = oldTop + (oEv.clientY - oldY) + (oldHeight - (oEv.clientY - oldY))
|
||||
}
|
||||
|
||||
box.children[0].style.width = width + 'px';
|
||||
|
||||
// 文本设置高度,图片自适应 无需设置
|
||||
if (item.type == 'text' || item.type == 'draw') {
|
||||
box.children[0].style.height = height + 'px'
|
||||
}
|
||||
box.style.top = top + 'px'
|
||||
} else if (className == 'box3') {
|
||||
// 左下角
|
||||
|
||||
let width = oldWidth - (oEv.clientX - oldX);
|
||||
const maxWidth = this.contentBoxWidth;
|
||||
|
||||
let height = oldHeight + (oEv.clientY - oldY);
|
||||
const maxHeight = this.contentBoxHeight - oldTop;
|
||||
|
||||
let left = oldLeft + (oEv.clientX - oldX);
|
||||
|
||||
if (width < minWidth) {
|
||||
width = minWidth
|
||||
}
|
||||
if (width > maxWidth) {
|
||||
width = maxWidth
|
||||
}
|
||||
|
||||
if (height < minHeight) {
|
||||
height = minHeight
|
||||
}
|
||||
if (height > maxHeight) {
|
||||
height = maxHeight
|
||||
}
|
||||
|
||||
if (oldLeft == 0 && oldTop == 0) {
|
||||
// 坐标:left = 0,top = 0
|
||||
|
||||
if (width == minWidth && height == minHeight) {
|
||||
// 宽高 = 最小值,left = 最小宽度
|
||||
left = minWidth
|
||||
} else if (width == minWidth && height > minHeight) {
|
||||
// 宽 = 最小值,高 > 最小值,left = 最小宽度
|
||||
left = minWidth
|
||||
} else if (width > minWidth && height == minHeight) {
|
||||
// 宽 > 最小值,高 = 最小值,不予处理
|
||||
} else if (width > minWidth && height > minHeight) {
|
||||
// 宽 > 最小值,高 > 最小值,不予处理
|
||||
}
|
||||
} else if (oldLeft == 0 && oldTop > 0) {
|
||||
// 坐标:left = 0,top > 0
|
||||
|
||||
if (width == minWidth && height == minHeight) {
|
||||
// 宽高 = 最小值,left = 最小宽度
|
||||
left = minWidth
|
||||
} else if (width == minWidth && height > minHeight) {
|
||||
// 宽 = 最小值,高 > 最小值,left = 最小宽度
|
||||
left = minWidth
|
||||
} else if (width > minWidth && height == minHeight) {
|
||||
// 宽 > 最小值,高 = 最小值,不予处理
|
||||
} else if (width > minWidth && height > minHeight) {
|
||||
// 宽 > 最小值,高 > 最小值,不予处理
|
||||
}
|
||||
} else if (oldLeft > 0 && oldTop == 0) {
|
||||
// 坐标:left > 0,top = 0
|
||||
|
||||
if (width == minWidth && height == minHeight) {
|
||||
// 宽高 = 最小值,left = 元素左偏移位置
|
||||
left = box.offsetLeft
|
||||
} else if (width == minWidth && height > minHeight) {
|
||||
// 宽 = 最小值,高 > 最小值,left = 元素左偏移位置
|
||||
left = box.offsetLeft
|
||||
} else if (width > minWidth && height == minHeight) {
|
||||
// 宽 > 最小值,高 = 最小值,不予处理
|
||||
} else if (width > minWidth && height > minHeight) {
|
||||
// 宽 > 最小值,高 > 最小值,不予处理
|
||||
}
|
||||
} else if (oldLeft > 0 && oldTop > 0) {
|
||||
// 坐标:left > 0,top > 0
|
||||
|
||||
if (width == minWidth && height == minHeight) {
|
||||
// 宽高 = 最小值,left = 元素左偏移位置
|
||||
left = box.offsetLeft
|
||||
} else if (width == minWidth && height > minHeight) {
|
||||
// 宽 = 最小值,高 > 最小值,left = 元素左偏移位置
|
||||
left = box.offsetLeft
|
||||
} else if (width > minWidth && height == minHeight) {
|
||||
// 宽 > 最小值,高 = 最小值,不予处理
|
||||
} else if (width > minWidth && height > minHeight) {
|
||||
// 宽 > 最小值,高 > 最小值,不予处理
|
||||
}
|
||||
}
|
||||
|
||||
if (left < 0) {
|
||||
left = 0;
|
||||
width = oldWidth - (oEv.clientX - oldX) + (oldLeft + (oEv.clientX - oldX))
|
||||
}
|
||||
|
||||
box.children[0].style.width = width + 'px';
|
||||
|
||||
// 文本设置高度,图片自适应 无需设置
|
||||
if (item.type == 'text' || item.type == 'draw') {
|
||||
box.children[0].style.height = height + 'px'
|
||||
}
|
||||
box.style.left = left + 'px'
|
||||
} else if (className == 'box4') {
|
||||
// 右下角
|
||||
|
||||
let width = oldWidth + (oEv.clientX - oldX);
|
||||
const maxWidth = this.contentBoxWidth - oldLeft;
|
||||
|
||||
let height = oldHeight + (oEv.clientY - oldY);
|
||||
const maxHeight = this.contentBoxHeight - oldTop;
|
||||
|
||||
if (width < minWidth) {
|
||||
width = minWidth
|
||||
}
|
||||
if (width > maxWidth) {
|
||||
width = maxWidth
|
||||
}
|
||||
|
||||
if (height < minHeight) {
|
||||
height = minHeight
|
||||
}
|
||||
if (height > maxHeight) {
|
||||
height = maxHeight
|
||||
}
|
||||
|
||||
box.children[0].style.width = width + 'px';
|
||||
|
||||
// 文本设置高度,图片自适应 无需设置
|
||||
if (item.type == 'text' || item.type == 'draw') {
|
||||
box.children[0].style.height = height + 'px'
|
||||
}
|
||||
}
|
||||
|
||||
this.value[index].x = box.offsetLeft;
|
||||
this.value[index].y = box.offsetTop;
|
||||
|
||||
this.value[index].width = parseInt(box.children[0].style.width.replace('px', ''));
|
||||
|
||||
};
|
||||
|
||||
// 鼠标抬起时
|
||||
document.onmouseup = () => {
|
||||
document.onmousemove = null;
|
||||
document.onmouseup = null
|
||||
}
|
||||
},
|
||||
getGlobalStyle() {
|
||||
let style = '';
|
||||
if (this.global.bgType == 'color') {
|
||||
style += `background-color:${this.global.bgColor};`;
|
||||
} else if (this.global.bgType == 'url') {
|
||||
if (this.global.bgUrl) {
|
||||
style += `background-image:url("${img(this.global.bgUrl)}")`;
|
||||
}
|
||||
}
|
||||
return style;
|
||||
},
|
||||
getMaxX() {
|
||||
const box: any = document.getElementById(this.editComponent.id);
|
||||
let x = this.contentBoxWidth;
|
||||
if (box) {
|
||||
x -= box.offsetWidth;
|
||||
}
|
||||
return x;
|
||||
},
|
||||
getMaxY() {
|
||||
const box: any = document.getElementById(this.editComponent.id);
|
||||
let y = this.contentBoxHeight;
|
||||
if (box) {
|
||||
y -= box.offsetHeight;
|
||||
}
|
||||
return y;
|
||||
},
|
||||
getMaxWidth() {
|
||||
let width = this.contentBoxWidth;
|
||||
width -= this.editComponent.x;
|
||||
return width;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export default usePosterStore
|
||||
@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import storage from '@/utils/storage'
|
||||
import { useCssVar } from '@vueuse/core'
|
||||
import { getWebConfig } from '@/app/api/sys'
|
||||
import {getWebConfig, getWebsiteLayout} from '@/app/api/sys'
|
||||
|
||||
interface System {
|
||||
menuIsCollapse: boolean,
|
||||
@ -12,7 +12,8 @@ interface System {
|
||||
sidebar: string,
|
||||
sidebarStyle: string,
|
||||
currHeadMenuName: any,
|
||||
website: Object
|
||||
website: Object,
|
||||
layoutConfig: Object
|
||||
}
|
||||
|
||||
const theme = storage.get('theme') ?? {}
|
||||
@ -20,7 +21,6 @@ const theme = storage.get('theme') ?? {}
|
||||
const useSystemStore = defineStore('system', {
|
||||
state: (): System => {
|
||||
return {
|
||||
// menuIsCollapse: storage.get('menuiscollapse') ?? false,
|
||||
menuIsCollapse: false,
|
||||
menuDrawer: false,
|
||||
dark: theme.dark ?? false,
|
||||
@ -29,7 +29,8 @@ const useSystemStore = defineStore('system', {
|
||||
lang: storage.get('lang') ?? 'zh-cn',
|
||||
sidebarStyle: theme.sidebarStyle ?? 'threeType',
|
||||
currHeadMenuName: '',
|
||||
website: {}
|
||||
website: {},
|
||||
layoutConfig: {}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
@ -50,6 +51,11 @@ const useSystemStore = defineStore('system', {
|
||||
await getWebConfig().then(({ data }) => {
|
||||
this.website = data
|
||||
}).catch()
|
||||
},
|
||||
async getWebsiteLayout() {
|
||||
await getWebsiteLayout().then(({ data }) => {
|
||||
this.layoutConfig = data
|
||||
}).catch()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -27,16 +27,6 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
.w-e-full-screen-container {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.w-e-toolbar {
|
||||
.w-e-bar-divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.main-container{
|
||||
// background-color: #fff;
|
||||
background-color: var(--el-bg-color-overlay);
|
||||
@ -64,7 +54,7 @@ html {
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -82,12 +72,6 @@ html.dark {
|
||||
.table-search-wrap {
|
||||
background-color: var(--el-bg-color)!important;
|
||||
}
|
||||
--w-e-toolbar-bg-color: var(--el-bg-color-overlay);
|
||||
--w-e-textarea-bg-color: var(--el-bg-color-overlay);
|
||||
--w-e-textarea-color: var(--el-input-text-color);
|
||||
--w-e-toolbar-border-color: var(--el-border-color);
|
||||
--w-e-toolbar-active-bg-color:var(--el-bg-color);
|
||||
--w-e-toolbar-active-color:var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
:root input:-webkit-autofill,
|
||||
@ -124,6 +108,7 @@ select:-webkit-autofill {
|
||||
|
||||
.region-input {
|
||||
--region-input-border-color: var(--el-border-color);
|
||||
--el-input-border-radius: 0;
|
||||
display: flex;
|
||||
box-shadow: 0 0 0 1px var(--region-input-border-color) inset;
|
||||
border-radius: var(--el-input-border-radius,var(--el-border-radius-base));
|
||||
@ -201,30 +186,30 @@ html.dark {
|
||||
}
|
||||
|
||||
// 详情的头部
|
||||
.detail-head{
|
||||
.detail-head {
|
||||
display: flex;
|
||||
margin: 15px;
|
||||
align-items: center;
|
||||
margin-left: 30px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
.left{
|
||||
color: #666;
|
||||
margin-top: 1px;
|
||||
|
||||
.left {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
margin-top: 1px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
.adorn{
|
||||
color: #999;
|
||||
margin: 0 12px;
|
||||
|
||||
.adorn {
|
||||
font-size: 14px;
|
||||
margin: 0 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.right{
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ********************************************** 修改整体样式 **********************************************
|
||||
// 修改选择框、ipnut、时间选择、按钮,input带按钮的圆角
|
||||
.el-input__wrapper, .el-input-group__append, .el-textarea__inner{
|
||||
@ -248,19 +233,7 @@ html.dark {
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
// 滚动条
|
||||
//.el-scrollbar__bar.is-vertical{
|
||||
// width: 10px !important;
|
||||
//}
|
||||
//.el-scrollbar__bar.is-vertical .el-scrollbar__thumb{
|
||||
// background-color: #8b8b8b !important;
|
||||
// opacity: 1 !important;
|
||||
// width: 9px !important;
|
||||
//}
|
||||
//.el-scrollbar__bar.is-vertical .el-scrollbar__thumb:hover{
|
||||
// background-color: #636363 !important;
|
||||
// opacity: 1 !important;
|
||||
//}
|
||||
|
||||
.el-card{
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
@ -269,4 +242,4 @@ html.dark {
|
||||
}
|
||||
.text-page-title{
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,19 +34,22 @@
|
||||
}
|
||||
|
||||
|
||||
.el-textarea__inner::-webkit-scrollbar {
|
||||
width: 6px ;
|
||||
height: 6px ;
|
||||
}
|
||||
.el-textarea__inner {
|
||||
|
||||
.el-textarea__inner::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px ;
|
||||
-moz-border-radius: 3px ;
|
||||
-webkit-border-radius: 3px ;
|
||||
background-color: #909399;
|
||||
opacity: .3;
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px ;
|
||||
height: 6px ;
|
||||
}
|
||||
|
||||
.el-textarea__inner::-webkit-scrollbar-track {
|
||||
background-color: transparent ;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px ;
|
||||
-moz-border-radius: 3px ;
|
||||
-webkit-border-radius: 3px ;
|
||||
background-color: #909399;
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent ;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +1,2 @@
|
||||
/* addon iconfont */
|
||||
@import "addon/o2o/iconfont.css";
|
||||
@import "addon/tourism/iconfont.css";
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 3883393 */
|
||||
src: url('//at.alicdn.com/t/c/font_3883393_eqk4fw84z0e.woff2?t=1712631982793') format('woff2'),
|
||||
url('//at.alicdn.com/t/c/font_3883393_eqk4fw84z0e.woff?t=1712631982793') format('woff'),
|
||||
url('//at.alicdn.com/t/c/font_3883393_eqk4fw84z0e.ttf?t=1712631982793') format('truetype');
|
||||
src: url('//at.alicdn.com/t/c/font_3883393_t75he8bd12.woff2?t=1715329624274') format('woff2'),
|
||||
url('//at.alicdn.com/t/c/font_3883393_t75he8bd12.woff?t=1715329624274') format('woff'),
|
||||
url('//at.alicdn.com/t/c/font_3883393_t75he8bd12.ttf?t=1715329624274') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@ -13,6 +13,158 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.iconjifenshangpin:before {
|
||||
content: "\e707";
|
||||
}
|
||||
|
||||
.iconnicheng1:before {
|
||||
content: "\e708";
|
||||
}
|
||||
|
||||
.iconhuihua1:before {
|
||||
content: "\e709";
|
||||
}
|
||||
|
||||
.icongeren:before {
|
||||
content: "\e70a";
|
||||
}
|
||||
|
||||
.iconhuiyuan12:before {
|
||||
content: "\e70b";
|
||||
}
|
||||
|
||||
.iconerweima:before {
|
||||
content: "\e70d";
|
||||
}
|
||||
|
||||
.iconhuajiaqian:before {
|
||||
content: "\e70e";
|
||||
}
|
||||
|
||||
.iconshoujia:before {
|
||||
content: "\e70f";
|
||||
}
|
||||
|
||||
.iconshangpintupian:before {
|
||||
content: "\e710";
|
||||
}
|
||||
|
||||
.icontupian1:before {
|
||||
content: "\e711";
|
||||
}
|
||||
|
||||
.iconjinbi:before {
|
||||
content: "\e712";
|
||||
}
|
||||
|
||||
.iconhuangguan:before {
|
||||
content: "\e713";
|
||||
}
|
||||
|
||||
.icona-Group13:before {
|
||||
content: "\e712";
|
||||
}
|
||||
|
||||
.iconyingxiao:before {
|
||||
content: "\e706";
|
||||
}
|
||||
|
||||
.iconxiazai19:before {
|
||||
content: "\e682";
|
||||
}
|
||||
|
||||
.iconshangjiantou:before {
|
||||
content: "\e678";
|
||||
}
|
||||
|
||||
.iconxiajiantou:before {
|
||||
content: "\e681";
|
||||
}
|
||||
|
||||
.iconriqi:before {
|
||||
content: "\e657";
|
||||
}
|
||||
|
||||
.icontuikuan:before {
|
||||
content: "\e75e";
|
||||
}
|
||||
|
||||
.icongouwu:before {
|
||||
content: "\e65a";
|
||||
}
|
||||
|
||||
.icondaishouhuo:before {
|
||||
content: "\e65c";
|
||||
}
|
||||
|
||||
.icondaifahuo:before {
|
||||
content: "\e669";
|
||||
}
|
||||
|
||||
.iconguahao:before {
|
||||
content: "\e66a";
|
||||
}
|
||||
|
||||
.icondaifukuan:before {
|
||||
content: "\e672";
|
||||
}
|
||||
|
||||
.icon31yiguanzhudianpu:before {
|
||||
content: "\e656";
|
||||
}
|
||||
|
||||
.icon31huidaodingbu:before {
|
||||
content: "\e658";
|
||||
}
|
||||
|
||||
.iconbianji:before {
|
||||
content: "\e659";
|
||||
}
|
||||
|
||||
.icontuihuobaozhang:before {
|
||||
content: "\e653";
|
||||
}
|
||||
|
||||
.iconhome:before {
|
||||
content: "\e633";
|
||||
}
|
||||
|
||||
.icon31daifahuo:before {
|
||||
content: "\e634";
|
||||
}
|
||||
|
||||
.icon31daifukuan:before {
|
||||
content: "\e637";
|
||||
}
|
||||
|
||||
.icontuikuantuihuo:before {
|
||||
content: "\e639";
|
||||
}
|
||||
|
||||
.icon31dianhua:before {
|
||||
content: "\e63c";
|
||||
}
|
||||
|
||||
.icon31shijian:before {
|
||||
content: "\e63d";
|
||||
}
|
||||
|
||||
.iconshuiqijiaoliu:before {
|
||||
content: "\e67d";
|
||||
}
|
||||
|
||||
.iconguanwang:before {
|
||||
content: "\e704";
|
||||
}
|
||||
|
||||
.icontransferout:before {
|
||||
content: "\e651";
|
||||
}
|
||||
|
||||
.icontransferout-copy:before {
|
||||
content: "\ecad";
|
||||
}
|
||||
|
||||
.iconxingzhuang-wenzi:before {
|
||||
content: "\eb99";
|
||||
}
|
||||
|
||||
@ -5,6 +5,272 @@
|
||||
"css_prefix_text": "icon",
|
||||
"description": "系统图标",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "40268072",
|
||||
"name": "积分商品",
|
||||
"font_class": "jifenshangpin",
|
||||
"unicode": "e707",
|
||||
"unicode_decimal": 59143
|
||||
},
|
||||
{
|
||||
"icon_id": "40266595",
|
||||
"name": "昵称",
|
||||
"font_class": "nicheng1",
|
||||
"unicode": "e708",
|
||||
"unicode_decimal": 59144
|
||||
},
|
||||
{
|
||||
"icon_id": "8361787",
|
||||
"name": "绘画",
|
||||
"font_class": "huihua1",
|
||||
"unicode": "e709",
|
||||
"unicode_decimal": 59145
|
||||
},
|
||||
{
|
||||
"icon_id": "8361772",
|
||||
"name": "个人3",
|
||||
"font_class": "geren",
|
||||
"unicode": "e70a",
|
||||
"unicode_decimal": 59146
|
||||
},
|
||||
{
|
||||
"icon_id": "8361774",
|
||||
"name": "会员",
|
||||
"font_class": "huiyuan12",
|
||||
"unicode": "e70b",
|
||||
"unicode_decimal": 59147
|
||||
},
|
||||
{
|
||||
"icon_id": "8361765",
|
||||
"name": "二维码",
|
||||
"font_class": "erweima",
|
||||
"unicode": "e70d",
|
||||
"unicode_decimal": 59149
|
||||
},
|
||||
{
|
||||
"icon_id": "40266986",
|
||||
"name": "划价签",
|
||||
"font_class": "huajiaqian",
|
||||
"unicode": "e70e",
|
||||
"unicode_decimal": 59150
|
||||
},
|
||||
{
|
||||
"icon_id": "40267219",
|
||||
"name": "售价",
|
||||
"font_class": "shoujia",
|
||||
"unicode": "e70f",
|
||||
"unicode_decimal": 59151
|
||||
},
|
||||
{
|
||||
"icon_id": "40267446",
|
||||
"name": "商品图片",
|
||||
"font_class": "shangpintupian",
|
||||
"unicode": "e710",
|
||||
"unicode_decimal": 59152
|
||||
},
|
||||
{
|
||||
"icon_id": "639372",
|
||||
"name": "图片",
|
||||
"font_class": "tupian1",
|
||||
"unicode": "e711",
|
||||
"unicode_decimal": 59153
|
||||
},
|
||||
{
|
||||
"icon_id": "8361802",
|
||||
"name": "金币",
|
||||
"font_class": "jinbi",
|
||||
"unicode": "e712",
|
||||
"unicode_decimal": 59154
|
||||
},
|
||||
{
|
||||
"icon_id": "8361785",
|
||||
"name": "皇冠2",
|
||||
"font_class": "huangguan",
|
||||
"unicode": "e713",
|
||||
"unicode_decimal": 59155
|
||||
},
|
||||
{
|
||||
"icon_id": "40262026",
|
||||
"name": "Group 13",
|
||||
"font_class": "a-Group13",
|
||||
"unicode": "e712",
|
||||
"unicode_decimal": 59154
|
||||
},
|
||||
{
|
||||
"icon_id": "5389828",
|
||||
"name": "营销",
|
||||
"font_class": "yingxiao",
|
||||
"unicode": "e706",
|
||||
"unicode_decimal": 59142
|
||||
},
|
||||
{
|
||||
"icon_id": "720977",
|
||||
"name": "坐标",
|
||||
"font_class": "xiazai19",
|
||||
"unicode": "e682",
|
||||
"unicode_decimal": 59010
|
||||
},
|
||||
{
|
||||
"icon_id": "1718351",
|
||||
"name": "上箭头",
|
||||
"font_class": "shangjiantou",
|
||||
"unicode": "e678",
|
||||
"unicode_decimal": 59000
|
||||
},
|
||||
{
|
||||
"icon_id": "1718353",
|
||||
"name": "下箭头",
|
||||
"font_class": "xiajiantou",
|
||||
"unicode": "e681",
|
||||
"unicode_decimal": 59009
|
||||
},
|
||||
{
|
||||
"icon_id": "5287753",
|
||||
"name": "日期",
|
||||
"font_class": "riqi",
|
||||
"unicode": "e657",
|
||||
"unicode_decimal": 58967
|
||||
},
|
||||
{
|
||||
"icon_id": "26779921",
|
||||
"name": "退款",
|
||||
"font_class": "tuikuan",
|
||||
"unicode": "e75e",
|
||||
"unicode_decimal": 59230
|
||||
},
|
||||
{
|
||||
"icon_id": "37128747",
|
||||
"name": "购物",
|
||||
"font_class": "gouwu",
|
||||
"unicode": "e65a",
|
||||
"unicode_decimal": 58970
|
||||
},
|
||||
{
|
||||
"icon_id": "37128750",
|
||||
"name": "待收货",
|
||||
"font_class": "daishouhuo",
|
||||
"unicode": "e65c",
|
||||
"unicode_decimal": 58972
|
||||
},
|
||||
{
|
||||
"icon_id": "37128751",
|
||||
"name": "待发货",
|
||||
"font_class": "daifahuo",
|
||||
"unicode": "e669",
|
||||
"unicode_decimal": 58985
|
||||
},
|
||||
{
|
||||
"icon_id": "37128753",
|
||||
"name": "挂号",
|
||||
"font_class": "guahao",
|
||||
"unicode": "e66a",
|
||||
"unicode_decimal": 58986
|
||||
},
|
||||
{
|
||||
"icon_id": "37128754",
|
||||
"name": "待付款",
|
||||
"font_class": "daifukuan",
|
||||
"unicode": "e672",
|
||||
"unicode_decimal": 58994
|
||||
},
|
||||
{
|
||||
"icon_id": "200868",
|
||||
"name": "3.1已关注店铺",
|
||||
"font_class": "31yiguanzhudianpu",
|
||||
"unicode": "e656",
|
||||
"unicode_decimal": 58966
|
||||
},
|
||||
{
|
||||
"icon_id": "201583",
|
||||
"name": "3.1回到顶部",
|
||||
"font_class": "31huidaodingbu",
|
||||
"unicode": "e658",
|
||||
"unicode_decimal": 58968
|
||||
},
|
||||
{
|
||||
"icon_id": "201638",
|
||||
"name": "编辑",
|
||||
"font_class": "bianji",
|
||||
"unicode": "e659",
|
||||
"unicode_decimal": 58969
|
||||
},
|
||||
{
|
||||
"icon_id": "104881",
|
||||
"name": "退货保障",
|
||||
"font_class": "tuihuobaozhang",
|
||||
"unicode": "e653",
|
||||
"unicode_decimal": 58963
|
||||
},
|
||||
{
|
||||
"icon_id": "160001",
|
||||
"name": "home",
|
||||
"font_class": "home",
|
||||
"unicode": "e633",
|
||||
"unicode_decimal": 58931
|
||||
},
|
||||
{
|
||||
"icon_id": "201095",
|
||||
"name": "3.1待发货",
|
||||
"font_class": "31daifahuo",
|
||||
"unicode": "e634",
|
||||
"unicode_decimal": 58932
|
||||
},
|
||||
{
|
||||
"icon_id": "201096",
|
||||
"name": "3.1待付款",
|
||||
"font_class": "31daifukuan",
|
||||
"unicode": "e637",
|
||||
"unicode_decimal": 58935
|
||||
},
|
||||
{
|
||||
"icon_id": "201102",
|
||||
"name": "3.1退款退货",
|
||||
"font_class": "tuikuantuihuo",
|
||||
"unicode": "e639",
|
||||
"unicode_decimal": 58937
|
||||
},
|
||||
{
|
||||
"icon_id": "201577",
|
||||
"name": "3.1电话",
|
||||
"font_class": "31dianhua",
|
||||
"unicode": "e63c",
|
||||
"unicode_decimal": 58940
|
||||
},
|
||||
{
|
||||
"icon_id": "201648",
|
||||
"name": "3.1 时间",
|
||||
"font_class": "31shijian",
|
||||
"unicode": "e63d",
|
||||
"unicode_decimal": 58941
|
||||
},
|
||||
{
|
||||
"icon_id": "5930930",
|
||||
"name": "税企交流",
|
||||
"font_class": "shuiqijiaoliu",
|
||||
"unicode": "e67d",
|
||||
"unicode_decimal": 59005
|
||||
},
|
||||
{
|
||||
"icon_id": "7551465",
|
||||
"name": "官网",
|
||||
"font_class": "guanwang",
|
||||
"unicode": "e704",
|
||||
"unicode_decimal": 59140
|
||||
},
|
||||
{
|
||||
"icon_id": "8036109",
|
||||
"name": "transfer-out",
|
||||
"font_class": "transferout",
|
||||
"unicode": "e651",
|
||||
"unicode_decimal": 58961
|
||||
},
|
||||
{
|
||||
"icon_id": "39893069",
|
||||
"name": "transfer-out-copy",
|
||||
"font_class": "transferout-copy",
|
||||
"unicode": "ecad",
|
||||
"unicode_decimal": 60589
|
||||
},
|
||||
{
|
||||
"icon_id": "4354254",
|
||||
"name": "形状-文字",
|
||||
|
||||
@ -129,6 +129,15 @@ export function img(path: string): string {
|
||||
return isUrl(path) ? path : `${import.meta.env.VITE_IMG_DOMAIN || location.origin}/${path}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出asset img
|
||||
* @param path
|
||||
* @returns
|
||||
*/
|
||||
export function assetImg (path: string) {
|
||||
return new URL('@/', import.meta.url) + path
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串字节长度
|
||||
* @param str
|
||||
@ -291,4 +300,4 @@ export function filterNumber(event:any){
|
||||
export function filterSpecial(event:any){
|
||||
event.target.value = event.target.value.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '')
|
||||
event.target.value = event.target.value.replace(/[`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、]/g,'')
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,12 +171,12 @@ export const createMarker = (map: any) => {
|
||||
* @param params
|
||||
*/
|
||||
export const latLngToAddress = (params: any) => {
|
||||
return jsonp(`https://apis.map.qq.com/ws/geocoder/v1/?key=${params.mapKey}&location=${params.lat},${params.lng}&output=jsonp&callback=callback`)
|
||||
return jsonp(`https://apis.map.qq.com/ws/geocoder/v1/?key=${params.mapKey}&location=${params.lat},${params.lng}&output=jsonp&callback=latLngToAddress`, { callbackName: 'latLngToAddress' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 地址解析
|
||||
*/
|
||||
export const addressToLatLng = (params: any) => {
|
||||
return jsonp(`https://apis.map.qq.com/ws/geocoder/v1/?key=${params.mapKey}&address=${params.address}&output=jsonp&callback=callback`)
|
||||
return jsonp(`https://apis.map.qq.com/ws/geocoder/v1/?key=${params.mapKey}&address=${params.address}&output=jsonp&callback=addressToLatLng`, { callbackName: 'addressToLatLng' })
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ class Request {
|
||||
constructor() {
|
||||
this.instance = axios.create({
|
||||
baseURL: import.meta.env.VITE_APP_BASE_URL.substr(-1) == '/' ? import.meta.env.VITE_APP_BASE_URL : `${import.meta.env.VITE_APP_BASE_URL}/`,
|
||||
timeout: 30000,
|
||||
timeout: 0,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'lang': storage.get('lang') ?? 'zh-cn'
|
||||
|
||||
@ -81,6 +81,14 @@ const test = {
|
||||
// 金额,只允许保留两位小数
|
||||
return /^[1-9]\d*(,\d{3})*(\.\d{1,2})?$|^0\.\d{1,2}$/.test(value)
|
||||
},
|
||||
/**
|
||||
* 验证小数
|
||||
*/
|
||||
decimal(value: string, digit: number) {
|
||||
const regexPattern = `^\\d+(?:\\.\\d{1,${digit}})?$`
|
||||
// 金额,只允许保留两位小数
|
||||
return new RegExp(regexPattern).test(value)
|
||||
},
|
||||
/**
|
||||
* 中文
|
||||
*/
|
||||
@ -209,7 +217,7 @@ const test = {
|
||||
*/
|
||||
image(value: string) {
|
||||
const newValue = value.split('?')[0]
|
||||
const IMAGE_REGEXP = /\.(jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg)/i
|
||||
const IMAGE_REGEXP = /\.(jpeg|jpg|gif|png|svg|jfif|bmp|dpg)/i // todo 暂不支持webp格式
|
||||
return IMAGE_REGEXP.test(newValue)
|
||||
},
|
||||
/**
|
||||
@ -236,4 +244,4 @@ const test = {
|
||||
}
|
||||
}
|
||||
|
||||
export default test
|
||||
export default test
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user