This commit is contained in:
wfz
2025-12-21 20:58:34 +08:00
parent 83bafa4e1e
commit c6077ff2ad
22 changed files with 2004 additions and 33 deletions

View File

@@ -1,4 +1,4 @@
{
"name": "设计中心",
"description": "展示已添加的设计组件实例"
"description": "实时预览选中的Vue页面或设计组件"
}

View File

@@ -1,9 +1,11 @@
<script setup lang="ts">
import { defineAsyncComponent, markRaw } from 'vue'
import { defineAsyncComponent, markRaw, computed, watch } from 'vue'
import { useDesignStore } from '../../stores/designStore'
import { useVueFileStore } from '../../stores/vueFileStore'
import config from './index.json'
const designStore = useDesignStore()
const vueFileStore = useVueFileStore()
// 自动扫描所有设计组件eager 模式确保同步加载)
const designComponentModules = import.meta.glob('../../designComponents/*/index.vue', { eager: true })
@@ -33,39 +35,78 @@ const handleRemove = (instanceId: string, event: Event) => {
event.stopPropagation()
designStore.removeComponent(instanceId)
}
// 扫描所有views目录下的Vue文件
const viewModules = import.meta.glob('../../../views/**/*.vue')
// 当前选中的Vue页面组件
const selectedPageComponent = computed(() => {
if (!vueFileStore.selectedFilePath) {
return null
}
// 动态加载选中的组件
const loader = viewModules[vueFileStore.selectedFilePath]
if (loader) {
return defineAsyncComponent(loader as any)
}
return null
})
// 监听选中文件变化
watch(() => vueFileStore.selectedFilePath, (newPath) => {
console.log('设计中心:选中的文件路径变化', newPath)
})
</script>
<template>
<div class="design-center">
<div class="center-header">
<span class="title">{{ config.name }}</span>
<span class="count">{{ designStore.components.length }} 个实例</span>
<span class="count" v-if="!vueFileStore.selectedFilePath">
{{ designStore.components.length }} 个实例
</span>
<span class="file-info" v-else>
📄 {{ vueFileStore.selectedFileName }}
</span>
</div>
<div class="center-body">
<div
v-for="instance in designStore.components"
:key="instance.id"
class="component-row"
:class="{ selected: designStore.selectedId === instance.id }"
@click="handleSelect(instance.id)"
>
<div class="component-label">
<span class="component-name">{{ instance.name }}</span>
<button class="remove-btn" @click="handleRemove(instance.id, $event)">×</button>
</div>
<div class="component-preview">
<component
v-if="getComponent(instance.componentId)"
:is="getComponent(instance.componentId)"
v-bind="instance.props"
/>
<!-- 动态渲染选中的Vue页面 -->
<div v-if="selectedPageComponent" class="page-preview">
<component :is="selectedPageComponent" />
</div>
<!-- 原有的设计组件实例列表 -->
<div v-else-if="designStore.components.length > 0" class="component-list">
<div
v-for="instance in designStore.components"
:key="instance.id"
class="component-row"
:class="{ selected: designStore.selectedId === instance.id }"
@click="handleSelect(instance.id)"
>
<div class="component-label">
<span class="component-name">{{ instance.name }}</span>
<button class="remove-btn" @click="handleRemove(instance.id, $event)">×</button>
</div>
<div class="component-preview">
<component
v-if="getComponent(instance.componentId)"
:is="getComponent(instance.componentId)"
v-bind="instance.props"
/>
</div>
</div>
</div>
<div v-if="designStore.components.length === 0" class="empty-tip">
<!-- 空状态 -->
<div v-else class="empty-tip">
<div class="empty-icon">🎨</div>
<div>暂无设计组件</div>
<div class="empty-hint">从左侧列表点击添加</div>
<div>暂无内容</div>
<div class="empty-hint">
点击页面管理选择Vue页面或从左侧列表添加设计组件
</div>
</div>
</div>
</div>
@@ -99,12 +140,32 @@ const handleRemove = (instanceId: string, event: Event) => {
font-size: 11px;
}
.file-info {
color: #4fc3f7;
font-size: 12px;
font-weight: 500;
}
.center-body {
flex: 1;
padding: 12px;
overflow: auto;
}
.page-preview {
width: 100%;
height: 100%;
background: white;
border-radius: 8px;
overflow: auto;
}
.component-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.component-row {
margin-bottom: 12px;
border-radius: 6px;

View File

@@ -0,0 +1,4 @@
{
"name": "页面管理",
"description": "浏览和管理src/views目录下的Vue页面文件"
}

View File

@@ -0,0 +1,254 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useVueFileStore, type VueFileNode } from '../../stores/vueFileStore'
import config from './index.json'
const vueFileStore = useVueFileStore()
// 扩展的节点数据用于el-tree
interface TreeNodeData {
label: string
path: string
type: 'file' | 'folder'
children?: TreeNodeData[]
}
const treeData = ref<TreeNodeData[]>([])
const defaultExpandedKeys = ref<string[]>([])
// 扫描views目录下的所有Vue文件
const scanViewsDirectory = () => {
// 使用Vite的import.meta.glob扫描views目录
const viewModules = import.meta.glob('../../../views/**/*.vue')
console.log('扫描到的文件:', Object.keys(viewModules))
// 构建文件树
const fileTree = vueFileStore.buildFileTree(viewModules)
// 转换为el-tree需要的格式
const convertToTreeData = (nodes: VueFileNode[]): TreeNodeData[] => {
return nodes.map(node => ({
label: node.name,
path: node.path,
type: node.type,
children: node.children ? convertToTreeData(node.children) : undefined
}))
}
treeData.value = convertToTreeData(fileTree)
// 默认展开所有文件夹
defaultExpandedKeys.value = getAllFolderPaths(treeData.value)
}
// 获取所有文件夹路径(用于默认展开)
const getAllFolderPaths = (nodes: TreeNodeData[]): string[] => {
const paths: string[] = []
const traverse = (items: TreeNodeData[]) => {
items.forEach(item => {
if (item.type === 'folder') {
paths.push(item.path)
if (item.children) {
traverse(item.children)
}
}
})
}
traverse(nodes)
return paths
}
// 处理节点点击事件
const handleNodeClick = (data: TreeNodeData) => {
if (data.type === 'file') {
// 只有文件才能被选中
vueFileStore.selectFile(data.path, data.label)
}
}
// 自定义树节点图标
const getNodeIcon = (data: TreeNodeData) => {
if (data.type === 'folder') {
return '📁'
}
return '📄'
}
onMounted(() => {
scanViewsDirectory()
})
</script>
<template>
<div class="page-manager">
<div class="manager-header">
<span class="title">{{ config.name }}</span>
<span class="count">{{ treeData.length }} </span>
</div>
<div class="manager-body">
<div class="current-file" v-if="vueFileStore.selectedFileName">
<div class="label">当前文件:</div>
<div class="file-name">{{ vueFileStore.selectedFileName }}</div>
</div>
<div class="tree-container">
<el-tree
:data="treeData"
:props="{ label: 'label', children: 'children' }"
:default-expanded-keys="defaultExpandedKeys"
node-key="path"
highlight-current
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<div class="custom-tree-node">
<span class="node-icon">{{ getNodeIcon(data) }}</span>
<span
class="node-label"
:class="{
'is-file': data.type === 'file',
'is-selected': data.path === vueFileStore.selectedFilePath
}"
>
{{ node.label }}
</span>
</div>
</template>
</el-tree>
<div v-if="treeData.length === 0" class="empty-tip">
<div class="empty-icon">📂</div>
<div>暂无Vue文件</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.page-manager {
display: flex;
flex-direction: column;
height: 100%;
background: #1e1e1e;
}
.manager-header {
padding: 8px 12px;
background: #2d2d2d;
border-bottom: 1px solid #3c3c3c;
display: flex;
align-items: center;
justify-content: space-between;
}
.title {
color: #cccccc;
font-size: 13px;
font-weight: 500;
}
.count {
color: #888888;
font-size: 11px;
}
.manager-body {
flex: 1;
padding: 12px;
overflow: auto;
}
.current-file {
margin-bottom: 12px;
padding: 8px 12px;
background: #094771;
border-radius: 4px;
border-left: 3px solid #007acc;
}
.label {
color: #888888;
font-size: 11px;
margin-bottom: 4px;
}
.file-name {
color: #e0e0e0;
font-size: 12px;
font-weight: 500;
}
.tree-container {
font-size: 13px;
}
/* Element Plus Tree 样式覆盖 */
:deep(.el-tree) {
background: transparent;
color: #cccccc;
}
:deep(.el-tree-node__content) {
background: transparent;
height: 32px;
padding: 0 8px;
border-radius: 4px;
}
:deep(.el-tree-node__content:hover) {
background: #2a2d2e;
}
:deep(.el-tree-node.is-current > .el-tree-node__content) {
background: #094771;
}
:deep(.el-tree-node__expand-icon) {
color: #888888;
}
.custom-tree-node {
display: flex;
align-items: center;
width: 100%;
}
.node-icon {
margin-right: 8px;
font-size: 14px;
}
.node-label {
flex: 1;
color: #cccccc;
}
.node-label.is-file {
cursor: pointer;
}
.node-label.is-file:hover {
color: #ffffff;
}
.node-label.is-selected {
color: #4fc3f7;
font-weight: 500;
}
.empty-tip {
color: #666666;
text-align: center;
padding: 40px 20px;
font-size: 13px;
}
.empty-icon {
font-size: 40px;
margin-bottom: 12px;
}
</style>

View File

@@ -0,0 +1,110 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
// Vue文件树节点
export interface VueFileNode {
name: string // 文件/文件夹名称
path: string // 完整路径
type: 'file' | 'folder' // 类型
children?: VueFileNode[] // 子节点
}
export const useVueFileStore = defineStore('vueFile', () => {
// 当前选中的Vue文件路径
const selectedFilePath = ref<string | null>(null)
// 当前选中的文件名
const selectedFileName = ref<string | null>(null)
// 文件树结构
const fileTree = ref<VueFileNode[]>([])
// 设置选中的文件
const selectFile = (path: string, name: string) => {
selectedFilePath.value = path
selectedFileName.value = name
console.log('选中文件:', { path, name })
}
// 清除选中
const clearSelection = () => {
selectedFilePath.value = null
selectedFileName.value = null
}
// 构建文件树从import.meta.glob结果
const buildFileTree = (modules: Record<string, any>): VueFileNode[] => {
const tree: VueFileNode[] = []
const folderMap = new Map<string, VueFileNode>()
for (const path in modules) {
// 移除开头的 '../views/' 或 './views/'
const relativePath = path.replace(/^\.\.?\/views\//, '')
const parts = relativePath.split('/')
let currentLevel = tree
let currentPath = ''
// 处理路径的每一部分
for (let i = 0; i < parts.length; i++) {
const part = parts[i]
currentPath = currentPath ? `${currentPath}/${part}` : part
const isFile = i === parts.length - 1 && part.endsWith('.vue')
if (isFile) {
// 添加文件节点
currentLevel.push({
name: part,
path: path,
type: 'file'
})
} else {
// 添加文件夹节点
let folder = folderMap.get(currentPath)
if (!folder) {
folder = {
name: part,
path: currentPath,
type: 'folder',
children: []
}
currentLevel.push(folder)
folderMap.set(currentPath, folder)
}
currentLevel = folder.children!
}
}
}
// 排序:文件夹在前,文件在后
const sortTree = (nodes: VueFileNode[]) => {
nodes.sort((a, b) => {
if (a.type !== b.type) {
return a.type === 'folder' ? -1 : 1
}
return a.name.localeCompare(b.name)
})
nodes.forEach(node => {
if (node.children) {
sortTree(node.children)
}
})
}
sortTree(tree)
return tree
}
return {
selectedFilePath,
selectedFileName,
fileTree,
selectFile,
clearSelection,
buildFileTree
}
})

View File

@@ -1,5 +1,7 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import './style.css'
import App from './App.vue'
import router from './router'
@@ -9,4 +11,5 @@ const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(ElementPlus)
app.mount('#app')

View File

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

View File

@@ -0,0 +1,64 @@
<template>
<div class="test-page">
<h2>测试页面 2 - 表单布局</h2>
<el-row :gutter="20">
<el-col :span="24">
<div class="grid-content">表单标题区域</div>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="6">
<div class="grid-content">标签</div>
</el-col>
<el-col :span="18">
<div class="grid-content">输入框</div>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="6">
<div class="grid-content">标签</div>
</el-col>
<el-col :span="18">
<div class="grid-content">输入框</div>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="24">
<div class="grid-content">提交按钮</div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formData = ref({
name: '',
email: ''
})
</script>
<style scoped>
.test-page {
padding: 20px;
background: #fafafa;
min-height: 100vh;
}
h2 {
color: #333;
margin-bottom: 20px;
}
.grid-content {
background: #67c23a;
color: white;
padding: 20px;
text-align: center;
border-radius: 4px;
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<div class="overview-page">
<h2>仪表板概览</h2>
<el-row :gutter="20">
<el-col :span="6">
<div class="grid-content 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="stat-title">活跃用户</div>
<div class="stat-value">856</div>
</div>
</el-col>
<el-col :span="6">
<div class="grid-content 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="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>
</el-col>
<el-col :span="8">
<div class="grid-content">侧边栏信息</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 {
padding: 20px;
background: #f0f2f5;
min-height: 100vh;
}
h2 {
color: #333;
margin-bottom: 20px;
}
.grid-content {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.stat-card {
text-align: center;
}
.stat-title {
color: #666;
font-size: 14px;
margin-bottom: 10px;
}
.stat-value {
color: #333;
font-size: 24px;
font-weight: bold;
}
.chart-area {
height: 300px;
display: flex;
align-items: center;
justify-content: center;
color: #999;
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<div class="profile-page">
<h2>用户资料</h2>
<el-row :gutter="20">
<el-col :span="8">
<div class="grid-content 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>
</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>
</el-col>
<el-col :span="12">
<div class="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 {
padding: 20px;
background: #f5f5f5;
min-height: 100vh;
}
h2 {
color: #333;
margin-bottom: 20px;
}
.grid-content {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.avatar-section {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.avatar {
width: 120px;
height: 120px;
background: #409eff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.info-section {
height: 200px;
}
.info-item {
padding: 10px;
background: #f9f9f9;
border-radius: 4px;
color: #666;
}
</style>