2
This commit is contained in:
@@ -16,7 +16,7 @@
|
|||||||
"materialId": "DesignComponentList"
|
"materialId": "DesignComponentList"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"activeTabId": "6hfm9ux"
|
"activeTabId": "up60643"
|
||||||
},
|
},
|
||||||
"centerPanel": {
|
"centerPanel": {
|
||||||
"id": "center",
|
"id": "center",
|
||||||
@@ -26,12 +26,6 @@
|
|||||||
"title": "设计中心",
|
"title": "设计中心",
|
||||||
"content": "新窗口内容",
|
"content": "新窗口内容",
|
||||||
"materialId": "DesignCenter"
|
"materialId": "DesignCenter"
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "rdp9iuv",
|
|
||||||
"title": "测试组件A",
|
|
||||||
"content": "新窗口内容",
|
|
||||||
"materialId": "TestWidget1"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"activeTabId": "j70ckww"
|
"activeTabId": "j70ckww"
|
||||||
@@ -147,5 +141,5 @@
|
|||||||
"activeTabId": "mxfx11j"
|
"activeTabId": "mxfx11j"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lastUpdated": "2025-12-21T12:24:13.100Z"
|
"lastUpdated": "2025-12-21T13:24:40.794Z"
|
||||||
}
|
}
|
||||||
@@ -52,8 +52,20 @@
|
|||||||
"列3"
|
"列3"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "jy87mdv",
|
||||||
|
"componentId": "RadioSelect",
|
||||||
|
"name": "单选器 2",
|
||||||
|
"props": {
|
||||||
|
"options": [
|
||||||
|
"选项1",
|
||||||
|
"选项2",
|
||||||
|
"选项3"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"selectedId": "xazr6j9",
|
"selectedId": "jy87mdv",
|
||||||
"lastUpdated": "2025-12-21T12:22:52.464Z"
|
"lastUpdated": "2025-12-21T13:23:32.873Z"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useInteractionStore } from '../plugins'
|
||||||
|
|
||||||
|
const interactionStore = useInteractionStore()
|
||||||
|
|
||||||
const currentTime = ref('')
|
const currentTime = ref('')
|
||||||
let timer: number | null = null
|
let timer: number | null = null
|
||||||
@@ -17,6 +20,35 @@ const updateTime = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 交互状态显示文本
|
||||||
|
const interactionText = computed(() => {
|
||||||
|
return interactionStore.formatInteraction(interactionStore.lastInteraction)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前悬停的元素信息
|
||||||
|
const hoverInfo = computed(() => {
|
||||||
|
const target = interactionStore.hoverTarget
|
||||||
|
if (!target) return null
|
||||||
|
|
||||||
|
const typeName = interactionStore.getTypeName(target.type)
|
||||||
|
if (target.type === 'dc') {
|
||||||
|
return `悬停: ${typeName} [${target.componentId}]`
|
||||||
|
}
|
||||||
|
return `悬停: ${typeName} [${target.path}]`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前选中的元素信息
|
||||||
|
const selectedInfo = computed(() => {
|
||||||
|
const target = interactionStore.selectedTarget
|
||||||
|
if (!target) return null
|
||||||
|
|
||||||
|
const typeName = interactionStore.getTypeName(target.type)
|
||||||
|
if (target.type === 'dc') {
|
||||||
|
return `选中: ${typeName} [${target.componentId}]`
|
||||||
|
}
|
||||||
|
return `选中: ${typeName} [${target.path}]`
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
updateTime()
|
updateTime()
|
||||||
timer = window.setInterval(updateTime, 1000)
|
timer = window.setInterval(updateTime, 1000)
|
||||||
@@ -31,7 +63,22 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<footer class="app-footer">
|
<footer class="app-footer">
|
||||||
<div class="footer-left"></div>
|
<div class="footer-left">
|
||||||
|
<span v-if="hoverInfo" class="info-item hover-info">
|
||||||
|
👁️ {{ hoverInfo }}
|
||||||
|
</span>
|
||||||
|
<span v-if="selectedInfo" class="info-item selected-info">
|
||||||
|
✅ {{ selectedInfo }}
|
||||||
|
</span>
|
||||||
|
<span v-if="!hoverInfo && !selectedInfo" class="info-item idle-info">
|
||||||
|
🎯 就绪 | 悬停或点击元素查看信息
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="footer-center">
|
||||||
|
<span v-if="interactionStore.lastInteraction" class="last-action">
|
||||||
|
最近: {{ interactionText }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="footer-right">
|
<div class="footer-right">
|
||||||
<span class="time">{{ currentTime }}</span>
|
<span class="time">{{ currentTime }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,10 +94,19 @@ onUnmounted(() => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-left {
|
.footer-left {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-right {
|
.footer-right {
|
||||||
@@ -58,6 +114,34 @@ onUnmounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-info {
|
||||||
|
color: #ffffc0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-info {
|
||||||
|
color: #90ee90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.idle-info {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-action {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 11px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
|
import { useInteractionStore, generateElementPath } from '../../plugins'
|
||||||
|
import type { ElementType, InteractionTarget } from '../../plugins'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
component: any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const interactionStore = useInteractionStore()
|
||||||
|
const containerRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
// 存储所有绑定的事件清理函数
|
||||||
|
const cleanupFunctions: (() => void)[] = []
|
||||||
|
|
||||||
|
// MutationObserver 用于监听DOM变化
|
||||||
|
let observer: MutationObserver | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为元素绑定交互事件
|
||||||
|
*/
|
||||||
|
const bindElementEvents = (element: HTMLElement, type: ElementType, path: string) => {
|
||||||
|
// 跳过已经绑定过的元素
|
||||||
|
if (element.hasAttribute('data-fauto-bindend')) return null
|
||||||
|
|
||||||
|
const target: InteractionTarget = {
|
||||||
|
type,
|
||||||
|
path,
|
||||||
|
element
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置数据属性
|
||||||
|
element.setAttribute('data-path', path)
|
||||||
|
element.setAttribute('data-type', type)
|
||||||
|
element.setAttribute('data-fauto-bindend', 'true')
|
||||||
|
|
||||||
|
// 添加可交互样式类
|
||||||
|
element.classList.add('fauto-interactive')
|
||||||
|
|
||||||
|
// 鼠标悬停
|
||||||
|
const handleMouseEnter = (e: MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
interactionStore.onHover(target)
|
||||||
|
element.classList.add('fauto-hover')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标离开
|
||||||
|
const handleMouseLeave = (e: MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
interactionStore.onLeave()
|
||||||
|
element.classList.remove('fauto-hover')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标点击
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
interactionStore.onClick(target)
|
||||||
|
|
||||||
|
// 移除其他元素的选中状态
|
||||||
|
document.querySelectorAll('.fauto-selected').forEach(el => {
|
||||||
|
el.classList.remove('fauto-selected')
|
||||||
|
})
|
||||||
|
element.classList.add('fauto-selected')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标按下(长按检测)
|
||||||
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
interactionStore.onMouseDown(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标移动(拖拽检测)
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (interactionStore.isLongPressing) {
|
||||||
|
e.stopPropagation()
|
||||||
|
interactionStore.onMouseMove(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标松开
|
||||||
|
const handleMouseUp = (e: MouseEvent) => {
|
||||||
|
interactionStore.onMouseUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
element.addEventListener('mouseenter', handleMouseEnter)
|
||||||
|
element.addEventListener('mouseleave', handleMouseLeave)
|
||||||
|
element.addEventListener('click', handleClick)
|
||||||
|
element.addEventListener('mousedown', handleMouseDown)
|
||||||
|
element.addEventListener('mousemove', handleMouseMove)
|
||||||
|
element.addEventListener('mouseup', handleMouseUp)
|
||||||
|
|
||||||
|
console.log(`[注入] ${type} 路径: ${path}`)
|
||||||
|
|
||||||
|
// 返回清理函数
|
||||||
|
return () => {
|
||||||
|
element.removeEventListener('mouseenter', handleMouseEnter)
|
||||||
|
element.removeEventListener('mouseleave', handleMouseLeave)
|
||||||
|
element.removeEventListener('click', handleClick)
|
||||||
|
element.removeEventListener('mousedown', handleMouseDown)
|
||||||
|
element.removeEventListener('mousemove', handleMouseMove)
|
||||||
|
element.removeEventListener('mouseup', handleMouseUp)
|
||||||
|
element.classList.remove('fauto-interactive', 'fauto-hover', 'fauto-selected')
|
||||||
|
element.removeAttribute('data-fauto-bindend')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描并注入事件到所有el-row和el-col元素
|
||||||
|
*/
|
||||||
|
const injectInteractionEvents = () => {
|
||||||
|
if (!containerRef.value) return
|
||||||
|
|
||||||
|
// 查找所有el-row和el-col元素
|
||||||
|
const rows = containerRef.value.querySelectorAll('.el-row')
|
||||||
|
const cols = containerRef.value.querySelectorAll('.el-col')
|
||||||
|
|
||||||
|
console.log(`[InteractiveWrapper] 发现 ${rows.length} 个 el-row, ${cols.length} 个 el-col`)
|
||||||
|
|
||||||
|
// 为el-row绑定事件
|
||||||
|
rows.forEach((row) => {
|
||||||
|
const path = generateElementPath(row)
|
||||||
|
const cleanup = bindElementEvents(row as HTMLElement, 'er', path)
|
||||||
|
if (cleanup) cleanupFunctions.push(cleanup)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 为el-col绑定事件
|
||||||
|
cols.forEach((col) => {
|
||||||
|
const path = generateElementPath(col)
|
||||||
|
const cleanup = bindElementEvents(col as HTMLElement, 'ec', path)
|
||||||
|
if (cleanup) cleanupFunctions.push(cleanup)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动DOM观察器,监听子元素变化
|
||||||
|
*/
|
||||||
|
const startObserver = () => {
|
||||||
|
if (!containerRef.value) return
|
||||||
|
|
||||||
|
observer = new MutationObserver((mutations) => {
|
||||||
|
// 检查是否有新的el-row或el-col添加
|
||||||
|
let hasNewElements = false
|
||||||
|
mutations.forEach(mutation => {
|
||||||
|
mutation.addedNodes.forEach(node => {
|
||||||
|
if (node instanceof HTMLElement) {
|
||||||
|
if (node.classList.contains('el-row') || node.classList.contains('el-col')) {
|
||||||
|
hasNewElements = true
|
||||||
|
}
|
||||||
|
// 检查子元素
|
||||||
|
if (node.querySelector('.el-row, .el-col')) {
|
||||||
|
hasNewElements = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hasNewElements) {
|
||||||
|
console.log('[MutationObserver] 检测到新元素,重新注入事件')
|
||||||
|
injectInteractionEvents()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(containerRef.value, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载后注入事件
|
||||||
|
onMounted(() => {
|
||||||
|
// 等待异步组件加载完成,使用多次nextTick和延时
|
||||||
|
nextTick(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
injectInteractionEvents()
|
||||||
|
startObserver()
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件卸载时清理事件
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanupFunctions.forEach(fn => fn())
|
||||||
|
cleanupFunctions.length = 0
|
||||||
|
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect()
|
||||||
|
observer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听组件变化,重新注入事件
|
||||||
|
watch(() => props.component, () => {
|
||||||
|
// 清理之前的绑定
|
||||||
|
cleanupFunctions.forEach(fn => fn())
|
||||||
|
cleanupFunctions.length = 0
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
injectInteractionEvents()
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="containerRef" class="interactive-wrapper">
|
||||||
|
<component :is="component" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 全局样式,用于交互元素 */
|
||||||
|
.interactive-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fauto-interactive {
|
||||||
|
position: relative;
|
||||||
|
transition: outline 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fauto-interactive.fauto-hover {
|
||||||
|
outline: 2px dashed #409eff;
|
||||||
|
outline-offset: -2px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fauto-interactive.fauto-selected {
|
||||||
|
outline: 2px solid #67c23a;
|
||||||
|
outline-offset: -2px;
|
||||||
|
z-index: 20;
|
||||||
|
box-shadow: 0 0 8px rgba(103, 194, 58, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* el-row 悬停时的特殊样式 */
|
||||||
|
.el-row.fauto-hover {
|
||||||
|
background-color: rgba(64, 158, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-row.fauto-selected {
|
||||||
|
background-color: rgba(103, 194, 58, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* el-col 悬停时的特殊样式 */
|
||||||
|
.el-col.fauto-hover {
|
||||||
|
background-color: rgba(64, 158, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-col.fauto-selected {
|
||||||
|
background-color: rgba(103, 194, 58, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { defineAsyncComponent, markRaw, computed, watch } from 'vue'
|
import { defineAsyncComponent, markRaw, computed, watch } from 'vue'
|
||||||
import { useDesignStore } from '../../stores/designStore'
|
import { useDesignStore } from '../../stores/designStore'
|
||||||
import { useVueFileStore } from '../../stores/vueFileStore'
|
import { useVueFileStore } from '../../stores/vueFileStore'
|
||||||
|
import InteractiveWrapper from './InteractiveWrapper.vue'
|
||||||
import config from './index.json'
|
import config from './index.json'
|
||||||
|
|
||||||
const designStore = useDesignStore()
|
const designStore = useDesignStore()
|
||||||
@@ -72,9 +73,9 @@ watch(() => vueFileStore.selectedFilePath, (newPath) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="center-body">
|
<div class="center-body">
|
||||||
<!-- 动态渲染选中的Vue页面 -->
|
<!-- 动态渲染选中的Vue页面(使用InteractiveWrapper注入交互事件) -->
|
||||||
<div v-if="selectedPageComponent" class="page-preview">
|
<div v-if="selectedPageComponent" class="page-preview">
|
||||||
<component :is="selectedPageComponent" />
|
<InteractiveWrapper :component="selectedPageComponent" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 原有的设计组件实例列表 -->
|
<!-- 原有的设计组件实例列表 -->
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { useDesignStore } from '../../stores/designStore'
|
import { useDesignStore } from '../../stores/designStore'
|
||||||
|
import { useInteractionStore } from '../../plugins'
|
||||||
|
import type { InteractionTarget } from '../../plugins'
|
||||||
import config from './index.json'
|
import config from './index.json'
|
||||||
|
|
||||||
const designStore = useDesignStore()
|
const designStore = useDesignStore()
|
||||||
|
const interactionStore = useInteractionStore()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 确保设计组件元数据已加载
|
// 确保设计组件元数据已加载
|
||||||
@@ -15,6 +18,33 @@ onMounted(() => {
|
|||||||
const handleAddComponent = (componentId: string) => {
|
const handleAddComponent = (componentId: string) => {
|
||||||
designStore.addComponent(componentId)
|
designStore.addComponent(componentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 鼠标悬停设计组件
|
||||||
|
const handleMouseEnter = (componentId: string, componentName: string) => {
|
||||||
|
const target: InteractionTarget = {
|
||||||
|
type: 'dc',
|
||||||
|
path: componentId,
|
||||||
|
componentId: componentName
|
||||||
|
}
|
||||||
|
interactionStore.onHover(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标离开设计组件
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
interactionStore.onLeave()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标点击设计组件
|
||||||
|
const handleClick = (componentId: string, componentName: string) => {
|
||||||
|
const target: InteractionTarget = {
|
||||||
|
type: 'dc',
|
||||||
|
path: componentId,
|
||||||
|
componentId: componentName
|
||||||
|
}
|
||||||
|
interactionStore.onClick(target)
|
||||||
|
// 添加组件
|
||||||
|
designStore.addComponent(componentId)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -28,7 +58,9 @@ const handleAddComponent = (componentId: string) => {
|
|||||||
v-for="meta in designStore.componentMetas"
|
v-for="meta in designStore.componentMetas"
|
||||||
:key="meta.id"
|
:key="meta.id"
|
||||||
class="component-item"
|
class="component-item"
|
||||||
@click="handleAddComponent(meta.id)"
|
@click="handleClick(meta.id, meta.name)"
|
||||||
|
@mouseenter="handleMouseEnter(meta.id, meta.name)"
|
||||||
|
@mouseleave="handleMouseLeave"
|
||||||
>
|
>
|
||||||
<div class="component-icon">📦</div>
|
<div class="component-icon">📦</div>
|
||||||
<div class="component-info">
|
<div class="component-info">
|
||||||
|
|||||||
27
draggable-panels/src/fauto/plugins/index.ts
Normal file
27
draggable-panels/src/fauto/plugins/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Fauto 插件系统入口
|
||||||
|
*
|
||||||
|
* 提供全局交互钩子和工具函数
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 导出交互Store
|
||||||
|
export { useInteractionStore } from './interactionStore'
|
||||||
|
export type {
|
||||||
|
ElementType,
|
||||||
|
InteractionEvent,
|
||||||
|
InteractionTarget,
|
||||||
|
InteractionState,
|
||||||
|
HookCallback
|
||||||
|
} from './interactionStore'
|
||||||
|
|
||||||
|
// 导出路径工具
|
||||||
|
export {
|
||||||
|
parsePath,
|
||||||
|
buildPath,
|
||||||
|
calculateSiblingIndex,
|
||||||
|
generateElementPath,
|
||||||
|
getParentPath,
|
||||||
|
getPathDepth,
|
||||||
|
isAncestorPath
|
||||||
|
} from './pathUtils'
|
||||||
|
export type { PathNode } from './pathUtils'
|
||||||
257
draggable-panels/src/fauto/plugins/interactionStore.ts
Normal file
257
draggable-panels/src/fauto/plugins/interactionStore.ts
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, shallowRef } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交互元素类型
|
||||||
|
* - er: el-row
|
||||||
|
* - ec: el-col
|
||||||
|
* - dc: design-component (设计组件)
|
||||||
|
*/
|
||||||
|
export type ElementType = 'er' | 'ec' | 'dc'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交互事件类型
|
||||||
|
*/
|
||||||
|
export type InteractionEvent =
|
||||||
|
| 'hover' // 鼠标悬停
|
||||||
|
| 'click' // 鼠标点击
|
||||||
|
| 'longpress' // 鼠标长按
|
||||||
|
| 'drag' // 长按并移动
|
||||||
|
| 'release' // 鼠标松开
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交互目标信息
|
||||||
|
*/
|
||||||
|
export interface InteractionTarget {
|
||||||
|
type: ElementType // 元素类型
|
||||||
|
path: string // 结构化路径ID (如 r1c2r1c1)
|
||||||
|
componentId?: string // 设计组件ID (仅dc类型)
|
||||||
|
element?: HTMLElement // DOM元素引用
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交互状态信息
|
||||||
|
*/
|
||||||
|
export interface InteractionState {
|
||||||
|
event: InteractionEvent // 当前事件类型
|
||||||
|
target: InteractionTarget // 目标元素信息
|
||||||
|
timestamp: number // 时间戳
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 钩子函数回调类型
|
||||||
|
*/
|
||||||
|
export type HookCallback = (state: InteractionState) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局交互钩子插件
|
||||||
|
* 管理所有的鼠标交互事件和状态
|
||||||
|
*/
|
||||||
|
export const useInteractionStore = defineStore('interaction', () => {
|
||||||
|
// 当前悬停的元素
|
||||||
|
const hoverTarget = ref<InteractionTarget | null>(null)
|
||||||
|
|
||||||
|
// 当前点击/选中的元素
|
||||||
|
const selectedTarget = ref<InteractionTarget | null>(null)
|
||||||
|
|
||||||
|
// 当前拖拽的元素
|
||||||
|
const dragTarget = ref<InteractionTarget | null>(null)
|
||||||
|
|
||||||
|
// 拖拽放置的目标元素
|
||||||
|
const dropTarget = ref<InteractionTarget | null>(null)
|
||||||
|
|
||||||
|
// 最后一次交互状态(用于Footer显示)
|
||||||
|
const lastInteraction = ref<InteractionState | null>(null)
|
||||||
|
|
||||||
|
// 是否正在长按
|
||||||
|
const isLongPressing = ref(false)
|
||||||
|
|
||||||
|
// 是否正在拖拽
|
||||||
|
const isDragging = ref(false)
|
||||||
|
|
||||||
|
// 长按计时器
|
||||||
|
let longPressTimer: number | null = null
|
||||||
|
|
||||||
|
// 钩子函数注册表
|
||||||
|
const hooks = shallowRef<Map<InteractionEvent, Set<HookCallback>>>(new Map())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册钩子函数
|
||||||
|
*/
|
||||||
|
const registerHook = (event: InteractionEvent, callback: HookCallback) => {
|
||||||
|
if (!hooks.value.has(event)) {
|
||||||
|
hooks.value.set(event, new Set())
|
||||||
|
}
|
||||||
|
hooks.value.get(event)!.add(callback)
|
||||||
|
|
||||||
|
// 返回取消注册函数
|
||||||
|
return () => {
|
||||||
|
hooks.value.get(event)?.delete(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发钩子函数
|
||||||
|
*/
|
||||||
|
const triggerHooks = (event: InteractionEvent, target: InteractionTarget) => {
|
||||||
|
const state: InteractionState = {
|
||||||
|
event,
|
||||||
|
target,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最后交互状态
|
||||||
|
lastInteraction.value = state
|
||||||
|
|
||||||
|
// 触发所有注册的回调
|
||||||
|
const callbacks = hooks.value.get(event)
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.forEach(cb => cb(state))
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Interaction] ${event}:`, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标悬停事件
|
||||||
|
*/
|
||||||
|
const onHover = (target: InteractionTarget) => {
|
||||||
|
hoverTarget.value = target
|
||||||
|
triggerHooks('hover', target)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标离开事件
|
||||||
|
*/
|
||||||
|
const onLeave = () => {
|
||||||
|
hoverTarget.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标点击事件
|
||||||
|
*/
|
||||||
|
const onClick = (target: InteractionTarget) => {
|
||||||
|
selectedTarget.value = target
|
||||||
|
triggerHooks('click', target)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标按下事件(开始检测长按)
|
||||||
|
*/
|
||||||
|
const onMouseDown = (target: InteractionTarget) => {
|
||||||
|
// 清除之前的计时器
|
||||||
|
if (longPressTimer) {
|
||||||
|
clearTimeout(longPressTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置长按检测(500ms后触发)
|
||||||
|
longPressTimer = window.setTimeout(() => {
|
||||||
|
isLongPressing.value = true
|
||||||
|
dragTarget.value = target
|
||||||
|
triggerHooks('longpress', target)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标移动事件(长按状态下触发拖拽)
|
||||||
|
*/
|
||||||
|
const onMouseMove = (target: InteractionTarget) => {
|
||||||
|
if (isLongPressing.value && dragTarget.value) {
|
||||||
|
if (!isDragging.value) {
|
||||||
|
isDragging.value = true
|
||||||
|
}
|
||||||
|
dropTarget.value = target
|
||||||
|
triggerHooks('drag', target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标松开事件
|
||||||
|
*/
|
||||||
|
const onMouseUp = () => {
|
||||||
|
// 清除长按计时器
|
||||||
|
if (longPressTimer) {
|
||||||
|
clearTimeout(longPressTimer)
|
||||||
|
longPressTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果正在拖拽,触发释放事件
|
||||||
|
if (isDragging.value && dragTarget.value && dropTarget.value) {
|
||||||
|
triggerHooks('release', dropTarget.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
isLongPressing.value = false
|
||||||
|
isDragging.value = false
|
||||||
|
dragTarget.value = null
|
||||||
|
dropTarget.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元素类型的中文名称
|
||||||
|
*/
|
||||||
|
const getTypeName = (type: ElementType): string => {
|
||||||
|
const names: Record<ElementType, string> = {
|
||||||
|
'er': 'el-row',
|
||||||
|
'ec': 'el-col',
|
||||||
|
'dc': '设计组件'
|
||||||
|
}
|
||||||
|
return names[type]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取事件类型的中文名称
|
||||||
|
*/
|
||||||
|
const getEventName = (event: InteractionEvent): string => {
|
||||||
|
const names: Record<InteractionEvent, string> = {
|
||||||
|
'hover': '悬停',
|
||||||
|
'click': '点击',
|
||||||
|
'longpress': '长按',
|
||||||
|
'drag': '拖拽',
|
||||||
|
'release': '释放'
|
||||||
|
}
|
||||||
|
return names[event]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化交互状态为显示文本
|
||||||
|
*/
|
||||||
|
const formatInteraction = (state: InteractionState | null): string => {
|
||||||
|
if (!state) return '无交互'
|
||||||
|
|
||||||
|
const eventName = getEventName(state.event)
|
||||||
|
const typeName = getTypeName(state.target.type)
|
||||||
|
const path = state.target.path
|
||||||
|
const componentId = state.target.componentId
|
||||||
|
|
||||||
|
if (state.target.type === 'dc') {
|
||||||
|
return `${eventName} → ${typeName} [${componentId}]`
|
||||||
|
}
|
||||||
|
return `${eventName} → ${typeName} [${path}]`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
hoverTarget,
|
||||||
|
selectedTarget,
|
||||||
|
dragTarget,
|
||||||
|
dropTarget,
|
||||||
|
lastInteraction,
|
||||||
|
isLongPressing,
|
||||||
|
isDragging,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
registerHook,
|
||||||
|
onHover,
|
||||||
|
onLeave,
|
||||||
|
onClick,
|
||||||
|
onMouseDown,
|
||||||
|
onMouseMove,
|
||||||
|
onMouseUp,
|
||||||
|
|
||||||
|
// 辅助方法
|
||||||
|
getTypeName,
|
||||||
|
getEventName,
|
||||||
|
formatInteraction
|
||||||
|
}
|
||||||
|
})
|
||||||
125
draggable-panels/src/fauto/plugins/pathUtils.ts
Normal file
125
draggable-panels/src/fauto/plugins/pathUtils.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* 结构化路径ID生成器
|
||||||
|
* 用于为el-row和el-col生成唯一的结构化路径
|
||||||
|
*
|
||||||
|
* 路径格式: r{n}c{m}r{x}c{y}...
|
||||||
|
* - r: el-row
|
||||||
|
* - c: el-col
|
||||||
|
* - 数字: 在同级元素中的索引(从1开始)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ElementType } from './interactionStore'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径节点信息
|
||||||
|
*/
|
||||||
|
export interface PathNode {
|
||||||
|
type: ElementType
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析结构化路径字符串
|
||||||
|
* @param path 路径字符串,如 "r1c2r1c1"
|
||||||
|
* @returns 路径节点数组
|
||||||
|
*/
|
||||||
|
export const parsePath = (path: string): PathNode[] => {
|
||||||
|
const nodes: PathNode[] = []
|
||||||
|
const regex = /(r|c)(\d+)/g
|
||||||
|
let match
|
||||||
|
|
||||||
|
while ((match = regex.exec(path)) !== null) {
|
||||||
|
nodes.push({
|
||||||
|
type: match[1] === 'r' ? 'er' : 'ec',
|
||||||
|
index: parseInt(match[2])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成结构化路径字符串
|
||||||
|
* @param nodes 路径节点数组
|
||||||
|
* @returns 路径字符串
|
||||||
|
*/
|
||||||
|
export const buildPath = (nodes: PathNode[]): string => {
|
||||||
|
return nodes.map(node => {
|
||||||
|
const prefix = node.type === 'er' ? 'r' : 'c'
|
||||||
|
return `${prefix}${node.index}`
|
||||||
|
}).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算元素在其父级中的索引
|
||||||
|
* @param element 目标元素
|
||||||
|
* @param className 要匹配的class名(el-row 或 el-col)
|
||||||
|
* @returns 索引(从1开始)
|
||||||
|
*/
|
||||||
|
export const calculateSiblingIndex = (element: Element, className: string): number => {
|
||||||
|
const parent = element.parentElement
|
||||||
|
if (!parent) return 1
|
||||||
|
|
||||||
|
// 通过class名称筛选同级元素(Element Plus组件渲染后是div带class)
|
||||||
|
const siblings = Array.from(parent.children).filter(
|
||||||
|
child => child.classList.contains(className)
|
||||||
|
)
|
||||||
|
|
||||||
|
const index = siblings.indexOf(element)
|
||||||
|
return index >= 0 ? index + 1 : 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归生成元素的完整结构化路径
|
||||||
|
* @param element 目标元素
|
||||||
|
* @returns 结构化路径字符串
|
||||||
|
*/
|
||||||
|
export const generateElementPath = (element: Element): string => {
|
||||||
|
const pathNodes: PathNode[] = []
|
||||||
|
let current: Element | null = element
|
||||||
|
|
||||||
|
while (current) {
|
||||||
|
// Element Plus的el-row/el-col渲染后是div标签,通过class来识别
|
||||||
|
if (current.classList.contains('el-row')) {
|
||||||
|
const index = calculateSiblingIndex(current, 'el-row')
|
||||||
|
pathNodes.unshift({ type: 'er', index })
|
||||||
|
} else if (current.classList.contains('el-col')) {
|
||||||
|
const index = calculateSiblingIndex(current, 'el-col')
|
||||||
|
pathNodes.unshift({ type: 'ec', index })
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current.parentElement
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildPath(pathNodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取路径的父路径
|
||||||
|
* @param path 当前路径
|
||||||
|
* @returns 父路径,如果已是根则返回空字符串
|
||||||
|
*/
|
||||||
|
export const getParentPath = (path: string): string => {
|
||||||
|
const nodes = parsePath(path)
|
||||||
|
if (nodes.length <= 1) return ''
|
||||||
|
return buildPath(nodes.slice(0, -1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取路径的深度
|
||||||
|
* @param path 路径字符串
|
||||||
|
* @returns 深度(节点数量)
|
||||||
|
*/
|
||||||
|
export const getPathDepth = (path: string): number => {
|
||||||
|
return parsePath(path).length
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查路径A是否是路径B的祖先
|
||||||
|
* @param ancestorPath 祖先路径
|
||||||
|
* @param descendantPath 后代路径
|
||||||
|
* @returns 是否为祖先关系
|
||||||
|
*/
|
||||||
|
export const isAncestorPath = (ancestorPath: string, descendantPath: string): boolean => {
|
||||||
|
return descendantPath.startsWith(ancestorPath) && descendantPath.length > ancestorPath.length
|
||||||
|
}
|
||||||
@@ -1,52 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="test-page">
|
<el-row class="page-container" :gutter="20">
|
||||||
<h2>测试页面 1</h2>
|
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<div class="grid-content">左侧内容区域</div>
|
<div class="design-component">左侧内容区域</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<div class="grid-content">右侧内容区域</div>
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="24">
|
||||||
|
<div class="design-component">右侧标题</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
<el-row :gutter="10">
|
||||||
<el-row :gutter="20" style="margin-top: 20px;">
|
<el-col :span="12">
|
||||||
<el-col :span="8">
|
<div class="design-component">右侧列1</div>
|
||||||
<div class="grid-content">列 1</div>
|
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="12">
|
||||||
<div class="grid-content">列 2</div>
|
<div class="design-component">右侧列2</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
</el-row>
|
||||||
<div class="grid-content">列 3</div>
|
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
// 测试页面1 - 基础布局
|
||||||
|
|
||||||
const message = ref('这是测试页面1')
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.test-page {
|
.page-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
min-height: 100vh;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
.design-component {
|
||||||
color: #333;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-content {
|
|
||||||
background: #409eff;
|
background: #409eff;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,64 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="test-page">
|
<el-row class="page-container" :gutter="20">
|
||||||
<h2>测试页面 2 - 表单布局</h2>
|
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<div class="grid-content">表单标题区域</div>
|
<div class="design-component form-title">表单标题区域</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-row :gutter="20" style="margin-top: 20px;">
|
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<div class="grid-content">标签</div>
|
<div class="design-component">用户名</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="18">
|
<el-col :span="18">
|
||||||
<div class="grid-content">输入框</div>
|
<div class="design-component">输入框占位</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-row :gutter="20" style="margin-top: 20px;">
|
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<div class="grid-content">标签</div>
|
<div class="design-component">密码</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="18">
|
<el-col :span="18">
|
||||||
<div class="grid-content">输入框</div>
|
<div class="design-component">密码框占位</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-row :gutter="20" style="margin-top: 20px;">
|
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<div class="grid-content">提交按钮</div>
|
<div class="design-component submit-btn">提交按钮</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
// 测试页面2 - 表单布局
|
||||||
|
|
||||||
const formData = ref({
|
|
||||||
name: '',
|
|
||||||
email: ''
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.test-page {
|
.page-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
min-height: 100vh;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
.design-component {
|
||||||
color: #333;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-content {
|
|
||||||
background: #67c23a;
|
background: #67c23a;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
background: #409eff;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
background: #e6a23c;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,72 +1,55 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="overview-page">
|
<el-row class="page-container" :gutter="20">
|
||||||
<h2>仪表板概览</h2>
|
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<div class="grid-content stat-card">
|
<div class="design-component stat-card">
|
||||||
<div class="stat-title">总用户</div>
|
<div class="stat-title">总用户</div>
|
||||||
<div class="stat-value">1,234</div>
|
<div class="stat-value">1,234</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<div class="grid-content stat-card">
|
<div class="design-component stat-card">
|
||||||
<div class="stat-title">活跃用户</div>
|
<div class="stat-title">活跃用户</div>
|
||||||
<div class="stat-value">856</div>
|
<div class="stat-value">856</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<div class="grid-content stat-card">
|
<div class="design-component stat-card">
|
||||||
<div class="stat-title">订单数</div>
|
<div class="stat-title">订单数</div>
|
||||||
<div class="stat-value">432</div>
|
<div class="stat-value">432</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<div class="grid-content stat-card">
|
<div class="design-component stat-card">
|
||||||
<div class="stat-title">收入</div>
|
<div class="stat-title">收入</div>
|
||||||
<div class="stat-value">¥12,345</div>
|
<div class="stat-value">¥12,345</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-row :gutter="20" style="margin-top: 20px;">
|
|
||||||
<el-col :span="16">
|
<el-col :span="16">
|
||||||
<div class="grid-content chart-area">图表区域</div>
|
<div class="design-component chart-area">图表区域占位</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<div class="grid-content">侧边栏信息</div>
|
<div class="design-component sidebar">侧边栏信息</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
// 仪表板概览页面
|
||||||
|
|
||||||
const stats = ref({
|
|
||||||
users: 1234,
|
|
||||||
active: 856,
|
|
||||||
orders: 432,
|
|
||||||
revenue: 12345
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.overview-page {
|
.page-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: #f0f2f5;
|
background: #f0f2f5;
|
||||||
min-height: 100vh;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
.design-component {
|
||||||
color: #333;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-content {
|
|
||||||
background: white;
|
background: white;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
@@ -91,5 +74,11 @@ h2 {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #999;
|
color: #999;
|
||||||
|
background: #e8f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
height: 300px;
|
||||||
|
background: #f0f9ff;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,64 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="profile-page">
|
<el-row class="page-container" :gutter="20">
|
||||||
<h2>用户资料</h2>
|
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<div class="grid-content avatar-section">
|
<div class="design-component avatar-section">
|
||||||
<div class="avatar">头像</div>
|
<div class="avatar">头像</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="16">
|
<el-col :span="16">
|
||||||
<div class="grid-content info-section">
|
|
||||||
<el-row :gutter="10">
|
<el-row :gutter="10">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<div class="info-item">姓名: 张三</div>
|
<div class="design-component info-item">姓名: 张三</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<div class="info-item">邮箱: zhang@example.com</div>
|
<div class="design-component info-item">邮箱: zhang@example.com</div>
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-row :gutter="10" style="margin-top: 10px;">
|
|
||||||
<el-col :span="12">
|
|
||||||
<div class="info-item">电话: 138****8888</div>
|
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<div class="info-item">部门: 技术部</div>
|
<div class="design-component info-item">电话: 138****8888</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="design-component info-item">部门: 技术部</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
// 用户资料页面
|
||||||
|
|
||||||
const userInfo = ref({
|
|
||||||
name: '张三',
|
|
||||||
email: 'zhang@example.com',
|
|
||||||
phone: '138****8888',
|
|
||||||
department: '技术部'
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.profile-page {
|
.page-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
min-height: 100vh;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
.design-component {
|
||||||
color: #333;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-content {
|
|
||||||
background: white;
|
background: white;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-section {
|
.avatar-section {
|
||||||
@@ -80,14 +62,9 @@ h2 {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-section {
|
|
||||||
height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item {
|
.info-item {
|
||||||
padding: 10px;
|
padding: 15px;
|
||||||
background: #f9f9f9;
|
background: #f9f9f9;
|
||||||
border-radius: 4px;
|
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user