首次提交

This commit is contained in:
2023-06-04 20:01:58 +08:00
parent 00c64c53bb
commit 587f078d21
560 changed files with 106725 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
<template>
<div class="page max-7xl mx-auto text-center">
<div class="bg mx-auto">
<img src="@/assets/404.svg" />
<div class="mt-2">{{ $t("sys.notFoundPage") }}</div>
</div>
<div class="mt-5">
<a-button type="primary" @click="$router.push({ name: 'dashboard' })">{{ $t("sys.goHome") }}</a-button>
</div>
</div>
</template>
<style scoped lang="less">
.page {
position: absolute;
top: 50%;
left: 50%;
margin-top: -200px;
margin-left: -195px;
}
.bg,
.bg img {
width: 390px;
}
</style>

View File

@@ -0,0 +1,78 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-layout class="layout flex flex-col h-full">
<a-layout-header class="ma-ui-header flex justify-between h-50 layout-banner-header operation-area">
<div class="flex justify-between md:justify-center logo">
<a-avatar class="mt-1 ml-2 md:ml-0" :size="40"
><img :src="`${$url}logo.svg`" class="bg-white"
/></a-avatar>
<span class="ml-2 text-xl mt-2.5 hidden md:block">{{ $title }}</span>
</div>
<div class="flex justify-between w-full layout-banner">
<children-banner v-model="userStore.routers" />
<ma-operation />
</div>
</a-layout-header>
<ma-tags class="hidden lg:flex ma-ui-tags" />
<ma-worker-area />
</a-layout>
</template>
<script setup>
import { ref, watch, onMounted } from "vue"
import { useAppStore, useUserStore } from "@/store"
import { useRoute } from "vue-router"
import MaOperation from "../ma-operation.vue"
import MaWorkerArea from "../ma-workerArea.vue"
import MaTags from "../ma-tags.vue"
import ChildrenBanner from "../components/children-banner.vue"
const route = useRoute()
const MaMenuRef = ref(null)
const userStore = useUserStore()
const appStore = useAppStore()
const actives = ref([])
onMounted(() => {
actives.value = [route.name]
})
watch(
() => route,
(v) => {
actives.value = [v.name]
},
{ deep: true }
)
</script>
<style scoped lang="less">
.tags {
margin-top: -1px;
}
:deep(.arco-menu-collapse-button) {
right: 10px;
}
:deep(.layout-banner .arco-menu-horizontal .arco-menu-inner) {
align-items: none;
padding: 8px 10px;
overflow-y: hidden;
}
:deep(.sys-menus .arco-menu-icon svg) {
display: inline;
vertical-align: none;
margin-bottom: 1px;
}
:deep(.sys-menus .arco-menu-icon .icon) {
padding-bottom: 1px;
}
</style>

View File

@@ -0,0 +1,27 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-layout class="layout flex justify-between h-full">
<ma-classic-slider class="ma-ui-slider" />
<a-layout-content class="flex flex-col">
<ma-classic-header class="ma-ui-header" />
<ma-worker-area />
</a-layout-content>
</a-layout>
</template>
<script setup>
import { ref } from "vue"
import MaClassicSlider from "./ma-classic-slider.vue"
import MaClassicHeader from "./ma-classic-header.vue"
import MaWorkerArea from "../ma-workerArea.vue"
</script>

View File

@@ -0,0 +1,27 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-layout-header class="layout-classic-header flex flex-col operation-area">
<div class="flex justify-between layout-classic-header-container">
<a-avatar class="mt-1 ml-2 inline lg:hidden" style="width: 45px" :size="40"
><img :src="`${$url}logo.svg`" class="bg-white"
/></a-avatar>
<ma-breadcrumb />
<ma-operation />
</div>
<ma-tags class="hidden lg:flex" />
</a-layout-header>
</template>
<script setup>
import MaBreadcrumb from "../ma-breadcrumb.vue"
import MaOperation from "../ma-operation.vue"
import MaTags from "../ma-tags.vue"
</script>

View File

@@ -0,0 +1,44 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-layout-sider
class="layout-classic-sider h-full flex flex-col hidden lg:block"
:style="`width: ${appStore.menuCollapse ? '48px' : appStore.menuWidth + 'px'};`"
>
<div class="flex justify-center logo">
<a-avatar class="mt-1" :size="40"><img :src="`${$url}logo.svg`" class="bg-white" /></a-avatar>
<span class="ml-2 text-xl mt-2.5" v-if="!appStore.menuCollapse">{{ $title }}</span>
</div>
<ma-menu ref="MaMenuRef" height="calc(100% - 51px)" :class="`${appStore.menuCollapse ? 'ml-1.5' : ''};`" />
</a-layout-sider>
</template>
<script setup>
import { ref, onMounted } from "vue"
import { useAppStore, useUserStore } from "@/store"
import MaMenu from "../ma-menu.vue"
const MaMenuRef = ref(null)
const userStore = useUserStore()
const appStore = useAppStore()
onMounted(() => {
setTimeout((_) => {
MaMenuRef.value.menus = userStore.routers
}, 50)
})
</script>
<style>
.logo {
height: 51px;
border-bottom: 1px solid var(--color-border-1);
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<a-layout-content class="layout flex justify-between">
<div id="layout-columns-left-panel" class="ma-ui-menu layout-columns-left-panel hidden lg:flex justify-between">
<ma-columns-menu />
</div>
<div class="layout-columns-right-panel flex flex-col" :style="`width: calc(100% - ${containerWidth}px)`">
<ma-columns-header class="ma-ui-header" />
<ma-worker-area />
</div>
</a-layout-content>
</template>
<script setup>
import { onMounted, ref } from "vue"
import ResizeObserver from "resize-observer-polyfill"
import MaColumnsHeader from "./ma-columns-header.vue"
import MaColumnsMenu from "./ma-columns-menu.vue"
import MaWorkerArea from "../ma-workerArea.vue"
const containerWidth = ref(0)
onMounted(() => {
const dom = document.getElementById("layout-columns-left-panel")
const robserver = new ResizeObserver((entries) => {
for (const entry of entries) {
// 可以通过 判断 entry.target得知当前改变的 Element分别进行处理。
switch (entry.target) {
case dom:
containerWidth.value = entry.contentRect.width
break
}
}
})
robserver.observe(dom)
})
</script>

View File

@@ -0,0 +1,28 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-layout-header class="layout-header flex flex-col operation-area">
<div class="flex justify-between" style="height: 50px">
<a-avatar class="mt-1 ml-2 inline lg:hidden" style="width: 45px" :size="40"
><img :src="`${$url}logo.svg`" class="bg-white"
/></a-avatar>
<ma-breadcrumb />
<ma-operation />
</div>
<ma-tags class="hidden lg:flex" />
</a-layout-header>
</template>
<script setup>
import MaBreadcrumb from "../ma-breadcrumb.vue"
import MaOperation from "../ma-operation.vue"
import MaTags from "../ma-tags.vue"
</script>

View File

@@ -0,0 +1,129 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<div class="sider customer-scrollbar flex flex-col items-center bg-gray-800 dark:border-blackgray-5">
<a-avatar class="mt-2" :size="40"><img :src="`${$url}logo.svg`" class="bg-white" /></a-avatar>
<ul class="mt-1 parent-menu-container">
<template v-for="(bigMenu, index) in userStore.routers" :key="index">
<li :class="`${classStyle}`" @click="loadMenu(bigMenu, index)">
<component v-if="bigMenu.meta.icon" :is="bigMenu.meta.icon" class="text-xl mt-1" />
<span class="mt-0.5" :style="appStore.language === 'en' ? 'font-size: 10px' : ''">{{
appStore.i18n
? $t(`menus.${bigMenu.name}`).indexOf(".") > 0
? bigMenu.meta.title
: $t(`menus.${bigMenu.name}`)
: bigMenu.meta.title
}}</span>
</li>
</template>
</ul>
</div>
<div class="layout-menu shadow flex flex-col" v-show="showMenu">
<div class="menu-title flex items-center" v-show="!appStore.menuCollapse">{{ title }}</div>
<a-layout-sider
:style="`width: ${appStore.menuCollapse ? '50px' : appStore.menuWidth + 'px'};
height: ${appStore.menuCollapse ? '100%' : 'calc(100% - 51px)'};`"
>
<ma-menu ref="MaMenuRef" :class="appStore.menuCollapse ? 'ml-0.5' : ''" />
</a-layout-sider>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from "vue"
import { useRoute, useRouter } from "vue-router"
import MaMenu from "../ma-menu.vue"
import { useAppStore, useUserStore } from "@/store"
const route = useRoute()
const router = useRouter()
const MaMenuRef = ref(null)
const appStore = useAppStore()
const userStore = useUserStore()
const showMenu = ref(false)
const title = ref("")
const classStyle = ref(
"flex flex-col parent-menu items-center rounded mt-1 text-gray-200 hover:bg-gray-700 dark:hover:text-gray-50 dark:hover:bg-blackgray-1"
)
onMounted(() => {
initMenu()
})
watch(
() => route,
(v) => {
initMenu()
},
{ deep: true }
)
const initMenu = () => {
let current
if (route.matched[1]?.meta?.breadcrumb) {
current = route.matched[1].meta.breadcrumb[0].name
} else {
current = "home"
}
if (userStore.routers && userStore.routers.length > 0) {
userStore.routers.map((item, index) => {
if (item.name == current) loadMenu(item, index)
})
}
}
const loadMenu = (bigMenu, index) => {
if (bigMenu.meta.type === "L") {
window.open(bigMenu.path)
return
}
if (bigMenu.children.length > 0) {
MaMenuRef.value.loadChildMenu(bigMenu)
showMenu.value = true
} else {
showMenu.value = false
router.push(bigMenu.path)
}
title.value = MaMenuRef.value?.title
document.querySelectorAll(".parent-menu").forEach((item, id) => {
index !== id ? item.classList.remove("active") : item.classList.add("active")
})
}
</script>
<style>
.parent-menu-container {
width: 62px;
}
.parent-menu {
width: 62px;
padding: 5px;
height: 57px;
cursor: pointer;
font-size: 13px;
fill: #fff;
transition: all 0.2s;
}
.parent-menu.active {
background: rgb(var(--primary-6));
color: #fff;
}
:deep(.arco-menu-vertical .arco-menu-inner) {
padding: 4px;
}
:deep(.arco-menu-vertical .arco-menu-item) {
padding: 0px 9px;
line-height: 36px;
}
</style>

View File

@@ -0,0 +1,90 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-layout-content class="sys-menus">
<a-menu
ref="MaMenuRef"
mode="horizontal"
class="layout-banner-menu hidden lg:flex"
:popup-max-height="360"
:selected-keys="actives"
>
<template v-for="menu in modelValue" :key="menu.id">
<template v-if="!menu.meta.hidden">
<template v-if="!menu.children || menu.children.length === 0">
<!-- 没有子菜单的进入 -->
<a-menu-item :key="menu.name" @click="routerPush(menu)">
<template #icon v-if="menu.meta.icon">
<component
:is="menu.meta.icon"
:class="menu.meta.icon.indexOf('ma') > 0 ? 'icon' : ''"
/>
</template>
{{
appStore.i18n
? $t(`menus.${menu.name}`).indexOf(".") > 0
? menu.meta.title
: $t(`menus.${menu.name}`)
: menu.meta.title
}}
</a-menu-item>
</template>
<!-- 有子菜单的进入 -->
<template v-else>
<SubMenu :menu-info="menu" />
</template>
</template>
</template>
</a-menu>
</a-layout-content>
</template>
<script setup>
import { ref, watch, onMounted } from "vue"
import { useTagStore, useAppStore } from "@/store"
import { useRouter, useRoute } from "vue-router"
import SubMenu from "./sub-menu.vue"
defineProps({ modelValue: Array })
const router = useRouter()
const emits = defineEmits(["go"])
const appStore = useAppStore()
const tagStore = useTagStore()
const route = useRoute()
const actives = ref([])
onMounted(() => {
actives.value = [route.name]
})
watch(
() => route,
(v) => {
actives.value = [v.name]
},
{ deep: true }
)
const routerPush = (menu) => {
if (menu.meta && menu.meta.type === "L") {
window.open(menu.path)
} else {
router.push(menu.path)
tagStore.addTag({ name: menu.name, title: menu.meta.title, path: menu.path })
}
}
</script>
<style>
.sys-menus .icon {
width: 1em;
height: 1em;
}
.arco-menu-selected .icon {
fill: rgb(var(--primary-6));
}
</style>

View File

@@ -0,0 +1,81 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-layout-content class="sys-menus">
<template v-for="menu in modelValue" :key="menu.id">
<template v-if="!menu.meta.hidden">
<a-menu-item
v-if="!menu.children || menu.children.length === 0"
:key="menu.name"
@click="routerPush(menu)"
>
<template #icon v-if="menu.meta.icon">
<component :is="menu.meta.icon" :class="menu.meta.icon.indexOf('ma') > 0 ? 'icon' : ''" />
</template>
{{
appStore.i18n
? $t(`menus.${menu.name}`).indexOf(".") > 0
? menu.meta.title
: $t(`menus.${menu.name}`)
: menu.meta.title
}}
</a-menu-item>
<a-sub-menu v-else :key="menu.name">
<template #icon v-if="menu.meta.icon">
<component :is="menu.meta.icon" :class="menu.meta.icon.indexOf('ma') > 0 ? 'icon' : ''" />
</template>
<template #title @click="routerPush(menu.path)">
{{
appStore.i18n
? $t(`menus.${menu.name}`).indexOf(".") > 0
? menu.meta.title
: $t(`menus.${menu.name}`)
: menu.meta.title
}}
</template>
<template v-if="menu.children">
<children-menu v-model="menu.children" />
</template>
</a-sub-menu>
</template>
</template>
</a-layout-content>
</template>
<script setup>
import { useTagStore, useAppStore } from "@/store"
import { useRouter } from "vue-router"
defineProps({ modelValue: Array })
const router = useRouter()
const emits = defineEmits(["go"])
const appStore = useAppStore()
const tagStore = useTagStore()
const routerPush = (menu) => {
if (menu.meta && menu.meta.type === "L") {
window.open(menu.path)
} else {
router.push(menu.path)
tagStore.addTag({ name: menu.name, title: menu.meta.title, path: menu.path })
}
}
</script>
<style>
.sys-menus .icon {
width: 1em;
height: 1em;
}
.arco-menu-selected .icon {
fill: rgb(var(--primary-6));
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<div class="w-full h-full" v-show="$route.meta.type === 'I'">
<iframe
v-for="item in iframeStore.iframes"
v-show="item.meta.url === $route.meta.url"
:src="item.meta.url"
:key="item.name"
frameborder="0"
class="w-full h-full"
/>
</div>
</template>
<script setup>
import { watch } from "vue"
import { useIframeStore } from "@/store"
import { useRoute } from "vue-router"
const iframeStore = useIframeStore()
const route = useRoute()
watch(
() => route,
(value) => {
pushRoute(value)
},
{ deep: true }
)
const pushRoute = (r) => {
if (r.meta.type === "I") {
iframeStore.addIframe(r)
}
}
pushRoute(route)
</script>

View File

@@ -0,0 +1,99 @@
<template>
<div class="mgs-nfc rounded p-2 shadow-lg">
<a-tabs default-active-key="message" type="rounded">
<a-tab-pane key="message">
<template #title>
{{ $t("sys.operationMessage.message") }}
<a-badge
:count="5"
dot
:dotStyle="{ width: '5px', height: '5px', top: '-8px' }"
v-if="messageStore.messageList.length > 0"
>
</a-badge>
</template>
<a-list :max-height="230" class="h-full" v-if="messageStore.messageList.length > 0">
<a-list-item
v-for="item in messageStore.messageList"
:key="item.id"
class="cursor-pointer"
@click="viewMessage(item)"
>
<a-list-item-meta :title="item.title">
<template #description>
<div class="flex justify-between" style="font-size: 13px">
<span>发送人{{ item.send_user.nickname }}</span>
<span>时间{{ item.created_at.substr(0, 10) }}</span>
</div>
</template>
<template #avatar>
<a-avatar shape="square">
<img
alt="avatar"
:src="`${item.send_user.avatar ? item.send_user.avatar : $url + 'avatar.jpg'}`"
/>
</a-avatar>
</template>
</a-list-item-meta>
</a-list-item>
</a-list>
<a-empty v-else class="h-full" />
</a-tab-pane>
<a-tab-pane key="todo">
<template #title>{{ $t("sys.operationMessage.todo") }}</template>
<a-empty class="h-full" />
</a-tab-pane>
</a-tabs>
</div>
<a-modal v-model:visible="detailVisible" fullscreen :footer="false">
<template #title>消息详情</template>
<a-typography :style="{ marginTop: '-30px' }">
<a-typography-title class="text-center">
{{ row?.title }}
</a-typography-title>
<a-typography-paragraph class="text-right" style="font-size: 13px; color: var(--color-text-3)">
<a-space size="large">
<span>创建时间{{ row?.created_at }}</span>
</a-space>
</a-typography-paragraph>
<a-typography-paragraph>
<div v-html="row?.content"></div>
</a-typography-paragraph>
</a-typography>
</a-modal>
</template>
<script setup>
import { ref } from "vue"
import { useMessageStore } from "@/store"
import queueMessage from "@/api/system/queueMessage"
const messageStore = useMessageStore()
const row = ref({})
const detailVisible = ref(false)
const viewMessage = async (record) => {
row.value = record
await queueMessage.updateReadStatus({ ids: [record.id] })
messageStore.messageList.find((item, idx) => {
if (item && record.id == item.id) messageStore.messageList.splice(idx, 1)
})
detailVisible.value = true
}
</script>
<style scoped lang="less">
.mgs-nfc {
width: 400px;
background: var(--color-bg-1);
height: 360px;
border: 1px solid var(--color-border-1);
margin-right: 10px;
margin-top: 9px;
position: relative;
}
:deep(.arco-list-item-meta-content) {
width: 100%;
}
</style>

View File

@@ -0,0 +1,11 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template></template>

View File

@@ -0,0 +1,63 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-modal v-model:visible="visible" width="600px" @cancel="close" :footer="false">
<template #title>{{ $t("sys.changeSkin") }}</template>
<div class="flex flex-col">
<a-card
v-for="(item, index) in skinList"
:key="item.name"
:class="index === 0 ? '' : 'mt-3'"
:body-style="{ width: '100%', display: 'flex', justifyContent: 'space-between', padding: '10px' }"
>
<a-row class="w-full flex items-center">
<a-col :span="3" class="flex flex-col text-center">
<div class="leading-6">{{ $t(`skin.${item.name}`) }}</div>
</a-col>
<a-col :span="6" class="flex flex-col text-center">
<a-image :src="$url + item.thumb" class="rounded border" />
</a-col>
<a-col :span="12" class="flex items-center pl-3 pr-3">
{{ $t(`skin.${item.name}Desc`) }}
</a-col>
<a-col :span="3" class="flex items-center justify-end">
<a-button
:type="appStore.skin === item.name ? 'primary' : 'secondary'"
:disabled="appStore.skin === item.name"
@click="useSkin(item.name)"
>
{{ appStore.skin === item.name ? $t("skin.activated") : $t("skin.use") }}
</a-button>
</a-col>
</a-row>
</a-card>
</div>
</a-modal>
</template>
<script setup>
import { reactive, ref } from "vue"
import { useAppStore } from "@/store"
import skins from "@/config/skins"
const visible = ref(false)
const appStore = useAppStore()
const open = () => (visible.value = true)
const close = () => (visible.value = false)
const useSkin = (name) => appStore.useSkin(name)
const skinList = reactive(skins)
defineExpose({ open })
</script>
<style scoped lang="less"></style>

View File

@@ -0,0 +1,58 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-sub-menu :key="menuInfo.name">
<template #title>
{{ $t(`menus.${menuInfo.name}`).indexOf(".") > 0 ? menuInfo.meta.title : $t(`menus.${menuInfo.name}`) }}
</template>
<template #icon v-if="menuInfo.meta.icon">
<component :is="menuInfo.meta.icon" :class="menuInfo.meta.icon.indexOf('ma') > 0 ? 'icon' : ''" />
</template>
<template v-for="item in menuInfo.children" :key="item.id">
<template v-if="!item.children || item.children.length === 0">
<a-menu-item :key="item.name" @click="routerPush(item)">
<template #icon v-if="item.meta.icon">
<component :is="item.meta.icon" :class="menuInfo.meta.icon.indexOf('ma') > 0 ? 'icon' : ''" />
</template>
{{ $t(`menus.${item.name}`).indexOf(".") > 0 ? item.meta.title : $t(`menus.${item.name}`) }}
</a-menu-item>
</template>
<template v-else>
<SubMenu :menu-info="item" />
</template>
</template>
</a-sub-menu>
</template>
<script setup name="SubMenu">
import { useRouter, useRoute } from "vue-router"
import { useTagStore } from "@/store"
defineProps({ menuInfo: Object })
const emits = defineEmits(["go"])
const router = useRouter()
const tagStore = useTagStore()
const routerPush = (menu) => {
if (menu.meta && menu.meta.type === "L") {
window.open(menu.path)
} else {
router.push(menu.path)
tagStore.addTag({ name: menu.name, title: menu.meta.title, path: menu.path })
}
}
</script>
<style scoped>
.arco-trigger-menu-icon .icon {
width: 1em;
height: 1em;
}
[mine-skin="mine"] .arco-menu-selected .icon {
fill: rgb(var(--primary-6));
}
</style>

View File

@@ -0,0 +1,29 @@
<template>
<div class="ml-2 mt-3.5 hidden lg:block">
<a-breadcrumb>
<a-breadcrumb-item class="cursor-pointer" @click="router.push('/dashboard')">
{{ $t("menus.dashboard") }}
</a-breadcrumb-item>
<template v-for="(r, index) in route.matched" :key="index">
<a-breadcrumb-item v-if="index > 0 && !['/', '/home', '/dashboard'].includes(r.path)">
{{
appStore.i18n
? $t("menus." + r.name).indexOf(".") > 0
? r.meta.title
: $t("menus." + r.name)
: r.meta.title
}}
</a-breadcrumb-item>
</template>
</a-breadcrumb>
</div>
</template>
<script setup>
import { useRoute, useRouter } from "vue-router"
import { useAppStore } from "@/store"
const appStore = useAppStore()
const route = useRoute()
const router = useRouter()
</script>

View File

@@ -0,0 +1,27 @@
<template>
<div class="block lg:hidden button-menu">
<a-trigger :trigger="['click']" clickToClose position="top" v-model:popupVisible="popupVisible">
<div :class="`button-trigger ${popupVisible ? 'button-trigger-active' : ''}`">
<icon-close v-if="popupVisible" />
<icon-menu v-else />
</div>
<template #content>
<a-menu mode="popButton" showCollapseButton :popup-max-height="360">
<children-menu v-model="userStore.routers" />
</a-menu>
</template>
</a-trigger>
</div>
</template>
<script setup>
import { ref } from "vue"
import { useAppStore, useUserStore } from "@/store"
import ChildrenMenu from "./components/children-menu.vue"
const userStore = useUserStore()
const popupVisible = ref(false)
</script>
<style scoped></style>

View File

@@ -0,0 +1,103 @@
<template>
<a-menu
class="ma-menu"
:style="{ width: appStore.menuWidth + 'px', height: props.height }"
breakpoint="md"
v-model:open-keys="openKeys"
v-model:selected-keys="actives"
:accordion="true"
:collapsed-width="45"
show-collapse-button
:collapsed="appStore.menuCollapse"
@collapse="onCollapse"
:popup-max-height="360"
auto-scroll-into-view
>
<children-menu v-model="menus" />
</a-menu>
</template>
<script setup>
import { ref, watch, onMounted } from "vue"
import { useRouter, useRoute } from "vue-router"
import { useI18n } from "vue-i18n"
import ChildrenMenu from "./components/children-menu.vue"
import { useAppStore, useUserStore } from "@/store"
const router = useRouter()
const route = useRoute()
const { t } = useI18n()
const appStore = useAppStore()
const userStore = useUserStore()
const menus = ref([])
const actives = ref([])
const openKeys = ref([])
const title = ref("")
onMounted(() => {
actives.value = [route.name]
findTopMenuName()
})
watch(
() => route,
(v) => {
actives.value = [v.name]
findTopMenuName()
},
{ deep: true }
)
const loadChildMenu = (obj) => {
if (obj.children && obj.children.length > 0) {
menus.value = obj.children
title.value = t("menus." + obj.name).indexOf(".") > 0 ? obj.meta.title : t("menus." + obj.name)
}
}
const findTopMenuName = () => {
if (route.matched[1] && route.matched[1].meta && !route.matched[1].meta.breadcrumb) {
openKeys.value = []
route.matched.map((item, index) => {
if (route.matched[0].name === "layout") {
openKeys.value.push("home")
}
})
} else {
openKeys.value = []
if (route.matched[1] && route.matched[1].meta) {
route.matched[1].meta.breadcrumb.map((item) => {
openKeys.value.push(item.name)
})
}
}
}
const onCollapse = (val) => {
appStore.toggleMenu(val)
}
const props = defineProps({
height: { type: String, default: "100%" }
})
defineExpose({ loadChildMenu, title, actives, menus, openKeys, findTopMenuName })
</script>
<style scoped>
:deep(.arco-menu-vertical .arco-menu-inner) {
padding: 0;
}
:deep(.arco-menu-collapse-button) {
right: 8px;
bottom: 8px;
}
:deep(.arco-menu-inner ::-webkit-scrollbar-thumb) {
border: 4px solid transparent;
background-clip: padding-box;
border-radius: 7px;
background-color: var(--color-text-4);
}
</style>

View File

@@ -0,0 +1,151 @@
<template>
<div class="mr-2 flex justify-end lg:justify-between w-full lg:w-auto">
<a-space class="mr-0 lg:mr-5" size="medium">
<a-tooltip :content="$t('sys.search')">
<a-button :shape="'circle'" @click="() => (appStore.searchOpen = true)" class="hidden lg:inline">
<template #icon>
<icon-search />
</template>
</a-button>
</a-tooltip>
<!-- <a-tooltip content="锁屏">-->
<!-- <a-button :shape="'circle'" class="hidden lg:inline">-->
<!-- <template #icon>-->
<!-- <icon-lock />-->
<!-- </template>-->
<!-- </a-button>-->
<!-- </a-tooltip>-->
<a-tooltip :content="isFullScreen ? $t('sys.closeFullScreen') : $t('sys.fullScreen')">
<a-button :shape="'circle'" class="hidden lg:inline" @click="screen">
<template #icon>
<icon-fullscreen-exit v-if="isFullScreen" />
<icon-fullscreen v-else />
</template>
</a-button>
</a-tooltip>
<a-trigger trigger="click">
<a-button :shape="'circle'">
<template #icon>
<a-badge
:count="5"
dot
:dotStyle="{ width: '5px', height: '5px' }"
v-if="messageStore.messageList.length > 0"
>
<icon-notification />
</a-badge>
<icon-notification v-else />
</template>
</a-button>
<template #content>
<message-notification />
</template>
</a-trigger>
<a-tooltip :content="$t('sys.pageSetting')">
<a-button :shape="'circle'" @click="() => (appStore.settingOpen = true)" class="hidden lg:inline">
<template #icon>
<icon-settings />
</template>
</a-button>
</a-tooltip>
</a-space>
<a-dropdown @select="handleSelect" trigger="hover">
<a-avatar class="bg-blue-500 text-3xl avatar" style="top: -1px">
<img :src="userStore.user && userStore.user.avatar ? userStore.user.avatar : $url + 'avatar.jpg'" />
</a-avatar>
<template #content>
<a-doption value="userCenter"><icon-user /> {{ $t("sys.userCenter") }}</a-doption>
<a-doption value="clearCache"><icon-delete /> {{ $t("sys.clearCache") }}</a-doption>
<a-divider style="margin: 5px 0" />
<a-doption value="logout"><icon-poweroff /> {{ $t("sys.logout") }}</a-doption>
</template>
</a-dropdown>
<a-modal v-model:visible="showLogoutModal" @ok="handleLogout" @cancel="handleLogoutCancel">
<template #title>{{ $t("sys.logoutAlert") }}</template>
<div>{{ $t("sys.logoutMessage") }}</div>
</a-modal>
</div>
</template>
<script setup>
import { ref } from "vue"
import { useAppStore, useUserStore, useMessageStore } from "@/store"
import tool from "@/utils/tool"
import MessageNotification from "./components/message-notification.vue"
import { useRouter } from "vue-router"
import { useI18n } from "vue-i18n"
import { Message } from "@arco-design/web-vue"
import WsMessage from "@/ws-serve/message"
import { info } from "@/utils/common"
import commonApi from "@/api/common"
const { t } = useI18n()
const messageStore = useMessageStore()
const userStore = useUserStore()
const appStore = useAppStore()
const setting = ref(null)
const router = useRouter()
const isFullScreen = ref(false)
const showLogoutModal = ref(false)
const isDev = ref(import.meta.env.DEV)
const handleSelect = async (name) => {
if (name === "userCenter") {
router.push({ name: "userCenter" })
}
if (name === "clearCache") {
const res = await commonApi.clearAllCache()
tool.local.remove("dictData")
res.success && Message.success(res.message)
}
if (name === "logout") {
showLogoutModal.value = true
document.querySelector("#app").style.filter = "grayscale(1)"
}
}
const handleLogout = async () => {
await userStore.logout()
document.querySelector("#app").style.filter = "grayscale(0)"
window.location.href = "/"
}
const handleLogoutCancel = () => {
document.querySelector("#app").style.filter = "grayscale(0)"
}
const screen = () => {
tool.screen(document.documentElement)
isFullScreen.value = !isFullScreen.value
}
const Wsm = new WsMessage()
Wsm.connection()
Wsm.getMessage()
Wsm.ws.on("ev_new_message", (msg, data) => {
if (data.length > messageStore.messageList.length) {
info("新消息提示", "您有新的消息,请注意查收!")
}
messageStore.messageList = data
})
</script>
<style scoped>
:deep(.arco-avatar-text) {
top: 1px;
}
:deep(.arco-divider-horizontal) {
margin: 5px 0;
}
.avatar {
cursor: pointer;
margin-top: 6px;
}
</style>

View File

@@ -0,0 +1,427 @@
<template>
<div class="flex justify-between tags-container" ref="containerDom" v-if="appStore.tag">
<div class="menu-tags-wrapper" ref="scrollbarDom" :class="{ 'tag-pn': tagShowPrevNext }">
<div class="tags" ref="tags">
<a
v-for="tag in tagStore.tags"
:key="tag.path"
@contextmenu.prevent="openContextMenu($event, tag)"
:class="route.fullPath == tag.path ? 'active' : ''"
@click="tagJump(tag)"
>
{{
tag.customTitle
? tag.customTitle
: appStore.i18n
? $t("menus." + tag.name).indexOf(".") > 0
? tag.title
: $t("menus." + tag.name)
: tag.title
}}
<icon-close class="tag-icon" v-if="!tag.affix" @click.stop="closeTag(tag)" />
</a>
</div>
<span class="ma-tag-prev" v-if="tagShowPrevNext">
<IconLeft :size="20" class="tag-scroll-icon" @click="handleScroll(-500)" />
</span>
<span class="ma-tag-next" v-if="tagShowPrevNext">
<IconRight :size="20" class="tag-scroll-icon" @click="handleScroll(500)" />
</span>
</div>
<a-trigger class="ma-tags-more-dropdown" :popup-translate="[-65, -6]" :show-arrow="true" trigger="hover">
<span class="ma-tags-more">
<span class="ma-tags-more-icon">
<i class="ma-box ma-box-t"></i>
<i class="ma-box ma-box-b"></i>
</span>
</span>
<template #content>
<ul class="ma-tags-more-contextmenu">
<li @click="tagToolRefreshTag">
<icon-refresh />
{{ $t("sys.tags.refresh") }}
</li>
<a-divider class="dropdown-divider" />
<li @click="tagToolCloseCurrentTag">
<icon-close-circle />
{{ $t("sys.tags.closeTag") }}
</li>
<li @click="tagToolCloseOtherTag">
<icon-close-circle-fill />
{{ $t("sys.tags.closeOtherTag") }}
</li>
</ul>
</template>
</a-trigger>
<ul class="tags-contextmenu" v-if="contextMenuVisible" :style="{ left: left + 'px', top: top + 'px' }">
<li @click="contextMenuRefreshTag">
<icon-refresh />
{{ $t("sys.tags.refresh") }}
</li>
<li @click="contextMenuMaxSizeTag">
<icon-fullscreen />
{{ $t("sys.tags.fullscreen") }}
</li>
<a-divider />
<li @click="contextMenuCloseTag" :class="contextMenuItem.affix ? 'disabled' : ''">
<icon-close-circle />
{{ $t("sys.tags.closeTag") }}
</li>
<li @click="contextMenuCloseOtherTag">
<icon-close-circle-fill />
{{ $t("sys.tags.closeOtherTag") }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref, watch, onMounted, nextTick } from "vue"
import { useAppStore, useTagStore } from "@/store"
import { useRoute, useRouter } from "vue-router"
import { addTag, closeTag, refreshTag } from "@/utils/common"
import Sortable from "sortablejs"
import { Message } from "@arco-design/web-vue"
import { IconFaceFrownFill } from "@arco-design/web-vue/dist/arco-vue-icon"
import tool from "@/utils/tool"
const route = useRoute()
const router = useRouter()
const appStore = useAppStore()
const tagStore = useTagStore()
const tags = ref(null)
const tagShowPrevNext = ref(false)
const contextMenuVisible = ref(false)
const contextMenuItem = ref(null)
const left = ref(0)
const top = ref(0)
const notAddTagList = ["login"]
watch(
() => appStore.tag,
(r) => {
nextTick(() => {
tagShowPrevNext.value = tags.value.scrollWidth > tags.value.offsetWidth
})
},
{ deep: true }
)
watch(
() => tagStore.tags,
(r) => {
nextTick(() => {
tagShowPrevNext.value = tags.value.scrollWidth > tags.value.offsetWidth
})
},
{ deep: true }
)
watch(
() => route,
(r) => {
if (!notAddTagList.includes(r.name)) {
addTag({
name: r.name,
path: r.fullPath,
affix: r.meta.affix,
title: r.meta.title
})
}
nextTick(() => {
if (tags.value && tags.value.scrollWidth > tags.value.clientWidth) {
//确保当前标签在可视范围内
tags.value.querySelector(".active").scrollIntoView()
}
})
},
{ deep: true }
)
watch(contextMenuVisible, (value) => {
const handler = (e) => {
const dom = document.querySelector(".tags-contextmenu")
if (dom && !dom.contains(e.target)) {
closeContextMenu()
}
}
value
? document.body.addEventListener("click", (e) => handler(e))
: document.body.removeEventListener("click", (e) => handler(e))
})
const tagJump = (tag) => {
router.push({ path: tag.path, query: tool.getRequestParams(tag.path) })
}
const openContextMenu = (e, tag) => {
contextMenuItem.value = tag
contextMenuVisible.value = true
left.value = e.clientX + 1
top.value = e.clientY + 1
nextTick(() => {
const dom = document.querySelector(".tags-contextmenu")
if (document.body.offsetWidth - e.clientX < dom.offsetWidth) {
left.value = document.body.offsetWidth - dom.offsetWidth + 1
top.value = e.clientY + 1
}
})
}
const closeContextMenu = () => {
contextMenuItem.value = null
contextMenuVisible.value = false
}
const contextMenuMaxSizeTag = () => {
const tag = contextMenuItem.value
contextMenuVisible.value = false
if (route.fullPath != tag.fullPath) {
router.push({ path: tag.path, query: tool.getRequestParams(tag.path) })
}
document.getElementById("app").classList.add("max-size")
}
const contextMenuRefreshTag = () => {
const tag = contextMenuItem.value
contextMenuVisible.value = false
if (route.fullPath != tag.fullPath) {
router.push({ path: tag.path, query: tool.getRequestParams(tag.path) })
}
refreshTag()
}
const tagToolRefreshTag = () => {
refreshTag()
}
const tagToolCloseCurrentTag = () => {
const list = [...tagStore.tags]
list.forEach((tag) => {
if (tag.affix || route.path == tag.path) {
closeTag(tag)
}
})
}
const contextMenuCloseTag = () => {
if (!contextMenuItem.value.affix) {
closeTag(contextMenuItem.value)
contextMenuVisible.value = false
}
}
const tagToolCloseOtherTag = () => {
const list = [...tagStore.tags]
list.forEach((tag) => {
if (tag.affix || route.path == tag.path) {
return true
} else {
closeTag(tag)
}
})
contextMenuVisible.value = false
}
const contextMenuCloseOtherTag = () => {
const currentTag = contextMenuItem.value
if (route.path != currentTag.path) {
router.push({ path: currentTag.path })
}
const list = [...tagStore.tags]
list.forEach((tag) => {
if (tag.affix || currentTag.path == tag.path) {
return true
} else {
closeTag(tag)
}
})
contextMenuVisible.value = false
}
const scrollHandler = (event) => {
const detail = event.wheelDelta || event.detail
const moveForwardStep = 1
const moveBackStep = -1
let step = 0
if (detail == 3 || (detail < 0 && detail != -3)) {
step = moveForwardStep * 50
} else {
step = moveBackStep * 50
}
tags.value.scrollLeft += step
}
const handleScroll = (offset) => {
const distance = tags.value.scrollLeft
const total = distance + offset
const step = offset / 50
moveSlow(distance, total, step)
}
const moveSlow = (distance, total, step) => {
if (step > 0) {
//往左滚
if (distance < total) {
distance += step
tags.value.scrollLeft = distance
window.requestAnimationFrame(() => {
moveSlow(distance, total, step)
})
} else {
tags.value.scrollLeft = total
}
} else {
//往右滚
if (distance > total) {
distance += step
tags.value.scrollLeft = distance
window.requestAnimationFrame(() => {
moveSlow(distance, total, step)
})
} else {
tags.value.scrollLeft = total
}
}
}
onMounted(() => {
if (tags.value) {
Sortable.create(tags.value, { draggable: "a", animation: 300 })
tags.value.addEventListener("mousewheel", scrollHandler, { passive: true }) ||
tags.value.addEventListener("DOMMouseScroll", scrollHandler, { passive: true })
nextTick(() => {
tagShowPrevNext.value = tags.value.scrollWidth > tags.value.offsetWidth
})
}
})
</script>
<style scoped lang="less">
.tag-pn {
padding: 0 15px;
margin: 0 5px;
}
.menu-tags-wrapper {
box-sizing: border-box;
overflow: hidden;
position: relative;
display: inline-flex;
.ma-tag-next,
.ma-tag-prev {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
height: 34px;
.tag-scroll-icon {
cursor: pointer;
color: var(--color-text-3);
}
.tag-scroll-icon:hover {
color: rgb(var(--primary-6));
}
}
.ma-tag-prev {
left: -4px;
}
.ma-tag-next {
right: -4px;
}
.tags a {
margin-left: 4px;
margin-right: 0;
}
}
.ma-tags-more {
position: relative;
box-sizing: border-box;
display: flex;
text-align: left;
justify-content: center;
align-items: center;
margin-right: 15px;
margin-left: 5px;
top: -1px;
.ma-tags-more-icon {
display: inline-block;
color: var(--color-text-2);
cursor: pointer;
transition: transform 0.3s ease-out;
.ma-box {
position: relative;
display: block;
width: 14px;
height: 8px;
.ma-box-t:before {
transition: transform 0.3s ease-out 0.3s;
}
}
.ma-box:before {
position: absolute;
top: 2px;
left: 0;
width: 6px;
height: 6px;
content: "";
background: var(--color-text-3);
}
.ma-box:after {
position: absolute;
top: 2px;
left: 8px;
width: 6px;
height: 6px;
content: "";
background: var(--color-text-3);
}
}
}
.ma-tags-more:hover .ma-tags-more-icon .ma-box:before {
background: rgb(var(--primary-6));
transform: rotate(45deg);
}
.ma-tags-more:hover .ma-tags-more-icon .ma-box:after {
background: rgb(var(--primary-6));
}
.ma-tags-more:hover .ma-tags-more-icon {
transform: rotate(90deg);
}
.dropdown-divider {
margin: 0;
}
.ma-tags-more-contextmenu {
border: 1px solid var(--color-border-2);
padding: 5px 0;
z-index: 100;
width: 170px;
background-color: var(--color-bg-5);
border-radius: 4px;
li {
padding: 7px 15px;
color: var(--color-text-2);
font-size: 13px;
}
li:hover {
background-color: rgb(var(--primary-1));
cursor: pointer;
}
.arco-divider-horizontal {
margin: 5px 0;
}
}
</style>

View File

@@ -0,0 +1,30 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-layout-content class="work-area customer-scrollbar relative">
<div class="h-full" :class="{ 'p-3': $route.path.indexOf('maIframe') === -1 }">
<router-view v-slot="{ Component }">
<transition :name="appStore.animation" mode="out-in">
<keep-alive :include="keepStore.keepAlives">
<component :is="Component" :key="$route.fullPath" v-if="keepStore.show" />
</keep-alive>
</transition>
</router-view>
<iframe-view />
</div>
</a-layout-content>
</template>
<script setup>
import { useAppStore, useKeepAliveStore } from "@/store"
import IframeView from "./components/iframe-view.vue"
const appStore = useAppStore()
const keepStore = useKeepAliveStore()
</script>

View File

@@ -0,0 +1,3 @@
<template>
<router-view />
</template>

View File

@@ -0,0 +1,89 @@
<template>
<div class="ma-content-block p-4">
<a-page-header
:style="{ background: 'var(--color-bg-2)' }"
:title="pageTitle"
:subtitle="`数据${opName}页`"
@back="pageBack"
>
<ma-form v-model="form" :columns="formConfig?.formColumns ?? []" ref="maFormRef" @onSubmit="submitForm" />
</a-page-header>
</div>
</template>
<script setup>
import { ref } from "vue"
import { useFormStore, useTagStore } from "@/store/index"
import { useRoute } from "vue-router"
import { isEmpty, isFunction } from "lodash"
import { Message } from "@arco-design/web-vue"
import { closeTag } from "@/utils/common"
const route = useRoute()
const formStore = useFormStore()
const tagStore = useTagStore()
const maFormRef = ref()
const tagId = ref(route.query?.tagId ?? "")
const op = ref(route.query?.op ?? "")
const key = ref(route.query?.key ?? "")
if (isEmpty(tagId.value)) {
Message.error("缺少tagId参数")
closeTag({ path: route.fullPath })
}
if (isEmpty(op.value)) {
Message.error("缺少op参数")
closeTag({ path: route.fullPath })
}
if (op.value === "edit" && isEmpty(key.value)) {
Message.error("缺少key参数")
closeTag({ path: route.fullPath })
}
if (!formStore.formList[tagId.value]) {
Message.error("缺少Form配置")
closeTag({ path: route.fullPath })
}
const formConfig = formStore.formList[tagId.value]?.config
const form = ref(
op.value === "add"
? formStore.formList[tagId.value]?.addData
: formStore.formList[tagId.value]["editData"][key.value]
)
const options = formConfig?.options
const opName = ref(op.value === "add" ? "新增" : "编辑")
const pageTitle = ref(opName.value + (formConfig?.options?.formOption?.tagName ?? "未命名"))
tagStore.updateTagTitle(route.fullPath, ` ${pageTitle.value} ${op.value === "edit" ? " | " + key.value : ""} `)
const pageBack = () => {
window.history.back(-1)
}
const submitForm = async () => {
const formData = maFormRef.value.getFormData()
if (await maFormRef.value.validateForm()) {
return false
}
let response
if (op.value === "add") {
isFunction(options.beforeAdd) && (await options.beforeAdd(formData))
response = await options.add.api(formData)
isFunction(options.afterAdd) && (await options.afterAdd(response, formData))
} else {
isFunction(options.beforeEdit) && (await options.beforeEdit(formData))
response = await options.edit.api(formData[options.pk], formData)
isFunction(options.afterEdit) && (await options.afterEdit(response, formData))
}
if (response.success) {
await maFormRef.value.resetForm()
Message.success(response.message || `${opName.value}成功!`)
closeTag({ path: route.fullPath })
formStore.crudList[options.id] = true
} else if (response.success === false && (typeof response.code === "undefined" || response.code !== 200)) {
Message.error(response.message || `${actionTitle.value}失败!`)
}
}
</script>

View File

@@ -0,0 +1,65 @@
<template>
<a-layout-content class="h-full main-container">
<columns-layout v-if="appStore.layout === 'columns'" />
<classic-layout v-if="appStore.layout === 'classic'" />
<banner-layout v-if="appStore.layout === 'banner'" />
<setting ref="settingRef" />
<transition name="ma-slide-down" mode="out-in">
<system-search ref="systemSearchRef" v-show="appStore.searchOpen" />
</transition>
<ma-button-menu />
<div class="max-size-exit" @click="tagExitMaxSize"><icon-close /></div>
</a-layout-content>
</template>
<script setup>
import { onMounted, ref, watch } from "vue"
import { useAppStore, useUserStore } from "@/store"
import ColumnsLayout from "./components/columns/index.vue"
import ClassicLayout from "./components/classic/index.vue"
import BannerLayout from "./components/banner/index.vue"
import Setting from "./setting.vue"
import SystemSearch from "./search.vue"
import MaButtonMenu from "./components/ma-buttonMenu.vue"
const appStore = useAppStore()
const userStore = useUserStore()
const settingRef = ref()
const systemSearchRef = ref()
watch(
() => appStore.settingOpen,
(vl) => {
if (vl === true) {
settingRef.value.open()
appStore.settingOpen = false
}
}
)
const tagExitMaxSize = () => {
document.getElementById("app").classList.remove("max-size")
}
onMounted(() => {
document.addEventListener("keydown", (e) => {
const keyCode = e.keyCode ?? e.which ?? e.charCode
const altKey = e.altKey ?? e.metaKey
if (altKey && keyCode === 83) {
appStore.searchOpen = true
return
}
if (keyCode === 27) {
appStore.searchOpen = false
return
}
})
})
</script>
<style scoped lang="less"></style>

View File

@@ -0,0 +1,244 @@
<template>
<div class="sys-search-container">
<div class="ssc-bg">
<div class="w-6/12 mx-auto center-box">
<div class="mt-10"><img :src="`${$url}logo.svg`" width="100" class="mx-auto" /></div>
<div class="mt-10">
<a-input
size="large"
ref="searchInputRef"
placeholder="搜索页面支持名称、标识以及URL的模糊查询"
@input="searchPage"
>
<template #prefix><icon-search /></template>
</a-input>
</div>
<div class="mt-5">
<a-space size="large" class="flex justify-center">
<a-space><a-tag>ALT+S</a-tag><a-tag>唤醒搜索面板</a-tag></a-space>
<a-space
><a-tag><icon-caret-up /></a-tag><a-tag><icon-caret-down /></a-tag
><a-tag>切换搜索结果</a-tag></a-space
>
<a-space><a-tag>Enter</a-tag><a-tag>进入页面</a-tag></a-space>
<a-space><a-tag>Esc</a-tag><a-tag>关闭搜索面板</a-tag></a-space>
</a-space>
</div>
<ul class="mt-10 results shadow-lg customer-scrollbar">
<template v-for="res in resultList">
<li
class="flex items-center"
v-if="res && res.path.indexOf(':') === -1 && res.components && res?.meta?.type === 'M'"
@click="gotoPage(res)"
>
<div class="icon-box flex justify-center items-center">
<component
v-if="res.meta.icon"
:is="res.meta.icon"
:class="res.meta.icon.indexOf('ma') > 0 ? 'icon' : ''"
/>
<icon-menu v-else />
</div>
<div class="ml-5 leading-6">
<div class="title">{{ res.meta.title }}</div>
<div class="path">{{ res.path }}</div>
</div>
</li>
</template>
</ul>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from "vue"
import { useRouter } from "vue-router"
import { useAppStore } from "@/store"
const appStore = useAppStore()
const router = useRouter()
const searchInputRef = ref()
const resultList = ref(router.getRoutes())
const searchPage = (value) => {
resultList.value = router.getRoutes().filter((item) => {
if (item.path && item.path.indexOf(value) > -1) {
return true
}
if (item.name && item.name.indexOf(value) > -1) {
return true
}
if (item.meta && item.meta.title && item.meta.title.indexOf(value) > -1) {
return true
}
return false
})
}
const gotoPage = (res) => {
appStore.searchOpen = false
router.push(res.path)
}
onMounted(() => {
searchInputRef.value.focus()
document.addEventListener("keydown", (e) => {
const keyCode = e.keyCode ?? e.which ?? e.charCode
const active = document.querySelector(".active-search-li")
const getActiveItemInfo = () => {
const li = document.querySelectorAll(".results li")
let activeItem = { idx: 0, path: "/" }
li.forEach((item, index) => {
if (item.className.split(" ").includes("active-search-li")) {
activeItem.path = item.querySelector(".path").innerHTML
activeItem.idx = index
return
}
})
return activeItem
}
const add = (index) => {
document.querySelectorAll(".results li")[index].classList.add("active-search-li")
}
const remove = (index) => {
document.querySelectorAll(".results li")[index].classList.remove("active-search-li")
}
if (appStore.searchOpen) {
// down
if (keyCode === 40) {
if (!active) {
add(0)
return
} else {
const li = document.querySelectorAll(".results li")
let item = getActiveItemInfo(),
nextIndex = item.idx + 1
if (nextIndex >= li.length) {
nextIndex = 0
}
remove(item.idx)
add(nextIndex)
}
}
// up
if (keyCode === 38) {
if (!active) {
add(document.querySelectorAll(".results li").length - 1)
return
} else {
const li = document.querySelectorAll(".results li")
let item = getActiveItemInfo(),
prevIndex = item.idx - 1
if (prevIndex < 0) {
prevIndex = li.length - 1
}
remove(item.idx)
add(prevIndex)
}
}
if (keyCode === 13) {
const item = getActiveItemInfo()
remove(item.idx)
item.path !== "/" && gotoPage(item)
}
}
nextTick(() => {
const dom = document.querySelector(".results")
if (dom && dom.scrollTop !== false) {
dom.scrollTop =
(getActiveItemInfo()["idx"] + 1) * 80 - document.querySelectorAll(".results li").length * 10
}
})
})
})
</script>
<style scoped lang="less">
.sys-search-container {
top: 0;
left: 0;
position: absolute;
z-index: 999;
width: 100%;
height: 100%;
overflow: hidden;
& .ssc-bg {
position: absolute;
z-index: 999;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: rgba(100, 100, 100, 0.2);
backdrop-filter: blur(12px);
}
& .center-box {
height: 90%;
}
& .results {
background-color: var(--color-bg-2);
border-radius: 6px;
height: calc(100% - 250px);
overflow-y: auto;
& li {
border-bottom: 1px solid var(--color-border-1);
cursor: pointer;
.title {
font-size: 16px;
}
.path {
color: var(--color-text-3);
}
}
li:hover,
.active-search-li {
background-color: var(--color-neutral-1);
.arco-icon,
.icon {
transition: all 0.25s;
width: 1.8em !important;
height: 1.8em !important;
}
.arco-icon {
color: rgb(var(--primary-6));
}
.icon {
fill: rgb(var(--primary-6));
}
.title {
color: rgb(var(--primary-6));
}
.path {
color: rgb(var(--primary-3));
}
}
.icon-box {
width: 80px;
height: 80px;
border-right: 1px solid var(--color-border-1);
}
}
.arco-icon,
.icon {
width: 1.5em !important;
height: 1.5em !important;
}
.arco-menu-selected .icon {
fill: rgb(var(--primary-6));
}
}
</style>

View File

@@ -0,0 +1,191 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-drawer
class="backend-setting"
v-model:visible="visible"
:on-before-ok="save"
width="350px"
:ok-text="$t('sys.saveToBackend')"
@cancel="close"
unmountOnClose
>
<template #title>{{ $t("sys.backendSettingTitle") }}</template>
<a-form :model="form" :auto-label-width="true">
<a-row class="flex justify-center mb-5">
<a-divider orientation="center"
><span class="title">{{ $t("sys.systemPrimaryColor") }}</span></a-divider
>
<ColorPicker
theme="dark"
:color="appStore.color"
:sucker-hide="true"
:colors-default="defaultColorList"
@changeColor="changeColor"
style="width: 218px"
/>
</a-row>
<a-divider orientation="center"
><span class="title">{{ $t("sys.personalizedConfig") }}</span></a-divider
>
<a-form-item :label="$t('sys.skin')" :help="$t('sys.skinHelp')">
{{ currentSkin }}
<a-button type="primary" status="success" size="mini" class="ml-2" @click="skin.open()">
{{ $t("sys.changeSkin") }}
</a-button>
</a-form-item>
<a-form-item :label="$t('sys.layouts')" :help="$t('sys.layoutsHelp')">
<a-select v-model="form.layout" @change="handleLayout">
<a-option value="classic">{{ $t("sys.layout.classic") }}</a-option>
<a-option value="columns">{{ $t("sys.layout.columns") }}</a-option>
<a-option value="banner">{{ $t("sys.layout.banner") }}</a-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('sys.i18n')" :help="$t('sys.i18nHelp')">
<a-switch v-model="form.i18n" @change="handleI18n" />
</a-form-item>
<a-form-item :label="$t('sys.language')" :help="$t('sys.languageHelp')" v-if="form.i18n">
<a-select v-model="form.language" @change="handleLanguage">
<a-option value="zh_CN">{{ $t("sys.chinese") }}</a-option>
<a-option value="en">{{ $t("sys.english") }}</a-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('sys.animation')" :help="$t('sys.animationHelp')">
<a-select v-model="form.animation" @change="handleAnimation">
<a-option value="ma-fade">{{ $t("sys.animate.fade") }}</a-option>
<a-option value="ma-slide-left">{{ $t("sys.animate.sliderLeft") }}</a-option>
<a-option value="ma-slide-right">{{ $t("sys.animate.sliderRight") }}</a-option>
<a-option value="ma-slide-down">{{ $t("sys.animate.sliderDown") }}</a-option>
<a-option value="ma-slide-up">{{ $t("sys.animate.sliderUp") }}</a-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('sys.dark')" :help="$t('sys.darkHelp')" v-if="currentSkin === 'Mine'">
<a-switch v-model="form.mode" @change="handleSettingMode" />
</a-form-item>
<a-form-item :label="$t('sys.tag')" :help="$t('sys.tagHelp')">
<a-switch v-model="form.tag" @change="handleSettingTag" />
</a-form-item>
<a-form-item v-if="form.layout !== 'banner'" :label="$t('sys.menuFold')" :help="$t('sys.menuFoldHelp')">
<a-switch v-model="form.menuCollapse" @change="handleMenuCollapse" />
</a-form-item>
<a-form-item v-if="form.layout !== 'banner'" :label="$t('sys.menuWidth')" :help="$t('sys.menuWidthHelp')">
<a-input-number v-model="form.menuWidth" mode="button" @change="handleMenuWidth" />
</a-form-item>
</a-form>
</a-drawer>
<Skin ref="skin" />
</template>
<script setup>
import { ref, reactive, watch } from "vue"
import { useAppStore, useUserStore } from "@/store"
import { Message } from "@arco-design/web-vue"
import user from "@/api/system/user"
import Skin from "./components/components/skin.vue"
import skins from "@/config/skins"
import { useI18n } from "vue-i18n"
import { ColorPicker } from "vue-color-kit"
import "vue-color-kit/dist/vue-color-kit.css"
const userStore = useUserStore()
const appStore = useAppStore()
const { t } = useI18n()
const skin = ref(null)
const visible = ref(false)
const okLoading = ref(false)
const currentSkin = ref("")
const form = reactive({
mode: appStore.mode === "dark",
tag: appStore.tag,
menuCollapse: appStore.menuCollapse,
menuWidth: appStore.menuWidth,
layout: appStore.layout,
language: appStore.language,
animation: appStore.animation,
i18n: appStore.i18n
})
const defaultColorList = reactive([
"#165DFF",
"#F53F3F",
"#F77234",
"#F7BA1E",
"#00B42A",
"#14C9C9",
"#3491FA",
"#722ED1",
"#F5319D",
"#D91AD9",
"#34C759",
"#43a047",
"#7cb342",
"#c0ca33",
"#86909c",
"#6d4c41"
])
const changeColor = (color) => {
appStore.changeColor(color.hex)
}
skins.map((item) => {
if (item.name === appStore.skin) currentSkin.value = t("skin." + item.name)
})
watch(
() => appStore.skin,
(v) => {
skins.map((item) => {
if (item.name === v) currentSkin.value = t("skin." + item.name)
})
}
)
const open = () => (visible.value = true)
const close = () => (visible.value = false)
const handleLayout = (val) => appStore.changeLayout(val)
const handleI18n = (val) => appStore.toggleI18n(val)
const handleLanguage = (val) => appStore.changeLanguage(val)
const handleAnimation = (val) => appStore.changeAnimation(val)
const handleSettingMode = (val) => appStore.toggleMode(val ? "dark" : "light")
const handleSettingTag = (val) => appStore.toggleTag(val)
const handleMenuCollapse = (val) => appStore.toggleMenu(val)
const handleMenuWidth = (val) => appStore.changeMenuWidth(val)
watch(
() => appStore.menuCollapse,
(val) => (form.menuCollapse = val)
)
const save = async (done) => {
const data = {
mode: appStore.mode,
tag: appStore.tag,
menuCollapse: appStore.menuCollapse,
menuWidth: appStore.menuWidth,
layout: appStore.layout,
skin: appStore.skin,
i18n: appStore.i18n,
language: appStore.language,
animation: appStore.animation,
color: appStore.color
}
user.updateInfo({ id: userStore.user.id, backend_setting: data }).then((res) => {
res.success && Message.success(res.message)
})
done(true)
}
defineExpose({ open })
</script>