山东大学软件学院项目实训weblab-10

本文档详述了一套用于Web开发和程序设计课程的容器化开发环境的搭建,包括添加Tab栏功能,实现文件的添加、删除和重命名。此外,还展示了如何打包文件并使用POST发送请求进行上传。代码示例涵盖了Vue组件的使用,如Element UI的Tabs和Upload组件,以及文件操作相关的功能实现。
摘要由CSDN通过智能技术生成

前言

项目地址
本项目是为开发一套容器化的开发、运行、测试环境,用以支持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>

完整效果

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值