This commit is contained in:
全栈小学生 2024-05-18 17:29:39 +08:00
parent d7fa4d55d9
commit 14585292f9
60 changed files with 4420 additions and 335 deletions

View File

@ -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())

View File

@ -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": "云编译",

View File

@ -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>

View File

@ -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>

View File

@ -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">

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View 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>

View 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>

View 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>

View 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>

View File

@ -0,0 +1,4 @@
{
"layout": "bussiness",
"cover": "/app/assets/images/layout_bussiness.png"
}

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View 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>

View 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>

View 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>

View 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>

View File

@ -0,0 +1,4 @@
{
"layout": "darkside",
"cover": "/app/assets/images/layout_darkside.png"
}

View File

@ -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 {

View File

@ -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>

View File

@ -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>

View File

@ -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 = () => {

View File

@ -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">

View File

@ -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>

View File

@ -0,0 +1,4 @@
{
"layout": "default",
"cover": "/app/assets/images/layout_default.png"
}

View File

@ -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 = () => {

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View 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>

View 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>

View 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>

View 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>

View File

@ -0,0 +1,4 @@
{
"layout": "profession",
"cover": "/app/assets/images/layout_profession.png"
}

View File

@ -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')
}

View File

@ -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 }

View File

@ -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')
}
]

View File

@ -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) {

View 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: '', // 背景图片
},
// 组件类型文本textimage图片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,
// 'oldXclientX-- ' + oldX + '' + oEv.clientX,
// 'oldYclientY-- ' + 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 = 0top = 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 = 0top > 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 > 0top = 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 > 0top > 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 = 0top = 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 = 0top > 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 = 0top = 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 > 0top > 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 = 0top = 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 = 0top > 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 > 0top = 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 > 0top > 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

View File

@ -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()
}
}
})

View File

@ -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;
}
}

View File

@ -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 ;
}
}

View File

@ -1 +1,2 @@
/* addon iconfont */
@import "addon/o2o/iconfont.css";
@import "addon/tourism/iconfont.css";

View File

@ -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";
}

View File

@ -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": "形状-文字",

View File

@ -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,'')
}
}

View File

@ -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' })
}

View File

@ -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'

View File

@ -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