基于vue3项目开发+MonacoEditor实现外部引入依赖,界面化所见即所得

最近一个项目中,基于vue3开发,想开发一个在线管理组件库的功能,具体业务实现:

1. 在私库Nexus上传组件包;

2. 然后用UNPKG实现路径访问在线解压文件;

3. 解压文件上传到gitee组件库中查看;

4. 然后通过页面配置填写需要引入的依赖地址(直接通过UNPKG读取包内文件内容),页面中填写dist文件夹中的文件路径,支持在当前组件中引入多个外部依赖(css、js);

5. 可以读取reademe.md文件并在页面中展示;

6. 输入并引入依赖后,在编辑器中输入样例代码,切换preview实现预览;

主要实现逻辑:用MonacoEditor作为编辑器组件,然后用importmap的方式在html页面的head中引入页面中配置的依赖地址,在编辑器中编辑代码然后通过响应式传入并渲染到html中进行功能展示。

代码片段截图

tab切换编辑器和视图展示:

监听切换tab,切换到视图界面获取编辑器中代码:

根据UNPKG地址获取md内容并用marked展示:


根据页面上填写的js和css文件地址合并数组传入preview组件:

preview组件中监听监听props.state做数据更新,props.resources监听更新html头部的importmap引入依赖:

创建沙盒,拼接html中head内的importmap,沙盒加载以及销毁部分,通过postMessage传递渲染内容:

html页面中监听postMessage传递的内容,然后用handleEval方法添加加载脚本:

MonacoEditor编辑器配置

功能页面展示如下:

代码编辑和readme.md引入展示:

示例代码preview展示:

全部功能代码如下:

表单

点击查看代码

复制代码
<template>
  <div class="crud-page" v-loading="loading">
    <el-form
      ref="ruleFormRef"
      :inline="true"
      :model="crudForm"
      class="crudForm"
      :rules="rules"
      label-width="150px"
    >
      <el-row class="componentMsg">
        <p>组件信息</p>
        <el-col :span="12">
          <el-form-item label="组件名称:" prop="name">
            <el-input
              v-model="crudForm.name"
              :disabled="disabledStatus"
              placeholder="请输入组件名称"
              clearable
            />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="组件分类:" prop="type">
            <el-tree-select
              v-model="crudForm.type"
              :disabled="disabledStatus"
              :props="treeprops"
              check-strictly
              filterable
              :data="typeOptions"
              :render-after-expand="false"
            />
            <!-- <el-select v-model="crudForm.type" placeholder="请选择组件分类">
              <el-option v-for="item in typeOptions" :key="item.value" :label="item.label" :value="item.value" />
            </el-select> -->
            <!-- <el-cascader v-model="editypeList" :options="typeOptions" :show-all-levels="false" @change="editType"/> -->
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="组件标签:" prop="tagList">
            <!-- <el-select v-model="crudForm.tag" multiple placeholder="请选择组件标签">
              <el-option label="标签一" value="tag1" />
              <el-option label="标签二" value="tag2" />
            </el-select> -->
            <el-select
              v-model="tagList"
              multiple
              filterable
              allow-create
              default-first-option
              :reserve-keyword="false"
              placeholder="请选择组件标签"
              @change="createTagFun"
              :disabled="disabledStatus"
            >
              <el-option
                v-for="item in tagOptions"
                :key="item.value"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="版本号:">
            <el-input
              v-model="crudForm.version"
              placeholder="请输入版本号"
              clearable
              :disabled="disabledStatus"
            />
          </el-form-item>
        </el-col>
        <!-- <el-col :span="12">
          <el-form-item label="所属任务:" prop="task">
            <el-select v-model="crudForm.task" placeholder="请选择所属任务">
              <el-option label="所属任务" value="suoshurenwu" />
            </el-select>
          </el-form-item>
        </el-col> -->
        <el-col :span="12">
          <el-form-item label="组件描述:" prop="description">
            <el-input
              type="textarea"
              v-model="crudForm.description"
              :rows="5"
              :max-rows="5"
              placeholder="请输入组件描述"
              :disabled="disabledStatus"
            ></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="发布日志:" prop="publishLog">
            <el-input
              type="textarea"
              v-model="crudForm.publishLog"
              :rows="5"
              :max-rows="5"
              placeholder="请输入发布日志"
              :disabled="disabledStatus"
            ></el-input>
          </el-form-item>
        </el-col>

        <el-col :span="12" v-show="!disabledStatus">
          <el-form-item label="仓库地址:">
            <el-input
              v-model="crudForm.warehouseAddress"
              placeholder="gitee仓库地址,例如:/packages/vue3/xxx"
              clearable
              :disabled="importDisabled"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12" v-show="disabledStatus">
          <el-form-item label="仓库地址:" prop="publishLog">
            <el-link
              :href="
                'https://gitee.com/ksbpump/ki-components/tree/develop' + crudForm.warehouseAddress
              "
              target="_blank"
            >
              去仓库查看
              <el-icon><Right /></el-icon>
            </el-link>
          </el-form-item>
        </el-col>
        <el-col :span="12" v-show="!disabledStatus">
          <el-button
            :icon="Search"
            style="float: left; margin-left: 10px"
            @click="viewMd"
            type="primary"
          >
            查看README.md
          </el-button>
        </el-col>

        <el-col :span="12" v-show="!disabledStatus">
          <el-form-item
            v-for="(ssJsConfig, index) in crudForm.ssJsConfig"
            :key="index"
            :label="index === 0 ? '组件js依赖:' : ' '"
          >
            <el-input
              v-model="ssJsConfig.dataKey"
              placeholder="组件别名"
              clearable
              :disabled="importDisabled"
              style="width: 20%"
            />
            <el-input
              v-model="ssJsConfig.dataValue"
              placeholder="组件依赖路径,例如:/dist/index.ems.js"
              clearable
              :disabled="importDisabled"
              style="width: 75%"
            />
            <el-icon
              size="16px"
              color="#f56c6c"
              style="margin-left: 5px; cursor: pointer"
              @click="deleteSsJs(ssJsConfig)"
              v-show="index != 0"
            >
              <RemoveFilled />
            </el-icon>
          </el-form-item>
        </el-col>
        <el-col :span="12" v-show="!disabledStatus" style="vertical-align: middle">
          <el-button
            :icon="Plus"
            style="float: left; margin-left: 10px"
            @click="addSsJs"
            type="success"
            :disabled="importDisabled"
          >
            添加组件js依赖
          </el-button>
        </el-col>
        <el-col :span="12" v-show="!disabledStatus">
          <el-form-item
            v-for="(ssCssConfig, index) in crudForm.ssCssConfig"
            :key="index"
            :label="index === 0 ? '组件css依赖:' : ' '"
          >
            <el-input
              v-model="ssCssConfig.dataValue"
              placeholder="组件依赖路径,例如:/dist/index.min.css"
              clearable
              :disabled="importDisabled"
              style="width: 95%"
            />
            <el-icon
              size="16px"
              color="#f56c6c"
              style="margin-left: 5px; cursor: pointer"
              @click="deleteSsCss(ssCssConfig)"
              v-show="index != 0"
            >
              <RemoveFilled />
            </el-icon>
          </el-form-item>
        </el-col>
        <el-col :span="12" v-show="!disabledStatus">
          <el-button
            :icon="Plus"
            style="float: left; margin-left: 10px"
            @click="addSsCss"
            type="success"
            :disabled="importDisabled"
          >
            添加组件css依赖
          </el-button>
        </el-col>

        <!-- 全局部分 -->
        <el-col :span="12" v-show="!disabledStatus">
          <el-form-item
            v-for="(ssJsConfig, index) in crudForm.ssAllJsConfig"
            :key="index"
            :label="index === 0 ? '全局组件js依赖:' : ' '"
          >
            <el-input
              v-model="ssJsConfig.dataKey"
              placeholder="组件别名"
              clearable
              :disabled="importDisabled"
              style="width: 20%"
            />
            <el-input
              v-model="ssJsConfig.name"
              placeholder="组件名称"
              clearable
              :disabled="importDisabled"
              style="width: 20%"
            />
            <el-input
              v-model="ssJsConfig.versionNum"
              placeholder="组件版本号"
              clearable
              :disabled="importDisabled"
              style="width: 20%"
            />
            <el-input
              v-model="ssJsConfig.dataValue"
              placeholder="组件依赖路径,例如:/dist/index.ems.js"
              clearable
              :disabled="importDisabled"
              style="width: 35%"
            />
            <el-icon
              size="16px"
              color="#f56c6c"
              style="margin-left: 5px; cursor: pointer"
              @click="deleteSsAllJs(ssJsConfig)"
              v-show="index != 0"
            >
              <RemoveFilled />
            </el-icon>
          </el-form-item>
        </el-col>
        <el-col :span="12" v-show="!disabledStatus" style="vertical-align: middle">
          <el-button
            :icon="Plus"
            style="float: left; margin-left: 10px"
            @click="addSsAllJs"
            type="success"
            :disabled="importDisabled"
          >
            添加全局组件js依赖
          </el-button>
        </el-col>
        <el-col :span="12" v-show="!disabledStatus">
          <el-form-item
            v-for="(ssCssConfig, index) in crudForm.ssAllCssConfig"
            :key="index"
            :label="index === 0 ? '全局组件css依赖:' : ' '"
          >
            <el-input
              v-model="ssCssConfig.name"
              placeholder="组件名称"
              clearable
              :disabled="importDisabled"
              style="width: 20%"
            />
            <el-input
              v-model="ssCssConfig.versionNum"
              placeholder="组件版本号"
              clearable
              :disabled="importDisabled"
              style="width: 20%"
            />
            <el-input
              v-model="ssCssConfig.dataValue"
              placeholder="组件依赖路径,例如:/dist/index.min.css"
              clearable
              :disabled="importDisabled"
              style="width: 55%"
            />
            <el-icon
              size="16px"
              color="#f56c6c"
              style="margin-left: 5px; cursor: pointer"
              @click="deleteSsAllCss(ssCssConfig)"
              v-show="index != 0"
            >
              <RemoveFilled />
            </el-icon>
          </el-form-item>
        </el-col>
        <el-col :span="12" v-show="!disabledStatus">
          <el-button
            :icon="Plus"
            style="float: left; margin-left: 10px"
            @click="addSsAllCss"
            type="success"
            :disabled="importDisabled"
          >
            添加全局组件css依赖
          </el-button>
        </el-col>

        <el-col class="install-picture" :span="12">
          <el-form-item label="效果图:" prop="images">
            <SingleImageUpload
              v-model="crudForm.images"
              :style="{ width: '64px', height: '64px' }"
              :disabled="disabledStatus"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12" v-show="!disabledStatus">
          <el-button
            :icon="BottomLeft"
            style="float: left; margin-left: 10px"
            @click="importModel"
            type="info"
            :disabled="importDisabled"
          >
            引入全部依赖
          </el-button>
        </el-col>
      </el-row>
      <el-row class="componentExample">
        <p>使用示例</p>
        <el-col class="install-example" :span="24">
          <span>使用示例:</span>
          <div class="exampleDiv" v-if="showExample">
            <el-tooltip class="box-item" effect="dark" content="复制" placement="top">
              <el-icon
                style="position: absolute; right: 25px; top: 23px; cursor: pointer; z-index: 100"
              >
                <DocumentCopy />
              </el-icon>
            </el-tooltip>
            <el-tabs type="border-card" v-model="tab">
              <el-tab-pane name="code">
                <template #label>
                  <span class="custom-tabs-label">
                    <el-icon><postcard /></el-icon>
                    <span>Code</span>
                  </span>
                </template>
                <MonacoEditor
                  ref="editorRef"
                  v-model="crudForm.installExample"
                  :visible="props.componentModel"
                  :style="{ height: '415px' }"
                  :options="editorOptions"
                  @update:modelValue="getCode"
                />
              </el-tab-pane>
              <el-tab-pane name="preview">
                <template #label>
                  <span class="custom-tabs-label">
                    <el-icon><Monitor /></el-icon>
                    <span>Preview</span>
                  </span>
                </template>
                <Preview
                  v-if="showExample"
                  :state="state"
                  :tab="tab"
                  :resources="resources"
                ></Preview>
              </el-tab-pane>
            </el-tabs>
          </div>
          <div v-else class="exampleText">
            请填写组件在仓库中的地址和组件依赖信息后,点击引入依赖按钮后进行示例填写
          </div>
        </el-col>
      </el-row>
      <el-row class="componentShuo" v-show="componentShuoShow">
        <p>使用说明</p>
        <div id="preview"></div>
      </el-row>
    </el-form>
    <el-row class="history-table">
      <p>版本历史</p>
      <el-table :data="tableData" style="width: 100%">
        <el-table-column prop="version" label="版本号" />
        <el-table-column prop="publishLog" label="更新日志" />
        <el-table-column prop="updateTime" label="更新时间" />
        <el-table-column prop="address" label="操作" width="80">
          <template #default="scope">
            <el-button link type="primary" size="small" @click="viewLogs(scope.row)">
              查看
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-row>
    <el-drawer
      v-model="innerDrawer"
      title="版本历史"
      :append-to-body="true"
      :before-close="handleClose"
      size="800"
      class="innerDrawer"
    >
      <el-form
        ref="ruleFormRef"
        :inline="true"
        :model="histroyForm"
        class="histroyForm"
        label-width="100px"
      >
        <el-row>
          <el-col :span="12">
            <el-form-item label="组件名称:" prop="name">
              <el-input
                v-model="histroyForm.name"
                :disabled="true"
                placeholder="请输入组件名称"
                clearable
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="组件分类:" prop="type">
              <el-tree-select
                v-model="histroyForm.type"
                :disabled="true"
                :props="treeprops"
                check-strictly
                filterable
                :data="typeOptions"
                :render-after-expand="false"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="组件标签:" prop="historyTagList">
              <el-select
                v-model="historyTagList"
                multiple
                filterable
                allow-create
                default-first-option
                :reserve-keyword="false"
                placeholder="请选择组件标签"
                :disabled="true"
              >
                <el-option
                  v-for="item in tagOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="版本号:" prop="version">
              <el-input
                v-model="histroyForm.version"
                placeholder="请输入版本号"
                clearable
                :disabled="true"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="更新时间:" prop="updateTime">
              <el-date-picker
                style="width: 100%"
                v-model="histroyForm.updateTime"
                type="datetime"
                placeholder="请选择时间"
                value-format="YYYY-MM-DD HH:mm:ss"
                :disabled="true"
              />
            </el-form-item>
          </el-col>
          <el-col :span="24">
            <el-form-item label="组件描述:" prop="description">
              <el-input
                type="textarea"
                v-model="histroyForm.description"
                :rows="5"
                :max-rows="5"
                placeholder="请输入组件描述"
                :disabled="true"
              ></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="24">
            <el-form-item label="更新日志:" prop="publishLog">
              <el-input
                type="textarea"
                v-model="histroyForm.publishLog"
                :rows="5"
                :max-rows="5"
                placeholder="请输入更新日志"
                :disabled="true"
              ></el-input>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
    </el-drawer>
  </div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick, watch } from "vue";
import type { FormInstance, FormRules } from "element-plus";
import Preview from "@/components/VueSfcEditor/preview/index.vue";
import {
  Postcard,
  Monitor,
  Search,
  Download,
  Right,
  CirclePlusFilled,
  RemoveFilled,
  BottomLeft,
} from "@element-plus/icons-vue";
import ComponentManageAPI from "@/api/modules/componentManage.api";
import { parse, compileScript, compileTemplate, compileStyle } from "@vue/compiler-sfc";
import { ElMessage } from "element-plus";

import { marked } from "marked";
import "highlight.js/styles/github.css";
import hljs from "highlight.js";

const loading = ref(false);
const props = defineProps({
  componentForm: {
    type: Object,
    default: () => ({}),
  },
  componentModel: {
    type: Boolean,
    default: false,
  },
});

const tab = ref("code");

const editStatus = ref("");

watch(
  () => props.componentModel,
  (val) => {
    if (val) {
      tab.value = "code";
      getCode("");
    }
  }
);

const disabledStatus = ref(false);

watch(
  () => tab.value,
  (val) => {
    if (val === "preview") {
      getCode(editorRef.value.getEditValue());
    }
  }
);

marked.use({
  async: false,
  highlight: (code, lang) => {
    const validLang = hljs.getLanguage(lang) ? lang : "plaintext";
    return hljs.highlight(code, { language: validLang }).value;
  },
});

marked.setOptions({
  gfm: true, // 支持 GitHub 风格语法
  breaks: false, // 换行符处理
  pedantic: false, // 避免严格模式导致解析异常
});

interface RuleForm {
  id: string;
  name: string;
  type: string;
  tagStr: string;
  updateTime: string;
  warehouseAddress: string;
  // task: string;
  installType: string;
  install: string;
  description: string;
  installExample: string;
  version: string;
  images: string;
  publishLog: string;
  status: string;
  readme: string;
  ssJsConfig: {
    dataKey: string;
    dataValue: string;
  }[];
  ssCssConfig: {
    dataValue: string;
  }[];
  ssAllJsConfig: {
    dataKey: string;
    name: string;
    versionNum: string;
    dataValue: string;
  }[];
  ssAllCssConfig: {
    name: string;
    versionNum: string;
    dataValue: string;
  }[];
}
const ruleFormRef = ref<FormInstance>();
const crudForm = reactive<RuleForm>({
  id: "",
  // name: "ksb-vis-timeline",
  name: "",
  type: "",
  tagStr: "",
  updateTime: "",
  // warehouseAddress: "/packages/vue3/ui-data",
  warehouseAddress: "",
  // task: "",
  installType: "NPM",
  install: "",
  description: "",
  installExample: "",
  // version: "0.0.0-no-version",
  version: "",
  images: "",
  publishLog: "",
  status: "0",
  readme: "",
  ssJsConfig: [
    // {
    //   dataKey: "vis",
    //   dataValue: "/dist/vis-timeline-graph2d.esm.js",
    // },
    {
      dataKey: "",
      dataValue: "",
    },
  ],
  ssCssConfig: [
    // {
    //   dataValue: "/dist/vis-timeline-graph2d.min.css",
    // },
    {
      dataValue: "",
    },
  ],
  ssAllJsConfig: [
    // {
    //   dataKey: "vue",
    //   name: "vue",
    //   versionNum: "3.4.19",
    //   dataValue: "/dist/vue.esm-browser.js",
    // },
    {
      dataKey: "",
      name: "",
      versionNum: "",
      dataValue: "",
    },
  ],
  ssAllCssConfig: [
    // {
    //   name: "",
    //   versionNum: "",
    //   dataValue: "",
    // },
    {
      name: "",
      versionNum: "",
      dataValue: "",
    },
  ],
});
const rules = reactive<FormRules<RuleForm>>({
  name: [{ required: true, message: "组件名称不能为空", trigger: "blur" }],
  type: [{ required: true, message: "请选择组件分类", trigger: "change" }],
  description: [{ required: true, message: "请输入组件描述", trigger: "blur" }],
});

// const installTypes = ["NPM", "pnpm", "bun", "yarn"];
const installTypes = ["NPM"];

import { Delete, Download, Plus, ZoomIn } from "@element-plus/icons-vue";

import type { UploadFile } from "element-plus";
import edit from "@/views/demo/curd/config/edit";
import { c } from "vite/dist/node/moduleRunnerTransport.d-DJ_mE5sf";
import { editor } from "monaco-editor";
import func from "vue-temp/vue-editor-bridge";

const disabled = ref(false);

const editorRef = ref<any>(null);

// 添加 Monaco Editor 配置
const editorOptions = {
  value: "",
  language: "html",
  theme: "vs-dark",
  automaticLayout: true,
  minimap: {
    enabled: false,
  },
  scrollBeyondLastLine: false,
  fontSize: 14,
  tabSize: 2,
  wordWrap: "on",
  formatOnPaste: true,
  formatOnType: true,
  suggestOnTriggerCharacters: true,
  acceptSuggestionOnEnter: "on",
  quickSuggestions: true,
  parameterHints: {
    enabled: true,
  },
  // 添加自定义配置以处理内存文件
  model: {
    uri: "inmemory://model/1",
    language: "html",
  },
};

const getCode = (code?: string) => {
  crudForm.installExample = code ?? "";
  state.code = code ?? "";
};

const state = reactive({
  // sfc 源代码
  code: "",
  // code: props?.componentData?.content || DefaultCode.trim(),
  updateCode(code) {
    state.code = code;
  },

  // 编译过程
  compile(code) {
    // 直接返回代码内容,不进行 Vue 编译
    return code;
  },
});
provide("store", state);
// console.log(state);

const treeprops = {
  label: "className",
  children: "childrenList",
  value: "id",
};

interface Tree {
  id: string;
  className: string;
  parentId: string;
  childrenList?: Tree[];
  classDesc: string;
  classCode: string;
}

const typeOptions = ref([]);
function getTypeListFun() {
  ComponentManageAPI.getTypeList({}).then((res) => {
    const dataTree: Tree[] = JSON.parse(JSON.stringify(res));
    typeOptions.value = dataTree;
  });
}

const tagOptions = ref([]);
function getTagListFun() {
  ComponentManageAPI.getTagList({}).then((res) => {
    tagOptions.value = [];
    res.forEach((item) => {
      tagOptions.value.push({
        value: item.tagName,
        label: item.tagName,
      });
    });
  });
}

const tagList = ref([]);

function createTagFun() {
  // 提取B的value集合,使用Set提高检索效率
  const BValueSet = new Set(tagOptions.value.map((item) => item.value));
  // 筛选A中存在但B中缺失的值
  const missingInB = tagList.value.filter((item) => !BValueSet.has(item));
  if (missingInB.length > 0) {
    ComponentManageAPI.createTag(missingInB.toString()).then((res) => {
      getTagListFun();
    });
  }
  crudForm.tagStr = tagList.value.toString();
}

function selectInstallTypeFun(type: string) {
  crudForm.installType = type;
}

let tableData = ref([]);
const historyTagList = ref([]);

const histroyForm = reactive({
  name: "",
  type: "",
  tagStr: "",
  description: "",
  installType: "",
  install: "",
  version: "",
  updateTime: "",
  publishLog: "",
});
const innerDrawer = ref(false);
function viewLogs(row) {
  innerDrawer.value = true;

  histroyForm.name = row.name;
  histroyForm.type = row.type;
  histroyForm.tagStr = row.tagStr;
  histroyForm.description = row.description;
  histroyForm.installType = row.installType;
  histroyForm.install = row.install;
  histroyForm.version = row.version;
  histroyForm.updateTime = row.updateTime;
  histroyForm.publishLog = row.publishLog;

  historyTagList.value = row.tagStr.split(",");
}
function handleClose() {
  innerDrawer.value = false;
}

const getEditorCode = () => {
  crudForm.installExample = editorRef.value.getEditValue();
};

const componentShuoShow = ref(false);

const UNPKGAddress = "http://10.22.0.120:4000";
// 查看md
function viewMd() {
  if (crudForm.name == "" || crudForm.version == "") {
    ElMessage({
      type: "error",
      message: "请填写组件名称和版本号后操作",
    });
    return;
  }

  fetchReadme(crudForm.name, crudForm.version).then((content) => {
    if (content) {
      nextTick(() => {
        const previewElement = document.getElementById("preview");
        if (previewElement) {
          previewElement.innerHTML = marked.parse(content);
          // 添加代码块样式
          previewElement.querySelectorAll("pre code").forEach((block) => {
            hljs.highlightElement(block);
          });
          componentShuoShow.value = true;
        } else {
          console.error("Preview element not found");
        }
      });
    }
  });
}

async function fetchReadme(packageName: string, version = "latest") {
  const url = UNPKGAddress + `/${packageName}@${version}/README.md`;
  try {
    const response = await fetch(url, { mode: "cors" });
    if (!response.ok) {
      ElMessage({
        type: "error",
        message: "HTTP错误" + ` ${response.status}`,
      });
    }
    const readmeContent = await response.text();
    console.log(readmeContent); // 输出或处理内容
    return readmeContent;
  } catch (error) {
    ElMessage({
      type: "error",
      message: "文件获取失败",
    });
    return null;
  }
}

const importDisabled = ref(false);
interface ResourceItem {
  type: string;
  name?: string;
  url: string;
}
const showExample = ref(false);
const resources = ref<ResourceItem[]>([]);
function importModel() {
  if (crudForm.name == "" || crudForm.version == "") {
    ElMessage({
      type: "error",
      message: "请填写组件名称和版本号后操作",
    });
    return;
  }
  if (
    !validateConfigs(
      crudForm.ssJsConfig,
      crudForm.ssCssConfig,
      crudForm.ssAllJsConfig,
      crudForm.ssAllCssConfig
    )
  ) {
    ElMessage({
      type: "error",
      message: "请正确填写依赖后操作",
    });
    return;
  }
  
resources.value = [];
  let list: any[] = [];
  crudForm.ssJsConfig.map((item) => {
    if (item.dataKey && item.dataValue) {
      list.push({
        type: "js",
        name: item.dataKey,
        url: UNPKGAddress + `/${crudForm.name}@${crudForm.version}${item.dataValue}`,
      });
    }
  });
  crudForm.ssAllJsConfig.map((item) => {
    if (item.dataKey && item.name && item.versionNum && item.dataValue) {
      list.push({
        type: "js",
        name: item.dataKey,
        url: UNPKGAddress + `/${item.name}@${item.versionNum}${item.dataValue}`,
      });
    }
  });
  crudForm.ssCssConfig.map((item) => {
    if (item.dataValue) {
      list.push({
        type: "css",
        url: UNPKGAddress + `/${crudForm.name}@${crudForm.version}${item.dataValue}`,
      });
    }
  });
  crudForm.ssAllCssConfig.map((item) => {
    if (item.name && item.versionNum && item.dataValue) {
      list.push({
        type: "css",
        url: UNPKGAddress + `/${item.name}@${item.versionNum}${item.dataValue}`,
      });
    }
  });
  
  resources.value = list;
  showExample.value = true;
}

async function fetchJsCss(packageName: string, version = "latest", dataValue: string) {
  const url = UNPKGAddress + `/${packageName}@${version}${dataValue}`;
  try {
    const response = await fetch(url, { mode: "cors" });
    if (!response.ok) {
      ElMessage({
        type: "error",
        message: "HTTP错误" + ` ${response.status}`,
      });
    }
    const readmeContent = await response.text();
    console.log(readmeContent); // 输出或处理内容
    return readmeContent;
  } catch (error) {
    ElMessage({
      type: "error",
      message: "文件获取失败",
    });
    return null;
  }
}

function validateConfigs(
  ssJsConfig: any,
  ssCssConfig: any,
  ssAllJsConfig: any,
  ssAllCssConfig: any
): boolean {
  // ====================== 防御性校验 ======================
  // 检查输入是否为数组
  if (
    !Array.isArray(ssJsConfig) ||
    !Array.isArray(ssCssConfig) ||
    !Array.isArray(ssAllJsConfig) ||
    !Array.isArray(ssAllCssConfig)
  ) {
    throw new Error("配置必须为数组类型");
  }

  // ====================== 核心校验逻辑 ======================
  // 校验规则1:检查 ssJsConfig 中的每条数据是否符合互斥规则
  const isJsConfigValid = ssJsConfig.every((item) => {
    // 确保处理字符串类型 (防御非字符串输入)
    const dataKey = String(item?.dataKey ?? "").trim();
    const dataValue = String(item?.dataValue ?? "").trim();

    // 互斥规则:两个字段要么同时为空,要么同时有值
    return (dataKey === "" && dataValue === "") || (dataKey !== "" && dataValue !== "");
  });

  const isAllJsConfigValid = ssAllJsConfig.every((item) => {
    // 确保处理字符串类型 (防御非字符串输入)
    const dataKey = String(item?.dataKey ?? "").trim();
    const name = String(item?.name ?? "").trim();
    const versionNum = String(item?.versionNum ?? "").trim();
    const dataValue = String(item?.dataValue ?? "").trim();

    // 互斥规则:两个字段要么同时为空,要么同时有值
    return (
      (dataKey === "" && name === "" && versionNum === "" && dataValue === "") ||
      (dataKey !== "" && name !== "" && versionNum !== "" && dataValue !== "")
    );
  });

  // 规则1不通过:直接返回错误
  if (!isJsConfigValid || !isAllJsConfigValid) {
    return false;
  }

  // ====================== 最终结果 ======================
  return true; // 或根据业务需求返回其他标识
}

// 组件内增删
function deleteSsJs(ssJsConfig: any) {
  let index = crudForm.ssJsConfig.indexOf(ssJsConfig);
  if (index !== -1) {
    crudForm.ssJsConfig.splice(index, 1);
  }
}

function addSsJs() {
  crudForm.ssJsConfig.push({
    dataKey: "",
    dataValue: "",
  });
}

function deleteSsCss(ssCssConfig: any) {
  let index = crudForm.ssCssConfig.indexOf(ssCssConfig);
  if (index !== -1) {
    crudForm.ssCssConfig.splice(index, 1);
  }
}

function addSsCss() {
  crudForm.ssCssConfig.push({
    dataValue: "",
  });
}

// 全局组件增删
function deleteSsAllJs(ssJsConfig: any) {
  let index = crudForm.ssAllJsConfig.indexOf(ssJsConfig);
  if (index !== -1) {
    crudForm.ssAllJsConfig.splice(index, 1);
  }
}

function addSsAllJs() {
  crudForm.ssAllJsConfig.push({
    dataKey: "",
    name: "",
    versionNum: "",
    dataValue: "",
  });
}

function deleteSsAllCss(ssCssConfig: any) {
  let index = crudForm.ssAllCssConfig.indexOf(ssCssConfig);
  if (index !== -1) {
    crudForm.ssAllCssConfig.splice(index, 1);
  }
}

function addSsAllCss() {
  crudForm.ssAllCssConfig.push({
    name: "",
    versionNum: "",
    dataValue: "",
  });
}

onMounted(() => {
  getTypeListFun();
  getTagListFun();
});

function getVersionListFun() {
  ComponentManageAPI.getVersionList({ componentId: crudForm.id }, 999, 1).then((res) => {
    tableData.value = res.records;
  });
}
// 暴露给父组件的方法和属性
defineExpose({
  ruleFormRef,
  crudForm,
  state,
  getEditorCode,
  setFormData: (data: any) => {
    if (data) {
      crudForm.id = data.id || "";
      crudForm.name = data.name || "";
      crudForm.type = data.type || "";
      crudForm.tagStr = data.tagStr || "";
      crudForm.installType = data.installType || "";
      crudForm.install = data.install || "";
      crudForm.description = data.description || "";
      crudForm.installExample = data.installExample || "";
      crudForm.version = data.version || "";
      crudForm.images = data.images || "";
      crudForm.publishLog = data.publishLog || "";
      crudForm.status = "0";
      crudForm.updateTime = data.updateTime || "";

      crudForm.warehouseAddress = data.warehouseAddress || "";
      crudForm.ssJsConfig = data.ssJsConfig || [{ dataKey: "", dataValue: "" }];
      crudForm.ssCssConfig = data.ssCssConfig || [{ dataValue: "" }];
      crudForm.ssAllJsConfig = data.ssAllJsConfig || [
        { dataKey: "", name: "", versionNum: "", dataValue: "" },
      ];
      crudForm.ssAllCssConfig = data.ssAllCssConfig || [
        { name: "", versionNum: "", dataValue: "" },
      ];

      tagList.value = [];
      historyTagList.value = [];

      // if (data.type) {
      //   editypeList.value = [data.type];
      // }

      if (data.tagStr) {
        tagList.value = data.tagStr.split(",");
      }

      if (data.status == "view") {
        editStatus.value = "view";
        disabledStatus.value = true;
        viewMd();
        getVersionListFun();
        importModel();
      } else if (data.status == "add") {
        editStatus.value = "add";
        disabledStatus.value = false;
        componentShuoShow.value = false;
        resources.value = [];
        importDisabled.value = false;
        showExample.value = false;
      } else {
        editStatus.value = "edit";
        getVersionListFun();
        importModel();
        disabledStatus.value = false;
        componentShuoShow.value = false;
        importDisabled.value = true;
        showExample.value = true;
      }
    }
  },
});
</script>
<style scoped lang="scss">
.crud-page {
  position: relative;
  height: 100%;
}
::v-deep .crudForm {
  .el-form-item {
    width: 100%;
    margin-right: 0;
    margin-bottom: 18px;
  }
  .el-cascader {
    width: 100%;
  }
}

.install-instructions,
.install-example {
  display: flex;
  justify-content: center;
  margin-bottom: 18px;
  position: relative;
  span {
    width: 100px;
    text-align: right;
    font-size: 14px;
    display: inline-block;
    padding-right: 12px;
    color: #606266;
  }
}
::v-deep .install-picture {
  display: flex;
  justify-content: center;
  margin-bottom: 0;
  .el-input__inner {
    cursor: pointer;
  }
}
.installDiv {
  border: 1px solid #dcdfe6;
  border-radius: 5px;
  display: inline-block;
  width: calc(100% - 100px);
  height: 110px;
  padding: 10px;
  .el-form-item {
    margin-bottom: 0;
  }
}
::v-deep .exampleDiv {
  border: 1px solid #dcdfe6;
  border-radius: 5px;
  display: inline-block;
  width: calc(100% - 100px);
  height: 500px;
  padding: 10px;
  .el-tabs__content {
    height: 435px;
    padding: 10px;
    overflow: auto;
  }
}
.exampleText {
  display: inline-block;
  width: calc(100% - 100px);
  color: #606266;
}
::v-deep textarea {
  resize: none; /* 禁止调整大小 */
}

.exampleDiv > .el-tabs__content {
  padding: 32px;
  color: #6b778c;
  font-size: 32px;
  font-weight: 600;
}
.exampleDiv .custom-tabs-label .el-icon {
  vertical-align: middle;
}
.exampleDiv .custom-tabs-label span {
  vertical-align: middle;
  margin-left: 4px;
  width: auto;
}

::v-deep .viewShowClass {
  p {
    padding-left: 20px;
    margin: 0 0 20px;
    font-size: 16px;
    font-weight: bold;
    color: #202020;
  }
  .el-input {
    width: 100%;
  }
}
::v-deep .history-table {
  // width: 100%;
  padding: 0 20px;
  margin: 20px 0;
  p {
    // padding-left:20px;
    margin: 0 0 20px;
    font-size: 16px;
    font-weight: bold;
    color: #202020;
  }
  .el-table__header th {
    background-color: #f6f6f6;
  }
}
.innerDrawer {
  .el-form-item {
    margin: 0 0 18px 0;
    width: 100%;
    .el-input {
      width: 100%;
    }
    .el-select {
      width: 100%;
    }
  }
  .install-instructions {
    .el-input {
      width: 100%;
    }
  }
}
::v-deep .componentMsg {
  p {
    padding-left: 20px;
    margin: 0 0 20px;
    font-size: 16px;
    font-weight: bold;
    color: #202020;
    width: 100%;
  }
  .el-input {
    width: 100%;
  }
}
::v-deep .componentExample {
  p {
    padding-left: 20px;
    margin: 0 0 20px;
    font-size: 16px;
    font-weight: bold;
    color: #202020;
    width: 100%;
  }
  .el-input {
    width: 100%;
  }
}
::v-deep .componentShuo {
  p {
    padding-left: 20px;
    margin: 0 0 20px;
    font-size: 16px;
    font-weight: bold;
    color: #202020;
    width: 100%;
  }
  .el-input {
    width: 100%;
  }
}
.el-tab-pane {
  height: 100%;
  overflow-y: auto;
}
#preview {
  padding: 20px;
  border: 1px solid #dcdfe6;
}
</style>

preview/index.vue

点击查看代码

复制代码
<template>
  <div class="preview" ref="preview"></div>
</template>

<script setup>
import { ref, onMounted, watch, inject, onUnmounted } from "vue";
import PreviewTemplate from "./preview-template.html?raw";

const preview = ref();

let proxy = ref(null);

// 注入store
const store = inject("store");

const template = ref(null)

const props = defineProps({
  state: {
    type: Object,
    default: () => ({}),
  },
  // 修改为更通用的资源引入配置
  resources: {
    type: Array,
    default: () => [
      // { type: 'js', name: 'vue', url: 'https://unpkg.com/[email protected]/dist/vue.esm-browser.js' },
      // { type: 'js', name: 'element-plus', url: 'https://unpkg.com/[email protected]/dist/index.full.mjs' },
      // { type: 'css', url: 'https://unpkg.com/[email protected]/dist/index.css' }
    ]
  }
});

watch(
  () => props.state,
  (newVal) => {
    // console.log('444444444444444444444')
    // console.log(template)
    setTimeout(() => {
      proxy.value = createProxy(template.value);
    }, 100)
  },
  { deep: true, immediate: true}
);

// 监听resources变化,重新创建沙盒
watch(
  () => props.resources,
  async () => {
    await createSandbox();
    // 重新创建代理
    proxy.value = createProxy(template.value);
  },
  { deep: true }
);

// 创建沙盒
function createSandbox () {
  // 清理旧的 iframe
  if (template.value) {
    template.value.remove();
    template.value = null;
  }
  
  template.value = document.createElement("iframe");
  template.value.setAttribute("frameborder", "0");
  template.value.style = "width: 100%; height:100%";
  
  // 创建基础HTML内容
  const baseHtml = PreviewTemplate;
  
  // 动态生成importmap
  const jsResources = props.resources.filter(resource => resource.type === 'js');
  const importMapScript = `<script type="importmap">
    {
      "imports": {
        ${jsResources.map(resource => `"${resource.name}": "${resource.url}"`).join(',\n        ')}
      }
    }
  <\/script>`;
  
  // 动态生成CSS链接
  const cssResources = props.resources.filter(resource => resource.type === 'css');
  const cssLinks = cssResources.map(resource => 
    `<link href="${resource.url}" rel="stylesheet" type="text/css" />`
  ).join('\n  ');
  
  // 组合最终的HTML内容
  template.value.srcdoc = baseHtml.replace('</head>', `${importMapScript}\n  ${cssLinks}\n</head>`);
  
  preview.value.appendChild(template.value);

  // 等待iframe加载完成
  return new Promise((resolve) => {
    template.value.onload = () => {
      resolve();
    };
  });
}

// 创建代理,用于监听code 变化,告诉沙盒重新渲染
function createProxy (iframe) {
  let _iframe = iframe;

  const stopWatch = watch(() => store?.code, (newCode) => {
    if (newCode) {
      compile(newCode);
    }
  }, { immediate: true });

  function compile (code) {
    if (!code?.trim()) {
      code = "<script setup> // <\/script>"
    }

    const compiledCode = store?.compile(code);

    if (_iframe?.contentWindow) {
      _iframe.contentWindow.postMessage(
        { type: "eval", code: compiledCode },
        "*"
      );
    }
  }

  // 销毁沙盒
  function destory () {
    _iframe?.remove();
    _iframe = null;
    stopWatch?.();
  }

  return {
    compile,
    destory,
  };
}

onMounted(async () => {
  await createSandbox();
  proxy.value = createProxy(template.value);
});

onUnmounted(() => proxy.value?.destory());
</script>

<style scoped>
.preview {
  width: 100%;
  height: 100%;
  overflow: hidden;
}
</style>

html预览html

点击查看代码

复制代码
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <style>
      body {
        margin: 0;
        padding: 20px;
      }
      #preview-content {
        width: 100%;
        height: 100%;
      }
    </style>
  </head>
  <body>
    <div id="preview-content"></div>

    <script>
      // 监听 message,preview/index.vue 通过 postmessage 传递需要执行的代码
      window.addEventListener("message", ({ data }) => {
        const { type, code } = data;
        if (type === "eval") {
          handleEval(code);
        }
      });

      // 处理需要执行的代码
      function handleEval(code) {
        const previewContent = document.getElementById('preview-content');
        if (!previewContent) return;

        try {
          // 清空之前的内容
          previewContent.innerHTML = '';

          // 分离 HTML 和 JavaScript 代码
          const htmlMatch = code.match(/<div[^>]*>[\s\S]*?<\/div>/);
          const scriptMatches = code.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g);
          const scriptSrcMatches = code.matchAll(/<script[^>]*src="([^"]*)"[^>]*>/g);

          // 创建一个临时的容器来处理脚本
          const tempContainer = document.createElement('div');
          tempContainer.style.display = 'none';
          document.body.appendChild(tempContainer);

          // 先添加所有外部脚本
          const externalScripts = [];
          for (const match of scriptSrcMatches) {
            const script = document.createElement('script');
            script.src = match[1];
            externalScripts.push(script);
            tempContainer.appendChild(script);
          }

          // 等待外部脚本加载完成
          Promise.all(externalScripts.map(script => {
            return new Promise((resolve, reject) => {
              script.onload = resolve;
              script.onerror = reject;
            });
          })).then(() => {
            // 渲染 HTML 内容
            if (htmlMatch) {
              previewContent.innerHTML = htmlMatch[0];
            } else {
              // 如果没有找到 HTML 内容,直接设置整个代码
              previewContent.innerHTML = code;
            }

            // 添加并执行所有内联脚本
            for (const match of scriptMatches) {
              const scriptContent = match[1];
              const isModule = match[0].includes('type="module"');
              const hasImport = scriptContent.includes('import ');

              if (isModule || hasImport) {
                // 对于 ES 模块,创建新的脚本标签
                const script = document.createElement('script');
                script.type = 'module';
                // 如果使用全局 Vue,需要修改 import 语句
                const modifiedContent = scriptContent.replace(
                  /import\s*{\s*([^}]+)\s*}\s*from\s*['"]vue['"]/g,
                  'const { $1 } = Vue'
                );
                script.textContent = modifiedContent;
                previewContent.appendChild(script);
              } else {
                // 对于全局脚本,使用 Function 构造器执行
                try {
                  new Function(scriptContent)();
                } catch (error) {
                  console.error('Error executing script:', error);
                  previewContent.innerHTML = `<pre>Error executing script: ${error.message}</pre>`;
                }
              }
            }

            // 清理临时容器
            tempContainer.remove();
          }).catch(error => {
            console.error('Error loading scripts:', error);
            previewContent.innerHTML = `<pre>Error loading scripts: ${error.message}</pre>`;
            tempContainer.remove();
          });
        } catch (error) {
          console.error('Error displaying content:', error);
          previewContent.innerHTML = `<pre>Error: ${error.message}</pre>`;
        }
      }
    </script>
  </body>
</html>

MonacoEditor配置

点击查看代码

复制代码
<template>
  <div v-show="visible" class="youlai-editor-wrapper" ref="monacoEdit"></div>
</template>

<script setup>
import { toRaw, onUnmounted, onMounted, watch } from "vue";
// 导入monaco编辑器
import * as monaco from "monaco-editor/esm/vs/editor/editor.main.js";
import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution";

// 编辑器容器div
const monacoEdit = ref(null);
// 编辑器实列
const editor = ref(null);

const emits = defineEmits(["update:modelValue"]);
const props = defineProps({
  modelValue: {
    type: String,
    default: "",
  },
  visible: {
    type: Boolean,
    default: false,
  },
});

// 注册 Vue 语言和语法高亮
const registerVueLanguage = () => {
  monaco.languages.register({ id: "vue" });
  monaco.languages.setMonarchTokensProvider("vue", {
    tokenizer: {
      root: [
        [/<template>/, "keyword"],
        [/<script>/, "keyword"],
        [/<style>/, "keyword"],
        [/<\/template>/, "keyword"],
        [/<\/script>/, "keyword"],
        [/<\/style>/, "keyword"],
        [/@\w+/, "decorator"],
        [/{{[^}]+}}/, "variable"],
        [/v-\w+/, "directive"],
        [/:[a-zA-Z0-9_-]+/, "binding"],
        [/@[a-zA-Z0-9_-]+/, "event"],
      ],
    },
  });
};

// 配置 TypeScript 编译选项
const configureTypeScript = () => {
  monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
    target: monaco.languages.typescript.ScriptTarget.ES2020,
    module: monaco.languages.typescript.ModuleKind.ESNext,
    moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
    strict: true,
    esModuleInterop: true,
    skipLibCheck: true,
    allowSyntheticDefaultImports: true,
    sourceMap: true,
    jsx: monaco.languages.typescript.JsxEmit.React,
    baseUrl: ".",
    paths: {
      "@/*": ["src/*"],
    },
  });
  // monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
  //   allowNonTsExtensions: true,
  // })
};

// 添加 Vue 3 类型定义
const addVueTypeDefinitions = () => {
  monaco.languages.typescript.typescriptDefaults.addExtraLib(
    `
    // Vue 3 核心类型定义
    declare module 'vue' {
      import { ComponentOptions } from 'vue';
      
      export declare const createApp: (rootComponent: ComponentOptions<any>) => any;
      
      export interface DefineComponent<
        PropsOrPropOptions = {},
        RawBindings = {},
        D = {},
        C extends ComputedOptions = ComputedOptions,
        M extends MethodOptions = MethodOptions,
        Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
        Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
        E extends EmitsOptions = EmitsOptions,
        EE extends string = string,
        PropsDefaults = PropsDefaultsType<PropsOrPropOptions>
      > {
        // Vue 组件类型定义
      }
      
      // 其他 Vue 3 类型...
    }
    
    // Vue Router 类型定义
    declare module 'vue-router' {
      import { Router, RouteLocationNormalized, RouteRecordRaw } from 'vue-router';
      
      export declare const createRouter: (options: any) => Router;
      export declare const createWebHistory: () => any;
      
      // 其他 Vue Router 类型...
    }
  `,
    "node_modules/@types/vue/index.d.ts"
  );
};

const initEdit = () => {
  setTimeout(() => {
    if (monacoEdit.value) {
      // 注册 Vue 语言支持
      registerVueLanguage();
      // 配置 TypeScript
      configureTypeScript();
      // 添加 Vue 类型定义
      addVueTypeDefinitions();

      // 创建模型
      const model = monaco.editor.createModel(
        props.modelValue,
        "html",
        monaco.Uri.parse("inmemory://model/1")
      );

      // 创建编辑器实列
      editor.value = monaco.editor.create(monacoEdit.value, {
        model,
        theme: "vs-light", // 官方自带三种主题vs, hc-black, or vs-dark
        autoIndex: true,
        language: "html", // 语言类型
        tabCompletion: "on",
        cursorSmoothCaretAnimation: true,
        minimap: {
          enabled: true,
        },
        formatOnPaste: false,
        mouseWheelZoom: true,
        folding: true, //代码折叠
        selectOnLineNumbers: true, // 显示行号
        wordWrap: "on", // 代码超出换行
        overviewRulerBorder: false, // 不要滚动条的边框
        foldingHighlight: true, // 折叠等高线
        foldingStrategy: "indentation", // 折叠方式  auto | indentation
        showFoldingControls: "always", // 是否一直显示折叠 always | mouseover
        disableLayerHinting: true, // 等宽优化
        emptySelectionClipboard: false, // 空选择剪切板
        selectionClipboard: false, // 选择剪切板
        automaticLayout: true, // 自动布局
        codeLens: false, // 代码镜头
        scrollBeyondLastLine: false, // 滚动完最后一行后再滚动一屏幕
        colorDecorators: true, // 颜色装饰器
        accessibilitySupport: "off", // 辅助功能支持  "auto" | "off" | "on"
        lineNumbers: "on", // 行号 取值: "on" | "off" | "relative" | "interval" | function
        lineNumbersMinChars: 5, // 行号最小字符   number
        enableSplitViewResizing: false,
        readOnly: false, //是否只读  取值 true | false
        scrollbar: {
          useShadows: false,
          verticalHasArrows: false,
          horizontalHasArrows: false,
          vertical: "visible",
          horizontal: "visible",
          verticalScrollbarSize: 10,
          horizontalScrollbarSize: 10,
          arrowSize: 30,
          mouseWheelScrollSensitivity: 1,
          alwaysConsumeMouseWheel: false,
        },
      });

      // 编辑器内容变更时回调
      editor.value.onDidChangeModelContent(() => {
        let code = toRaw(editor.value).getValue();
        emits("update:modelValue", code);
      });

      // 添加被动事件监听器
      const editorElement = monacoEdit.value;
      if (editorElement) {
        editorElement.addEventListener("wheel", () => {}, { passive: true });
        editorElement.addEventListener("touchstart", () => {}, { passive: true });
        editorElement.addEventListener("touchmove", () => {}, { passive: true });
      }
    }
  }, 100);
};

const getEditValue = () => {
  return toRaw(editor.value).getValue();
};

defineExpose({ getEditValue });

// 添加对 visible prop 的监听
watch(
  () => props.visible,
  (newVal) => {
    if (newVal) {
      // 当对话框显示时,确保编辑器已初始化
      // if (!editor.value) {
      //   initEdit();
      // }
      if (editor.value) {
        const rawEditor = toRaw(editor.value);
        const model = rawEditor.getModel();

        // 分步释放资源
        rawEditor.dispose();
        if (model && !model.isDisposed()) {
          model.dispose();
        }

        initEdit();
      }else{
        initEdit();
      }
    }
  },
  { immediate: true }
);


onUnmounted(() => {
  if (editor.value) {
    const rawEditor = toRaw(editor.value);
    const model = rawEditor.getModel();

    // 分步释放资源
    rawEditor.dispose();
    if (model && !model.isDisposed()) {
      model.dispose();
    }
  }
});
</script>

<style scoped>
.youlai-editor-wrapper {
  width: 100%;
}
</style>