zwpanel/src/views/home/index.vue

649 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { VueDraggable } from 'vue-draggable-plus'
import { NBackTop, NButton, NButtonGroup, NDropdown, NModal, NSkeleton, NSpin, useDialog, useMessage } from 'naive-ui'
import { nextTick, onMounted, ref } from 'vue'
import { AppIcon, AppStarter, EditItem } from './components'
import { Clock, SearchBox, SystemMonitor } from '@/components/deskModule'
import { SvgIcon } from '@/components/common'
import { deletes, getListByGroupId, saveSort } from '@/api/panel/itemIcon'
import { getList as getGroupList } from '@/api/panel/itemIconGroup'
import { setTitle, updateLocalUserInfo } from '@/utils/cmn'
import { useAuthStore, usePanelState } from '@/store'
import { PanelPanelConfigStyleEnum, PanelStateNetworkModeEnum } from '@/enums'
import { VisitMode } from '@/enums/auth'
import { router } from '@/router'
interface ItemGroup extends Panel.ItemIconGroup {
sortStatus?: boolean
hoverStatus: boolean
items?: Panel.ItemInfo[]
}
const ms = useMessage()
const dialog = useDialog()
const panelState = usePanelState()
const authStore = useAuthStore()
const scrollContainerRef = ref<HTMLElement | undefined>(undefined)
const editItemInfoShow = ref<boolean>(false)
const editItemInfoData = ref<Panel.ItemInfo | null>(null)
const windowShow = ref<boolean>(false)
const windowSrc = ref<string>('')
const windowTitle = ref<string>('')
const windowIframeRef = ref(null)
const windowIframeIsLoad = ref<boolean>(false)
const dropdownMenuX = ref(0)
const dropdownMenuY = ref(0)
const dropdownShow = ref(false)
const currentRightSelectItem = ref<Panel.ItemInfo | null>(null)
const currentAddItenIconGroupId = ref<number | undefined>()
const settingModalShow = ref(false)
const items = ref<ItemGroup[]>([])
const filterItems = ref<ItemGroup[]>([])
function openPage(openMethod: number, url: string, title?: string) {
switch (openMethod) {
case 1:
window.location.href = url
break
case 2:
window.open(url)
break
case 3:
windowShow.value = true
windowSrc.value = url
windowTitle.value = title || url
windowIframeIsLoad.value = true
break
default:
break
}
}
function handleItemClick(itemGroupIndex: number, item: Panel.ItemInfo) {
if (items.value[itemGroupIndex] && items.value[itemGroupIndex].sortStatus) {
handleEditItem(item)
return
}
let jumpUrl = ''
if (item)
jumpUrl = (panelState.networkMode === PanelStateNetworkModeEnum.lan ? item.lanUrl : item.url) as string
if (item.lanUrl === '')
jumpUrl = item.url
openPage(item.openMethod, jumpUrl, item.title)
}
function handWindowIframeIdLoad(payload: Event) {
windowIframeIsLoad.value = false
}
function getList() {
// 获取组数据
getGroupList<Common.ListResponse<ItemGroup[]>>().then(({ code, data, msg }) => {
if (code === 0)
items.value = data.list
for (let i = 0; i < data.list.length; i++) {
const element = data.list[i]
if (element.id)
updateItemIconGroupByNet(i, element.id)
}
filterItems.value = items.value
// console.log(items)
})
}
// 从后端获取组下面的图标
function updateItemIconGroupByNet(itemIconGroupIndex: number, itemIconGroupId: number) {
getListByGroupId<Common.ListResponse<Panel.ItemInfo[]>>(itemIconGroupId).then((res) => {
if (res.code === 0)
items.value[itemIconGroupIndex].items = res.data.list
})
}
function handleRightMenuSelect(key: string | number) {
dropdownShow.value = false
// console.log(currentRightSelectItem, key)
let jumpUrl = panelState.networkMode === PanelStateNetworkModeEnum.lan ? currentRightSelectItem.value?.lanUrl : currentRightSelectItem.value?.url
if (currentRightSelectItem.value?.lanUrl === '')
jumpUrl = currentRightSelectItem.value.url
switch (key) {
case 'newWindows':
window.open(jumpUrl)
break
case 'openWanUrl':
if (currentRightSelectItem.value)
openPage(currentRightSelectItem.value?.openMethod, currentRightSelectItem.value?.url, currentRightSelectItem.value?.title)
break
case 'openLanUrl':
if (currentRightSelectItem.value && currentRightSelectItem.value.lanUrl)
openPage(currentRightSelectItem.value?.openMethod, currentRightSelectItem.value.lanUrl, currentRightSelectItem.value?.title)
break
case 'edit':
// 这里有个奇怪的问题,如果不使用{...}的方式 父组件的值会同步修改 标记一下
handleEditItem({ ...currentRightSelectItem.value } as Panel.ItemInfo)
break
case 'delete':
dialog.warning({
title: '警告',
content: `你确定要删除图标 ${currentRightSelectItem.value?.title} `,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
deletes([currentRightSelectItem.value?.id as number]).then(({ code, msg }) => {
if (code === 0) {
ms.success('已删除')
getList()
}
else {
ms.error(`删除失败:${msg}`)
}
})
},
})
break
default:
break
}
}
function handleContextMenu(e: MouseEvent, itemGroupIndex: number, item: Panel.ItemInfo) {
if (items.value[itemGroupIndex] && items.value[itemGroupIndex].sortStatus)
return
e.preventDefault()
currentRightSelectItem.value = item
dropdownShow.value = false
nextTick().then(() => {
dropdownShow.value = true
dropdownMenuX.value = e.clientX
dropdownMenuY.value = e.clientY
})
}
function onClickoutside() {
// message.info('clickoutside')
dropdownShow.value = false
}
function handleEditSuccess(item: Panel.ItemInfo) {
getList()
}
function handleChangeNetwork(mode: PanelStateNetworkModeEnum) {
panelState.setNetworkMode(mode)
if (mode === PanelStateNetworkModeEnum.lan)
ms.success('已经切换成局域网模式(此配置仅保存在本地)')
else
ms.success('已经切换成互联网模式(此配置仅保存在本地)')
}
// 结束拖拽
// function handleEndDrag(event: any, itemIconGroup: Panel.ItemIconGroup) {
// // console.log(event)
// // console.log(items.value)
// }
function handleSaveSort(itemGroup: ItemGroup) {
const saveItems: Common.SortItemRequest[] = []
if (itemGroup.items) {
for (let i = 0; i < itemGroup.items.length; i++) {
const element = itemGroup.items[i]
saveItems.push({
id: element.id as number,
sort: i + 1,
})
}
saveSort({ itemIconGroupId: itemGroup.id as number, sortItems: saveItems }).then(({ code, msg }) => {
if (code === 0) {
ms.success('保存成功')
itemGroup.sortStatus = false
}
else {
ms.error(`保存失败:${msg}`)
}
})
}
}
function getDropdownMenuOptions() {
const dropdownMenuOptions = [
{
label: '新窗口打开',
key: 'newWindows',
},
]
if (currentRightSelectItem.value?.lanUrl && panelState.networkMode === PanelStateNetworkModeEnum.wan) {
dropdownMenuOptions.push({
label: '打开局域网地址',
key: 'openLanUrl',
})
}
if (currentRightSelectItem.value?.lanUrl && panelState.networkMode === PanelStateNetworkModeEnum.lan) {
dropdownMenuOptions.push({
label: '打开互联网地址',
key: 'openWanUrl',
})
}
if (authStore.visitMode === VisitMode.VISIT_MODE_LOGIN) {
dropdownMenuOptions.push({
label: '编辑',
key: 'edit',
}, {
label: '删除',
key: 'delete',
})
}
return dropdownMenuOptions
}
onMounted(() => {
// 更新用户信息
updateLocalUserInfo()
getList()
// 更新同步云端配置
panelState.updatePanelConfigByCloud()
// 设置标题
if (panelState.panelConfig.logoText)
setTitle(panelState.panelConfig.logoText)
})
// 前端搜索过滤
function itemFrontEndSearch(keyword?: string) {
keyword = keyword?.trim()
if (keyword !== '' && panelState.panelConfig.searchBoxSearchIcon) {
const filteredData = ref<ItemGroup[]>([])
for (let i = 0; i < items.value.length; i++) {
const element = items.value[i].items?.filter((item: Panel.ItemInfo) => {
return (
item.title.toLowerCase().includes(keyword?.toLowerCase() ?? '')
|| item.url.toLowerCase().includes(keyword?.toLowerCase() ?? '')
|| item.description?.toLowerCase().includes(keyword?.toLowerCase() ?? '')
)
})
if (element && element.length > 0)
filteredData.value.push({ items: element, hoverStatus: false })
}
filterItems.value = filteredData.value
}
else {
filterItems.value = items.value
}
}
function handleSetHoverStatus(groupIndex: number, hoverStatus: boolean) {
if (items.value[groupIndex])
items.value[groupIndex].hoverStatus = hoverStatus
}
function handleSetSortStatus(groupIndex: number, sortStatus: boolean) {
if (items.value[groupIndex])
items.value[groupIndex].sortStatus = sortStatus
// 并未保存排序重新更新数据
if (!sortStatus) {
// 单独更新组
if (items.value[groupIndex] && items.value[groupIndex].id)
updateItemIconGroupByNet(groupIndex, items.value[groupIndex].id as number)
}
}
function handleEditItem(item: Panel.ItemInfo) {
editItemInfoData.value = item
editItemInfoShow.value = true
currentAddItenIconGroupId.value = undefined
}
function handleAddItem(itemIconGroupId?: number) {
editItemInfoData.value = null
editItemInfoShow.value = true
if (itemIconGroupId)
currentAddItenIconGroupId.value = itemIconGroupId
}
</script>
<template>
<div class="w-full h-full sun-main ">
<div
class="cover" :style="{
filter: `blur(${panelState.panelConfig.backgroundBlur}px)`,
background: `url(${panelState.panelConfig.backgroundImageSrc}) no-repeat`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}"
/>
<div class="mask" :style="{ backgroundColor: `rgba(0,0,0,${panelState.panelConfig.backgroundMaskNumber})` }" />
<div ref="scrollContainerRef" class="absolute w-full h-full overflow-auto">
<div v-if="panelState.panelConfig.searchBoxShow" class="flex mx-auto ">
<SystemMonitor @itemSearch="itemFrontEndSearch" />
</div>
<div
class="p-2.5 mx-auto"
:style="{
marginTop: `${panelState.panelConfig.marginTop}%`,
marginBottom: `${panelState.panelConfig.marginBottom}%`,
maxWidth: (panelState.panelConfig.maxWidth ?? '1200') + panelState.panelConfig.maxWidthUnit,
}"
>
<!-- -->
<div class="mx-[auto] w-[80%]">
<div class="flex mx-[auto] items-center justify-center text-white">
<div>
<span class="text-2xl md:text-6xl font-bold text-shadow">
{{ panelState.panelConfig.logoText }}
</span>
</div>
<div class="text-base lg:text-2xl mx-[10px]">
|
</div>
<div class="text-shadow">
<Clock :hide-second="!panelState.panelConfig.clockShowSecond" />
</div>
</div>
<div v-if="panelState.panelConfig.searchBoxShow" class="flex mt-[20px] mx-auto sm:w-full lg:w-[80%]">
<SearchBox @itemSearch="itemFrontEndSearch" />
</div>
</div>
<!-- 应用盒子 -->
<div class="mt-[50px]" :style="{ marginLeft: `${panelState.panelConfig.marginX}px`, marginRight: `${panelState.panelConfig.marginX}px` }">
<!-- <div v-if="panelState.panelConfig.searchBoxShow" class="flex mt-[20px] mx-auto ">
<SystemMonitor @itemSearch="itemFrontEndSearch" />
</div> -->
<!-- 组纵向排列 -->
<div
v-for="(itemGroup, itemGroupIndex) in filterItems" :key="itemGroupIndex"
class="mt-[50px]"
:class="itemGroup.sortStatus ? 'shadow-2xl border shadow-[0_0_30px_10px_rgba(0,0,0,0.3)] p-[10px] rounded-2xl' : ''"
@mouseenter="handleSetHoverStatus(itemGroupIndex, true)"
@mouseleave="handleSetHoverStatus(itemGroupIndex, false)"
>
<!-- 分组标题 -->
<div class="text-white text-xl font-extrabold mb-[20px] ml-[10px] flex items-center">
<span class="text-shadow">
{{ itemGroup.title }}
</span>
<div
v-if="authStore.visitMode === VisitMode.VISIT_MODE_LOGIN"
class="ml-2 delay-100 transition-opacity flex"
:class="itemGroup.hoverStatus ? 'opacity-100' : 'opacity-0'"
>
<span class="mr-2 cursor-pointer" title="添加快捷图标" @click="handleAddItem(itemGroup.id)">
<SvgIcon class="text-white font-xl" icon="typcn:plus" />
</span>
<span class="mr-2 cursor-pointer " title="排序组快捷图标" @click="handleSetSortStatus(itemGroupIndex, !itemGroup.sortStatus)">
<SvgIcon class="text-white font-xl" icon="ri:drag-drop-line" />
</span>
</div>
</div>
<!-- 详情图标 -->
<div v-if="panelState.panelConfig.iconStyle === PanelPanelConfigStyleEnum.info">
<div v-if="itemGroup.items">
<VueDraggable
v-model="itemGroup.items" item-key="sort" :animation="300"
class="icon-info-box"
filter=".not-drag"
:disabled="!itemGroup.sortStatus"
>
<div v-for="item, index in itemGroup.items" :key="index" :title="item.description" @contextmenu="(e) => handleContextMenu(e, itemGroupIndex, item)">
<AppIcon
:class="itemGroup.sortStatus ? 'cursor-move' : 'cursor-pointer'"
:item-info="item"
:icon-text-color="panelState.panelConfig.iconTextColor"
:icon-text-info-hide-description="panelState.panelConfig.iconTextInfoHideDescription || false"
:icon-text-icon-hide-title="panelState.panelConfig.iconTextIconHideTitle || false"
:style="0"
@click="handleItemClick(itemGroupIndex, item)"
/>
</div>
<div v-if="itemGroup.items.length === 0" class="not-drag">
<AppIcon
:class="itemGroup.sortStatus ? 'cursor-move' : 'cursor-pointer'"
:item-info="{ icon: { itemType: 3, text: 'subway:add' }, title: '添加图标', url: '', openMethod: 0 }"
:icon-text-color="panelState.panelConfig.iconTextColor"
:icon-text-info-hide-description="panelState.panelConfig.iconTextInfoHideDescription || false"
:icon-text-icon-hide-title="panelState.panelConfig.iconTextIconHideTitle || false"
:style="0"
@click="handleAddItem(itemGroup.id)"
/>
</div>
</VueDraggable>
</div>
</div>
<!-- APP图标宫型盒子 -->
<div v-if="panelState.panelConfig.iconStyle === PanelPanelConfigStyleEnum.icon">
<div v-if="itemGroup.items">
<VueDraggable
v-model="itemGroup.items" item-key="id" :animation="300"
class="icon-small-box"
filter=".not-drag"
:disabled="!itemGroup.sortStatus"
>
<div v-for="item, index in itemGroup.items" :key="index" :title="item.description" @contextmenu="(e) => handleContextMenu(e, itemGroupIndex, item)">
<AppIcon
:class="itemGroup.sortStatus ? 'cursor-move' : 'cursor-pointer'"
:item-info="item"
:icon-text-color="panelState.panelConfig.iconTextColor"
:icon-text-info-hide-description="!panelState.panelConfig.iconTextInfoHideDescription"
:icon-text-icon-hide-title="panelState.panelConfig.iconTextIconHideTitle || false"
:style="1"
@click="handleItemClick(itemGroupIndex, item)"
/>
</div>
<div v-if="itemGroup.items.length === 0" class="not-drag">
<AppIcon
class="cursor-pointer"
:item-info="{ icon: { itemType: 3, text: 'subway:add' }, title: '添加图标', url: '', openMethod: 0 }"
:icon-text-color="panelState.panelConfig.iconTextColor"
:icon-text-info-hide-description="!panelState.panelConfig.iconTextInfoHideDescription"
:icon-text-icon-hide-title="panelState.panelConfig.iconTextIconHideTitle || false"
:style="1"
@click="handleAddItem(itemGroup.id)"
/>
</div>
</vuedraggable>
</div>
</div>
<!-- 编辑栏 -->
<div v-if="itemGroup.sortStatus" class="flex mt-[10px]">
<div>
<NButton color="#2a2a2a6b" @click="handleSaveSort(itemGroup)">
<template #icon>
<SvgIcon class="text-white font-xl" icon="material-symbols:save" />
</template>
<div>
保存排序
</div>
</NButton>
</div>
</div>
</div>
</div>
<div class="mt-5 footer" v-html="panelState.panelConfig.footerHtml" />
</div>
</div>
<!-- 右键菜单 -->
<NDropdown
placement="bottom-start" trigger="manual" :x="dropdownMenuX" :y="dropdownMenuY"
:options="getDropdownMenuOptions()" :show="dropdownShow" :on-clickoutside="onClickoutside" @select="handleRightMenuSelect"
/>
<!-- 悬浮按钮 -->
<div class="fixed-element shadow-[0_0_10px_2px_rgba(0,0,0,0.2)]">
<NButtonGroup vertical>
<NButton
v-if="panelState.networkMode === PanelStateNetworkModeEnum.lan" color="#2a2a2a6b"
title="当前:局域网模式,点击切换成互联网模式" @click="handleChangeNetwork(PanelStateNetworkModeEnum.wan)"
>
<template #icon>
<SvgIcon class="text-white font-xl" icon="material-symbols:lan-outline-rounded" />
</template>
</NButton>
<NButton
v-if="panelState.networkMode === PanelStateNetworkModeEnum.wan" color="#2a2a2a6b"
title="当前:互联网模式,点击切换成局域网模式" @click="handleChangeNetwork(PanelStateNetworkModeEnum.lan)"
>
<template #icon>
<SvgIcon class="text-white font-xl" icon="mdi:wan" />
</template>
</NButton>
<NButton v-if="authStore.visitMode === VisitMode.VISIT_MODE_LOGIN" color="#2a2a2a6b" @click="settingModalShow = !settingModalShow">
<template #icon>
<SvgIcon class="text-white font-xl" icon="majesticons-applications" />
</template>
</NButton>
<NButton v-if="authStore.visitMode === VisitMode.VISIT_MODE_PUBLIC" color="#2a2a2a6b" title="登录" @click="router.push('/login')">
<template #icon>
<SvgIcon class="text-white font-xl" icon="material-symbols:account-circle" />
</template>
</NButton>
</NButtonGroup>
<NBackTop
:listen-to="() => scrollContainerRef"
:right="10"
:bottom="10"
style="background-color:transparent;border: none;box-shadow: none;"
>
<div class="shadow-[0_0_10px_2px_rgba(0,0,0,0.2)]">
<NButton color="#2a2a2a6b">
<template #icon>
<SvgIcon class="text-white font-xl" icon="icon-park-outline:to-top" />
</template>
</NButton>
</div>
</NBackTop>
<AppStarter v-model:visible="settingModalShow" />
<!-- <Setting v-model:visible="settingModalShow" /> -->
</div>
<EditItem v-model:visible="editItemInfoShow" :item-info="editItemInfoData" :item-group-id="currentAddItenIconGroupId" @done="handleEditSuccess" />
<!-- 弹窗 -->
<NModal
v-model:show="windowShow" :mask-closable="false" preset="card"
style="max-width: 1000px;height: 600px;border-radius: 1rem;" :bordered="false" size="small" role="dialog"
aria-modal="true"
>
<template #header>
<div class="flex items-center">
<span class="mr-[20px]">
{{ windowTitle }}
</span>
<NSpin v-if="windowIframeIsLoad" size="small" />
</div>
</template>
<div class="w-full h-full rounded-2xl overflow-hidden border">
<NSkeleton v-if="windowIframeIsLoad" height="100%" width="100%" />
<iframe
v-show="!windowIframeIsLoad" id="windowIframeId" ref="windowIframeRef" :src="windowSrc"
class="w-full h-full" frameborder="0" @load="handWindowIframeIdLoad"
/>
</div>
</NModal>
</div>
</template>
<style>
body,
html {
overflow: hidden;
background-color: rgb(54, 54, 54);
}
</style>
<style scoped>
.mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.sun-main {
user-select: none;
}
.cover {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
/* background: url(@/assets/start_sky.jpg) no-repeat; */
transform: scale(1.05);
}
.text-shadow {
text-shadow: 2px 2px 50px rgb(0, 0, 0);
}
.app-icon-text-shadow {
text-shadow: 2px 2px 5px rgb(0, 0, 0);
}
.fixed-element {
position: fixed;
/* 将元素固定在屏幕上 */
right: 10px;
/* 距离屏幕顶部的距离 */
bottom: 50px;
/* 距离屏幕左侧的距离 */
}
.icon-info-box {
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 18px;
}
.icon-small-box {
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(75px, 1fr));
gap: 18px;
}
@media (max-width: 500px) {
.icon-info-box{
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
}
</style>