v2版本初稿

This commit is contained in:
xielue 2025-06-14 12:15:33 +08:00
parent 11d49fd2be
commit b582b40d5f
19 changed files with 971 additions and 1 deletions

24
src/AppV2.vue Normal file
View File

@ -0,0 +1,24 @@
<template>
<div id="app">
<IndexV2 />
</div>
</template>
<script>
import IndexV2 from '@/v2/design.vue'
export default {
name: 'AppV2',
components: {
IndexV2
}
}
</script>
<style>
#app {
width: 100%;
height: 100%;
font-family: Arial, sans-serif;
}
</style>

BIN
src/assets/chart-bar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

BIN
src/assets/chart-line.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
src/assets/chart-pie.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
src/assets/table-basic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
src/assets/table-paged.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
src/assets/text-title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 B

View File

@ -1,5 +1,6 @@
import Vue from 'vue'
import App from './App.vue'
//import App from './App.vue'
import App from './AppV2.vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

View File

@ -0,0 +1,65 @@
<template>
<div class="canvas">
<div class="canvas-content">
<component
v-for="(element, index) in elements"
:key="index"
:is="element.component"
:option="element.option"
:style="{
position: 'absolute',
left: (element.option && element.option.left) ? element.option.left + 'px' :
(element.option && element.option.x) ? element.option.x + 'px' : '100px',
top: (element.option && element.option.top) ? element.option.top + 'px' :
(element.option && element.option.y) ? element.option.y + 'px' : '100px',
width: (element.option && element.option.width) ? element.option.width + 'px' : 'auto',
height: (element.option && element.option.height) ? element.option.height + 'px' : 'auto',
border: activeIndex === index ? '2px solid #1890ff' : '1px dashed #ccc'
}"
@click.native="selectElement(index)"
/>
</div>
</div>
</template>
<script>
export default {
name: 'Canvas',
props: {
elements: {
type: Array,
default: () => [] //
}
},
data() {
return {
activeIndex: -1
}
},
methods: {
selectElement(index) {
this.activeIndex = index
this.$emit('element-selected', index)
}
}
}
</script>
<style scoped>
.canvas {
width: 100%;
height: 100%;
position: relative;
}
.canvas-content {
width: 800px;
height: 600px;
margin: 0 auto;
background: white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: relative;
}
/* 移除.canvas-element样式由组件自身控制 */
</style>

View File

@ -0,0 +1,61 @@
<template>
<div class="layer-panel">
<div class="panel-header">
<h3>图层</h3>
</div>
<div class="layer-list">
<div
class="layer-item"
v-for="(element, index) in elements"
:key="index"
@click="$emit('layer-selected', index)"
>
{{ element.name || `元素${index + 1}` }}
</div>
</div>
</div>
</template>
<script>
export default {
name: 'LayerPanel',
props: {
elements: {
type: Array,
default: () => []
}
}
}
</script>
<style scoped>
.layer-panel {
height: 100%;
display: flex;
flex-direction: column;
}
.panel-header {
padding: 8px 12px;
border-bottom: 1px solid #e1e4e8;
height: 40px; /* 固定高度 */
min-height: 40px; /* 确保不会压缩 */
display: flex;
align-items: center;
}
.layer-list {
flex: 1;
overflow-y: auto;
}
.layer-item {
padding: 10px;
border-bottom: 1px solid #eee;
cursor: pointer;
}
.layer-item:hover {
background: #e9e9e9;
}
</style>

View File

@ -0,0 +1,170 @@
<template>
<div class="property-panel">
<div class="panel-header">
<h3>属性设置</h3>
</div>
<div class="tabs">
<button v-for="tab in tabs" :key="tab" @click="currentTab = tab" :class="{ active: currentTab === tab }">
{{ tab }}
</button>
</div>
<div class="tab-content" v-if="element">
<div v-for="(tab, tabIndex) in tabs" :key="tabIndex" v-show="currentTab === tab">
<div class="form-group" v-for="(prop, propIndex) in groupedProps[tab]" :key="propIndex">
<label>{{ prop.label }}</label>
<input
:type="prop.type"
v-model="element.option[prop.name]"
:disabled="prop.disabled"
@change="updateElementOption">
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'PropertyPanel',
props: {
element: {
type: Object,
default: () => ({
name: '未选择',
type: 'none',
props: [],
option: {
width: 100,
height: 50,
x: 0,
y: 0,
backgroundColor: '#ffffff'
}
})
},
index: Number
},
computed: {
groupedProps() {
if(!this.element){
return [];
}
const groups = {}
//
groups['基础'] = [
{ name: 'name', type: 'string', label: '名称' },
{ name: 'type', type: 'string', label: '类型', disabled: true }
]
//
groups['样式'] = [
{ name: 'width', type: 'number', label: '宽度', group: '样式' },
{ name: 'height', type: 'number', label: '高度', group: '样式' },
{ name: 'x', type: 'number', label: 'X坐标', group: '样式' },
{ name: 'y', type: 'number', label: 'Y坐标', group: '样式' },
{ name: 'backgroundColor', type: 'color', label: '背景色', group: '样式' }
]
this.element.props = this.element.props || [];
// element.props
this.element.props.forEach(prop => {
if (!groups[prop.group]) {
groups[prop.group] = []
}
groups[prop.group].push(prop)
})
return groups
},
tabs() {
return Object.keys(this.groupedProps)
}
},
methods: {
updateElementOption() {
console.log('updateElementOption', this.element)
this.$emit('update-element', {
index: this.index,
element: this.element
})
}
},
data() {
return {
currentTab: '基础'
}
},
watch: {
},
created() {
}
}
</script>
<style scoped>
.property-panel {
height: 100%;
display: flex;
flex-direction: column;
font-size: 14px;
/* 添加固定字体大小 */
}
.panel-header {
padding: 8px 12px;
border-bottom: 1px solid #e1e4e8;
height: 40px;
min-height: 40px;
display: flex;
align-items: center;
font-size: 16px;
/* 标题稍大一点 */
}
.tab-content {
flex: 1;
overflow-y: auto;
}
.tabs {
display: flex;
border-bottom: 1px solid #ddd;
}
.tabs button {
flex: 1;
padding: 10px;
background: none;
border: none;
cursor: pointer;
}
.tabs button.active {
background: #e9e9e9;
border-bottom: 2px solid #1890ff;
}
.tab-content {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input {
width: 100%;
padding: 5px;
border: 1px solid #ddd;
border-radius: 3px;
}
</style>

View File

@ -0,0 +1,151 @@
<template>
<div class="toolbar">
<div class="toolbar-left">
</div>
<!-- 修改为鼠标经过效果 -->
<div class="component-selector">
<div
class="component-category"
v-for="category in componentCategories"
:key="category.name"
@mouseenter="activeCategory = category.name"
@mouseleave="activeCategory = ''"
>
{{ category.name }}
<div
class="component-list"
v-if="activeCategory === category.name"
@mouseenter="activeCategory = category.name"
>
<div
class="component-item"
v-for="item in category.items"
:key="item.name"
@click="addToCanvas(item)"
>
<img :src="item.icon" :alt="item.name">
<span>{{ item.name }}</span>
</div>
</div>
</div>
</div>
<div class="toolbar-right">
<button @click="clearCache">清除</button>
<button>保存</button>
<button>预览</button>
</div>
</div>
</template>
<script>
import { getComponentCategories } from './componentData'
export default {
name: 'Toolbar',
data() {
return {
activeCategory: '',
componentCategories: getComponentCategories() //
}
},
methods: {
addToCanvas(item) {
const newItem = {
...item,
option: {
...item.defaultOption,
name : item.name,
type : item.type,
...item.option
}
}
this.$emit('add-component', newItem)
this.activeCategory = ''
},
clearCache() {
localStorage.removeItem('canvasElements')
this.$emit('clear-canvas') //
}
}
}
</script>
<style scoped>
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
position: relative;
}
.component-selector {
display: flex;
gap: 15px;
}
.component-category {
position: relative;
padding: 8px 15px;
cursor: pointer;
background: #2c3e50;
color: white;
border-radius: 4px;
}
/* 移除悬停显示样式,改回点击显示 */
/* .component-category:hover .component-list {
display: block;
} */
.component-list {
position: absolute;
top: 100%;
left: 0;
background: #2c3e50;
border: 1px solid #3d5166;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
z-index: 100;
min-width: 150px;
padding: 10px;
/* 移除display:none由v-if控制显示 */
}
.component-item {
display: flex;
align-items: center;
padding: 8px;
cursor: pointer;
transition: background 0.2s;
color: white; /* 确保文字为白色 */
}
.component-item:hover {
background: #3d5166; /* 悬停背景色 */
}
.component-item img {
width: 24px;
height: 24px;
margin-right: 8px;
filter: brightness(0) invert(1); /* 使图标变为白色 */
}
/* 原有按钮样式保持不变 */
.toolbar button {
margin: 0 5px;
padding: 5px 10px;
background: #333;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
.toolbar button:hover {
background: #555;
}
</style>

View File

@ -0,0 +1,107 @@
export const componentList = [
{
id: 'chart-line',
name: '线图',
type: 'line-chart',
icon: require('@/assets/chart-line.png'),
groupName: '图表',
component: () => import('./elements/chart-line.vue') // 新增组件引用
},
{
id: 'chart-bar',
name: '柱图',
type: 'bar-chart',
icon: require('@/assets/chart-bar.png'),
groupName: '图表',
component: () => import('./elements/chart-bar.vue'),
props: [
{
name: 'title',
type: 'string',
label: '标题',
default: '柱图标题',
group: '基础'
},
{
name: 'dbName',
type: 'string',
label: '数据源对象',
default: '',
group: '数据'
},
]
},
{
id: 'chart-pie',
name: '饼图',
type: 'pie-chart',
icon: require('@/assets/chart-pie.png'),
groupName: '图表',
component: () => import('./elements/chart-pie.vue'),
props: [ // 新增props数组
{
name: 'title',
type: 'string',
label: '标题',
default: '饼图标题',
group: '基础'
},
{
name: 'dbName',
type: 'string',
label: '数据源对象',
default: '',
group: '数据'
},
],
defaultOption: {
x: 100,
y: 100,
width: 300,
height: 300
}
},
{
id: 'table-basic',
name: '基础表格',
type: 'basic-table',
icon: require('@/assets/table-basic.png'),
groupName: '表格'
},
{
id: 'table-paged',
name: '分页表格',
type: 'paged-table',
icon: require('@/assets/table-paged.png'),
groupName: '表格'
},
{
id: 'text-title',
name: '标题',
type: 'title-text',
icon: require('@/assets/text-title.png'),
groupName: '文字'
},
{
id: 'text-paragraph',
name: '正文',
type: 'paragraph',
icon: require('@/assets/text-paragraph.png'),
groupName: '文字'
}
]
// 根据groupName分组
export function getComponentCategories() {
const groups = {}
componentList.forEach(item => {
if (!groups[item.groupName]) {
groups[item.groupName] = {
name: item.groupName,
items: []
}
}
groups[item.groupName].items.push(item)
})
return Object.values(groups)
}

View File

@ -0,0 +1,24 @@
<template>
<div class="chart-bar">
<h3>{{ option }}</h3>
</div>
</template>
<script>
export default {
name: 'ChartBar',
props: {
//
option: {
type: Object,
default: () => ({})
}
}
}
</script>
<style scoped>
.chart-bar {
/* 组件样式 */
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<div class="chart-line">
<!-- 线图组件内容 -->
<h3>{{ option }}</h3>
</div>
</template>
<script>
export default {
name: 'ChartLine',
props: {
//
option: {
type: Object,
default: () => ({})
}
}
}
</script>
<style scoped>
.chart-line {
/* 组件样式 */
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<div class="chart-pie" ref="chartDom" style="width: 100%; height: 100%;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'ChartPie',
props: {
//
//
option: {
type: Object,
default: () => ({})
}
},
data() {
return {
chartInstance: null
}
},
mounted() {
this.initChart();
},
beforeDestroy() {
if (this.chartInstance) {
this.chartInstance.dispose();
}
},
methods: {
initChart() {
this.chartInstance = echarts.init(this.$refs.chartDom);
const option = {
title: {
text: this.option.name,
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '访问来源',
type: 'pie',
radius: '50%',
data: [
{ value: 1048, name: '搜索引擎' },
{ value: 735, name: '直接访问' },
{ value: 580, name: '邮件营销' },
{ value: 484, name: '联盟广告' },
{ value: 300, name: '视频广告' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
this.chartInstance.setOption(option);
}
}
}
</script>
<style scoped>
.chart-pie {
width: 100%;
height: 400px;
}
</style>

145
src/v2/design.vue Normal file
View File

@ -0,0 +1,145 @@
<template>
<div class="bi-designer">
<!-- 顶部工具栏 -->
<toolbar
@add-component="handleAddComponent"
@clear-canvas="handleClearCanvas"
></toolbar>
<div class="main-content">
<!-- 左侧图层面板 -->
<layer-panel
:elements="elements"
@layer-selected="handleElementSelected"
></layer-panel>
<!-- 中间画布区域 -->
<div class="canvas-container">
<canvas-component
:elements="elements"
@element-selected="handleElementSelected"
></canvas-component>
<property-panel
:element="selectedElement"
:index="selectedIndex"
@update-element="handleUpdateElement"
></property-panel>
</div>
</div>
</div>
</template>
<script>
import Toolbar from './components/Toolbar.vue'
import LayerPanel from './components/LayerPanel.vue'
import CanvasComponent from './components/Canvas.vue'
import PropertyPanel from './components/PropertyPanel.vue'
export default {
components: {
Toolbar,
LayerPanel,
CanvasComponent,
PropertyPanel
},
data() {
return {
selectedElement:null,
selectedIndex:-1,
elements: JSON.parse(localStorage.getItem('canvasElements')) || [] //
}
},
mounted() {
},
methods: {
handleAddComponent(component) {
console.log(component)
this.elements.push({
...component,
x: 100,
y: 100
})
this.saveToStorage()
},
saveToStorage() {
localStorage.setItem('canvasElements', JSON.stringify(this.elements))
}
,
handleClearCanvas() {
this.elements = []
this.saveToStorage()
}, //
handleElementSelected(index) {
this.selectedIndex = index
this.selectedElement = index >= 0 ? {...this.elements[index]} : null
},
handleUpdateElement({index, element}) {
this.$set(this.elements, index, element)
this.saveToStorage()
}
}
}
</script>
<style>
/* 全局基础字体大小 */
html, body {
font-size: 14px !important;
}
</style>
<style scoped>
.bi-designer {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
margin: 0;
padding: 0;
overflow: hidden;
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
}
.toolbar {
height: 48px;
background: #1f2329;
color: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.main-content {
display: flex;
flex: 1;
overflow: hidden;
background: #f5f6f7;
}
.layer-panel {
width: 240px;
background: #fff;
border-right: 1px solid #e1e4e8;
overflow: hidden;
display: flex;
flex-direction: column;
}
.canvas-container {
flex: 1;
padding: 16px;
overflow: auto;
background: #f5f6f7;
display: flex;
justify-content: center;
align-items: flex-start;
}
.property-panel {
width: 280px;
background: #fff;
border-left: 1px solid #e1e4e8;
overflow: hidden;
display: flex;
flex-direction: column;
}
</style>

115
src/v2/index.vue Normal file
View File

@ -0,0 +1,115 @@
<template>
<div class="bi-designer">
<canvas-component
:elements="elements"
></canvas-component>
</div>
</div>
</template>
<script>
import CanvasComponent from './components/Canvas.vue'
export default {
components: {
CanvasComponent
},
data() {
return {
elements: JSON.parse(localStorage.getItem('canvasElements')) || [] //
}
},
mounted() {
},
methods: {
handleAddComponent(component) {
console.log(component)
this.elements.push({
...component,
x: 100,
y: 100
})
this.saveToStorage()
},
saveToStorage() {
localStorage.setItem('canvasElements', JSON.stringify(this.elements))
}
,
handleClearCanvas() {
this.elements = []
this.saveToStorage()
}, //
handleElementSelected(index) {
this.selectedIndex = index
this.selectedElement = index >= 0 ? {...this.elements[index]} : null
},
handleUpdateElement({index, element}) {
this.$set(this.elements, index, element)
this.saveToStorage()
}
}
}
</script>
<style>
/* 全局基础字体大小 */
html, body {
font-size: 14px !important;
}
</style>
<style scoped>
.bi-designer {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
margin: 0;
padding: 0;
overflow: hidden;
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
}
.toolbar {
height: 48px;
background: #1f2329;
color: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.main-content {
display: flex;
flex: 1;
overflow: hidden;
background: #f5f6f7;
}
.layer-panel {
width: 240px;
background: #fff;
border-right: 1px solid #e1e4e8;
overflow: hidden;
display: flex;
flex-direction: column;
}
.canvas-container {
flex: 1;
padding: 16px;
overflow: auto;
background: #f5f6f7;
display: flex;
justify-content: center;
align-items: flex-start;
}
.property-panel {
width: 280px;
background: #fff;
border-left: 1px solid #e1e4e8;
overflow: hidden;
display: flex;
flex-direction: column;
}
</style>