mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2025-07-08 05:11:52 +00:00
多文件编辑支持
This commit is contained in:
145
src/App.vue
145
src/App.vue
@ -1,9 +1,25 @@
|
||||
<script setup>
|
||||
import Yys from './components/Yys.vue'
|
||||
import Toolbar from './components/Toolbar.vue'
|
||||
import {ref} from "vue";
|
||||
<script setup lang="ts">
|
||||
import Yys from './components/Yys.vue';
|
||||
import Toolbar from './components/Toolbar.vue';
|
||||
import ProjectExplorer from './components/ProjectExplorer.vue';
|
||||
import { computed, ref, onMounted, onUnmounted } from "vue";
|
||||
import { useFilesStore } from "@/stores/files";
|
||||
import Vue3DraggableResizable from 'vue3-draggable-resizable';
|
||||
import {TabPaneName, TabsPaneContext} from "element-plus";
|
||||
|
||||
const filesStore = useFilesStore();
|
||||
|
||||
const yysRef = ref(null);
|
||||
const width = ref('100%');
|
||||
const height = ref('100vh');
|
||||
const toolbarHeight = 48; // 工具栏的高度
|
||||
const windowHeight = ref(window.innerHeight);
|
||||
const contentHeight = computed(() => `${windowHeight.value - toolbarHeight}px`);
|
||||
|
||||
const onResizing = (x, y, width, height) => {
|
||||
width.value = width;
|
||||
height.value = height;
|
||||
};
|
||||
|
||||
const onHandleInport = (file) => {
|
||||
yysRef.value.importGroups(file);
|
||||
@ -12,29 +28,118 @@ const onHandleInport = (file) => {
|
||||
const onHandleExport = () => {
|
||||
yysRef.value.exportGroups();
|
||||
};
|
||||
|
||||
const element = ref({
|
||||
x: 400,
|
||||
y: 20,
|
||||
width: 1080,
|
||||
height: windowHeight.value - toolbarHeight,
|
||||
isActive: false,
|
||||
});
|
||||
|
||||
const handleFileSelected = (fileId) => {
|
||||
filesStore.setActiveFile(fileId);
|
||||
};
|
||||
|
||||
const handleTabsEdit = (
|
||||
targetName: TabPaneName | undefined,
|
||||
action: 'remove' | 'add'
|
||||
)=> {
|
||||
const tabIndex = filesStore.fileList.findIndex(file => file.name === parseInt(name.toString()));
|
||||
if (tabIndex !== -1) {
|
||||
filesStore.fileList.splice(tabIndex, 1);
|
||||
if (filesStore.fileList.length > 0) {
|
||||
filesStore.setActiveFile(filesStore.fileList[0].name);
|
||||
} else {
|
||||
filesStore.setActiveFile(-1); // 或者其他适当的值表示没有活动文件
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', () => {
|
||||
windowHeight.value = window.innerHeight;
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', () => {
|
||||
windowHeight.value = window.innerHeight;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main id="main-container">
|
||||
<!-- 添加工具栏 -->
|
||||
<Toolbar title="yys-editor" username="示例用户" data-html2canvas-ignore="true" @handleExport="onHandleExport" @handleImport="onHandleInport"/>
|
||||
<!-- 添加 Watermark 组件 -->
|
||||
<Yys ref="yysRef"/>
|
||||
<div class="container">
|
||||
<!-- 工具栏 -->
|
||||
<Toolbar title="yys-editor" username="示例用户" @handleExport="onHandleExport" @handleImport="onHandleInport" />
|
||||
<!-- 侧边栏和工作区 -->
|
||||
<div class="main-content">
|
||||
<!-- 侧边栏 -->
|
||||
<aside class="sidebar">
|
||||
<ProjectExplorer :files="filesStore.fileList" @file-selected="handleFileSelected" />
|
||||
</aside>
|
||||
|
||||
</main>
|
||||
<!-- 工作区 -->
|
||||
<div class="workspace">
|
||||
<main id="main-container" :style="{ height: contentHeight, overflow: 'auto' }">
|
||||
<el-tabs
|
||||
v-model="filesStore.activeFile"
|
||||
type="card"
|
||||
class="demo-tabs"
|
||||
editable
|
||||
@edit="handleTabsEdit"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="(file, index) in filesStore.fileList"
|
||||
:key="index"
|
||||
:label="file.label"
|
||||
:name="file.name.toString()"
|
||||
>
|
||||
<Yys ref="yysRef" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 确保 #main-container 具有相对定位 */
|
||||
#main-container {
|
||||
margin-top: 48px; /* 与工具栏高度相同 */
|
||||
position: relative;
|
||||
|
||||
min-height: 100vh; /* 允许容器扩展 */
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 如果 Yys 组件需要特定的高度或布局,可以根据需要调整 */
|
||||
.toolbar {
|
||||
height: 48px; /* 与 toolbarHeight 一致 */
|
||||
flex-shrink: 0; /* 防止 Toolbar 被压缩 */
|
||||
background-color: #fff; /* 添加背景色以便观察 */
|
||||
z-index: 1; /* 确保 Toolbar 在上层 */
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: flex;
|
||||
flex: 1; /* 占据剩余空间 */
|
||||
overflow: hidden; /* 防止内容溢出 */
|
||||
margin-top: 48px; /* 确保 main-content 在 Toolbar 下方 */
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 200px; /* 侧边栏宽度 */
|
||||
background-color: #f0f0f0; /* 背景色 */
|
||||
flex-shrink: 0; /* 防止侧边栏被压缩 */
|
||||
overflow-y: auto; /* 允许侧边栏内容滚动 */
|
||||
}
|
||||
|
||||
.workspace {
|
||||
flex: 1; /* 占据剩余空间 */
|
||||
overflow: hidden; /* 防止内容溢出 */
|
||||
}
|
||||
|
||||
#main-container {
|
||||
position: relative;
|
||||
height: 100%; /* 确保内容区域占满父容器 */
|
||||
overflow-y: auto; /* 允许内容滚动 */
|
||||
}
|
||||
</style>
|
38
src/components/ProjectExplorer.vue
Normal file
38
src/components/ProjectExplorer.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="project-explorer">
|
||||
<el-tree
|
||||
:data="files"
|
||||
:props="defaultProps"
|
||||
@node-click="handleNodeClick"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
files: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['file-selected']);
|
||||
|
||||
const defaultProps = {
|
||||
children: 'children',
|
||||
label: 'label',
|
||||
};
|
||||
|
||||
const handleNodeClick = (data) => {
|
||||
emit('file-selected', data.name);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.project-explorer {
|
||||
height: 50%; /* 占据侧边栏的一半 */
|
||||
overflow-y: auto; /* 允许内容滚动 */
|
||||
}
|
||||
</style>
|
@ -152,7 +152,7 @@ function calculateVisualHeight(selector) {
|
||||
|
||||
|
||||
const ignoreElements = (element) => {
|
||||
return element.classList.contains('ql-toolbar');
|
||||
return element.classList.contains('ql-toolbar') || element.classList.contains('el-tabs__header');
|
||||
};
|
||||
|
||||
const prepareCapture = async () => {
|
||||
@ -163,19 +163,34 @@ const prepareCapture = async () => {
|
||||
style.textContent = `.ql-container.ql-snow { border: none !important; }`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// 获取目标元素
|
||||
const element = document.querySelector('#main-container');
|
||||
if (!element) {
|
||||
console.error('Element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存原始 overflow 样式
|
||||
const originalOverflow = element.style.overflow;
|
||||
|
||||
try {
|
||||
const element = document.querySelector('#main-container');
|
||||
// 临时隐藏 overflow 样式
|
||||
element.style.overflow = 'visible';
|
||||
|
||||
// 计算需要忽略的元素高度
|
||||
let totalHeight = calculateVisualHeight('[data-html2canvas-ignore]') + calculateVisualHeight('.ql-toolbar');
|
||||
console.log('所有携带指定属性的元素高度之和:', totalHeight);
|
||||
if (!element) {
|
||||
console.error('Element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('主元素宽度', element.scrollWidth);
|
||||
console.log('主元素高度', element.scrollHeight);
|
||||
|
||||
// 1. 生成原始截图
|
||||
const canvas = await html2canvas(element, {
|
||||
ignoreElements: ignoreElements,
|
||||
height: element.scrollHeight - totalHeight
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
width: element.scrollWidth,
|
||||
height: element.scrollHeight - totalHeight,
|
||||
});
|
||||
|
||||
// 2. 创建新Canvas添加水印
|
||||
@ -220,16 +235,19 @@ const prepareCapture = async () => {
|
||||
}
|
||||
|
||||
ctx.restore(); // 恢复原始状态
|
||||
|
||||
// 3. 存储带水印的图片
|
||||
state.previewImage = watermarkedCanvas.toDataURL();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Capture failed', error);
|
||||
} finally {
|
||||
// 恢复原始 overflow 样式
|
||||
element.style.overflow = originalOverflow;
|
||||
|
||||
// 移除临时样式
|
||||
document.head.removeChild(style);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadImage = () => {
|
||||
if (state.previewImage) {
|
||||
const link = document.createElement('a');
|
||||
|
@ -153,6 +153,18 @@ const state = reactive({
|
||||
shortDescription: '',
|
||||
groupInfo: [{}, {}, {}, {}, {}],
|
||||
details: ''
|
||||
},{
|
||||
shortDescription: '',
|
||||
groupInfo: [{}, {}, {}, {}, {}],
|
||||
details: ''
|
||||
},{
|
||||
shortDescription: '',
|
||||
groupInfo: [{}, {}, {}, {}, {}],
|
||||
details: ''
|
||||
},{
|
||||
shortDescription: '',
|
||||
groupInfo: [{}, {}, {}, {}, {}],
|
||||
details: ''
|
||||
},
|
||||
],
|
||||
groupIndex: 0,
|
||||
|
20
src/main.js
20
src/main.js
@ -3,22 +3,26 @@ import App from './App.vue'
|
||||
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
import Vue3DraggableResizable from 'vue3-draggable-resizable'
|
||||
// default styles
|
||||
import 'vue3-draggable-resizable/dist/Vue3DraggableResizable.css'
|
||||
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
// 引入语言文件
|
||||
import zh from './locales/zh.json'
|
||||
import ja from './locales/ja.json'
|
||||
|
||||
import { createPinia } from 'pinia' // 导入 Pinia
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
|
||||
// 获取用户的首选语言
|
||||
const userLanguage = navigator.language
|
||||
|
||||
@ -40,10 +44,10 @@ const i18n = createI18n({
|
||||
},
|
||||
})
|
||||
|
||||
app.use(i18n)
|
||||
app.use(ElementPlus)
|
||||
app.mount('#app')
|
||||
|
||||
|
||||
|
||||
const pinia = createPinia() // 创建 Pinia 实例
|
||||
|
||||
app.use(pinia) // 使用 Pinia
|
||||
.use(i18n)
|
||||
.use(ElementPlus)
|
||||
.use(Vue3DraggableResizable)
|
||||
.mount('#app')
|
16
src/stores/files.ts
Normal file
16
src/stores/files.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useFilesStore = defineStore('files', {
|
||||
state: () => ({
|
||||
fileList: [{ label: 'File 1', name: 1 },{ label: 'File 2', name: 2 }],
|
||||
activeFile: 1,
|
||||
}),
|
||||
actions: {
|
||||
addFile(file: { label: string; name: number }) {
|
||||
this.fileList.push(file);
|
||||
},
|
||||
setActiveFile(fileId: number) {
|
||||
this.activeFile = fileId;
|
||||
},
|
||||
},
|
||||
});
|
Reference in New Issue
Block a user