前言
项目地址
本项目是为开发一套容器化的开发、运行、测试环境,用以支持Web开发、程序设计等课程的实验教学。
任务
添加tab栏,并完成打包发送请求。
添加tab栏
coding页面结构
<template>
<el-header>
<topmenu :activeIndex=activeIndex @on-index-change="onIndexChange"></topmenu>
</el-header>
<el-main v-show="activeIndex == '2'">
<el-row class="tac">
<el-col :span="5">
<side ref="sideRef" @set-file-context="setFileContext" @detele-file="removeTab" @rename="rename" />
</el-col>
<el-col :span="1"></el-col>
<el-col :span="18">
<div v-show="editableTabsValue != '-1'">
<el-tabs v-model="editableTabsValue" type="card" class="demo-tabs" closable @tab-remove="removeTab"
@tab-click="clickTab">
<el-tab-pane v-for="item in editableTabs" :key="item.name" :label="item.title" :name="item.name">
</el-tab-pane>
</el-tabs>
<code-editor-vue ref="cmRef" @on-change="onCodeChange"></code-editor-vue>
</div>
<div v-show="editableTabsValue == '-1'">
<el-result title="open file to edit">
<template #icon>
<el-icon :size="280">
<files />
</el-icon>
</template>
</el-result>
</div>
<div class="grid-content"></div>
<el-divider content-position="center">
<btns @save-z-i-p="saveZIP"></btns>
</el-divider>
<div class="grid-content"></div>
<ResultsDisplay></ResultsDisplay>
</el-col>
</el-row>
</el-main>
<el-main v-show="activeIndex == '3'">
<div class="login-wrap">
<el-form class="login-container">
<el-upload class="upload-demo" drag action="#" multiple accept=".zip" :http-request="handleUpload">
<el-icon class="el-icon--upload">
<upload-filled />
</el-icon>
<div class="el-upload__text">
Drop file here or
<em>click to upload</em>
</div>
<template #tip>
<div class="el-upload__tip">zip files only</div>
</template>
</el-upload>
</el-form>
</div>
</el-main>
</template>
添加、删除、重命名等操作
const addTab = (name: string, id: string) => {
editableTabs.value.push({
title: name,
name: id,
})
editableTabsValue.value = id;
}
const removeTab = (id: string) => {
const tabs = editableTabs.value
let activeName = editableTabsValue.value
if (activeName === id) {
tabs.forEach((tab, index) => {
if (tab.name === id) {
const nextTab = tabs[index + 1] || tabs[index - 1]
if (nextTab) {
activeName = nextTab.name
const code = sideRef.value.getCode(activeName);
cmRef.value.setCode(code.code, code.type);
}
}
})
}
editableTabsValue.value = activeName
editableTabs.value = tabs.filter((tab) => tab.name !== id)
if (editableTabs.value.length == 0)
editableTabsValue.value = '-1';
}
const clickTab = (pane: TabsPaneContext, ev: Event) => {
const code = sideRef.value.getCode(pane.props.name);
cmRef.value.setCode(code.code, code.type);
}
const rename = (id: string, new_name: string) => {
const tabs = editableTabs.value
tabs.forEach((tab, index) => {
if (tab.name === id) {
tab.title = new_name;
}
})
}
打包并完成post发送
const handleUpload = (file: any) => {
let param = new FormData();
param.append('file', file.file);
upload(param);
}
const upload = (param: FormData) => {
request('/weblab/submit/submitByZip', param, lstore.getToken)
.then(res => {
if (res.status == 200 && res.data.msg == 'success') {
console.log(res);
ElMessage({
showClose: true,
message: '上传成功',
type: 'success',
center: true,
grouping: true
})
}
})
.catch(error => {
console.log(error);
})
}
完整代码
fileziper.ts
import JSZip from 'jszip';
import { saveAs } from 'file-saver';
enum FileType {
root,
folder,
ts,
js,
html,
css,
md,
txt,
}
const fileTypes = function (type: string): FileType | undefined {
switch (type) {
case "folder":
return FileType.folder;
case "ts":
return FileType.ts;
case "js":
return FileType.js;
case "html":
return FileType.html;
case "css":
return FileType.css;
case "md":
return FileType.md;
case "txt":
return FileType.txt;
default:
return undefined
}
}
class Ziper {
projName: string = 'web';
zip: any;
file: any;
constructor() {
this.zip = new JSZip();
}
setProjectName(name: string) {
this.projName = name;
}
updateProject(data: any, parent = this.zip) {
for (var item in data) {
if (data[item].type == FileType.folder) {
let pa = this._addFolder(parent, data[item].name)
this.updateProject(data[item].children, pa);
} else {
this._addFile(parent, data[item].name, data[item].value);
}
}
}
async uploadFile(): Promise<any> {
let projName = this.projName + '.zip';
const blob = await this.zip.generateAsync({ type: "blob" })
var file = new File([blob], projName, { type: "zip" });
let param = new FormData();
param.append('file', file);
saveAs(file)
return new Promise((resolve, reject) => {
resolve(param);
});
}
_addFile(parent: any, name: string, value: string) {
parent.file(name, value);
}
_addFolder(parent: any, name: string) {
return parent.folder(name);
}
_saveZIP() {
// this._fromZIP()
let projName = this.projName;
this.zip.generateAsync({ type: "blob" }).bind(this)
.then((blob: any) => {
// saveAs(blob, projName);
// console.log(blob)
this.file = blob;
})
.catch(() => {
console.log("error")
this.file = undefined;
})
return this.file;
}
_fromZIP() {
}
}
const ziper = new Ziper();
export { ziper, FileType, fileTypes };
coding.vue
import { FileType } from "../fileziper";
<!--login-coding-page-->
<template>
<el-header>
<topmenu :activeIndex=activeIndex @on-index-change="onIndexChange"></topmenu>
</el-header>
<el-main v-show="activeIndex == '2'">
<el-row class="tac">
<el-col :span="5">
<side ref="sideRef" @set-file-context="setFileContext" @detele-file="removeTab" @rename="rename" />
</el-col>
<el-col :span="1"></el-col>
<el-col :span="18">
<div v-show="editableTabsValue != '-1'">
<el-tabs v-model="editableTabsValue" type="card" class="demo-tabs" closable @tab-remove="removeTab"
@tab-click="clickTab">
<el-tab-pane v-for="item in editableTabs" :key="item.name" :label="item.title" :name="item.name">
</el-tab-pane>
</el-tabs>
<code-editor-vue ref="cmRef" @on-change="onCodeChange"></code-editor-vue>
</div>
<div v-show="editableTabsValue == '-1'">
<el-result title="open file to edit">
<template #icon>
<el-icon :size="280">
<files />
</el-icon>
</template>
</el-result>
</div>
<div class="grid-content"></div>
<el-divider content-position="center">
<btns @save-z-i-p="saveZIP"></btns>
</el-divider>
<div class="grid-content"></div>
<ResultsDisplay></ResultsDisplay>
</el-col>
</el-row>
</el-main>
<el-main v-show="activeIndex == '3'">
<div class="login-wrap">
<el-form class="login-container">
<el-upload class="upload-demo" drag action="#" multiple accept=".zip" :http-request="handleUpload">
<el-icon class="el-icon--upload">
<upload-filled />
</el-icon>
<div class="el-upload__text">
Drop file here or
<em>click to upload</em>
</div>
<template #tip>
<div class="el-upload__tip">zip files only</div>
</template>
</el-upload>
</el-form>
</div>
</el-main>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import CodeEditorVue from "../components/CodeEditor.vue";
import ResultsDisplay from "../components/ResultsDisplay.vue"
import btns from "../layout/btns.vue"
import topmenu from "../layout/topmenu.vue";
import side from "../layout/sidecolumn.vue"
import { ziper } from "../fileziper"
import type { FileType } from "../fileziper";
import { useLoginStore } from '@/stores/store';
import { Files, UploadFilled } from '@element-plus/icons-vue';
import { request } from "@/network/request";
import { ElMessage, type TabsPaneContext } from "element-plus";
const lstore = useLoginStore();
const sideRef = ref();
const cmRef = ref();
const activeIndex = ref('2');
const editableTabsValue = ref('-1')
interface tab {
title: string,
name: string
}
const editableTabs = ref<tab[]>([])
const onIndexChange = (idx: string) => {
activeIndex.value = idx;
}
const setFileContext = (name: string, id: string, code: string | undefined, type: FileType) => {
cmRef.value.setCode(code, type);
const tabs = editableTabs.value
let flag = false;
tabs.forEach((tab, index) => {
if (tab.name == id) {
editableTabsValue.value = id;
flag = true;
}
})
if (!flag) {
addTab(name, id);
}
}
const onCodeChange = (value: string) => {
sideRef.value.setCode(editableTabsValue.value, value);
}
const saveZIP = async () => {
ziper.setProjectName(sideRef.value.getProjectName());
ziper.updateProject(sideRef.value.getData());
const param: any = await ziper.uploadFile();
if (param != undefined)
upload(param);
}
const handleUpload = (file: any) => {
let param = new FormData();
param.append('file', file.file);
upload(param);
}
const upload = (param: FormData) => {
request('/weblab/submit/submitByZip', param, lstore.getToken)
.then(res => {
if (res.status == 200 && res.data.msg == 'success') {
console.log(res);
ElMessage({
showClose: true,
message: '上传成功',
type: 'success',
center: true,
grouping: true
})
}
})
.catch(error => {
console.log(error);
})
}
const addTab = (name: string, id: string) => {
editableTabs.value.push({
title: name,
name: id,
})
editableTabsValue.value = id;
}
const removeTab = (id: string) => {
const tabs = editableTabs.value
let activeName = editableTabsValue.value
if (activeName === id) {
tabs.forEach((tab, index) => {
if (tab.name === id) {
const nextTab = tabs[index + 1] || tabs[index - 1]
if (nextTab) {
activeName = nextTab.name
const code = sideRef.value.getCode(activeName);
cmRef.value.setCode(code.code, code.type);
}
}
})
}
editableTabsValue.value = activeName
editableTabs.value = tabs.filter((tab) => tab.name !== id)
if (editableTabs.value.length == 0)
editableTabsValue.value = '-1';
}
const clickTab = (pane: TabsPaneContext, ev: Event) => {
const code = sideRef.value.getCode(pane.props.name);
cmRef.value.setCode(code.code, code.type);
}
const rename = (id: string, new_name: string) => {
const tabs = editableTabs.value
tabs.forEach((tab, index) => {
if (tab.name === id) {
tab.title = new_name;
}
})
}
</script>
<style>
.grid-content {
border-radius: 4px;
min-height: 10px;
}
.demo-tabs>.el-tabs__content {
padding: 0px;
color: #6b778c;
font-size: 32px;
font-weight: 600;
}
</style>
菜单栏页面:
<template>
<div class="custom-tree-container">
<el-menu default-active="2" class="el-menu-vertical-demo">
<el-input v-model="query" placeholder="Please enter keyword" style="width: 220px" />
<el-tree ref="treeRef" :data="dataSource" :props="props" :filter-node-method="filterMethod" :height="720"
node-key="id" default-expand-all @node-contextmenu="RightClick" @node-click="LeftClick" highlight-current>
<template #default="{ node, data }">
<el-icon>
<folder v-if="data.type == FileType.folder || data.type == FileType.root"></folder>
<document v-else></document>
</el-icon>
<span>{{ node.label }}</span>
</template>
</el-tree>
<el-card v-show="visible" id="menu" shadow="always"
:style="{ position: 'fixed', left: mouseX + 'px', top: mouseY + 'px', zIndex: '999', cursor: 'pointer' }">
<div>
<el-button type="text" @click="AddFoloder()"
v-show="targetData?.type === FileType.folder || targetData?.type === FileType.root">add new folder
</el-button>
</div>
<div>
<el-button type="text" @click="AddFile()"
v-show="targetData?.type === FileType.folder || targetData?.type === FileType.root">add new file
</el-button>
</div>
<div>
<el-button type="text" @click="Delete()" v-show="targetData?.type != FileType.root">delete</el-button>
</div>
<div>
<el-button type="text" @click="Rename()">rename</el-button>
</div>
</el-card>
</el-menu>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import type { ElTree } from 'element-plus'
import type Node from 'element-plus/es/components/tree/src/model/node'
import { ElMessage, ElMessageBox } from 'element-plus'
import { fileTypes, FileType } from '../fileziper';
import { Folder, Document } from '@element-plus/icons-vue';
interface Tree {
id: string
name: string
type: FileType
children: Tree[]
value?: string
}
const visible = ref(false)
const query = ref('')
const treeRef = ref<InstanceType<typeof ElTree>>()
const targetData = ref<Tree>()
const targetNode = ref<Node>()
// const selectedData = ref<Tree>()
const totalId = ref(0);
const mouseX = ref(0)
const mouseY = ref(0)
const props = {
value: 'id',
label: 'name',
children: 'children',
}
const dataSource = ref<Tree[]>
([
{
id: `${totalId.value++}`, name: 'Web', type: FileType.root, children:
[
{
id: `${totalId.value++}`, name: 'src', type: FileType.folder, children:
[
{
id: `${totalId.value++}`, name: 'main.js', type: FileType.js, children: [], value: ''
},
{
id: `${totalId.value++}`, name: 'main.html', type: FileType.html, children: [], value: ''
},
{
id: `${totalId.value++}`, name: 'main.css', type: FileType.css, children: [], value: ''
},
{
id: `${totalId.value++}`, name: 'README.md', type: FileType.md, children: [], value: ''
}
]
},
{
id: `${totalId.value++}`, name: 'public', type: FileType.folder, children:
[
{
id: `${totalId.value++}`, name: 'README.md', type: FileType.md, children: [], value: ''
}
]
}
]
}
])
watch(query, (val) => {
treeRef.value!.filter(val)
})
const filterMethod = (query: string, data: Tree) => {
if (!query) return true;
return data.name!.includes(query)
}
const RightClick = function (e: PointerEvent, data: Tree, node: Node) {
visible.value = true;
targetData.value = data;
targetNode.value = node;
mouseX.value = e.clientX;
mouseY.value = e.clientY;
document.addEventListener('click', cancelRightClick)
}
const LeftClick = function (data: Tree) {
visible.value = false;
if (data.type != FileType.folder && data.type != FileType.root) {
// selectedData.value = data;
emit("setFileContext", data.name, data.id, data.value, data.type);
} else {
// selectedData.value = undefined;
}
}
const cancelRightClick = function () {
visible.value = false;
document.removeEventListener('click', cancelRightClick);
}
const AddFile = () => {
ElMessageBox.prompt('Please input folder of file(name.type)', 'Add new file', {
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
inputPattern:
/[\w!#$%&'*+/=?^_`{|}~-]+(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?/,
inputErrorMessage: 'Please input file(name.type)',
})
.then(({ value }) => {
const val = value.split(".");
let type = val[val.length - 1];
let fileType = fileTypes(type);
if (fileType != undefined) {
if (targetData.value != null) {
for (let i = 0; i < targetData.value.children.length; i++) {
if (fileType == targetData.value.children[i].type && value == targetData.value.children[i].name)
throw false;
}
}
targetData.value!.children.push({
id: `${totalId.value++}`,
name: value,
type: fileType,
children: [],
value: ''
})
dataSource.value = [...dataSource.value]
} else {
ElMessage({
type: 'error',
message: `not support this type`,
})
}
// ElMessage({
// type: 'success',
// message: `Your file is:${value}`,
// })
})
.catch(() => {
ElMessage({
type: 'error',
message: "file already exist",
})
})
}
const AddFoloder = () => {
ElMessageBox.prompt('Please input folder', 'Add new folder', {
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
inputPattern:
/[\w!#$%&'*+/=?^_`{|}~-]?/,
inputErrorMessage: 'Please input folder',
})
.then(({ value }) => {
if (targetData.value != null) {
for (let i = 0; i < targetData.value.children.length; i++) {
if (targetData.value.children[i].type == FileType.folder && value == targetData.value.children[i].name)
throw false;
}
}
targetData.value!.children.push({
id: `${totalId.value++}`,
name: value,
type: FileType.folder,
children: []
})
dataSource.value = [...dataSource.value]
// ElMessage({
// type: 'success',
// message: `Your folder is:${value}`,
// })
})
.catch(() => {
ElMessage({
type: 'error',
message: "folder already exist",
})
})
}
const Delete = function () {
ElMessageBox.confirm('You will delete the file,continue?', 'Warning',
{
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning',
})
.then(() => {
if(targetData.value?.type==FileType.folder){
deleteFile(targetData.value.children);
}else{
emit('deteleFile',targetData.value?.id);
}
const parent = targetNode.value?.parent
const children: Tree[] = parent?.data.children || parent?.data
const index = children.findIndex((d) => d.id === targetData.value?.id)
children.splice(index, 1)
dataSource.value = [...dataSource.value]
// selectedData.value = undefined;
})
.catch(() => {
})
}
const deleteFile=(data:Tree[])=>{
for(var item in data){
if(data[item].type==FileType.folder){
deleteFile(data[item].children);
}else{
emit('deteleFile',data[item].id);
}
}
}
const Rename = function () {
const isFolder = targetData.value?.type === FileType.folder || targetData.value?.type === FileType.root;
ElMessageBox.prompt('Please input new name', 'Rename', {
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
inputPattern: isFolder
? /[\w!#$%&'*+/=?^_`{|}~-]?/ : /[\w!#$%&'*+/=?^_`{|}~-]+(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?/,
inputErrorMessage: 'Please input correct name',
})
.then(({ value }) => {
let fileType;
const pa = targetNode.value?.parent.data.children;
if (!isFolder) {
const val = value.split(".");
let type = val[val.length - 1];
fileType = fileTypes(type);
for (let i = 0; i < pa.length; i++) {
if (value == pa[i].name && fileType == pa[i].type)
throw 'file';
}
} else {
for (let i = 0; i < pa.length; i++) {
if (pa[i].type == FileType.folder && value == pa[i].name)
throw 'folder';
}
fileType = FileType.folder;
}
if (fileType != undefined) {
emit('rename',targetData.value?.id,value);
targetData.value!.name = value;
dataSource.value = [...dataSource.value]
} else {
ElMessage({
type: 'error',
message: `not support this type`,
})
}
// ElMessage({
// type: 'success',
// message: `Your folder is:${value}`,
// })
})
.catch((e) => {
ElMessage({
type: 'error',
message: `${e} already exist`,
})
})
}
const emit = defineEmits(['setFileContext','deteleFile','rename']);
const setCode = (id:string,value: string) => {
// if (selectedData.value != null) {
// selectedData.value.value = value;
// }
const node=treeRef.value?.getNode(id);
if(node){
node.data.value=value;
}
}
const getCode=(id:string)=>{
const node=treeRef.value?.getNode(id);
return {"code":node?.data.value,"type":node?.data.type};
}
const getData = () => {
return dataSource.value[0].children;
}
const getProjectName = () => {
return dataSource.value[0].name;
}
defineExpose({ setCode, getData, getProjectName ,getCode})
</script>
<style>
.dialog-footer button:first-child {
margin-right: 10px;
}
</style>