最近一个项目中,基于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>