This commit is contained in:
wfz
2025-12-21 21:26:20 +08:00
parent c6077ff2ad
commit 3c38f1bee9
13 changed files with 933 additions and 201 deletions

View File

@@ -16,7 +16,7 @@
"materialId": "DesignComponentList"
}
],
"activeTabId": "6hfm9ux"
"activeTabId": "up60643"
},
"centerPanel": {
"id": "center",
@@ -26,12 +26,6 @@
"title": "设计中心",
"content": "新窗口内容",
"materialId": "DesignCenter"
},
{
"id": "rdp9iuv",
"title": "测试组件A",
"content": "新窗口内容",
"materialId": "TestWidget1"
}
],
"activeTabId": "j70ckww"
@@ -147,5 +141,5 @@
"activeTabId": "mxfx11j"
}
},
"lastUpdated": "2025-12-21T12:24:13.100Z"
"lastUpdated": "2025-12-21T13:24:40.794Z"
}

View File

@@ -52,8 +52,20 @@
"列3"
]
}
},
{
"id": "jy87mdv",
"componentId": "RadioSelect",
"name": "单选器 2",
"props": {
"options": [
"选项1",
"选项2",
"选项3"
]
}
}
],
"selectedId": "xazr6j9",
"lastUpdated": "2025-12-21T12:22:52.464Z"
"selectedId": "jy87mdv",
"lastUpdated": "2025-12-21T13:23:32.873Z"
}

View File

@@ -1,5 +1,8 @@
<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('')
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(() => {
updateTime()
timer = window.setInterval(updateTime, 1000)
@@ -31,7 +63,22 @@ onUnmounted(() => {
<template>
<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">
<span class="time">{{ currentTime }}</span>
</div>
@@ -47,10 +94,19 @@ onUnmounted(() => {
justify-content: space-between;
padding: 0 12px;
flex-shrink: 0;
gap: 20px;
}
.footer-left {
flex: 1;
display: flex;
align-items: center;
gap: 16px;
}
.footer-center {
display: flex;
align-items: center;
}
.footer-right {
@@ -58,6 +114,34 @@ onUnmounted(() => {
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 {
color: white;
font-size: 12px;

View File

@@ -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>

View File

@@ -2,6 +2,7 @@
import { defineAsyncComponent, markRaw, computed, watch } from 'vue'
import { useDesignStore } from '../../stores/designStore'
import { useVueFileStore } from '../../stores/vueFileStore'
import InteractiveWrapper from './InteractiveWrapper.vue'
import config from './index.json'
const designStore = useDesignStore()
@@ -72,9 +73,9 @@ watch(() => vueFileStore.selectedFilePath, (newPath) => {
</span>
</div>
<div class="center-body">
<!-- 动态渲染选中的Vue页面 -->
<!-- 动态渲染选中的Vue页面使用InteractiveWrapper注入交互事件 -->
<div v-if="selectedPageComponent" class="page-preview">
<component :is="selectedPageComponent" />
<InteractiveWrapper :component="selectedPageComponent" />
</div>
<!-- 原有的设计组件实例列表 -->

View File

@@ -1,9 +1,12 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { useDesignStore } from '../../stores/designStore'
import { useInteractionStore } from '../../plugins'
import type { InteractionTarget } from '../../plugins'
import config from './index.json'
const designStore = useDesignStore()
const interactionStore = useInteractionStore()
onMounted(() => {
// 确保设计组件元数据已加载
@@ -15,6 +18,33 @@ onMounted(() => {
const handleAddComponent = (componentId: string) => {
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>
<template>
@@ -28,7 +58,9 @@ const handleAddComponent = (componentId: string) => {
v-for="meta in designStore.componentMetas"
:key="meta.id"
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-info">

View 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'

View 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
}
})

View 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
}

View File

@@ -1,52 +1,43 @@
<template>
<div class="test-page">
<h2>测试页面 1</h2>
<el-row :gutter="20">
<el-row class="page-container" :gutter="20">
<el-col :span="12">
<div class="grid-content">左侧内容区域</div>
<div class="design-component">左侧内容区域</div>
</el-col>
<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-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="8">
<div class="grid-content"> 1</div>
<el-row :gutter="10">
<el-col :span="12">
<div class="design-component">右侧列1</div>
</el-col>
<el-col :span="8">
<div class="grid-content"> 2</div>
</el-col>
<el-col :span="8">
<div class="grid-content"> 3</div>
<el-col :span="12">
<div class="design-component">右侧列2</div>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const message = ref('这是测试页面1')
// 测试页面1 - 基础布局
</script>
<style scoped>
.test-page {
.page-container {
padding: 20px;
background: #f5f5f5;
min-height: 100vh;
min-height: 100%;
}
h2 {
color: #333;
margin-bottom: 20px;
}
.grid-content {
.design-component {
background: #409eff;
color: white;
padding: 30px;
text-align: center;
border-radius: 4px;
margin-bottom: 10px;
}
</style>

View File

@@ -1,64 +1,53 @@
<template>
<div class="test-page">
<h2>测试页面 2 - 表单布局</h2>
<el-row :gutter="20">
<el-row class="page-container" :gutter="20">
<el-col :span="24">
<div class="grid-content">表单标题区域</div>
<div class="design-component form-title">表单标题区域</div>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="6">
<div class="grid-content">标签</div>
<div class="design-component">用户名</div>
</el-col>
<el-col :span="18">
<div class="grid-content">输入框</div>
<div class="design-component">输入框占位</div>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="6">
<div class="grid-content">标签</div>
<div class="design-component">密码</div>
</el-col>
<el-col :span="18">
<div class="grid-content">输入框</div>
<div class="design-component">密码框占位</div>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="24">
<div class="grid-content">提交按钮</div>
<div class="design-component submit-btn">提交按钮</div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formData = ref({
name: '',
email: ''
})
// 测试页面2 - 表单布局
</script>
<style scoped>
.test-page {
.page-container {
padding: 20px;
background: #fafafa;
min-height: 100vh;
min-height: 100%;
}
h2 {
color: #333;
margin-bottom: 20px;
}
.grid-content {
.design-component {
background: #67c23a;
color: white;
padding: 20px;
text-align: center;
border-radius: 4px;
margin-bottom: 10px;
}
.form-title {
background: #409eff;
font-size: 18px;
font-weight: bold;
}
.submit-btn {
background: #e6a23c;
}
</style>

View File

@@ -1,72 +1,55 @@
<template>
<div class="overview-page">
<h2>仪表板概览</h2>
<el-row :gutter="20">
<el-row class="page-container" :gutter="20">
<el-col :span="6">
<div class="grid-content stat-card">
<div class="design-component stat-card">
<div class="stat-title">总用户</div>
<div class="stat-value">1,234</div>
</div>
</el-col>
<el-col :span="6">
<div class="grid-content stat-card">
<div class="design-component stat-card">
<div class="stat-title">活跃用户</div>
<div class="stat-value">856</div>
</div>
</el-col>
<el-col :span="6">
<div class="grid-content stat-card">
<div class="design-component stat-card">
<div class="stat-title">订单数</div>
<div class="stat-value">432</div>
</div>
</el-col>
<el-col :span="6">
<div class="grid-content stat-card">
<div class="design-component stat-card">
<div class="stat-title">收入</div>
<div class="stat-value">¥12,345</div>
</div>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="16">
<div class="grid-content chart-area">图表区域</div>
<div class="design-component chart-area">图表区域占位</div>
</el-col>
<el-col :span="8">
<div class="grid-content">侧边栏信息</div>
<div class="design-component sidebar">侧边栏信息</div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const stats = ref({
users: 1234,
active: 856,
orders: 432,
revenue: 12345
})
// 仪表板概览页面
</script>
<style scoped>
.overview-page {
.page-container {
padding: 20px;
background: #f0f2f5;
min-height: 100vh;
min-height: 100%;
}
h2 {
color: #333;
margin-bottom: 20px;
}
.grid-content {
.design-component {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.stat-card {
@@ -91,5 +74,11 @@ h2 {
align-items: center;
justify-content: center;
color: #999;
background: #e8f4ff;
}
.sidebar {
height: 300px;
background: #f0f9ff;
}
</style>

View File

@@ -1,64 +1,46 @@
<template>
<div class="profile-page">
<h2>用户资料</h2>
<el-row :gutter="20">
<el-row class="page-container" :gutter="20">
<el-col :span="8">
<div class="grid-content avatar-section">
<div class="design-component avatar-section">
<div class="avatar">头像</div>
</div>
</el-col>
<el-col :span="16">
<div class="grid-content info-section">
<el-row :gutter="10">
<el-col :span="12">
<div class="info-item">姓名: 张三</div>
<div class="design-component info-item">姓名: 张三</div>
</el-col>
<el-col :span="12">
<div class="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>
<div class="design-component info-item">邮箱: zhang@example.com</div>
</el-col>
<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-row>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const userInfo = ref({
name: '张三',
email: 'zhang@example.com',
phone: '138****8888',
department: '技术部'
})
// 用户资料页面
</script>
<style scoped>
.profile-page {
.page-container {
padding: 20px;
background: #f5f5f5;
min-height: 100vh;
min-height: 100%;
}
h2 {
color: #333;
margin-bottom: 20px;
}
.grid-content {
.design-component {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 10px;
}
.avatar-section {
@@ -80,14 +62,9 @@ h2 {
font-size: 18px;
}
.info-section {
height: 200px;
}
.info-item {
padding: 10px;
padding: 15px;
background: #f9f9f9;
border-radius: 4px;
color: #666;
}
</style>