多文件编辑支持

This commit is contained in:
2025-03-16 14:04:01 +08:00
parent d156c6a18d
commit 0d493c2c14
8 changed files with 510 additions and 138 deletions

View File

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

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

View File

@ -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');

View File

@ -153,6 +153,18 @@ const state = reactive({
shortDescription: '',
groupInfo: [{}, {}, {}, {}, {}],
details: ''
},{
shortDescription: '',
groupInfo: [{}, {}, {}, {}, {}],
details: ''
},{
shortDescription: '',
groupInfo: [{}, {}, {}, {}, {}],
details: ''
},{
shortDescription: '',
groupInfo: [{}, {}, {}, {}, {}],
details: ''
},
],
groupIndex: 0,

View File

@ -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
View 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;
},
},
});