自动生成md文件以及config.mjs文件-vitepress

效果:

config.mjs文件

javascript 复制代码
import {defineConfig} from 'vitepress'
import hljs from 'highlight.js/lib/core'
import javascript from 'highlight.js/lib/languages/javascript'
import xml from 'highlight.js/lib/languages/xml'
import {ref} from "./cache/deps/vue.js";
// 注册语言
hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('html', xml)

export default defineConfig(async () => {
  return {
    markdown: {
      theme: 'material-theme-palenight',
      lineNumbers: true,
      math: true,
      container: {
        tipLabel: '提示',
        warningLabel: '警告',
        dangerLabel: '危险',
        infoLabel: "信息",
        detailsLabel: "详情"
      },

    },
    enhanceApp: {
      setup: (ctx) => {
        ctx.app.component('AdBanner', AdBanner)
      }
    },

    title: "平台基础管理系统PBM帮助文档",
    description: "系统使用说明",
    themeConfig: {
      outline: {
        level: [2, 3],
        label: '页面导航'
      },
      siteTitle: "帮助文档",
      logo: "../pubilc/assets/logo.png",
      nav: [
        {
          text: '首页',
          link: '/'
        },
        {
          text: '数据采集',
          items: [
            {
              text: '直报',
              link: 'DFS'
            },
            {
              text: '爬虫',
              link: 'DAS'
            },
            {
              text: '爬虫2',
              link: 'das2'
            },
            {
              text: '爬虫2',
              link: 'das2'
            },
          ]
        },
      ]
    ,
sidebar: {
      'DFS': {
        items: [
          {
            text: '表单管理平行测试',
            link: 'null'
          }
        ]
      },
      'formManage': {
        items: [
          {
            text: '表单管理',
            collapsible: true,
            collapsed: false,
            items: [
              {
                text: '表单管理文档',
                link: 'testMD'
              },
              {
                text: '表单路径测试',
                link: 'testForm'
              },
              {
                text: '测试文档2',
                link: 'test2'
              },
              {
                text: '测试3',
                link: 'test3'
              }
            ]
          }
        ]
      },
    },
      footer: {
        message: "平台基础管理系统PBM帮助文档",
        copyright: '2025.5.20   @langtao'
      },
      styl: {

      }

    }
  }
})

前端代码:

列表:

javascript 复制代码
<template>
  <div class="app-container" style="width:100%;height:100%;">
    <el-aside style="padding-right: 10px; padding-left: 15px; padding-top: 0px;float:left;width:20%;background:none;">
      <div class="block" style="width:100%;height:100%;">
        <!-- <el-select v-model="xiangmuId" class="filter-item" placeholder="项目" style="width: 100%" clearable @change="handChange">
          <el-option v-for="item in xmList" :key="item.id" :label="item.xiangmumingcheng" :value="item.id" />
        </el-select> -->
        <el-cascader ref="cascaderHandle" v-model="xiangmuId" style="width:100%;" clearable :options="xmList" :props="optionProps" :show-all-levels="false" @change="change">
          <template slot-scope="{ data }">
            <span @click="clickNode">{{ data.xiangmumingcheng }}</span>
          </template>
        </el-cascader>
        <el-tree
          id="el-tree"
          ref="tree"
          class="filter-tree"
          :data="data2"
          node-key="id"
          :highlight-current="true"
          :props="defaultProps"
          :default-expanded-keys="TreeArr"
          :expand-on-click-node="false"
          @node-click="handleNodeClick"
          @node-contextmenu="rightClick"
        />
        <div v-show="menu1Visible">
            <ul id="menu1" class="menu1" style="font-size:14px;">
              <li class="menu_item" @click="handleAdd">新增下级目录</li>
              <li class="menu_item" @click="download()">生成md文件</li>

            </ul>
          </div>
        <div v-show="menu2Visible">
            <ul id="menu2" class="menu2" style="font-size:14px;">
              <li class="menu_item" @click="handleAdd">新增下级目录</li>
            </ul>
          </div>
        <div v-show="menu3Visible">
            <ul id="menu3" class="menu3" style="font-size:14px;">
              <li class="menu_item" @click="handleAdd">新增下级目录</li>
              <li class="menu_item" @click="handleEdit">编辑目录</li>
              <li class="menu_item" @click="add()">新增文档</li>
              <li class="menu_item" @click="handleDisable">删除目录</li>
              
            </ul>
          </div>
          <div v-show="menu4Visible">
            <ul id="menu4" class="menu4" style="font-size:14px;">
              <li class="menu_item" @click="handleEdit">编辑目录</li>
              <li class="menu_item" @click="add()">新增文档</li>
              <li class="menu_item" @click="handleDisable">删除目录</li>
            </ul>
          </div>
        
          <div v-show="menu5Visible">
            <ul id="menu5" class="menu5" style="font-size:14px;">
              <li class="menu_item" @click="edit()">编辑文档</li>
              <li class="menu_item" @click="remove()">删除文档</li>  
            </ul>
          </div>
      </div>
    </el-aside>
    <el-aside style="padding-right: 5px; padding-left: 5px; padding-top: 0px;float:left;width:80%;background:none;">
      <div class="filter-container" style="text-align:center;margin-top:0px;padding-bottom: 0px;">
        <el-button class="filter-item" icon="el-icon-s-data" type="info" @click="history()">文档历史版本</el-button>
        <el-button class="filter-item" icon="el-icon-warning-outline" type="primary" @click="yulan()">预览</el-button>
      </div>

      <el-table v-show="displayVisible" v-loading="listLoading" :data="numList" :height="tableHeight4" style="width: 100%; overflow:auto;" border>
        <el-table-column type="index" header-align="center" align="center" label="序号" width="80px" />
        <el-table-column header-align="center" align="left" label="操作" width="320px">
          <template slot-scope="scope">
            <el-button type="text" size="small" @click="hlfp(scope)">查看</el-button>
          </template>
        </el-table-column>
        <el-table-column header-align="center" align="left" label="项目名称">
          <template slot-scope="scope">
            <span>{{ scope.row.xmName }}</span>
          </template>
        </el-table-column>
        <el-table-column header-align="center" align="right" label="文档数量">
          <template slot-scope="scope">
            <span>{{ scope.row.num }}</span>
          </template>
        </el-table-column>
      </el-table>

      <div v-show="displayVisible1" style="width:100%; margin:0 auto;">
        <div>
          <table class="table">
            <tr>
              <td class="title-name">
                <strong>项目:</strong>
              </td>
              <td class="table-content">
                <span>{{ from.xmName }}</span>
              </td>
              <td class="title-name">
                <strong>文档名称:</strong>
              </td>
              <td class="table-content">
                <span>{{ from.tableName }}</span>
              </td>
            </tr>
            <tr>
              <td class="title-name">
                <strong>备注:</strong>
              </td>
              <td colspan="3" style="100%">
                <span>{{ from.tableDetailExplain }}</span>
              </td>
            </tr>
            <tr>
              <td class="title-name">
                <strong>内容:</strong>
              </td>
            </tr>
            <tr>
              <td colspan="4" style="98%">
                <span v-html="from.htmlData" />
              </td>
            </tr>
          </table>
        </div>
      </div>

    
  
      <el-dialog :visible.sync="dialogAddVisible" width="600px" :show-close="false" append-to-body :title="'新增下级目录'">
          <dialog-add v-if="dialogAddVisible" ref="dialogAdd" :xiangmu="xiangmuId" :classify="classifyId" :level="treeLevel" :visible.sync="dialogAddVisible" />
        </el-dialog>

        <el-dialog :visible.sync="dialogEditVisible" width="600px" :show-close="false" append-to-body :title="'编辑目录'">
          <dialog-edit v-if="dialogEditVisible" ref="dialogEdit" :proid="code" :level="treeLevel" :visible.sync="dialogEditVisible" />
        </el-dialog>

      <el-dialog :visible.sync="addApiVisible" width="1200px" append-to-body :show-close="false" :title="'新增文档'">
        <add-api v-if="addApiVisible" ref="addApi" :classify="classifyId" :xiangmu="xiangmuId" :visible.sync="addApiVisible" />
      </el-dialog>

      <el-dialog :visible.sync="editApiVisible" width="1200px" append-to-body :show-close="false" :title="'编辑文档'">
        <edit-api v-if="editApiVisible" ref="editApi" :proid="classifyId" :visible.sync="editApiVisible" />
      </el-dialog>

      <el-dialog :visible.sync="historyVisible" width="1000px" append-to-body :title="'文档历史记录'">
        <history v-if="historyVisible" ref="history" :history="batchCode" :visible.sync="historyVisible" />
      </el-dialog>

      <el-dialog :visible.sync="downloadVisible" width="400px" append-to-body :show-close="false" :title="'生成MD文件'">
        <download v-if="downloadVisible" ref="download" :xiangmu="xiangmuId" :classify="classifyId" :level="treeLevel2" :visible.sync="downloadVisible" />
      </el-dialog>

      <el-dialog :visible.sync="yulanVisible" width="1200px" append-to-body :title="'预览'">
        <yulan v-if="yulanVisible" ref="yulan" :proid="xiangmuId" :visible.sync="yulanVisible" />
      </el-dialog>

    </el-aside>
  </div>
</template>

<script>
import { getXiangMuList } from '@/api/projectInfo'
import { getInfo, getFileList, remove } from '@/api/usersManualFile'
import { getInitialLoading, getAgainLoading, makeMD } from '@/api/configMenu'
import { disabled} from '@/api/usersManualCatalog'
import { getDictListByCode } from '@/api/dict'
import { Message, MessageBox } from 'element-ui'
import { tableHeight4 } from '@/utils/tableHeight'
import DialogAdd from './add' // eslint-disable-line no-unused-vars
import DialogEdit from './edit' // eslint-disable-line no-unused-vars
import AddApi from '../manual/addApi' // eslint-disable-line no-unused-vars
import EditApi from '../manual/editApi' // eslint-disable-line no-unused-vars
import history from '../manual/history' // eslint-disable-line no-unused-vars
import download from '../manual/downloadByXm' // eslint-disable-line no-unused-vars
import yulan from '../manual/yulan' // eslint-disable-line no-unused-vars

export default {
  name: 'Zzjg',
  components: { AddApi, EditApi,history, download, yulan, DialogAdd, DialogEdit, },
  mixins: [tableHeight4],
  provide() {
    return {
      getTreeList: this.getTreeList
    }
  },
  data() {
    return {
      TreeArr: [],
      optionProps: {
        value: 'id',
        children: 'children',
        label: 'xiangmumingcheng',
        checkStrictly: false,
        expandTrigger: 'hover'
      },
      IdArr: [],
      displayVisible: false,
      displayVisible1: false,
      listLoading: false,
      total: 0,
      xiangmuId: '',
      numList: [],
      xmList: [],
      userList: [],
      classifyId: '',
      batchCode: '',
      flag: '',
      DATA: null,
      NODE: null,
      objectID: null,
      menu1Visible: false,
      menu2Visible: false,
      menu3Visible: false,
      menu4Visible: false,
      menu4Visible2: false,
      menu5Visible:false,
      treeLevel:'',
      treeLevel2: '',
      downloadId: '',
      dialogAddVisible: false,
      dialogEditVisible: false,
      addApiVisible: false,
      editApiVisible: false,
      historyVisible: false,
      downloadVisible: false,
      yulanVisible: false,
      stateOptions: [],
      data: [],
      data2: [],
      from: {},
      queryPage: {},
      datas: '',
      img1: require('../../../assets/images/Folder-01.png'),
      defaultProps: {
        children: 'children',
        label: 'menuName'
      }
    }
  },
  created() {
    this.getList()
    this.getXiangMuList()
    this.getDictList('YW_BASE_STATUS')
  },
  methods: {
    getList() {
      this.displayVisible = true
      this.displayVisible1 = false
      getFileList().then(response => {
        this.numList = response.data
        this.listLoading = false
      }).catch(response => {
        this.listLoading = false
      })
    },
    // 提示框
    renderContent: function(h, { node, data, store }) {
      var text = data.name
      if (data.flag === 'ML') {
        if (text.length > 15) {
          return (
            < span >
              < i > <img src={this.img1} style='width: 18px;height: 18px;margin-right:5px;padding-top:1px' /></i>
              <el-tooltip class='item' id='tool' effect='light' popper-class='draw' visible-arrow='false' content={data.name} placement='bottom-start' >
                <span class='style-demo' >{data.name}</span >
              </el-tooltip>
            </span>)
        } else {
          return (
            < span >
              < i > <img src={this.img1} style='width: 18px;height: 18px;margin-right:5px;padding-top:1px' /></i>
              <span class='style-demo' >{data.name}</span >
            </span>
          )
        }
      } else {
        if (text.length > 15) {
          return (
            < span >
              <el-tooltip class='item' id='tool' effect='light' popper-class='draw' visible-arrow='false' content={data.name} placement='bottom-start' >
                <span class='style-demo' >{data.name}</span >
              </el-tooltip>
            </span>)
        } else {
          return (
            < span >
              <span class='style-demo' >{data.name}</span >
            </span>
          )
        }
      }
    },
  
    hlfp(scope) {
      this.displayVisible = false
      this.displayVisible1 = true
      this.xiangmuId = scope.row.xmId

      // 加载列表
      getInitialLoading({ xmId: scope.row.projectId }).then(response => {
        this.data = response.data

        this.data2 = this.buildTree2(this.data)
        this.data2.forEach(m => {
          this.TreeArr.push(m.id)
        })
        this.listLoading = false
      }).catch(response => {
        this.listLoading = false
      })
    },
    getXiangMuList() { // 加载列表
      this.xmList = []

      getXiangMuList(this.queryPage).then(response => {
        this.xmList = this.buildTree(response.data)

        this.listLoading = false
      }).catch(response => {
        this.listLoading = false
      })
    },
    buildTree(data) {
      var rdata = []
      for (let i = 0; i < data.length; i++) {
        var e1 = data[i]
        if (e1.parentId === '-1') {
          rdata.push(e1)
        }
        for (let j = 0; j < data.length; j++) {
          var e2 = data[j]
          if (e1.parentId === e2.id) {
            if (!e2.children) {
              e2.children = []
            }
            e2.children.push(e1)
          }
        }
      }
      return rdata
    },
    clickNode($event) {
      $event.target.parentElement.parentElement.firstElementChild.click()
      this.$refs.cascaderHandle.dropDownVisible = false
    },
    change(val) {
      const nodesObj = this.$refs['cascaderHandle'].getCheckedNodes()
      this.data = []
      this.data2 = []
      if (val.length === 2) {
        this.displayVisible = false
        this.xiangmuId = nodesObj[0].data.id
        this.getTreeList(nodesObj[0].data.id)
        this.displayVisible1 = true
      } else {
        this.displayVisible = true
        this.displayVisible1 = false
      }
    },
    // handChange(e) {
    //   this.displayVisible = false
    //   this.data = []
    //   this.data2 = []
    //   if (e !== null && e !== '' && e !== 'null') {
    //     this.getTreeList(e)
    //     this.displayVisible1 = true
    //   }
    // },
    getTreeList(code) { // 加载列表
      // 加载列表
      getInitialLoading({ xmId: code }).then(response => {
        this.data = response.data
        console.log(code,'123213')
        this.data2 = this.buildTree2(this.data)
        this.data2.forEach(m => {
          this.TreeArr.push(m.id)
        })
        this.listLoading = false
      }).catch(response => {
        this.listLoading = false
      })
    },
    
    buildTree2(data) {
      var rdata = []
      for (let i = 0; i < data.length; i++) {
        var e1 = data[i]
        if (e1.parentId === '-1') {
          rdata.push(e1)
        }
        for (let j = 0; j < data.length; j++) {
          var e2 = data[j]
          if (e1.parentId === e2.id) {
            if (!e2.children) {
              e2.children = []
            }
            e2.children.push(e1)
          }
        }
      }
      return rdata
    },
    getDictList(code) {
      getDictListByCode(code).then(response => {
        if (code === 'YW_BASE_STATUS') {
          this.stateOptions = response.data
        }
      })
    },
    getDicName(code, flag) {
      var dict = []
      if (flag === 'YW_BASE_STATUS') {
        dict = this.stateOptions
      }
      for (var i in dict) {
        if (dict[i].code === code) {
          return dict[i].name
        }
      }
    },
    // 右键点击
    rightClick(MouseEvent, object, Node, element) { // 鼠标右击触发事件
      this.DATA = object
      this.NODE = Node
      console.log(this.NODE,this.DATA,'123',this.NODE.level)
      if (this.NODE.level === 1) {
        this.menu1Visible = true // 显示模态窗口,跳出自定义菜单栏
        this.menu2Visible = false // 显示模态窗口,跳出自定义菜单栏
        this.menu3Visible = false 
        var menu1 = document.querySelector('#menu1')
        document.addEventListener('click', this.foo) // 给整个document添加监听鼠标事件,点击任何位置执行foo方法

        menu1.style.display = 'block'
        menu1.style.left = MouseEvent.clientX - 0 + 'px'
        menu1.style.top = MouseEvent.clientY - 20 + 'px'

      } else if (this.NODE.level > 1 && this.NODE.level < 3) {
        if (this.NODE.data.flag === 'ML') {
          this.menu1Visible = false // 显示模态窗口,跳出自定义菜单栏
          this.menu2Visible = true // 显示模态窗口,跳出自定义菜单栏

          var menu2 = document.querySelector('#menu2')
          document.addEventListener('click', this.foo) // 给整个document添加监听鼠标事件,点击任何位置执行foo方法
          menu2.style.display = 'block'
          menu2.style.left = MouseEvent.clientX - 0 + 'px'
          menu2.style.top = MouseEvent.clientY - 20 + 'px'
        }
      }
      else if(this.NODE.level === 3){
        this.menu1Visible = false
        this.menu2Visible = false
        this.menu3Visible = true
        this.menu4Visible = false
        this.menu4Visible2 = false
        this.menu5Visible = false
        var menu3 = document.querySelector('#menu3')
          document.addEventListener('click', this.foo) // 给整个document添加监听鼠标事件,点击任何位置执行foo方法
          menu3.style.display = 'block'
          menu3.style.left = MouseEvent.clientX - 0 + 'px'
          menu3.style.top = MouseEvent.clientY - 20 + 'px'
      }
      else if(this.NODE.level ===4){

        this.menu1Visible = false
        this.menu2Visible = false
        this.menu3Visible = false
        if (this.NODE.data.flag === 'ML') {
          this.menu4Visible = true
          var menu4 = document.querySelector('#menu4')
          document.addEventListener('click', this.foo) // 给整个document添加监听鼠标事件,点击任何位置执行foo方法
          menu4.style.display = 'block'
          menu4.style.left = MouseEvent.clientX - 0 + 'px'

          menu4.style.top = MouseEvent.clientY - 20 + 'px'
        }else{
          this.menu4Visible2 = true
          var menu42 = document.querySelector('#menu42')
          document.addEventListener('click', this.foo) // 给整个document添加监听鼠标事件,点击任何位置执行foo方法
          menu42.style.display = 'block'
          menu42.style.left = MouseEvent.clientX - 0 + 'px'

          menu42.style.top = MouseEvent.clientY - 20 + 'px'
        }
       

      }

      else if(this.NODE.level === 5){
        this.menu1Visible = false
        this.menu2Visible = false
        this.menu3Visible = false
        this.menu4Visible = false
        this.menu4Visible2 = false
        this.menu5Visible = true
        var menu5 = document.querySelector('#menu5')
          document.addEventListener('click', this.foo) // 给整个document添加监听鼠标事件,点击任何位置执行foo方法
          menu5.style.display = 'block'
          menu5.style.left = MouseEvent.clientX - 0 + 'px'
          menu5.style.top = MouseEvent.clientY - 20 + 'px'
      }

      this.classifyId = this.NODE.data.id
      this.treeLevel = this.NODE.level+1
      this.treeLevel2 = this.NODE.level
      this.downloadId = this.NODE.data.id
      console.log(this.downloadId,'this.downloadId')
      this.xiangmuId = this.NODE.data.projectId
    },
    foo() { // 取消鼠标监听事件 菜单栏
      this.menu1Visible = false
      this.menu2Visible = false
      this.menu3Visible = false
      this.menu4Visible = false
      this.menu4Visible2 = false
      this.menu5Visible = false
      document.removeEventListener('click', this.foo) // 要及时关掉监听,不关掉的是一个坑,不信你试试,虽然前台显示的时候没有啥毛病,加一个alert你就知道了
    },
    handleNodeClick(data) {
      this.from = {}
      this.displayVisible1 = true

      this.classifyId = data.id
      console.log(this.classifyId,'this.classifyId',this.optionProps.label)
      this.xiangmuId = data.projectId
      this.flag = data.flag
      this.batchCode = data.code
      this.parent = data.parentId
      if (this.flag === 'WD') {
        getInfo({ code: data.id }).then(response => {
          this.from = response.data[0]
        }).catch(function() {
        })
      } else {
        var dataA = data
        if (!this.IdArr.includes(dataA.id)) {
  
        // 树
          getAgainLoading({ parentId: data.id, xmId: data.projectId }).then(response => {
            this.data = response.data
            for (let i = 0; i < this.data.length; i++) {
              const newChild = this.data[i]
              if (!dataA.children) {
                this.$set(dataA, 'children', [])
              }
              dataA.children.push(newChild)
            }
            this.dataId = dataA.id
            this.IdArr.push(dataA.id)
            this.listLoading = false
          }).catch(response => {
            this.listLoading = false
          })
        }
      }
    },
    handleAdd() {
      this.dialogAddVisible = true
      this.$nextTick(() => {
        this.$refs.dialogAdd
      })
    },
    handleEdit() {
      this.dialogEditVisible = true
      this.code = this.classifyId
      this.$nextTick(() => {
        this.$refs.dialogEdit
      })
    },
    add() {
      if (this.parent === '-1') {
        Message({
          message: '请选择子节点!',
          type: 'error',
          duration: 5 * 1000
        })
      } else if (this.parent !== '-1' && this.flag !== 'WD') {
        if (this.xiangmuId !== null && this.xiangmuId !== '') {
          if (this.classifyId !== null && this.classifyId !== '') {
            this.addApiVisible = true
            this.$nextTick(() => {
              this.$refs.addApi
            })
          } else {
            Message({
              message: '请选择节点!',
              type: 'error',
              duration: 5 * 1000
            })
          }
        } else {
          Message({
            message: '请选择项目!',
            type: 'error',
            duration: 5 * 1000
          })
        }
      }
    },
    edit() {
      if (this.parent !== '-1' && this.flag === 'WD') {
        if (this.xiangmuId !== null && this.xiangmuId !== '') {
          if (this.classifyId !== null && this.classifyId !== '') {
            this.editApiVisible = true
            this.$nextTick(() => {
              this.$refs.editApi
            })
          } else {
            Message({
              message: '请选择子节点!',
              type: 'error',
              duration: 5 * 1000
            })
          }
        } else {
          Message({
            message: '请选择项目!',
            type: 'error',
            duration: 5 * 1000
          })
        }
      } else {
        Message({
          message: '请选择文档节点!',
          type: 'error',
          duration: 5 * 1000
        })
      }
    },
    history() {
      if (this.parent !== '-1' && this.flag === 'WD') {
        if (this.xiangmuId !== null && this.xiangmuId !== '') {
          if (this.classifyId !== null && this.classifyId !== '') {
            this.historyVisible = true
            this.$nextTick(() => {
              this.$refs.history
            })
          } else {
            Message({
              message: '请选择子节点!',
              type: 'error',
              duration: 5 * 1000
            })
          }
        } else {
          Message({
            message: '请选择项目!',
            type: 'error',
            duration: 5 * 1000
          })
        }
      } else {
        Message({
          message: '请选择文档节点!',
          type: 'error',
          duration: 5 * 1000
        })
      }
    },
    download() {
      if (this.xiangmuId !== null && this.xiangmuId !== '') {
        this.downloadVisible = true
        this.$nextTick(() => {
          this.$refs.download
        })
      } else {
        Message({
          message: '请选择项目!',
          type: 'error',
          duration: 5 * 1000
        })
      }
    },
    handleDisable() {
      MessageBox.confirm('确认删除节点', '确定', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        disabled(this.NODE.id).then(response => {
          Message({
            message: '删除节点成功',
            type: 'success',
            duration: 5 * 1000
          })
          // 重新加载表格
          this.getTreeList(this.xiangmuId)
        })
      })
    },
    remove() {
      if (this.parent !== '-1' && this.flag === 'WD') {
        if (this.xiangmuId !== null && this.xiangmuId !== '') {
          if (this.classifyId !== null && this.classifyId !== '') {
            MessageBox.confirm('确认删除', '确定', {
              confirmButtonText: '确定',
              cancelButtonText: '取消',
              type: 'warning'
            }).then(() => {
              remove({ code: this.classifyId }).then(response => {
                Message({
                  message: '删除成功',
                  type: 'success',
                  duration: 5 * 1000
                })
                // 重新加载表格
                this.getTreeList(this.xiangmuId)
                // 重新加载表格
                this.handleNodeClick()
              })
            })
          } else {
            Message({
              message: '请选择子节点!',
              type: 'error',
              duration: 5 * 1000
            })
          }
        } else {
          Message({
            message: '请选择项目!',
            type: 'error',
            duration: 5 * 1000
          })
        }
      } else {
        Message({
          message: '请选择文档节点!',
          type: 'error',
          duration: 5 * 1000
        })
      }
    },
    yulan() {
      if (this.xiangmuId !== null && this.xiangmuId !== '') {
        this.yulanVisible = true
        this.$nextTick(() => {
          this.$refs.yulan
        })
      } else {
        Message({
          message: '请选择项目!',
          type: 'error',
          duration: 5 * 1000
        })
      }
    }
  }
}
</script>

<style scoped>
.app-container{
    height:calc(100% - 20px) !important;
    width:calc(100% - 20px) !important;
    padding:10px 0;
}
#app-contain{
  height:100%;
  width:100%
}
.custom-tree-node {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 14px;
  padding-right: 8px;
}
/* 右键会选中文字,为了美观将它禁用*/
#el-tree{
  user-select:none;
}
#el-tree >>> .style-demo {
  color: #474a4d;
  font-size: 14px;
  font-family: "黑体";
  text-overflow: ellipsis;
  white-space: nowrap;
  overflow: hidden;
}
.menu1 {
  height: 60px;
  width: 120px;
  position: fixed;
  border: 1px solid #ccc;
  background-color: white;
  list-style: none;
  padding-left: 10px;
}
.menu2 {
  height: 40px;
  width: 120px;
  position: fixed;
  border: 1px solid #ccc;
  background-color: white;
  list-style: none;
  padding-left: 10px;
}
.menu3 {
  height: 120px;
  width: 120px;
  position: fixed;
  border: 1px solid #ccc;
  background-color: white;
  list-style: none;
  padding-left: 10px;
}
.menu4 {
  height: 90px;
  width: 120px;
  position: fixed;
  border: 1px solid #ccc;
  background-color: white;
  list-style: none;
  padding-left: 10px;
}
.menu42 {
  height: 80px;
  width: 120px;
  position: fixed;
  border: 1px solid #ccc;
  background-color: white;
  list-style: none;
  padding-left: 10px;
}
.menu5 {
  height: 60px;
  width: 120px;
  position: fixed;
  border: 1px solid #ccc;
  background-color: white;
  list-style: none;
  padding-left: 10px;
}
.menu_item {
  line-height: 20px;
  text-align: left;
  margin-top: 10px;
}
.collapse-title{
  font-size: 16px;
  color:#1196EE;
}
.table{
  border-collapse: separate;
  border-spacing: 10px 10px;
}
.title-name{
  width: 100px;
  text-align: left;
  vertical-align: middle;
}
.table-content{
  width: 450px;
}

</style>

生成md文件:

javascript 复制代码
<template>
  

  <div class="app-container" style="width:100%;height:100%;">
    <div class="block" style="width:100%;height:100%;">
     是否要按照目录生成MD文件
    </div>
    <span slot="footer" class="dialog-footer">
      <div style="text-align:center;margin-top:20px;">
        <el-button type="primary" @click="onSave">确定</el-button>
        <el-button type="danger" class="quxiao_btn" @click="closePage()">取消</el-button>
      </div>
    </span>
  </div>
</template>

<script>

import { getTreeList, makeMD } from '@/api/configMenu'
import { Message } from 'element-ui'

export default {
  name: 'Zzjg',
  props: { // 第二种方式
    xiangmu: {
      type: String,
      required: true
    },
    classify: {
      type: String,
      required: true
    },
    level: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      data: [],
      data2: [],
      ids: '',
      defaultProps: {
        children: 'children',
        label: 'name'
      }
    }
  },
  created() {
    this.getTreeList()
  },
  methods: {
    getTreeList() { // 加载列表
      getTreeList({ xiangmuId: this.xiangmu }).then(response => {
        this.data = response.data

        this.data2 = this.buildTree2(this.data)
      }).catch(response => {
      })
    },
    buildTree2(data) {
      var rdata = []
      for (let i = 0; i < data.length; i++) {
        var e1 = data[i]
        if (e1.parentId === '-1') {
          rdata.push(e1)
        }
        for (let j = 0; j < data.length; j++) {
          var e2 = data[j]
          if (e1.parentId === e2.id) {
            if (!e2.children) {
              e2.children = []
            }
            e2.children.push(e1)
          }
        }
      }
      return rdata
    },
    onSave() {
      var ids = ''
      var flag = ''
      var level = ''

      ids = this.classify
    
      console.log(this.level,'level')
      
      const loading = this.$loading({
        lock: true,
        text: 'Loading',
        spinner: 'el-icon-loading',
        background: 'rgba(0, 0, 0, 0.7)'
      })

      makeMD({ id: ids, flag: flag, level:this.level }).then(response => {

        var fileName = 'download.zip'
        // const contentDisposition = response.headers['content-disposition']
        //   if (contentDisposition) {
        //     fileName = window.decodeURI(response.headers['content-disposition'].split('=')[1], 'UTF-8')
        //   }
          const blob = new Blob([response], {
            type: `application/zip` // word文档为msword,pdf文档为pdf
          })
          const objectUrl = URL.createObjectURL(blob)
          const link = document.createElement('a')
          link.href = objectUrl
          console.log(objectUrl,'objectUrl')
          link.setAttribute('download', fileName)
          document.body.appendChild(link)
          link.click()
          // 释放内存
          window.URL.revokeObjectURL(link.href)
          loading.close()
          this.closePage()
        if (response.code === 20000) {
          Message({
              message: '生成成功',
              type: 'success',
              duration: 5 * 1000
            })
            loading.close()
            this.closePage()
        }
        
      }).catch(response => {
        loading.close()
      })
    },
    closePage() {
      this.$emit('update:visible', false)
    }
  }
}
</script>

新增方法:

javascript 复制代码
<template>
  <div class="markdown">
    <div style="width:100%;height:auto; margin:0 auto;">
      <el-form ref="form" :model="form" :rules="rules" label-width="80px" style="margin: 0 auto;width:100%;">
        <el-row>
          <el-col :span="12">
            <el-form-item label="手册名称" prop="tableName">
              <el-input v-model="form.tableName" placeholder="手册名称" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="排序号" prop="sortNum">
              <el-input-number v-model="form.sortNum" :step="1" style="width:100%;text-align:left;" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="路径" prop="link">
              <el-input v-model="form.link" placeholder="路径" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="内容" prop="sortNum">
              <mavon-editor ref="md" v-model="content" style="height: 400px" @change="change" @imgAdd="imgAdd" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <div style="text-align:center; padding-top:10px">
        <el-button type="primary" @click="save">保存</el-button>
        <el-button type="danger" @click="closePage">取消</el-button>
      </div>
    </div>
  </div>
</template>

<script>
import { Message } from 'element-ui'
import { save, getMdTemplate } from '@/api/usersManualFile'
import { upload } from '@/api/projectFile'

import { mavonEditor } from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'

export default {
  name: '',
  components: {
    mavonEditor
  },
  inject: ['getTreeList'],
  props: { // 第二种方式
    xiangmu: {
      type: String,
      required: true
    },
    classify: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      form: {
        sortNum: 1
      },
      rules: {
        sortNum: [
          { required: true, message: '排序号不能为空', trigger: 'blur' }
        ],
        tableName: [
          { required: true, message: 'api名称不能为空', trigger: 'blur' }
        ]
      },
      content: '',
      html: '',
      configs: {}
    }
  },
  created() {
    this.getDetailed()
  },
  methods: {
    getDetailed() {
      const loading = this.$loading({
        lock: true,
        text: 'Loading',
        spinner: 'el-icon-loading',
        background: 'rgba(0, 0, 0, 0.7)'
      })
      getMdTemplate().then(response => {
        this.content = response.data

        loading.close()
      }).catch(function() {
        loading.close()
      })
    },
    // 将图片上传到服务器,返回地址替换到md中
    imgAdd(pos, $file) {
      const formdata = new FormData()
      formdata.append('image', $file)
      // 访问后台服务器方法
      upload(formdata).then(res => {
        if (res.code === 20000) {
          this.$refs.md.$img2Url(pos, res.data.abstractPath)
        } else {
          this.$message.error(res.message)
        }
      }).catch(err => {
        console.log(err)
      })
    },
    // 所有操作都会被解析重新渲染
    change(value, render) {
      this.form.htmlData = render
      this.form.markdownData = value
    },
    save() { // 新增
      this.$refs.form.validate(valid => {
        if (valid) {
          const loading = this.$loading({
            lock: true,
            text: 'Loading',
            spinner: 'el-icon-loading',
            background: 'rgba(0, 0, 0, 0.7)'
          })

          this.form.xmId = this.xiangmu
          this.form.classifyId = this.classify
          save(this.form).then(response => {
            Message({
              message: '新增成功',
              type: 'success',
              duration: 5 * 1000
            })

            this.$emit('update:visible', false)
            // this.getList()
            this.getTreeList()
            loading.close()
          }).catch(response => {
            loading.close()
          })
        } else {
          return false
        }
      })
    },
    closePage() {
      this.$emit('update:visible', false)
    }
  }
}
</script>

后端生成方法:

java 复制代码
 /**
     * 生成md文件
     *
     * @Title: makeMD
     * @Description:
     * @author sxy
     * @param flag
     * @param ids
     * @return
     * @throws IOException
     */
    @RequestMapping(value = "/makeMD", method = RequestMethod.POST, produces = "application/json")
//    @GetMapping(value="/makeMD", method = RequestMethod.POST, produces = "application/json")
    @ResponseBody
    public void makeMD(HttpServletRequest request, HttpServletResponse response, String flag, String id,Integer level)
            throws IOException {

        if (id == null || StringUtils.isBlank(id)) {
//            return ResultData.error(ResultData.PARAM_ERROR_CODE, "参数错误");
        }

        // 创建存放生成的MD文件的根目录
        String rootDir = uploadPath + File.separator + "md_files";
        File rootDirFile = new File(rootDir);
        if (!rootDirFile.exists()) {
            rootDirFile.mkdirs();
        }

        // 生成日期子目录
        // 生成日期时间子目录,格式为:年-月-日_时-分-秒
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
        String dateSubDir = LocalDateTime.now().format(formatter);

        String fullDirPath = rootDir + File.separator + dateSubDir;
        File dateDir = new File(fullDirPath);
        if (!dateDir.exists()) {
            dateDir.mkdirs();
        }

        //md文件列表
        List<UsersManualFile> list = new ArrayList<>();
        //如果不是根节点的话,则去查找UsersManualFile表里的parent_id的数据
        if (level != 1) {
            QueryWrapper<UsersManualFile> queryWrapper = new QueryWrapper<UsersManualFile>();
            queryWrapper.eq("parent_id", id);
            list = usersManualFileMapper.selectList(queryWrapper);


        }else {
            QueryWrapper<UsersManualFile> queryWrapper = new QueryWrapper<UsersManualFile>();
            queryWrapper.eq("umf_xm_id", id);
            list = usersManualFileMapper.selectList(queryWrapper);

            //mjs文件列表
            QueryWrapper<ConfigMenu> queryWrapper2 = new QueryWrapper<ConfigMenu>();
            queryWrapper2.eq("project_id", id);
            List<ConfigMenu> configMenuList = configMenuMapper.selectList(queryWrapper2);
            List<ConfigMenu> navMenuList = configMenuList;

            if (configMenuList == null || configMenuList.size() == 0) {
//                return ResultData.error(ResultData.PARAM_ERROR_CODE, "无数据");
            }
            //动态nav
            StringBuilder  mjsTemplateNav = new StringBuilder();
            mjsTemplateNav.append(
                    "      nav: [\n" +
                            "        {\n" +
                            "          text: '首页',\n" +
                            "          link: '/'\n" +
                            "        },\n"
            );
            //动态sidebar
            StringBuilder mjsTemplateSidebar = new StringBuilder();
            mjsTemplateSidebar.append("sidebar: {\n");


            //动态nav
            for (ConfigMenu configMenu : configMenuList) {
                // 二级节点为横向菜单
                if ("2".equals(configMenu.getLevel())) {
                    // 判断该菜单是否存在下拉子项
                    boolean hasSubItems = navMenuList.stream()
                            .anyMatch(menu -> menu.getParentId().equals(configMenu.getId()));

                    if (hasSubItems) {
                        // 如果有下拉子项,则使用 items 格式
                        mjsTemplateNav.append("        {\n");
                        mjsTemplateNav.append("          text: '").append(configMenu.getMenuName()).append("',\n");
                        mjsTemplateNav.append("          items: [\n");

                        for (ConfigMenu subMenu : navMenuList) {
                            if (subMenu.getParentId().equals(configMenu.getId())) {
                                mjsTemplateNav.append("            {\n");
                                mjsTemplateNav.append("              text: '").append(subMenu.getMenuName()).append("',\n");
                                mjsTemplateNav.append("              link: '").append(subMenu.getLink()).append("'\n");
                                mjsTemplateNav.append("            },\n");
                            }
                        }

                        mjsTemplateNav.append("          ]\n");
                        mjsTemplateNav.append("        },\n");
                    } else {
                        // 如果没有下拉子项,则使用简单格式
                        mjsTemplateNav.append("        {\n");
                        mjsTemplateNav.append("          text: '").append(configMenu.getMenuName()).append("',\n");
                        mjsTemplateNav.append("          link: '").append(configMenu.getLink()).append("'\n");
                        mjsTemplateNav.append("        },\n");
                    }
                }
            }


            //动态生成sidebar
            // 创建一个映射,用于存储每个路径对应的菜单项
            Map<String, List<ConfigMenu>> pathToMenus = new HashMap<>();

            for (ConfigMenu configMenu : configMenuList) {
                if ("3".equals(configMenu.getLevel()) || "4".equals(configMenu.getLevel())) {
                    // 如果是三级或四级菜单,检查其是否有对应的文档文件
                    for (UsersManualFile usersManualFile : list) {
                        if (usersManualFile.getParentId().equals(configMenu.getId())) {
                            // 根据菜单的路径分组
                            String path = configMenu.getLink();
                            pathToMenus.computeIfAbsent(path, k -> new ArrayList<>()).add(configMenu);
                        }
                    }
                }
            }

            // 生成每个路径的配置
            for (Map.Entry<String, List<ConfigMenu>> entry : pathToMenus.entrySet()) {
                String path = entry.getKey();
                List<ConfigMenu> menus = entry.getValue();

                // 对菜单项进行去重
                List<ConfigMenu> uniqueMenus = new ArrayList<>();
                for (ConfigMenu menu : menus) {
                    if (!uniqueMenus.contains(menu)) {
                        uniqueMenus.add(menu);
                    }
                }
                menus = uniqueMenus;

                mjsTemplateSidebar.append("      '").append(path).append("': {\n");
                mjsTemplateSidebar.append("        items: [\n");

                for (ConfigMenu configMenu : menus) {
                    if ("3".equals(configMenu.getLevel())) {
                        // 如果是三级菜单,直接添加文档项
                        for (UsersManualFile usersManualFile : list) {
                            if (usersManualFile.getParentId().equals(configMenu.getId())) {
                                mjsTemplateSidebar.append("          {\n");
                                mjsTemplateSidebar.append("            text: '").append(usersManualFile.getTableName()).append("',\n");
                                mjsTemplateSidebar.append("            link: '").append(usersManualFile.getLink()).append("'\n");
                                mjsTemplateSidebar.append("          },\n");
                            }
                        }
                    } else if ("4".equals(configMenu.getLevel())) {
                        // 如果是四级菜单,创建一个可折叠的项
                        mjsTemplateSidebar.append("          {\n");
                        mjsTemplateSidebar.append("            text: '").append(configMenu.getMenuName()).append("',\n");
                        mjsTemplateSidebar.append("            collapsible: true,\n");
                        mjsTemplateSidebar.append("            collapsed: false,\n");
                        mjsTemplateSidebar.append("            items: [\n");

                        // 查找对应的文档文件
                        boolean hasItems = false;
                        for (UsersManualFile usersManualFile : list) {
                            if (usersManualFile.getParentId().equals(configMenu.getId())) {
                                if (!hasItems) {
                                    hasItems = true;
                                }
                                mjsTemplateSidebar.append("              {\n");
                                mjsTemplateSidebar.append("                text: '").append(usersManualFile.getTableName()).append("',\n");
                                mjsTemplateSidebar.append("                link: '").append(usersManualFile.getLink()).append("'\n");
                                mjsTemplateSidebar.append("              },\n");
                            }
                        }

                        if (hasItems) {
                            mjsTemplateSidebar.setLength(mjsTemplateSidebar.length() - 2); // 去掉最后一个逗号
                            mjsTemplateSidebar.append("\n            ]\n");
                        } else {
                            mjsTemplateSidebar.append("            ]\n");
                        }

                        mjsTemplateSidebar.append("          },\n");
                    }
                }

                mjsTemplateSidebar.setLength(mjsTemplateSidebar.length() - 2); // 去掉最后一个逗号
                mjsTemplateSidebar.append("\n        ]\n");
                mjsTemplateSidebar.append("      },\n");
            }

            mjsTemplateSidebar.append("    },\n");


            mjsTemplateNav.append("      ]\n" +
                    "    ,\n");


            //根据id获取项目名称
            ProjectInfo projectInfo = projectInfoMapper.selectById(id);
            String  projectName = "";
            if (projectInfo != null){
                projectName = projectInfo.getXiangmumingcheng();
            }

            //配置mjs文件内容
            // 固定模板头部
            String mjsTemplateHeder = "import {defineConfig} from 'vitepress'\n" +
                    "import hljs from 'highlight.js/lib/core'\n" +
                    "import javascript from 'highlight.js/lib/languages/javascript'\n" +
                    "import xml from 'highlight.js/lib/languages/xml'\n" +
                    "import {ref} from \"./cache/deps/vue.js\";\n" +
                    "// 注册语言\n" +
                    "hljs.registerLanguage('javascript', javascript)\n" +
                    "hljs.registerLanguage('html', xml)\n" +
                    "\n" +
                    "export default defineConfig(async () => {\n" +
                    "  return {\n" +
                    "    markdown: {\n" +
                    "      theme: 'material-theme-palenight',\n" +
                    "      lineNumbers: true,\n" +
                    "      math: true,\n" +
                    "      container: {\n" +
                    "        tipLabel: '提示',\n" +
                    "        warningLabel: '警告',\n" +
                    "        dangerLabel: '危险',\n" +
                    "        infoLabel: \"信息\",\n" +
                    "        detailsLabel: \"详情\"\n" +
                    "      },\n" +
                    "\n" +
                    "    },\n" +
                    "    enhanceApp: {\n" +
                    "      setup: (ctx) => {\n" +
                    "        ctx.app.component('AdBanner', AdBanner)\n" +
                    "      }\n" +
                    "    },\n" +
                    "\n" +
                    //动态修改项目名称
                    "    title: \""+projectName+"帮助文档\",\n" +
                    "    description: \"系统使用说明\",\n" +
                    "    themeConfig: {\n" +
                    "      outline: {\n" +
                    "        level: [2, 3],\n" +
                    "        label: '页面导航'\n" +
                    "      },\n" +
                    "      siteTitle: \"帮助文档\",\n" +
                    "      logo: \"../pubilc/assets/logo.png\",\n";


            //固定模板尾部
            String mjsTemplateTail =
                    "      footer: {\n" +
                            "        message: \""+projectName+"帮助文档\",\n" +
                            "        copyright: '2025.5.20   @langtao'\n" +
                            "      },\n" +
                            "      styl: {\n" +
                            "\n" +
                            "      }\n" +
                            "\n" +
                            "    }\n" +
                            "  }\n" +
                            "})";





            // 写入动态生成的 .mjs 文件
            String mjsContent = mjsTemplateHeder+mjsTemplateNav+mjsTemplateSidebar+mjsTemplateTail;

            String fileName = "config" + ".mjs";
            String filePath = fullDirPath + File.separator + fileName;

            // 写入文件
            try (FileWriter writer = new FileWriter(filePath)) {
                writer.write(mjsContent);
            }
        }




        if (list == null || list.size() == 0) {
//            return ResultData.error(ResultData.PARAM_ERROR_CODE, "无数据");
        }





        // 存储生成的md文件的相对路径
        List<String> mdFilePaths = new ArrayList<>();

        for (UsersManualFile usersManualFile : list) {
            // 使用文件ID作为文件名前缀,避免文件名重复
            String fileNamePrefix = usersManualFile.getId().toString().substring(usersManualFile.getId().toString().length() - 8);
            String fileName = fileNamePrefix + "-" + usersManualFile.getTableName() + ".md";
            String filePath = fullDirPath + File.separator + fileName;

            // 写入MD内容
            try (FileWriter writer = new FileWriter(filePath)) {
                writer.write(usersManualFile.getMarkdownData());
            }

            // 添加相对路径到结果列表
            mdFilePaths.add("md_files/" + dateSubDir  + fileName);
        }

        // 在生成MD文件和MJS文件后,将文件夹压缩成ZIP
        String zipPath = fullDirPath + ".zip";
        FileOutputStream fos = new FileOutputStream(zipPath);
        ZipOutputStream zos = new ZipOutputStream(fos);

        // 添加文件夹到ZIP
        addFolderToZip(fullDirPath, fullDirPath, zos);

        // 关闭流
        zos.close();
        fos.close();

        // 设置响应头,以便前端下载
        response.setHeader("Content-Disposition", "attachment; filename=" + dateSubDir + ".zip");
        response.setHeader("Content-Type", "application/octet-stream");

        // 将ZIP文件发送给前端
        FileInputStream fis = new FileInputStream(zipPath);
        IOUtils.copy(fis, response.getOutputStream());
        response.getOutputStream().flush();
        fis.close();

        // 删除临时文件
//        deleteDir(new File(fullDirPath));
//        new File(zipPath).delete();


//        return ResultData.success("ok", response);
    }


    /**
     * 将文件夹添加到ZIP压缩包中
     */
    private void addFolderToZip(String srcFolder, String baseFolder, ZipOutputStream zos) throws IOException {
        File folder = new File(srcFolder);
        for (File file : folder.listFiles()) {
            if (file.isFile()) {
                FileInputStream fis = new FileInputStream(file);
                String entryName = file.getAbsolutePath().substring(baseFolder.length() + 1);
                ZipEntry entry = new ZipEntry(entryName);
                zos.putNextEntry(entry);
                IOUtils.copy(fis, zos);
                zos.closeEntry();
                fis.close();
            } else if (file.isDirectory()) {
                addFolderToZip(file.getAbsolutePath(), baseFolder, zos);
            }
        }
    }

    /**
     * 删除文件夹及其内容
     */
    private void deleteDir(File dir) {
        if (dir.isDirectory()) {
            for (File child : dir.listFiles()) {
                deleteDir(child);
            }
        }
        dir.delete();
    }
相关推荐
2302_809798324 小时前
【项目记录】准备工作及查询部门
状态模式
蓝婷儿16 小时前
第七章:组件之城 · 重构世界的拼图术
重构·状态模式
ALLSectorSorft1 天前
外卖跑腿小程序评价系统框架搭建
小程序·状态模式
Yvonne爱编码2 天前
CSS-5.1 Transition 过渡
前端·css·状态模式·html5·hbuilder
hope_wisdom3 天前
实战设计模式之状态模式
设计模式·系统架构·状态模式·软件工程·架构设计
結城3 天前
前后端的双精度浮点数精度不一致问题解决方案,自定义Spring的消息转换器处理JSON转换
spring·json·状态模式
是赵敢敢啊3 天前
实现动态增QuartzJob,通过自定义注解调用相应方法
状态模式
Yvonne爱编码7 天前
CSS- 4.4 固定定位(fixed)& 咖啡售卖官网实例
前端·css·html·状态模式·hbuilder
Yvonne爱编码7 天前
CSS- 4.2 相对定位(position: relative)
前端·css·状态模式·html5·hbuilder