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:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -2,3 +2,4 @@ export * from './alarm';
|
||||
export * from './camera';
|
||||
export * from './config-panel';
|
||||
export * from './resource-panel';
|
||||
export * from './screen';
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user