Skip to content

以窗口为单位的后台管理系统

实现了一个可以打开窗口,窗口最小化、拖动、双击最大化/还原的,基于 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>
评论区
评论区空空如也
发送评论
名字
0 / 20
邮箱
0 / 100
评论内容
0 / 140
由于是非实名评论,所以不提供删除功能。如果你需要删除你发送的评论,或者是其他人的评论对你造成了困扰,请 发邮件给我 。同时评论区会使用 AI + 人工的方式进行审核,以达到合规要求。

© thebestxt.cc
辽ICP备16009524号-8
本站所有文章版权所有,转载请注明出处