feat(vimp): 新增ScreenPanel和screen-store支持多屏增删与重命名

- 新建 stores/screen.ts(id用 crypto.randomUUID 唯一生成、addScreen/removeScreen/renameScreen actions)
- 新建 screen-panel.vue 替换 canvas-area.vue(NTab + NTabs type=card + addable/closable)
- 添加屏幕增删的二次确认弹窗(window.$dialog.warning/info)
- 双击Tab弹出重命名输入框(沿用删除dialog的交互风格)
- 调整 onClose 入参命名为 id,语义与 store 一致
- 包裹 Tab 区域加 user-select: none,防止双击/拖动时文字被选中
- 附带:config-panel.vue 同步 Prettier 格式化(无逻辑改动)
This commit is contained in:
yangsy
2026-06-11 13:55:27 +08:00
parent 05fb7a627d
commit 71fec2e1dd
6 changed files with 177 additions and 93 deletions
-64
View File
@@ -1,64 +0,0 @@
<script setup lang="ts">
import { NTabs, NTabPane } from 'naive-ui'
import { ref } from 'vue'
const screens = [
{ id: 'screen-1', name: '屏幕 1' },
{ id: 'screen-2', name: '屏幕 2' },
{ id: 'screen-3', name: '屏幕 3' },
{ id: 'screen-4', name: '屏幕 4' },
]
const activeScreen = ref(screens[0]?.id ?? '')
const onDragover = (e: DragEvent) => {
e.preventDefault()
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
}
const onDrop = (e: DragEvent) => {
e.preventDefault()
const type = e.dataTransfer?.getData('type')
if (!type) return
if (type === 'camera' || type === 'alarm') {
const code = e.dataTransfer.getData('code')
const name = e.dataTransfer.getData('name')
window.$message?.info(`播放:${JSON.stringify({ code, name })}`)
}
}
</script>
<template>
<div :style="{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }">
<div :style="{ flexShrink: 0, padding: '0 8px' }">
<NTabs
v-model:value="activeScreen"
type="line"
size="small"
animated
>
<NTabPane
v-for="s in screens"
:key="s.id"
:name="s.id"
:tab="s.name"
/>
</NTabs>
</div>
<div
:style="{
flex: 1,
minHeight: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#f5f5f5',
backgroundImage: 'linear-gradient(45deg, #e5e5e5 25%, transparent 25%), linear-gradient(-45deg, #e5e5e5 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #e5e5e5 75%), linear-gradient(-45deg, transparent 75%, #e5e5e5 75%)',
backgroundSize: '16px 16px',
backgroundPosition: '0 0, 0 8px, 8px -8px, -8px 0',
}"
@dragover="onDragover"
@drop="onDrop"
/>
</div>
</template>
+19 -27
View File
@@ -1,14 +1,14 @@
<script setup lang="ts">
import { NButton, NIcon, NTabPane, NTabs, NText } from 'naive-ui'
import { ChevronRightIcon, DatabaseIcon, LayoutGridIcon, SlidersHorizontalIcon, ZapIcon, type Component } from 'lucide-vue-next'
import { ref } from 'vue'
import { useConfigPanelStore } from '../stores'
import { storeToRefs } from 'pinia'
import { NButton, NIcon, NTabPane, NTabs, NText } from 'naive-ui';
import { ChevronRightIcon, DatabaseIcon, LayoutGridIcon, SlidersHorizontalIcon, ZapIcon } from 'lucide-vue-next';
import { ref, type Component } from 'vue';
import { useConfigPanelStore } from '../stores';
import { storeToRefs } from 'pinia';
interface ControlTabPane {
name: string
tab: string
icon: Component
name: string;
tab: string;
icon: Component;
}
const tabs: ControlTabPane[] = [
@@ -16,22 +16,22 @@ const tabs: ControlTabPane[] = [
{ name: 'config', tab: '属性', icon: SlidersHorizontalIcon },
{ name: 'data', tab: '数据', icon: DatabaseIcon },
{ name: 'interaction', tab: '事件', icon: ZapIcon },
]
];
const PANEL_WIDTH_EXPANDED = '320px'
const PANEL_WIDTH_COLLAPSED = '72px'
const TAB_WIDTH = '72px'
const PANEL_WIDTH_EXPANDED = '320px';
const PANEL_WIDTH_COLLAPSED = '72px';
const TAB_WIDTH = '72px';
const activeTab = ref(tabs[0]?.name ?? '')
const activeTab = ref(tabs[0]?.name ?? '');
const configPanelStore = useConfigPanelStore()
const { collapsed } = storeToRefs(configPanelStore)
const configPanelStore = useConfigPanelStore();
const { collapsed } = storeToRefs(configPanelStore);
const expandConfigPanel = () => {
if (collapsed.value) {
configPanelStore.toggleCollapsed()
configPanelStore.toggleCollapsed();
}
}
};
</script>
<template>
@@ -107,22 +107,14 @@ const expandConfigPanel = () => {
'--n-tab-gap-vertical': '0',
}"
>
<NTabPane
v-for="t in tabs"
:key="t.name"
:name="t.name"
:tab="t.tab"
:tab-props="{ onClick: () => expandConfigPanel() }"
>
<NTabPane v-for="t in tabs" :key="t.name" :name="t.name" :tab="t.tab" :tab-props="{ onClick: () => expandConfigPanel() }">
<template #tab>
<div :style="{ width: '48px', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }">
<NIcon :size="18" :component="t.icon" />
<div :style="{ fontSize: '12px' }">{{ t.tab }}</div>
</div>
</template>
<div :style="{ padding: '20px', textAlign: 'center', fontSize: '12px' }">
{{ t.tab }}面板占位
</div>
<div :style="{ padding: '20px', textAlign: 'center', fontSize: '12px' }">{{ t.tab }}面板占位</div>
</NTabPane>
</NTabs>
</div>
@@ -0,0 +1,97 @@
<script setup lang="ts">
import { NInput, NTabs, NTab } from 'naive-ui';
import { h, ref } from 'vue';
import { useScreenStore } from '../stores';
import { storeToRefs } from 'pinia';
const screenStore = useScreenStore();
const { screens, activeScreenId } = storeToRefs(screenStore);
const onAdd = () => {
screenStore.addScreen();
};
const onClose = (id: string) => {
const screen = screens.value.find(s => s.id === id)
if (!screen) return
window.$dialog.warning({
title: '删除屏幕',
content: `确认删除屏幕"${screen.name}"吗?此操作无法撤销。`,
positiveText: '删除',
negativeText: '取消',
onPositiveClick: () => {
screenStore.removeScreen(id)
},
})
};
const renameValue = ref('');
const onTabDblclick = (id: string) => {
const screen = screens.value.find(s => s.id === id)
if (!screen) return
renameValue.value = screen.name
window.$dialog.info({
title: '重命名屏幕',
positiveText: '确认',
negativeText: '取消',
content: () =>
h(NInput, {
value: renameValue.value,
'onUpdate:value': (v: string) => {
renameValue.value = v
},
placeholder: '请输入屏幕名称',
autofocus: true,
onKeyup: (e: KeyboardEvent) => {
if (e.key === 'Enter') {
handleRenameConfirm(id)
}
},
}),
onPositiveClick: () => {
handleRenameConfirm(id)
},
})
};
const handleRenameConfirm = (id: string) => {
screenStore.renameScreen(id, renameValue.value)
};
const onDragover = (e: DragEvent) => {
e.preventDefault()
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
}
const onDrop = (e: DragEvent) => {
e.preventDefault()
// TODO: 放置占位组件(等 grid-layout-plus 接入后实现)
}
</script>
<template>
<div :style="{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }">
<div :style="{ flexShrink: 0, padding: '0 8px', userSelect: 'none' }">
<NTabs v-model:value="activeScreenId" type="card" size="small" animated :addable="true" @add="onAdd" @close="onClose">
<NTab v-for="s in screens" :key="s.id" :name="s.id" :tab="s.name" closable @dblclick="onTabDblclick(s.id)" />
</NTabs>
</div>
<div
:style="{
flex: 1,
minHeight: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#f5f5f5',
backgroundImage:
'linear-gradient(45deg, #e5e5e5 25%, transparent 25%), linear-gradient(-45deg, #e5e5e5 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #e5e5e5 75%), linear-gradient(-45deg, transparent 75%, #e5e5e5 75%)',
backgroundSize: '16px 16px',
backgroundPosition: '0 0, 0 8px, 8px -8px, -8px 0',
}"
@dragover="onDragover"
@drop="onDrop"
/>
</div>
</template>
+1
View File
@@ -2,3 +2,4 @@ export * from './alarm';
export * from './camera';
export * from './config-panel';
export * from './resource-panel';
export * from './screen';
+58
View File
@@ -0,0 +1,58 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export interface Screen {
id: string
name: string
}
let counter = 4
const genId = (): string => `screen-${crypto.randomUUID()}`
const defaultName = (n: number): string => `屏幕 ${n}`
export const useScreenStore = defineStore('vimp-screen', () => {
const screens = ref<Screen[]>([
{ id: 'screen-1', name: '屏幕 1' },
{ id: 'screen-2', name: '屏幕 2' },
{ id: 'screen-3', name: '屏幕 3' },
{ id: 'screen-4', name: '屏幕 4' },
])
const activeScreenId = ref<string>(screens.value[0]?.id ?? '')
const addScreen = () => {
const id = genId()
screens.value.push({ id, name: defaultName(screens.value.length + 1) })
activeScreenId.value = id
}
const removeScreen = (id: string) => {
if (screens.value.length <= 1) return
const index = screens.value.findIndex(s => s.id === id)
if (index === -1) return
screens.value.splice(index, 1)
if (activeScreenId.value === id) {
const fallback = screens.value[index] ?? screens.value[index - 1]
if (fallback) activeScreenId.value = fallback.id
}
}
const renameScreen = (id: string, name: string) => {
const target = screens.value.find(s => s.id === id)
if (!target) return
const trimmed = name.trim()
if (trimmed.length === 0) return
if (trimmed === target.name) return
target.name = trimmed
}
return {
screens,
activeScreenId,
addScreen,
removeScreen,
renameScreen,
}
})
+2 -2
View File
@@ -1,13 +1,13 @@
<script setup lang="ts">
import ResourcePanel from './components/resource-panel.vue'
import ConfigPanel from './components/config-panel.vue'
import CanvasArea from './components/canvas-area.vue'
import ScreenPanel from './components/screen-panel.vue'
</script>
<template>
<div style="height: 100%; display: flex; overflow: hidden">
<ResourcePanel />
<CanvasArea />
<ScreenPanel />
<ConfigPanel />
</div>
</template>