wip: 添加表格到编辑器 正在完成编辑表格功能

master
shengwen.chen 2 months ago
parent a6647dcefe
commit ad14e6d7ee

1
.gitignore vendored

@ -20,6 +20,7 @@ coverage
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.idea
*.suo
*.ntvs*

@ -0,0 +1,35 @@
{
//
"editor.formatOnType": false,
"editor.formatOnSave": true,
"editor.formatOnPaste": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
//
"editor.tabSize": 2,
"editor.snippetSuggestions": "top",
"editor.guides.bracketPairs": "active",
"editor.suggestSelection": "first",
"editor.acceptSuggestionOnCommitCharacter": false,
"editor.quickSuggestions": {
"other": true,
"comments": true,
"strings": true
},
//
"editor.wordWrap": "on",
"editor.wordWrapColumn": 180,
"editor.rulers": [180],
//
"files.autoSave": "off",
// Git
"git.confirmSync": false,
//
"workbench.startupEditor": "newUntitledFile"
}

20
package-lock.json generated

@ -8,6 +8,7 @@
"name": "smdoceditor",
"version": "0.0.0",
"dependencies": {
"@iconify/vue": "^4.3.0",
"axios": "^1.8.4",
"element-plus": "^2.9.7",
"pinia": "^3.0.1",
@ -1337,6 +1338,25 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@iconify/types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="
},
"node_modules/@iconify/vue": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@iconify/vue/-/vue-4.3.0.tgz",
"integrity": "sha512-Xq0h6zMrHBbrW8jXJ9fISi+x8oDQllg5hTDkDuxnWiskJ63rpJu9CvJshj8VniHVTbsxCg9fVoPAaNp3RQI5OQ==",
"dependencies": {
"@iconify/types": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/cyberalien"
},
"peerDependencies": {
"vue": ">=3"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",

@ -14,6 +14,7 @@
"format": "prettier --write src/"
},
"dependencies": {
"@iconify/vue": "^4.3.0",
"axios": "^1.8.4",
"element-plus": "^2.9.7",
"pinia": "^3.0.1",

@ -1,6 +1,7 @@
<!-- eslint-disable @typescript-eslint/no-explicit-any -->
<script setup lang="ts">
import { inject, defineProps } from 'vue'
import type { IWidget } from '@/types/widgetsConfigInterface'
import { inject } from 'vue'
import type { IWidget, IWidgetJson } from '@/types/widgetsConfigInterface'
import RendererDom from '@/components/RendererDom.vue'
const props = defineProps<{
@ -10,6 +11,7 @@ const props = defineProps<{
//
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const docForm = inject('docForm') as any
const widgetJson = inject('widgetJson') as IWidgetJson
//
if (props['itemData']['category'] == 'formItem') {
@ -19,24 +21,97 @@ if (props['itemData']['category'] == 'formItem') {
docForm[props['itemData']['options']['name']] = props['itemData']['options']['defaultValue'] || 0
}
}
// id
const handleFindDomById = (data: any, returnParent = false, parent: any) => {
if (Array.isArray(data)) {
for (const item of data) {
const found: any = handleFindDomById(item, returnParent, data);
if (found) return found;
}
} else {
if (data.id === widgetJson['activeId']) {
console.log(parent);
console.log(data);
return returnParent ? parent : data;
}
//
if (data.subDomList) {
const result: any = handleFindDomById(data.subDomList, returnParent, data);
if (result) return result;
}
}
return null;
}
//
const handleSelectDom = (activeId: any) => {
widgetJson['activeId'] = activeId
}
//
const handleFindLastDom = () => {
}
//
const handleAddRow = () => {
// widgetJson['activeId'] = 'input-12366'
const res = handleFindDomById(widgetJson['subDomList'], true, null);
console.log(res);
}
//
const handleAddCol = () => {
}
//
const handleDelDom = () => {
}
</script>
<template>
<!-- 针对表格容器 -->
<template v-if="itemData['type'] == 'table'">
<table border="1" cellpadding="10" cellspacing="0" class="w-full">
<template v-for="(row, rowIndex) in itemData['rows']" :key="rowIndex">
<tr>
<RendererDom v-for="subData in row['cols']" :key="subData['id']" :itemData="subData" />
</tr>
<div class="table-container" :class="{ activeDom: itemData['id'] == widgetJson['activeId'] }"
:data-id="itemData['id']" @click.stop="handleSelectDom(itemData['id'])">
<table border="1" cellpadding="10" cellspacing="0" class="w-full">
<template v-for="(row, rowIndex) in itemData['subDomList']" :key="rowIndex">
<tr>
<RendererDom v-for="subData in row" :key="subData['id']" :itemData="subData" />
</tr>
</template>
</table>
<template v-if="itemData['id'] == widgetJson['activeId']">
<div class="activeDomWrapper">{{ widgetJson['activeDomName'] }}</div>
<div class="fnArea">
<div @click="handleFindLastDom" title="选中父组件">
<Icon icon="ion:return-up-back" />
</div>
<div @click="handleAddRow" title="插入新行">
<Icon icon="iconoir:arrow-separate-vertical" />
</div>
<div @click="handleAddCol" title="插入新列">
<Icon icon="iconoir:arrow-separate" />
</div>
<div @click="handleDelDom" title="移除组件">
<Icon icon="mi:delete" />
</div>
</div>
</template>
</table>
</div>
</template>
<!-- 针对表格列 -->
<template v-if="itemData['type'] == 'table-cell'">
<td v-if="!itemData['merged']" :colspan="itemData['options']['colspan']" :rowspan="itemData['options']['rowspan']">
<RendererDom v-for="subData in itemData['widgetList']" :key="subData['id']" :itemData="subData" />
<td v-if="!itemData['merged']" :colspan="itemData['options']['colspan']" :rowspan="itemData['options']['rowspan']"
:data-id="itemData['id']" :class="{ activeDom: widgetJson['activeId'] == itemData['id'] }"
@click.stop="handleSelectDom(itemData['id'])">
<RendererDom v-for="subData in itemData['subDomList']" :key="subData['id']" :itemData="subData" />
</td>
</template>
@ -65,4 +140,4 @@ if (props['itemData']['category'] == 'formItem') {
</el-radio-group>
</el-form-item>
</template>
</template>
</template>

@ -1,5 +1,7 @@
/* eslint-disable vue/multi-word-component-names */
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { Icon } from '@iconify/vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import './assets/css/main.css'
@ -8,6 +10,7 @@ import router from './router'
const app = createApp(App)
app.use(ElementPlus)
app.component('Icon', Icon)
app.use(createPinia())
app.use(router)

@ -2,7 +2,7 @@ export interface IOption {
// 元素唯一标识符
name: string
// 隐藏状态
hidden: boolean
hidden?: boolean
// 自定义类名
customClass?: string
// 标签名称
@ -23,7 +23,7 @@ export type ICategory = 'container' | 'formItem'
export interface IWidget {
// 元素唯一标识符
key?: number
key?: string
// 元素id
id?: string
// 类别
@ -34,16 +34,23 @@ export interface IWidget {
displayName: string
// 图标
icon: string
// 子类
rows?: [{ cols: [IWidget] }]
// 属性
options: IOption
// 子结构
widgetList?: []
subDomList?: [IWidget] | [[IWidget]]
// 单元格是否被合并
merged?: boolean
// X轴坐标
axisX?: number
// Y轴坐标
axisY?: number
}
export interface IWidgetJson {
widgetList: IWidget[]
// 页面结构
subDomList: IWidget[]
// 当前选中元素ID
activeId: string
// 当前选中元素名称
activeDomName: string
}

@ -1,16 +1,76 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import melonJson from "./melon.json";
// import melonjson from "./melon.json";
/**
*
* @param widgetJson
*/
export const initJson = (widgetJson: any) => {
widgetJson['widgetList'] = []
widgetJson['subDomList'] = []
widgetJson['activeId'] = ""
widgetJson['activeDomName'] = ""
// widgetJson['formConfig'] = {}
}
export const addItem = (fatherId = "", widgetJson: any) => {
/**
*
* @param addItemDataOrigin
* @param fatherId id
* @param widgetJson wJSON
*/
export const addItem = (addItemDataOrigin: any, fatherId = "", widgetJson: any) => {
const { addItemData, activeId, activeDomName } = formatAddItem(addItemDataOrigin)
if (fatherId) { } else {
widgetJson['widgetList'] = [melonJson]
widgetJson['subDomList'] = [addItemData]
}
}
widgetJson['activeId'] = activeId
widgetJson['activeDomName'] = activeDomName
}
/**
*
* @param addItemDataOrigin
*/
const formatAddItem = (addItemDataOrigin: any) => {
const addItemData = JSON.parse(JSON.stringify(addItemDataOrigin));
let activeId = '';
let activeDomName = '';
addItemData['key'] = Date.now().toString().slice(-6);
addItemData['id'] = `${addItemData['type']}-${addItemData['key']}`;
activeId = addItemData['id']
activeDomName = addItemData['displayName']
addItemData['options'] = {
name: `table-${addItemData['key']}`,
hidden: false,
customClass: ''
};
// 针对表格 需要初始化子单元格
if (addItemData['type'] == 'table') {
const key = addItemData['key'].slice(-1) + addItemData['key'].slice(0, -1);
// 子单元格
const colItem = {
key: key,
id: `table-cell-${key}`,
category: "container",
type: "table-cell",
displayName: "子单元格",
icon: "Monitor",
merged: false,
subDomList: [],
options: {
name: `table-cell-${key}`,
hidden: false,
colspan: 1,
rowspan: 1,
customClass: ''
}
};
addItemData['subDomList'] = [[colItem]]
// activeId = colItem['id']
// activeDomName = colItem['displayName']
}
return {
addItemData,
activeId,
activeDomName
}
}

@ -5,166 +5,155 @@
"type": "table",
"displayName": "表格",
"icon": "Monitor",
"rows": [
{
"cols": [
{
"key": 47005,
"id": "table-cell-12332",
"category": "container",
"type": "table-cell",
"displayName": "子单元格",
"icon": "Monitor",
"merged": false,
"widgetList": [
{
"key": 47016,
"id": "input-12355",
"category": "formItem",
"type": "el-input",
"displayName": "输入框",
"icon": "Folder",
"options": {
"name": "input-12355",
"keyNameEnabled": false,
"keyName": "",
"label": "名称",
"labelAlign": "",
"type": "text",
"defaultValue": "",
"hidden": false,
"customClass": "",
"placeholder": "请输入名称"
}
"subDomList": [
[
{
"key": 47005,
"id": "table-cell-12332",
"category": "container",
"type": "table-cell",
"displayName": "子单元格",
"icon": "Monitor",
"merged": false,
"subDomList": [
{
"key": 47016,
"id": "input-12355",
"category": "formItem",
"type": "el-input",
"displayName": "输入框",
"icon": "Folder",
"options": {
"name": "input-12355",
"keyNameEnabled": false,
"keyName": "",
"label": "名称",
"labelAlign": "",
"defaultValue": "",
"hidden": false,
"customClass": "",
"placeholder": "请输入名称"
}
],
"rows": [],
"options": {
"name": "table-cell-12332",
"cellWidth": "",
"cellHeight": "",
"colspan": 1,
"rowspan": 1,
"wordBreak": false,
"customClass": ""
}
},
{
"key": 47015,
"id": "table-cell-12344",
"category": "container",
"type": "table-cell",
"displayName": "子单元格",
"icon": "Monitor",
"merged": false,
"widgetList": [
{
"key": 47016,
"id": "input-12366",
"category": "formItem",
"type": "el-input-number",
"displayName": "输入框",
"icon": "Folder",
"options": {
"name": "input-12366",
"keyNameEnabled": false,
"keyName": "",
"label": "年龄",
"labelAlign": "",
"type": "number",
"defaultValue": "",
"hidden": false,
"customClass": ""
}
],
"options": {
"name": "table-cell-12332",
"cellWidth": "",
"cellHeight": "",
"colspan": 1,
"rowspan": 1,
"wordBreak": false,
"customClass": ""
}
},
{
"key": 47015,
"id": "table-cell-12344",
"category": "container",
"type": "table-cell",
"displayName": "子单元格",
"icon": "Monitor",
"merged": false,
"subDomList": [
{
"key": 47016,
"id": "input-12366",
"category": "formItem",
"type": "el-input-number",
"displayName": "输入框",
"icon": "Folder",
"options": {
"name": "input-12366",
"keyNameEnabled": false,
"keyName": "",
"label": "年龄",
"labelAlign": "",
"defaultValue": "",
"hidden": false,
"customClass": ""
}
],
"rows": [],
"options": {
"name": "table-cell-12344",
"cellWidth": "",
"cellHeight": "",
"colspan": 1,
"rowspan": 1,
"wordBreak": false,
"customClass": ""
}
],
"options": {
"name": "table-cell-12344",
"cellWidth": "",
"cellHeight": "",
"colspan": 1,
"rowspan": 1,
"wordBreak": false,
"customClass": ""
}
]
},
{
"cols": [
{
"key": 47015,
"id": "table-cell-12344",
"category": "container",
"type": "table-cell",
"displayName": "子单元格",
"icon": "Monitor",
"merged": false,
"widgetList": [
{
"key": 47016,
"id": "radio-12377",
"category": "formItem",
"type": "el-radio",
"displayName": "单选框",
"icon": "Folder",
"options": {
"name": "input-12377",
"keyNameEnabled": false,
"keyName": "",
"label": "性别",
"labelAlign": "",
"type": "text",
"defaultValue": "1",
"hidden": false,
"customClass": "",
"optionItems": [
{
"label": "男",
"value": "1"
},
{
"label": "女",
"value": 2
}
]
}
}
],
[
{
"key": 47015,
"id": "table-cell-12344",
"category": "container",
"type": "table-cell",
"displayName": "子单元格",
"icon": "Monitor",
"merged": false,
"subDomList": [
{
"key": 47016,
"id": "radio-12377",
"category": "formItem",
"type": "el-radio",
"displayName": "单选框",
"icon": "Folder",
"options": {
"name": "input-12377",
"keyNameEnabled": false,
"keyName": "",
"label": "性别",
"labelAlign": "",
"defaultValue": "1",
"hidden": false,
"customClass": "",
"optionItems": [
{
"label": "男",
"value": "1"
},
{
"label": "女",
"value": 2
}
]
}
],
"rows": [],
"options": {
"name": "table-cell-12344",
"cellWidth": "",
"cellHeight": "",
"colspan": 2,
"rowspan": 1,
"wordBreak": false,
"customClass": ""
}
},
{
"key": 47018,
"id": "table-cell-12399",
"category": "container",
"type": "table-cell",
"displayName": "子单元格",
"icon": "Monitor",
"merged": true,
"widgetList": [],
"rows": [],
"options": {
"name": "table-cell-12399",
"cellWidth": "",
"cellHeight": "",
"colspan": 2,
"rowspan": 1,
"wordBreak": false,
"customClass": ""
}
],
"options": {
"name": "table-cell-12344",
"cellWidth": "",
"cellHeight": "",
"colspan": 2,
"rowspan": 1,
"wordBreak": false,
"customClass": ""
}
},
{
"key": 47018,
"id": "table-cell-12399",
"category": "container",
"type": "table-cell",
"displayName": "子单元格",
"icon": "Monitor",
"merged": true,
"widgetList": [],
"options": {
"name": "table-cell-12399",
"cellWidth": "",
"cellHeight": "",
"colspan": 2,
"rowspan": 1,
"wordBreak": false,
"customClass": ""
}
]
}
}
]
],
"options": {
"name": "",

@ -3,23 +3,17 @@ import type { IWidget } from "@/types/widgetsConfigInterface"
export const containers: IWidget[] = [
{
category: 'container',
type: 'el-table',
type: 'table',
displayName: '表格',
icon: 'Monitor',
options: {
name: '',
hidden: false,
}
options: { name: '' }
},
{
category: 'container',
type: 'el-table-column',
type: 'table-cell',
displayName: '子单元格',
icon: 'Monitor',
options: {
name: '',
hidden: false,
}
options: { name: '' }
}
]
@ -30,29 +24,20 @@ export const basicFields: IWidget[] = [
type: 'el-input',
displayName: '输入框',
icon: 'Folder',
options: {
name: '',
hidden: false,
},
options: { name: '' }
},
{
category: 'formItem',
type: 'el-input-number',
displayName: '数值输入框',
icon: 'Folder',
options: {
name: '',
hidden: false,
},
options: { name: '' }
},
{
category: 'formItem',
type: 'el-redio',
displayName: ' 单选项',
icon: 'Folder',
options: {
name: '',
hidden: false,
},
options: { name: '' }
},
]

@ -1,13 +1,13 @@
<script setup lang="ts">
import { provide, ref } from 'vue'
import { provide, reactive } from 'vue'
import { initJson } from '@/utils'
import LeftSide from './subCom/LeftSide.vue'
import NavBar from './subCom/NavBar.vue'
import MainWrapper from './subCom/MainWrapper.vue'
import RightSide from './subCom/RightSide.vue'
const widgetJson = ref({})
initJson(widgetJson['value'])
const widgetJson = reactive({})
initJson(widgetJson)
provide('widgetJson', widgetJson)
</script>

@ -35,9 +35,7 @@ const widgetJson = inject('widgetJson')
<el-collapse-item :title="group['display']" :name="group['activeName']">
<div class="flex flex-wrap justify-between content-center px-2">
<template v-for="item in group['subItem']" :key="item['displayName']">
<div
class="basis-2/5 flex justify-between items-center border border-solid border-[#c1c1c1] px-2 mb-4"
@click="addItem('', widgetJson)">
<div class="itemBox" @click="addItem(item, '', widgetJson)">
<UseIcon :iconName="item['icon']"></UseIcon>
<p>{{ item['displayName'] }}</p>
</div>
@ -60,5 +58,26 @@ section {
padding-left: 8px;
}
}
.itemBox {
flex-basis: 40%;
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid #c1c1c1;
padding: 0 8px;
margin-bottom: 16px;
cursor: pointer;
&:hover {
border-color: var(--el-color-primary);
background-color: #F1F2F3;
:deep(.el-icon) {
color: var(--el-color-primary);
}
}
}
}
</style>

@ -13,12 +13,69 @@ provide('docForm', docForm)
</script>
<template>
<section>
<section class="editWrapper">
<h1 class="text-sky-300">{{ widgetJson }}</h1>
<el-form :model="docForm" label-width="80px">
<template v-for="item in widgetJson['widgetList']" :key="item['id']">
<template v-for="item in widgetJson['subDomList']" :key="item['id']">
<RendererDom :itemData="item" />
</template>
</el-form>
</section>
</template>
<style lang="scss" scoped>
.editWrapper {
--active-color: #409EFF;
:deep(.table-container) {
box-sizing: border-box;
padding: 8px;
border: 1px dashed var(--el-color-primary);
table {
border: none;
td {
border: 1px dashed var(--el-color-primary);
}
}
&.activeDom,
.activeDom {
outline: 2px solid var(--active-color);
padding: 16px;
position: relative;
}
&.activeDomWrapper,
.activeDomWrapper {
position: absolute;
top: 0;
left: 0;
padding: 0px 12px;
background-color: var(--active-color);
color: #fff;
font-size: 13px;
}
&.fnArea,
.fnArea {
position: absolute;
bottom: 0;
right: 0;
padding: 2px 4px;
background-color: var(--active-color);
color: #fff;
font-size: 18px;
display: flex;
align-items: center;
div {
margin: 0 4px;
cursor: pointer;
}
}
}
}
</style>

Loading…
Cancel
Save