当前位置: 首页 > wzjs >正文

河北省网站备案系统网站构成三要素

河北省网站备案系统,网站构成三要素,网站搜索引擎优化,网站建设专业团队交互说明 在编辑器中输入{时&#xff0c;会自动弹出选项弹窗&#xff0c;然后可以选值插入。 代码 父组件 <variable-editorv-model"content":variables"variables"placeholder"请输入模板内容..."blur"handleBlur" />data…

交互说明 

在编辑器中输入{时,会自动弹出选项弹窗,然后可以选值插入。

代码

父组件

<variable-editorv-model="content":variables="variables"placeholder="请输入模板内容..."@blur="handleBlur"
/>data() {return {content: "这是一个示例 {user.name}",variables: [{id: "user",label: "user",type: "object",children: [{ id: "user.name", label: "name", type: "string" },{ id: "user.age", label: "age", type: "number" },],},{id: "items",label: "items",type: "array<object>",children: [{ id: "items.title", label: "title", type: "string" },{ id: "items.price", label: "price", type: "number" },],},],};},handleBlur(val) {console.log("编辑器内容已更新:", val);
},

子组件 

<template><div class="variable-editor"><div ref="editorRef" class="editor-container"></div><el-popoverv-if="variables && variables.length > 0"ref="popover"placement="left-start":value="popoverOpen":visible-arrow="false"trigger="manual"@after-enter="handleAfterOpen"><divclass="tree-wrap my-variable-popover"tabindex="-1"@keydown="handleKeyDown"@keydown.capture="handleKeyDownCapture"ref="treeRef"><el-tree:data="variables":props="defaultProps"default-expand-all:highlight-current="true":current-node-key="selectedKeys[0]"@current-change="handleCurrentChange"@node-click="handleVariableInsert"ref="tree"node-key="id"><div slot-scope="{ node, data }" class="flex-row-center"><i v-if="getTypeIcon(data)" :class="getTypeIcon(data)"></i><span class="ml-1">{{ node.label }}</span></div></el-tree></div><span slot="reference" ref="anchorRef" class="anchor-point"></span></el-popover></div>
</template><script>
import {EditorView,ViewPlugin,placeholder,Decoration,keymap,
} from "@codemirror/view";
import { EditorState, RangeSetBuilder, StateEffect } from "@codemirror/state";
import { defaultKeymap, insertNewlineAndIndent } from "@codemirror/commands";// 扁平化树结构
const flattenTree = (nodes, result = []) => {for (const node of nodes) {result.push({ key: node.id, title: node.label });if (node.children) {flattenTree(node.children, result);}}return result;
};export default {name: "VariableEditor",props: {value: {type: String,default: "",},variables: {type: Array,default: () => [],},placeholder: {type: String,default: "请输入内容...",},},data() {return {popoverOpen: false,selectedKeys: [],editorView: null,lastCursorPos: null,flattenedTree: [],defaultProps: {children: "children",label: "label",},// 类型图标映射typeIcons: {string: "el-icon-document",number: "el-icon-tickets",boolean: "el-icon-switch-button",object: "el-icon-folder","array<object>": "el-icon-collection",},};},computed: {currentIndex() {return this.flattenedTree.findIndex((node) => node.key === this.selectedKeys[0]);},},mounted() {this.flattenedTree = flattenTree(this.variables);this.initEditor();},beforeDestroy() {if (this.editorView) {this.editorView.destroy();}},watch: {variables: {handler(newVal) {this.flattenedTree = flattenTree(newVal);if (this.editorView) {// 重新配置编辑器以更新插件this.editorView.dispatch({effects: StateEffect.reconfigure.of(this.createExtensions()),});}},deep: true,},value(newVal) {if (this.editorView && newVal !== this.editorView.state.doc.toString()) {this.editorView.dispatch({changes: {from: 0,to: this.editorView.state.doc.length,insert: newVal,},});}},popoverOpen(val) {if (val && this.flattenedTree.length > 0) {this.selectedKeys = [this.flattenedTree[0].key];this.$nextTick(() => {if (this.$refs.tree) {this.$refs.tree.setCurrentKey(this.selectedKeys[0]);}});}},},methods: {getTypeIcon(data) {return this.typeIcons[data.type] || this.typeIcons.string;},initEditor() {if (!this.$refs.editorRef) return;this.editorView = new EditorView({doc: this.value,parent: this.$refs.editorRef,extensions: this.createExtensions(),});// 添加失焦事件this.$refs.editorRef.addEventListener("blur", this.onEditorBlur);},createExtensions() {return [placeholder(this.placeholder || "请输入内容..."),EditorView.editable.of(true),EditorView.lineWrapping,keymap.of([...defaultKeymap,{ key: "Enter", run: insertNewlineAndIndent },]),EditorState.languageData.of(() => {return [{ autocomplete: () => [] }];}),this.createUpdateListener(),this.createVariablePlugin(),this.createInterpolationPlugin(this.variables),];},createUpdateListener() {return EditorView.updateListener.of((update) => {if (update.docChanged) {// const content = update.state.doc.toString();// 不要在每次更改时都触发,而是在失焦时触发}});},createVariablePlugin() {const self = this;return ViewPlugin.fromClass(class {constructor(view) {this.view = view;}update(update) {if (update.docChanged || update.selectionSet) {const pos = update.state.selection.main.head;const doc = update.state.doc.toString();// 只有当光标位置真正变化时才更新if (self.lastCursorPos !== pos) {self.lastCursorPos = pos;// 延迟更新 Popover 位置setTimeout(() => {self.$refs.popover &&self.$refs.popover.$el &&self.$refs.popover.updatePopper();}, 10);}// 1. 正则查找所有的 {xxx}const regex = /\{(.*?)\}/g;let match;let inInterpolation = false;while ((match = regex.exec(doc)) !== null) {const start = match.index;const end = start + match[0].length;if (pos > start && pos < end) {// 光标在插值表达式内inInterpolation = true;setTimeout(() => {const coords = this.view.coordsAtPos(pos);const editorRect = this.view.dom.getBoundingClientRect();if (coords) {self.$refs.anchorRef.style.position = "absolute";self.$refs.anchorRef.style.left = `${coords.left - editorRect.left - 10}px`;self.$refs.anchorRef.style.top = `${coords.top - editorRect.top}px`;self.$refs.anchorRef.dataset.start = start;self.$refs.anchorRef.dataset.end = end;self.popoverOpen = true;}}, 0);break;}}if (!inInterpolation) {// 检测输入 { 的情况const prev = update.state.sliceDoc(pos - 1, pos);if (prev === "{") {setTimeout(() => {const coords = this.view.coordsAtPos(pos);const editorRect = this.view.dom.getBoundingClientRect();if (coords) {self.$refs.anchorRef.style.position = "absolute";self.$refs.anchorRef.style.left = `${coords.left - editorRect.left - 10}px`;self.$refs.anchorRef.style.top = `${coords.top - editorRect.top}px`;self.$refs.anchorRef.dataset.start = pos;self.$refs.anchorRef.dataset.end = pos;self.popoverOpen = true;}}, 0);} else {self.popoverOpen = false;}}}}});},createInterpolationPlugin(variables) {const self = this;return ViewPlugin.fromClass(class {constructor(view) {this.decorations = this.buildDecorations(view);}update(update) {if (update.docChanged || update.viewportChanged) {this.decorations = this.buildDecorations(update.view);}}buildDecorations(view) {const builder = new RangeSetBuilder();const doc = view.state.doc;const text = doc.toString();const regex = /\{(.*?)\}/g;let match;while ((match = regex.exec(text)) !== null) {const [full, expr] = match;const start = match.index;const end = start + full.length;const isValid = self.validatePath(variables, expr.trim());const deco = Decoration.mark({class: isValid? "cm-decoration-interpolation-valid": "cm-decoration-interpolation-invalid",});builder.add(start, end, deco);}return builder.finish();}},{decorations: (v) => v.decorations,});},validatePath(schema, rawPath) {const segments = rawPath.replace(/\[(\d+)\]/g, "[$1]").split(".");// 递归匹配function match(nodes, index) {if (index >= segments.length) return true;const currentKey = segments[index];for (const node of nodes) {const { label: title, type, children } = node;// 匹配数组字段,如 abc[0]if (/\[\d+\]$/.test(currentKey)) {const name = currentKey.replace(/\[\d+\]$/, "");if (title === name && type === "array<object>" && children) {return match(children, index + 1);}}// 匹配普通字段if (title === currentKey) {if ((type === "object" || type === "array<object>") && children) {return match(children, index + 1);}// 如果不是object类型,且已经是最后一个字段return index === segments.length - 1;}}return false;}return match(schema, 0);},handleAfterOpen() {if (this.$refs.treeRef) {this.$refs.treeRef.focus();}},handleCurrentChange(data) {if (data) {this.selectedKeys = [data.id];}},handleVariableInsert(data) {const key = data.id;this.selectedKeys = [key];const view = this.editorView;if (!view) return;const state = view.state;const pos = state.selection.main.head;const doc = state.doc.toString();let insertText = `{${key}}`;let targetFrom = pos;let targetTo = pos;let foundInBraces = false;// 检查光标是否在 {...} 内部const regex = /\{[^}]*\}/g;let match;while ((match = regex.exec(doc)) !== null) {const [full] = match;const start = match.index;const end = start + full.length;if (pos > start && pos < end) {targetFrom = start;targetTo = end;foundInBraces = true;break;}}// 如果不在 {...} 中,但光标前是 `{`,只插入 `${key}}`,不要加多一个 `{`if (!foundInBraces && doc[pos - 1] === "{") {targetFrom = pos;insertText = `${key}}`; // 前面已经有 {,只补后半段}const transaction = state.update({changes: {from: targetFrom,to: targetTo,insert: insertText,},selection: { anchor: targetFrom + insertText.length },});view.dispatch(transaction);view.focus();this.popoverOpen = false;},onEditorBlur(e) {const related = e.relatedTarget;// 如果焦点转移到了 Popover 内部,则不处理 blurif (related && related.closest(".my-variable-popover")) {return;}const view = this.editorView;if (view) {this.$emit("input", view.state.doc.toString());this.$emit("blur");}},handleKeyDownCapture(e) {if (!["ArrowUp", "ArrowDown", "Enter"].includes(e.key)) {e.stopPropagation();}},handleKeyDown(e) {if (!["ArrowUp", "ArrowDown", "Enter"].includes(e.key)) return;if (e.key === "ArrowDown") {let nextKey;if (this.currentIndex < this.flattenedTree.length - 1) {nextKey = this.flattenedTree[this.currentIndex + 1].key;} else {nextKey = this.flattenedTree[0].key;}this.selectedKeys = [nextKey];this.$refs.tree.setCurrentKey(nextKey);} else if (e.key === "ArrowUp") {let prevKey;if (this.currentIndex > 0) {prevKey = this.flattenedTree[this.currentIndex - 1].key;} else {prevKey = this.flattenedTree[this.flattenedTree.length - 1].key;}this.selectedKeys = [prevKey];this.$refs.tree.setCurrentKey(prevKey);} else if (e.key === "Enter" && this.selectedKeys[0]) {// 查找对应的节点数据const findNodeData = (key, nodes) => {for (const node of nodes) {if (node.id === key) return node;if (node.children) {const found = findNodeData(key, node.children);if (found) return found;}}return null;};const nodeData = findNodeData(this.selectedKeys[0], this.variables);if (nodeData) {this.handleVariableInsert(nodeData);}}},},
};
</script><style scoped>
.variable-editor {position: relative;width: 100%;
}.editor-container {width: 100%;border: 1px solid #dcdfe6;border-radius: 4px;min-height: 150px;overflow: hidden;transition: border-color 0.2s, box-shadow 0.2s;
}/* CodeMirror 6 编辑器样式 */
:global(.cm-editor) {height: 150px !important;min-height: 150px !important;overflow-y: auto;
}/* 编辑器获取焦点时的样式 */
:global(.cm-editor.cm-focused) {outline: none;
}/* 使用更具体的选择器确保只有一层边框高亮 */
.editor-container:focus-within {border-color: #409eff !important;
}.anchor-point {position: absolute;z-index: 10;
}.tree-wrap {min-width: 200px;
}.flex-row-center {display: flex;align-items: center;flex-wrap: nowrap;
}.ml-1 {margin-left: 4px;
}/* 添加到全局样式中 */
:global(.cm-decoration-interpolation-valid) {color: #409eff;background-color: rgba(64, 158, 255, 0.1);
}:global(.cm-decoration-interpolation-invalid) {color: #f56c6c;background-color: rgba(245, 108, 108, 0.1);text-decoration: wavy underline #f56c6c;
}
</style>

依赖安装 

npm install @codemirror/state @codemirror/view @codemirror/commands

http://www.dtcms.com/wzjs/837708.html

相关文章:

  • 重庆做手机网站建设平面设计是做什么的啊
  • 网站建设都需要哪些工具或软件iis6无法新建网站
  • 做微信商城网站深圳市住建局造价站
  • 一个ip地址做多个网站黑龙江 俄语网站制作
  • 昆明网站定制开发深圳公司招聘网最新招聘信息
  • 上海哪家做网站关键词排名康复中心网站建设方案
  • 绍兴微网站建设wordpress 栏目分页
  • 萧山网站建设网站建设推进情况
  • 中国联通网站建设与维护网站内容建设的原则是什么
  • 何如做外贸网站推网唐山网站建设哪家优惠
  • 商务网站可以做哪些关键词优化一年的收费标准
  • 吉林智能网站建设企业网站整体排名大幅下降
  • 莱芜网站建设及优化全景图网站怎么做
  • 短视频制作完成网站网站原型图展示
  • python 兼职网站开发广东网络公司网站建设
  • 国内h5网站欣赏建设银行信用卡申请官方网站
  • 网站建设与网页设计专业企业产品推广运营公司
  • 最贵网站建设多少钱厦门小程序开发
  • 建设农业网站西宁网站建设多少钱
  • 东营 网站 建设周口seo推广
  • 做视频挣钱的网站宜州网站建设服务
  • 做网站赚多少钱百度网盟推广价格
  • 高端定制网站开发建站教程详解百度指数介绍
  • 做教育的网站有哪些内容吗电商和网站设计哪个好
  • 如果是创建的网站做网站费用分几块
  • 在网站里继费网站可以给pdf做笔记
  • 南昌市建设监督网站站长黑群晖做php网站
  • 网站主机和服务器的区别delphi 可做网站吗
  • 个人网页网站建设路由器上做网站
  • 成都网站只国际新闻最新消息内容