拓冰建站拓冰建站
首页 / 资讯中心 / 正文

X6 内置 Attributes 与自定义 Attribute 注册完全指南

X6 内置 Attributes 与自定义 Attribute 注册完全指南【免费下载链接】X6 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6X6 是基于 SVG 与 HTML 渲染的 JavaScript 图编辑引擎其样式与几何布局体系建立在attrs之上。attrs不仅支持原生 SVG 属性还内置了一整套特殊属性Attributes用于实现相对定位、尺寸同步、文本换行、路径沿边等高频能力。本文以 site/docs/api/registry/attr.en.md 为主线结合仓库源码src/registry/attr与测试用例tests/registry/attr/ref.spec.ts逐项讲解内置属性的语义、取值规则与适用元素并深入registry注册机制演示如何通过Graph.registerAttr编写自定义属性实现完全可控的渲染行为。一、Attributes 概述在 X6 中节点的外观由markupDOM 结构与attrs样式与几何共同描述。markup为每个结构元素指定selectorattrs则通过selector精确命中元素并施加属性graph.addNode({ markup: [ { tagName: rect, selector: body }, { tagName: rect, selector: custom }, ], attrs: { body: { width: 100, height: 50, fill: #f5f5f5 }, custom: { fill: red }, }, })attrs的值可以是浏览器原生 SVG 属性如fill、stroke、x、y也可以是 X6 定义的特殊属性。特殊属性会被 X6 在渲染阶段转换成浏览器可识别的属性src/registry/attr/index.ts 中的attrPresets将 raw.ts 的原生属性别名与特殊属性合并后注册进attrRegistry。下文所有内置属性均来自 src/registry/attr 目录下的实现。二、相对定位属性ref 系列相对定位是 X6 最有价值的特性之一子元素可以相对于另一个参考元素reference element定位与缩放。参考元素由ref指定其选择器与markup中定义的selector一致。渲染时参考元素的包围盒refBBox即 Rectangle作为所有计算的基准。1. ref指定参考元素ref本身不产生渲染效果它只是声明以哪个元素为参考。下面代码中custom元素的宽高分别取body的一半graph.addNode({ markup: [ { tagName: rect, selector: body }, { tagName: rect, selector: custom }, ], attrs: { body: { width: 100, height: 50 }, // custom 元素的长宽是 body 的一半 custom: { ref: body, refWidth: 0.5, refHeight: 0.5 }, }, })ref的实现位于 src/registry/attr/ref.ts它故意不设置任何属性值仅作为占位真正的计算交给refX/refY/refWidth等属性完成。若未指定ref则参考元素默认为节点本身refBBox即节点位置与尺寸构成的矩形。2. refX / refY相对左上角定位refX设置元素的x坐标基准是参考元素左上角的x坐标refY对应y坐标基准是左上角的y坐标。取值规则值在[0, 1]之间或是百分比如50%按参考元素宽高的比例偏移。refX: 0.5表示目标x坐标在参考x坐标右侧偏移参考宽度的 50%。值0或1按绝对像素偏移。refX: 20表示目标x坐标在参考x坐标右侧偏移 20px。源码positionWrapper(x, width, origin)src/registry/attr/ref.ts正是这一规则的实现百分比或0 v 1时delta refBBox[axis] refBBox[dimension] * value否则delta refOrigin[axis] value。测试tests/registry/attr/ref.spec.ts 验证了在refBBox Rectangle(100, 100, 200, 150)时refX(50%)得到x 200refX(20)得到x 120。3. refX2 / refY2同时叠加相对与绝对偏移当需要相对偏移 固定像素同时生效时使用refX2/refY2{ refX: 50%, // 参考宽度的一半 refX2: 20, // 再额外偏移 20px }即目标x坐标 参考x坐标 参考宽度的 50% 20px。源码中refX2 refX、refY2 refYsrc/registry/attr/ref.ts是简单别名真正叠加的效果来自渲染管线分别执行两次定位计算。4. refDx / refDy相对右下角定位与refX/refY不同refDx/refDy以参考元素右下角坐标为基准源码positionWrapper(x, width, corner)src/registry/attr/ref.ts。取值规则与上面完全一致[0, 1]或百分比表示按参考宽高的比例0或1表示绝对像素偏移。测试tests/registry/attr/ref.spec.ts 中refDx(-10)在refBBox Rectangle(100, 100, 200, 150)下得到x 290即右下角 300 减 10refDy(-15)得到y 235。5. refWidth / refHeight尺寸跟随参考元素refWidth设置元素宽度refHeight设置元素高度基准是参考元素的宽高[0, 1]或百分比如75%按参考宽高的比例。refWidth: 0.75表示宽度为参考宽度的 75%。0或1在参考宽高基础上增减。refWidth: 20表示比参考元素宽 20px。源码setWrapper(width, width)src/registry/attr/ref.ts中超出部分用Math.max(value refBBox[dimension], 0)兜底避免出现负尺寸。测试验证refWidth(50%)在参考宽 200 时得到 100refWidth(-20)得到 180tests/registry/attr/ref.spec.ts。⚠️ 注意refWidth/refHeight仅适用于支持宽高属性的元素如rect。6. refWidth2 / refHeight2相对与绝对尺寸叠加{ refWidth: 75%, // 参考宽度的 75% refWidth2: 20, // 再增加 20px }最终宽度 参考宽度的 75% 20px。源码同样以别名方式实现src/registry/attr/ref.ts。7. refCx / refCy圆心跟随参考元素refCx设置原生cx属性基准是参考元素的宽度refCy设置原生cy基准是参考元素的高度。取值规则同前[0, 1]/百分比为比例0/1为绝对偏移源码 src/registry/attr/ref.ts。仅适用于支持cx/cy的元素如ellipse。8. refRx / refRy圆角半径跟随参考元素refRx设置rxrefRy设置ry分别以参考元素宽度、高度为基准源码 src/registry/attr/ref.ts。仅适用于支持rx/ry的元素如rect。9. refRInscribed别名 refR内切圆半径refRInscribed设置r以参考元素宽高较小者为基准长度。源码 src/registry/attr/ref.ts 会动态比较refBBox.height与refBBox.width谁小就用谁的setWrapper计算。测试tests/registry/attr/ref.spec.ts 验证宽 200 高 100 时取宽度50% 得 50宽 100 高 200 时取高度50% 得 50。别名refR refRInscribed定义在 src/registry/attr/ref.ts。10. refRCircumscribed外接圆半径refRCircumscribed同样设置r但以参考元素的对角线长度为基准源码 src/registry/attr/ref.ts对角线计算为sqrt(h² w²)。测试验证refRCircumscribed(50%)得到对角线的一半tests/registry/attr/ref.spec.ts。两者配合可让圆形元素精确内切或外接参考矩形。三、path 与 points 属性让形状适配参考元素X6 提供四个特殊属性将path的d与polygon/polyline的points等比缩放并对齐到参考元素。它们的实现核心是shapeWrappersrc/registry/attr/ref.ts解析路径/点集得到形状及其包围盒用refBBox.getMaxScaleToFit计算等比缩放系数再按resetOffset决定是否把原点平移到0,0。Keep系列保留原数据的偏移Reset系列消除偏移。1. refDKeepOffset缩放 pathData 使其与参考元素同尺寸并将目标path起点与参考元素起点对齐保留pathData 自带偏移import { Graph, Node } from antv/x6 const Path Node.define({ markup: [{ tagName: path }], attrs: { path: { refDKeepOffset: M 10 10 30 10 30 30 z, // path 偏移为 10,10 fill: red, stroke: black, }, }, }) const container document.getElementById(container)! const graph new Graph({ container, width: 800, height: 80, grid: true }) const path new Path().resize(40, 40).addTo(graph) const view graph.findView(path) console.log(view.findOne(path).getAttribute(d)) // M 10 10 50 10 50 50 z2. refDResetOffset别名 refD行为与refDKeepOffset相同但会移除pathData 的偏移将左上角平移到原点与参考元素严格对齐// 同样配置但使用 refDResetOffset console.log(view.findOne(path).getAttribute(d)) // M 0 0 40 0 40 40 z别名refD refDResetOffsetsrc/registry/attr/ref.ts。3. refPointsKeepOffset针对polygon/polyline的points属性语义与refDKeepOffset一致等比缩放 对齐起点 保留偏移const Polygon Node.define({ markup: [{ tagName: polygon }], attrs: { polygon: { refPointsKeepOffset: 10,10 30,10 30,30, // points 偏移为 10,10 fill: red, stroke: black, }, }, }) // resize(40, 40) 后 // 10,10 50,10 50,504. refPointsResetOffset别名 refPoints缩放并对齐同时移除偏移与参考元素严格对齐// 同样配置但使用 refPointsResetOffset // 0,0 40,0 40,40别名refPoints refPointsResetOffsetsrc/registry/attr/ref.ts。5. resetOffset独立的偏移归零若只想消除某个已有d的偏移而不缩放可使用resetOffsetpath.attr({ path: { d: M 10 10 20 20, resetOffset: true, // 平移后 d 的值变为 M 0 0 10 10 }, })其实现是返回{ x: -refBBox.x, y: -refBBox.y }的偏移定义src/registry/attr/align.ts。四、对齐属性xAlign 与 yAlignxAlign控制元素与自身x坐标的水平对齐方式left元素左边与x对齐middle元素中心与x对齐right元素右边与x对齐yAlign控制垂直对齐top元素顶边与y对齐middle元素中心与y对齐bottom元素底边与y对齐实现为offsetWrapper(x, width, right)/offsetWrapper(y, height, bottom)src/registry/attr/align.ts通过返回偏移量将元素中心/边线对齐到指定坐标。可交互示例见仓库演示 examples/src/pages/shape 下的相关页面。五、视觉属性fill / stroke / filter / style / html / title1. fill / stroke支持渐变填充当fill值为对象时表示渐变填充字符串则按普通颜色处理rect.attr(body/fill, { type: linearGradient, stops: [ { offset: 0%, color: #E67E22 }, { offset: 20%, color: #D35400 }, { offset: 40%, color: #E74C3C }, { offset: 60%, color: #C0392B }, { offset: 80%, color: #F39C12 }, ], })实现src/registry/attr/fill.ts通过qualify: ObjectExt.isPlainObject判断对象值set调用view.graph.defineGradient生成defs中的渐变并把fill替换为url(#id)。stroke用法相同源码见 src/registry/attr/stroke.ts。2. filterSVG 滤镜filter值为对象时使用 X6 内置滤镜定义字符串则使用原生形式如url(#myfilter)rect.attr(body/filter, { name: dropShadow, args: { dx: 2, dy: 2, blur: 3 }, })实现src/registry/attr/filter.ts同样以isPlainObject限定set调用view.graph.defineFilter并返回url(#id)。可用的滤镜名称dropShadow、blur、outline等及参数见 src/registry/filter 目录及文档 site/docs/api/registry/filter.zh.md。3. style内联 CSSstyle将对象形式的键值对以内联样式写入元素rect.attr(body/style, { fill: red, cursor: pointer })实现src/registry/attr/style.ts调用Dom.css(elem, styles)同样以isPlainObject限定。4. html设置 innerHTMLhtml直接设置元素内部 HTMLrect.attr(body/html, div classinnerHello/div)实现src/registry/attr/html.ts为elem.innerHTML \${html}。5. titleSVG 描述title为目标元素追加title子元素不影响渲染只提供描述悬停提示rect.attr(body/title, Description of the rectangle)实现src/registry/attr/title.ts只对 SVG 元素生效qualify判断elem instanceof SVGElement会复用已存在的title子元素并保持其为第一个子节点。六、文本属性text / textWrap / textPath / lineHeight / textVerticalAnchor1. text设置文本内容仅适用于text元素。单行文本不含\n直接写入text多行文本则每行生成一个tspanrect.attr(label/text, Hello X6)实现src/registry/attr/text.ts还会读取textPath、textVerticalAnchor、lineHeight、annotations、eol、displayEmpty等关联属性并基于JSON.stringify([text, options])做变更缓存避免无谓的 DOM 重写。注意qualify逻辑当textWrap为对象时text属性不单独生效交给textWrap统一处理。2. textWrap自动换行文本仅适用于text元素。与text不同textWrap会自动断行使文本完整包围在参考元素的包围盒内。属性值为对象text文本内容width/height调整可用空间。负值表示缩小相当于给文本留内边距正值表示放大百分比表示参考元素宽高的比例ellipsis设为true时文本超出显示范围自动追加...breakWord默认true单词会被截断设为false时不截断完整单词文本可能超出宽度范围textWrap: { text: lorem ipsum dolor sit amet consectetur adipiscing elit, width: -10, // 宽度减少 10px height: 50%, // 高度为参考元素高度的一半 ellipsis: true, // 文本超出显示范围时自动添加省略号 breakWord: true, // 是否截断单词 }源码src/registry/attr/text.ts先按width/height调整refBBox百分比按比例、非正数在原尺寸上增减再调用Dom.breakText依据字号、字重、字体、行高计算换行与省略号最后复用text属性的set写入 DOM。可交互演示见仓库 examples/src/pages/shape 相关页面。3. textPath沿路径文本仅适用于text元素值为字符串时表示沿该 pathData 渲染文本值为对象时可通过d指定路径或通过selector引用节点/边中已定义的 SVGPathElement支持 CSS 选择器与markup中定义的 selectorstartOffset控制文本在路径上的位置如50%表示位于路径 50% 处20表示距路径起点 20pxrect.attr(label/textPath, { d: M 10 10 100 10, startOffset: 50%, })实现中selector会通过view.find(selector)查找路径节点Dom.ensureId为其分配 id 后转为xlink:href引用src/registry/attr/text.ts。4. lineHeight行高仅适用于text元素指定文本行高见 W3C 行高规范实现会将其透传给文本渲染逻辑src/registry/attr/text.ts。5. textVerticalAnchor垂直锚点仅适用于text元素控制文本与y坐标的垂直对齐top文本顶部与y对齐middle文本中心与y对齐bottom文本底部与y对齐七、边的属性connection 与 atConnection 系列以下属性仅对边Edge的path元素或边上的子元素生效其qualify统一判断view.cell.isEdge()src/registry/attr/connection.ts。1. connection指定边路径渲染元素仅适用于边的path元素。设为true时边的 pathData 会写入该元素的dedge.attr(pathSelector, { connection: true, stroke: red, fill: none, })也支持带stubs与reverse的对象src/registry/attr/connection.tsreverse: false默认stubs为正数表示只渲染首尾两段。{ stubs: 20 }只渲染连接线最前与最后各 20pxstubs为负数表示中间缺失段的长度。{ stubs: -20 }中间留 20px 缺口reverse: truestubs为正数表示首尾不渲染的长度。{ stubs: 20 }首尾各 20px 不渲染stubs为负数表示只渲染中间段。{ stubs: -20 }只渲染中间 20pxedge.attr(pathSelector, { connection: { stubs: -20, reverse: true }, })实现通过view.getConnection()获取连接路径用divideAtLength按长度切割并拼接片段。2. atConnectionLengthKeepGradient别名 atConnectionLength将边中的指定元素移动到距边起点指定长度处并自动旋转使其方向与边在该点的切线一致edge.attr(rectSelector, { atConnectionLengthKeepGradient: 30, // 也可写 atConnectionLength: 30 width: 10, height: 10, fill: red, })正值表示距边起点的偏移负值表示距边终点的偏移3. atConnectionLengthIgnoreGradient同上移动到指定长度处但忽略边的方向不自动旋转edge.attr(rectSelector, { atConnectionLengthIgnoreGradient: 30, width: 10, height: 10, fill: red, })4. atConnectionRatioKeepGradient别名 atConnectionRatio移动到边的比例位置[0, 1]处并自动旋转对齐切线edge.attr(rectSelector, { atConnectionRatioKeepGradient: 0.5, // 也可写 atConnectionRatio: 0.5 width: 10, height: 10, fill: red, })5. atConnectionRatioIgnoreGradient移动到比例位置[0, 1]处忽略边的方向、不旋转edge.attr(rectSelector, { atConnectionRatioIgnoreGradient: 0.5, width: 10, height: 10, fill: red, })四个属性的底层实现共享atConnectionWrappersrc/registry/attr/connection.ts通过view.getTangentAtLength/view.getTangentAtRatio获取切线rotate: true时计算切线与水平轴的夹角并写入transform: translate(...) rotate(...)rotate: false时仅平移。别名atConnectionLength、atConnectionRatio分别指向 KeepGradient 版本src/registry/attr/connection.ts。八、标记属性sourceMarker / targetMarker / vertexMarker三个属性都适用于所有path元素用于在路径起点、终点、顶点处附加 SVG 标记如箭头并自动旋转使其与路径方向一致。edge.attr(connection/sourceMarker, { tagName: circle, fill: #666, stroke: #333, r: 5, cx: 5, })实现src/registry/attr/marker.ts将标记定义写入marker-start/marker-end/marker-mid值可以是字符串预设标记名如block或对象nameargs通过markerRegistry解析后调用view.graph.defineMarker生成defs引用。targetMarker会额外叠加transform: rotate(180)src/registry/attr/marker.ts因此对水平直线起点向左的箭头在终点会自动向右。预设标记类型见 src/registry/marker 目录及文档 site/docs/api/registry/marker.en.md。九、交互属性magnet / port / event1. magnet可连接标记magnet: true表示该元素可被连接即连线过程中可作为起点或终点类似连接桩rect.attr(body/magnet, true)2. port连接桩 ID为标记了magnet的元素指定端口 ID边连接后该 ID 会保存到边的source/target中字符串直接作为端口 ID对象使用对象id属性的值作为端口 ID实现src/registry/attr/port.ts在对象含id时返回port.id否则原样返回字符串。完整的端口定义方式见 site/docs/tutorial/intermediate/port.zh.md。3. event自定义元素事件为指定元素注册自定义事件名之后可在 Graph 上绑定回调node.attr({ // 表示一个删除按钮点击后删除节点 image: { event: node:delete, xlinkHref: trash.png, width: 20, height: 20, }, }) // 绑定事件回调触发时删除节点 graph.on(node:delete, ({ view, e }) { e.stopPropagation() view.cell.remove() })十、XML/XLink 属性别名X6 将带命名空间的原生属性包装为易于书写的别名定义见 src/registry/attr/raw.ts别名原生属性xlinkHrefxlink:hrefxlinkShowxlink:showxlinkTypexlink:typexlinkTitlexlink:titlexlinkArcrolexlink:arcrolexmlSpacexml:spacexmlBasexml:basexmlLangxml:lang例如node.attr({ image: { xlinkHref: xxx.png, }, })十一、自定义 Attributeregistry 注册机制除内置属性外X6 通过registry对象提供register与unregister两个方法用于注册和删除特殊属性同时挂载为Graph.registerAttr/Graph.unregisterAttr两个静态方法src/graph/graph.ts推荐使用静态方法。1. 注册与注销 APIregister(entities: { [name: string]: Definition }, force?: boolean): void register(name: string, entity: Definition, force?: boolean): Definition unregister(name: string): Definition | nullforce为true时可覆盖已注册的同名属性底层仓库为attrRegistrysrc/registry/attr/index.ts内置属性在模块加载时即注册完成2. Definition 的多种形式内部实现会把特殊属性转换为浏览器可识别的属性因此属性值理论上可为任意类型属性定义也支持多种形式字符串原生属性别名例如xlinkHref的定义实质是为xlink:href起一个更易写的别名// 定义 Graph.registerAttr(xlinkHref, xlink:href) // 使用 node.attr({ image: { xlinkHref: xxx.png, }, })qualify资格函数在介绍set/offset/position前需要先理解资格函数qualify——只有通过它判断的属性值才会被特殊属性处理。例如stroke只在值为Object类型时使用渐变色填充边框才作为特殊属性type QualifyFunction ( this: CellView, // 节点/边的视图 val: ComplexAttrValue, // 当前属性值 options: { elem: Element // 应用当前属性的元素 attrs: ComplexAttrs // 应用在该元素上的属性键值对 cell: Cell // 节点/边 view: CellView // 节点/边的视图 }, ) boolean // 返回 true 表示通过资格判断对应类型定义见 src/registry/attr/index.ts。内置stroke的实现即依赖qualifyexport const stroke: Attr.Definition { qualify(val) { // 仅当属性值为对象时才触发特殊属性处理逻辑 return ObjectExt.isPlainObject(val) }, set(stroke, { view }) { return url(#${view.graph.defineGradient(stroke as any)}) }, }set属性设置定义最常用export interface SetDefinition { qualify?: QualifyFunction set: ( this: CellView, val: ComplexAttrValue, options: { refBBox: Rectangle // 参考元素的包围盒无参考元素时为节点位置与尺寸构成的矩形 elem: Element // 应用当前属性的元素 attrs: ComplexAttrs cell: Cell view: CellView }, ) SimpleAttrValue | SimpleAttrs | void }set的三种返回方式分别对应三种使用场景1返回string或number作为该特殊属性的最终值通常用于扩展原生属性定义。如stroke/fill支持渐变填充export const stroke: Attr.SetDefinition { qualify: ObjectExt.isPlainObject, set(stroke, { view }) { // 返回字符串返回值作为 stroke 属性的值 return url(#${view.graph.defineGradient(stroke as any)}) }, }2返回简单对象作为属性键值对应用到对应元素。如sourceMarker/targetMarkerexport const sourceMarker: Attr.SetDefinition { qualify: ObjectExt.isPlainObject, set(marker: string | JSONObject, { view, attrs }) { // 返回简单对象返回值作为属性键值对应用到对应元素 return { marker-start: createMarker(marker, view, attrs), } }, }3无返回值在方法内部自行完成属性赋值。如htmlexport const html: Attr.Definition { set(html, { view, elem }) { // 无返回值属性赋值在方法内部完成 view.$(elem).html(${html}) }, }对应源码见 src/registry/attr/html.ts当前实现为elem.innerHTML。offset偏移定义export interface OffsetDefinition { qualify?: QualifyFunction offset: ( this: CellView, val: ComplexAttrValue, options: { refBBox: Rectangle elem: Element attrs: ComplexAttrs cell: Cell view: CellView }, ) Point.PointLike // 返回绝对偏移 }返回Point.PointLike表示x/y方向的绝对偏移如xAlignexport const xAlign: Attr.OffsetDefinition { offset(alignment, { refBBox }) { // ...计算逻辑 // 返回 x 轴上的绝对偏移 return { x, y: 0 } }, }对应源码见 src/registry/attr/align.tsresetOffset也是典型的offset型定义。position定位定义export interface PositionDefinition { qualify?: QualifyFunction offset: ( this: CellView, val: ComplexAttrValue, options: { refBBox: Rectangle elem: Element attrs: ComplexAttrs cell: Cell view: CellView }, ) Point.PointLike // 返回相对于节点的绝对定位坐标 }返回相对于节点的绝对定位坐标如refX、refDxexport const refX: Attr.PositionDefinition { position(val, { refBBox }) { // ...计算逻辑 // 返回 x 轴上的绝对定位 return { x, y: 0 } }, }对应源码见 src/registry/attr/ref.ts。类型定义AttrPositionFunction、AttrOffsetFunction、AttrSetFunction统一位于 src/registry/attr/index.ts。3. 完整示例注册一个自定义属性综合上述概念注册一个将元素宽度设为参考元素宽度的一半的自定义属性import { Graph } from antv/x6 Graph.registerAttr( halfWidth, { set(val, { refBBox }) { return { width: refBBox.width / 2 } }, }, true, // force允许覆盖 ) // 使用 graph.addNode({ markup: [ { tagName: rect, selector: body }, { tagName: rect, selector: child }, ], attrs: { body: { width: 200, height: 100 }, child: { ref: body, halfWidth: true, height: 40, fill: orange }, }, })十二、小结X6 的 Attributes 体系由三层构成原生属性层raw中定义的大量 SVG/XML 原生属性别名src/registry/attr/raw.ts提供更简洁的书写方式内置特殊属性层ref系列定位、path/points适配、text/textWrap文本排版、connection/atConnection*边路径、sourceMarker/targetMarker标记、magnet/port/event交互等全部注册在attrRegistry中src/registry/attr/index.ts自定义属性层通过Graph.registerAttr结合qualifyset/offset/position三种定义形式将任意计算逻辑接入渲染管线。掌握refBBox与qualify两个核心概念即可理解绝大多数内置属性的行为并能自由扩展出符合业务需求的自定义属性。相关测试tests/registry/attr/ref.spec.ts对refX/refDx/refWidth/refR*的计算规则做了精确验证可作为自定义属性开发时的参考样板。【免费下载链接】X6 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门