0%
以窗口为单位的后台管理系统
实现了一个可以打开窗口,窗口最小化、拖动、双击最大化/还原的,基于 element-plus 的后台管理系统模板。
typescript
import type { Component } from 'vue'
import Login from '@/views/login/login.vue'
import Dashboard from '@/views/dashboard/dashboard.vue'
import UserMgt from '@/views/user/userMgt.vue'
import LoginIcon from '@/assets/login-icon.svg?raw'
import DashboardIcon from '@/assets/dashboard-icon.svg?raw'
import UserMgtIcon from '@/assets/userMgt-icon.svg?raw'
export interface WindowItem {
id: string,
name: string,
component: Component,
appIcon: string
}
const windows: WindowItem[] = [
{
id: 'login',
name: '登录',
component: Login,
appIcon: LoginIcon
},
{
id: 'dashboard',
name: '控制台',
component: Dashboard,
appIcon: DashboardIcon
},
{
id: 'user',
name: '用户管理',
component: UserMgt,
appIcon: UserMgtIcon
}
]
export default windows
ts
import { ref, computed, onMounted, type Ref } from 'vue'
import { defineStore } from 'pinia'
import _windows from '@/router/windows'
import windows, { type WindowItem } from '@/router/windows'
export enum WindowStatus {
min = 1,
normal = 2,
max = 3
}
export interface WindowStackItem {
id: WindowItem['id'],
show: boolean,
status: WindowStatus,
name?: string,
icon?: string,
order: number
}
export const useSystemStore = defineStore('system', () => {
const windowStack: Ref<WindowStackItem[]> = ref([])
onMounted(() => {
})
function getItemById(id: WindowItem['id']): WindowStackItem | null {
const find = windowStack.value.find(v => v.id === id)
return find ? find : null
}
function getWindowItemById(id: WindowItem['id']): WindowItem | null {
const find = _windows.find(v => v.id === id)
return find ? find : null
}
function getItemIndexById(id: WindowItem['id']): number {
return windowStack.value.findIndex(v => v.id === id)
}
function setStatus(id: WindowItem['id'], status: WindowStackItem['status']): void {
const find = getItemById(id)
if (find && find.status) {
find.status = status
}
}
function remove(id: WindowItem['id']): void {
const findIndex = getItemIndexById(id)
if (findIndex > -1) {
windowStack.value.splice(findIndex, 1)
}
}
function setTop(id: WindowItem['id']): void {
const findIndex = getItemIndexById(id)
if (findIndex > -1) {
let lastOrder = 0
windowStack.value.forEach((v, k) => {
if (k === findIndex) {
v.order = windowStack.value.length - 1
} else {
v.order = lastOrder
lastOrder ++
}
})
}
}
function activeItem(id: WindowItem['id']): void {
const findIndex: number = windowStack.value.findIndex(vv => vv.id === id)
const v = getWindowItemById(id)
if (findIndex > -1) {
windowStack.value[findIndex].status = WindowStatus.normal
setTop(id)
} else {
if (v === null) return
windowStack.value.push({
id: v.id,
show: true,
name: v.name,
icon: v.appIcon,
status: WindowStatus.normal,
order: windowStack.value.length + 1
})
}
}
return { windowStack, setStatus, remove, setTop, activeItem, getItemById }
})
vue
<script setup lang="ts">
import MinsizeIcon from '@/assets/minsize-icon.svg'
import CloseIcon from '@/assets/close-icon.svg'
import MaxIcon from '@/assets/max-icon.svg'
import MaxReverseIcon from '@/assets/max-reverse-icon.svg'
import { onMounted, ref, type Ref, getCurrentInstance } from 'vue'
import { useSystemStore, WindowStatus } from '@/stores/system'
// import Hammer from 'hammerjs/hammer.min.js'
const systemStore = useSystemStore()
const props = defineProps({
windowId: {
type: String,
default: ''
},
order: {
type: Number,
default: 0
},
title: {
type: String,
default: ''
},
windowItem: {
type: Object,
default: () => ({})
}
})
let dialogDiv: HTMLElement | null = null
let titleDiv: HTMLElement | null = null
onMounted(() => {
dialogDiv = document.getElementById(`dialog-container-${props.windowId}`)
titleDiv = document.getElementById(`dialog-title-${props.windowId}`)
dialogDiv!.style.top = `${50 * props.order + 150}px`
dialogDiv!.style.left = `${50 * props.order + 300}px`
bindDrag()
})
const bindDrag = () => {
if (titleDiv === null || dialogDiv === null) return
function handleDrag(e: MouseEvent): void {
if (props.windowItem.status === WindowStatus.max) {
handleMax()
dialogDiv!.style.top = `${e.clientY}px`
}
dialogDiv!.style.top = `${Number(dialogDiv!.style.top.replace('px', '')) + e.movementY}px`
dialogDiv!.style.left = `${Number(dialogDiv!.style.left.replace('px', '')) + e.movementX}px`
}
dialogDiv.addEventListener('mousedown', (e: MouseEvent) => {
systemStore.setTop(props.windowId)
})
titleDiv.addEventListener('mousedown', (e: MouseEvent) => {
e.preventDefault()
document!.addEventListener('mousemove', handleDrag)
})
document.addEventListener('mouseup', (e: MouseEvent) => {
e.preventDefault()
document.removeEventListener('mousemove', handleDrag)
})
titleDiv.addEventListener('mouseleave', (e: MouseEvent) => {
e.preventDefault()
})
}
const handleMin = () => {
systemStore.setStatus(props.windowId, WindowStatus.min)
}
const handleClose = () => {
systemStore.remove(props.windowId)
}
const handleMax = () => {
if (props.windowItem.status === WindowStatus.max) {
dialogDiv!.style.top = `${150}px`
dialogDiv!.style.left = `${300}px`
}
systemStore.setStatus(props.windowId, props.windowItem.status === WindowStatus.max ? WindowStatus.normal : WindowStatus.max)
}
</script>
<template>
<div :id="`dialog-container-${props.windowId}`" :class="['dialog-container', props.windowItem.status === WindowStatus.max ? 'dialog-max-size' : '']" :style="{zIndex: 1000 + props.order}" v-show="props.windowItem.status !== 1">
<div class="header">
<div
class="title"
:id="`dialog-title-${props.windowId}`"
@dblclick="handleMax">
<slot name="title" v-if="$slots.title"></slot>
<span v-if="props.title">{{ props.title }}</span>
</div>
<div class="right">
<MinsizeIcon class="icon min" @click.prevent="handleMin"></MinsizeIcon>
<MaxIcon class="icon max" v-show="props.windowItem.status !== WindowStatus.max" @click.prevent="handleMax"></MaxIcon>
<MaxReverseIcon class="icon max reverse" v-show="props.windowItem.status === WindowStatus.max" @click.prevent="handleMax"></MaxReverseIcon>
<CloseIcon class="icon close" @click.prevent="handleClose"></CloseIcon>
</div>
</div>
<div class="body-container">
<slot></slot>
</div>
</div>
</template>
<style lang="scss" scoped>
.dialog-container {
width: 50vw;
height: 400px;
position: fixed;
box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
border-radius: 10px;
display: flex;
flex-direction: column;
top: 150px;
left: 300px;
&.dialog-max-size {
width: 100vw !important;
height: calc(100vh - 44px - 66px) !important;
top: 44px !important;
left: 0 !important;
z-index: 10000 !important;
}
.header {
width: 100%;
height: 55px;
background-color: #ebf4ff;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
&:hover {
}
&:active {
background-color: #deebfb;
}
.title {
margin-left: 15px;
color: #333;
width: calc(100% - 120px);
}
.right {
width: 100px;
margin-right: 15px;
.icon {
width: 20px;
height: 20px;
&:hover {
cursor: pointer;
fill: #666;
}
&:not(:last-of-type) {
margin-right: 15px;
}
}
}
}
.body-container {
width: 100%;
height: calc(100% - 55px);
background-color: #fff;
overflow: scroll;
&::-webkit-scrollbar {
display: none;
}
}
}
</style>