保存
This commit is contained in:
53
src/ui/App.vue
Normal file
53
src/ui/App.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<n-config-provider
|
||||
:config-provider-props="configProviderProps"
|
||||
class="pos-relative w-screen h-vh"
|
||||
>
|
||||
<div class="desktop-root" @contextmenu="onContextMenu">
|
||||
<div class="desktop-bg">
|
||||
<DesktopContainer/>
|
||||
</div>
|
||||
<div class="task-bar">
|
||||
<div id="taskbar" class="w-[80px] h-full flex justify-center items-center bg-blue">
|
||||
测试
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { configProviderProps } from '@/core/common/naive-ui/theme.ts'
|
||||
import DesktopContainer from '@/ui/desktop-container/DesktopContainer.vue'
|
||||
|
||||
const onContextMenu = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
console.log(e)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$taskBarHeight: 40px;
|
||||
.desktop-root {
|
||||
@apply w-full h-full flex flex-col;
|
||||
|
||||
.desktop-bg {
|
||||
@apply w-full h-full flex-1 p-2 pos-relative;
|
||||
background-image: url('imgs/desktop-bg-2.jpeg');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
height: calc(100% - #{$taskBarHeight});
|
||||
}
|
||||
|
||||
.desktop-icons-container {
|
||||
@apply w-full h-full flex-1 grid grid-auto-flow-col pos-relative;
|
||||
}
|
||||
|
||||
.task-bar {
|
||||
@apply w-full bg-gray-200 flex justify-center items-center;
|
||||
height: $taskBarHeight;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
52
src/ui/desktop-container/AppIcon.vue
Normal file
52
src/ui/desktop-container/AppIcon.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div
|
||||
class="icon-container"
|
||||
:style="`grid-column: ${iconInfo.x}/${iconInfo.x + 1};grid-row: ${iconInfo.y}/${iconInfo.y + 1};`"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart"
|
||||
@dragend="onDragEnd"
|
||||
>
|
||||
{{ iconInfo.name }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IDesktopAppIcon } from '@/ui/types/IDesktopAppIcon.ts'
|
||||
import type { IGridTemplateParams } from '@/ui/types/IGridTemplateParams.ts'
|
||||
|
||||
const { iconInfo, gridTemplate } = defineProps<{ iconInfo: IDesktopAppIcon, gridTemplate: IGridTemplateParams }>()
|
||||
|
||||
const onDragStart = (e: DragEvent) => {}
|
||||
|
||||
const onDragEnd = (e: DragEvent) => {
|
||||
const el = e.target as HTMLElement | null
|
||||
if (!el) return
|
||||
// 鼠标所在位置已存在图标元素
|
||||
const pointTarget = document.elementFromPoint(e.clientX, e.clientY)
|
||||
if (!pointTarget) return
|
||||
if (pointTarget.classList.contains('icon-container')) return
|
||||
if (!pointTarget.classList.contains('desktop-icons-container')) return
|
||||
|
||||
// 获取容器边界
|
||||
const rect = el.parentElement!.getBoundingClientRect()
|
||||
|
||||
// 鼠标相对容器左上角坐标
|
||||
const mouseX = e.clientX - rect.left
|
||||
const mouseY = e.clientY - rect.top
|
||||
|
||||
// 计算鼠标所在单元格坐标(向上取整,从1开始)
|
||||
const gridX = Math.ceil(mouseX / gridTemplate.cellRealWidth)
|
||||
const gridY = Math.ceil(mouseY / gridTemplate.cellRealHeight)
|
||||
|
||||
iconInfo.x = gridX
|
||||
iconInfo.y = gridY
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.icon-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@apply flex flex-col items-center justify-center bg-gray-200;
|
||||
}
|
||||
</style>
|
||||
23
src/ui/desktop-container/DesktopContainer.vue
Normal file
23
src/ui/desktop-container/DesktopContainer.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="desktop-icons-container" :style="gridStyle">
|
||||
<AppIcon
|
||||
v-for="(appIcon, i) in appIconsRef"
|
||||
:key="i"
|
||||
:iconInfo="appIcon"
|
||||
:gridTemplate="gridTemplate"
|
||||
@dblclick="runApp(appIcon)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppIcon from '@/ui/desktop-container/AppIcon.vue'
|
||||
import type { IDesktopAppIcon } from '@/ui/types/IDesktopAppIcon.ts'
|
||||
import { useDesktopContainerInit } from '@/ui/desktop-container/useDesktopContainerInit.ts'
|
||||
|
||||
const { appIconsRef, gridStyle, gridTemplate } = useDesktopContainerInit('.desktop-icons-container')
|
||||
|
||||
const runApp = (appIcon: IDesktopAppIcon) => {}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
164
src/ui/desktop-container/useDesktopContainerInit.ts
Normal file
164
src/ui/desktop-container/useDesktopContainerInit.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import type { IDesktopAppIcon } from '@/ui/types/IDesktopAppIcon.ts'
|
||||
import {
|
||||
computed,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
reactive,
|
||||
ref,
|
||||
toRaw,
|
||||
toValue,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import type { IGridTemplateParams } from '@/ui/types/IGridTemplateParams.ts'
|
||||
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
|
||||
|
||||
export function useDesktopContainerInit(containerStr: string) {
|
||||
let container:HTMLElement
|
||||
// 初始值
|
||||
const gridTemplate = reactive<IGridTemplateParams>({
|
||||
cellExpectWidth: 90,
|
||||
cellExpectHeight: 110,
|
||||
cellRealWidth: 90,
|
||||
cellRealHeight: 110,
|
||||
gapX: 4,
|
||||
gapY: 4,
|
||||
colCount: 1,
|
||||
rowCount: 1
|
||||
})
|
||||
|
||||
const gridStyle = computed(() => ({
|
||||
gridTemplateColumns: `repeat(${gridTemplate.colCount}, minmax(${gridTemplate.cellExpectWidth}px, 1fr))`,
|
||||
gridTemplateRows: `repeat(${gridTemplate.rowCount}, minmax(${gridTemplate.cellExpectHeight}px, 1fr))`,
|
||||
gap: `${gridTemplate.gapX}px ${gridTemplate.gapY}px`
|
||||
}))
|
||||
|
||||
const ro = new ResizeObserver(entries => {
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
gridTemplate.colCount = Math.floor((containerRect.width + gridTemplate.gapX) / (gridTemplate.cellExpectWidth + gridTemplate.gapX));
|
||||
gridTemplate.rowCount = Math.floor((containerRect.height + gridTemplate.gapY) / (gridTemplate.cellExpectHeight + gridTemplate.gapY));
|
||||
|
||||
const w = containerRect.width - (gridTemplate.gapX * (gridTemplate.colCount - 1))
|
||||
const h = containerRect.height - (gridTemplate.gapY * (gridTemplate.rowCount - 1))
|
||||
gridTemplate.cellRealWidth = Number((w / gridTemplate.colCount).toFixed(2))
|
||||
gridTemplate.cellRealHeight = Number((h / gridTemplate.rowCount).toFixed(2))
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
container = document.querySelector(containerStr)!
|
||||
ro.observe(container)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
ro.unobserve(container)
|
||||
ro.disconnect()
|
||||
})
|
||||
|
||||
// 有桌面图标的app
|
||||
// const appInfos = processManager.processInfos.filter(processInfo => !processInfo.isJustProcess)
|
||||
const appInfos: IProcessInfo[] = []
|
||||
const oldAppIcons: IDesktopAppIcon[] = JSON.parse(localStorage.getItem('desktopAppIconInfo') || '[]')
|
||||
const appIcons: IDesktopAppIcon[] = appInfos.map((processInfo, index) => {
|
||||
const oldAppIcon = oldAppIcons.find(oldAppIcon => oldAppIcon.name === processInfo.name)
|
||||
|
||||
// 左上角坐标原点,从上到下从左到右 索引从1开始
|
||||
const x = index % gridTemplate.rowCount + 1
|
||||
const y = Math.floor(index / gridTemplate.rowCount) + 1
|
||||
|
||||
return {
|
||||
name: processInfo.name,
|
||||
icon: processInfo.icon,
|
||||
path: processInfo.startName,
|
||||
x: oldAppIcon ? oldAppIcon.x : x,
|
||||
y: oldAppIcon ? oldAppIcon.y : y
|
||||
}
|
||||
})
|
||||
|
||||
const appIconsRef = ref(appIcons)
|
||||
const exceedApp = ref<IDesktopAppIcon[]>([])
|
||||
|
||||
watch(() => [gridTemplate.colCount, gridTemplate.rowCount], ([nCols, nRows], [oCols, oRows]) => {
|
||||
// if (oCols == 1 && oRows == 1) return
|
||||
if (oCols === nCols && oRows === nRows) return
|
||||
const { appIcons, hideAppIcons } = rearrangeIcons(toRaw(appIconsRef.value), nCols, nRows)
|
||||
appIconsRef.value = appIcons
|
||||
exceedApp.value = hideAppIcons
|
||||
})
|
||||
|
||||
watch(() => appIconsRef.value, (appIcons) => {
|
||||
localStorage.setItem('desktopAppIconInfo', JSON.stringify(appIcons))
|
||||
})
|
||||
|
||||
return {
|
||||
gridTemplate,
|
||||
appIconsRef,
|
||||
gridStyle
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新安排图标位置
|
||||
* @param appIconInfos 图标信息
|
||||
* @param maxCol 列数
|
||||
* @param maxRow 行数
|
||||
*/
|
||||
function rearrangeIcons(
|
||||
appIconInfos: IDesktopAppIcon[],
|
||||
maxCol: number,
|
||||
maxRow: number
|
||||
): IRearrangeInfo {
|
||||
const occupied = new Set<string>();
|
||||
|
||||
function key(x: number, y: number) {
|
||||
return `${x},${y}`;
|
||||
}
|
||||
|
||||
const appIcons: IDesktopAppIcon[] = []
|
||||
const hideAppIcons: IDesktopAppIcon[] = []
|
||||
const temp: IDesktopAppIcon[] = []
|
||||
|
||||
for (const appIcon of appIconInfos) {
|
||||
const { x, y } = appIcon;
|
||||
|
||||
if (x <= maxCol && y <= maxRow) {
|
||||
if (!occupied.has(key(x, y))) {
|
||||
occupied.add(key(x, y))
|
||||
appIcons.push({ ...appIcon, x, y })
|
||||
}
|
||||
} else {
|
||||
temp.push(appIcon)
|
||||
}
|
||||
}
|
||||
|
||||
const max = maxCol * maxRow
|
||||
for (const appIcon of temp) {
|
||||
if (appIcons.length < max) {
|
||||
// 最后格子也被占 → 从 (1,1) 开始找空位
|
||||
let placed = false;
|
||||
for (let c = 1; c <= maxCol; c++) {
|
||||
for (let r = 1; r <= maxRow; r++) {
|
||||
if (!occupied.has(key(c, r))) {
|
||||
occupied.add(key(c, r));
|
||||
appIcons.push({ ...appIcon, x: c, y: r });
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
} else {
|
||||
// 放不下了
|
||||
hideAppIcons.push(appIcon)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
appIcons,
|
||||
hideAppIcons
|
||||
};
|
||||
}
|
||||
|
||||
interface IRearrangeInfo {
|
||||
/** 正常的桌面图标信息 */
|
||||
appIcons: IDesktopAppIcon[];
|
||||
/** 隐藏的桌面图标信息(超出屏幕显示的) */
|
||||
hideAppIcons: IDesktopAppIcon[];
|
||||
}
|
||||
BIN
src/ui/imgs/desktop-bg-1.jpeg
Normal file
BIN
src/ui/imgs/desktop-bg-1.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
src/ui/imgs/desktop-bg-2.jpeg
Normal file
BIN
src/ui/imgs/desktop-bg-2.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
15
src/ui/types/IDesktopAppIcon.ts
Normal file
15
src/ui/types/IDesktopAppIcon.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 桌面应用图标信息
|
||||
*/
|
||||
export interface IDesktopAppIcon {
|
||||
/** 图标name */
|
||||
name: string;
|
||||
/** 图标 */
|
||||
icon: string;
|
||||
/** 图标路径 */
|
||||
path: string;
|
||||
/** 图标在grid布局中的列 */
|
||||
x: number;
|
||||
/** 图标在grid布局中的行 */
|
||||
y: number;
|
||||
}
|
||||
21
src/ui/types/IGridTemplateParams.ts
Normal file
21
src/ui/types/IGridTemplateParams.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 桌面网格模板参数
|
||||
*/
|
||||
export interface IGridTemplateParams {
|
||||
/** 单元格预设宽度 */
|
||||
readonly cellExpectWidth: number
|
||||
/** 单元格预设高度 */
|
||||
readonly cellExpectHeight: number
|
||||
/** 单元格实际宽度 */
|
||||
cellRealWidth: number
|
||||
/** 单元格实际高度 */
|
||||
cellRealHeight: number
|
||||
/** 列间距 */
|
||||
gapX: number
|
||||
/** 行间距 */
|
||||
gapY: number
|
||||
/** 总列数 */
|
||||
colCount: number
|
||||
/** 总行数 */
|
||||
rowCount: number
|
||||
}
|
||||
10
src/ui/types/WindowFormTypes.ts
Normal file
10
src/ui/types/WindowFormTypes.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 窗体位置坐标 - 左上角
|
||||
*/
|
||||
export interface WindowFormPos {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/** 窗口状态 */
|
||||
export type TWindowFormState = 'default' | 'minimized' | 'maximized';
|
||||
Reference in New Issue
Block a user