Commit ff03e1af by pangchong

chore(deps): 添加 diff 库依赖并清理 editor 静态资源

- 在 package.json 和 package-lock.json 中新增 diff 库版本 7.0.0 依赖
- 删除 public/editor 目录下的 favicon.svg、index.html 及相关静态资源文件
- 优化 vite.config.ts 移除不再使用的自定义静态资源中间件和 fs 导入
- 新增 husky 钩子配置,添加 commit-msg 检查和提交格式提示的 pre-commit 钩子
- 更新 .gemini/.rules.md,完善逻辑 Hook 内函数声明规范的文档说明
- 新增 scratch_test.js 脚本,用于读取 XML 文件并统计标签数量
parent f55c6aab
...@@ -35,6 +35,7 @@ ...@@ -35,6 +35,7 @@
- **Vue 单文件 TS 提取要求**:Vue 页面主入口或下属组件的 `.vue` 文件中,禁止书写冗长的业务处理函数,必须将其全部提取到各自目录下的 `functionals/index.ts` 中。 - **Vue 单文件 TS 提取要求**:Vue 页面主入口或下属组件的 `.vue` 文件中,禁止书写冗长的业务处理函数,必须将其全部提取到各自目录下的 `functionals/index.ts` 中。
- **避免硬编码与混杂 Class**:在 `functionals/index.ts` 中编写业务逻辑时,组件级及非跨页面的功能逻辑**不要使用 Class 进行封装**,而应当采用 Vue 风格的以 `use` 开头的 **逻辑 Hook 方法**(例如 `export function useListEditor() { ... }`),并在 `.vue` 中通过解构调用。Class 应当仅被用来定义一类具备具体强实体特征或需要持久状态维持的特定全局/跨模块方法服务。 - **避免硬编码与混杂 Class**:在 `functionals/index.ts` 中编写业务逻辑时,组件级及非跨页面的功能逻辑**不要使用 Class 进行封装**,而应当采用 Vue 风格的以 `use` 开头的 **逻辑 Hook 方法**(例如 `export function useListEditor() { ... }`),并在 `.vue` 中通过解构调用。Class 应当仅被用来定义一类具备具体强实体特征或需要持久状态维持的特定全局/跨模块方法服务。
- **Hook 内部函数声明规范**:在 `use` 开头的逻辑 Hook 方法内部声明的其他业务函数或辅助函数,**必须**使用 `const` 箭头函数形式进行定义(例如 `const updateAttributes = (...) => { ... }`),**禁止**在 Hook 内部使用 `function` 关键字声明函数。
- **TypeScript 类型存放约束**:模块或组件相关的类型/接口(如 `interface``type`)必须统一放在同级 `constants/index.ts` 中,严禁在 `functionals/index.ts``.vue` 单文件中硬编码声明。 - **TypeScript 类型存放约束**:模块或组件相关的类型/接口(如 `interface``type`)必须统一放在同级 `constants/index.ts` 中,严禁在 `functionals/index.ts``.vue` 单文件中硬编码声明。
--- ---
......
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint --edit $1
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo '***************************************************'
echo '********************注意提交格式*******************'
echo '***************************************************'
echo 'git commit -m <type>[optional scope]: <description>'
echo '***************************************************
'feat': 新功能 feature
'fix': 修复 bug
'docs': 文档注释
'style': 代码格式(不影响代码运行的变动)
'refactor': 重构(既不增加新功能,也不是修复bug)
'perf': 性能优化
'test': 增加测试
'chore': 构建过程或辅助工具的变动
'revert': 回退
'build': 打包'
echo '***************************************************'
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
"bpmn-js-properties-panel": "^5.53.0", "bpmn-js-properties-panel": "^5.53.0",
"camunda-bpmn-moddle": "^7.0.1", "camunda-bpmn-moddle": "^7.0.1",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"diff": "^7.0.0",
"docx-preview": "^0.3.7", "docx-preview": "^0.3.7",
"less": "^4.5.1", "less": "^4.5.1",
"lodash-es": "^4.18.1", "lodash-es": "^4.18.1",
...@@ -2892,6 +2893,15 @@ ...@@ -2892,6 +2893,15 @@
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/diff": {
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/diff/-/diff-7.0.0.tgz",
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/dingbat-to-unicode": { "node_modules/dingbat-to-unicode": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
......
...@@ -36,7 +36,8 @@ ...@@ -36,7 +36,8 @@
"vue-i18n": "^11.2.8", "vue-i18n": "^11.2.8",
"vue-router": "^5.0.3", "vue-router": "^5.0.3",
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",
"xlsx": "^0.18.5" "xlsx": "^0.18.5",
"diff": "^7.0.0"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^19.8.0", "@commitlint/cli": "^19.8.0",
......
import{y as e,F as o,b4 as r,bY as n,J as t,p as i,h as a,I as s,k as l,bv as d,bn as c,bp as u,bk as f,d as b,Q as h,f as p,bD as v,a2 as g,j as y,bC as m,bZ as x,Z as w,R as C,r as $,Y as S,b as z,e as B,q as k,a0 as P,ah as H,u as F,at as T,g as R,ao as E,v as j,P as A,i as D,aj as W,ak as O}from"./index-BGNM-WBG.js";import{u as I}from"./use-rtl-iN3przWb.js";const M="undefined"!=typeof document&&"undefined"!=typeof window;function _(e){return e.replace(/#|\(|\)|,|\s|\./g,"_")}function L(e,...o){if(!Array.isArray(e))return e(...o);e.forEach(e=>L(e,...o))}function q(n,t=!0,i=[]){return n.forEach(n=>{if(null!==n)if("object"==typeof n)if(Array.isArray(n))q(n,t,i);else if(n.type===o){if(null===n.children)return;Array.isArray(n.children)&&q(n.children,t,i)}else{if(n.type===r&&t)return;i.push(n)}else"string"!=typeof n&&"number"!=typeof n||i.push(e(String(n)))}),i}function V(e,o="default",r=[]){const n=e.$slots[o];return void 0===n?r:n()}function K(e){return e.some(e=>!n(e)||e.type!==r&&!(e.type===o&&!K(e.children)))?e:null}function N(e,o){return e&&K(e())||o()}function Q(e,o,r){return e&&K(e(o))||r(o)}function G(e,o){return o(e&&K(e())||null)}function Y(e){return!(e&&K(e()))}const Z=l("n-form-item");function J(e,{defaultSize:o="medium",mergedSize:r,mergedDisabled:n}={}){const l=t(Z,null);i(Z,null);const d=a(r?()=>r(l):()=>{const{size:r}=e;if(r)return r;if(l){const{mergedSize:e}=l;if(void 0!==e.value)return e.value}return o}),c=a(n?()=>n(l):()=>{const{disabled:o}=e;return void 0!==o?o:!!l&&l.disabled.value}),u=a(()=>{const{status:o}=e;return o||(null==l?void 0:l.mergedValidationStatus.value)});return s(()=>{l&&l.restoreValidation()}),{mergedSizeRef:d,mergedDisabledRef:c,mergedStatusRef:u,nTriggerFormBlur(){l&&l.handleContentBlur()},nTriggerFormChange(){l&&l.handleContentChange()},nTriggerFormFocus(){l&&l.handleContentFocus()},nTriggerFormInput(){l&&l.handleContentInput()}}}function U(e){return"symbol"==typeof e||d(e)&&"[object Symbol]"==c(e)}function X(e,o){for(var r=-1,n=null==e?0:e.length,t=Array(n);++r<n;)t[r]=o(e[r],r,e);return t}var ee=u?u.prototype:void 0,oe=ee?ee.toString:void 0;function re(e){if("string"==typeof e)return e;if(f(e))return X(e,re)+"";if(U(e))return oe?oe.call(e):"";var o=e+"";return"0"==o&&1/e==-1/0?"-0":o}function ne(e){return null==e?"":re(e)}function te(e,o,r){var n=e.length;return r=void 0===r?n:r,!o&&r>=n?e:function(e,o,r){var n=-1,t=e.length;o<0&&(o=-o>t?0:t+o),(r=r>t?t:r)<0&&(r+=t),t=o>r?0:r-o>>>0,o>>>=0;for(var i=Array(t);++n<t;)i[n]=e[n+o];return i}(e,o,r)}var ie=RegExp("[\\u200d\\ud800-\\udfff\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\ufe0e\\ufe0f]");function ae(e){return ie.test(e)}var se="\\ud800-\\udfff",le="["+se+"]",de="[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]",ce="\\ud83c[\\udffb-\\udfff]",ue="[^"+se+"]",fe="(?:\\ud83c[\\udde6-\\uddff]){2}",be="[\\ud800-\\udbff][\\udc00-\\udfff]",he="(?:"+de+"|"+ce+")"+"?",pe="[\\ufe0e\\ufe0f]?",ve=pe+he+("(?:\\u200d(?:"+[ue,fe,be].join("|")+")"+pe+he+")*"),ge="(?:"+[ue+de+"?",de,fe,be,le].join("|")+")",ye=RegExp(ce+"(?="+ce+")|"+ge+ve,"g");function me(e){return ae(e)?function(e){return e.match(ye)||[]}(e):function(e){return e.split("")}(e)}var xe,we=(xe="toUpperCase",function(e){var o=ae(e=ne(e))?me(e):void 0,r=o?o[0]:e.charAt(0),n=o?te(o,1).join(""):e.slice(1);return r[xe]()+n});const Ce=b("base-icon","\n height: 1em;\n width: 1em;\n line-height: 1em;\n text-align: center;\n display: inline-block;\n position: relative;\n fill: currentColor;\n transform: translateZ(0);\n",[h("svg","\n height: 1em;\n width: 1em;\n ")]),$e=p({name:"BaseIcon",props:{role:String,ariaLabel:String,ariaDisabled:{type:Boolean,default:void 0},ariaHidden:{type:Boolean,default:void 0},clsPrefix:{type:String,required:!0},onClick:Function,onMousedown:Function,onMouseup:Function},setup(e){v("-base-icon",Ce,g(e,"clsPrefix"))},render(){return y("i",{class:`${this.clsPrefix}-base-icon`,onClick:this.onClick,onMousedown:this.onMousedown,onMouseup:this.onMouseup,role:this.role,"aria-label":this.ariaLabel,"aria-hidden":this.ariaHidden,"aria-disabled":this.ariaDisabled},this.$slots)}});function Se(e,o){const r=p({render:()=>o()});return p({name:we(e),setup(){var o;const n=null===(o=t(m,null))||void 0===o?void 0:o.mergedIconsRef;return()=>{var o;const t=null===(o=null==n?void 0:n.value)||void 0===o?void 0:o[e];return t?t():y(r,null)}}})}const ze=p({name:"FadeInExpandTransition",props:{appear:Boolean,group:Boolean,mode:String,onLeave:Function,onAfterLeave:Function,onAfterEnter:Function,width:Boolean,reverse:Boolean},setup(e,{slots:o}){function r(o){e.width?o.style.maxWidth=`${o.offsetWidth}px`:o.style.maxHeight=`${o.offsetHeight}px`,o.offsetWidth}function n(o){e.width?o.style.maxWidth="0":o.style.maxHeight="0",o.offsetWidth;const{onLeave:r}=e;r&&r()}function t(o){e.width?o.style.maxWidth="":o.style.maxHeight="";const{onAfterLeave:r}=e;r&&r()}function i(o){if(o.style.transition="none",e.width){const e=o.offsetWidth;o.style.maxWidth="0",o.offsetWidth,o.style.transition="",o.style.maxWidth=`${e}px`}else if(e.reverse)o.style.maxHeight=`${o.offsetHeight}px`,o.offsetHeight,o.style.transition="",o.style.maxHeight="0";else{const e=o.offsetHeight;o.style.maxHeight="0",o.offsetWidth,o.style.transition="",o.style.maxHeight=`${e}px`}o.offsetWidth}function a(o){var r;e.width?o.style.maxWidth="":e.reverse||(o.style.maxHeight=""),null===(r=e.onAfterEnter)||void 0===r||r.call(e)}return()=>{const{group:s,width:l,appear:d,mode:c}=e,u=s?x:w,f={name:l?"fade-in-width-expand-transition":"fade-in-height-expand-transition",appear:d,onEnter:i,onAfterEnter:a,onBeforeLeave:r,onLeave:n,onAfterLeave:t};return s||(f.mode=c),y(u,f,o)}}}),{cubicBezierEaseInOut:Be}=C;const ke=b("base-wave","\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n border-radius: inherit;\n"),Pe=p({name:"BaseWave",props:{clsPrefix:{type:String,required:!0}},setup(e){v("-base-wave",ke,g(e,"clsPrefix"));const o=$(null),r=$(!1);let n=null;return s(()=>{null!==n&&window.clearTimeout(n)}),{active:r,selfRef:o,play(){null!==n&&(window.clearTimeout(n),r.value=!1,n=null),S(()=>{var e;null===(e=o.value)||void 0===e||e.offsetHeight,r.value=!0,n=window.setTimeout(()=>{r.value=!1,n=null},1e3)})}}},render(){const{clsPrefix:e}=this;return y("div",{ref:"selfRef","aria-hidden":!0,class:[`${e}-base-wave`,this.active&&`${e}-base-wave--active`]})}}),He=M&&"chrome"in window;M&&navigator.userAgent.includes("Firefox");const Fe=M&&navigator.userAgent.includes("Safari")&&!He;function Te(e){return z(e,[255,255,255,.16])}function Re(e){return z(e,[0,0,0,.12])}const Ee=l("n-button-group"),je=h([b("button","\n margin: 0;\n font-weight: var(--n-font-weight);\n line-height: 1;\n font-family: inherit;\n padding: var(--n-padding);\n height: var(--n-height);\n font-size: var(--n-font-size);\n border-radius: var(--n-border-radius);\n color: var(--n-text-color);\n background-color: var(--n-color);\n width: var(--n-width);\n white-space: nowrap;\n outline: none;\n position: relative;\n z-index: auto;\n border: none;\n display: inline-flex;\n flex-wrap: nowrap;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n user-select: none;\n -webkit-user-select: none;\n text-align: center;\n cursor: pointer;\n text-decoration: none;\n transition:\n color .3s var(--n-bezier),\n background-color .3s var(--n-bezier),\n opacity .3s var(--n-bezier),\n border-color .3s var(--n-bezier);\n ",[B("color",[k("border",{borderColor:"var(--n-border-color)"}),B("disabled",[k("border",{borderColor:"var(--n-border-color-disabled)"})]),P("disabled",[h("&:focus",[k("state-border",{borderColor:"var(--n-border-color-focus)"})]),h("&:hover",[k("state-border",{borderColor:"var(--n-border-color-hover)"})]),h("&:active",[k("state-border",{borderColor:"var(--n-border-color-pressed)"})]),B("pressed",[k("state-border",{borderColor:"var(--n-border-color-pressed)"})])])]),B("disabled",{backgroundColor:"var(--n-color-disabled)",color:"var(--n-text-color-disabled)"},[k("border",{border:"var(--n-border-disabled)"})]),P("disabled",[h("&:focus",{backgroundColor:"var(--n-color-focus)",color:"var(--n-text-color-focus)"},[k("state-border",{border:"var(--n-border-focus)"})]),h("&:hover",{backgroundColor:"var(--n-color-hover)",color:"var(--n-text-color-hover)"},[k("state-border",{border:"var(--n-border-hover)"})]),h("&:active",{backgroundColor:"var(--n-color-pressed)",color:"var(--n-text-color-pressed)"},[k("state-border",{border:"var(--n-border-pressed)"})]),B("pressed",{backgroundColor:"var(--n-color-pressed)",color:"var(--n-text-color-pressed)"},[k("state-border",{border:"var(--n-border-pressed)"})])]),B("loading","cursor: wait;"),b("base-wave","\n pointer-events: none;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n animation-iteration-count: 1;\n animation-duration: var(--n-ripple-duration);\n animation-timing-function: var(--n-bezier-ease-out), var(--n-bezier-ease-out);\n ",[B("active",{zIndex:1,animationName:"button-wave-spread, button-wave-opacity"})]),M&&"MozBoxSizing"in document.createElement("div").style?h("&::moz-focus-inner",{border:0}):null,k("border, state-border","\n position: absolute;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n border-radius: inherit;\n transition: border-color .3s var(--n-bezier);\n pointer-events: none;\n "),k("border",{border:"var(--n-border)"}),k("state-border",{border:"var(--n-border)",borderColor:"#0000",zIndex:1}),k("icon","\n margin: var(--n-icon-margin);\n margin-left: 0;\n height: var(--n-icon-size);\n width: var(--n-icon-size);\n max-width: var(--n-icon-size);\n font-size: var(--n-icon-size);\n position: relative;\n flex-shrink: 0;\n ",[b("icon-slot","\n height: var(--n-icon-size);\n width: var(--n-icon-size);\n position: absolute;\n left: 0;\n top: 50%;\n transform: translateY(-50%);\n display: flex;\n align-items: center;\n justify-content: center;\n ",[H({top:"50%",originalTransform:"translateY(-50%)"})]),function({duration:e=".2s",delay:o=".1s"}={}){return[h("&.fade-in-width-expand-transition-leave-from, &.fade-in-width-expand-transition-enter-to",{opacity:1}),h("&.fade-in-width-expand-transition-leave-to, &.fade-in-width-expand-transition-enter-from","\n opacity: 0!important;\n margin-left: 0!important;\n margin-right: 0!important;\n "),h("&.fade-in-width-expand-transition-leave-active",`\n overflow: hidden;\n transition:\n opacity ${e} ${Be},\n max-width ${e} ${Be} ${o},\n margin-left ${e} ${Be} ${o},\n margin-right ${e} ${Be} ${o};\n `),h("&.fade-in-width-expand-transition-enter-active",`\n overflow: hidden;\n transition:\n opacity ${e} ${Be} ${o},\n max-width ${e} ${Be},\n margin-left ${e} ${Be},\n margin-right ${e} ${Be};\n `)]}()]),k("content","\n display: flex;\n align-items: center;\n flex-wrap: nowrap;\n min-width: 0;\n ",[h("~",[k("icon",{margin:"var(--n-icon-margin)",marginRight:0})])]),B("block","\n display: flex;\n width: 100%;\n "),B("dashed",[k("border, state-border",{borderStyle:"dashed !important"})]),B("disabled",{cursor:"not-allowed",opacity:"var(--n-opacity-disabled)"})]),h("@keyframes button-wave-spread",{from:{boxShadow:"0 0 0.5px 0 var(--n-ripple-color)"},to:{boxShadow:"0 0 0.5px 4.5px var(--n-ripple-color)"}}),h("@keyframes button-wave-opacity",{from:{opacity:"var(--n-wave-opacity)"},to:{opacity:0}})]),Ae=Object.assign(Object.assign({},F.props),{color:String,textColor:String,text:Boolean,block:Boolean,loading:Boolean,disabled:Boolean,circle:Boolean,size:String,ghost:Boolean,round:Boolean,secondary:Boolean,tertiary:Boolean,quaternary:Boolean,strong:Boolean,focusable:{type:Boolean,default:!0},keyboard:{type:Boolean,default:!0},tag:{type:String,default:"button"},type:{type:String,default:"default"},dashed:Boolean,renderIcon:Function,iconPlacement:{type:String,default:"left"},attrType:{type:String,default:"button"},bordered:{type:Boolean,default:!0},onClick:[Function,Array],nativeFocusBehavior:{type:Boolean,default:!Fe}}),De=p({name:"Button",props:Ae,slots:Object,setup(e){const o=$(null),r=$(null),n=$(!1),i=T(()=>!e.quaternary&&!e.tertiary&&!e.secondary&&!e.text&&(!e.color||e.ghost||e.dashed)&&e.bordered),s=t(Ee,{}),{mergedSizeRef:l}=J({},{defaultSize:"medium",mergedSize:o=>{const{size:r}=e;if(r)return r;const{size:n}=s;if(n)return n;const{mergedSize:t}=o||{};return t?t.value:"medium"}}),d=a(()=>e.focusable&&!e.disabled),{inlineThemeDisabled:c,mergedClsPrefixRef:u,mergedRtlRef:f}=R(e),b=F("Button","-button",je,E,e,u),h=I("Button",f,u),p=a(()=>{const o=b.value,{common:{cubicBezierEaseInOut:r,cubicBezierEaseOut:n},self:t}=o,{rippleDuration:i,opacityDisabled:a,fontWeight:s,fontWeightStrong:d}=t,c=l.value,{dashed:u,type:f,ghost:h,text:p,color:v,round:g,circle:y,textColor:m,secondary:x,tertiary:w,quaternary:C,strong:$}=e,S={"--n-font-weight":$?d:s};let z={"--n-color":"initial","--n-color-hover":"initial","--n-color-pressed":"initial","--n-color-focus":"initial","--n-color-disabled":"initial","--n-ripple-color":"initial","--n-text-color":"initial","--n-text-color-hover":"initial","--n-text-color-pressed":"initial","--n-text-color-focus":"initial","--n-text-color-disabled":"initial"};const B="tertiary"===f,k="default"===f,P=B?"default":f;if(p){const e=m||v;z={"--n-color":"#0000","--n-color-hover":"#0000","--n-color-pressed":"#0000","--n-color-focus":"#0000","--n-color-disabled":"#0000","--n-ripple-color":"#0000","--n-text-color":e||t[j("textColorText",P)],"--n-text-color-hover":e?Te(e):t[j("textColorTextHover",P)],"--n-text-color-pressed":e?Re(e):t[j("textColorTextPressed",P)],"--n-text-color-focus":e?Te(e):t[j("textColorTextHover",P)],"--n-text-color-disabled":e||t[j("textColorTextDisabled",P)]}}else if(h||u){const e=m||v;z={"--n-color":"#0000","--n-color-hover":"#0000","--n-color-pressed":"#0000","--n-color-focus":"#0000","--n-color-disabled":"#0000","--n-ripple-color":v||t[j("rippleColor",P)],"--n-text-color":e||t[j("textColorGhost",P)],"--n-text-color-hover":e?Te(e):t[j("textColorGhostHover",P)],"--n-text-color-pressed":e?Re(e):t[j("textColorGhostPressed",P)],"--n-text-color-focus":e?Te(e):t[j("textColorGhostHover",P)],"--n-text-color-disabled":e||t[j("textColorGhostDisabled",P)]}}else if(x){const e=k?t.textColor:B?t.textColorTertiary:t[j("color",P)],o=v||e,r="default"!==f&&"tertiary"!==f;z={"--n-color":r?A(o,{alpha:Number(t.colorOpacitySecondary)}):t.colorSecondary,"--n-color-hover":r?A(o,{alpha:Number(t.colorOpacitySecondaryHover)}):t.colorSecondaryHover,"--n-color-pressed":r?A(o,{alpha:Number(t.colorOpacitySecondaryPressed)}):t.colorSecondaryPressed,"--n-color-focus":r?A(o,{alpha:Number(t.colorOpacitySecondaryHover)}):t.colorSecondaryHover,"--n-color-disabled":t.colorSecondary,"--n-ripple-color":"#0000","--n-text-color":o,"--n-text-color-hover":o,"--n-text-color-pressed":o,"--n-text-color-focus":o,"--n-text-color-disabled":o}}else if(w||C){const e=k?t.textColor:B?t.textColorTertiary:t[j("color",P)],o=v||e;w?(z["--n-color"]=t.colorTertiary,z["--n-color-hover"]=t.colorTertiaryHover,z["--n-color-pressed"]=t.colorTertiaryPressed,z["--n-color-focus"]=t.colorSecondaryHover,z["--n-color-disabled"]=t.colorTertiary):(z["--n-color"]=t.colorQuaternary,z["--n-color-hover"]=t.colorQuaternaryHover,z["--n-color-pressed"]=t.colorQuaternaryPressed,z["--n-color-focus"]=t.colorQuaternaryHover,z["--n-color-disabled"]=t.colorQuaternary),z["--n-ripple-color"]="#0000",z["--n-text-color"]=o,z["--n-text-color-hover"]=o,z["--n-text-color-pressed"]=o,z["--n-text-color-focus"]=o,z["--n-text-color-disabled"]=o}else z={"--n-color":v||t[j("color",P)],"--n-color-hover":v?Te(v):t[j("colorHover",P)],"--n-color-pressed":v?Re(v):t[j("colorPressed",P)],"--n-color-focus":v?Te(v):t[j("colorFocus",P)],"--n-color-disabled":v||t[j("colorDisabled",P)],"--n-ripple-color":v||t[j("rippleColor",P)],"--n-text-color":m||(v?t.textColorPrimary:B?t.textColorTertiary:t[j("textColor",P)]),"--n-text-color-hover":m||(v?t.textColorHoverPrimary:t[j("textColorHover",P)]),"--n-text-color-pressed":m||(v?t.textColorPressedPrimary:t[j("textColorPressed",P)]),"--n-text-color-focus":m||(v?t.textColorFocusPrimary:t[j("textColorFocus",P)]),"--n-text-color-disabled":m||(v?t.textColorDisabledPrimary:t[j("textColorDisabled",P)])};let H={"--n-border":"initial","--n-border-hover":"initial","--n-border-pressed":"initial","--n-border-focus":"initial","--n-border-disabled":"initial"};H=p?{"--n-border":"none","--n-border-hover":"none","--n-border-pressed":"none","--n-border-focus":"none","--n-border-disabled":"none"}:{"--n-border":t[j("border",P)],"--n-border-hover":t[j("borderHover",P)],"--n-border-pressed":t[j("borderPressed",P)],"--n-border-focus":t[j("borderFocus",P)],"--n-border-disabled":t[j("borderDisabled",P)]};const{[j("height",c)]:F,[j("fontSize",c)]:T,[j("padding",c)]:R,[j("paddingRound",c)]:E,[j("iconSize",c)]:D,[j("borderRadius",c)]:W,[j("iconMargin",c)]:O,waveOpacity:I}=t,M={"--n-width":y&&!p?F:"initial","--n-height":p?"initial":F,"--n-font-size":T,"--n-padding":y||p?"initial":g?E:R,"--n-icon-size":D,"--n-icon-margin":O,"--n-border-radius":p?"initial":y||g?F:W};return Object.assign(Object.assign(Object.assign(Object.assign({"--n-bezier":r,"--n-bezier-ease-out":n,"--n-ripple-duration":i,"--n-opacity-disabled":a,"--n-wave-opacity":I},S),z),H),M)}),v=c?D("button",a(()=>{let o="";const{dashed:r,type:n,ghost:t,text:i,color:a,round:s,circle:d,textColor:c,secondary:u,tertiary:f,quaternary:b,strong:h}=e;r&&(o+="a"),t&&(o+="b"),i&&(o+="c"),s&&(o+="d"),d&&(o+="e"),u&&(o+="f"),f&&(o+="g"),b&&(o+="h"),h&&(o+="i"),a&&(o+=`j${_(a)}`),c&&(o+=`k${_(c)}`);const{value:p}=l;return o+=`l${p[0]}`,o+=`m${n[0]}`,o}),p,e):void 0;return{selfElRef:o,waveElRef:r,mergedClsPrefix:u,mergedFocusable:d,mergedSize:l,showBorder:i,enterPressed:n,rtlEnabled:h,handleMousedown:r=>{var n;d.value||r.preventDefault(),e.nativeFocusBehavior||(r.preventDefault(),e.disabled||d.value&&(null===(n=o.value)||void 0===n||n.focus({preventScroll:!0})))},handleKeydown:o=>{if("Enter"===o.key){if(!e.keyboard||e.loading)return void o.preventDefault();n.value=!0}},handleBlur:()=>{n.value=!1},handleKeyup:o=>{if("Enter"===o.key){if(!e.keyboard)return;n.value=!1}},handleClick:o=>{var n;if(!e.disabled&&!e.loading){const{onClick:t}=e;t&&L(t,o),e.text||null===(n=r.value)||void 0===n||n.play()}},customColorCssVars:a(()=>{const{color:o}=e;if(!o)return null;const r=Te(o);return{"--n-border-color":o,"--n-border-color-hover":r,"--n-border-color-pressed":Re(o),"--n-border-color-focus":r,"--n-border-color-disabled":o}}),cssVars:c?void 0:p,themeClass:null==v?void 0:v.themeClass,onRender:null==v?void 0:v.onRender}},render(){const{mergedClsPrefix:e,tag:o,onRender:r}=this;null==r||r();const n=G(this.$slots.default,o=>o&&y("span",{class:`${e}-button__content`},o));return y(o,{ref:"selfElRef",class:[this.themeClass,`${e}-button`,`${e}-button--${this.type}-type`,`${e}-button--${this.mergedSize}-type`,this.rtlEnabled&&`${e}-button--rtl`,this.disabled&&`${e}-button--disabled`,this.block&&`${e}-button--block`,this.enterPressed&&`${e}-button--pressed`,!this.text&&this.dashed&&`${e}-button--dashed`,this.color&&`${e}-button--color`,this.secondary&&`${e}-button--secondary`,this.loading&&`${e}-button--loading`,this.ghost&&`${e}-button--ghost`],tabindex:this.mergedFocusable?0:-1,type:this.attrType,style:this.cssVars,disabled:this.disabled,onClick:this.handleClick,onBlur:this.handleBlur,onMousedown:this.handleMousedown,onKeyup:this.handleKeyup,onKeydown:this.handleKeydown},"right"===this.iconPlacement&&n,y(ze,{width:!0},{default:()=>G(this.$slots.icon,o=>(this.loading||this.renderIcon||o)&&y("span",{class:`${e}-button__icon`,style:{margin:Y(this.$slots.default)?"0":""}},y(W,null,{default:()=>this.loading?y(O,{clsPrefix:e,key:"loading",class:`${e}-icon-slot`,strokeWidth:20}):y("div",{key:"icon",class:`${e}-icon-slot`,role:"none"},this.renderIcon?this.renderIcon():o)})))}),"left"===this.iconPlacement&&n,this.text?null:y(Pe,{ref:"waveElRef",clsPrefix:e}),this.showBorder?y("div",{"aria-hidden":!0,class:`${e}-button__border`,style:this.customColorCssVars}):null,this.showBorder?y("div",{"aria-hidden":!0,class:`${e}-button__state-border`,style:this.customColorCssVars}):null)}}),We=De,Oe=De;export{$e as N,Oe as X,We as _,ze as a,N as b,G as c,Q as d,L as e,Z as f,q as g,V as h,M as i,Y as j,U as k,X as l,_ as m,Fe as n,K as o,Se as r,ne as t,J as u};
This source diff could not be displayed because it is too large. You can view the blob instead.
import{b8 as e,b9 as t,f as n,K as o,M as r,I as i,aT as l,S as a,d as s,Q as c,e as u,q as d,ad as v,u as h,g as f,r as b,s as p,h as g,az as w,a4 as m,i as y,j as z,F as x,Z as S,T}from"./index-BGNM-WBG.js";import{u as R}from"./use-rtl-iN3przWb.js";function E(e){return e.composedPath()[0]||null}function B(e){return e.composedPath()[0]}const M={mousemoveoutside:new WeakMap,clickoutside:new WeakMap};function O(e,t,n){const o=M[e];let r=o.get(t);void 0===r&&o.set(t,r=new WeakMap);let i=r.get(n);return void 0===i&&r.set(n,i=function(e,t,n){if("mousemoveoutside"===e){const e=e=>{t.contains(B(e))||n(e)};return{mousemove:e,touchstart:e}}if("clickoutside"===e){let e=!1;const o=n=>{e=!t.contains(B(n))},r=o=>{e&&(t.contains(B(o))||n(o))};return{mousedown:o,mouseup:r,touchstart:o,touchend:r}}return{}}(e,t,n)),i}const{on:P,off:k}=function(){if("undefined"==typeof window)return{on:()=>{},off:()=>{}};const e=new WeakMap,t=new WeakMap;function n(){e.set(this,!0)}function o(){e.set(this,!0),t.set(this,!0)}function r(e,t,n){const o=e[t];return e[t]=function(){return n.apply(e,arguments),o.apply(e,arguments)},e}function i(e,t){e[t]=Event.prototype[t]}const l=new WeakMap,a=Object.getOwnPropertyDescriptor(Event.prototype,"currentTarget");function s(){var e;return null!==(e=l.get(this))&&void 0!==e?e:null}function c(e,t){void 0!==a&&Object.defineProperty(e,"currentTarget",{configurable:!0,enumerable:!0,get:null!=t?t:a.get})}const u={bubble:{},capture:{}},d={},v=function(){const a=function(a){const{type:d,eventPhase:v,bubbles:h}=a,f=B(a);if(2===v)return;const b=1===v?"capture":"bubble";let p=f;const g=[];for(;null===p&&(p=window),g.push(p),p!==window;)p=p.parentNode||null;const w=u.capture[d],m=u.bubble[d];if(r(a,"stopPropagation",n),r(a,"stopImmediatePropagation",o),c(a,s),"capture"===b){if(void 0===w)return;for(let n=g.length-1;n>=0&&!e.has(a);--n){const e=g[n],o=w.get(e);if(void 0!==o){l.set(a,e);for(const e of o){if(t.has(a))break;e(a)}}if(0===n&&!h&&void 0!==m){const n=m.get(e);if(void 0!==n)for(const e of n){if(t.has(a))break;e(a)}}}}else if("bubble"===b){if(void 0===m)return;for(let n=0;n<g.length&&!e.has(a);++n){const e=g[n],o=m.get(e);if(void 0!==o){l.set(a,e);for(const e of o){if(t.has(a))break;e(a)}}}}i(a,"stopPropagation"),i(a,"stopImmediatePropagation"),c(a)};return a.displayName="evtdUnifiedHandler",a}(),h=function(){const e=function(e){const{type:t,eventPhase:n}=e;if(2!==n)return;const o=d[t];void 0!==o&&o.forEach(t=>t(e))};return e.displayName="evtdUnifiedWindowEventHandler",e}();function f(e,t){const n=u[e];return void 0===n[t]&&(n[t]=new Map,window.addEventListener(t,v,"capture"===e)),n[t]}function b(e,t){let n=e.get(t);return void 0===n&&e.set(t,n=new Set),n}function p(e,t,n,o){const r=function(e,t,n,o){if("mousemoveoutside"===e||"clickoutside"===e){const r=O(e,t,n);return Object.keys(r).forEach(e=>{k(e,document,r[e],o)}),!0}return!1}(e,t,n,o);if(r)return;const i=!0===o||"object"==typeof o&&!0===o.capture,l=i?"capture":"bubble",a=f(l,e),s=b(a,t);if(t===window){if(!function(e,t,n,o){const r=u[t][n];if(void 0!==r){const t=r.get(e);if(void 0!==t&&t.has(o))return!0}return!1}(t,i?"bubble":"capture",e,n)&&function(e,t){const n=d[e];return!(void 0===n||!n.has(t))}(e,n)){const t=d[e];t.delete(n),0===t.size&&(window.removeEventListener(e,h),d[e]=void 0)}}s.has(n)&&s.delete(n),0===s.size&&a.delete(t),0===a.size&&(window.removeEventListener(e,v,"capture"===l),u[l][e]=void 0)}return{on:function(e,t,n,o){let r;r="object"==typeof o&&!0===o.once?i=>{p(e,t,r,o),n(i)}:n;if(function(e,t,n,o){if("mousemoveoutside"===e||"clickoutside"===e){const r=O(e,t,n);return Object.keys(r).forEach(e=>{P(e,document,r[e],o)}),!0}return!1}(e,t,r,o))return;const i=b(f(!0===o||"object"==typeof o&&!0===o.capture?"capture":"bubble",e),t);if(i.has(r)||i.add(r),t===window){const t=function(e){return void 0===d[e]&&(d[e]=new Set,window.addEventListener(e,h)),d[e]}(e);t.has(r)||t.add(r)}},off:p}}(),C="undefined"!=typeof window&&(/iPad|iPhone|iPod/.test(navigator.platform)||"MacIntel"===navigator.platform&&navigator.maxTouchPoints>1)&&!window.MSStream;function W(n){const o={isDeactivated:!1};let r=!1;return e(()=>{o.isDeactivated=!1,r?n():r=!0}),t(()=>{o.isDeactivated=!0,r||(r=!0)}),o}var $,D,L=[],H=function(){return L.some(function(e){return e.activeTargets.length>0})},I="ResizeObserver loop completed with undelivered notifications.";(D=$||($={})).BORDER_BOX="border-box",D.CONTENT_BOX="content-box",D.DEVICE_PIXEL_CONTENT_BOX="device-pixel-content-box";var N,X=function(e){return Object.freeze(e)},j=function(){return function(e,t){this.inlineSize=e,this.blockSize=t,X(this)}}(),F=function(){function e(e,t,n,o){return this.x=e,this.y=t,this.width=n,this.height=o,this.top=this.y,this.left=this.x,this.bottom=this.top+this.height,this.right=this.left+this.width,X(this)}return e.prototype.toJSON=function(){var e=this;return{x:e.x,y:e.y,top:e.top,right:e.right,bottom:e.bottom,left:e.left,width:e.width,height:e.height}},e.fromRect=function(t){return new e(t.x,t.y,t.width,t.height)},e}(),_=function(e){return e instanceof SVGElement&&"getBBox"in e},V=function(e){if(_(e)){var t=e.getBBox(),n=t.width,o=t.height;return!n&&!o}var r=e,i=r.offsetWidth,l=r.offsetHeight;return!(i||l||e.getClientRects().length)},A=function(e){var t;if(e instanceof Element)return!0;var n=null===(t=null==e?void 0:e.ownerDocument)||void 0===t?void 0:t.defaultView;return!!(n&&e instanceof n.Element)},Y="undefined"!=typeof window?window:{},U=new WeakMap,q=/auto|scroll/,G=/^tb|vertical/,J=/msie|trident/i.test(Y.navigator&&Y.navigator.userAgent),K=function(e){return parseFloat(e||"0")},Q=function(e,t,n){return void 0===e&&(e=0),void 0===t&&(t=0),void 0===n&&(n=!1),new j((n?t:e)||0,(n?e:t)||0)},Z=X({devicePixelContentBoxSize:Q(),borderBoxSize:Q(),contentBoxSize:Q(),contentRect:new F(0,0,0,0)}),ee=function(e,t){if(void 0===t&&(t=!1),U.has(e)&&!t)return U.get(e);if(V(e))return U.set(e,Z),Z;var n=getComputedStyle(e),o=_(e)&&e.ownerSVGElement&&e.getBBox(),r=!J&&"border-box"===n.boxSizing,i=G.test(n.writingMode||""),l=!o&&q.test(n.overflowY||""),a=!o&&q.test(n.overflowX||""),s=o?0:K(n.paddingTop),c=o?0:K(n.paddingRight),u=o?0:K(n.paddingBottom),d=o?0:K(n.paddingLeft),v=o?0:K(n.borderTopWidth),h=o?0:K(n.borderRightWidth),f=o?0:K(n.borderBottomWidth),b=d+c,p=s+u,g=(o?0:K(n.borderLeftWidth))+h,w=v+f,m=a?e.offsetHeight-w-e.clientHeight:0,y=l?e.offsetWidth-g-e.clientWidth:0,z=r?b+g:0,x=r?p+w:0,S=o?o.width:K(n.width)-z-y,T=o?o.height:K(n.height)-x-m,R=S+b+y+g,E=T+p+m+w,B=X({devicePixelContentBoxSize:Q(Math.round(S*devicePixelRatio),Math.round(T*devicePixelRatio),i),borderBoxSize:Q(R,E,i),contentBoxSize:Q(S,T,i),contentRect:new F(d,s,S,T)});return U.set(e,B),B},te=function(e,t,n){var o=ee(e,n),r=o.borderBoxSize,i=o.contentBoxSize,l=o.devicePixelContentBoxSize;switch(t){case $.DEVICE_PIXEL_CONTENT_BOX:return l;case $.BORDER_BOX:return r;default:return i}},ne=function(){return function(e){var t=ee(e);this.target=e,this.contentRect=t.contentRect,this.borderBoxSize=X([t.borderBoxSize]),this.contentBoxSize=X([t.contentBoxSize]),this.devicePixelContentBoxSize=X([t.devicePixelContentBoxSize])}}(),oe=function(e){if(V(e))return 1/0;for(var t=0,n=e.parentNode;n;)t+=1,n=n.parentNode;return t},re=function(){var e=1/0,t=[];L.forEach(function(n){if(0!==n.activeTargets.length){var o=[];n.activeTargets.forEach(function(t){var n=new ne(t.target),r=oe(t.target);o.push(n),t.lastReportedSize=te(t.target,t.observedBox),r<e&&(e=r)}),t.push(function(){n.callback.call(n.observer,o,n.observer)}),n.activeTargets.splice(0,n.activeTargets.length)}});for(var n=0,o=t;n<o.length;n++){(0,o[n])()}return e},ie=function(e){L.forEach(function(t){t.activeTargets.splice(0,t.activeTargets.length),t.skippedTargets.splice(0,t.skippedTargets.length),t.observationTargets.forEach(function(n){n.isActive()&&(oe(n.target)>e?t.activeTargets.push(n):t.skippedTargets.push(n))})})},le=function(){var e,t=0;for(ie(t);H();)t=re(),ie(t);return L.some(function(e){return e.skippedTargets.length>0})&&("function"==typeof ErrorEvent?e=new ErrorEvent("error",{message:I}):((e=document.createEvent("Event")).initEvent("error",!1,!1),e.message=I),window.dispatchEvent(e)),t>0},ae=[],se=function(e){if(!N){var t=0,n=document.createTextNode("");new MutationObserver(function(){return ae.splice(0).forEach(function(e){return e()})}).observe(n,{characterData:!0}),N=function(){n.textContent="".concat(t?t--:t++)}}ae.push(e),N()},ce=0,ue={attributes:!0,characterData:!0,childList:!0,subtree:!0},de=["resize","load","transitionend","animationend","animationstart","animationiteration","keyup","keydown","mouseup","mousedown","mouseover","mouseout","blur","focus"],ve=function(e){return void 0===e&&(e=0),Date.now()+e},he=!1,fe=new(function(){function e(){var e=this;this.stopped=!0,this.listener=function(){return e.schedule()}}return e.prototype.run=function(e){var t=this;if(void 0===e&&(e=250),!he){he=!0;var n,o=ve(e);n=function(){var n=!1;try{n=le()}finally{if(he=!1,e=o-ve(),!ce)return;n?t.run(1e3):e>0?t.run(e):t.start()}},se(function(){requestAnimationFrame(n)})}},e.prototype.schedule=function(){this.stop(),this.run()},e.prototype.observe=function(){var e=this,t=function(){return e.observer&&e.observer.observe(document.body,ue)};document.body?t():Y.addEventListener("DOMContentLoaded",t)},e.prototype.start=function(){var e=this;this.stopped&&(this.stopped=!1,this.observer=new MutationObserver(this.listener),this.observe(),de.forEach(function(t){return Y.addEventListener(t,e.listener,!0)}))},e.prototype.stop=function(){var e=this;this.stopped||(this.observer&&this.observer.disconnect(),de.forEach(function(t){return Y.removeEventListener(t,e.listener,!0)}),this.stopped=!0)},e}()),be=function(e){!ce&&e>0&&fe.start(),!(ce+=e)&&fe.stop()},pe=function(){function e(e,t){this.target=e,this.observedBox=t||$.CONTENT_BOX,this.lastReportedSize={inlineSize:0,blockSize:0}}return e.prototype.isActive=function(){var e,t=te(this.target,this.observedBox,!0);return e=this.target,_(e)||function(e){switch(e.tagName){case"INPUT":if("image"!==e.type)break;case"VIDEO":case"AUDIO":case"EMBED":case"OBJECT":case"CANVAS":case"IFRAME":case"IMG":return!0}return!1}(e)||"inline"!==getComputedStyle(e).display||(this.lastReportedSize=t),this.lastReportedSize.inlineSize!==t.inlineSize||this.lastReportedSize.blockSize!==t.blockSize},e}(),ge=function(){return function(e,t){this.activeTargets=[],this.skippedTargets=[],this.observationTargets=[],this.observer=e,this.callback=t}}(),we=new WeakMap,me=function(e,t){for(var n=0;n<e.length;n+=1)if(e[n].target===t)return n;return-1},ye=function(){function e(){}return e.connect=function(e,t){var n=new ge(e,t);we.set(e,n)},e.observe=function(e,t,n){var o=we.get(e),r=0===o.observationTargets.length;me(o.observationTargets,t)<0&&(r&&L.push(o),o.observationTargets.push(new pe(t,n&&n.box)),be(1),fe.schedule())},e.unobserve=function(e,t){var n=we.get(e),o=me(n.observationTargets,t),r=1===n.observationTargets.length;o>=0&&(r&&L.splice(L.indexOf(n),1),n.observationTargets.splice(o,1),be(-1))},e.disconnect=function(e){var t=this,n=we.get(e);n.observationTargets.slice().forEach(function(n){return t.unobserve(e,n.target)}),n.activeTargets.splice(0,n.activeTargets.length)},e}(),ze=function(){function e(e){if(0===arguments.length)throw new TypeError("Failed to construct 'ResizeObserver': 1 argument required, but only 0 present.");if("function"!=typeof e)throw new TypeError("Failed to construct 'ResizeObserver': The callback provided as parameter 1 is not a function.");ye.connect(this,e)}return e.prototype.observe=function(e,t){if(0===arguments.length)throw new TypeError("Failed to execute 'observe' on 'ResizeObserver': 1 argument required, but only 0 present.");if(!A(e))throw new TypeError("Failed to execute 'observe' on 'ResizeObserver': parameter 1 is not of type 'Element");ye.observe(this,e,t)},e.prototype.unobserve=function(e){if(0===arguments.length)throw new TypeError("Failed to execute 'unobserve' on 'ResizeObserver': 1 argument required, but only 0 present.");if(!A(e))throw new TypeError("Failed to execute 'unobserve' on 'ResizeObserver': parameter 1 is not of type 'Element");ye.unobserve(this,e)},e.prototype.disconnect=function(){ye.disconnect(this)},e.toString=function(){return"function ResizeObserver () { [polyfill code] }"},e}();const xe=new class{constructor(){this.handleResize=this.handleResize.bind(this),this.observer=new("undefined"!=typeof window&&window.ResizeObserver||ze)(this.handleResize),this.elHandlersMap=new Map}handleResize(e){for(const t of e){const e=this.elHandlersMap.get(t.target);void 0!==e&&e(t)}}registerHandler(e,t){this.elHandlersMap.set(e,t),this.observer.observe(e)}unregisterHandler(e){this.elHandlersMap.has(e)&&(this.elHandlersMap.delete(e),this.observer.unobserve(e))}},Se=n({name:"ResizeObserver",props:{onResize:Function},setup(e){let t=!1;const n=o().proxy;function l(t){const{onResize:n}=e;void 0!==n&&n(t)}r(()=>{const e=n.$el;void 0!==e&&(e.nextElementSibling!==e.nextSibling&&3===e.nodeType&&""!==e.nodeValue||null!==e.nextElementSibling&&(xe.registerHandler(e.nextElementSibling,l),t=!0))}),i(()=>{t&&xe.unregisterHandler(n.$el.nextElementSibling)})},render(){return l(this.$slots,"default")}});function Te(e){const{left:t,right:n,top:o,bottom:r}=a(e);return`${o} ${t} ${r} ${n}`}const Re=n({render(){var e,t;return null===(t=(e=this.$slots).default)||void 0===t?void 0:t.call(e)}}),Ee=s("scrollbar","\n overflow: hidden;\n position: relative;\n z-index: auto;\n height: 100%;\n width: 100%;\n",[c(">",[s("scrollbar-container","\n width: 100%;\n overflow: scroll;\n height: 100%;\n min-height: inherit;\n max-height: inherit;\n scrollbar-width: none;\n ",[c("&::-webkit-scrollbar, &::-webkit-scrollbar-track-piece, &::-webkit-scrollbar-thumb","\n width: 0;\n height: 0;\n display: none;\n "),c(">",[s("scrollbar-content","\n box-sizing: border-box;\n min-width: 100%;\n ")])])]),c(">, +",[s("scrollbar-rail","\n position: absolute;\n pointer-events: none;\n user-select: none;\n background: var(--n-scrollbar-rail-color);\n -webkit-user-select: none;\n ",[u("horizontal","\n height: var(--n-scrollbar-height);\n ",[c(">",[d("scrollbar","\n height: var(--n-scrollbar-height);\n border-radius: var(--n-scrollbar-border-radius);\n right: 0;\n ")])]),u("horizontal--top","\n top: var(--n-scrollbar-rail-top-horizontal-top); \n right: var(--n-scrollbar-rail-right-horizontal-top); \n bottom: var(--n-scrollbar-rail-bottom-horizontal-top); \n left: var(--n-scrollbar-rail-left-horizontal-top); \n "),u("horizontal--bottom","\n top: var(--n-scrollbar-rail-top-horizontal-bottom); \n right: var(--n-scrollbar-rail-right-horizontal-bottom); \n bottom: var(--n-scrollbar-rail-bottom-horizontal-bottom); \n left: var(--n-scrollbar-rail-left-horizontal-bottom); \n "),u("vertical","\n width: var(--n-scrollbar-width);\n ",[c(">",[d("scrollbar","\n width: var(--n-scrollbar-width);\n border-radius: var(--n-scrollbar-border-radius);\n bottom: 0;\n ")])]),u("vertical--left","\n top: var(--n-scrollbar-rail-top-vertical-left); \n right: var(--n-scrollbar-rail-right-vertical-left); \n bottom: var(--n-scrollbar-rail-bottom-vertical-left); \n left: var(--n-scrollbar-rail-left-vertical-left); \n "),u("vertical--right","\n top: var(--n-scrollbar-rail-top-vertical-right); \n right: var(--n-scrollbar-rail-right-vertical-right); \n bottom: var(--n-scrollbar-rail-bottom-vertical-right); \n left: var(--n-scrollbar-rail-left-vertical-right); \n "),u("disabled",[c(">",[d("scrollbar","pointer-events: none;")])]),c(">",[d("scrollbar","\n z-index: 1;\n position: absolute;\n cursor: pointer;\n pointer-events: all;\n background-color: var(--n-scrollbar-color);\n transition: background-color .2s var(--n-scrollbar-bezier);\n ",[v(),c("&:hover","background-color: var(--n-scrollbar-color-hover);")])])])])]),Be=n({name:"Scrollbar",props:Object.assign(Object.assign({},h.props),{duration:{type:Number,default:0},scrollable:{type:Boolean,default:!0},xScrollable:Boolean,trigger:{type:String,default:"hover"},useUnifiedContainer:Boolean,triggerDisplayManually:Boolean,container:Function,content:Function,containerClass:String,containerStyle:[String,Object],contentClass:[String,Array],contentStyle:[String,Object],horizontalRailStyle:[String,Object],verticalRailStyle:[String,Object],onScroll:Function,onWheel:Function,onResize:Function,internalOnUpdateScrollLeft:Function,internalHoistYRail:Boolean,yPlacement:{type:String,default:"right"},xPlacement:{type:String,default:"bottom"}}),inheritAttrs:!1,setup(e){const{mergedClsPrefixRef:t,inlineThemeDisabled:n,mergedRtlRef:o}=f(e),l=R("Scrollbar",o,t),s=b(null),c=b(null),u=b(null),d=b(null),v=b(null),z=b(null),x=b(null),S=b(null),T=b(null),B=b(null),M=b(null),O=b(0),$=b(0),D=b(!1),L=b(!1);let H,I,N=!1,X=!1,j=0,F=0,_=0,V=0;const A=C,Y=h("Scrollbar","-scrollbar",Ee,p,e,t),U=g(()=>{const{value:e}=S,{value:t}=z,{value:n}=B;return null===e||null===t||null===n?0:Math.min(e,n*e/t+1.5*w(Y.value.self.width))}),q=g(()=>`${U.value}px`),G=g(()=>{const{value:e}=T,{value:t}=x,{value:n}=M;return null===e||null===t||null===n?0:n*e/t+1.5*w(Y.value.self.height)}),J=g(()=>`${G.value}px`),K=g(()=>{const{value:e}=S,{value:t}=O,{value:n}=z,{value:o}=B;if(null===e||null===n||null===o)return 0;{const r=n-e;return r?t/r*(o-U.value):0}}),Q=g(()=>`${K.value}px`),Z=g(()=>{const{value:e}=T,{value:t}=$,{value:n}=x,{value:o}=M;if(null===e||null===n||null===o)return 0;{const r=n-e;return r?t/r*(o-G.value):0}}),ee=g(()=>`${Z.value}px`),te=g(()=>{const{value:e}=S,{value:t}=z;return null!==e&&null!==t&&t>e}),ne=g(()=>{const{value:e}=T,{value:t}=x;return null!==e&&null!==t&&t>e}),oe=g(()=>{const{trigger:t}=e;return"none"===t||D.value}),re=g(()=>{const{trigger:t}=e;return"none"===t||L.value}),ie=g(()=>{const{container:t}=e;return t?t():c.value}),le=g(()=>{const{content:t}=e;return t?t():u.value}),ae=(t,n)=>{if(!e.scrollable)return;if("number"==typeof t)return void ce(t,null!=n?n:0,0,!1,"auto");const{left:o,top:r,index:i,elSize:l,position:a,behavior:s,el:c,debounce:u=!0}=t;void 0===o&&void 0===r||ce(null!=o?o:0,null!=r?r:0,0,!1,s),void 0!==c?ce(0,c.offsetTop,c.offsetHeight,u,s):void 0!==i&&void 0!==l?ce(0,i*l,l,u,s):"bottom"===a?ce(0,Number.MAX_SAFE_INTEGER,0,!1,s):"top"===a&&ce(0,0,0,!1,s)},se=W(()=>{e.container||ae({top:O.value,left:$.value})});function ce(e,t,n,o,r){const{value:i}=ie;if(i){if(o){const{scrollTop:o,offsetHeight:l}=i;if(t>o)return void(t+n<=o+l||i.scrollTo({left:e,top:t+n-l,behavior:r}))}i.scrollTo({left:e,top:t,behavior:r})}}function ue(){!function(){void 0!==I&&window.clearTimeout(I);I=window.setTimeout(()=>{L.value=!1},e.duration)}(),function(){void 0!==H&&window.clearTimeout(H);H=window.setTimeout(()=>{D.value=!1},e.duration)}()}function de(){const{value:e}=ie;e&&(O.value=e.scrollTop,$.value=e.scrollLeft*((null==l?void 0:l.value)?-1:1))}function ve(){const{value:e}=ie;e&&(O.value=e.scrollTop,$.value=e.scrollLeft*((null==l?void 0:l.value)?-1:1),S.value=e.offsetHeight,T.value=e.offsetWidth,z.value=e.scrollHeight,x.value=e.scrollWidth);const{value:t}=v,{value:n}=d;t&&(M.value=t.offsetWidth),n&&(B.value=n.offsetHeight)}function he(){e.scrollable&&(e.useUnifiedContainer?ve():(!function(){const{value:e}=le;e&&(z.value=e.offsetHeight,x.value=e.offsetWidth);const{value:t}=ie;t&&(S.value=t.offsetHeight,T.value=t.offsetWidth);const{value:n}=v,{value:o}=d;n&&(M.value=n.offsetWidth),o&&(B.value=o.offsetHeight)}(),de()))}function fe(e){var t;return!(null===(t=s.value)||void 0===t?void 0:t.contains(E(e)))}function be(t){if(!X)return;void 0!==H&&window.clearTimeout(H),void 0!==I&&window.clearTimeout(I);const{value:n}=T,{value:o}=x,{value:r}=G;if(null===n||null===o)return;const i=(null==l?void 0:l.value)?window.innerWidth-t.clientX-_:t.clientX-_,a=o-n;let s=F+i*(o-n)/(n-r);s=Math.min(a,s),s=Math.max(s,0);const{value:c}=ie;if(c){c.scrollLeft=s*((null==l?void 0:l.value)?-1:1);const{internalOnUpdateScrollLeft:t}=e;t&&t(s)}}function pe(e){e.preventDefault(),e.stopPropagation(),k("mousemove",window,be,!0),k("mouseup",window,pe,!0),X=!1,he(),fe(e)&&ue()}function ge(e){if(!N)return;void 0!==H&&window.clearTimeout(H),void 0!==I&&window.clearTimeout(I);const{value:t}=S,{value:n}=z,{value:o}=U;if(null===t||null===n)return;const r=e.clientY-V,i=n-t;let l=j+r*(n-t)/(t-o);l=Math.min(i,l),l=Math.max(l,0);const{value:a}=ie;a&&(a.scrollTop=l)}function we(e){e.preventDefault(),e.stopPropagation(),k("mousemove",window,ge,!0),k("mouseup",window,we,!0),N=!1,he(),fe(e)&&ue()}m(()=>{const{value:e}=ne,{value:n}=te,{value:o}=t,{value:r}=v,{value:i}=d;r&&(e?r.classList.remove(`${o}-scrollbar-rail--disabled`):r.classList.add(`${o}-scrollbar-rail--disabled`)),i&&(n?i.classList.remove(`${o}-scrollbar-rail--disabled`):i.classList.add(`${o}-scrollbar-rail--disabled`))}),r(()=>{e.container||he()}),i(()=>{void 0!==H&&window.clearTimeout(H),void 0!==I&&window.clearTimeout(I),k("mousemove",window,ge,!0),k("mouseup",window,we,!0)});const me=g(()=>{const{common:{cubicBezierEaseInOut:e},self:{color:t,colorHover:n,height:o,width:r,borderRadius:i,railInsetHorizontalTop:s,railInsetHorizontalBottom:c,railInsetVerticalRight:u,railInsetVerticalLeft:d,railColor:v}}=Y.value,{top:h,right:f,bottom:b,left:p}=a(s),{top:g,right:w,bottom:m,left:y}=a(c),{top:z,right:x,bottom:S,left:T}=a((null==l?void 0:l.value)?Te(u):u),{top:R,right:E,bottom:B,left:M}=a((null==l?void 0:l.value)?Te(d):d);return{"--n-scrollbar-bezier":e,"--n-scrollbar-color":t,"--n-scrollbar-color-hover":n,"--n-scrollbar-border-radius":i,"--n-scrollbar-width":r,"--n-scrollbar-height":o,"--n-scrollbar-rail-top-horizontal-top":h,"--n-scrollbar-rail-right-horizontal-top":f,"--n-scrollbar-rail-bottom-horizontal-top":b,"--n-scrollbar-rail-left-horizontal-top":p,"--n-scrollbar-rail-top-horizontal-bottom":g,"--n-scrollbar-rail-right-horizontal-bottom":w,"--n-scrollbar-rail-bottom-horizontal-bottom":m,"--n-scrollbar-rail-left-horizontal-bottom":y,"--n-scrollbar-rail-top-vertical-right":z,"--n-scrollbar-rail-right-vertical-right":x,"--n-scrollbar-rail-bottom-vertical-right":S,"--n-scrollbar-rail-left-vertical-right":T,"--n-scrollbar-rail-top-vertical-left":R,"--n-scrollbar-rail-right-vertical-left":E,"--n-scrollbar-rail-bottom-vertical-left":B,"--n-scrollbar-rail-left-vertical-left":M,"--n-scrollbar-rail-color":v}}),ye=n?y("scrollbar",void 0,me,e):void 0,ze={scrollTo:ae,scrollBy:(t,n)=>{if(!e.scrollable)return;const{value:o}=ie;o&&("object"==typeof t?o.scrollBy(t):o.scrollBy(t,n||0))},sync:he,syncUnifiedContainer:ve,handleMouseEnterWrapper:function(){!function(){void 0!==H&&window.clearTimeout(H);D.value=!0}(),function(){void 0!==I&&window.clearTimeout(I);L.value=!0}(),he()},handleMouseLeaveWrapper:function(){ue()}};return Object.assign(Object.assign({},ze),{mergedClsPrefix:t,rtlEnabled:l,containerScrollTop:O,wrapperRef:s,containerRef:c,contentRef:u,yRailRef:d,xRailRef:v,needYBar:te,needXBar:ne,yBarSizePx:q,xBarSizePx:J,yBarTopPx:Q,xBarLeftPx:ee,isShowXBar:oe,isShowYBar:re,isIos:A,handleScroll:function(t){const{onScroll:n}=e;n&&n(t),de()},handleContentResize:()=>{se.isDeactivated||he()},handleContainerResize:t=>{if(se.isDeactivated)return;const{onResize:n}=e;n&&n(t),he()},handleYScrollMouseDown:function(e){e.preventDefault(),e.stopPropagation(),N=!0,P("mousemove",window,ge,!0),P("mouseup",window,we,!0),j=O.value,V=e.clientY},handleXScrollMouseDown:function(e){e.preventDefault(),e.stopPropagation(),X=!0,P("mousemove",window,be,!0),P("mouseup",window,pe,!0),F=$.value,_=(null==l?void 0:l.value)?window.innerWidth-e.clientX:e.clientX},cssVars:n?void 0:me,themeClass:null==ye?void 0:ye.themeClass,onRender:null==ye?void 0:ye.onRender})},render(){var e;const{$slots:t,mergedClsPrefix:n,triggerDisplayManually:o,rtlEnabled:r,internalHoistYRail:i,yPlacement:l,xPlacement:a,xScrollable:s}=this;if(!this.scrollable)return null===(e=t.default)||void 0===e?void 0:e.call(t);const c="none"===this.trigger,u=(e,t)=>z("div",{ref:"yRailRef",class:[`${n}-scrollbar-rail`,`${n}-scrollbar-rail--vertical`,`${n}-scrollbar-rail--vertical--${l}`,e],"data-scrollbar-rail":!0,style:[t||"",this.verticalRailStyle],"aria-hidden":!0},z(c?Re:S,c?null:{name:"fade-in-transition"},{default:()=>this.needYBar&&this.isShowYBar&&!this.isIos?z("div",{class:`${n}-scrollbar-rail__scrollbar`,style:{height:this.yBarSizePx,top:this.yBarTopPx},onMousedown:this.handleYScrollMouseDown}):null})),d=()=>{var e,l;return null===(e=this.onRender)||void 0===e||e.call(this),z("div",T(this.$attrs,{role:"none",ref:"wrapperRef",class:[`${n}-scrollbar`,this.themeClass,r&&`${n}-scrollbar--rtl`],style:this.cssVars,onMouseenter:o?void 0:this.handleMouseEnterWrapper,onMouseleave:o?void 0:this.handleMouseLeaveWrapper}),[this.container?null===(l=t.default)||void 0===l?void 0:l.call(t):z("div",{role:"none",ref:"containerRef",class:[`${n}-scrollbar-container`,this.containerClass],style:this.containerStyle,onScroll:this.handleScroll,onWheel:this.onWheel},z(Se,{onResize:this.handleContentResize},{default:()=>z("div",{ref:"contentRef",role:"none",style:[{width:this.xScrollable?"fit-content":null},this.contentStyle],class:[`${n}-scrollbar-content`,this.contentClass]},t)})),i?null:u(void 0,void 0),s&&z("div",{ref:"xRailRef",class:[`${n}-scrollbar-rail`,`${n}-scrollbar-rail--horizontal`,`${n}-scrollbar-rail--horizontal--${a}`],style:this.horizontalRailStyle,"data-scrollbar-rail":!0,"aria-hidden":!0},z(c?Re:S,c?null:{name:"fade-in-transition"},{default:()=>this.needXBar&&this.isShowXBar&&!this.isIos?z("div",{class:`${n}-scrollbar-rail__scrollbar`,style:{width:this.xBarSizePx,right:r?this.xBarLeftPx:void 0,left:r?void 0:this.xBarLeftPx},onMousedown:this.handleXScrollMouseDown}):null}))])},v=this.container?d():z(Se,{onResize:this.handleContainerResize},{default:d});return i?z(x,null,v,u(this.themeClass,this.cssVars)):v}}),Me=Be,Oe=Be;export{Me as N,Se as V,Re as W,Oe as X,k as a,E as g,P as o,xe as r,W as u};
import{r as e,i as t,g as r,h as n}from"./Button-ByITIhW2.js";import{j as o,b3 as a,f as i,g as s,u as l,h as p,b4 as d,v as g,b5 as c,az as C}from"./index-BGNM-WBG.js";import{u as m}from"./use-rtl-iN3przWb.js";const u=e("error",()=>o("svg",{viewBox:"0 0 48 48",version:"1.1",xmlns:"http://www.w3.org/2000/svg"},o("g",{stroke:"none","stroke-width":"1","fill-rule":"evenodd"},o("g",{"fill-rule":"nonzero"},o("path",{d:"M24,4 C35.045695,4 44,12.954305 44,24 C44,35.045695 35.045695,44 24,44 C12.954305,44 4,35.045695 4,24 C4,12.954305 12.954305,4 24,4 Z M17.8838835,16.1161165 L17.7823881,16.0249942 C17.3266086,15.6583353 16.6733914,15.6583353 16.2176119,16.0249942 L16.1161165,16.1161165 L16.0249942,16.2176119 C15.6583353,16.6733914 15.6583353,17.3266086 16.0249942,17.7823881 L16.1161165,17.8838835 L22.233,24 L16.1161165,30.1161165 L16.0249942,30.2176119 C15.6583353,30.6733914 15.6583353,31.3266086 16.0249942,31.7823881 L16.1161165,31.8838835 L16.2176119,31.9750058 C16.6733914,32.3416647 17.3266086,32.3416647 17.7823881,31.9750058 L17.8838835,31.8838835 L24,25.767 L30.1161165,31.8838835 L30.2176119,31.9750058 C30.6733914,32.3416647 31.3266086,32.3416647 31.7823881,31.9750058 L31.8838835,31.8838835 L31.9750058,31.7823881 C32.3416647,31.3266086 32.3416647,30.6733914 31.9750058,30.2176119 L31.8838835,30.1161165 L25.767,24 L31.8838835,17.8838835 L31.9750058,17.7823881 C32.3416647,17.3266086 32.3416647,16.6733914 31.9750058,16.2176119 L31.8838835,16.1161165 L31.7823881,16.0249942 C31.3266086,15.6583353 30.6733914,15.6583353 30.2176119,16.0249942 L30.1161165,16.1161165 L24,22.233 L17.8838835,16.1161165 L17.7823881,16.0249942 L17.8838835,16.1161165 Z"}))))),f=e("info",()=>o("svg",{viewBox:"0 0 28 28",version:"1.1",xmlns:"http://www.w3.org/2000/svg"},o("g",{stroke:"none","stroke-width":"1","fill-rule":"evenodd"},o("g",{"fill-rule":"nonzero"},o("path",{d:"M14,2 C20.6274,2 26,7.37258 26,14 C26,20.6274 20.6274,26 14,26 C7.37258,26 2,20.6274 2,14 C2,7.37258 7.37258,2 14,2 Z M14,11 C13.4477,11 13,11.4477 13,12 L13,12 L13,20 C13,20.5523 13.4477,21 14,21 C14.5523,21 15,20.5523 15,20 L15,20 L15,12 C15,11.4477 14.5523,11 14,11 Z M14,6.75 C13.3096,6.75 12.75,7.30964 12.75,8 C12.75,8.69036 13.3096,9.25 14,9.25 C14.6904,9.25 15.25,8.69036 15.25,8 C15.25,7.30964 14.6904,6.75 14,6.75 Z"}))))),L=e("success",()=>o("svg",{viewBox:"0 0 48 48",version:"1.1",xmlns:"http://www.w3.org/2000/svg"},o("g",{stroke:"none","stroke-width":"1","fill-rule":"evenodd"},o("g",{"fill-rule":"nonzero"},o("path",{d:"M24,4 C35.045695,4 44,12.954305 44,24 C44,35.045695 35.045695,44 24,44 C12.954305,44 4,35.045695 4,24 C4,12.954305 12.954305,4 24,4 Z M32.6338835,17.6161165 C32.1782718,17.1605048 31.4584514,17.1301307 30.9676119,17.5249942 L30.8661165,17.6161165 L20.75,27.732233 L17.1338835,24.1161165 C16.6457281,23.6279612 15.8542719,23.6279612 15.3661165,24.1161165 C14.9105048,24.5717282 14.8801307,25.2915486 15.2749942,25.7823881 L15.3661165,25.8838835 L19.8661165,30.3838835 C20.3217282,30.8394952 21.0415486,30.8698693 21.5323881,30.4750058 L21.6338835,30.3838835 L32.6338835,19.3838835 C33.1220388,18.8957281 33.1220388,18.1042719 32.6338835,17.6161165 Z"}))))),v=e("warning",()=>o("svg",{viewBox:"0 0 24 24",version:"1.1",xmlns:"http://www.w3.org/2000/svg"},o("g",{stroke:"none","stroke-width":"1","fill-rule":"evenodd"},o("g",{"fill-rule":"nonzero"},o("path",{d:"M12,2 C17.523,2 22,6.478 22,12 C22,17.522 17.523,22 12,22 C6.477,22 2,17.522 2,12 C2,6.478 6.477,2 12,2 Z M12.0018002,15.0037242 C11.450254,15.0037242 11.0031376,15.4508407 11.0031376,16.0023869 C11.0031376,16.553933 11.450254,17.0010495 12.0018002,17.0010495 C12.5533463,17.0010495 13.0004628,16.553933 13.0004628,16.0023869 C13.0004628,15.4508407 12.5533463,15.0037242 12.0018002,15.0037242 Z M11.99964,7 C11.4868042,7.00018474 11.0642719,7.38637706 11.0066858,7.8837365 L11,8.00036004 L11.0018003,13.0012393 L11.00857,13.117858 C11.0665141,13.6151758 11.4893244,14.0010638 12.0021602,14.0008793 C12.514996,14.0006946 12.9375283,13.6145023 12.9951144,13.1171428 L13.0018002,13.0005193 L13,7.99964009 L12.9932303,7.8830214 C12.9352861,7.38570354 12.5124758,6.99981552 11.99964,7 Z"})))));const w={name:"Space",self:function(){return a}};let h;function x(){if(!t)return!0;if(void 0===h){const e=document.createElement("div");e.style.display="flex",e.style.flexDirection="column",e.style.rowGap="1px",e.appendChild(document.createElement("div")),e.appendChild(document.createElement("div")),document.body.appendChild(e);const t=1===e.scrollHeight;return document.body.removeChild(e),h=t}return h}const y=i({name:"Space",props:Object.assign(Object.assign({},l.props),{align:String,justify:{type:String,default:"start"},inline:Boolean,vertical:Boolean,reverse:Boolean,size:{type:[String,Number,Array],default:"medium"},wrapItem:{type:Boolean,default:!0},itemClass:String,itemStyle:[String,Object],wrap:{type:Boolean,default:!0},internalUseGap:{type:Boolean,default:void 0}}),setup(e){const{mergedClsPrefixRef:t,mergedRtlRef:r}=s(e),n=l("Space","-space",void 0,w,e,t),o=m("Space",r,t);return{useGap:x(),rtlEnabled:o,mergedClsPrefix:t,margin:p(()=>{const{size:t}=e;if(Array.isArray(t))return{horizontal:t[0],vertical:t[1]};if("number"==typeof t)return{horizontal:t,vertical:t};const{self:{[g("gap",t)]:r}}=n.value,{row:o,col:a}=c(r);return{horizontal:C(a),vertical:C(o)}})}},render(){const{vertical:e,reverse:t,align:a,inline:i,justify:s,itemClass:l,itemStyle:p,margin:g,wrap:c,mergedClsPrefix:C,rtlEnabled:m,useGap:u,wrapItem:f,internalUseGap:L}=this,v=r(n(this),!1);if(!v.length)return null;const w=`${g.horizontal}px`,h=g.horizontal/2+"px",x=`${g.vertical}px`,y=g.vertical/2+"px",b=v.length-1,B=s.startsWith("space-");return o("div",{role:"none",class:[`${C}-space`,m&&`${C}-space--rtl`],style:{display:i?"inline-flex":"flex",flexDirection:e&&!t?"column":e&&t?"column-reverse":!e&&t?"row-reverse":"row",justifyContent:["start","end"].includes(s)?`flex-${s}`:s,flexWrap:!c||e?"nowrap":"wrap",marginTop:u||e?"":`-${y}`,marginBottom:u||e?"":`-${y}`,alignItems:a,gap:u?`${g.vertical}px ${g.horizontal}px`:""}},f||!u&&!L?v.map((t,r)=>t.type===d?t:o("div",{role:"none",class:l,style:[p,{maxWidth:"100%"},u?"":e?{marginBottom:r!==b?x:""}:m?{marginLeft:B?"space-between"===s&&r===b?"":h:r!==b?w:"",marginRight:B?"space-between"===s&&0===r?"":h:"",paddingTop:y,paddingBottom:y}:{marginRight:B?"space-between"===s&&r===b?"":h:r!==b?w:"",marginLeft:B?"space-between"===s&&0===r?"":h:"",paddingTop:y,paddingBottom:y}]},t)):v)}});export{u as E,f as I,L as S,v as W,y as _};
const o=(o,t)=>{const c=o.__vccOpts||o;for(const[s,n]of t)c[s]=n;return c};export{o as _};
This source diff could not be displayed because it is too large. You can view the blob instead.
import{j as t,d as e,q as s,f as n,g as l,u as i,h as r,i as o,t as c,v as a,o as u,x as f,n as d,w as p,y as h,z as v}from"./index-BGNM-WBG.js";import{N as x,_ as g}from"./Button-ByITIhW2.js";import{I as z,S as m,W as w,E as y,_ as C}from"./Space-BiFpHGv7.js";import"./use-rtl-iN3przWb.js";const B=e("result","\n color: var(--n-text-color);\n line-height: var(--n-line-height);\n font-size: var(--n-font-size);\n transition:\n color .3s var(--n-bezier);\n",[e("result-icon","\n display: flex;\n justify-content: center;\n transition: color .3s var(--n-bezier);\n ",[s("status-image","\n font-size: var(--n-icon-size);\n width: 1em;\n height: 1em;\n "),e("base-icon","\n color: var(--n-icon-color);\n font-size: var(--n-icon-size);\n ")]),e("result-content",{marginTop:"24px"}),e("result-footer","\n margin-top: 24px;\n text-align: center;\n "),e("result-header",[s("title","\n margin-top: 16px;\n font-weight: var(--n-title-font-weight);\n transition: color .3s var(--n-bezier);\n text-align: center;\n color: var(--n-title-text-color);\n font-size: var(--n-title-font-size);\n "),s("description","\n margin-top: 4px;\n text-align: center;\n font-size: var(--n-font-size);\n ")])]),F={403:function(){return t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 36 36"},t("path",{fill:"#EF9645",d:"M15.5 2.965c1.381 0 2.5 1.119 2.5 2.5v.005L20.5.465c1.381 0 2.5 1.119 2.5 2.5V4.25l2.5-1.535c1.381 0 2.5 1.119 2.5 2.5V8.75L29 18H15.458L15.5 2.965z"}),t("path",{fill:"#FFDC5D",d:"M4.625 16.219c1.381-.611 3.354.208 4.75 2.188.917 1.3 1.187 3.151 2.391 3.344.46.073 1.234-.313 1.234-1.397V4.5s0-2 2-2 2 2 2 2v11.633c0-.029 1-.064 1-.082V2s0-2 2-2 2 2 2 2v14.053c0 .017 1 .041 1 .069V4.25s0-2 2-2 2 2 2 2v12.638c0 .118 1 .251 1 .398V8.75s0-2 2-2 2 2 2 2V24c0 6.627-5.373 12-12 12-4.775 0-8.06-2.598-9.896-5.292C8.547 28.423 8.096 26.051 8 25.334c0 0-.123-1.479-1.156-2.865-1.469-1.969-2.5-3.156-3.125-3.866-.317-.359-.625-1.707.906-2.384z"}))},404:function(){return t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 36 36"},t("circle",{fill:"#FFCB4C",cx:"18",cy:"17.018",r:"17"}),t("path",{fill:"#65471B",d:"M14.524 21.036c-.145-.116-.258-.274-.312-.464-.134-.46.13-.918.59-1.021 4.528-1.021 7.577 1.363 7.706 1.465.384.306.459.845.173 1.205-.286.358-.828.401-1.211.097-.11-.084-2.523-1.923-6.182-1.098-.274.061-.554-.016-.764-.184z"}),t("ellipse",{fill:"#65471B",cx:"13.119",cy:"11.174",rx:"2.125",ry:"2.656"}),t("ellipse",{fill:"#65471B",cx:"24.375",cy:"12.236",rx:"2.125",ry:"2.656"}),t("path",{fill:"#F19020",d:"M17.276 35.149s1.265-.411 1.429-1.352c.173-.972-.624-1.167-.624-1.167s1.041-.208 1.172-1.376c.123-1.101-.861-1.363-.861-1.363s.97-.4 1.016-1.539c.038-.959-.995-1.428-.995-1.428s5.038-1.221 5.556-1.341c.516-.12 1.32-.615 1.069-1.694-.249-1.08-1.204-1.118-1.697-1.003-.494.115-6.744 1.566-8.9 2.068l-1.439.334c-.54.127-.785-.11-.404-.512.508-.536.833-1.129.946-2.113.119-1.035-.232-2.313-.433-2.809-.374-.921-1.005-1.649-1.734-1.899-1.137-.39-1.945.321-1.542 1.561.604 1.854.208 3.375-.833 4.293-2.449 2.157-3.588 3.695-2.83 6.973.828 3.575 4.377 5.876 7.952 5.048l3.152-.681z"}),t("path",{fill:"#65471B",d:"M9.296 6.351c-.164-.088-.303-.224-.391-.399-.216-.428-.04-.927.393-1.112 4.266-1.831 7.699-.043 7.843.034.433.231.608.747.391 1.154-.216.405-.74.546-1.173.318-.123-.063-2.832-1.432-6.278.047-.257.109-.547.085-.785-.042zm12.135 3.75c-.156-.098-.286-.243-.362-.424-.187-.442.023-.927.468-1.084 4.381-1.536 7.685.48 7.823.567.415.26.555.787.312 1.178-.242.39-.776.495-1.191.238-.12-.072-2.727-1.621-6.267-.379-.266.091-.553.046-.783-.096z"}))},418:function(){return t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 36 36"},t("ellipse",{fill:"#292F33",cx:"18",cy:"26",rx:"18",ry:"10"}),t("ellipse",{fill:"#66757F",cx:"18",cy:"24",rx:"18",ry:"10"}),t("path",{fill:"#E1E8ED",d:"M18 31C3.042 31 1 16 1 12h34c0 2-1.958 19-17 19z"}),t("path",{fill:"#77B255",d:"M35 12.056c0 5.216-7.611 9.444-17 9.444S1 17.271 1 12.056C1 6.84 8.611 3.611 18 3.611s17 3.229 17 8.445z"}),t("ellipse",{fill:"#A6D388",cx:"18",cy:"13",rx:"15",ry:"7"}),t("path",{d:"M21 17c-.256 0-.512-.098-.707-.293-2.337-2.337-2.376-4.885-.125-8.262.739-1.109.9-2.246.478-3.377-.461-1.236-1.438-1.996-1.731-2.077-.553 0-.958-.443-.958-.996 0-.552.491-.995 1.043-.995.997 0 2.395 1.153 3.183 2.625 1.034 1.933.91 4.039-.351 5.929-1.961 2.942-1.531 4.332-.125 5.738.391.391.391 1.023 0 1.414-.195.196-.451.294-.707.294zm-6-2c-.256 0-.512-.098-.707-.293-2.337-2.337-2.376-4.885-.125-8.262.727-1.091.893-2.083.494-2.947-.444-.961-1.431-1.469-1.684-1.499-.552 0-.989-.447-.989-1 0-.552.458-1 1.011-1 .997 0 2.585.974 3.36 2.423.481.899 1.052 2.761-.528 5.131-1.961 2.942-1.531 4.332-.125 5.738.391.391.391 1.023 0 1.414-.195.197-.451.295-.707.295z",fill:"#5C913B"}))},500:function(){return t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 36 36"},t("path",{fill:"#FFCC4D",d:"M36 18c0 9.941-8.059 18-18 18-9.94 0-18-8.059-18-18C0 8.06 8.06 0 18 0c9.941 0 18 8.06 18 18"}),t("ellipse",{fill:"#664500",cx:"18",cy:"27",rx:"5",ry:"6"}),t("path",{fill:"#664500",d:"M5.999 11c-.208 0-.419-.065-.599-.2-.442-.331-.531-.958-.2-1.4C8.462 5.05 12.816 5 13 5c.552 0 1 .448 1 1 0 .551-.445.998-.996 1-.155.002-3.568.086-6.204 3.6-.196.262-.497.4-.801.4zm24.002 0c-.305 0-.604-.138-.801-.4-2.64-3.521-6.061-3.598-6.206-3.6-.55-.006-.994-.456-.991-1.005C22.006 5.444 22.45 5 23 5c.184 0 4.537.05 7.8 4.4.332.442.242 1.069-.2 1.4-.18.135-.39.2-.599.2zm-16.087 4.5l1.793-1.793c.391-.391.391-1.023 0-1.414s-1.023-.391-1.414 0L12.5 14.086l-1.793-1.793c-.391-.391-1.023-.391-1.414 0s-.391 1.023 0 1.414l1.793 1.793-1.793 1.793c-.391.391-.391 1.023 0 1.414.195.195.451.293.707.293s.512-.098.707-.293l1.793-1.793 1.793 1.793c.195.195.451.293.707.293s.512-.098.707-.293c.391-.391.391-1.023 0-1.414L13.914 15.5zm11 0l1.793-1.793c.391-.391.391-1.023 0-1.414s-1.023-.391-1.414 0L23.5 14.086l-1.793-1.793c-.391-.391-1.023-.391-1.414 0s-.391 1.023 0 1.414l1.793 1.793-1.793 1.793c-.391.391-.391 1.023 0 1.414.195.195.451.293.707.293s.512-.098.707-.293l1.793-1.793 1.793 1.793c.195.195.451.293.707.293s.512-.098.707-.293c.391-.391.391-1.023 0-1.414L24.914 15.5z"}))},info:()=>t(z,null),success:()=>t(m,null),warning:()=>t(w,null),error:()=>t(y,null)},b=n({name:"Result",props:Object.assign(Object.assign({},i.props),{size:{type:String,default:"medium"},status:{type:String,default:"info"},title:String,description:String}),slots:Object,setup(t){const{mergedClsPrefixRef:e,inlineThemeDisabled:s}=l(t),n=i("Result","-result",B,c,t,e),u=r(()=>{const{size:e,status:s}=t,{common:{cubicBezierEaseInOut:l},self:{textColor:i,lineHeight:r,titleTextColor:o,titleFontWeight:c,[a("iconColor",s)]:u,[a("fontSize",e)]:f,[a("titleFontSize",e)]:d,[a("iconSize",e)]:p}}=n.value;return{"--n-bezier":l,"--n-font-size":f,"--n-icon-size":p,"--n-line-height":r,"--n-text-color":i,"--n-title-font-size":d,"--n-title-font-weight":c,"--n-title-text-color":o,"--n-icon-color":u||""}}),f=s?o("result",r(()=>{const{size:e,status:s}=t;let n="";return e&&(n+=e[0]),s&&(n+=s[0]),n}),u,t):void 0;return{mergedClsPrefix:e,cssVars:s?void 0:u,themeClass:null==f?void 0:f.themeClass,onRender:null==f?void 0:f.onRender}},render(){var e;const{status:s,$slots:n,mergedClsPrefix:l,onRender:i}=this;return null==i||i(),t("div",{class:[`${l}-result`,this.themeClass],style:this.cssVars},t("div",{class:`${l}-result-icon`},(null===(e=n.icon)||void 0===e?void 0:e.call(n))||t(x,{clsPrefix:l},{default:()=>F[s]()})),t("div",{class:`${l}-result-header`},this.title?t("div",{class:`${l}-result-header__title`},this.title):null,this.description?t("div",{class:`${l}-result-header__description`},this.description):null),n.default&&t("div",{class:`${l}-result-content`},n),n.footer&&t("div",{class:`${l}-result-footer`},n.footer()))}}),_={class:"flex justify-center items-center h-full"},j=n({__name:"index",setup(t){const e=v(),s=()=>{e.push("/")};return(t,e)=>{const n=g,l=C,i=b;return u(),f("div",_,[d(i,{status:"404",title:"页面不存在"},{footer:p(()=>[d(l,null,{default:p(()=>[d(n,{type:"primary",onClick:s},{default:p(()=>[h("返回")]),_:1})]),_:1})]),_:1})])}}});export{j as default};
import{_ as o}from"./_plugin-vue_export-helper-BCo6x5W8.js";import{c as e,a as r,s as t,b as l,d as n,e as s,f as a,u as i,r as d,g as c,p as u,h as b,i as h,j as v,k as f,l as C,o as p,m,w as g,n as x}from"./index-BGNM-WBG.js";import{u as y,N as S}from"./Scrollbar-DmIJvlAu.js";import"./use-rtl-iN3przWb.js";const B=e({name:"Layout",common:r,peers:{Scrollbar:t},self:function(o){const{baseColor:e,textColor2:r,bodyColor:t,cardColor:n,dividerColor:s,actionColor:a,scrollbarColor:i,scrollbarColorHover:d,invertedColor:c}=o;return{textColor:r,textColorInverted:"#FFF",color:t,colorEmbedded:a,headerColor:n,headerColorInverted:c,footerColor:a,footerColorInverted:c,headerBorderColor:s,headerBorderColorInverted:c,footerBorderColor:s,footerBorderColorInverted:c,siderBorderColor:s,siderBorderColorInverted:c,siderColor:n,siderColorInverted:c,siderToggleButtonBorder:`1px solid ${s}`,siderToggleButtonColor:e,siderToggleButtonIconColor:r,siderToggleButtonIconColorInverted:r,siderToggleBarColor:l(t,i),siderToggleBarColorHover:l(t,d),__invertScrollbar:"true"}}}),z={type:String,default:"static"},I=n("layout","\n color: var(--n-text-color);\n background-color: var(--n-color);\n box-sizing: border-box;\n position: relative;\n z-index: auto;\n flex: auto;\n overflow: hidden;\n transition:\n box-shadow .3s var(--n-bezier),\n background-color .3s var(--n-bezier),\n color .3s var(--n-bezier);\n",[n("layout-scroll-container","\n overflow-x: hidden;\n box-sizing: border-box;\n height: 100%;\n "),s("absolute-positioned","\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n ")]),T={embedded:Boolean,position:z,nativeScrollbar:{type:Boolean,default:!0},scrollbarProps:Object,onScroll:Function,contentClass:String,contentStyle:{type:[String,Object],default:""},hasSider:Boolean,siderPlacement:{type:String,default:"left"}},$=f("n-layout");function R(o){return a({name:o?"LayoutContent":"Layout",props:Object.assign(Object.assign({},i.props),T),setup(o){const e=d(null),r=d(null),{mergedClsPrefixRef:t,inlineThemeDisabled:l}=c(o),n=i("Layout","-layout",I,B,o,t);u($,o);let s=0,a=0;y(()=>{if(o.nativeScrollbar){const o=e.value;o&&(o.scrollTop=a,o.scrollLeft=s)}});const v={scrollTo:function(t,l){if(o.nativeScrollbar){const{value:o}=e;o&&(void 0===l?o.scrollTo(t):o.scrollTo(t,l))}else{const{value:o}=r;o&&o.scrollTo(t,l)}}},f=b(()=>{const{common:{cubicBezierEaseInOut:e},self:r}=n.value;return{"--n-bezier":e,"--n-color":o.embedded?r.colorEmbedded:r.color,"--n-text-color":r.textColor}}),C=l?h("layout",b(()=>o.embedded?"e":""),f,o):void 0;return Object.assign({mergedClsPrefix:t,scrollableElRef:e,scrollbarInstRef:r,hasSiderStyle:{display:"flex",flexWrap:"nowrap",width:"100%",flexDirection:"row"},mergedTheme:n,handleNativeElScroll:e=>{var r;const t=e.target;s=t.scrollLeft,a=t.scrollTop,null===(r=o.onScroll)||void 0===r||r.call(o,e)},cssVars:l?void 0:f,themeClass:null==C?void 0:C.themeClass,onRender:null==C?void 0:C.onRender},v)},render(){var e;const{mergedClsPrefix:r,hasSider:t}=this;null===(e=this.onRender)||void 0===e||e.call(this);const l=t?this.hasSiderStyle:void 0,n=[this.themeClass,o&&`${r}-layout-content`,`${r}-layout`,`${r}-layout--${this.position}-positioned`];return v("div",{class:n,style:this.cssVars},this.nativeScrollbar?v("div",{ref:"scrollableElRef",class:[`${r}-layout-scroll-container`,this.contentClass],style:[this.contentStyle,l],onScroll:this.handleNativeElScroll},this.$slots):v(S,Object.assign({},this.scrollbarProps,{onScroll:this.onScroll,ref:"scrollbarInstRef",theme:this.mergedTheme.peers.Scrollbar,themeOverrides:this.mergedTheme.peerOverrides.Scrollbar,contentClass:this.contentClass,contentStyle:[this.contentStyle,l]}),this.$slots))}})}const j=R(!1),O=R(!0),w=n("layout-footer","\n transition:\n box-shadow .3s var(--n-bezier),\n color .3s var(--n-bezier),\n background-color .3s var(--n-bezier),\n border-color .3s var(--n-bezier);\n color: var(--n-text-color);\n background-color: var(--n-color);\n box-sizing: border-box;\n",[s("absolute-positioned","\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n "),s("bordered","\n border-top: solid 1px var(--n-border-color);\n ")]),P=Object.assign(Object.assign({},i.props),{inverted:Boolean,position:z,bordered:Boolean}),L=a({name:"LayoutFooter",props:P,setup(o){const{mergedClsPrefixRef:e,inlineThemeDisabled:r}=c(o),t=i("Layout","-layout-footer",w,B,o,e),l=b(()=>{const{common:{cubicBezierEaseInOut:e},self:r}=t.value,l={"--n-bezier":e};return o.inverted?(l["--n-color"]=r.footerColorInverted,l["--n-text-color"]=r.textColorInverted,l["--n-border-color"]=r.footerBorderColorInverted):(l["--n-color"]=r.footerColor,l["--n-text-color"]=r.textColor,l["--n-border-color"]=r.footerBorderColor),l}),n=r?h("layout-footer",b(()=>o.inverted?"a":"b"),l,o):void 0;return{mergedClsPrefix:e,cssVars:r?void 0:l,themeClass:null==n?void 0:n.themeClass,onRender:null==n?void 0:n.onRender}},render(){var o;const{mergedClsPrefix:e}=this;return null===(o=this.onRender)||void 0===o||o.call(this),v("div",{class:[`${e}-layout-footer`,this.themeClass,this.position&&`${e}-layout-footer--${this.position}-positioned`,this.bordered&&`${e}-layout-footer--bordered`],style:this.cssVars},this.$slots)}}),E=n("layout-header","\n transition:\n color .3s var(--n-bezier),\n background-color .3s var(--n-bezier),\n box-shadow .3s var(--n-bezier),\n border-color .3s var(--n-bezier);\n box-sizing: border-box;\n width: 100%;\n background-color: var(--n-color);\n color: var(--n-text-color);\n",[s("absolute-positioned","\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n "),s("bordered","\n border-bottom: solid 1px var(--n-border-color);\n ")]),k={position:z,inverted:Boolean,bordered:{type:Boolean,default:!1}},_=a({name:"LayoutHeader",props:Object.assign(Object.assign({},i.props),k),setup(o){const{mergedClsPrefixRef:e,inlineThemeDisabled:r}=c(o),t=i("Layout","-layout-header",E,B,o,e),l=b(()=>{const{common:{cubicBezierEaseInOut:e},self:r}=t.value,l={"--n-bezier":e};return o.inverted?(l["--n-color"]=r.headerColorInverted,l["--n-text-color"]=r.textColorInverted,l["--n-border-color"]=r.headerBorderColorInverted):(l["--n-color"]=r.headerColor,l["--n-text-color"]=r.textColor,l["--n-border-color"]=r.headerBorderColor),l}),n=r?h("layout-header",b(()=>o.inverted?"a":"b"),l,o):void 0;return{mergedClsPrefix:e,cssVars:r?void 0:l,themeClass:null==n?void 0:n.themeClass,onRender:null==n?void 0:n.onRender}},render(){var o;const{mergedClsPrefix:e}=this;return null===(o=this.onRender)||void 0===o||o.call(this),v("div",{class:[`${e}-layout-header`,this.themeClass,this.position&&`${e}-layout-header--${this.position}-positioned`,this.bordered&&`${e}-layout-header--bordered`],style:this.cssVars},this.$slots)}});const V=o({},[["render",function(o,e){const r=_,t=C("router-view"),l=O,n=L,s=j;return p(),m(s,{class:"h-full overflow-hidden"},{default:g(()=>[x(r),x(l,{"content-class":"flex flex-col flex-auto overflow-x-auto bg-codeColor",class:"h-full flex flex-col"},{default:g(()=>[x(t)]),_:1}),x(n)]),_:1})}]]);export{V as default};
This source diff could not be displayed because it is too large. You can view the blob instead.
import{_ as r}from"./_plugin-vue_export-helper-BCo6x5W8.js";import{o as e,x as o}from"./index-BGNM-WBG.js";const t=r({},[["render",function(r,t){return e(),o("div")}]]);export{t as default};
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:"";content:none}table{border-collapse:collapse;border-spacing:0}html,body,#app{width:100%;height:100%;overflow:hidden;font-size:14px;font-family:PuHuiRegular}div{box-sizing:border-box}::-webkit-scrollbar{border-radius:5px;-moz-border-radius:5px;-webkit-border-radius:5px;width:5px;height:5px}::-webkit-scrollbar-thumb{border-radius:5px;-moz-border-radius:5px;-webkit-border-radius:5px;background-color:var(--scrollbar-color);background-clip:padding-box;min-height:28px}::-webkit-scrollbar-thumb:hover{border-radius:5px;-moz-border-radius:5px;-webkit-border-radius:5px;background-color:var(--scrollbar-color-hover)}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.\!container{width:100%!important}.container{width:100%}@media (min-width: 640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width: 768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width: 1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width: 1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width: 1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.z-10{z-index:10}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mr-\[15px\]{margin-right:15px}.mt-3{margin-top:.75rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-full{height:100%}.max-h-48{max-height:12rem}.max-h-\[300px\]{max-height:300px}.min-h-\[100px\]{min-height:100px}.w-\[350px\]{width:350px}.w-full{width:100%}.min-w-\[72px\]{min-width:72px}.flex-1{flex:1 1 0%}.flex-auto{flex:1 1 auto}.flex-shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-normal{white-space:normal}.rounded{border-radius:.25rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-solid{border-style:solid}.border-borderColor{border-color:var(--border-color)}.border-green-100{--tw-border-opacity: 1;border-color:rgb(220 252 231 / var(--tw-border-opacity))}.border-red-100{--tw-border-opacity: 1;border-color:rgb(254 226 226 / var(--tw-border-opacity))}.bg-baseColor{background-color:var(--base-color)}.bg-codeColor{background-color:var(--code-color)}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity))}.bg-progressRailColor{background-color:var(--progress-rail-color)}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity))}.bg-warningColorSuppl{background-color:var(--warning-color-suppl)}.\!p-0{padding:0!important}.p-\[10px\]{padding:10px}.px-3{padding-left:.75rem;padding-right:.75rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-\[15px\]{padding-top:15px;padding-bottom:15px}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-\[11px\]{font-size:11px}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.italic{font-style:italic}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.opacity-60{opacity:.6}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:before,:after{z-index:1}.w-e-text-container [data-slate-editor]{font-size:13px;line-height:1.6}img{width:100%}a{text-decoration:none}text{text-indent:0!important;display:inline-block!important}[data-w-e-type=TITLE],[data-w-e-type=HTML-TITLE],[data-w-e-type=TITLEC]{font-weight:700;font-size:15px}[data-w-e-type=PARAC]>*,[data-w-e-type=PARA]>*{display:inline!important;text-indent:0!important}[data-w-e-type=TITLE]>*,[data-w-e-type=TITLEC]>*,[data-w-e-type=HTML-TITLE]>*{display:inline-block!important;text-indent:0!important}[data-w-e-type=ZONELST]{display:none!important}[data-w-e-type=STDNAME]{display:inline-block!important;text-indent:0!important;margin:0 5px!important}:not([data-w-e-type=CEP],[data-w-e-type=SUBTASK])[MERGED=TRUE]{background-color:#ff0!important}[MERGED=TRUE]:before{background-color:#ff0!important}[data-w-e-type=JOBCARD] [data-w-e-type=REFBLOCK] [data-w-e-type=GRPHCREF]{text-decoration:none;color:#00f;cursor:pointer}[data-w-e-type=CON]{display:inline!important}[data-w-e-type=CON] [data-w-e-type=CONNBR]{text-indent:0!important;display:inline!important;white-space:nowrap!important}[data-w-e-type=CON] [data-w-e-type=CONNAME]{text-indent:0!important;display:inline!important}[data-w-e-type=SPECNOTE] [data-w-e-type=PARAC]:before{font-weight:700;content:"特 殊 注 释: "}[data-w-e-type=SPECNOTE] [data-w-e-type=PARA]:before{font-weight:700;content:"SPECIAL NOTE: "}[data-w-e-type=EIN]{display:inline-flex!important;text-indent:0!important;margin:0 3px!important;cursor:pointer;color:#00f}[data-w-e-type=EIN]:before{content:"FIN";color:var(--w-e-textarea-color);margin-right:4px}[data-w-e-type=EIN] span{text-decoration:underline}[data-w-e-type=EINDATA]{display:inline!important;text-indent:0!important}[data-w-e-type=EINLST][data-same-effect=true]{display:inline-flex!important;flex-wrap:wrap;align-items:center;text-indent:0!important;gap:0}[data-w-e-type=EINLST][data-same-effect=true]:before{content:"FIN:";color:var(--w-e-textarea-color);font-weight:400;margin-right:4px;white-space:nowrap}[data-w-e-type=EINLST][data-same-effect=true] [data-w-e-type=EINDATA]{display:inline!important;text-indent:0!important}[data-w-e-type=EINLST][data-same-effect=true] [data-w-e-type=EFFECT],[data-w-e-type=EINLST][data-same-effect=true] [data-w-e-type=CONEFFECT]{display:none!important}[data-w-e-type=EINLST][data-same-effect=true] [data-w-e-type=EIN]{display:inline!important;margin:0 4px!important}[data-w-e-type=EINLST][data-same-effect=true] [data-w-e-type=EIN]:before{content:""!important}[data-w-e-type=EINLST][data-same-effect=true] [data-w-e-type=EINMFR]{display:inline!important;margin:0 4px!important;text-indent:0!important}[data-w-e-type=EINLST][data-same-effect=true] [data-w-e-type=EINMFR] [data-w-e-type=MFR]{display:none!important}[data-w-e-type=EINLST][data-same-effect=false]{display:block!important;text-indent:0!important}[data-w-e-type=EINLST][data-same-effect=false]:before{content:"FIN:"!important;display:block!important;color:var(--w-e-textarea-color);font-weight:400}[data-w-e-type=EINLST][data-same-effect=false] [data-w-e-type=EINDATA]{display:block!important;text-indent:0!important;padding:1px 0}[data-w-e-type=EINLST][data-same-effect=false] [data-w-e-type=EFFECT],[data-w-e-type=EINLST][data-same-effect=false] [data-w-e-type=CONEFFECT]{display:block!important;font-style:italic;font-weight:700;color:red;padding:0!important;text-indent:0!important}[data-w-e-type=EINLST][data-same-effect=false] [data-w-e-type=EFFECT] [data-slate-leaf=true],[data-w-e-type=EINLST][data-same-effect=false] [data-w-e-type=CONEFFECT] [data-slate-leaf=true]{position:absolute!important;width:1px!important;height:1px!important;opacity:0!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;pointer-events:none!important;-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}[data-w-e-type=EINLST][data-same-effect=false] [data-w-e-type=EIN]{display:inline!important;margin:0 4px!important}[data-w-e-type=EINLST][data-same-effect=false] [data-w-e-type=EIN]:before{content:""!important}[data-w-e-type=EINLST][data-same-effect=false] [data-w-e-type=EINMFR]{display:inline!important;margin:0 4px!important;text-indent:0!important}[data-w-e-type=EINLST][data-same-effect=false] [data-w-e-type=EINMFR] [data-w-e-type=MFR]{display:none!important}[data-w-e-type=EINLST][data-sbeff-mode=true]{display:block!important;text-indent:0!important}[data-w-e-type=EINLST][data-sbeff-mode=true]:before{content:"FIN:"!important;display:block!important;color:var(--w-e-textarea-color);font-weight:400;margin-right:0;white-space:normal}[data-w-e-type=EINLST][data-sbeff-mode=true] [data-w-e-type=EINDATA]{display:inline!important;text-indent:0!important}[data-w-e-type=EINLST][data-sbeff-mode=true] [data-w-e-type=EFFECT],[data-w-e-type=EINLST][data-sbeff-mode=true] [data-w-e-type=CONEFFECT]{display:none!important}[data-w-e-type=EINLST][data-sbeff-mode=true] [data-w-e-type=EIN]{display:inline!important;margin:0 4px!important}[data-w-e-type=EINLST][data-sbeff-mode=true] [data-w-e-type=EIN]:before{content:""!important}[data-w-e-type=EINLST][data-sbeff-mode=true] [data-w-e-type=EINMFR]{display:inline!important;margin:0 4px!important;text-indent:0!important}[data-w-e-type=EINLST][data-sbeff-mode=true] [data-w-e-type=EINMFR] [data-w-e-type=MFR]{display:none!important}[data-w-e-type=REFBLOCK]{display:inline!important;text-indent:0!important;cursor:pointer;text-decoration:none;color:#00f}[data-w-e-type=REFBLOCK][IS-DOUBL=Y]{text-decoration:underline}[data-w-e-type=PARAC] [data-w-e-type=REFBLOCK]:not(:has([data-w-e-type=REFINT],[data-w-e-type=REFEXT],[data-w-e-type=GRPHCREF])):before{content:"(参考.AMM TASK ";color:#00f;text-decoration:none}[data-w-e-type=PARA] [data-w-e-type=REFBLOCK]:not(:has([data-w-e-type=REFINT],[data-w-e-type=REFEXT],[data-w-e-type=GRPHCREF])):before{content:"(Ref.AMM TASK ";color:#00f;text-decoration:none}[data-w-e-type=REFBLOCK]:not(:has([data-w-e-type=REFINT],[data-w-e-type=REFEXT],[data-w-e-type=GRPHCREF])):after{content:")";color:#00f;text-decoration:none}[data-w-e-type=REFBLOCK]:has([data-w-e-type=REFINT],[data-w-e-type=REFEXT],[data-w-e-type=GRPHCREF]):before{content:" "}[data-w-e-type=REFBLOCK]:has([data-w-e-type=REFINT],[data-w-e-type=REFEXT],[data-w-e-type=GRPHCREF]):after{content:" "}[data-w-e-type=REFBLOCK]:before,[data-w-e-type=REFBLOCK]:after{font-size:13px!important}[data-w-e-type=REFEXT]{display:inline!important;text-indent:0!important;text-decoration:none;cursor:pointer;color:#00f}[data-w-e-type=REFEXT] span{text-decoration:underline;font-size:13px!important}[data-w-e-type=REFEXT]:before{content:"("}[data-w-e-type=REFEXT]:after{content:") ";color:#00f}[data-w-e-type=PARAC] [data-w-e-type=REFEXT]:after{content:")"}[data-w-e-type=REFEXT]:before,[data-w-e-type=REFEXT]:after{font-size:13px!important}[data-w-e-type=REFBLOCK] [data-w-e-type=REFEXT]:before,[data-w-e-type=REFBLOCK] [data-w-e-type=REFEXT]:after{content:""!important;text-decoration:none}[data-w-e-type=REFINT]{display:inline!important;text-indent:0!important;cursor:pointer;color:#00f;font-size:13px!important}[data-w-e-type=REFINT] span{text-decoration:underline}[data-w-e-type=REFINT]:before{content:" (Ref.AMM TASK "}[data-w-e-type=PARAC] [data-w-e-type=REFINT]:before{content:"(参考.AMM TASK "}[data-w-e-type=REFINT]:after{content:") "}[data-w-e-type=PARAC] [data-w-e-type=REFINT]:after{content:")"}[data-w-e-type=REFINT]:before,[data-w-e-type=REFINT]:after{font-size:13px!important}[data-w-e-type=EQUNAME]{display:inline!important;text-indent:0!important}[data-w-e-type=EQUNAME]:before{content:"("}[data-w-e-type=EQUNAME]:after{content:")"}[data-w-e-type=EXTERNAL-LINKS]{display:block!important;margin:10px 0!important}[data-w-e-type=EXTERNAL-LINKS]:before{content:"参考文件:";display:block;margin:5px 0 5px 10px;font-weight:700}[data-w-e-type=EXTERNAL-LINK]{display:block!important;margin:5px 0 5px 10px!important;cursor:pointer}[data-w-e-type=EXTERNAL-LINK][FILELINK]:not([FILELINK=""]){color:#00f;text-decoration:underline}[data-w-e-type=GRPHCREF]{font-weight:700;cursor:pointer;display:inline-block!important;width:100%;color:#00f;font-size:13px!important;text-indent:0!important;word-break:break-all;white-space:normal}[data-w-e-type=GRPHCREF]>*:not([data-w-e-type=EFFECT]){display:inline!important;text-decoration:underline}[data-w-e-type=REFBLOCK] [data-w-e-type=GRPHCREF]{display:inline!important;width:auto!important}[data-w-e-type=GRPHCREF]:before{content:"Ref. Fig. ";text-decoration:underline}[data-w-e-type=GRPHCREF]:after{content:"";display:none!important}[data-w-e-type=GRPHCREF]:has(>[data-w-e-type=EFFECT]){display:block!important}[data-w-e-type=GRPHCREF]:has(>[data-w-e-type=EFFECT]):before{content:""!important;display:none!important}[data-w-e-type=GRPHCREF]>[data-w-e-type=EFFECT]+*{display:inline!important}[data-w-e-type=GRPHCREF]>[data-w-e-type=EFFECT]+*:before{content:"Ref. Fig. ";text-decoration:underline!important}[data-w-e-type=GRPHCREF]>[data-w-e-type=EFFECT]{display:block!important}[data-w-e-type=PARAC] [data-w-e-type=GRPHCREF]:before{content:"参考图 ";text-decoration:underline}[data-w-e-type=PARAC] [data-w-e-type=GRPHCREF]>[data-w-e-type=EFFECT]+*:before{content:"参考图 ";text-decoration:underline!important}[data-w-e-type=GRPHCREF]:before,[data-w-e-type=GRPHCREF]:after{font-size:13px!important;text-indent:0!important}[data-w-e-type=REFBLOCK] [data-w-e-type=GRPHCREF]:before,[data-w-e-type=REFBLOCK] [data-w-e-type=GRPHCREF]:after,[data-w-e-type=REFBLOCK] [data-w-e-type=GRPHCREF]>span:before{content:""!important;text-decoration:none}[data-w-e-type=PAN]{display:inline!important;text-indent:0!important}[data-w-e-type=PAN]:before{content:" "}[data-w-e-type=PAN]:after{content:" "}[data-w-e-type=REFBLOCK] [data-w-e-type=REFINT]>[data-w-e-type=EFFECT],[data-w-e-type=REFBLOCK] [data-w-e-type=REFEXT]>[data-w-e-type=EFFECT],[data-w-e-type=REFBLOCK] [data-w-e-type=GRPHCREF]>[data-w-e-type=EFFECT],[data-w-e-type=REFINT]>[data-w-e-type=EFFECT]{display:none!important}[data-w-e-type=ENTRY] [data-w-e-type=CON] [data-w-e-type=CONNBR]:before,[data-w-e-type=ENTRY] [data-w-e-type=CON] [data-w-e-type=CONNBR]:after{content:""!important}[data-w-e-type=ENTRY] [data-w-e-type=REFEXT]:before,[data-w-e-type=ENTRY] [data-w-e-type=REFEXT]:after{content:""!important}[data-w-e-type=ENTRY] [data-w-e-type=REFEXT]>[data-w-e-type=EFFECT]{display:none!important}[data-w-e-type=ENTRY] [data-w-e-type=REFINT]:before{content:"Ref. "!important;text-decoration:underline}[data-w-e-type=ENTRY] [data-w-e-type=REFINT]:after{content:""!important}[data-w-e-type=ENTRY] [data-w-e-type=GRPHCREF]:has([data-w-e-type=EFFECT]){display:flex!important;flex-direction:column;width:100%;text-decoration:none!important}[data-w-e-type=ENTRY] [data-w-e-type=GRPHCREF]:has([data-w-e-type=EFFECT])>[data-w-e-type=EFFECT]{text-align:left;display:inline!important;white-space:nowrap;text-decoration:none!important}[data-w-e-type=ENTRY] [data-w-e-type=TOOLNBR]:before{content:" "!important}[data-w-e-type=ENTRY] [data-w-e-type=TOOLNBR]:after{content:""!important}[data-w-e-type=NOTE]{padding-left:80px!important;color:#00f}[data-w-e-type=NOTE]:before{content:"注意 NOTE:";text-decoration:underline;font-weight:700;position:absolute;left:0}[data-w-e-type=WARNING]{color:red;padding-left:120px!important;font-weight:700;text-transform:uppercase}[data-w-e-type=WARNING]:before{content:"警告 WARNING:";font-weight:700;text-decoration:underline;position:absolute;left:0}[data-w-e-type=CAUTION]{color:#ff6a00;font-weight:700;padding-left:140px!important}[data-w-e-type=CAUTION]:before{content:"警戒 CAUTION:";font-weight:700;text-decoration:underline;position:absolute;left:0}[data-w-e-type=HTML-TABLE] [MERGED=TRUE] [data-w-e-type=ROW],[data-w-e-type=HTML-TABLE] [MERGED=TRUE] [data-w-e-type=ENTRY]{background-color:#ff0!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=TITLEC],[data-w-e-type=HTML-TABLE] [data-w-e-type=TITLE]{text-indent:0!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ROW]{display:contents!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ROW]>span{grid-column:1 / 2;grid-row:1 / 2;height:0!important;width:0!important;overflow:hidden!important;margin:0!important;padding:0!important;border:none!important;opacity:0!important;pointer-events:none!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ROW] [data-w-e-type=NOTE]{text-indent:0!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ROW]>[data-w-e-type=EFFECT],[data-w-e-type=HTML-TABLE] [data-w-e-type=ROW]>[data-w-e-type=CONEFFECT]{grid-column:1 / -1!important;display:block!important;text-indent:0!important;width:100%;border-bottom:1px solid black;font-style:italic;font-weight:700;padding:.5mm}[data-w-e-type=HTML-TABLE] [data-w-e-type=TGROUP]{border:1px solid black;border-collapse:collapse}[data-w-e-type=HTML-TABLE] [data-w-e-type=TGROUP] [data-w-e-type=PARA],[data-w-e-type=HTML-TABLE] [data-w-e-type=TGROUP] [data-w-e-type=PARAC],[data-w-e-type=HTML-TABLE] [data-w-e-type=TGROUP] [data-w-e-type=ZONE]{text-indent:0!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=HTML-THEAD]{display:grid!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=HTML-THEAD] [data-w-e-type=ENTRY]{padding:0 3px;background-color:gray;font-weight:700}[data-w-e-type=HTML-TABLE] [data-w-e-type=HTML-TBODY]{display:grid!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=HTML-TBODY] [data-w-e-type=ENTRY]{padding:6px 3px;min-height:33.8px}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY]{border-right:1px solid black;border-bottom:1px solid black;background-color:var(--table-color-striped);display:flex!important;flex-direction:column!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY][DISABLED=TRUE]{background-color:#fff!important;background-image:linear-gradient(45deg,#cccccc 15%,transparent 15%,transparent 50%,#cccccc 50%,#cccccc 65%,transparent 65%,transparent)!important;background-size:12px 12px!important}body.env-bx [data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY][DISABLED=TRUE]{background-color:#6a7280!important;background-image:none!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY][ALIGN=CENTER]{text-align:center!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY][ALIGN=RIGHT]{text-align:right!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY][ALIGN=LEFT]{text-align:left!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY][ALIGN=JUSTIFY]{text-align:justify!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY][ALIGN=CHAR]{text-align:left!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY]:not([ALIGN])[data-default-align=CENTER]{text-align:center!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY]:not([ALIGN])[data-default-align=RIGHT]{text-align:right!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY]:not([ALIGN])[data-default-align=LEFT]{text-align:left!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY]:not([ALIGN])[data-default-align=JUSTIFY]{text-align:justify!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY]:not([ALIGN])[data-default-align=CHAR]{text-align:left!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY][VALIGN=TOP]{justify-content:flex-start!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY][VALIGN=MIDDLE]{justify-content:center!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY][VALIGN=BOTTOM]{justify-content:flex-end!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY] [data-w-e-type=PARA],[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY] [data-w-e-type=PARAC]{width:100%;flex-shrink:0}[data-w-e-type=GRAPHIC]{display:block!important;margin:10px 0}[data-w-e-type=GRAPHIC]:has(>[data-w-e-type=SHEET]>[data-w-e-type=EFFECT])>[data-w-e-type=EFFECT],[data-w-e-type=GRAPHIC]:has(>[data-w-e-type=SHEET]>[data-w-e-type=CONEFFECT])>[data-w-e-type=CONEFFECT]{display:none!important}[data-w-e-type=GRAPHIC]:has(>[data-w-e-type=SHEET]>[data-w-e-type=TITLE])>[data-w-e-type=TITLE],[data-w-e-type=GRAPHIC]:has(>[data-w-e-type=SHEET]>[data-w-e-type=TITLEC])>[data-w-e-type=TITLEC],[data-w-e-type=GRAPHIC]:has(>[data-w-e-type=SHEET]>[data-w-e-type=HTML-TITLE])>[data-w-e-type=HTML-TITLE]{display:none!important}[data-w-e-type=GRAPHIC] [data-w-e-type=EFFECT],[data-w-e-type=GRAPHIC] [data-w-e-type=CONEFFECT],[data-w-e-type=GRAPHIC] [data-w-e-type=TITLE],[data-w-e-type=GRAPHIC] [data-w-e-type=TITLEC],[data-w-e-type=GRAPHIC] [data-w-e-type=HTML-TITLE]{text-align:center}[data-w-e-type=SHEET]{display:flex!important;flex-direction:column;margin-bottom:20px}[data-w-e-type=SHEET]>[data-sheet-img-container]{order:0}[data-w-e-type=SHEET]>[data-slate-placeholder]{order:1}[data-w-e-type=SHEET]>[data-w-e-type=TITLE][data-sheet-caption=true],[data-w-e-type=SHEET]>[data-w-e-type=TITLEC][data-sheet-caption=true],[data-w-e-type=SHEET]>[data-w-e-type=HTML-TITLE][data-sheet-caption=true]{order:2;display:block!important;margin-top:6px}[data-w-e-type=SHEET]>[data-w-e-type=TITLE][data-sheet-caption=true]:before,[data-w-e-type=SHEET]>[data-w-e-type=TITLEC][data-sheet-caption=true]:before,[data-w-e-type=SHEET]>[data-w-e-type=HTML-TITLE][data-sheet-caption=true]:before{content:attr(data-caption-prefix);display:inline}[data-w-e-type=SHEET]>[data-w-e-type=EFFECT][data-sheet-effect=true],[data-w-e-type=SHEET]>[data-w-e-type=CONEFFECT][data-sheet-effect=true]{order:3;display:block!important}[data-w-e-type=SHEET]>[data-w-e-type=EFFECT][data-sheet-effect=true]>*,[data-w-e-type=SHEET]>[data-w-e-type=CONEFFECT][data-sheet-effect=true]>*{display:none!important}[data-w-e-type=SHEET]>[data-w-e-type=EFFECT][data-sheet-effect=true]:before,[data-w-e-type=SHEET]>[data-w-e-type=CONEFFECT][data-sheet-effect=true]:before{content:"** ON A/C FSN " attr(data-effno);display:block}[data-w-e-type=GDESC]{display:block!important;margin:5px 20px;text-align:left}[data-w-e-type=FTNOTE]{display:block!important;font-size:.9em;margin-top:5px;text-align:left}[data-w-e-type=EFFECT] [data-slate-leaf=true]{position:absolute!important;top:0!important;left:0!important;width:1px!important;height:1px!important;opacity:0!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;pointer-events:none!important;-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}[data-w-e-type=SUPERSCRIPT],[data-w-e-type=SUPER]{font-size:8pt;vertical-align:super;display:inline!important}[data-w-e-type=SUBSCRIPT],[data-w-e-type=SUB]{font-size:8pt;vertical-align:sub;display:inline!important}[data-w-e-type=SBEFFC],[data-w-e-type=SBEFF]{font-style:italic;font-weight:700;padding:.5mm}[data-w-e-type=REFINT] [data-w-e-type=SBEFFC],[data-w-e-type=REFINT] [data-w-e-type=SBEFF]{display:inline!important}[data-w-e-type=SBEFFC]:not([data-w-e-type=REFINT] *),[data-w-e-type=SBEFF]:not([data-w-e-type=REFINT] *){display:block!important}[data-w-e-type=SIGNOFF]{scroll-margin-left:0!important;scroll-margin-right:0!important;scroll-margin-bottom:0!important;text-align:right!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}[data-w-e-type=SIGNOFF] span:before{display:inline-block;width:100%}[data-w-e-type=SIGNOFF] [data-slate-placeholder]{scroll-margin-left:0!important;scroll-margin-right:0!important;scroll-margin-bottom:0!important}.env-dev [data-w-e-type=SIGNOFF]:not([CK-LEVEL]) p:first-child span:first-child:before,.env-dev [data-w-e-type=SIGNOFF][CK-LEVEL=""] p:first-child span:first-child:before,.env-dev [data-w-e-type=SIGNOFF][CK-LEVEL=A] p:first-child span:first-child:before{content:"工作者\aMechanic";white-space:pre-line}.env-dev [data-w-e-type=SIGNOFF][CK-LEVEL=B] p:nth-child(1) span:first-child:before{content:"工作者\aMechanic";white-space:pre-line}.env-dev [data-w-e-type=SIGNOFF][CK-LEVEL=B] p:nth-child(2) span:first-child:before{content:"检查者\aInspector";white-space:pre-line}.env-dev [data-w-e-type=SIGNOFF][CK-LEVEL=C] p:nth-child(1) span:first-child:before{content:"工作者\aMechanic";white-space:pre-line}.env-dev [data-w-e-type=SIGNOFF][CK-LEVEL=C] p:nth-child(2) span:first-child:before{content:"必检\aInspected";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF] p{width:200px}.env-bx [data-w-e-type=SIGNOFF]:not([CK-LEVEL]) p:first-child span:first-child:before,.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=""] p:first-child span:first-child:before,.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=A] p:first-child span:first-child:before{content:"工作者\aMechanic";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=B] p:nth-child(1) span:first-child:before{content:"工作者\aMechanic";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=B] p:nth-child(2) span:first-child:before{content:"检验员\aInspector";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=C] p:nth-child(1) span:first-child:before{content:"工作者\aMechanic";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=C] p:nth-child(2) span:first-child:before{content:"必检\aInspected";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=S] p:first-child span:first-child:before{content:"权限许可\aPermission";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=S] p:first-child span:nth-child(2):before{content:"S";line-height:40px}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=S] p:nth-child(2) span:first-child:before{content:"工作者\aMechanic";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=S] p:nth-child(3) span:first-child:before{content:"检验员\aInspector";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=M] p:first-child span:first-child:before{content:"权限许可\aPermission";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=M] p:first-child span:nth-child(2):before{content:"M";line-height:40px}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=M] p:nth-child(2) span:first-child:before{content:"工作者\aMechanic";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=M] p:nth-child(3) span:first-child:before{content:"检验员\aInspector";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL="M(N/A)"] p:first-child span:first-child:before{content:"权限许可\aPermission";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL="M(N/A)"] p:first-child span:nth-child(2):before{content:"M";line-height:40px}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL="M(N/A)"] p:nth-child(2) span:first-child:before{content:"工作者\aMechanic";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL="M(N/A)"] p:nth-child(3) span:first-child:before{content:"检验员\aInspector";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL="M(N/A)"] p:nth-child(3) span:nth-child(2):before{content:"N/A";line-height:40px}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=T] p:first-child span:first-child:before{content:"权限许可\aPermission";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=T] p:first-child span:nth-child(2):before{content:"T";line-height:40px}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=T] p:nth-child(2) span:first-child:before{content:"工作者\aMechanic";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=T] p:nth-child(3) span:first-child:before{content:"检验员\aInspector";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=R] p:first-child span:first-child:before{content:"权限许可\aPermission";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=R] p:first-child span:nth-child(2):before{content:"R";line-height:40px}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=R] p:nth-child(2) span:first-child:before{content:"工作者\aMechanic";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=R] p:nth-child(3) span:first-child:before{content:"检验员\aInspector";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=G] p:first-child span:first-child:before{content:"权限许可\aPermission";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=G] p:first-child span:nth-child(2):before{content:"G";line-height:40px}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=G] p:nth-child(2) span:first-child:before{content:"工作者\aMechanic";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=G] p:nth-child(3) span:first-child:before{content:"检验员\aInspector";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=SP] p:first-child span:first-child:before{content:"权限许可\aPermission";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=SP] p:first-child span:nth-child(2):before{content:"SP";line-height:40px}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=SP] p:nth-child(2) span:first-child:before{content:"工作者\aMechanic";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=SP] p:nth-child(3) span:first-child:before{content:"SP检验员\aSP Inspector";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=SPAM] p:first-child span:first-child:before{content:"权限许可\aPermission";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=SPAM] p:first-child span:nth-child(2):before{content:"SP";line-height:40px}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=SPAM] p:nth-child(2) span:first-child:before{content:"工作者\aMechanic";white-space:pre-line}.env-bx [data-w-e-type=SIGNOFF][CK-LEVEL=SPAM] p:nth-child(3) span:first-child:before{content:"航材检验员\a AM Inspector";white-space:pre-line}.env-xm [data-w-e-type=SIGNOFF]:not([CK-LEVEL]) p:first-child span:first-child:before,.env-xm [data-w-e-type=SIGNOFF][CK-LEVEL=""] p:first-child span:first-child:before,.env-xm [data-w-e-type=SIGNOFF][CK-LEVEL=A] p:first-child span:first-child:before{content:"工作者\aPerf.By :";white-space:pre-line}.env-xm [data-w-e-type=SIGNOFF][CK-LEVEL=B] p:nth-child(1) span:first-child:before{content:"工作者\aPerf.By :";white-space:pre-line}.env-xm [data-w-e-type=SIGNOFF][CK-LEVEL=B] p:nth-child(2) span:first-child:before{content:"互检者\aRCI.BY :";white-space:pre-line}.env-xm [data-w-e-type=SIGNOFF][CK-LEVEL=C] p:nth-child(1) span:first-child:before{content:"工作者\aPerf.By :";white-space:pre-line}.env-xm [data-w-e-type=SIGNOFF][CK-LEVEL=C] p:nth-child(2) span:first-child:before{content:"检查者\aRll.By :";white-space:pre-line}[data-w-e-type=SIGNOFF] p{width:170px;height:42px;display:inline-block!important;border:1px solid black;font-size:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:default}[data-w-e-type=SIGNOFF] p:nth-child(2n){border-left:none}[data-w-e-type=SIGNOFF] span{width:50%;height:100%;float:left;text-align:center;font-weight:700;border-right:1px solid black;border-bottom:1px solid black;font-size:14px;vertical-align:top;box-sizing:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none}[data-w-e-type=SIGNOFF] span text{display:block!important}[data-w-e-type=SIGNOFF] span:last-child,[data-w-e-type=SIGNOFF] span:nth-last-child(2){border-bottom:none}[data-w-e-type=SIGNOFF] [data-slate-placeholder]{display:none!important}:not([data-w-e-type=SUBTASK])[chapnbr]:before{content:"任务 " attr(chapnbr) "-" attr(sectnbr) "-" attr(subjnbr) "-" attr(func) "-" attr(seq) "-" attr(confnbr) "-" attr(confltr)}[data-w-e-type=CEP][chapnbr]:before{content:"任务 " attr(chapnbr) "-" attr(sectnbr) "-" attr(subjnbr) "-" attr(func) "-" attr(seq) "-" attr(confltr)}[data-w-e-type=SUBTASK]:before{content:"子任务 " attr(chapnbr) "-" attr(sectnbr) "-" attr(subjnbr) "-" attr(func) "-" attr(seq) "-" attr(confltr)}[data-w-e-type=EFFECT]{scroll-margin-left:0!important;scroll-margin-right:0!important;scroll-margin-bottom:0!important;font-style:italic;font-weight:700;padding:.5mm;color:red}[data-w-e-type=EFFECT][EFFTEXT]:before{content:"** ON A/C: " attr(efftext)}[data-w-e-type=EFFECT]:not([EFFTEXT])[EFFRG="001999"]:before,[data-w-e-type=EFFECT]:not([EFFTEXT])[EFFRG="001-999"]:before{content:"** ON A/C: ALL"}[data-w-e-type=EFFECT]:not([EFFTEXT]):not([EFFRG="001999"]):not([EFFRG="001-999"]):before{content:"** ON A/C: " attr(effrg)}[data-w-e-type=CONEFFECT]{font-style:italic;font-weight:700;padding:.5mm;color:red}[data-w-e-type=CONEFFECT]:before{content:"** CONF: " attr(effrg)}[data-w-e-type=SBEFF]{text-indent:0!important;font-style:italic;font-weight:700;padding:.5mm}[data-w-e-type=SBEFF][EFFRG="001999"]:before,[data-w-e-type=SBEFF][EFFRG="001-999"]:before{content:attr(sbcond) " SB " attr(sbnbr) " for A/C ALL";color:red}[data-w-e-type=SBEFF]:not([EFFRG="001999"]):not([EFFRG="001-999"]):before{content:attr(sbcond) " SB " attr(sbnbr) " for A/C " attr(effrg);color:red}[data-w-e-type=SBEFFC]{text-indent:0!important;font-style:italic;font-weight:700;padding:.5mm}[data-w-e-type=SBEFFC][EFFRG="001999"]:before,[data-w-e-type=SBEFFC][EFFRG="001-999"]:before{content:attr(sbcond) " SB " attr(sbnbr) " for A/C ALL";color:red}[data-w-e-type=SBEFFC]:not([EFFRG="001999"]):not([EFFRG="001-999"]):before{content:attr(sbcond) " SB " attr(sbnbr) " for A/C " attr(effrg);color:red}[data-w-e-type=SUB],[data-w-e-type=SUBSCRIPT]{display:inline!important;text-indent:0!important;font-size:.75em;vertical-align:sub}[data-w-e-type=SUPER],[data-w-e-type=SUPERSCRIPT]{display:inline!important;text-indent:0!important;font-size:.75em;vertical-align:super}[data-w-e-type=RECORD-LINE]{display:block!important}[data-w-e-type=RECORD-LINE]>*{display:inline!important}[data-w-e-type=RECORD-LINE] [data-w-e-type=PARA],[data-w-e-type=RECORD-LINE] [data-w-e-type=PARAC]{display:inline!important;width:auto!important}[data-w-e-type=RECORD-LINE] [data-w-e-type=RECORD]{display:inline-block!important;border-bottom:1px solid black;min-width:50px;text-align:center}[data-w-e-type=SELECTION]{display:inline-block!important}[data-w-e-type=SELECTION]:after{content:"*";color:red}[data-w-e-type=SELECTION][required=NO]:after{content:""!important;color:red}[data-w-e-type=SELECTION-ITEM]{margin-right:50px!important;display:inline-flex!important;align-items:center}[data-w-e-type=SELECTION-ITEM] [data-w-e-type=SELECTION-LBL-CN],[data-w-e-type=SELECTION-ITEM] [data-w-e-type=SELECTION-LBL-EN]{text-indent:0!important}[data-w-e-type=SELECTION-ITEM]:after{content:"□";font-size:18px;position:absolute;top:-6px;right:-15px}[data-w-e-type=SELECTION][multi=NO] [data-w-e-type=SELECTION-ITEM]:after{content:"○"}[data-w-e-type=TXTGRPHC]{display:block!important;font-family:Courier New,Courier,monospace!important;padding:6px 24px!important;margin:6px auto!important;line-height:1.6;text-indent:0!important;box-sizing:border-box}[data-w-e-type=TXTLINE]{display:block!important;white-space:pre-wrap!important;text-indent:0!important;font-family:SimSun,宋体,serif!important}[data-w-e-type=TXTLINE]:empty:before{content:" ";white-space:pre}[data-w-e-type=TOOLNBR]{text-indent:0!important;display:inline!important;color:#00f}[data-w-e-type=TOOLNBR] span{text-decoration:underline}[data-w-e-type=TOOLNAME]{text-indent:0!important;display:inline!important}[data-w-e-type=HNANOTE]{color:#00f}[data-w-e-type=ZONE]{text-decoration:underline;color:#00f}[data-w-e-type=COLSPEC],[data-w-e-type=SPANSPEC]{display:none!important}[data-w-e-type=EXPD]{display:inline-block!important;text-indent:0!important}[data-w-e-type=EXPD] [data-w-e-type=CSN]{text-indent:0!important;display:inline-block!important}[data-w-e-type=EXPD] [data-w-e-type=CSN][data-formatted=true],[data-w-e-type=EXPD] [data-w-e-type=CSN][data-formatted=true] *{font-size:0!important}[data-w-e-type=EXPD] [data-w-e-type=CSN][data-formatted=true]:before{content:attr(data-content)!important;font-size:13px!important;color:inherit}[data-w-e-type=EXPD] [data-w-e-type=CSN][data-formatted=true]:after{content:" "!important;font-size:13px!important}[data-w-e-type=EXPD] [data-w-e-type=CSN]:not([data-formatted=true]):before{content:"IPC-CSN ("}[data-w-e-type=EXPD] [data-w-e-type=CSN]:not([data-formatted=true]):after{content:") "}[data-w-e-type=EXPD] [data-w-e-type=EXPDNAME],[data-w-e-type=EXPD] [data-w-e-type=ITEMNBR]{text-indent:0!important;display:inline!important}[data-w-e-type=ENTRY] [data-w-e-type=EXPD] [data-w-e-type=CSN]:not([data-formatted=true]):before{content:""}[data-w-e-type=ENTRY] [data-w-e-type=EXPD] [data-w-e-type=CSN]:not([data-formatted=true]):after{content:" "}[data-w-e-type=HTML-PLACEHOLDER]{display:inline!important;text-indent:0!important}.column-resize-handle{position:absolute;top:0;width:8px;height:100%;cursor:col-resize;z-index:100;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:0;transition:opacity .2s}[type=HTML-TABLE]:hover .column-resize-handle{opacity:1}.column-resize-handle.resizing{opacity:1!important}.column-resize-indicator{position:absolute;right:3px;top:0;width:2px;height:100%;background-color:#d9d9d9;transition:all .2s;pointer-events:none}.column-resize-handle:hover .column-resize-indicator{background-color:#1890ff;width:3px}.column-resize-handle.resizing .column-resize-indicator{background-color:#1890ff!important;width:3px!important}body.resizing-column,body.resizing-column *{cursor:col-resize!important;-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}:root{--paragraph-indent: 2em;--no-indent: 0}[data-w-e-type=PARA],[data-w-e-type=PARAC]{text-indent:var(--paragraph-indent)}[data-w-e-type=PARA][data-no-indent],[data-w-e-type=PARAC][data-no-indent],[data-w-e-type=PARA]:first-child,[data-w-e-type=PARAC]:first-child,[data-w-e-type=HTML-TABLE] [data-w-e-type=PARA],[data-w-e-type=HTML-TABLE] [data-w-e-type=PARAC],[data-w-e-type=L1ITEM] [data-w-e-type=PARA],[data-w-e-type=L1ITEM] [data-w-e-type=PARAC],[data-w-e-type=L2ITEM] [data-w-e-type=PARA],[data-w-e-type=L2ITEM] [data-w-e-type=PARAC],[data-w-e-type=L3ITEM] [data-w-e-type=PARA],[data-w-e-type=L3ITEM] [data-w-e-type=PARAC],[data-w-e-type=L4ITEM] [data-w-e-type=PARA],[data-w-e-type=L4ITEM] [data-w-e-type=PARAC],[data-w-e-type=L5ITEM] [data-w-e-type=PARA],[data-w-e-type=L5ITEM] [data-w-e-type=PARAC],[data-w-e-type=WARNING] [data-w-e-type=PARA],[data-w-e-type=WARNING] [data-w-e-type=PARAC],[data-w-e-type=CAUTION] [data-w-e-type=PARA],[data-w-e-type=CAUTION] [data-w-e-type=PARAC],[data-w-e-type=SPECNOTE] [data-w-e-type=PARA],[data-w-e-type=SPECNOTE] [data-w-e-type=PARAC],[data-w-e-type=NOTE] [data-w-e-type=PARA],[data-w-e-type=NOTE] [data-w-e-type=PARAC],[data-w-e-type=PRETOPIC] [data-w-e-type=PARA],[data-w-e-type=PRETOPIC] [data-w-e-type=PARAC],[data-w-e-type=STEP] [data-w-e-type=PARA],[data-w-e-type=STEP] [data-w-e-type=PARAC],[data-w-e-type=TITLE],[data-w-e-type=TITLEC],[data-w-e-type=HTML-TITLE],[data-w-e-type=REFINT],[data-w-e-type=REFEXT],[data-w-e-type=GRPHCREF],[data-w-e-type=STDNAME],[data-w-e-type=CON],[data-w-e-type=CONNBR],[data-w-e-type=CONNAME],[data-w-e-type=TED],[data-w-e-type=TOOLNBR],[data-w-e-type=TOOLNAME],[data-w-e-type=EXPD],[data-w-e-type=HTML-PLACEHOLDER],[data-w-e-type=SBEFF],[data-w-e-type=SELECTION-LBL-CN],[data-w-e-type=SELECTION-LBL-EN]{text-indent:var(--no-indent)!important}[data-w-e-type=PARA]>*,[data-w-e-type=PARAC]>*,[data-w-e-type=TITLE]>*,[data-w-e-type=TITLEC]>*,[data-w-e-type=HTML-TITLE]>*{text-indent:var(--no-indent)!important}[data-w-e-type=CON]{display:inline-flex!important;align-items:baseline;flex-wrap:wrap;margin:0 5px!important;gap:0}[data-w-e-type=CONNAME]{order:1;text-indent:0!important;display:inline!important}[data-w-e-type=CONNBR]{order:2;color:#00f;display:inline!important;white-space:nowrap!important}[data-w-e-type=CONNBR] span{text-decoration:underline}[data-w-e-type=CON] [data-w-e-type=CONNBR]:before{content:" (Material Ref. "!important;white-space:nowrap!important}[data-w-e-type=CON] [data-w-e-type=CONNBR]:after{content:")"!important;white-space:nowrap!important}[data-w-e-type=CBLST]{display:block!important;width:100%;margin:10px 0;text-indent:0!important}[data-w-e-type=CBSUBLST]{display:block!important;width:100%;margin-bottom:0;border:1px solid black;border-top:none;border-collapse:collapse;text-indent:0!important;position:relative;padding-top:0}[data-w-e-type=CBSUBLST] *{position:static!important}[data-w-e-type=CBLST]>[data-w-e-type=CBSUBLST]:first-of-type{border-top:1px solid black}[data-w-e-type=CBLST]>[data-w-e-type=CBSUBLST]:first-of-type:before{content:"";display:block;width:100%;height:42px;background-color:gray;border-bottom:2px solid black;box-sizing:border-box;background-image:linear-gradient(to right,#000,#000),linear-gradient(to right,#000,#000),linear-gradient(to right,#000,#000);background-size:1px 42px,1px 42px,1px 42px;background-position:15% 0,50% 0,75% 0;background-repeat:no-repeat}[data-w-e-type=CBLST]>[data-w-e-type=CBSUBLST]:not(:first-of-type):before{display:none!important}[data-w-e-type=CBLST]>[data-w-e-type=CBSUBLST]:first-of-type>[data-w-e-type=CBDATA]:first-of-type>[data-w-e-type=PAN]:before{content:"面板 \aPANEL";position:absolute;top:0;left:0;width:15%;height:42px;display:flex;align-items:center;justify-content:center;font-weight:700;text-align:center;white-space:pre-line;line-height:1.2;font-size:11px;box-sizing:border-box;z-index:10;pointer-events:none}[data-w-e-type=CBLST]>[data-w-e-type=CBSUBLST]:first-of-type>[data-w-e-type=CBDATA]:first-of-type>[data-w-e-type=CBNAME]:before{content:"说明 \a DESIGNATION";position:absolute;top:0;left:15%;width:35%;height:42px;display:flex;align-items:center;justify-content:center;font-weight:700;text-align:center;white-space:pre-line;line-height:1.2;font-size:11px;box-sizing:border-box;z-index:10;pointer-events:none}[data-w-e-type=CBLST]>[data-w-e-type=CBSUBLST]:first-of-type>[data-w-e-type=CBDATA]:first-of-type>[data-w-e-type=CB]:before{content:"功能识别号 \a FIN";position:absolute;top:0;left:50%;width:25%;height:42px;display:flex;align-items:center;justify-content:center;font-weight:700;text-align:center;white-space:pre-line;line-height:1.2;font-size:11px;box-sizing:border-box;z-index:10;pointer-events:none}[data-w-e-type=CBLST]>[data-w-e-type=CBSUBLST]:first-of-type>[data-w-e-type=CBDATA]:first-of-type>[data-w-e-type=CB]:after{content:"位置 \aLOCATION";position:absolute;top:0;left:75%;width:25%;height:42px;display:flex;align-items:center;justify-content:center;font-weight:700;text-align:center;white-space:pre-line;line-height:1.2;font-size:11px;box-sizing:border-box;z-index:10;pointer-events:none}[data-w-e-type=CBSUBLST]>[data-w-e-type=EIN]{display:inline!important;text-indent:0!important;color:#00f}[data-w-e-type=CBSUBLST]>[data-w-e-type=EIN]:before{content:"FOR FIN "}[data-w-e-type=CBSUBLST]>[data-w-e-type=EQUNAME]{display:inline!important;text-indent:0!important}[data-w-e-type=CBSUBLST]>[data-w-e-type=EQUNAME]:before{content:" ("}[data-w-e-type=CBSUBLST]>[data-w-e-type=EQUNAME]:after{content:")"}[data-w-e-type=CBSUBLST]:has(>[data-w-e-type=EIN])>[data-w-e-type=CBDATA]:first-of-type{border-top:1px solid black}[data-w-e-type=CBSUBLST]:has(>[data-w-e-type=EQUNAME])>[data-w-e-type=CBDATA]:first-of-type{border-top:1px solid black}[data-w-e-type=CBSUBLST]>[data-w-e-type=EFFECT],[data-w-e-type=CBSUBLST]>[data-w-e-type=CONEFFECT]{display:block!important;background-color:#ffebee;padding:8px 12px;border-bottom:1px solid black;font-weight:700;color:red;text-indent:0!important}[data-w-e-type=CBSUBLST]>[data-w-e-type=EFFECT][EFFRG="001999"]:before,[data-w-e-type=CBSUBLST]>[data-w-e-type=CONEFFECT][EFFRG="001999"]:before,[data-w-e-type=CBSUBLST]>[data-w-e-type=EFFECT][EFFRG="001-999"]:before,[data-w-e-type=CBSUBLST]>[data-w-e-type=CONEFFECT][EFFRG="001-999"]:before{content:"** ON A/C: ALL";color:red;font-weight:700}[data-w-e-type=CBSUBLST]>[data-w-e-type=EFFECT]:not([EFFRG="001999"]):not([EFFRG="001-999"]):before,[data-w-e-type=CBSUBLST]>[data-w-e-type=CONEFFECT]:not([EFFRG="001999"]):not([EFFRG="001-999"]):before{content:"** ON A/C: " attr(effrg);color:red;font-weight:700}[data-w-e-type=CBSUBLST]>[data-w-e-type=EFFECT] [data-slate-leaf=true],[data-w-e-type=CBSUBLST]>[data-w-e-type=CONEFFECT] [data-slate-leaf=true]{position:absolute!important;top:0!important;left:0!important;width:1px!important;height:1px!important;opacity:0!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;pointer-events:none!important;-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}[data-w-e-type=CBDATA]{display:grid!important;grid-template-columns:15% 35% 25% 25%;grid-template-areas:"pan designation fin location";border-bottom:1px solid black;text-indent:0!important;min-height:40px}[data-w-e-type=CBDATA]:last-child{border-bottom:none}[data-w-e-type=CBDATA]:has(>[data-w-e-type=EFFECT]),[data-w-e-type=CBDATA]:has(>[data-w-e-type=CONEFFECT]){grid-template-areas:"effect effect effect effect" "pan designation fin location"}[data-w-e-type=CBDATA]>[data-w-e-type=EFFECT],[data-w-e-type=CBDATA]>[data-w-e-type=CONEFFECT]{grid-area:effect;display:block!important;padding:6px 12px;border-bottom:1px solid black;font-weight:700;text-indent:0!important}[data-w-e-type=CBDATA]>[data-w-e-type=EFFECT][EFFRG="001999"]:before,[data-w-e-type=CBDATA]>[data-w-e-type=CONEFFECT][EFFRG="001999"]:before,[data-w-e-type=CBDATA]>[data-w-e-type=EFFECT][EFFRG="001-999"]:before,[data-w-e-type=CBDATA]>[data-w-e-type=CONEFFECT][EFFRG="001-999"]:before{content:"** ON A/C: ALL";color:red;font-weight:700}[data-w-e-type=CBDATA]>[data-w-e-type=EFFECT]:not([EFFRG="001999"]):not([EFFRG="001-999"]):before,[data-w-e-type=CBDATA]>[data-w-e-type=CONEFFECT]:not([EFFRG="001999"]):not([EFFRG="001-999"]):before{content:"** ON A/C: " attr(effrg);color:red;font-weight:700}[data-w-e-type=CBDATA]>[data-w-e-type=EFFECT] [data-slate-leaf=true],[data-w-e-type=CBDATA]>[data-w-e-type=CONEFFECT] [data-slate-leaf=true]{position:absolute!important;top:0!important;left:0!important;width:1px!important;height:1px!important;opacity:0!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;pointer-events:none!important;-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}[data-w-e-type=CBDATA]>[data-w-e-type=PAN]{grid-area:pan;padding:8px 12px;border-right:1px solid black;text-align:center;display:flex!important;align-items:center;justify-content:center;text-indent:0!important}[data-w-e-type=CBDATA]>[data-w-e-type=CB]{grid-area:fin;padding:8px 12px;border-right:1px solid black;text-align:center;display:flex!important;align-items:center;justify-content:center;text-indent:0!important}[data-w-e-type=CBDATA]>[data-w-e-type=CBNAME]{grid-area:designation;padding:8px 12px;border-right:1px solid black;display:flex!important;align-items:center;text-indent:0!important}[data-w-e-type=CBDATA]>[data-w-e-type=CBLOC]{grid-area:location;padding:8px 12px;text-align:center;display:flex!important;align-items:center;justify-content:center;text-indent:0!important}.env-xm [data-w-e-type=REFBLOCK]:has([data-w-e-type=REFINT])>[data-w-e-type=HTML-PLACEHOLDER]:first-of-type{display:none!important}[data-w-e-type=TOR]{display:inline!important;text-indent:0!important}[data-w-e-type=TORVALUE]{display:inline!important}[data-w-e-type=TORVALUE]:first-child>[data-torvalue-display][data-is-mdan=true]:before{content:" " attr(data-content);color:#923385}[data-w-e-type=TORVALUE]:not(:first-child)>[data-torvalue-display][data-is-mdan=true]:before{content:" (" attr(data-content) ")";color:#923385}[data-w-e-type=TORVALUE]:first-child>[data-torvalue-display][data-is-mdan=false][data-has-max=true]:before,[data-w-e-type=PARA] [data-w-e-type=TORVALUE]:first-child>[data-torvalue-display][data-is-mdan=false][data-has-max=true]:before{content:" to between " attr(data-content);color:#923385}[data-w-e-type=TORVALUE]:first-child>[data-torvalue-display][data-is-mdan=false][data-has-max=false]:before,[data-w-e-type=PARA] [data-w-e-type=TORVALUE]:first-child>[data-torvalue-display][data-is-mdan=false][data-has-max=false]:before{content:" to " attr(data-content);color:#923385}[data-w-e-type=TORVALUE]:not(:first-child)>[data-torvalue-display][data-is-mdan=false]:before,[data-w-e-type=PARA] [data-w-e-type=TORVALUE]:not(:first-child)>[data-torvalue-display][data-is-mdan=false]:before{content:" (" attr(data-content) ")";color:#923385}[data-w-e-type=PARAC] [data-w-e-type=TORVALUE]:first-child>[data-torvalue-display][data-is-mdan=true]:before{content:" " attr(data-content-parac);color:#923385}[data-w-e-type=PARAC] [data-w-e-type=TORVALUE]:not(:first-child)>[data-torvalue-display][data-is-mdan=true]:before{content:" (" attr(data-content-parac) ")";color:#923385}[data-w-e-type=PARAC] [data-w-e-type=TORVALUE]:first-child>[data-torvalue-display][data-is-mdan=false][data-has-max=true]:before{content:" " attr(data-content-parac);color:#923385}[data-w-e-type=PARAC] [data-w-e-type=TORVALUE]:first-child>[data-torvalue-display][data-is-mdan=false][data-has-max=false]:before{content:" " attr(data-content-parac);color:#923385}[data-w-e-type=PARAC] [data-w-e-type=TORVALUE]:not(:first-child)>[data-torvalue-display][data-is-mdan=false]:before{content:" (" attr(data-content-parac) ")";color:#923385}[data-w-e-type=EFFBLOCK]{text-decoration:underline;display:inline!important}[data-w-e-type=EFFBLOCK]:after{content:" (" attr(EFFDESC) ") ";color:red;text-decoration:none;display:inline-block}[data-w-e-type=IF-HL]{color:red;display:inline!important}[data-w-e-type=IF-BOLD]{font-weight:700;display:inline!important}[data-w-e-type=TOPIC-GROUP]{display:block!important;background-color:#ccc;padding:4px 0;text-align:center;font-weight:700;width:100%}[data-w-e-type=ATANBR],[data-w-e-type=MFR],[data-w-e-type=SBNBR],[data-w-e-type=STDNAME]{display:inline!important;text-indent:0!important}[data-w-e-type=TED]{display:inline!important;text-indent:0!important;margin:0 4px!important}[data-w-e-type=TED]:before{content:" ";white-space:pre}[data-w-e-type=TED] [data-w-e-type=TOOLNAME],[data-w-e-type=TED] [data-w-e-type=TOOLNBR]{display:inline!important;text-indent:0!important}[data-w-e-type=TED] [data-w-e-type=TOOLNBR]:before{content:" ("}[data-w-e-type=TED] [data-w-e-type=TOOLNBR]:after{content:") "}[data-w-e-type=EXPD]{display:inline!important;text-indent:0!important}[data-w-e-type=EXPD] [data-w-e-type=ITEMNBR]:before{content:" ("}[data-w-e-type=EXPD] [data-w-e-type=ITEMNBR]:after{content:")"}[data-page-break=true]{position:relative}[data-page-break=true]:after{content:"━━━━━━━━━━━━━━━━━━━━━━━━ 📄 分页符 ━━━━━━━━━━━━━━━━━━━━━━━━";display:block;text-align:center;color:#06c;font-size:11px;font-weight:500;margin:8px 0;padding:4px 10px;border-top:2px dashed #4d9fff;border-bottom:2px dashed #4d9fff;background:linear-gradient(to bottom,#e6f2ff,#f0f7ff);-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:none;border-radius:2px}@media print{[data-page-break=true]:after{content:none!important;display:none!important;margin:0!important;padding:0!important;border:none!important;background:none!important}}.preview-root [data-page-break=true]:after,.preview-mode [data-page-break=true]:after{content:""!important;display:none!important;height:0;margin:0;padding:0;border:none;background:none}.hover\:border-red-300:hover{--tw-border-opacity: 1;border-color:rgb(252 165 165 / var(--tw-border-opacity))}.hover\:bg-red-100:hover{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity))}.group:hover .group-hover\:opacity-100{opacity:1}
.n-card .modal-content-class{padding-bottom:0;margin-bottom:var(--n-padding-bottom);max-height:75vh;overflow-y:auto}.virtual-tree-container[data-v-cb507100]{position:relative;overflow-y:auto;overflow-x:hidden;padding-bottom:30vh}.virtual-tree-content[data-v-cb507100]{position:relative;width:100%}.virtual-tree-list[data-v-cb507100]{position:absolute;top:0;left:0;right:0;will-change:transform}.tree-node[data-v-cb507100]{-webkit-user-select:none;-moz-user-select:none;user-select:none}.tree-node-content[data-v-cb507100]{display:flex;align-items:center;height:32px;cursor:pointer;border-radius:4px;transition:background-color .2s;position:relative}.tree-node-content[data-v-cb507100]:hover{background-color:#0000000a}.tree-node-selected>.tree-node-content[data-v-cb507100]{background-color:#4098fc33}.tree-node-context-menu>.tree-node-content[data-v-cb507100]{background-color:#0000000a}.tree-node-switcher[data-v-cb507100]{display:flex;align-items:center;justify-content:center;width:24px;height:24px;margin-right:0;cursor:pointer;border-radius:4px;transition:background-color .2s}.tree-node-switcher[data-v-cb507100]:hover{background-color:#0000000f}.tree-switcher-icon[data-v-cb507100]{color:#1890ff}.tree-node-indent[data-v-cb507100]{width:24px;margin-right:0}.tree-node-icon[data-v-cb507100]{display:inline-flex;align-items:center;margin-right:6px;flex-shrink:0;color:#606266}.tree-node[data-page-break=true] .tree-node-label[data-v-cb507100]{color:#1890ff!important;font-weight:600}.tree-node-title[data-v-cb507100]{flex:1;display:flex;align-items:center;min-width:0;font-size:12px;line-height:1.4}.tree-node-label[data-v-cb507100]{color:#000000d9;margin-right:4px;font-weight:500;word-break:keep-all}.tree-node-selected>.tree-node-content .tree-node-label[data-v-cb507100]{color:var(--info-color-suppl)}.tree-node-text[data-v-cb507100]{color:#00000073;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px}.tree-node-selected>.tree-node-content .tree-node-text[data-v-cb507100]{color:var(--info-color-suppl)}.tree-node-special[data-v-cb507100]{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px}.empty-state[data-v-cb507100]{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:flex;flex-direction:column;align-items:center;padding:40px 20px;text-align:center;color:#999}.empty-icon[data-v-cb507100]{font-size:48px;margin-bottom:16px;opacity:.6}.empty-text[data-v-cb507100]{font-size:14px;line-height:1.5}.tree-lines-layer[data-v-cb507100]{position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:0}.tree-vertical-line[data-v-cb507100]{position:absolute;width:1px;border-left:1px dashed rgba(0,0,0,.15)}.tree-node[data-v-cb507100]{position:relative;z-index:1}.tree-node-content[data-v-cb507100]:after{content:"";position:absolute;top:16px;height:1px;width:12px;border-top:1px dashed rgba(0,0,0,.15);pointer-events:none;left:calc(var(--tree-level, 0) * 20px + 8px)}.tree-node-content[style*="--tree-level: 0"][data-v-cb507100]:after{display:none}.tree-node-label[data-v-cb507100] mark,.tree-node-text[data-v-cb507100] mark,.tree-node-special[data-v-cb507100] mark{background-color:#ffd666;color:#000;padding:0 2px;border-radius:2px;font-weight:600}.tree-node-selected .tree-node-label[data-v-cb507100] mark,.tree-node-selected .tree-node-text[data-v-cb507100] mark,.tree-node-selected .tree-node-special[data-v-cb507100] mark{background-color:#ffa940;color:#fff}.tree-node-content[data-v-cb507100]{cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none}.tree-node-content[data-v-cb507100]:active{cursor:grabbing}.tree-node-content>*[data-v-cb507100]{-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.drag-over-before[data-v-cb507100]{border-top:2px solid #18a058}.drag-over-after[data-v-cb507100]{border-bottom:2px solid #18a058}.drag-over-inner[data-v-cb507100]{background-color:#18a0581a!important;border:1px dashed #18a058}.drag-over-forbid[data-v-cb507100]{background-color:#d030501a!important;border:1px dashed #d03050;cursor:not-allowed}.tree-node-multi-selected>.tree-node-content[data-v-cb507100]{background-color:#18a0581f;box-shadow:inset 0 0 0 1px #18a05866;border-radius:4px}.tree-node-selected.tree-node-multi-selected>.tree-node-content[data-v-cb507100]{background-color:#4098fc26;box-shadow:inset 0 0 0 1.5px #18a058}.tree-node-multi-selected>.tree-node-content .tree-node-label[data-v-cb507100]{color:#18a058;font-weight:600}.tree-header-area[data-v-cb507100]{padding:8px 10px 6px;flex-shrink:0;background:#fff;border-bottom:1px solid #f0f0f0;margin-bottom:4px;z-index:10}.tree-search-row[data-v-cb507100]{display:flex;align-items:center;gap:6px}.tree-search-row .n-input[data-v-cb507100]{flex:1;min-width:0}.ctrl-hint[data-v-cb507100]{display:flex;align-items:center;gap:2px;font-size:10px;color:#bbb;white-space:nowrap;flex-shrink:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.batch-toolbar[data-v-cb507100]{display:flex;align-items:center;justify-content:space-between;padding:8px 2px 2px;font-size:12px;overflow:hidden}.batch-toolbar-info[data-v-cb507100]{display:flex;align-items:center;gap:4px;color:#18a058;font-weight:500;font-size:12px}.batch-toolbar-info b[data-v-cb507100]{font-size:13px}.batch-toolbar-actions[data-v-cb507100]{display:flex;align-items:center;gap:4px;color:#999;font-size:11px}.batch-clear-link[data-v-cb507100]{cursor:pointer;color:#999;transition:color .15s;padding:2px 5px;border-radius:4px}.batch-clear-link[data-v-cb507100]:hover{color:#555;background:#0000000d}.batch-divider[data-v-cb507100]{color:#eee;font-size:11px;margin:0 2px}.batch-delete-link[data-v-cb507100]{display:inline-flex;align-items:center;gap:3px;cursor:pointer;color:#d03050;font-weight:600;padding:2px 6px;border-radius:4px;transition:all .15s;background:#d030500d}.batch-delete-link[data-v-cb507100]:hover{background:#d030501f;color:#c02040}.batch-toolbar-enter-active[data-v-cb507100],.batch-toolbar-leave-active[data-v-cb507100]{transition:all .2s ease;max-height:40px;opacity:1}.batch-toolbar-enter-from[data-v-cb507100],.batch-toolbar-leave-to[data-v-cb507100]{max-height:0;opacity:0;padding-top:0;padding-bottom:0}.kbd-key[data-v-cb507100]{display:inline-block;padding:0 4px;font-size:10px;font-family:monospace;background:#f0f0f0;border:1px solid #ccc;border-bottom-width:2px;border-radius:3px;color:#555;line-height:1.5;font-style:normal}:root,:host{--w-e-textarea-bg-color: #fff;--w-e-textarea-color: #333;--w-e-textarea-border-color: #ccc;--w-e-textarea-slight-border-color: #e8e8e8;--w-e-textarea-slight-color: #d4d4d4;--w-e-textarea-slight-bg-color: #f5f2f0;--w-e-textarea-selected-border-color: #B4D5FF;--w-e-textarea-handler-bg-color: #4290f7;--w-e-toolbar-color: #595959;--w-e-toolbar-bg-color: #fff;--w-e-toolbar-active-color: #333;--w-e-toolbar-active-bg-color: #f1f1f1;--w-e-toolbar-disabled-color: #999;--w-e-toolbar-border-color: #e8e8e8;--w-e-modal-button-bg-color: #fafafa;--w-e-modal-button-border-color: #d9d9d9}.w-e-text-container *,.w-e-toolbar *{box-sizing:border-box;margin:0;outline:none;padding:0}.w-e-text-container blockquote,.w-e-text-container li,.w-e-text-container p,.w-e-text-container td,.w-e-text-container th,.w-e-toolbar *{line-height:1.5}.w-e-text-container{background-color:var(--w-e-textarea-bg-color);color:var(--w-e-textarea-color);height:100%;position:relative}.w-e-text-container .w-e-scroll{-webkit-overflow-scrolling:touch;height:100%}.w-e-text-container [data-slate-editor]{word-wrap:break-word;border-top:1px solid transparent;min-height:100%;outline:0;padding:0 10px;white-space:pre-wrap}.w-e-text-container [data-slate-editor] p{margin:15px 0}.w-e-text-container [data-slate-editor] h1,.w-e-text-container [data-slate-editor] h2,.w-e-text-container [data-slate-editor] h3,.w-e-text-container [data-slate-editor] h4,.w-e-text-container [data-slate-editor] h5{margin:20px 0}.w-e-text-container [data-slate-editor] img{cursor:default;display:inline!important;max-width:100%;min-height:20px;min-width:20px}.w-e-text-container [data-slate-editor] span{text-indent:0}.w-e-text-container [data-slate-editor] [data-selected=true]{box-shadow:0 0 0 2px var(--w-e-textarea-selected-border-color)}.w-e-text-placeholder{font-style:italic;left:10px;top:17px;width:90%}.w-e-max-length-info,.w-e-text-placeholder{color:var(--w-e-textarea-slight-color);pointer-events:none;position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none}.w-e-max-length-info{bottom:.5em;right:1em}.w-e-bar{background-color:var(--w-e-toolbar-bg-color);color:var(--w-e-toolbar-color);font-size:14px;padding:0 5px}.w-e-bar svg{fill:var(--w-e-toolbar-color);height:14px;width:14px}.w-e-bar-show{display:flex}.w-e-bar-hidden{display:none}.w-e-hover-bar{border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 5px #0000001f;position:absolute}.w-e-toolbar{flex-wrap:wrap;position:relative}.w-e-bar-divider{background-color:var(--w-e-toolbar-border-color);display:inline-flex;height:40px;margin:0 5px;width:1px}.w-e-bar-item{display:flex;height:40px;padding:4px;position:relative;text-align:center}.w-e-bar-item,.w-e-bar-item button{align-items:center;justify-content:center}.w-e-bar-item button{background:transparent;border:none;color:var(--w-e-toolbar-color);cursor:pointer;display:inline-flex;height:32px;overflow:hidden;padding:0 8px;white-space:nowrap}.w-e-bar-item button:hover{background-color:var(--w-e-toolbar-active-bg-color);color:var(--w-e-toolbar-active-color)}.w-e-bar-item button .title{margin-left:5px}.w-e-bar-item .active{background-color:var(--w-e-toolbar-active-bg-color);color:var(--w-e-toolbar-active-color)}.w-e-bar-item .disabled{color:var(--w-e-toolbar-disabled-color);cursor:not-allowed}.w-e-bar-item .disabled svg{fill:var(--w-e-toolbar-disabled-color)}.w-e-bar-item .disabled:hover{background-color:var(--w-e-toolbar-bg-color);color:var(--w-e-toolbar-disabled-color)}.w-e-bar-item .disabled:hover svg{fill:var(--w-e-toolbar-disabled-color)}.w-e-menu-tooltip-v5:before{background-color:var(--w-e-toolbar-active-color);border-radius:5px;color:var(--w-e-toolbar-bg-color);content:attr(data-tooltip);font-size:.75em;opacity:0;padding:5px 10px;position:absolute;text-align:center;top:40px;transition:opacity .6s;visibility:hidden;white-space:pre;z-index:1}.w-e-menu-tooltip-v5:after{border:5px solid transparent;border-bottom:5px solid var(--w-e-toolbar-active-color);content:"";opacity:0;position:absolute;top:30px;transition:opacity .6s;visibility:hidden}.w-e-menu-tooltip-v5:hover:after,.w-e-menu-tooltip-v5:hover:before{opacity:1;visibility:visible}.w-e-menu-tooltip-v5.tooltip-right:before{left:100%;top:10px}.w-e-menu-tooltip-v5.tooltip-right:after{border-bottom-color:transparent;border-left-color:transparent;border-right-color:var(--w-e-toolbar-active-color);border-top-color:transparent;left:100%;margin-left:-10px;top:16px}.w-e-bar-item-group .w-e-bar-item-menus-container{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;display:none;left:0;margin-top:40px;position:absolute;top:0;z-index:1}.w-e-bar-item-group:hover .w-e-bar-item-menus-container{display:block}.w-e-select-list{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;left:0;margin-top:40px;max-height:350px;min-width:100px;overflow-y:auto;position:absolute;top:0;z-index:1}.w-e-select-list ul{line-height:1;list-style:none}.w-e-select-list ul .selected{background-color:var(--w-e-toolbar-active-bg-color)}.w-e-select-list ul li{cursor:pointer;padding:7px 0 7px 25px;position:relative;text-align:left;white-space:nowrap}.w-e-select-list ul li:hover{background-color:var(--w-e-toolbar-active-bg-color)}.w-e-select-list ul li svg{left:0;margin-left:5px;margin-top:-7px;position:absolute;top:50%}.w-e-bar-bottom .w-e-select-list{bottom:0;margin-bottom:40px;margin-top:0;top:inherit}.w-e-drop-panel{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;margin-top:40px;min-width:200px;padding:10px;position:absolute;top:0;z-index:1}.w-e-bar-bottom .w-e-drop-panel{bottom:0;margin-bottom:40px;margin-top:0;top:inherit}.w-e-modal{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;color:var(--w-e-toolbar-color);font-size:14px;min-height:40px;min-width:100px;padding:20px 15px 0;position:absolute;text-align:left;z-index:1}.w-e-modal .btn-close{cursor:pointer;line-height:1;padding:5px;position:absolute;right:8px;top:7px}.w-e-modal .btn-close svg{fill:var(--w-e-toolbar-color);height:10px;width:10px}.w-e-modal .babel-container{display:block;margin-bottom:15px}.w-e-modal .babel-container span{display:block;margin-bottom:10px}.w-e-modal .button-container{margin-bottom:15px}.w-e-modal button{background-color:var(--w-e-modal-button-bg-color);border:1px solid var(--w-e-modal-button-border-color);border-radius:4px;color:var(--w-e-toolbar-color);cursor:pointer;font-weight:400;height:32px;padding:4.5px 15px;text-align:center;touch-action:manipulation;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.w-e-modal input[type=number],.w-e-modal input[type=text],.w-e-modal textarea{font-feature-settings:"tnum";background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-modal-button-border-color);border-radius:4px;color:var(--w-e-toolbar-color);font-variant:tabular-nums;padding:4.5px 11px;transition:all .3s;width:100%}.w-e-modal textarea{min-height:60px}body .w-e-modal,body .w-e-modal *{box-sizing:border-box}.w-e-progress-bar{background-color:var(--w-e-textarea-handler-bg-color);height:1px;position:absolute;transition:width .3s;width:0}.w-e-full-screen-container{bottom:0!important;display:flex!important;flex-direction:column!important;height:100%!important;left:0!important;margin:0!important;padding:0!important;position:fixed;right:0!important;top:0!important;width:100%!important}.w-e-full-screen-container [data-w-e-textarea=true]{flex:1!important}.w-e-text-container [data-slate-editor] code{background-color:var(--w-e-textarea-slight-bg-color);border-radius:3px;font-family:monospace;padding:3px}.w-e-panel-content-color{list-style:none;text-align:left;width:230px}.w-e-panel-content-color li{border:1px solid var(--w-e-toolbar-bg-color);border-radius:3px;cursor:pointer;display:inline-block;padding:2px}.w-e-panel-content-color li:hover{border-color:var(--w-e-toolbar-color)}.w-e-panel-content-color li .color-block{border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;height:17px;width:17px}.w-e-panel-content-color .active{border-color:var(--w-e-toolbar-color)}.w-e-panel-content-color .clear{line-height:1.5;margin-bottom:5px;width:100%}.w-e-panel-content-color .clear svg{height:16px;margin-bottom:-4px;width:16px}.w-e-text-container [data-slate-editor] blockquote{background-color:var(--w-e-textarea-slight-bg-color);border-left:8px solid var(--w-e-textarea-selected-border-color);display:block;font-size:100%;line-height:1.5;margin:10px 0;padding:10px}.w-e-panel-content-emotion{font-size:20px;list-style:none;text-align:left;width:300px}.w-e-panel-content-emotion li{border-radius:3px;cursor:pointer;display:inline-block;padding:0 5px}.w-e-panel-content-emotion li:hover{background-color:var(--w-e-textarea-slight-bg-color)}.w-e-textarea-divider{border-radius:3px;margin:20px auto;padding:20px}.w-e-textarea-divider hr{background-color:var(--w-e-textarea-border-color);border:0;display:block;height:1px}.w-e-text-container [data-slate-editor] pre>code{background-color:var(--w-e-textarea-slight-bg-color);border:1px solid var(--w-e-textarea-slight-border-color);border-radius:4px;display:block;font-size:14px;padding:10px;text-indent:0}.w-e-text-container [data-slate-editor] .w-e-image-container{display:inline-block;margin:0 3px}.w-e-text-container [data-slate-editor] .w-e-image-container:hover{box-shadow:0 0 0 2px var(--w-e-textarea-selected-border-color)}.w-e-text-container [data-slate-editor] .w-e-selected-image-container{overflow:hidden;position:relative}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .w-e-image-dragger{background-color:var(--w-e-textarea-handler-bg-color);height:7px;position:absolute;width:7px}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .left-top{cursor:nwse-resize;left:0;top:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .right-top{cursor:nesw-resize;right:0;top:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .left-bottom{bottom:0;cursor:nesw-resize;left:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .right-bottom{bottom:0;cursor:nwse-resize;right:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container:hover,.w-e-text-container [contenteditable=false] .w-e-image-container:hover{box-shadow:none}.w-e-text-container [data-slate-editor] .table-container{border:1px dashed var(--w-e-textarea-border-color);border-radius:5px;margin-top:10px;overflow-x:auto;padding:10px;width:100%}.w-e-text-container [data-slate-editor] table{border-collapse:collapse}.w-e-text-container [data-slate-editor] table td,.w-e-text-container [data-slate-editor] table th{border:1px solid var(--w-e-textarea-border-color);line-height:1.5;min-width:30px;padding:3px 5px;text-align:left}.w-e-text-container [data-slate-editor] table th{background-color:var(--w-e-textarea-slight-bg-color);font-weight:700;text-align:center}.w-e-panel-content-table{background-color:var(--w-e-toolbar-bg-color)}.w-e-panel-content-table table{border-collapse:collapse}.w-e-panel-content-table td{border:1px solid var(--w-e-toolbar-border-color);cursor:pointer;height:15px;padding:3px 5px;width:20px}.w-e-panel-content-table td.active{background-color:var(--w-e-toolbar-active-bg-color)}.w-e-textarea-video-container{background-image:linear-gradient(45deg,#eee 25%,transparent 0,transparent 75%,#eee 0,#eee),linear-gradient(45deg,#eee 25%,#fff 0,#fff 75%,#eee 0,#eee);background-position:0 0,10px 10px;background-size:20px 20px;border:1px dashed var(--w-e-textarea-border-color);border-radius:5px;margin:10px auto 0;padding:10px 0;text-align:center}.w-e-text-container [data-slate-editor] pre>code{word-wrap:normal;font-family:Consolas,Monaco,Andale Mono,Ubuntu Mono,monospace;-webkit-hyphens:none;hyphens:none;line-height:1.5;margin:.5em 0;overflow:auto;padding:1em;-moz-tab-size:4;-o-tab-size:4;tab-size:4;text-align:left;text-shadow:0 1px #fff;white-space:pre;word-break:normal;word-spacing:normal}.w-e-text-container [data-slate-editor] pre>code .token.cdata,.w-e-text-container [data-slate-editor] pre>code .token.comment,.w-e-text-container [data-slate-editor] pre>code .token.doctype,.w-e-text-container [data-slate-editor] pre>code .token.prolog{color:#708090}.w-e-text-container [data-slate-editor] pre>code .token.punctuation{color:#999}.w-e-text-container [data-slate-editor] pre>code .token.namespace{opacity:.7}.w-e-text-container [data-slate-editor] pre>code .token.boolean,.w-e-text-container [data-slate-editor] pre>code .token.constant,.w-e-text-container [data-slate-editor] pre>code .token.deleted,.w-e-text-container [data-slate-editor] pre>code .token.number,.w-e-text-container [data-slate-editor] pre>code .token.property,.w-e-text-container [data-slate-editor] pre>code .token.symbol,.w-e-text-container [data-slate-editor] pre>code .token.tag{color:#905}.w-e-text-container [data-slate-editor] pre>code .token.attr-name,.w-e-text-container [data-slate-editor] pre>code .token.builtin,.w-e-text-container [data-slate-editor] pre>code .token.char,.w-e-text-container [data-slate-editor] pre>code .token.inserted,.w-e-text-container [data-slate-editor] pre>code .token.selector,.w-e-text-container [data-slate-editor] pre>code .token.string{color:#690}.w-e-text-container [data-slate-editor] pre>code .language-css .token.string,.w-e-text-container [data-slate-editor] pre>code .style .token.string,.w-e-text-container [data-slate-editor] pre>code .token.entity,.w-e-text-container [data-slate-editor] pre>code .token.operator,.w-e-text-container [data-slate-editor] pre>code .token.url{color:#9a6e3a}.w-e-text-container [data-slate-editor] pre>code .token.atrule,.w-e-text-container [data-slate-editor] pre>code .token.attr-value,.w-e-text-container [data-slate-editor] pre>code .token.keyword{color:#07a}.w-e-text-container [data-slate-editor] pre>code .token.class-name,.w-e-text-container [data-slate-editor] pre>code .token.function{color:#dd4a68}.w-e-text-container [data-slate-editor] pre>code .token.important,.w-e-text-container [data-slate-editor] pre>code .token.regex,.w-e-text-container [data-slate-editor] pre>code .token.variable{color:#e90}.w-e-text-container [data-slate-editor] pre>code .token.bold,.w-e-text-container [data-slate-editor] pre>code .token.important{font-weight:700}.w-e-text-container [data-slate-editor] pre>code .token.italic{font-style:italic}.w-e-text-container [data-slate-editor] pre>code .token.entity{cursor:help}[data-modify-type=removed]{background-color:var(--error-color-suppl)}[data-modify-type=added]{background-color:var(--success-color-suppl)}[data-modify-type=blank]{display:none!important}[data-modify-type=placeholder]{position:relative;color:var(--progress-rail-color)}[data-modify-type=placeholder]:before{content:"";position:absolute;top:0;left:0;width:100%;height:100%;background-color:var(--progress-rail-color)}[data-modify-type=placeholder] [data-modify-type=placeholder] .modify-arrow-left,[data-modify-type=placeholder] [data-modify-type=placeholder] .modify-arrow-right{display:none}.compareContainer .w-e-text-container [data-slate-editor]{padding:0 30px}.modify-arrow-right,.modify-arrow-left{height:100%;position:absolute;top:50%;transform:translateY(-50%);display:flex;justify-content:center;align-items:center;font-size:16px;cursor:pointer;color:var(--w-e-textarea-color);z-index:100;background-color:var(--border-color)}.modify-arrow-right:before,.modify-arrow-left:before{content:"";position:absolute;top:0;left:0;width:100%;height:100%;border:1px dashed var(--w-e-textarea-color)}.modify-arrow-right{right:-25px}.modify-arrow-left{left:-25px}.choose-row[data-v-ac55e72e]{width:100%;position:absolute;z-index:99;opacity:.6}.view-iframe[data-v-10493071]{width:100%;min-height:70vh;border:none;background:#fff}.rule-preview[data-v-07dbb7ba]{margin-top:12px;padding:12px 16px;background:#1a1a2e;border-radius:6px;font-family:JetBrains Mono,Fira Code,Cascadia Code,Consolas,monospace;font-size:13px;line-height:1.7;white-space:pre-wrap;word-break:break-word;color:#cdd6f4;border:1px solid #313244}[data-v-07dbb7ba] .rule-elem{color:#89b4fa;font-weight:500}[data-v-07dbb7ba] .rule-occ{color:#a6e3a1;font-weight:600}[data-v-07dbb7ba] .rule-pipe,[data-v-07dbb7ba] .rule-punc:not(:empty){color:#f38ba8}[data-v-07dbb7ba] .rule-comma{color:#f5c2e7}[data-v-07dbb7ba] .rule-pcdata{color:#fab387;font-style:italic}[data-v-07dbb7ba] .rule-pcdata-hint{color:#6c7086;font-size:11px;margin-left:4px}.node-tree-popover-wrapper[data-v-7a37e7a9]{position:fixed;z-index:2000;background:#fff;border:1px solid #e0e0e0;border-radius:6px;box-shadow:0 3px 12px #00000026;min-width:320px;max-width:380px;max-height:600px;overflow-y:auto}.node-tree-popover-wrapper .node-tree-popover-card[data-v-7a37e7a9]{display:flex;flex-direction:column;height:100%}.node-tree-popover-wrapper .node-tree-popover-card .node-tree-popover-header[data-v-7a37e7a9]{display:flex;align-items:center;justify-content:space-between;padding:12px;border-bottom:1px solid #e0e0e0;font-weight:500;min-height:44px}.node-tree-popover-wrapper .node-tree-popover-card .popover-header[data-v-7a37e7a9]{display:flex;align-items:center;justify-content:space-between;padding:12px;border-bottom:1px solid #e0e0e0;font-weight:500}.node-tree-popover-body[data-v-7a37e7a9]{display:flex;flex-direction:column;gap:12px;padding:12px;min-width:350px;max-height:500px;overflow-y:auto}.node-tree-popover-body .current-node-info[data-v-7a37e7a9]{padding:10px;background:linear-gradient(135deg,#f5f7fa,#c3cfe2);border-radius:6px;border-left:3px solid #1890ff}.node-tree-popover-body .current-node-info .info-row[data-v-7a37e7a9]{display:flex;gap:8px;margin-bottom:6px;font-size:12px}.node-tree-popover-body .current-node-info .info-row[data-v-7a37e7a9]:last-child{margin-bottom:0}.node-tree-popover-body .current-node-info .info-row .label[data-v-7a37e7a9]{font-weight:600;min-width:70px;color:#333}.node-tree-popover-body .current-node-info .info-row .value[data-v-7a37e7a9]{flex:1;color:#555;word-break:break-all;font-family:Monaco,Menlo,monospace;font-size:11px}.node-tree-popover-body .node-structure-tree .tree-title[data-v-7a37e7a9]{font-size:13px;font-weight:600;color:#333;margin-bottom:8px;padding-bottom:6px;border-bottom:2px solid #1890ff}.node-tree-popover-body .node-structure-tree .tree-content[data-v-7a37e7a9]{display:flex;flex-direction:column;gap:0;max-height:280px;overflow-y:auto;border:1px solid #d9d9d9;border-radius:6px;background-color:#fff}.node-tree-popover-body .node-structure-tree .tree-content .tree-node-item[data-v-7a37e7a9]{display:flex;align-items:center;gap:0;padding:4px 6px;font-size:12px;transition:all .2s;border-bottom:1px solid #f0f0f0;border-left:3px solid #e8e8e8;position:relative}.node-tree-popover-body .node-structure-tree .tree-content .tree-node-item[data-v-7a37e7a9]:last-child{border-bottom:none}.node-tree-popover-body .node-structure-tree .tree-content .tree-node-item[data-v-7a37e7a9]:hover{background-color:#fafafa;border-left-color:#d4d4d4}.node-tree-popover-body .node-structure-tree .tree-content .tree-node-item.is-parent[data-v-7a37e7a9]{background-color:#f5f7fa;border-left-color:#94a3b8;color:#334155;font-weight:500}.node-tree-popover-body .node-structure-tree .tree-content .tree-node-item.is-current[data-v-7a37e7a9]{background-color:#dbeafe;border-left-color:#0284c7;font-weight:500}.node-tree-popover-body .node-structure-tree .tree-content .tree-node-item.is-current.is-parent[data-v-7a37e7a9]{background-color:#dbeafe;border-left-color:#0284c7;color:#262626}.node-tree-popover-body .node-structure-tree .tree-content .tree-node-item .expand-btn[data-v-7a37e7a9]{min-width:16px;width:16px;height:16px;padding:0;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-right:2px}.node-tree-popover-body .node-structure-tree .tree-content .tree-node-item .expand-placeholder[data-v-7a37e7a9]{width:16px;height:16px;flex-shrink:0;margin-right:2px}.node-tree-popover-body .node-structure-tree .tree-content .tree-node-item .node-label[data-v-7a37e7a9]{flex:1;text-align:left;padding:2px 4px;height:auto;display:flex;align-items:center;gap:4px;cursor:pointer;border-radius:3px;transition:all .15s;justify-content:flex-start}.node-tree-popover-body .node-structure-tree .tree-content .tree-node-item .node-label[data-v-7a37e7a9]:hover{background-color:#0ea5e914}.node-tree-popover-body .node-structure-tree .tree-content .tree-node-item .node-label.is-current-node[data-v-7a37e7a9]{font-weight:500;color:#0ea5e9}.node-tree-popover-body .node-structure-tree .tree-content .tree-node-item .node-label.is-selected-node[data-v-7a37e7a9]{font-weight:600;color:#0284c7}.node-tree-popover-body .node-structure-tree .tree-content .tree-node-item .node-label .node-type[data-v-7a37e7a9]{font-weight:600;color:#0284c7;min-width:45px;padding:2px 6px;background-color:#0284c71a;border-radius:3px;font-size:11px;white-space:nowrap;text-align:center;flex-shrink:0}.node-tree-popover-body .node-structure-tree .tree-content .tree-node-item .node-label .node-preview[data-v-7a37e7a9]{color:#64748b;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:80px;font-style:italic}.node-tree-popover-body .node-structure-tree .tree-content .tree-node-item .child-count[data-v-7a37e7a9]{font-size:10px;font-weight:600;color:#fff;background-color:#1890ff;padding:2px 6px;border-radius:10px;min-width:20px;text-align:center;flex-shrink:0}.node-tree-popover-body .operation-buttons[data-v-7a37e7a9]{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px}.node-tree-popover-body .operation-buttons[data-v-7a37e7a9] .n-button{font-size:12px;height:32px}.w-e-text-container .w-e-scroll{padding-bottom:30vh}[data-w-e-type=HTML-TABLE] .chooseEntry{background-color:var(--avatar-color)}[data-w-e-type=HTML-TABLE] [data-w-e-type=HTML-TBODY].bg-warningColorSuppl .chooseEntry{background-color:var(--warning-color)}[data-w-e-type=HTML-TABLE] [data-w-e-type=HTML-THEAD] [data-w-e-type=ENTRY].chooseEntry{background-color:var(--avatar-color)}[data-w-e-type=HTML-TABLE] [data-w-e-type=HTML-THEAD].bg-warningColorSuppl [data-w-e-type=ENTRY]{background-color:var(--warning-color)}[data-w-e-type=HTML-TABLE] [data-w-e-type=HTML-THEAD].bg-warningColorSuppl [data-w-e-type=ENTRY].chooseEntry{background-color:var(--avatar-color)}[data-w-e-type=HTML-TABLE] [data-w-e-type=HTML-TBODY] [data-w-e-type=ENTRY]:not([DISABLED=TRUE]).chooseEntry{background-color:var(--avatar-color)!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ROW].bg-warningColorSuppl [data-w-e-type=ENTRY]{background-color:var(--warning-color)}[data-w-e-type=HTML-TABLE] [data-w-e-type=ROW].bg-warningColorSuppl [data-w-e-type=ENTRY].chooseEntry{background-color:var(--avatar-color)!important}[data-w-e-type=HTML-TABLE] [data-w-e-type=ENTRY].bg-warningColorSuppl{background-color:var(--warning-color)}.highlightColumn{background-color:var(--warning-color)!important}.highlightColumn .bg-warningColorSuppl{background-color:var(--warning-color)}.split-wrapper{position:relative}.left-pane{position:relative;transition:width .3s ease}.sidebar-toggle{position:absolute;top:50%;transform:translate(-50%,-50%);width:34px;height:34px;border-radius:999px;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:50;box-shadow:0 4px 12px #0000001f;background:var(--card-color);border:1px solid var(--border-color);color:var(--primary-color);transition:background-color .2s ease,border-color .2s ease,transform .2s ease}.sidebar-toggle:hover{border-color:var(--primary-color);background-color:#1890ff14}.sidebar-toggle.collapsed{transform:translateY(-50%)}
import{f as a,A as e,h as l,o as t,x as s,n as r,w as o,y as n,B as u,F as i,j as m,C as c,D as p,E as d}from"./index-BGNM-WBG.js";import{_ as h,a as f}from"./DataTable-CEJURamz.js";import{_ as b}from"./Button-ByITIhW2.js";import"./use-rtl-iN3przWb.js";import"./Scrollbar-DmIJvlAu.js";const y=a({__name:"theme",setup(a){const y=e(),j=()=>{y.changeTheme()},k=[{title:"变量名",key:"label"},{title:"变量值",key:"value"},{title:"颜色",key:"color",render:a=>m("div",{style:{width:"30px",height:"30px",backgroundColor:a.value}})},{title:"值",key:"value",render:a=>m("span",{},y.theme?c[a.label]:p[a.label])}],x=l(()=>Object.entries(d.colors).map(([a,e])=>({label:a,value:e})));return(a,e)=>{const l=b,m=h,c=f;return t(),s(i,null,[r(m,null,{default:o(()=>[r(l,{type:"primary",onClick:j},{default:o(()=>[n("主题切换")]),_:1})]),_:1}),r(c,{columns:k,data:u(x),"flex-height":"",class:"h-full"},null,8,["data"])],64)}}});export{y as default};
import{b_ as t,b7 as n,h as r,J as s,bC as e,H as a,a4 as o,bW as u}from"./index-BGNM-WBG.js";function i(i,l,c){if(!l)return;const f=n(),d=r(()=>{const{value:t}=l;if(!t)return;const n=t[i];return n||void 0}),v=s(e,null),b=()=>{o(()=>{const{value:n}=c,r=`${n}${i}Rtl`;if(function(n,r){if(void 0===n)return!1;if(r){const{context:{ids:t}}=r;return t.has(n)}return null!==t(n)}(r,f))return;const{value:s}=d;s&&s.style.mount({id:r,head:!0,anchorMetaName:u,props:{bPrefix:n?`.${n}-`:void 0},ssr:f,parent:null==v?void 0:v.styleMountTarget})})};return f?b():a(b),d}export{i as u};
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1724118561720" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4707" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M922.85749971 610.25000029C845.74999971 509.75 730.62500029 406.54999971 698.89249971 381.42500029 676.25 363.5 654.46250029 354.5 632.69749971 354.5a95.30250029 95.30250029 0 0 0-61.65749971 25.12500029c-3.62999971 3.59250029-9.0675 8.0775-15.4125 13.46249971-45.3375 37.69499971-186.795 166.0275-190.4175 169.61249971a58.94250029 58.94250029 0 0 1-39.89999971 16.155 69.21749971 69.21749971 0 0 1-43.50000058-20.63999971A1055.0475 1055.0475 0 0 1 166.625 448.73c-34.45499971-38.5875-63.4725-72.69000029-63.4725-72.69000029s-6.345 12.56249971-9.97499971 22.4325a17.02500029 17.02500029 0 0 0 0.90749971 14.36250058C113.12749971 439.75999971 199.25 541.16749971 317.15 642.57499971A100.26749971 100.26749971 0 0 0 383.33750029 669.5a95.30250029 95.30250029 0 0 0 61.65749971-25.12500029c3.62999971-3.59250029 9.0675-8.0775 15.4125-13.46249971 45.3375-37.69499971 131.48250029-115.76999971 171.37500029-152.56500029 9.0675-8.0775 15.4125-13.46249971 18.135-16.155a58.94250029 58.94250029 0 0 1 39.89999971-16.155 74.295 74.295 0 0 1 43.49999971 20.64000058c25.38749971 21.53999971 66.195 59.24999971 115.155 109.49999942 35.36250029 37.69499971 63.4725 71.7975 63.4725 72.69000029 0 0 6.345-13.46249971 9.97500058-22.4325a16.12500029 16.12500029 0 0 0 0.93749942-16.18499971z" fill="#8DC21F" p-id="4708"></path><path d="M469.61 235.0475c-39.8025 28.95000029-202.635 180.0225-234.29999971 216.20999971a550.50000029 550.50000029 0 0 0 56.99999971 53.40000058 19.8 19.8 0 0 0 28.95000029-3.61500029c62.42249971-63.32249971 190.88250029-176.40749971 208.06499971-189.97499971 47.9475-40.70999971 104.94-44.325 155.59499971-6.33000058 124.83749971 94.05 236.10750029 241.50750029 255.105 268.65 0 0 2.71500029-13.5675 4.5-25.32749942a28.5975 28.5975 0 0 0-3.61499942-20.80500029c-117.60000029-169.1775-242.42249971-274.11000029-290.37000058-300.34500029-54.27749971-28.04249971-117.60000029-38.0025-180.92999971 8.13750029z" fill="#0084CF" p-id="4709"></path><path d="M337.50500029 147.2975c-80.4825 65.13000029-184.5 161.02500029-210.75000029 189.97499971a706.5 706.5 0 0 0 50.66250029 54.27750058 11.12249971 11.12249971 0 0 0 17.19-1.80750058c79.605-76.89750029 165.54750029-155.60250029 216.2025-194.50499942 84.13499971-65.1375 183.645-71.46749971 280.44 5.42999971a1192.66499971 1192.66499971 0 0 1 114.89249971 104.94 1622.99999971 1622.99999971 0 0 1 141.12 180 206.505 206.505 0 0 0-4.5-37.99500029 57.83249971 57.83249971 0 0 0-5.42999971-14.47499971 857.025 857.025 0 0 0-100.43250029-145.59000029C728.3375 160.865 660.49250029 122.86999971 616.16750029 99.35 557.36 69.49999971 444.28249971 60.4475 337.50500029 147.2975z" fill="#0084CF" p-id="4710"></path><path d="M554.6375 782.36c39.8025-28.95000029 202.64249971-180 234.29999971-216.20999971a615.465 615.465 0 0 0-56.99999971-54.27750058 19.8 19.8 0 0 0-28.95000029 3.61500029c-62.3925 63.32249971-190.85249971 176.40749971-208.04249971 189.97499971-47.9475 40.70999971-104.94 44.325-156.50250029 6.33000058C213.605 617.71249971 103.23500029 470.25500029 84.23749971 442.21249971c0 0-2.71500029 13.5675-4.5 25.32750029a28.5975 28.5975 0 0 0 3.6 20.8125c117.60000029 169.16249971 242.4375 274.11000029 290.385 300.35999971 54.27749971 29.82750029 117.6075 39.75000029 180.91500029-6.35249971z" fill="#0084CF" p-id="4711"></path><path d="M686.75000029 870.10250029c80.51249971-65.1375 184.545-161.02500029 210.74999942-189.97500058a706.5 706.5 0 0 0-50.66249942-54.27749971 11.12249971 11.12249971 0 0 0-17.19 1.80749971C750.05 704.555 664.10749971 783.26 613.4525 822.1625c-84.13499971 65.1375-183.645 71.46749971-280.44-5.42999971a1192.67250029 1192.67250029 0 0 1-114.89249971-104.9625A1622.99999971 1622.99999971 0 0 1 77.00000029 531.77000029a214.43249971 214.43249971 0 0 0 4.5 37.99499942 57.83249971 57.83249971 0 0 0 5.45249971 14.50500029 857.025 857.025 0 0 0 100.4175 145.64999971c108.55500029 126.60000029 176.4 164.61 220.725 188.1 58.80750029 29.88749971 171.88499971 38.93249971 278.65500029-47.91749942z" fill="#0084CF" p-id="4712"></path></svg>
\ No newline at end of file
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Cache-Control" content="no-cache, no-store">
<script type="module" crossorigin src="./assets/index-BGNM-WBG.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CKDo6wGH.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
\ No newline at end of file
armo-xm项目重构-前端
import fs from 'fs';
const xml = fs.readFileSync('e:/refactor-Editor/Ifar-Xml-Editor/src/assets/file/WORK_CARD_20260318095803.xml', 'utf-8');
// Count tags using regex
const matches = xml.match(/<[a-zA-Z0-9_-]+/g);
console.log('Total tags count:', matches ? matches.length : 0);
// Let's count specific tags to see if there is anything huge
const tagCounts = {};
if (matches) {
for (const match of matches) {
const tag = match.slice(1);
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
}
}
console.log('Tag counts:', Object.entries(tagCounts).sort((a, b) => b[1] - a[1]).slice(0, 20));
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -20,7 +20,9 @@ ...@@ -20,7 +20,9 @@
<n-scrollbar :style="{ maxHeight: typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight }"> <n-scrollbar :style="{ maxHeight: typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight }">
<n-spin :show="loading" :style="{ padding: typeof padding === 'number' ? `${padding}px` : padding }"> <n-spin :show="loading" :style="{ padding: typeof padding === 'number' ? `${padding}px` : padding }">
<slot></slot> <div class="space-y-4">
<slot></slot>
</div>
</n-spin> </n-spin>
</n-scrollbar> </n-scrollbar>
...@@ -92,4 +94,20 @@ const handleCancel = () => { ...@@ -92,4 +94,20 @@ const handleCancel = () => {
} }
</script> </script>
<style></style> <style></style>
<style scoped></style> <style scoped>
:deep(.rule-op) {
@apply text-primary font-bold;
}
:deep(.rule-quantifier) {
@apply text-warning;
}
:deep(.rule-group) {
@apply text-color3;
}
:deep(.rule-element) {
@apply text-success font-semibold;
}
:deep(.rule-pcdata) {
@apply text-danger;
}
</style>
<template> <template>
<n-layout has-sider class="h-screen"> <div class="h-screen flex flex-col overflow-hidden" :style="{ backgroundColor: themeVars.bodyColor }">
<n-layout-sider <!-- 主页面视图内容区 -->
collapse-mode="width" <router-view v-slot="{ Component, route }">
:collapsed-width="64" <transition :name="appStore.transitionName || 'none'" mode="out-in">
:width="240" <keep-alive>
:collapsed="collapsed" <component :is="Component" :key="route.fullPath" class="flex-1 flex flex-col min-h-0" />
:native-scrollbar="true" </keep-alive>
@collapse="collapsed = true" </transition>
@expand="collapsed = false" </router-view>
bordered
class="sider-with-trigger" <!-- 全局弹窗组件,供 window 全局挂载调用 -->
> <CommonImportModal ref="globalImportModalRef" />
<div class="sider-inner"> <CommonExportModal ref="globalExportModalRef" />
<!-- Logo 区 --> <CommonUploadModal ref="globalUploadModalRef" />
<div class="h-16 px-4 flex items-center justify-center border-b flex-shrink-0" :style="{ borderColor: themeVars.dividerColor }"> <CommonDownloadModal ref="globalDownloadModalRef" />
<n-icon size="32" :color="themeVars.primaryColor"><airplane-outline /></n-icon> </div>
<span
v-if="!collapsed"
class="text-xl font-black ml-3 truncate tracking-widest transition-colors"
:style="{ color: themeVars.textColor1 }"
>
AMRO 系统
</span>
</div>
<!-- 底部固定按钮 -->
<div class="sider-collapse-btn-wrap flex-shrink-0" :style="{ borderColor: themeVars.dividerColor }">
<div
class="sider-collapse-btn"
:style="{ borderColor: themeVars.dividerColor, background: themeVars.cardColor }"
@click="collapsed = !collapsed"
>
<n-icon size="18" :color="themeVars.textColor2">
<menu-outline />
</n-icon>
</div>
</div>
</div>
</n-layout-sider>
<n-layout class="flex flex-col h-screen overflow-hidden">
<n-layout-header
bordered
:position="appStore.fixedHeader ? 'absolute' : 'static'"
class="h-16 flex items-center justify-between px-6 flex-shrink-0 transition-colors"
:style="{ zIndex: appStore.fixedHeader ? 10 : 'auto' }"
>
<div class="flex items-center space-x-3">
<!-- 主题切换 -->
<CommonButton quaternary circle @click="appStore.isDark = !appStore.isDark">
<template #icon>
<n-icon>
<sunny-outline v-if="appStore.isDark" />
<moon-outline v-else />
</n-icon>
</template>
</CommonButton>
<!-- 偏好设置 -->
<SettingsDrawer />
</div>
</n-layout-header>
<n-layout-content
:content-style="{
paddingTop: appStore.contentCompact ? '6px' : '12px',
paddingBottom: appStore.contentCompact ? '6px' : '12px',
paddingLeft: appStore.contentCompact ? '6px' : '12px',
paddingRight: appStore.settingsPinned && appStore.settingsOpen ? '332px' : appStore.contentCompact ? '6px' : '12px',
display: 'flex',
flexDirection: 'column',
transition: 'all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)'
}"
class="transition-colors"
:style="{ backgroundColor: themeVars.bodyColor }"
>
<router-view v-slot="{ Component, route }">
<transition :name="appStore.transitionName || 'none'" mode="out-in">
<keep-alive>
<component :is="Component" :key="route.fullPath" class="flex-1 flex flex-col min-h-0" />
</keep-alive>
</transition>
</router-view>
</n-layout-content>
</n-layout>
</n-layout>
<CommonImportModal ref="globalImportModalRef" />
<CommonExportModal ref="globalExportModalRef" />
<CommonUploadModal ref="globalUploadModalRef" />
<CommonDownloadModal ref="globalDownloadModalRef" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useAppStore } from '@/store/app/index' import { useAppStore } from '@/store/app/index'
import SettingsDrawer from './components/SettingsDrawer.vue'
import CommonDownloadModal from '@/components/CommonDownloadModal.vue' import CommonDownloadModal from '@/components/CommonDownloadModal.vue'
import { AirplaneOutline, SunnyOutline, MoonOutline, MenuOutline } from '@vicons/ionicons5'
import router from '@/router/index'
const themeVars = useThemeVars() const themeVars = useThemeVars()
const route = useRoute()
const appStore = useAppStore() const appStore = useAppStore()
const globalImportModalRef = ref() const globalImportModalRef = ref()
const globalExportModalRef = ref() const globalExportModalRef = ref()
...@@ -104,18 +29,10 @@ const globalAttachmentModalRef = ref() ...@@ -104,18 +29,10 @@ const globalAttachmentModalRef = ref()
const globalUploadModalRef = ref() const globalUploadModalRef = ref()
const globalPreviewModalRef = ref() const globalPreviewModalRef = ref()
const globalDownloadModalRef = ref() const globalDownloadModalRef = ref()
const collapsed = computed({
get: () => appStore.collapsed,
set: (val) => (appStore.collapsed = val)
})
const activeKey = ref(route.path)
// 菜单数据已通过 Pinia 持久化,无需在此重复获取
// 全局快捷键处理 // 全局快捷键处理
const handleGlobalKeydown = (e: KeyboardEvent) => { const handleGlobalKeydown = (e: KeyboardEvent) => {
const ctrl = e.ctrlKey || e.metaKey const ctrl = e.ctrlKey || e.metaKey
const alt = e.altKey
const shift = e.shiftKey const shift = e.shiftKey
// Ctrl+Shift+D: 切换深色/浅色主题 // Ctrl+Shift+D: 切换深色/浅色主题
...@@ -124,13 +41,6 @@ const handleGlobalKeydown = (e: KeyboardEvent) => { ...@@ -124,13 +41,6 @@ const handleGlobalKeydown = (e: KeyboardEvent) => {
appStore.isDark = !appStore.isDark appStore.isDark = !appStore.isDark
return return
} }
// Alt+H: 返回首页
if (alt && e.key.toLowerCase() === 'h') {
e.preventDefault()
router.push('/')
return
}
} }
onMounted(() => { onMounted(() => {
...@@ -141,7 +51,11 @@ onMounted(() => { ...@@ -141,7 +51,11 @@ onMounted(() => {
window.$uploadModal = globalUploadModalRef.value window.$uploadModal = globalUploadModalRef.value
window.$previewModal = globalPreviewModalRef.value window.$previewModal = globalPreviewModalRef.value
window.$downloadModal = globalDownloadModalRef.value window.$downloadModal = globalDownloadModalRef.value
// 初始化应用主题
appStore.applyTheme()
}) })
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('keydown', handleGlobalKeydown) window.removeEventListener('keydown', handleGlobalKeydown)
}) })
...@@ -190,79 +104,4 @@ onUnmounted(() => { ...@@ -190,79 +104,4 @@ onUnmounted(() => {
opacity: 0; opacity: 0;
transform: scale(0.96); transform: scale(0.96);
} }
/* 沉浸式风格:选中背景充满侧边栏宽度 */
:deep(.n-menu-item-content) {
border-radius: 0 !important;
margin: 1px 0 !important; /* 保持极小的上下间隙以区分项 */
transition: all 0.3s ease !important;
}
/* 修复折叠状态下的全宽居中问题 */
:deep(.n-menu.n-menu--collapsed .n-menu-item-content) {
padding: 0 !important;
margin: 1px 0 !important;
display: flex !important;
justify-content: center !important;
align-items: center !important;
width: 64px !important; /* 必须显式指定宽度以覆盖 Naive UI 内部计算值 */
}
:deep(.n-menu.n-menu--collapsed .n-menu-item-content .n-menu-item-content__icon) {
margin-right: 0 !important;
}
/* 确保折叠时文字和箭头不占位 */
:deep(.n-menu.n-menu--collapsed .n-menu-item-content-header),
:deep(.n-menu.n-menu--collapsed .n-menu-item-content__arrow) {
display: none !important;
}
:deep(.n-menu-item-content--selected) {
font-weight: 600;
}
/* 自定义折叠按钮区域 */
.sider-collapse-btn-wrap {
padding: 10px 0 12px;
display: flex;
justify-content: center;
border-top: 1px solid;
}
.sider-collapse-btn {
width: 40px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid;
border-radius: 6px;
cursor: pointer;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
transition: all 0.2s ease;
}
.sider-collapse-btn:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
transform: translateY(-1px);
}
/* sider 内部 flex 列布局,按钮固定底部 */
.sider-with-trigger :deep(.n-layout-sider-scroll-container) {
overflow: hidden !important;
}
.sider-inner {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.sider-menu-scroll {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
</style> </style>
...@@ -14,6 +14,7 @@ const constantRoutes: Array<RouteRecordRaw> = [ ...@@ -14,6 +14,7 @@ const constantRoutes: Array<RouteRecordRaw> = [
path: '/', path: '/',
name: 'layout', name: 'layout',
component: MainLayout, component: MainLayout,
redirect: '/editor',
children: [ children: [
{ {
path: '/theme', path: '/theme',
...@@ -22,16 +23,16 @@ const constantRoutes: Array<RouteRecordRaw> = [ ...@@ -22,16 +23,16 @@ const constantRoutes: Array<RouteRecordRaw> = [
meta: { title: '主题色板' } meta: { title: '主题色板' }
}, },
{ {
path: '/xml-editor', path: '/editor',
name: 'xml-editor', name: 'editor',
component: () => import('@/views/xmlEditor.vue'), component: () => import('@/views/editor/index.vue'),
meta: { title: '工卡 XML 编辑器' } meta: { title: '工卡 XML 编辑器', fullScreen: true }
} }
] ]
}, },
{ {
path: '/views/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
component: MainLayout redirect: '/404'
} }
] ]
......
import { findNodeById, serializeTreeToXml, findParentNode, parseXmlToTree } from '@/utils/xmlParser' import { findNodeById, serializeTreeToXml, findParentNode, parseXmlToTree } from '@/utils/xmlParser'
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
import type { EditorState } from './types' import type { EditorState } from './types'
import { createDefaultAttributes } from '@/utils/dtdManager'
/**
* 辅助函数:根据标签名创建具有完整子结构的默认 XML 节点对象
*/
const createDefaultNodeStructure = (tagName: string): XmlNode => {
const id = crypto.randomUUID()
const attributes = createDefaultAttributes(tagName)
if (tagName === 'TABLE') {
const tgroupId = crypto.randomUUID()
const theadId = crypto.randomUUID()
const tbodyId = crypto.randomUUID()
const rowId1 = crypto.randomUUID()
const rowId2 = crypto.randomUUID()
return {
id,
tagName: 'TABLE',
attributes: {},
children: [
{
id: tgroupId,
tagName: 'TGROUP',
attributes: { COLS: '3' },
children: [
{ id: crypto.randomUUID(), tagName: 'COLSPEC', attributes: { COLNAME: 'col1', COLNUM: '1', COLWIDTH: '1*' }, children: [], textContent: '', mixedContent: [], parentId: tgroupId },
{ id: crypto.randomUUID(), tagName: 'COLSPEC', attributes: { COLNAME: 'col2', COLNUM: '2', COLWIDTH: '1*' }, children: [], textContent: '', mixedContent: [], parentId: tgroupId },
{ id: crypto.randomUUID(), tagName: 'COLSPEC', attributes: { COLNAME: 'col3', COLNUM: '3', COLWIDTH: '1*' }, children: [], textContent: '', mixedContent: [], parentId: tgroupId },
{
id: theadId,
tagName: 'THEAD',
attributes: {},
children: [
{
id: rowId1,
tagName: 'ROW',
attributes: {},
children: [
{ id: crypto.randomUUID(), tagName: 'ENTRY', attributes: {}, children: [], textContent: '列头 1', mixedContent: [], parentId: rowId1 },
{ id: crypto.randomUUID(), tagName: 'ENTRY', attributes: {}, children: [], textContent: '列头 2', mixedContent: [], parentId: rowId1 },
{ id: crypto.randomUUID(), tagName: 'ENTRY', attributes: {}, children: [], textContent: '列头 3', mixedContent: [], parentId: rowId1 }
],
textContent: '',
mixedContent: [],
parentId: theadId
}
],
textContent: '',
mixedContent: [],
parentId: tgroupId
},
{
id: tbodyId,
tagName: 'TBODY',
attributes: {},
children: [
{
id: rowId2,
tagName: 'ROW',
attributes: {},
children: [
{ id: crypto.randomUUID(), tagName: 'ENTRY', attributes: {}, children: [], textContent: '内容 1-1', mixedContent: [], parentId: rowId2 },
{ id: crypto.randomUUID(), tagName: 'ENTRY', attributes: {}, children: [], textContent: '内容 1-2', mixedContent: [], parentId: rowId2 },
{ id: crypto.randomUUID(), tagName: 'ENTRY', attributes: {}, children: [], textContent: '内容 1-3', mixedContent: [], parentId: rowId2 }
],
textContent: '',
mixedContent: [],
parentId: tbodyId
}
],
textContent: '',
mixedContent: [],
parentId: tgroupId
}
],
textContent: '',
mixedContent: [],
parentId: id
}
],
textContent: '',
mixedContent: [],
parentId: null
}
}
if (tagName === 'GRAPHIC') {
const sheetId = crypto.randomUUID()
return {
id,
tagName: 'GRAPHIC',
attributes: { KEY: 'IMG_' + Date.now(), ...attributes },
children: [
{
id: crypto.randomUUID(),
tagName: 'TITLE',
attributes: {},
children: [],
textContent: '图片标题',
mixedContent: [],
parentId: id
},
{
id: sheetId,
tagName: 'SHEET',
attributes: { SHEETNBR: '1', GNBR: 'default_gnbr_img' },
children: [
{
id: crypto.randomUUID(),
tagName: 'TITLE',
attributes: {},
children: [],
textContent: '图纸工作表标题',
mixedContent: [],
parentId: sheetId
}
],
textContent: '',
mixedContent: [],
parentId: id
}
],
textContent: '',
mixedContent: [],
parentId: null
}
}
if (tagName === 'PRETOPIC') {
return {
id,
tagName: 'PRETOPIC',
attributes: { ...attributes },
children: [
{
id: crypto.randomUUID(),
tagName: 'TITLEC',
attributes: {},
children: [],
textContent: '新模板段落中文标题',
mixedContent: [],
parentId: id
},
{
id: crypto.randomUUID(),
tagName: 'TITLE',
attributes: {},
children: [],
textContent: 'New Template Paragraph English Title',
mixedContent: [],
parentId: id
},
{
id: crypto.randomUUID(),
tagName: 'PARAC',
attributes: {},
children: [],
textContent: '这里是模板的默认文本内容,您可以直接在此进行编辑。',
mixedContent: [],
parentId: id
}
],
textContent: '',
mixedContent: [],
parentId: null
}
}
if (tagName === 'SELECTION') {
return {
id,
tagName: 'SELECTION',
attributes: { ...attributes },
children: [
{ id: crypto.randomUUID(), tagName: 'SELECT-ITEM', attributes: { VALUE: 'yes' }, children: [], textContent: '是 (Yes)', mixedContent: [], parentId: id },
{ id: crypto.randomUUID(), tagName: 'SELECT-ITEM', attributes: { VALUE: 'no' }, children: [], textContent: '否 (No)', mixedContent: [], parentId: id }
],
textContent: '',
mixedContent: [],
parentId: null
}
}
let defaultText = ''
if (tagName === 'RECORD-LINE') defaultText = '记录项:__________________'
if (tagName === 'DATE') defaultText = new Date().toISOString().split('T')[0]
if (tagName === 'UNIT-RECORD') defaultText = '测量值:____ 毫米'
return {
id,
tagName,
attributes,
children: [],
textContent: defaultText,
mixedContent: [],
parentId: null
}
}
export const useEditorStore = defineStore('editor', { export const useEditorStore = defineStore('editor', {
state: (): EditorState => ({ state: (): EditorState => ({
xmlTree: null, xmlTree: null,
selectedNodeId: null, selectedNodeId: null,
undoStack: [], undoStack: [],
redoStack: [] redoStack: [],
lastUndoRedoTime: 0
}), }),
getters: { getters: {
nodeMap(state): Map<string, { node: XmlNode; parent: XmlNode | null }> {
const map = new Map<string, { node: XmlNode; parent: XmlNode | null }>()
if (state.xmlTree) {
const traverse = (node: XmlNode, parent: XmlNode | null) => {
map.set(node.id, { node, parent })
for (let i = 0; i < node.children.length; i++) {
traverse(node.children[i], node)
}
}
traverse(state.xmlTree, null)
}
return map
},
selectedNode(state): XmlNode | null { selectedNode(state): XmlNode | null {
if (!state.xmlTree || !state.selectedNodeId) return null if (!state.selectedNodeId) return null
return findNodeById(state.xmlTree, state.selectedNodeId) return this.nodeMap.get(state.selectedNodeId)?.node ?? null
}, },
selectedNodeParent(state): XmlNode | null { selectedNodeParent(state): XmlNode | null {
if (!state.xmlTree || !state.selectedNodeId) return null if (!state.selectedNodeId) return null
return findParentNode(state.xmlTree, state.selectedNodeId) return this.nodeMap.get(state.selectedNodeId)?.parent ?? null
} }
}, },
actions: { actions: {
insertNode(tagName: string, insertBelow: boolean) {
if (!this.xmlTree || !this.selectedNodeId) {
window.$message.warning('请先在树中选择一个目标节点!')
return
}
const selected = this.selectedNode
if (!selected) return
const newNode = createDefaultNodeStructure(tagName)
const setParentRecursive = (n: XmlNode, pid: string) => {
n.parentId = pid
n.children.forEach(c => setParentRecursive(c, n.id))
}
if (insertBelow) {
const parent = this.selectedNodeParent
if (!parent) {
window.$message.warning('无法在根节点下方插入兄弟节点')
return
}
this.saveSnapshot()
setParentRecursive(newNode, parent.id)
const index = parent.children.findIndex(c => c.id === this.selectedNodeId)
if (index !== -1) {
parent.children.splice(index + 1, 0, newNode)
} else {
parent.children.push(newNode)
}
this.selectedNodeId = newNode.id
window.$message.success(`成功在下方插入节点 <${tagName}>`)
} else {
this.saveSnapshot()
setParentRecursive(newNode, selected.id)
selected.children.push(newNode)
this.selectedNodeId = newNode.id
window.$message.success(`成功向节点内插入子节点 <${tagName}>`)
}
},
setXmlTree(tree: XmlNode) { setXmlTree(tree: XmlNode) {
this.xmlTree = tree this.xmlTree = tree
this.selectedNodeId = tree.id this.selectedNodeId = tree.id
this.undoStack = [] this.undoStack = []
this.redoStack = [] this.redoStack = []
this.lastUndoRedoTime = 0
}, },
setSelectedNodeId(id: string | null) { setSelectedNodeId(id: string | null) {
...@@ -45,36 +302,30 @@ export const useEditorStore = defineStore('editor', { ...@@ -45,36 +302,30 @@ export const useEditorStore = defineStore('editor', {
*/ */
saveSnapshot() { saveSnapshot() {
if (!this.xmlTree) return if (!this.xmlTree) return
const xmlStr = serializeTreeToXml(this.xmlTree) const clone = JSON.parse(JSON.stringify(this.xmlTree))
// 限制撤销栈大小为 50 // 限制撤销栈大小为 50
if (this.undoStack.length >= 50) { if (this.undoStack.length >= 50) {
this.undoStack.shift() this.undoStack.shift()
} }
this.undoStack.push(xmlStr) this.undoStack.push(clone)
// 每次新操作后,清空重做栈 // 每次新操作后,清空重做栈
this.redoStack = [] this.redoStack = []
}, },
/**
* 撤销
*/
undo() { undo() {
if (this.undoStack.length === 0 || !this.xmlTree) return if (this.undoStack.length === 0 || !this.xmlTree) return
const currentXml = serializeTreeToXml(this.xmlTree) const currentClone = JSON.parse(JSON.stringify(this.xmlTree))
this.redoStack.push(currentXml) this.redoStack.push(currentClone)
const previousXml = this.undoStack.pop()!
// 重新解析上一个 XML 状态并设置 const previousTree = this.undoStack.pop()!
const tree = parseXmlToTree(previousXml)
// 保持选中 ID 存在于新树中,若不存在则默认选中根节点 this.xmlTree = previousTree
this.xmlTree = tree if (this.selectedNodeId && !this.nodeMap.has(this.selectedNodeId)) {
if (this.selectedNodeId && !findNodeById(tree, this.selectedNodeId)) { this.selectedNodeId = previousTree.id
this.selectedNodeId = tree.id
} }
this.lastUndoRedoTime = Date.now()
}, },
/** /**
...@@ -83,17 +334,16 @@ export const useEditorStore = defineStore('editor', { ...@@ -83,17 +334,16 @@ export const useEditorStore = defineStore('editor', {
redo() { redo() {
if (this.redoStack.length === 0 || !this.xmlTree) return if (this.redoStack.length === 0 || !this.xmlTree) return
const currentXml = serializeTreeToXml(this.xmlTree) const currentClone = JSON.parse(JSON.stringify(this.xmlTree))
this.undoStack.push(currentXml) this.undoStack.push(currentClone)
const nextXml = this.redoStack.pop()!
const tree = parseXmlToTree(nextXml) const nextTree = this.redoStack.pop()!
this.xmlTree = tree this.xmlTree = nextTree
if (this.selectedNodeId && !findNodeById(tree, this.selectedNodeId)) { if (this.selectedNodeId && !this.nodeMap.has(this.selectedNodeId)) {
this.selectedNodeId = tree.id this.selectedNodeId = nextTree.id
} }
this.lastUndoRedoTime = Date.now()
}, },
/** /**
...@@ -177,7 +427,7 @@ export const useEditorStore = defineStore('editor', { ...@@ -177,7 +427,7 @@ export const useEditorStore = defineStore('editor', {
* 移动子节点位置 * 移动子节点位置
*/ */
moveChildNode(nodeId: string, direction: 'up' | 'down') { moveChildNode(nodeId: string, direction: 'up' | 'down') {
const parent = findParentNode(this.xmlTree!, nodeId) const parent = this.nodeMap.get(nodeId)?.parent
if (!parent) return if (!parent) return
const index = parent.children.findIndex((c) => c.id === nodeId) const index = parent.children.findIndex((c) => c.id === nodeId)
......
...@@ -3,6 +3,7 @@ import type { XmlNode } from '@/types/xmlNode' ...@@ -3,6 +3,7 @@ import type { XmlNode } from '@/types/xmlNode'
export interface EditorState { export interface EditorState {
xmlTree: XmlNode | null xmlTree: XmlNode | null
selectedNodeId: string | null selectedNodeId: string | null
undoStack: string[] // 保存 XML 字符串的历史快照 undoStack: XmlNode[] // 保存 XmlNode 历史快照
redoStack: string[] // 保存 XML 字符串的重做快照 redoStack: XmlNode[] // 保存 XmlNode 重做快照
lastUndoRedoTime: number // 回退重做动作的时间戳,用于触发视口重定位
} }
...@@ -13,7 +13,8 @@ function generateId(): string { ...@@ -13,7 +13,8 @@ function generateId(): string {
const INLINE_ELEMENTS = new Set([ const INLINE_ELEMENTS = new Set([
'REFBLOCK', 'REFINT', 'REFEXT', 'EIN', 'PAN', 'STDNAME', 'TED', 'REFBLOCK', 'REFINT', 'REFEXT', 'EIN', 'PAN', 'STDNAME', 'TED',
'TOOLNBR', 'TOOLNAME', 'ZONE', 'EFFECT', 'CONEFFECT', 'TOOLNBR', 'TOOLNAME', 'ZONE', 'EFFECT', 'CONEFFECT',
'CB', 'CBNAME', 'CBLOC', 'GRPHCREF' 'CB', 'CBNAME', 'CBLOC', 'GRPHCREF',
'SUPER', 'SUPERSCRIPT', 'SUB', 'SUBSCRIPT'
]) ])
export function isInlineElement(tagName: string): boolean { export function isInlineElement(tagName: string): boolean {
...@@ -21,7 +22,7 @@ export function isInlineElement(tagName: string): boolean { ...@@ -21,7 +22,7 @@ export function isInlineElement(tagName: string): boolean {
} }
/** /**
* 将 XML 字符串解析为 XmlNode 树 * 将 XML 字符串解析为 XmlNode 树(同步版,适合小文件)
*/ */
export function parseXmlToTree(xmlString: string): XmlNode { export function parseXmlToTree(xmlString: string): XmlNode {
const parser = new DOMParser() const parser = new DOMParser()
...@@ -38,6 +39,23 @@ export function parseXmlToTree(xmlString: string): XmlNode { ...@@ -38,6 +39,23 @@ export function parseXmlToTree(xmlString: string): XmlNode {
} }
/** /**
* 将 XML 字符串解析为 XmlNode 树(异步版本)
* 通过 setTimeout 延迟执行,使主线程有机会渲染 UI(如加载动画),然后进行解析
*/
export function parseXmlToTreeAsync(xmlString: string): Promise<XmlNode> {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
const tree = parseXmlToTree(xmlString)
resolve(tree)
} catch (err: any) {
reject(err instanceof Error ? err : new Error(String(err)))
}
}, 50) // 给主线程留出足够的绘图帧时间
})
}
/**
* 递归将 DOM Element 转换为 XmlNode * 递归将 DOM Element 转换为 XmlNode
*/ */
function domElementToXmlNode(element: Element, parentId: string | null): XmlNode { function domElementToXmlNode(element: Element, parentId: string | null): XmlNode {
......
...@@ -6,7 +6,7 @@ import { useEditorStore } from '@/store/editor' ...@@ -6,7 +6,7 @@ import { useEditorStore } from '@/store/editor'
export function useAttributeEditor() { export function useAttributeEditor() {
const store = useEditorStore() const store = useEditorStore()
function updateAttributes(model: Record<string, string>): void { const updateAttributes = (model: Record<string, string>): void => {
const updatedAttrs: Record<string, string> = {} const updatedAttrs: Record<string, string> = {}
for (const [k, v] of Object.entries(model)) { for (const [k, v] of Object.entries(model)) {
if (v !== '' && v !== null && v !== undefined) { if (v !== '' && v !== null && v !== undefined) {
......
<template>
<component
:is="isInline ? 'span' : 'div'"
:data-node-id="node.id"
class="doc-node-wrapper relative transition-all"
:class="[
isInline ? 'inline align-baseline mx-0.5' : 'block my-1',
isSelected ? 'ring-2 ring-primary ring-offset-1 rounded-sm bg-primary/5' : '',
!isInline && isContainer ? 'py-1 px-1' : ''
]"
@click.stop="editorStore.setSelectedNodeId(node.id)"
>
<!-- 0. 各类 HEADER 节点特殊处理 (不展示) -->
<template v-if="isHeaderTag"></template>
<!-- 1. WARNING / CAUTION / NOTE 结构特殊处理 (仿照 PDF) -->
<template v-else-if="node.tagName === 'WARNING' || node.tagName === 'CAUTION' || node.tagName === 'NOTE'">
<div
class="relative my-3 text-sm"
:class="[
node.tagName === 'WARNING' ? 'text-red-600 font-bold uppercase' : '',
node.tagName === 'CAUTION' ? 'text-[#ff6a00] font-bold' : '',
node.tagName === 'NOTE' ? 'text-blue-600' : ''
]"
:style="{
paddingLeft: node.tagName === 'WARNING' ? '120px' : node.tagName === 'CAUTION' ? '140px' : '80px'
}"
>
<span
class="absolute left-0 top-0 font-bold underline select-none"
:class="[
node.tagName === 'WARNING' ? 'text-red-600' : '',
node.tagName === 'CAUTION' ? 'text-[#ff6a00]' : '',
node.tagName === 'NOTE' ? 'text-blue-600' : ''
]"
>
{{ node.tagName === 'WARNING' ? '警告 WARNING:' : node.tagName === 'CAUTION' ? '警戒 CAUTION:' : '注意 NOTE:' }}
</span>
<div class="space-y-1">
<DocNodeRenderer v-for="child in node.children" :key="child.id" :node="child" />
</div>
</div>
</template>
<!-- 2. EFFECT / CONEFFECT (适用性) 特殊处理 -->
<template v-else-if="node.tagName === 'EFFECT' || node.tagName === 'CONEFFECT'">
<span v-if="isInline" class="text-danger font-bold text-xs select-none py-1 uppercase mx-1">
** {{ node.tagName === 'EFFECT' ? 'ON A/C' : 'CONF' }}: {{ formatEff(node.attributes.EFFRG || node.textContent) }}
</span>
<div v-else class="text-danger font-bold text-xs select-none py-1 uppercase my-1 border-t border-b border-dashed border-danger/30 pl-1">
** {{ node.tagName === 'EFFECT' ? 'ON A/C' : 'CONF' }}: {{ formatEff(node.attributes.EFFRG || node.textContent) }}
</div>
</template>
<!-- 3. SBEFF / SBEFFC (服务通告适用性) 处理 -->
<template v-else-if="node.tagName === 'SBEFF' || node.tagName === 'SBEFFC'">
<span v-if="isInline" class="text-danger font-bold text-xs select-none py-1 uppercase mx-1">
** SB: {{ node.attributes.SBCOND }} SB {{ node.attributes.SBNBR }} for A/C {{ formatEff(node.attributes.EFFRG) }}
</span>
<div v-else class="text-danger font-bold text-xs select-none py-1 uppercase my-1 border-t border-b border-dashed border-danger/30 pl-1">
** SB: {{ node.attributes.SBCOND }} SB {{ node.attributes.SBNBR }} for A/C {{ formatEff(node.attributes.EFFRG) }}
</div>
</template>
<!-- 4. TITLEC (中文标题) 特殊处理 -->
<template v-else-if="node.tagName === 'TITLEC'">
<div
contenteditable="true"
class="font-bold text-base text-color1 py-1 focus:outline-none focus:bg-fill-3 px-1 rounded transition-colors"
@blur="handleTextBlur"
@keydown.enter.prevent
v-text="node.textContent"
></div>
</template>
<!-- 5. TITLE (英文标题) 特殊处理 -->
<template v-else-if="node.tagName === 'TITLE'">
<div
contenteditable="true"
class="font-bold italic text-sm text-color2 py-1 focus:outline-none focus:bg-fill-3 px-1 rounded transition-colors"
@blur="handleTextBlur"
@keydown.enter.prevent
v-text="node.textContent"
></div>
</template>
<!-- 6. PARA / PARAC 段落文本处理 -->
<template v-else-if="node.tagName === 'PARA' || node.tagName === 'PARAC'">
<!-- 如果有混合内容,则渲染混合片段 -->
<div
v-if="node.mixedContent && node.mixedContent.length > 0"
class="flex flex-wrap items-center leading-relaxed text-sm"
:class="[isInsideAlert ? 'text-inherit' : 'text-color2']"
>
<template v-for="(item, idx) in node.mixedContent" :key="idx">
<span
v-if="item.type === 'text'"
contenteditable="true"
class="focus:outline-none focus:bg-fill-3 px-0.5 rounded"
@blur="(e) => handleMixedTextBlur(idx, e)"
v-text="item.text"
></span>
<DocNodeRenderer
v-else-if="item.type === 'element' && item.nodeId && getChildNode(item.nodeId)"
:node="getChildNode(item.nodeId)!"
is-inline
:inside-ref-block="insideRefBlock"
/>
</template>
</div>
<!-- 如果没有混合内容但有子节点,则直接渲染子节点 -->
<div
v-else-if="node.children && node.children.length > 0"
class="text-sm leading-relaxed py-1"
:class="[isInsideAlert ? 'text-inherit' : 'text-color2']"
>
<DocNodeRenderer v-for="child in node.children" :key="child.id" :node="child" is-inline :inside-ref-block="insideRefBlock" />
</div>
<div
v-else
contenteditable="true"
class="text-sm leading-relaxed py-1 focus:outline-none focus:bg-fill-3 px-1 rounded transition-colors"
:class="[isInsideAlert ? 'text-inherit' : 'text-color2']"
@blur="handleTextBlur"
v-text="node.textContent"
></div>
</template>
<!-- 7. REFBLOCK 特殊处理 -->
<template v-else-if="node.tagName === 'REFBLOCK'">
<span class="inline-ref-block font-medium">
<template v-if="node.children && node.children.length > 0">
<DocNodeRenderer v-for="child in node.children" :key="child.id" :node="child" is-inline :inside-ref-block="true" />
</template>
<template v-else>
<span
contenteditable="true"
class="focus:outline-none focus:bg-fill-3 px-0.5 rounded"
@blur="handleTextBlur"
v-text="node.textContent"
></span>
</template>
</span>
</template>
<!-- 8. REFINT / REFEXT / GRPHCREF (引用链接) 特殊处理 -->
<template v-else-if="node.tagName === 'REFINT' || node.tagName === 'REFEXT' || node.tagName === 'GRPHCREF'">
<span
class="inline-ref font-mono font-medium text-primary hover:underline cursor-pointer select-all"
@click.stop="editorStore.setSelectedNodeId(node.id)"
>
<template v-if="!insideRefBlock">
{{ isChineseContext ? '(参考: ' : '(Ref: ' }}{{ node.textContent || node.attributes.REFLOC || '' }}{{ ')' }}
</template>
<template v-else>
{{ node.textContent || node.attributes.REFLOC || '' }}
</template>
</span>
</template>
<!-- 9. EIN (功能号) 特殊处理 -->
<template v-else-if="node.tagName === 'EIN'">
<span
class="font-mono font-bold bg-fill-2 border border-divider px-1.5 py-0.5 rounded text-xs text-color1 mx-0.5 inline-flex items-center cursor-pointer hover:bg-fill-3"
@click.stop="editorStore.setSelectedNodeId(node.id)"
>
FIN {{ (node.textContent || '').replace(/-/g, '') }}
</span>
</template>
<!-- 10. PAN (盖板/面板) 特殊处理 -->
<template v-else-if="node.tagName === 'PAN'">
<span
class="underline font-bold text-color1 mx-0.5 cursor-pointer hover:text-primary"
@click.stop="editorStore.setSelectedNodeId(node.id)"
>
{{ node.textContent }}
</span>
</template>
<!-- 11. STDNAME (标准名称) 特殊处理 -->
<template v-else-if="node.tagName === 'STDNAME'">
<span class="font-bold text-color1 mx-0.5 cursor-pointer" @click.stop="editorStore.setSelectedNodeId(node.id)">
{{ node.textContent }}
</span>
</template>
<!-- 12. ZONE (区域) 特殊处理 -->
<template v-else-if="node.tagName === 'ZONE'">
<span
class="font-mono font-bold bg-warning/10 text-warning border border-warning/20 px-1 py-0.2 rounded text-xs mx-0.5 inline-flex items-center cursor-pointer hover:bg-warning/20"
@click.stop="editorStore.setSelectedNodeId(node.id)"
>
{{ node.textContent }}
</span>
</template>
<!-- 13. TED (工具/设备数据) 特殊处理 -->
<template v-else-if="node.tagName === 'TED'">
<span class="italic text-color1 mx-0.5 cursor-pointer hover:text-primary" @click.stop="editorStore.setSelectedNodeId(node.id)">
{{ getTedText(node) }}
</span>
</template>
<!-- 14. CON (消耗品数据) 特殊处理 -->
<template v-else-if="node.tagName === 'CON'">
<span class="italic text-color1 mx-0.5 cursor-pointer hover:text-primary" @click.stop="editorStore.setSelectedNodeId(node.id)">
{{ getConText(node) }}
</span>
</template>
<!-- 15. CB / CBNAME / CBLOC (断路器信息) 特殊处理 -->
<template v-else-if="node.tagName === 'CB' || node.tagName === 'CBNAME' || node.tagName === 'CBLOC'">
<span
class="font-mono font-bold text-color1 mx-0.5 px-1 bg-fill-2 rounded border border-divider cursor-pointer hover:bg-fill-3"
@click.stop="editorStore.setSelectedNodeId(node.id)"
>
{{ node.textContent }}
</span>
</template>
<!-- 16. CBLST (电路断路器列表) 特殊处理 (仿照 PDF 样式表格) -->
<template v-else-if="node.tagName === 'CBLST'">
<div class="my-4 overflow-x-auto select-none border border-divider rounded-lg p-3 bg-card shadow-sm">
<div class="text-xs font-bold text-color3 mb-2 flex items-center space-x-1">
<n-icon color="var(--primary-color)"><grid-outline /></n-icon>
<span>
电路断路器清单 (CBLST) - 行动:
{{ node.attributes.ACTION === 'verif-close' ? '确认关闭' : node.attributes.ACTION === 'open' ? '断开' : '操作' }}
</span>
</div>
<table class="w-full border-collapse border border-divider text-xs">
<thead>
<tr class="bg-fill-2 text-color1 font-bold h-8">
<th class="border border-divider px-2 text-center w-1/4">面板 PANEL</th>
<th class="border border-divider px-2 text-center w-5/12">说明 DESIGNATION</th>
<th class="border border-divider px-2 text-center w-1/6">功能号 FIN</th>
<th class="border border-divider px-2 text-center w-1/6">位置 LOCATION</th>
</tr>
</thead>
<tbody>
<template v-for="subList in node.children" :key="subList.id">
<!-- 如果 CBSUBLST 下有除了 CBDATA 以外的子节点,比如说明文本,可以单独渲染一行 -->
<tr v-if="hasNonCbDataChildren(subList)" class="bg-fill-1 text-color2">
<td colspan="4" class="border border-divider px-2 py-1 font-semibold">
<DocNodeRenderer v-for="child in getNonCbDataChildren(subList)" :key="child.id" :node="child" />
</td>
</tr>
<!-- 遍历 CBDATA 行 -->
<template v-for="cbData in subList.children.filter((c) => c.tagName === 'CBDATA')" :key="cbData.id">
<!-- 如果 CBDATA 带有 EFFECT,按 PDF 样式可以单独渲染一行作为适用性提示 -->
<tr v-if="cbData.children.some((c) => c.tagName === 'EFFECT')" class="bg-fill-1">
<td colspan="4" class="border border-divider px-2 py-0.5 text-danger font-bold text-[10px]">
<DocNodeRenderer
v-for="eff in cbData.children.filter((c) => c.tagName === 'EFFECT')"
:key="eff.id"
:node="eff"
is-inline
/>
</td>
</tr>
<tr
:data-node-id="cbData.id"
class="h-8 hover:bg-fill-3 transition-colors cursor-pointer"
:class="[editorStore.selectedNodeId === cbData.id ? 'bg-primary/10 font-bold' : '']"
@click.stop="editorStore.setSelectedNodeId(cbData.id)"
>
<td class="border border-divider px-2 text-center font-mono">
{{ getCbValue(cbData, 'PAN') }}
</td>
<td class="border border-divider px-2">
{{ getCbValue(cbData, 'CBNAME') }}
</td>
<td class="border border-divider px-2 text-center font-mono font-bold text-primary">
{{ getCbValue(cbData, 'CB').replace(/-/g, '') }}
</td>
<td class="border border-divider px-2 text-center font-mono">
{{ getCbValue(cbData, 'CBLOC') }}
</td>
</tr>
</template>
</template>
</tbody>
</table>
</div>
</template>
<!-- 17. 列表项目 L1ITEM / L2ITEM / L3ITEM / L4ITEM / UNLITEM / NUMLITEM 处理 -->
<template v-else-if="isListItem">
<div class="flex items-baseline space-x-2 my-1.5 pl-4">
<span class="text-sm font-bold select-none shrink-0 w-6 text-right" :class="[isInsideAlert ? 'text-inherit' : 'text-color1']">
{{ getListBullet(node) }}
</span>
<div class="flex-1 min-w-0">
<template v-if="node.children && node.children.length > 0">
<div class="space-y-1">
<DocNodeRenderer v-for="child in node.children" :key="child.id" :node="child" />
</div>
</template>
<template v-else>
<div
contenteditable="true"
class="text-sm leading-relaxed focus:outline-none focus:bg-fill-3 px-1 rounded"
:class="[isInsideAlert ? 'text-inherit' : 'text-color2']"
@blur="handleTextBlur"
v-text="node.textContent"
></div>
</template>
</div>
</div>
</template>
<!-- 18. CALS TABLE (表格) 可视化编辑器集成 -->
<template v-else-if="node.tagName === 'TABLE'">
<div class="my-4 border border-divider rounded-lg overflow-hidden bg-card p-3 shadow-sm">
<div class="flex items-center space-x-2 mb-2 pb-2 border-b border-divider">
<n-icon color="var(--primary-color)"><grid-outline /></n-icon>
<span class="text-xs font-bold text-color2">表格编辑区域</span>
</div>
<TableEditor :node="node" />
</div>
</template>
<!-- 19. GRAPHIC (图片) 可视化处理 -->
<template v-else-if="node.tagName === 'GRAPHIC'">
<div class="my-4 border border-divider rounded-lg overflow-hidden bg-fill-2 p-4 flex flex-col items-center">
<div class="w-full flex items-center justify-between pb-2 border-b border-divider mb-3">
<span class="text-xs font-bold text-color2 flex items-center space-x-1">
<n-icon><image-outline /></n-icon>
<span>工卡附图 [GNBR: {{ node.children.find((c) => c.tagName === 'SHEET')?.attributes.GNBR || '无' }}]</span>
</span>
<n-tag size="small" type="primary">GRAPHIC</n-tag>
</div>
<!-- 拟物化卡片模拟设计图 -->
<div
class="w-full max-w-lg aspect-[16/10] rounded border border-divider bg-fill-3 flex flex-col items-center justify-center relative p-4 shadow-inner"
style="background-image: radial-gradient(circle, rgba(0, 0, 0, 0.03) 1px, transparent 1px); background-size: 16px 16px"
>
<div class="text-center space-y-2 select-none pointer-events-none">
<n-icon size="48" color="var(--text-color-3)"><image-outline /></n-icon>
<div class="text-xs text-color3">航空器结构部件装配原理示意图</div>
<div class="text-[10px] text-color3/60 font-mono">
GNBR Ref: {{ node.children.find((c) => c.tagName === 'SHEET')?.attributes.GNBR }}
</div>
</div>
</div>
<!-- 图片标题编辑 -->
<div class="w-full mt-2 text-center">
<span class="text-xs text-color3 italic">图标题:</span>
<span
contenteditable="true"
class="text-xs font-bold text-color2 focus:outline-none focus:bg-fill-3 px-2 py-0.5 rounded border border-dashed border-divider hover:border-primary"
@blur="handleGraphicTitleBlur"
v-text="getGraphicTitle()"
></span>
</div>
</div>
</template>
<!-- 20. RECORD-LINE (记录项) 处理 -->
<template v-else-if="node.tagName === 'RECORD-LINE'">
<div class="my-2 p-2 bg-fill-3 rounded border border-divider flex items-center space-x-2">
<n-tag type="info" size="small" round>记录项</n-tag>
<div
contenteditable="true"
class="flex-1 text-sm text-color2 font-medium focus:outline-none focus:bg-fill-2 px-1 rounded"
@blur="handleTextBlur"
v-text="node.textContent"
></div>
<div class="text-xs text-color3 select-none">[已预留输入线]</div>
</div>
</template>
<!-- 21. UNIT-RECORD (单位记录项) 处理 -->
<template v-else-if="node.tagName === 'UNIT-RECORD'">
<div class="my-2 p-2 bg-fill-3 rounded border border-divider flex items-center justify-between">
<div class="flex items-center space-x-2 flex-1">
<n-tag type="success" size="small" round>单位记录项</n-tag>
<div
contenteditable="true"
class="flex-1 text-sm text-color2 font-medium focus:outline-none focus:bg-fill-2 px-1 rounded"
@blur="handleTextBlur"
v-text="node.textContent"
></div>
</div>
<div class="text-xs bg-fill-4 px-2 py-1 rounded border border-divider text-color2 ml-4 select-none">
单位: {{ node.attributes.UNIT || 'mm' }}
</div>
</div>
</template>
<!-- 22. SELECTION (选项组) 处理 -->
<template v-else-if="node.tagName === 'SELECTION'">
<div class="my-3 p-3 bg-fill-3 rounded-lg border border-divider space-y-2">
<div class="text-xs font-bold text-color3 mb-1 select-none">选项组配置:</div>
<div class="flex flex-wrap gap-4">
<div
v-for="item in node.children"
:key="item.id"
class="flex items-center space-x-2 bg-fill-1 px-3 py-1.5 rounded border border-divider shadow-sm hover:border-primary transition-all"
>
<n-checkbox :checked="false" disabled />
<span
contenteditable="true"
class="text-xs text-color2 focus:outline-none focus:bg-fill-3 px-1 rounded"
@blur="(e) => handleChildTextBlur(item.id, e)"
v-text="item.textContent"
></span>
</div>
</div>
</div>
</template>
<!-- 23. SIGNOFF (签字点) 处理 (仿照 PDF 签字表) -->
<template v-else-if="node.tagName === 'SIGNOFF'">
<div class="my-3 overflow-x-auto select-none">
<table class="min-w-[400px] border-collapse border border-black text-xs">
<tbody>
<tr class="h-8">
<!-- 标签 -->
<td class="px-2 border border-black font-bold text-center bg-fill-2 min-w-[60px]">
{{ node.attributes.TAG || '签字' }}
</td>
<!-- Mech 栏 -->
<td class="px-2 border border-black font-bold text-center bg-fill-2 w-20">操作人 MECH</td>
<td class="px-2 border border-black text-center w-28 font-mono">
{{ node.attributes.mech ? `${node.attributes.mech} ${node.attributes.mechName || ''}` : '——' }}
</td>
<!-- Insp (若有) -->
<template v-if="['B', 'D', 'E', 'C'].includes(node.attributes['CK-LEVEL'] || 'B')">
<td class="px-2 border border-black font-bold text-center bg-fill-2 w-20">
{{ node.attributes['CK-LEVEL'] === 'C' ? '确认人 VERF' : '检验员 INSP' }}
</td>
<td class="px-2 border border-black text-center w-28 font-mono">
<template v-if="node.attributes['CK-LEVEL'] === 'C'">
{{ node.attributes.verf ? `${node.attributes.verf} ${node.attributes.verfName || ''}` : '——' }}
</template>
<template v-else>
{{ node.attributes.insp ? `${node.attributes.insp} ${node.attributes.inspName || ''}` : '——' }}
</template>
</td>
</template>
</tr>
</tbody>
</table>
</div>
</template>
<!-- 24. TOPIC / PRETOPIC 特殊处理 (仿照 PDF 标题与层级) -->
<template v-else-if="node.tagName === 'TOPIC' || node.tagName === 'PRETOPIC'">
<div class="my-4">
<!-- 标题部分 -->
<div class="font-bold text-base text-color1 border-b border-divider pb-1.5 mb-2 flex items-baseline">
<span class="mr-2 text-primary font-mono select-none">{{ getTopicSeqNum(node) }}</span>
<div class="flex-1 flex flex-col">
<DocNodeRenderer
v-for="titleNode in getTopicTitleNodes(node)"
:key="titleNode.id"
:node="titleNode"
/>
</div>
</div>
<!-- 子节点部分 (排除 TITLE TITLEC) -->
<div class="space-y-1 pl-4 border-l border-dashed border-divider/60">
<DocNodeRenderer v-for="child in getTopicContentNodes(node)" :key="child.id" :node="child" />
</div>
</div>
</template>
<!-- 25. ASSODATA 隐藏处理 (PDF 隐藏) -->
<template v-else-if="node.tagName === 'ASSODATA'">
<div
v-if="isSelected"
class="p-4 border border-dashed border-divider rounded bg-fill-2 text-center text-xs text-color3 select-none animate-pulse"
>
[ 节点 &lt;ASSODATA&gt; PDF 渲染中已设为隐藏,不予展示内容 ]
</div>
</template>
<!-- 26. 其它所有容器节点(如 JOBCARD, CEP, TFMATR, SUBTASK, LIST1... -->
<template v-else-if="isContainer">
<div class="space-y-1">
<DocNodeRenderer v-for="child in node.children" :key="child.id" :node="child" />
</div>
</template>
<!-- 26.5 上标与下标 (SUPER / SUPERSCRIPT / SUB / SUBSCRIPT) 特殊处理 -->
<template v-else-if="node.tagName === 'SUPER' || node.tagName === 'SUPERSCRIPT' || node.tagName === 'SUB' || node.tagName === 'SUBSCRIPT'">
<span
class="inline select-all text-[0.75em]"
:class="[
(node.tagName === 'SUPER' || node.tagName === 'SUPERSCRIPT') ? 'align-super' : 'align-sub'
]"
style="text-indent: 0 !important;"
>
<template v-if="node.children && node.children.length > 0">
<DocNodeRenderer v-for="child in node.children" :key="child.id" :node="child" is-inline />
</template>
<template v-else>
<span
contenteditable="true"
class="focus:outline-none focus:bg-fill-3 px-0.5 rounded"
@blur="handleTextBlur"
v-text="node.textContent"
></span>
</template>
</span>
</template>
<!-- 27. 兜底未匹配的普通文本标签 -->
<template v-else>
<span
v-if="isInline"
contenteditable="true"
class="focus:outline-none focus:bg-fill-3 px-0.5 rounded"
@blur="handleTextBlur"
v-text="node.textContent || node.tagName"
></span>
<div
v-else
contenteditable="true"
class="text-sm text-color2 leading-relaxed focus:outline-none focus:bg-fill-3 px-1 rounded"
@blur="handleTextBlur"
v-text="node.textContent || node.tagName"
></div>
</template>
</component>
</template>
<script setup lang="ts">
import DocNodeRenderer from './index.vue'
import { ImageOutline, GridOutline } from '@vicons/ionicons5'
import type { XmlNode } from '@/types/xmlNode'
import { useEditorStore } from '@/store/editor'
import TableEditor from '../TableEditor/index.vue'
const props = withDefaults(
defineProps<{
node: XmlNode
isInline?: boolean
insideRefBlock?: boolean
}>(),
{
isInline: false,
insideRefBlock: false
}
)
const editorStore = useEditorStore()
const isSelected = computed(() => editorStore.selectedNodeId === props.node.id)
// 判断是否是列表容器
const isContainer = computed(() => {
return (
!props.isInline &&
props.node.children &&
props.node.children.length > 0 &&
props.node.tagName !== 'TABLE' &&
props.node.tagName !== 'GRAPHIC' &&
props.node.tagName !== 'SELECTION' &&
props.node.tagName !== 'TOPIC' &&
props.node.tagName !== 'PRETOPIC' &&
props.node.tagName !== 'CBLST'
)
})
// 判断是否是列表项目项
const isListItem = computed(() => {
return ['L1ITEM', 'L2ITEM', 'L3ITEM', 'L4ITEM', 'L5ITEM', 'L6ITEM', 'L7ITEM', 'UNLITEM', 'NUMLITEM'].includes(props.node.tagName)
})
// 判断是否是 Header 标签
const isHeaderTag = computed(() => {
return ['SMJC-HEADER', 'LMJC-HEADER', 'NRCJC-HEADER', 'TCJC-HEADER', 'QECJC-HEADER', 'EOTK-HEADER', 'DRJC-HEADER'].includes(props.node.tagName)
})
// 查找 header 的子元素值
const getHeaderValue = (tagName: string): string => {
const child = props.node.children.find((c) => c.tagName === tagName)
return child ? child.textContent || '' : ''
}
// 判断当前节点是否嵌套在 WARNING / CAUTION / NOTE 中
const isInsideAlert = computed(() => {
let parent = editorStore.nodeMap.get(props.node.id)?.parent
while (parent) {
if (['WARNING', 'CAUTION', 'NOTE'].includes(parent.tagName)) {
return true
}
parent = editorStore.nodeMap.get(parent.id)?.parent
}
return false
})
// 判断是否在中文标签上下文中
const isChineseContext = computed(() => {
let parent = editorStore.nodeMap.get(props.node.id)?.parent
while (parent) {
if (parent.tagName.endsWith('C')) {
return true
}
parent = editorStore.nodeMap.get(parent.id)?.parent
}
return false
})
// 格式化 EFFRG
const formatEff = (eff: string | undefined): string => {
if (!eff) return 'ALL'
const cleaned = eff.replace(/\s+/g, '')
if (cleaned === '001999') return 'ALL'
if (cleaned.length === 6) {
return `${cleaned.substring(0, 3)}-${cleaned.substring(3)}`
}
return cleaned
}
// 罗马数字转换辅助
const romanize = (num: number): string => {
const lookup: Array<[string, number]> = [
['x', 10],
['ix', 9],
['v', 5],
['iv', 4],
['i', 1]
]
let roman = ''
let val = num
for (const [letter, limit] of lookup) {
while (val >= limit) {
roman += letter
val -= limit
}
}
return roman
}
// 列表标号与数字格式化
const getListBullet = (node: XmlNode): string => {
if (node.tagName === 'UNLITEM') {
const parent = editorStore.nodeMap.get(node.id)?.parent
const bullType = parent?.attributes?.BULLTYPE
if (bullType === 'BULLET') return '•'
if (bullType === 'NDASH') return '–'
if (bullType === 'MDASH') return '—'
if (bullType === 'DIAMOND') return '♦'
if (bullType === 'ASTERISK') return '*'
if (bullType === 'DELTA') return 'Δ'
if (bullType === 'SQUARE') return '♦'
if (bullType === 'NONE') return ''
return '•'
}
if (node.tagName === 'NUMLITEM') {
const parent = editorStore.nodeMap.get(node.id)?.parent
if (!parent) return '1.'
const idx = parent.children.filter((c) => c.tagName === 'NUMLITEM').findIndex((c) => c.id === node.id)
return `${idx + 1}.`
}
if (node.tagName === 'L1ITEM') {
const parent = editorStore.nodeMap.get(node.id)?.parent
if (!parent) return 'A.'
const idx = parent.children.filter((c) => c.tagName === 'L1ITEM').findIndex((c) => c.id === node.id)
return `${String.fromCharCode(65 + idx)}.`
}
if (node.tagName === 'L2ITEM') {
const parent = editorStore.nodeMap.get(node.id)?.parent
if (!parent) return '(1)'
const idx = parent.children.filter((c) => c.tagName === 'L2ITEM').findIndex((c) => c.id === node.id)
return `(${idx + 1})`
}
if (node.tagName === 'L3ITEM') {
const parent = editorStore.nodeMap.get(node.id)?.parent
if (!parent) return '(a)'
const idx = parent.children.filter((c) => c.tagName === 'L3ITEM').findIndex((c) => c.id === node.id)
return `(${String.fromCharCode(97 + idx)})`
}
if (node.tagName === 'L4ITEM') {
const parent = editorStore.nodeMap.get(node.id)?.parent
if (!parent) return '(i)'
const idx = parent.children.filter((c) => c.tagName === 'L4ITEM').findIndex((c) => c.id === node.id)
return `(${romanize(idx + 1)})`
}
if (node.tagName === 'L5ITEM') {
const parent = editorStore.nodeMap.get(node.id)?.parent
if (!parent) return 'a'
const idx = parent.children.filter((c) => c.tagName === 'L5ITEM').findIndex((c) => c.id === node.id)
return `${String.fromCharCode(97 + idx)}`
}
if (node.tagName === 'L6ITEM') {
const parent = editorStore.nodeMap.get(node.id)?.parent
if (!parent) return '1.'
const idx = parent.children.filter((c) => c.tagName === 'L6ITEM').findIndex((c) => c.id === node.id)
return `${idx + 1}.`
}
if (node.tagName === 'L7ITEM') {
const parent = editorStore.nodeMap.get(node.id)?.parent
if (!parent) return 'i'
const idx = parent.children.filter((c) => c.tagName === 'L7ITEM').findIndex((c) => c.id === node.id)
return `${romanize(idx + 1)}`
}
return '•'
}
// 获取 TOPIC / PRETOPIC 序号
const getTopicSeqNum = (node: XmlNode): string => {
const parent = editorStore.nodeMap.get(node.id)?.parent
if (!parent) return ''
if (parent.tagName === 'CEP' || parent.tagName === 'TASK') {
const topicSiblings = parent.children.filter((c) => c.tagName === 'TOPIC' || c.tagName === 'PRETOPIC')
const idx = topicSiblings.findIndex((c) => c.id === node.id)
if (idx !== -1) {
return `${idx + 1}. `
}
}
return ''
}
// 提取 TOPIC 的 TITLE 节点
const getTopicTitleNodes = (n: XmlNode): XmlNode[] => {
return n.children.filter((c) => c.tagName === 'TITLE' || c.tagName === 'TITLEC')
}
// 提取 TOPIC 的内容节点
const getTopicContentNodes = (n: XmlNode): XmlNode[] => {
return n.children.filter((c) => c.tagName !== 'TITLE' && c.tagName !== 'TITLEC')
}
// 行内混合文本处理
const getChildNode = (childId: string): XmlNode | undefined => {
return props.node.children.find((c) => c.id === childId)
}
// 文本框值修改同步
const handleTextBlur = (e: FocusEvent) => {
const el = e.target as HTMLElement
const val = el.innerText || ''
if (val !== props.node.textContent) {
editorStore.saveSnapshot()
props.node.textContent = val
}
}
// 子节点内容修改同步 (主要是选项组里的 items)
const handleChildTextBlur = (childId: string, e: FocusEvent) => {
const el = e.target as HTMLElement
const val = el.innerText || ''
const child = props.node.children.find((c) => c.id === childId)
if (child && child.textContent !== val) {
editorStore.saveSnapshot()
child.textContent = val
}
}
// 混合文本片段修改同步
const handleMixedTextBlur = (index: number, e: FocusEvent) => {
const el = e.target as HTMLElement
const val = el.innerText || ''
if (props.node.mixedContent[index].text !== val) {
editorStore.saveSnapshot()
props.node.mixedContent[index].text = val
}
}
// 获取附图的标题
const getGraphicTitle = (): string => {
const titleNode = props.node.children.find((c) => c.tagName === 'TITLE')
return titleNode ? titleNode.textContent || '' : ''
}
const handleGraphicTitleBlur = (e: FocusEvent) => {
const el = e.target as HTMLElement
const val = el.innerText || ''
let titleNode = props.node.children.find((c) => c.tagName === 'TITLE')
if (titleNode && titleNode.textContent !== val) {
editorStore.saveSnapshot()
titleNode.textContent = val
}
}
// 工具和设备文本获取
const getTedText = (n: XmlNode): string => {
const name = n.children.find((c) => c.tagName === 'TOOLNAME')?.textContent || ''
const nbr = n.children.find((c) => c.tagName === 'TOOLNBR')?.textContent || ''
return nbr ? `${name} (${nbr})` : name
}
// 消耗品文本获取
const getConText = (n: XmlNode): string => {
const name = n.children.find((c) => c.tagName === 'CONNAME')?.textContent || ''
const nbr = n.children.find((c) => c.tagName === 'CONNBR')?.textContent || ''
return nbr ? `${name} (Material Ref. ${nbr})` : name
}
// 断路器列表辅助
const hasNonCbDataChildren = (subList: XmlNode): boolean => {
return subList.children.some((c) => c.tagName !== 'CBDATA')
}
const getNonCbDataChildren = (subList: XmlNode): XmlNode[] => {
return subList.children.filter((c) => c.tagName !== 'CBDATA')
}
const getCbValue = (cbData: XmlNode, tagName: string): string => {
const child = cbData.children.find((c) => c.tagName === tagName)
return child ? child.textContent || '' : ''
}
</script>
<style scoped>
.doc-node-wrapper {
transition: all 0.2s ease-in-out;
}
.doc-node-wrapper:hover {
background-color: var(--n-color-hover);
}
</style>
import type { XmlNode } from '@/types/xmlNode'
// 列表节点标签定义
export const ALL_LIST_TAGS = ['LIST1', 'LIST2', 'LIST3', 'UNLIST']
// 表格节点标签定义
export const CALS_TABLE_TAGS = ['TABLE', 'TGROUP']
// 容器节点:直接穿透,不独立成块,而是遍历其子节点
export const TRANSPARENT_TAGS = new Set(['JOBCARD', 'CEP', 'TASK'])
/**
* 虚拟滚动:各类节点的默认估计高度(px)
* 合理的预估可减少布局跳变,提升大文件初始滚动精度
*/
export const ESTIMATED_HEIGHT = 200
/** 按节点类型获取预估高度(用于虚拟列表初始高度计算) */
export const getEstimatedHeight = (tagName: string): number => {
switch (tagName) {
case 'TABLE': return 400 // 表格通常较高
case 'GRAPHIC': return 300 // 图片/图纸
case 'WARNING': return 180 // 警告块
case 'CAUTION': return 180
case 'NOTE': return 150
case 'PRETOPIC': return 160 // 模板段落
case 'UNLIST':
case 'LIST1':
case 'LIST2':
case 'LIST3': return 200 // 列表
case 'PARA':
case 'PARAC': return 80 // 普通段落
case 'SMUC-HEADER': return 120
case 'FINLIST': return 100
default: return ESTIMATED_HEIGHT
}
}
// 编辑器文档块接口
export interface EditorBlock {
id: string
tagName: string
rawNode: XmlNode
}
// 虚拟滚动坐标接口
export interface BlockPosition {
id: string
top: number
bottom: number
height: number
}
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { useEditorStore } from '@/store/editor'
import type { XmlNode } from '@/types/xmlNode'
import {
TRANSPARENT_TAGS,
ESTIMATED_HEIGHT,
getEstimatedHeight,
type EditorBlock,
type BlockPosition
} from '../constants'
/**
* EditorPanel 组件核心逻辑 Hook
* 包含:节点扁平化、虚拟滚动(带精确高度测量 + 内存安全清理)、滚动联动定位
*/
export function useEditorPanel() {
const editorStore = useEditorStore()
// ── 基础状态 ──────────────────────────────────────────────────────────────
const selectedNode = computed(() => editorStore.selectedNode)
const nodePath = computed(() => {
if (!editorStore.selectedNodeId) return []
const path: XmlNode[] = []
let curr = editorStore.nodeMap.get(editorStore.selectedNodeId)
while (curr) {
path.unshift(curr.node)
curr = curr.parent ? editorStore.nodeMap.get(curr.parent.id) : undefined
}
return path
})
const editorTitle = computed(() => {
if (!editorStore.xmlTree) return '文档编辑器'
return '工卡文档 (Jobcard Flow)'
})
// ── 节点扁平化 ────────────────────────────────────────────────────────────
/**
* 递归构建扁平化文档块列表
* JOBCARD / CEP / TASK 为透明容器,穿透递归其子节点
*/
const getEditorBlocks = (node: XmlNode): EditorBlock[] => {
const blocks: EditorBlock[] = []
const walk = (n: XmlNode) => {
if (TRANSPARENT_TAGS.has(n.tagName)) {
n.children.forEach(walk)
} else {
blocks.push({ id: n.id, tagName: n.tagName, rawNode: n })
}
}
walk(node)
return blocks
}
const blocksList = computed<EditorBlock[]>(() => {
if (!editorStore.xmlTree) return []
return getEditorBlocks(editorStore.xmlTree)
})
// ── 虚拟滚动核心 ──────────────────────────────────────────────────────────
// 块实际高度缓存:id → height(px)
const heightsMap = ref<Record<string, number>>({})
// 切换 XML 文件时,重置高度缓存
watch(() => editorStore.xmlTree?.id, () => {
heightsMap.value = {}
})
const positions = computed<BlockPosition[]>(() => {
const list: BlockPosition[] = []
let currentTop = 0
for (const block of blocksList.value) {
// 优先使用 ResizeObserver 测量到的真实高度,否则按节点类型估算
const h = heightsMap.value[block.id] ?? getEstimatedHeight(block.tagName)
list.push({ id: block.id, top: currentTop, bottom: currentTop + h, height: h })
currentTop += h
}
return list
})
const totalHeight = computed(() => {
const pos = positions.value
return pos.length === 0 ? 0 : pos[pos.length - 1].bottom
})
const viewportRef = ref<HTMLElement | null>(null)
const scrollTop = ref(0)
const viewportHeight = ref(600)
// ── 节流滚动处理(rAF)────────────────────────────────────────────────────
let rafId: number | null = null
const handleScroll = (e: Event) => {
if (rafId !== null) return
rafId = requestAnimationFrame(() => {
scrollTop.value = (e.target as HTMLElement).scrollTop
rafId = null
})
}
// ── 上下缓冲 5 块,防止快速滚动白屏 ─────────────────────────────────────
const BUFFER = 5
// 二分查找:可视区域起始索引
const startIndex = computed(() => {
const pos = positions.value
if (pos.length === 0) return 0
let low = 0, high = pos.length - 1
while (low <= high) {
const mid = Math.floor((low + high) / 2)
if (pos[mid].bottom > scrollTop.value) high = mid - 1
else low = mid + 1
}
return Math.max(0, low - BUFFER)
})
// 二分查找:可视区域结束索引
const endIndex = computed(() => {
const pos = positions.value
if (pos.length === 0) return 0
const visibleBottom = scrollTop.value + viewportHeight.value
let low = 0, high = pos.length - 1
while (low <= high) {
const mid = Math.floor((low + high) / 2)
if (pos[mid].top >= visibleBottom) high = mid - 1
else low = mid + 1
}
return Math.min(pos.length, low + BUFFER)
})
const visibleBlocks = computed(() =>
blocksList.value.slice(startIndex.value, endIndex.value)
)
const startOffset = computed(() => {
const pos = positions.value
const idx = startIndex.value
return pos.length === 0 || idx >= pos.length ? 0 : pos[idx].top
})
// ── ResizeObserver:精准测量块高度,内存安全 ─────────────────────────────
// 块 id → DOM 元素映射,用于清理
const blockElMap = new Map<string, Element>()
let resizeObserver: ResizeObserver | null = null
let viewObserver: ResizeObserver | null = null
onMounted(() => {
// 块高度观察器
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const id = (entry.target as HTMLElement).dataset.blockId
if (id) {
const h = entry.contentRect.height
if (h > 0 && heightsMap.value[id] !== h) {
heightsMap.value[id] = h
}
}
}
})
// 视口大小观察器
if (viewportRef.value) {
viewportHeight.value = viewportRef.value.clientHeight
viewObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
viewportHeight.value = entry.contentRect.height
}
})
viewObserver.observe(viewportRef.value)
}
})
onBeforeUnmount(() => {
resizeObserver?.disconnect()
viewObserver?.disconnect()
blockElMap.clear()
if (rafId !== null) cancelAnimationFrame(rafId)
})
/**
* 块 ref 回调:el 非 null 时开始观察,el 为 null(卸载)时停止观察
* 使用 data-block-id 属性传递 id,避免闭包
*/
const setBlockRef = (el: any, id: string) => {
if (el) {
// 新元素挂载
;(el as HTMLElement).dataset.blockId = id
blockElMap.set(id, el)
resizeObserver?.observe(el)
} else {
// 元素从 DOM 卸载
const old = blockElMap.get(id)
if (old) {
resizeObserver?.unobserve(old)
blockElMap.delete(id)
}
}
}
const isAncestorOrSelf = (blockId: string, childId: string): boolean => {
let curr = editorStore.nodeMap.get(childId)
while (curr) {
if (curr.node.id === blockId) return true
curr = curr.parent ? editorStore.nodeMap.get(curr.parent.id) : undefined
}
return false
}
const syncEditorScroll = (newId: string | null, force = false) => {
if (!newId || !editorStore.xmlTree) return
const blockIdx = blocksList.value.findIndex(b => isAncestorOrSelf(b.id, newId))
if (blockIdx === -1) return
nextTick(() => {
if (!viewportRef.value) return
// 1. 先从路径中由下到上找最近的已挂载 DOM 元素
let el: HTMLElement | null = null
const path: XmlNode[] = []
let curr = editorStore.nodeMap.get(newId)
while (curr) {
path.unshift(curr.node)
curr = curr.parent ? editorStore.nodeMap.get(curr.parent.id) : undefined
}
if (path.length > 0) {
for (let i = path.length - 1; i >= 0; i--) {
el = viewportRef.value.querySelector(
`[data-node-id="${path[i].id}"]`
) as HTMLElement | null
if (el) break
}
}
if (el) {
const rect = el.getBoundingClientRect()
const containerRect = viewportRef.value.getBoundingClientRect()
// 已在可视范围内且非强制滚动,不做处理
if (!force && rect.top >= containerRect.top + 20 && rect.bottom <= containerRect.bottom - 20) {
return
}
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
return
}
// 2. DOM 未挂载:先瞬间跳转到估算位置触发虚拟列表挂载
const pos = positions.value[blockIdx]
if (!pos) return
const vHeight = viewportRef.value.clientHeight
viewportRef.value.scrollTo({
top: Math.max(0, pos.top - vHeight / 2 + pos.height / 2),
behavior: 'instant' as ScrollBehavior
})
// 等挂载后再精确定位
setTimeout(() => {
if (!viewportRef.value) return
let targetEl: HTMLElement | null = null
if (path && path.length > 0) {
for (let i = path.length - 1; i >= 0; i--) {
targetEl = viewportRef.value.querySelector(
`[data-node-id="${path[i].id}"]`
) as HTMLElement | null
if (targetEl) break
}
}
targetEl?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 100)
})
}
watch(() => editorStore.selectedNodeId, (newId) => syncEditorScroll(newId, false))
watch(() => editorStore.lastUndoRedoTime, () => syncEditorScroll(editorStore.selectedNodeId, true))
return {
selectedNode,
nodePath,
editorTitle,
viewportRef,
totalHeight,
startOffset,
visibleBlocks,
handleScroll,
setBlockRef
}
}
<template>
<div class="flex-1 flex flex-col min-h-0 bg-transparent">
<template v-if="editorStore.xmlTree">
<!-- 顶部面包屑与属性面板控制 -->
<div
class="px-4 py-2 border-b border-divider bg-fill-2 flex items-center justify-between text-xs shrink-0"
>
<div class="flex items-center space-x-2">
<span class="text-color3 select-none">当前路径:</span>
<n-breadcrumb v-if="nodePath.length > 0">
<n-breadcrumb-item
v-for="n in nodePath"
:key="n.id"
@click="editorStore.setSelectedNodeId(n.id)"
class="cursor-pointer hover:text-primary transition-colors text-color2 font-medium"
>
{{ n.tagName }}
</n-breadcrumb-item>
</n-breadcrumb>
<span v-else class="text-color3 italic">未选择节点</span>
</div>
<!-- 修改属性按钮(触发弹框) -->
<CommonButton
size="tiny"
secondary
:disabled="!selectedNode"
@click="handleEditSelectedNode"
>
<template #icon>
<n-icon>
<settings-outline />
</n-icon>
</template>
修改属性
</CommonButton>
</div>
<!-- 文档编辑区(虚拟滚动容器) -->
<div
ref="viewportRef"
class="flex-1 overflow-y-auto min-h-0 leading-relaxed relative"
@scroll="handleScroll"
>
<!-- 占位撑高,模拟全量内容总高度 -->
<div :style="{ height: totalHeight + 'px', position: 'relative' }">
<!-- 仅渲染可视区块,通过 translateY 定位 -->
<div
:style="{ transform: `translateY(${startOffset}px)` }"
class="absolute top-0 left-0 right-0"
>
<div
v-for="block in visibleBlocks"
:key="block.id"
:ref="(el: any) => setBlockRef(el, block.id)"
class="w-full px-6 py-2 border-b border-divider/30"
>
<DocNodeRenderer :node="block.rawNode" />
</div>
</div>
</div>
</div>
</template>
<template v-else>
<div class="flex-1 flex flex-col items-center justify-center text-color3">
<n-empty size="large" description="请导入或选择文档以开始编辑" />
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { DocumentTextOutline, SettingsOutline } from '@vicons/ionicons5'
import { useEditorStore } from '@/store/editor'
import DocNodeRenderer from '../DocNodeRenderer/index.vue'
import { useEditorPanel } from './functionals'
import { addNodeVisible, addNodeMode, addNodeTargetId, addNodeAllowedTags } from '../NodeTree/functionals'
const themeVars = useThemeVars()
const editorStore = useEditorStore()
const {
selectedNode,
nodePath,
editorTitle,
viewportRef,
totalHeight,
startOffset,
visibleBlocks,
handleScroll,
setBlockRef
} = useEditorPanel()
const handleEditSelectedNode = () => {
if (!selectedNode.value) return
addNodeTargetId.value = selectedNode.value.id
addNodeMode.value = 'edit'
addNodeAllowedTags.value = [selectedNode.value.tagName]
addNodeVisible.value = true
}
</script>
<template>
<div class="editor-toolbar flex flex-wrap items-center justify-between px-4 py-2 border-b border-divider bg-fill-2 gap-3 select-none">
<!-- 左侧:内容插入操作区 -->
<div class="flex flex-wrap items-center gap-2">
<!-- 插入位置设置 -->
<div class="flex items-center gap-1.5 bg-fill-3 px-2.5 py-1 rounded-md border border-divider text-xs">
<span class="font-medium text-color3">插入</span>
<span class="font-semibold" :class="insertBelow ? 'text-primary' : 'text-color2'">
{{ insertBelow ? '下方' : '上方' }}
</span>
<n-switch v-model:value="insertBelow" size="small" />
</div>
<n-divider vertical class="!mx-0" />
<!-- 插入元素按钮组 -->
<div class="flex flex-wrap items-center gap-1">
<CommonButton
v-for="btn in greenButtons"
:key="btn.tag"
type="primary"
size="small"
class="insert-btn"
@click="handleInsert(btn.tag)"
>
<template #icon>
<n-icon><component :is="btn.icon" /></n-icon>
</template>
{{ btn.label.replace('插入', '') }}
</CommonButton>
</div>
</div>
<!-- 右侧:全局工具与管理操作区 -->
<div class="flex flex-wrap items-center gap-1.5">
<!-- 翻译辅助组 -->
<div class="flex items-center gap-1 bg-fill-3 px-1 py-0.5 rounded-md border border-divider">
<CommonButton size="small" quaternary class="util-btn" @click="handleTranslate('batch')">
<template #icon>
<n-icon><language-outline /></n-icon>
</template>
批量翻译
</CommonButton>
<CommonButton size="small" quaternary class="util-btn" @click="handleTranslate('extract')">
<template #icon>
<n-icon><download-outline /></n-icon>
</template>
提取翻译
</CommonButton>
<CommonButton size="small" quaternary class="util-btn" @click="handleTranslate('search')">
<template #icon>
<n-icon><search-outline /></n-icon>
</template>
搜索翻译
</CommonButton>
</div>
<n-divider vertical class="!mx-0" />
<!-- XML文件管理 -->
<CommonButton size="small" secondary class="xml-btn" :disabled="isUploading" :loading="isUploading" @click="triggerUpload">
<template #icon>
<n-icon><cloud-upload-outline /></n-icon>
</template>
{{ isUploading ? '解析中…' : '导入 XML' }}
</CommonButton>
<input type="file" ref="fileInputRef" style="display: none" accept=".xml" @change="handleFileUpload" />
<CommonButton size="small" secondary class="xml-btn" @click="emit('export')">
<template #icon>
<n-icon><cloud-download-outline /></n-icon>
</template>
导出 XML
</CommonButton>
<CommonButton size="small" type="primary" class="preview-btn" @click="emit('preview')">
<template #icon>
<n-icon><eye-outline /></n-icon>
</template>
预览工卡
</CommonButton>
<n-divider vertical class="!mx-0" />
<!-- 撤销 / 重做 -->
<n-tooltip trigger="hover">
<template #trigger>
<CommonButton size="small" quaternary :disabled="!canUndo" @click="editorStore.undo()">
<template #icon>
<n-icon><arrow-undo-outline /></n-icon>
</template>
后退
</CommonButton>
</template>
撤销 (Ctrl+Z)
</n-tooltip>
<n-tooltip trigger="hover">
<template #trigger>
<CommonButton size="small" quaternary :disabled="!canRedo" @click="editorStore.redo()">
<template #icon>
<n-icon><arrow-redo-outline /></n-icon>
</template>
前进
</CommonButton>
</template>
重做 (Ctrl+Y)
</n-tooltip>
<n-divider vertical class="!mx-0" />
<!-- 主题切换 -->
<n-tooltip trigger="hover">
<template #trigger>
<CommonButton size="small" quaternary circle @click="appStore.isDark = !appStore.isDark">
<template #icon>
<n-icon>
<sunny-outline v-if="appStore.isDark" />
<moon-outline v-else />
</n-icon>
</template>
</CommonButton>
</template>
{{ appStore.isDark ? '切换亮色主题' : '切换暗色主题' }}
</n-tooltip>
<!-- 偏好设置 -->
<SettingsDrawer />
</div>
</div>
</template>
<script setup lang="ts">
import {
ImageOutline,
GridOutline,
DocumentTextOutline,
ListOutline,
CalendarOutline,
CalculatorOutline,
CheckboxOutline,
CreateOutline,
LanguageOutline,
DownloadOutline,
SearchOutline,
CloudUploadOutline,
CloudDownloadOutline,
EyeOutline,
ArrowUndoOutline,
ArrowRedoOutline,
SunnyOutline,
MoonOutline
} from '@vicons/ionicons5'
import { useEditorStore } from '@/store/editor'
import { useAppStore } from '@/store/app/index'
import SettingsDrawer from '@/layouts/components/SettingsDrawer.vue'
import { parseXmlToTreeAsync } from '@/utils/xmlParser'
const emit = defineEmits(['save', 'validate', 'export', 'preview'])
const editorStore = useEditorStore()
const appStore = useAppStore()
const insertBelow = ref(true)
const fileInputRef = ref<HTMLInputElement | null>(null)
const canUndo = computed(() => editorStore.undoStack.length > 0)
const canRedo = computed(() => editorStore.redoStack.length > 0)
const greenButtons = [
{ label: '插入图片', tag: 'GRAPHIC', icon: ImageOutline },
{ label: '插入表格', tag: 'TABLE', icon: GridOutline },
{ label: '插入模板', tag: 'PRETOPIC', icon: DocumentTextOutline },
{ label: '插入记录项', tag: 'RECORD-LINE', icon: ListOutline },
{ label: '插入校验日期', tag: 'DATE', icon: CalendarOutline },
{ label: '插入单位记录项', tag: 'UNIT-RECORD', icon: CalculatorOutline },
{ label: '插入选项组', tag: 'SELECTION', icon: CheckboxOutline },
{ label: '插入签字点', tag: 'SIGNOFF', icon: CreateOutline }
]
const handleInsert = (tag: string) => editorStore.insertNode(tag, insertBelow.value)
const handleTranslate = (type: 'batch' | 'extract' | 'search') => {
const labelMap = { batch: '批量翻译', extract: '提取翻译', search: '搜索翻译' }
window.$message.info(`已触发 ${labelMap[type]} 功能,自动匹配双语对照。`)
}
const triggerUpload = () => fileInputRef.value?.click()
const isUploading = ref(false)
const handleFileUpload = async (e: Event) => {
const target = e.target as HTMLInputElement
const file = target.files?.[0]
if (!file) return
target.value = ''
const text = await file.text()
isUploading.value = true
window.$message.info(`正在解析 ${file.name}${(file.size / 1024).toFixed(0)} KB),请稍候…`)
try {
const tree = await parseXmlToTreeAsync(text)
editorStore.setXmlTree(tree)
window.$message.success(`${file.name} 解析成功!`)
} catch (err: any) {
window.$message.error('XML 解析失败: ' + err.message)
} finally {
isUploading.value = false
}
}
</script>
<style scoped>
/* 消除工具栏容器本身的 focus outline */
.editor-toolbar {
outline: none;
}
/* 插入按钮:深色文字 + 更紧凑的圆角 */
.editor-toolbar :deep(.insert-btn.n-button) {
--n-border-radius: 6px;
font-weight: 600;
font-size: 12px;
}
/* 翻译辅助按钮:去掉 margin,更紧凑 */
.editor-toolbar :deep(.util-btn.n-button) {
--n-border-radius: 6px;
font-size: 12px;
}
/* XML 管理按钮 */
.editor-toolbar :deep(.xml-btn.n-button) {
--n-border-radius: 6px;
font-size: 12px;
font-weight: 600;
}
/* 预览工卡按钮:突出主色 */
.editor-toolbar :deep(.preview-btn.n-button) {
--n-border-radius: 6px;
font-size: 12px;
font-weight: 700;
}
/* 全局清除 n-button 间的默认 margin-right */
.editor-toolbar :deep(.n-button + .n-button) {
margin-right: 0 !important;
}
/* n-divider vertical 高度统一 */
.editor-toolbar :deep(.n-divider--vertical) {
height: 20px;
margin: 0 4px;
}
</style>
...@@ -12,14 +12,11 @@ export function useListEditor() { ...@@ -12,14 +12,11 @@ export function useListEditor() {
/** /**
* 判断当前节点是否为列表容器 * 判断当前节点是否为列表容器
*/ */
function isListContainer(tagName: string): boolean { const isListContainer = (tagName: string): boolean => {
return ALL_LIST_TAGS.includes(tagName) return ALL_LIST_TAGS.includes(tagName)
} }
/** const getItemTagName = (containerTag: string): string => {
* 根据容器标签获取相对应的列表项子标签
*/
function getItemTagName(containerTag: string): string {
switch (containerTag) { switch (containerTag) {
case 'LIST1': return 'L1ITEM' case 'LIST1': return 'L1ITEM'
case 'LIST2': return 'L2ITEM' case 'LIST2': return 'L2ITEM'
...@@ -29,10 +26,7 @@ export function useListEditor() { ...@@ -29,10 +26,7 @@ export function useListEditor() {
} }
} }
/** const parseListItems = (node: XmlNode): ListItemModel[] => {
* 提取列表容器内的所有子列表项
*/
function parseListItems(node: XmlNode): ListItemModel[] {
if (!isListContainer(node.tagName)) return [] if (!isListContainer(node.tagName)) return []
const itemTagName = getItemTagName(node.tagName) const itemTagName = getItemTagName(node.tagName)
...@@ -53,10 +47,7 @@ export function useListEditor() { ...@@ -53,10 +47,7 @@ export function useListEditor() {
}) })
} }
/** const updateItemText = (node: XmlNode, itemId: string, text: string): void => {
* 修改具体列表项的内容,实时同步到列表项子树中
*/
function updateItemText(node: XmlNode, itemId: string, text: string): void {
const itemNode = node.children.find(c => c.id === itemId) const itemNode = node.children.find(c => c.id === itemId)
if (!itemNode) return if (!itemNode) return
...@@ -73,10 +64,7 @@ export function useListEditor() { ...@@ -73,10 +64,7 @@ export function useListEditor() {
store.triggerSync() store.triggerSync()
} }
/** const addItem = (node: XmlNode): void => {
* 添加一个新的列表项
*/
function addItem(node: XmlNode): void {
const itemTag = getItemTagName(node.tagName) const itemTag = getItemTagName(node.tagName)
const itemId = crypto.randomUUID() const itemId = crypto.randomUUID()
const newItem: XmlNode = { const newItem: XmlNode = {
...@@ -106,10 +94,7 @@ export function useListEditor() { ...@@ -106,10 +94,7 @@ export function useListEditor() {
store.triggerSync() store.triggerSync()
} }
/** const deleteItem = (node: XmlNode, itemId: string): void => {
* 删除一个列表项
*/
function deleteItem(node: XmlNode, itemId: string): void {
const idx = node.children.findIndex(c => c.id === itemId) const idx = node.children.findIndex(c => c.id === itemId)
if (idx !== -1) { if (idx !== -1) {
node.children.splice(idx, 1) node.children.splice(idx, 1)
...@@ -117,10 +102,7 @@ export function useListEditor() { ...@@ -117,10 +102,7 @@ export function useListEditor() {
} }
} }
/** const moveItem = (node: XmlNode, itemId: string, direction: 'up' | 'down'): void => {
* 移动列表项顺序
*/
function moveItem(node: XmlNode, itemId: string, direction: 'up' | 'down'): void {
const idx = node.children.findIndex(c => c.id === itemId) const idx = node.children.findIndex(c => c.id === itemId)
if (idx === -1) return if (idx === -1) return
......
import type { FormInst } from 'naive-ui'
import { getElementAttributes, createDefaultAttributes, isTextOnlyElement, isMixedContentElement } from '@/utils/dtdManager'
import { useEditorStore } from '@/store/editor'
import type { XmlNode, DtdAttribute } from '@/types/xmlNode'
import type { AttrDef } from '../../../constants'
import { addNodeVisible, addNodeMode, addNodeTargetId, addNodeAllowedTags } from '../../../functionals'
/**
* AddNodeModal 逻辑 Hook
*/
export function useAddNodeModal(formRef: Ref<FormInst | null>) {
const store = useEditorStore()
const saving = ref(false)
// 表单数据
const form = reactive<{
tagName: string
attrs: Record<string, string>
textContent: string
}>({
tagName: '',
attrs: {},
textContent: ''
})
const showTextContentField = computed(() => {
if (!form.tagName) return false
return isTextOnlyElement(form.tagName) || isMixedContentElement(form.tagName)
})
const rules = {
tagName: [{ required: true, message: '请选择标签', trigger: 'change' }]
}
// 弹窗标题
const modalTitle = computed(() => {
const map: Record<string, string> = {
child: '添加子节点',
before: '插入到上方',
after: '插入到下方',
edit: '编辑节点'
}
const nodeId = addNodeTargetId.value
const node = store.nodeMap.get(nodeId)?.node ?? null
if (addNodeMode.value === 'edit') {
return node ? `编辑节点 <${node.tagName}>` : '编辑节点'
}
return node ? `${node.tagName} ${map[addNodeMode.value]}` : map[addNodeMode.value]
})
// 标签下拉选项
const tagOptions = computed(() => {
if (addNodeMode.value === 'edit') {
return form.tagName ? [{ label: form.tagName, value: form.tagName }] : []
}
return addNodeAllowedTags.value.map((tag) => ({ label: tag, value: tag }))
})
// 属性定义列表
const attributeDefs = computed<AttrDef[]>(() => {
if (!form.tagName) return []
const defs = getElementAttributes(form.tagName)
return Object.entries(defs).map(([name, def]: [string, DtdAttribute]) => ({
name,
typeDefinition: def.typeDefinition,
enumValues: def.enumValues,
required: def.requirement === 'REQUIRED',
defaultValue: def.defaultValue
}))
})
// 切换标签时重置属性表单,填入默认值
const onTagChange = (tag: string) => {
const defaults = createDefaultAttributes(tag)
form.attrs = { ...defaults }
}
// 重置表单,在弹窗关闭后自动被 v-model 关联或事件重置
watch(addNodeVisible, (visible) => {
if (visible) {
if (addNodeMode.value === 'edit') {
const nodeId = addNodeTargetId.value
const node = store.nodeMap.get(nodeId)?.node ?? null
if (node) {
form.tagName = node.tagName
form.textContent = node.textContent || ''
const defs = getElementAttributes(node.tagName)
const attrs: Record<string, string> = {}
for (const name of Object.keys(defs)) {
attrs[name] = node.attributes[name] || ''
}
form.attrs = attrs
}
}
} else {
form.tagName = ''
form.attrs = {}
form.textContent = ''
}
})
// 确认编辑/插入
const handleConfirm = async () => {
try {
await formRef.value?.validate()
} catch {
return
}
saving.value = true
try {
const tree = store.xmlTree
if (!tree) return
const targetNode = store.nodeMap.get(addNodeTargetId.value)?.node
if (!targetNode) return
store.saveSnapshot()
if (addNodeMode.value === 'edit') {
targetNode.attributes = { ...form.attrs }
targetNode.textContent = form.textContent
// 如果当前编辑的正是选中的节点,强制更新 store 的选中节点引用以重新渲染视图
if (store.selectedNodeId === targetNode.id) {
store.setSelectedNodeId(targetNode.id)
}
window.$message?.success(`成功更新节点 <${targetNode.tagName}> 的属性与内容`)
addNodeVisible.value = false
} else {
const newId = crypto.randomUUID()
const newNode: XmlNode = {
id: newId,
tagName: form.tagName,
attributes: { ...form.attrs },
children: [],
textContent: form.textContent,
mixedContent: [],
parentId: null
}
if (addNodeMode.value === 'child') {
newNode.parentId = targetNode.id
targetNode.children.push(newNode)
} else {
const parent = store.nodeMap.get(addNodeTargetId.value)?.parent
if (!parent) return
newNode.parentId = parent.id
const idx = parent.children.findIndex((c) => c.id === addNodeTargetId.value)
const insertIdx = addNodeMode.value === 'before' ? idx : idx + 1
parent.children.splice(insertIdx, 0, newNode)
}
store.setSelectedNodeId(newId)
window.$message?.success(`成功插入节点 <${form.tagName}>`)
addNodeVisible.value = false
}
} finally {
saving.value = false
}
}
return {
form,
rules,
saving,
modalTitle,
tagOptions,
attributeDefs,
showTextContentField,
onTagChange,
handleConfirm
}
}
<template>
<CommonModal
v-model="addNodeVisible"
:title="modalTitle"
:width="520"
:loading="saving"
@confirm="handleConfirm"
>
<n-form ref="formRef" :model="form" :rules="rules" label-placement="top" require-mark-placement="right-hanging">
<!-- 标签选择 -->
<n-form-item label="标签" path="tagName">
<n-select
v-model:value="form.tagName"
:options="tagOptions"
placeholder="请选择"
filterable
:disabled="addNodeMode === 'edit'"
@update:value="onTagChange"
/>
</n-form-item>
<!-- 动态属性表单 -->
<template v-if="form.tagName && attributeDefs.length > 0">
<n-divider class="!my-2">
<span class="text-xs text-color3">属性配置</span>
</n-divider>
<n-form-item
v-for="attr in attributeDefs"
:key="attr.name"
:label="attr.name"
:path="`attrs.${attr.name}`"
>
<!-- 枚举类型:select -->
<n-select
v-if="attr.enumValues && attr.enumValues.length > 0"
v-model:value="form.attrs[attr.name]"
:options="attr.enumValues.map(v => ({ label: v, value: v }))"
placeholder="请选择"
/>
<!-- 普通文本 -->
<n-input
v-else
v-model:value="form.attrs[attr.name]"
placeholder="请输入"
/>
<!-- 属性说明 -->
<template v-if="attr.typeDefinition" #feedback>
<span class="text-[10px] text-color3">{{ attr.typeDefinition }}</span>
</template>
</n-form-item>
</template>
<!-- 文本内容编辑 (PCDATA) -->
<template v-if="showTextContentField">
<n-divider class="!my-2">
<span class="text-xs text-color3">文本内容 (PCDATA)</span>
</n-divider>
<n-form-item label="内容" path="textContent">
<n-input
v-model:value="form.textContent"
type="textarea"
placeholder="请输入文本内容"
:autosize="{ minRows: 2, maxRows: 6 }"
/>
</n-form-item>
</template>
</n-form>
</CommonModal>
</template>
<script setup lang="ts">
import type { FormInst } from 'naive-ui'
import { addNodeVisible, addNodeMode } from '../../functionals'
import { useAddNodeModal } from './functionals'
const formRef = ref<FormInst | null>(null)
const {
form,
rules,
saving,
modalTitle,
tagOptions,
attributeDefs,
showTextContentField,
onTagChange,
handleConfirm
} = useAddNodeModal(formRef)
</script>
import { checkRuleData } from '../../../functionals'
/**
* CheckRuleModal 逻辑 Hook
*/
export function useCheckRuleModal() {
// 语法高亮:关键字 + 标签名(使用全局主题样式,禁止硬编码 Hex)
const highlightedRule = computed(() => {
const text = checkRuleData.value.humanReadable || checkRuleData.value.rawModel
return text
.replace(/\|/g, '<span class="text-primary font-bold">|</span>')
.replace(/[?*+]/g, '<span class="text-warning">$&</span>')
.replace(/[()]/g, '<span class="text-color3">$&</span>')
.replace(/([A-Z][A-Z0-9\-]*)/g, '<span class="text-success font-semibold">$1</span>')
.replace(/#PCDATA/g, '<span class="text-danger">#PCDATA</span>')
})
return {
highlightedRule
}
}
<template>
<CommonModal v-model="checkRuleVisible" :title="`查看节点规则:${checkRuleData.nodeName}`" :width="640" :show-confirm="false" cancel-text="关闭">
<!-- 原始内容模型 -->
<div class="px-3 py-2 rounded bg-fill-2 border border-divider font-mono text-xs text-color2 leading-relaxed break-all">
{{ checkRuleData.rawModel }}
</div>
<!-- 人类可读的格式化规则 -->
<div class="rounded overflow-hidden border border-divider">
<div class="bg-fill-3 p-4 font-mono text-xs text-color1 leading-loose whitespace-pre">
<span v-html="highlightedRule"></span>
</div>
</div>
</CommonModal>
</template>
<script setup lang="ts">
import { checkRuleVisible, checkRuleData } from '../../functionals'
import { useCheckRuleModal } from './functionals'
const { highlightedRule } = useCheckRuleModal()
</script>
// 列表/文档型叶子节点标签判定
export const DOCUMENT_LIKE_TAGS = ['PARA', 'PARAC', 'TITLE', 'TITLEC', 'WARNING', 'CAUTION', 'NOTE']
// 查看规则弹窗数据定义
export interface CheckRuleData {
nodeName: string
rawModel: string
humanReadable: string
}
// 插入节点模式类型
export type InsertMode = 'child' | 'before' | 'after' | 'edit'
// 属性定义接口 (用于 AddNodeModal.vue)
export interface AttrDef {
name: string
typeDefinition: string
enumValues: string[] | null
required: boolean
defaultValue: string | null
}
import { NIcon } from 'naive-ui'
import type { DropdownOption } from 'naive-ui'
import {
AddCircleOutline,
CopyOutline,
TrashOutline,
CreateOutline,
ArrowUpOutline,
ArrowDownOutline,
EyeOutline,
ClipboardOutline,
SaveOutline,
BuildOutline
} from '@vicons/ionicons5'
import { useEditorStore } from '@/store/editor'
import type { XmlNode } from '@/types/xmlNode'
import { getAllowedChildren, getInsertableChildren, canDeleteChild, getElementRule } from '@/utils/dtdManager'
import type { CheckRuleData, InsertMode } from '../constants'
// ══════════════════════════════════════════════════════════
// 全局共享状态:查看规则弹窗
// ══════════════════════════════════════════════════════════
export const checkRuleVisible = ref(false)
export const checkRuleData = ref<CheckRuleData>({
nodeName: '',
rawModel: '',
humanReadable: ''
})
// ══════════════════════════════════════════════════════════
// 全局共享状态:添加子节点 / 插入兄弟节点弹窗
// ══════════════════════════════════════════════════════════
export const addNodeVisible = ref(false)
export const addNodeMode = ref<InsertMode>('child')
export const addNodeTargetId = ref<string>('')
export const addNodeAllowedTags = ref<string[]>([])
// 复制节点缓存
export const copyNodeCache = ref<XmlNode | null>(null)
// ══════════════════════════════════════════════════════════
// Helper:渲染图标
// ══════════════════════════════════════════════════════════
const icon = (component: any) => () => h(NIcon, null, { default: () => h(component) })
/**
* NodeTree 组件核心逻辑 Hook
*/
export function useNodeTree() {
const store = useEditorStore()
// ── 生成右键下拉菜单选项 ──────────────────────────────
const getDropdownOptions = (nodeId: string): DropdownOption[] => {
const tree = store.xmlTree
if (!tree) return []
const item = store.nodeMap.get(nodeId)
if (!item) return []
const { node, parent } = item
const options: DropdownOption[] = []
// 查看规则
options.push({
label: '查看规则',
key: 'checkRule',
icon: icon(EyeOutline)
})
// 编辑节点(属性)
options.push({
label: '编辑节点',
key: 'editNode',
icon: icon(CreateOutline)
})
// 复制节点
options.push({
label: '复制节点',
key: 'copyNode',
icon: icon(CopyOutline)
})
// 粘贴节点(有缓存时显示)
if (copyNodeCache.value) {
const pasteChildren: DropdownOption[] = [
{ label: '粘贴到上方', key: 'pasteAbove', icon: icon(ClipboardOutline) },
{ label: '粘贴到下方', key: 'pasteBelow', icon: icon(ClipboardOutline) },
{ label: '粘贴到内部', key: 'pasteInside', icon: icon(ClipboardOutline) }
]
options.push({
label: '粘贴节点',
key: 'pasteNode',
icon: icon(ClipboardOutline),
children: pasteChildren
})
}
// 删除节点(受 DTD 约束)
if (parent) {
const childCount = parent.children.filter((c) => c.tagName === node.tagName).length
const deletable = canDeleteChild(parent.tagName, node.tagName, childCount)
options.push({
label: deletable ? '删除节点' : '删除节点 (受 DTD 约束)',
key: 'deleteNode',
icon: icon(TrashOutline),
disabled: !deletable
})
}
// 编辑结构子菜单
const structureChildren: DropdownOption[] = []
// 添加子节点
const allowedChild = getAllowedChildren(node.tagName)
if (allowedChild.length > 0) {
structureChildren.push({
label: '添加子节点',
key: 'addChildNode',
icon: icon(AddCircleOutline)
})
}
// 插入到上方(兄弟节点前)
if (parent) {
const insertable = getInsertableChildren(
parent.tagName,
parent.children.map((c) => c.tagName)
)
if (insertable.length > 0) {
structureChildren.push({
label: '插入到上方',
key: 'insertBefore',
icon: icon(ArrowUpOutline)
})
structureChildren.push({
label: '插入到下方',
key: 'insertAfter',
icon: icon(ArrowDownOutline)
})
}
}
if (structureChildren.length > 0) {
options.push({
label: '编辑结构',
key: 'editStructure',
icon: icon(BuildOutline),
children: structureChildren
})
}
// 保存为模板
options.push({
label: '保存为模板',
key: 'saveAsTemplate',
icon: icon(SaveOutline)
})
return options
}
// ── 处理菜单动作 ──────────────────────────────────────
const handleDropdownAction = async (key: string, nodeId: string): Promise<void> => {
const tree = store.xmlTree
if (!tree) return
const item = store.nodeMap.get(nodeId)
if (!item) return
const { node, parent } = item
switch (key) {
// ── 查看规则 ──
case 'checkRule': {
const rule = getElementRule(node.tagName)
checkRuleData.value = {
nodeName: node.tagName,
rawModel: rule?.contentModel.raw || '(#PCDATA)',
humanReadable: rule?.contentModel.humanReadable || ''
}
checkRuleVisible.value = true
break
}
// ── 编辑节点:打开属性编辑弹窗 ──
case 'editNode': {
addNodeTargetId.value = nodeId
addNodeMode.value = 'edit'
addNodeAllowedTags.value = [node.tagName]
addNodeVisible.value = true
break
}
// ── 复制节点 ──
case 'copyNode': {
copyNodeCache.value = JSON.parse(JSON.stringify(node)) // 深拷贝
window.$message?.success('节点已复制')
break
}
// ── 粘贴到上方 ──
case 'pasteAbove': {
if (!copyNodeCache.value || !parent) break
const cloned = deepCloneWithNewIds(copyNodeCache.value, parent.id)
const idx = parent.children.findIndex((c) => c.id === nodeId)
store.saveSnapshot()
parent.children.splice(idx, 0, cloned)
store.setSelectedNodeId(cloned.id)
window.$message?.success('粘贴成功')
break
}
// ── 粘贴到下方 ──
case 'pasteBelow': {
if (!copyNodeCache.value || !parent) break
const cloned = deepCloneWithNewIds(copyNodeCache.value, parent.id)
const idx = parent.children.findIndex((c) => c.id === nodeId)
store.saveSnapshot()
parent.children.splice(idx + 1, 0, cloned)
store.setSelectedNodeId(cloned.id)
window.$message?.success('粘贴成功')
break
}
// ── 粘贴到内部 ──
case 'pasteInside': {
if (!copyNodeCache.value) break
const cloned = deepCloneWithNewIds(copyNodeCache.value, nodeId)
store.saveSnapshot()
node.children.push(cloned)
store.setSelectedNodeId(cloned.id)
window.$message?.success('粘贴成功')
break
}
// ── 删除节点 ──
case 'deleteNode': {
if (nodeId === tree.id) {
window.$message?.warning('不能删除根节点')
break
}
try {
await window.$dialog.warning({
title: '确认删除',
content: `确定要删除节点 <${node.tagName}> 吗?该操作将连带删除其所有子节点,且不可撤销!`
})
store.deleteSelectedNode()
window.$message?.success('删除成功')
} catch {
// 取消
}
break
}
// ── 添加子节点 ──
case 'addChildNode': {
const allowed = getInsertableChildren(
node.tagName,
node.children.map((c) => c.tagName)
)
addNodeTargetId.value = nodeId
addNodeMode.value = 'child'
addNodeAllowedTags.value = allowed
addNodeVisible.value = true
break
}
// ── 插入到上方(兄弟,前)──
case 'insertBefore': {
if (!parent) break
const insertable = getInsertableChildren(
parent.tagName,
parent.children.map((c) => c.tagName)
)
addNodeTargetId.value = nodeId
addNodeMode.value = 'before'
addNodeAllowedTags.value = insertable
addNodeVisible.value = true
break
}
// ── 插入到下方(兄弟,后)──
case 'insertAfter': {
if (!parent) break
const insertable = getInsertableChildren(
parent.tagName,
parent.children.map((c) => c.tagName)
)
addNodeTargetId.value = nodeId
addNodeMode.value = 'after'
addNodeAllowedTags.value = insertable
addNodeVisible.value = true
break
}
// ── 保存为模板(暂提示)──
case 'saveAsTemplate': {
window.$message?.info('保存为模板功能开发中')
break
}
}
}
const deepCloneWithNewIds = (node: XmlNode, newParentId: string | null): XmlNode => {
const newId = crypto.randomUUID()
const cloned: XmlNode = {
...node,
id: newId,
parentId: newParentId,
attributes: { ...node.attributes },
mixedContent: node.mixedContent.map((m) => ({ ...m })),
children: node.children.map((child) => deepCloneWithNewIds(child, newId))
}
return cloned
}
return {
getDropdownOptions,
handleDropdownAction
}
}
<template>
<div class="flex flex-col h-full border-r border-divider relative bg-card">
<!-- 搜索过滤 -->
<div class="p-3 border-b border-divider">
<n-input v-model:value="pattern" placeholder="搜索节点名称..." size="small">
<template #prefix>
<n-icon><search-outline /></n-icon>
</template>
</n-input>
</div>
<!-- 虚拟滚动树组件容器 -->
<div
ref="viewportRef"
class="flex-1 overflow-y-auto p-2 relative select-none virtual-tree-container"
@scroll="handleScroll"
>
<div v-if="flatList.length > 0" :style="{ height: totalHeight + 'px', position: 'relative' }">
<!-- 背景连接线层 - 绘制连续的垂直虚线 -->
<div class="tree-lines-layer">
<div
v-for="line in visibleVerticalLines"
:key="line.key"
class="tree-vertical-line"
:style="{
left: `${line.left}px`,
top: `${line.top}px`,
height: `${line.height}px`
}"
></div>
</div>
<!-- 可见列表节点 -->
<div
:style="{ transform: `translateY(${startOffset}px)` }"
class="absolute top-0 left-0 right-0 space-y-0.5"
>
<div
v-for="item in visibleItems"
:key="item.id"
class="flex items-center h-[32px] px-2 rounded cursor-pointer transition-colors group/row tree-node-content"
:class="[
editorStore.selectedNodeId === item.id
? 'bg-primary text-white tree-node-selected'
: 'hover:bg-fill-3 text-color2'
]"
:style="{
paddingLeft: (item.depth * 20 + 8) + 'px',
'--tree-level': item.depth
}"
@click="handleSelect(item.id)"
@contextmenu.prevent="(e) => handleContextMenu(e, item)"
>
<!-- Switcher (展开/折叠 减号/加号) -->
<div
v-if="item.hasChildren"
class="w-4 h-4 flex items-center justify-center mr-1 text-color3 hover:text-color1 cursor-pointer transition-colors z-10"
:class="[
editorStore.selectedNodeId === item.id ? 'text-white/80 hover:text-white' : 'text-primary'
]"
@click.stop="toggleExpand(item.id)"
>
<!-- 展开状态:减号 -->
<svg v-if="item.isExpanded" class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor">
<path d="M3 8h10v1H3V8z" />
</svg>
<!-- 折叠状态:加号 -->
<svg v-else class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" />
</svg>
</div>
<!-- 占位符 (无子节点时填充宽度以便对齐) -->
<div v-else class="w-4 h-4 mr-1"></div>
<!-- Icon -->
<div class="mr-1.5 flex items-center justify-center shrink-0 z-10" :class="[editorStore.selectedNodeId === item.id ? 'text-white' : 'text-primary']">
<n-icon size="16">
<component :is="getNodeIcon(item)" />
</n-icon>
</div>
<!-- Label & Subtitle -->
<div class="flex-1 min-w-0 flex items-center space-x-1 z-10">
<!-- 节点名称高亮 -->
<span
class="font-bold text-sm truncate"
v-html="highlightText(item.tagName, pattern)"
></span>
<!-- 子标题高亮 -->
<span
v-if="item.subtitle"
class="text-xs italic truncate"
:class="[editorStore.selectedNodeId === item.id ? 'text-white/70' : 'text-color3']"
v-html="highlightText(item.subtitle, pattern)"
></span>
</div>
</div>
</div>
</div>
<div v-else class="h-full flex items-center justify-center text-color3">
<n-empty description="暂无节点数据" />
</div>
</div>
<!-- 右键下拉菜单 -->
<n-dropdown
trigger="manual"
placement="bottom-start"
:show="showDropdown"
:options="dropdownOptions"
:x="dropdownX"
:y="dropdownY"
@clickoutside="showDropdown = false"
@select="handleDropdownSelect"
/>
<!-- 查看规则弹窗 -->
<CheckRuleModal />
<!-- 添加/插入节点弹窗 -->
<AddNodeModal />
</div>
</template>
<script setup lang="ts">
import type { DropdownOption } from 'naive-ui'
import { NIcon } from 'naive-ui'
import {
SearchOutline,
CodeWorkingOutline,
DocumentTextOutline,
FolderOpenOutline
} from '@vicons/ionicons5'
import { useEditorStore } from '@/store/editor'
import { useNodeTree } from './functionals'
import { DOCUMENT_LIKE_TAGS } from './constants'
import type { XmlNode } from '@/types/xmlNode'
import CheckRuleModal from './components/CheckRuleModal/index.vue'
import AddNodeModal from './components/AddNodeModal/index.vue'
const props = defineProps<{
expandedKeys: string[]
}>()
const emit = defineEmits(['update:expandedKeys'])
const editorStore = useEditorStore()
const { getDropdownOptions, handleDropdownAction } = useNodeTree()
const pattern = ref('')
const ITEM_HEIGHT = 32
const viewportRef = ref<HTMLElement | null>(null)
const scrollTop = ref(0)
const viewportHeight = ref(400)
// 下拉菜单状态
const showDropdown = ref(false)
const dropdownX = ref(0)
const dropdownY = ref(0)
const contextNodeId = ref<string | null>(null)
// 内部管理的展开状态 Set
const expandedKeys = ref<Set<string>>(new Set())
// 将外部传入的展开状态同步到 Set
watch(() => props.expandedKeys, (keys) => {
expandedKeys.value = new Set(keys)
}, { deep: true })
// 辅助:获取所有有子节点的节点 ID
const collectAllExpandableKeys = (node: XmlNode): string[] => {
const keys: string[] = []
const walk = (n: XmlNode) => {
if (n.children && n.children.length > 0) {
keys.push(n.id)
n.children.forEach(walk)
}
}
walk(node)
return keys
}
// 默认全展开(仅加载新文档或文档 ID 发生改变时执行)
watch(() => editorStore.xmlTree, (newVal, oldVal) => {
if (newVal) {
if (!oldVal || newVal.id !== oldVal.id) {
const keys = collectAllExpandableKeys(newVal)
expandedKeys.value = new Set(keys)
emit('update:expandedKeys', keys)
}
}
}, { immediate: true })
// 监听滚动
const handleScroll = (e: Event) => {
const target = e.target as HTMLElement
scrollTop.value = target.scrollTop
}
// 监听 resize 或初始化高度
onMounted(() => {
if (viewportRef.value) {
viewportHeight.value = viewportRef.value.clientHeight
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
viewportHeight.value = entry.contentRect.height
}
})
observer.observe(viewportRef.value)
}
})
// 搜索匹配检查
const matchNode = (node: XmlNode, search: string): boolean => {
if (!search) return true
const term = search.toLowerCase()
if (node.tagName.toLowerCase().includes(term)) return true
if (node.attributes.ID && node.attributes.ID.toLowerCase().includes(term)) return true
if (node.attributes.EFFECT && node.attributes.EFFECT.toLowerCase().includes(term)) return true
if (node.textContent && node.textContent.toLowerCase().includes(term)) return true
return node.children.some(child => matchNode(child, search))
}
interface FlatNode {
id: string
tagName: string
subtitle: string
depth: number
hasChildren: boolean
isExpanded: boolean
rawNode: XmlNode
}
// 递归构建扁平列表
const buildFlatList = (node: XmlNode, depth = 0, search = ''): FlatNode[] => {
if (search && !matchNode(node, search)) {
return []
}
const list: FlatNode[] = []
const hasChildren = node.children && node.children.length > 0
const isExpanded = expandedKeys.value.has(node.id)
let subtitle = ''
if (node.attributes.ID) {
subtitle = ` : ${node.attributes.ID}`
} else if (node.attributes.EFFECT) {
subtitle = ` : ${node.attributes.EFFECT}`
} else if (node.textContent && node.textContent.trim().length > 0) {
const clean = node.textContent.trim()
subtitle = ` : ${clean.length > 25 ? clean.substring(0, 25) + '...' : clean}`
}
list.push({
id: node.id,
tagName: node.tagName,
subtitle,
depth,
hasChildren,
isExpanded,
rawNode: node
})
// 如果处于搜索状态,强制展开展示搜索结果;否则根据 isExpanded 展开
if (hasChildren && (isExpanded || search)) {
for (const child of node.children) {
list.push(...buildFlatList(child, depth + 1, search))
}
}
return list
}
// 扁平列表数据
const flatList = computed(() => {
if (!editorStore.xmlTree) return []
return buildFlatList(editorStore.xmlTree, 0, pattern.value)
})
const totalHeight = computed(() => {
return flatList.value.length * ITEM_HEIGHT
})
const startIndex = computed(() => {
return Math.max(0, Math.floor(scrollTop.value / ITEM_HEIGHT) - 5)
})
const endIndex = computed(() => {
return Math.min(
flatList.value.length,
Math.ceil((scrollTop.value + viewportHeight.value) / ITEM_HEIGHT) + 5
)
})
const visibleItems = computed(() => {
return flatList.value.slice(startIndex.value, endIndex.value)
})
const startOffset = computed(() => {
return startIndex.value * ITEM_HEIGHT
})
// === 计算连接虚线 ===
const verticalLines = computed(() => {
const lines: Array<{ key: string; left: number; top: number; height: number }> = []
const nodes = flatList.value
if (nodes.length === 0) return lines
nodes.forEach((node, index) => {
// 如果节点有子节点且处于展开状态,绘制向下连接其子节点的虚线
if (node.hasChildren && node.isExpanded) {
const childLevel = node.depth + 1
let firstChildIndex = -1
let lastChildIndex = -1
for (let i = index + 1; i < nodes.length; i++) {
if (nodes[i].depth < childLevel) {
break
}
if (nodes[i].depth === childLevel) {
if (firstChildIndex === -1) {
firstChildIndex = i
}
lastChildIndex = i
}
}
if (firstChildIndex !== -1 && lastChildIndex !== -1) {
const lineLeft = childLevel * 20 + 8
const lineTop = (index + 0.5) * ITEM_HEIGHT
const lineHeight = (lastChildIndex - index) * ITEM_HEIGHT
lines.push({
key: `${node.id}-vline`,
left: lineLeft,
top: lineTop,
height: lineHeight
})
}
}
})
return lines
})
// 仅渲染可视区域的垂直虚线
const visibleVerticalLines = computed(() => {
const sTop = scrollTop.value
const vHeight = viewportHeight.value
const sBottom = sTop + vHeight
return verticalLines.value.filter(line => {
const lineBottom = line.top + line.height
return lineBottom >= sTop && line.top <= sBottom
})
})
// 展开/折叠逻辑
const toggleExpand = (id: string) => {
if (expandedKeys.value.has(id)) {
expandedKeys.value.delete(id)
} else {
expandedKeys.value.add(id)
}
expandedKeys.value = new Set(expandedKeys.value)
emit('update:expandedKeys', Array.from(expandedKeys.value))
}
// 选中逻辑
const handleSelect = (id: string) => {
editorStore.setSelectedNodeId(id)
}
// 获取节点图标
const getNodeIcon = (item: FlatNode) => {
if (item.hasChildren) {
return FolderOpenOutline
} else if (DOCUMENT_LIKE_TAGS.includes(item.tagName)) {
return DocumentTextOutline
}
return CodeWorkingOutline
}
// 搜索高亮逻辑
const highlightText = (text: string, keyword: string): string => {
if (!keyword || !text) return text
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const regex = new RegExp(`(${escaped})`, 'gi')
return text.replace(regex, '<mark class="highlight-mark">$1</mark>')
}
// 树展开与定位同步辅助函数
const syncTreeSelection = (newId: string | null) => {
if (!newId) return
// 自动展开其所有祖先节点
const path: XmlNode[] = []
let curr = editorStore.nodeMap.get(newId)
while (curr) {
path.unshift(curr.node)
curr = curr.parent ? editorStore.nodeMap.get(curr.parent.id) : undefined
}
if (path.length > 0) {
let changed = false
for (let i = 0; i < path.length - 1; i++) {
const ancestorId = path[i].id
if (!expandedKeys.value.has(ancestorId)) {
expandedKeys.value.add(ancestorId)
changed = true
}
}
if (changed) {
expandedKeys.value = new Set(expandedKeys.value)
emit('update:expandedKeys', Array.from(expandedKeys.value))
}
}
// 自动滚动定位到视口中央
nextTick(() => {
const idx = flatList.value.findIndex(item => item.id === newId)
if (idx !== -1 && viewportRef.value) {
const itemTop = idx * ITEM_HEIGHT
const vHeight = viewportRef.value.clientHeight
viewportRef.value.scrollTo({
top: Math.max(0, itemTop - vHeight / 2),
behavior: 'smooth'
})
}
})
}
// 监听选中的节点,进行树组件的自动展开与滚动定位
watch(() => editorStore.selectedNodeId, syncTreeSelection)
// 监听回退/重做以同步树的展开与定位
watch(() => editorStore.lastUndoRedoTime, () => {
syncTreeSelection(editorStore.selectedNodeId)
})
// 右键下拉菜单数据
const dropdownOptions = computed<DropdownOption[]>(() => {
if (!contextNodeId.value) return []
return getDropdownOptions(contextNodeId.value)
})
function handleContextMenu(e: MouseEvent, item: FlatNode) {
showDropdown.value = false
contextNodeId.value = item.id
nextTick(() => {
dropdownX.value = e.clientX
dropdownY.value = e.clientY
showDropdown.value = true
})
}
// 菜单选择处理,委托给 Hook
async function handleDropdownSelect(key: string) {
showDropdown.value = false
if (!contextNodeId.value) return
await handleDropdownAction(key, contextNodeId.value)
// 如果添加了节点,确保父级节点展开状态
if (key.startsWith('add-child-')) {
if (!expandedKeys.value.has(contextNodeId.value)) {
expandedKeys.value.add(contextNodeId.value)
expandedKeys.value = new Set(expandedKeys.value)
emit('update:expandedKeys', Array.from(expandedKeys.value))
}
}
}
</script>
<style scoped>
.virtual-tree-container {
position: relative;
overflow-y: auto;
overflow-x: hidden;
}
/* 树形连接线背景层 */
.tree-lines-layer {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 0;
}
/* 垂直连接虚线 */
.tree-vertical-line {
position: absolute;
width: 1px;
border-left: 1px dashed var(--divider-color, rgba(0, 0, 0, 0.15));
}
/* 树节点内容 */
.tree-node-content {
position: relative;
z-index: 1;
transition: background-color 0.15s ease, color 0.15s ease;
}
/* 水平连接虚线 */
.tree-node-content::after {
content: '';
position: absolute;
top: 16px;
height: 1px;
width: 12px;
border-top: 1px dashed var(--divider-color, rgba(0, 0, 0, 0.15));
pointer-events: none;
left: calc(var(--tree-level, 0) * 20px + 8px);
}
/* 根级节点不显示水平虚线 */
.tree-node-content[style*="--tree-level: 0"]::after {
display: none;
}
/* 搜索高亮标记样式 */
:deep(.highlight-mark) {
background-color: var(--primary-color-hover, rgba(24, 160, 88, 0.2));
color: var(--primary-color, #18a058);
padding: 0 2px;
border-radius: 2px;
font-weight: 600;
}
/* 选中节点时的标记高亮样式 */
.tree-node-selected :deep(.highlight-mark) {
background-color: rgba(255, 255, 255, 0.3) !important;
color: #fff !important;
}
</style>
import type { XmlNode } from '@/types/xmlNode'
export interface CellParagraphModel {
id: string;
tagName: string;
text: string;
}
export interface TableCellModel {
id: string;
attributes: Record<string, string>;
paragraphs: CellParagraphModel[];
}
export interface TableRowModel {
id: string;
cells: TableCellModel[];
}
export interface TableStructureModel {
cols: number;
colSpecs: XmlNode[];
theadRows: TableRowModel[];
tbodyRows: TableRowModel[];
tgroupId: string;
theadId: string;
tbodyId: string;
}
// 表格组件相关的提示文本定义
export const CELL_EDIT_TIP = '提示:表格支持直接点击单元格进行双击/聚焦修改,光标离开时自动保存文本。'
export const DEFAULT_CELL_TEXT = '新单元格'
import { useEditorStore } from '@/store/editor' import { useEditorStore } from '@/store/editor'
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
import type { TableRowModel, TableStructureModel } from '../constants' import type { TableCellModel, TableRowModel, TableStructureModel } from '../constants'
const getDeepText = (node: XmlNode): string => {
if (node.textContent) return node.textContent
if (node.mixedContent && node.mixedContent.length > 0) {
return node.mixedContent
.map(item => {
if (item.type === 'text') return item.text || ''
if (item.type === 'element' && item.nodeId) {
const child = node.children.find(c => c.id === item.nodeId)
return child ? getDeepText(child) : ''
}
return ''
})
.join('')
}
if (node.children && node.children.length > 0) {
return node.children.map(getDeepText).join('')
}
return ''
}
const getCellText = (cellNode: XmlNode): string => {
if (cellNode.textContent) return cellNode.textContent
const parac = cellNode.children.find(c => c.tagName === 'PARAC')
if (parac) {
return getDeepText(parac)
}
const para = cellNode.children.find(c => c.tagName === 'PARA')
if (para) {
return getDeepText(para)
}
if (cellNode.children.length > 0) {
return getDeepText(cellNode.children[0])
}
return ''
}
const setDeepText = (node: XmlNode, text: string): void => {
if (node.children && node.children.length > 0) {
for (const child of node.children) {
setDeepText(child, text)
}
node.textContent = ''
node.mixedContent = []
} else {
node.textContent = text
if (node.mixedContent.length > 0) {
node.mixedContent = [{ type: 'text', text }]
}
}
}
/** /**
* CALS 表格编辑器 (TableEditor) 组件专用 Hook 逻辑 * CALS 表格编辑器 (TableEditor) 组件专用 Hook 逻辑
...@@ -11,7 +63,7 @@ export function useTableEditor() { ...@@ -11,7 +63,7 @@ export function useTableEditor() {
/** /**
* 辅助查找当前节点树中的 TGROUP 节点 * 辅助查找当前节点树中的 TGROUP 节点
*/ */
function findTgroup(node: XmlNode): XmlNode | null { const findTgroup = (node: XmlNode): XmlNode | null => {
if (node.tagName === 'TGROUP') return node if (node.tagName === 'TGROUP') return node
if (node.tagName === 'TABLE') { if (node.tagName === 'TABLE') {
const tgroup = node.children.find(c => c.tagName === 'TGROUP') const tgroup = node.children.find(c => c.tagName === 'TGROUP')
...@@ -20,13 +72,10 @@ export function useTableEditor() { ...@@ -20,13 +72,10 @@ export function useTableEditor() {
return null return null
} }
/** const parseTable = (node: XmlNode): TableStructureModel => {
* 解析 TABLE/TGROUP 结构,转化为可视化渲染的模型
*/
function parseTable(node: XmlNode): TableStructureModel {
const tgroup = findTgroup(node) const tgroup = findTgroup(node)
if (!tgroup) { if (!tgroup) {
return { cols: 0, colSpecs: [], theadRows: [], tbodyRows: [] } return { cols: 0, colSpecs: [], theadRows: [], tbodyRows: [], tgroupId: '', theadId: '', tbodyId: '' }
} }
const cols = parseInt(tgroup.attributes.COLS || '0', 10) || 1 const cols = parseInt(tgroup.attributes.COLS || '0', 10) || 1
...@@ -40,20 +89,43 @@ export function useTableEditor() { ...@@ -40,20 +89,43 @@ export function useTableEditor() {
return sectionNode.children return sectionNode.children
.filter(c => c.tagName === 'ROW') .filter(c => c.tagName === 'ROW')
.map(rowNode => { .map(rowNode => {
const cells = rowNode.children const cells: TableCellModel[] = rowNode.children
.filter(c => c.tagName === 'ENTRY') .filter(c => c.tagName === 'ENTRY')
.map(cellNode => ({ .map(cellNode => {
id: cellNode.id, let paragraphs = cellNode.children
text: cellNode.textContent || '', .filter(c => c.tagName === 'PARAC' || c.tagName === 'PARA')
attributes: { ...cellNode.attributes } .map(pNode => ({
})) id: pNode.id,
tagName: pNode.tagName,
text: getDeepText(pNode)
}))
if (paragraphs.length === 0) {
paragraphs.push({
id: cellNode.id,
tagName: 'ENTRY',
text: cellNode.textContent || ''
})
}
return {
id: cellNode.id,
attributes: { ...cellNode.attributes },
paragraphs
}
})
// 补齐缺少的列,避免渲染空洞 // 补齐缺少的列,避免渲染空洞
while (cells.length < cols) { while (cells.length < cols) {
const cellId = crypto.randomUUID()
cells.push({ cells.push({
id: crypto.randomUUID(), id: cellId,
text: '', attributes: {},
attributes: {} paragraphs: [{
id: cellId,
tagName: 'ENTRY',
text: ''
}]
}) })
} }
...@@ -68,14 +140,14 @@ export function useTableEditor() { ...@@ -68,14 +140,14 @@ export function useTableEditor() {
cols, cols,
colSpecs, colSpecs,
theadRows: parseRows(thead), theadRows: parseRows(thead),
tbodyRows: parseRows(tbody) tbodyRows: parseRows(tbody),
tgroupId: tgroup.id,
theadId: thead?.id || '',
tbodyId: tbody?.id || ''
} }
} }
/** const createDefaultEntry = (parentRowId: string): XmlNode => {
* 创建一个默认的 ENTRY 单元格
*/
function createDefaultEntry(parentRowId: string): XmlNode {
return { return {
id: crypto.randomUUID(), id: crypto.randomUUID(),
tagName: 'ENTRY', tagName: 'ENTRY',
...@@ -87,18 +159,21 @@ export function useTableEditor() { ...@@ -87,18 +159,21 @@ export function useTableEditor() {
} }
} }
/** const updateCellText = (node: XmlNode, cellId: string, text: string): void => {
* 更新具体单元格的文本值
*/
function updateCellText(node: XmlNode, cellId: string, text: string): void {
const tgroup = findTgroup(node) const tgroup = findTgroup(node)
if (!tgroup) return if (!tgroup) return
const findAndChange = (currentNode: XmlNode): boolean => { const findAndChange = (currentNode: XmlNode): boolean => {
if (currentNode.id === cellId) { if (currentNode.id === cellId) {
currentNode.textContent = text if (currentNode.children && currentNode.children.length > 0) {
if (currentNode.mixedContent.length > 0) { for (const child of currentNode.children) {
currentNode.mixedContent = [{ type: 'text', text }] setDeepText(child, text)
}
} else {
currentNode.textContent = text
if (currentNode.mixedContent.length > 0) {
currentNode.mixedContent = [{ type: 'text', text }]
}
} }
return true return true
} }
...@@ -112,10 +187,7 @@ export function useTableEditor() { ...@@ -112,10 +187,7 @@ export function useTableEditor() {
store.triggerSync() store.triggerSync()
} }
/** const addRow = (node: XmlNode, section: 'THEAD' | 'TBODY' = 'TBODY'): void => {
* 添加一行数据
*/
function addRow(node: XmlNode, section: 'THEAD' | 'TBODY' = 'TBODY'): void {
const tgroup = findTgroup(node) const tgroup = findTgroup(node)
if (!tgroup) return if (!tgroup) return
...@@ -153,10 +225,7 @@ export function useTableEditor() { ...@@ -153,10 +225,7 @@ export function useTableEditor() {
store.triggerSync() store.triggerSync()
} }
/** const deleteRow = (node: XmlNode, rowId: string): void => {
* 删除一行数据
*/
function deleteRow(node: XmlNode, rowId: string): void {
const tgroup = findTgroup(node) const tgroup = findTgroup(node)
if (!tgroup) return if (!tgroup) return
...@@ -178,10 +247,7 @@ export function useTableEditor() { ...@@ -178,10 +247,7 @@ export function useTableEditor() {
} }
} }
/** const addColumn = (node: XmlNode): void => {
* 增加一列数据
*/
function addColumn(node: XmlNode): void {
const tgroup = findTgroup(node) const tgroup = findTgroup(node)
if (!tgroup) return if (!tgroup) return
...@@ -227,10 +293,7 @@ export function useTableEditor() { ...@@ -227,10 +293,7 @@ export function useTableEditor() {
store.triggerSync() store.triggerSync()
} }
/** const deleteColumn = (node: XmlNode, colIndex: number): void => {
* 删除一列数据
*/
function deleteColumn(node: XmlNode, colIndex: number): void {
const tgroup = findTgroup(node) const tgroup = findTgroup(node)
if (!tgroup) return if (!tgroup) return
......
<template>
<div class="flex-1 flex flex-col p-4 space-y-4 overflow-auto bg-transparent min-h-0">
<!-- 表格工具栏 -->
<div class="flex items-center justify-between pb-2 border-b border-divider">
<div class="flex items-center space-x-2">
<n-tag type="info" size="small">{{ node.tagName }}</n-tag>
<span class="text-xs text-color3">{{ CELL_EDIT_TIP }}</span>
</div>
<div class="flex items-center space-x-2">
<CommonButton size="tiny" secondary type="primary" @click="handleColumnAdd">
<template #icon><n-icon><add-outline /></n-icon></template>
添加列
</CommonButton>
<CommonButton size="tiny" secondary type="primary" @click="handleRowAdd('TBODY')">
<template #icon><n-icon><add-outline /></n-icon></template>
添加行
</CommonButton>
</div>
</div>
<!-- 可视化二维表格视图 -->
<div class="flex-1 overflow-auto border border-divider rounded-lg shadow-inner bg-fill-2 p-2">
<table
class="w-full border-collapse text-sm table-fixed min-w-[600px] transition-all"
:data-node-id="structure.tgroupId"
:class="[isNodeSelected(structure.tgroupId) ? 'ring-2 ring-primary ring-offset-2 rounded' : '']"
@click="editorStore.setSelectedNodeId(structure.tgroupId)"
>
<!-- 表头规格 -->
<colgroup>
<!-- 行选择列 -->
<col class="w-[45px]" />
<!-- 数据列 -->
<col v-for="i in structure.cols" :key="i" class="min-w-[120px]" />
<!-- 操作列 -->
<col class="w-[60px]" />
</colgroup>
<!-- THEAD 渲染 -->
<thead
v-if="structure.theadRows.length > 0"
:data-node-id="structure.theadId"
@click.stop="editorStore.setSelectedNodeId(structure.theadId)"
>
<tr
v-for="(row, rIdx) in structure.theadRows"
:key="row.id"
:data-node-id="row.id"
class="border-b border-divider hover:bg-fill-3 group transition-colors cursor-pointer"
:class="[isNodeSelected(row.id) ? 'bg-primary/10' : '']"
@click.stop="editorStore.setSelectedNodeId(row.id)"
>
<!-- 表头行选择号 -->
<th
class="p-2 border border-divider text-center select-none cursor-pointer transition-colors bg-fill-4 text-color3 font-bold"
:class="[
isNodeSelected(row.id)
? 'bg-primary text-white'
: 'hover:bg-fill-3'
]"
@click.stop="editorStore.setSelectedNodeId(row.id)"
>
H{{ structure.theadRows.length > 1 ? rIdx + 1 : '' }}
</th>
<th
v-for="(cell, cIdx) in row.cells"
:key="cell.id"
:data-node-id="cell.id"
class="p-2 text-left font-bold bg-fill-4 border border-divider text-color1 transition-colors cursor-pointer relative"
:class="[
isNodeSelected(cell.id)
? 'bg-primary/5 outline outline-2 outline-primary outline-offset-[-2px]'
: '',
isNodeSelected(structure.theadId)
? 'bg-primary/10 border-primary/40'
: ''
]"
@click.stop="editorStore.setSelectedNodeId(cell.id)"
>
<div
v-for="para in cell.paragraphs"
:key="para.id"
:data-node-id="para.id"
contenteditable="true"
class="w-full min-h-[28px] px-1.5 py-1 rounded focus:outline-none focus:ring-1 focus:ring-primary focus:bg-fill-1 text-color1 leading-relaxed transition-all my-0.5"
:class="[
isNodeSelected(para.id)
? 'ring-2 ring-primary bg-primary/5 font-bold'
: 'hover:bg-fill-3'
]"
@click.stop="editorStore.setSelectedNodeId(para.id)"
@blur="(e) => handleCellBlur(para.id, e)"
v-text="para.text"
></div>
</th>
<!-- 表头操作栏 -->
<th class="p-2 border border-divider bg-fill-4 text-center">
<CommonButton size="tiny" quaternary circle type="error" @click.stop="handleRowDelete(row.id)">
<template #icon><n-icon><trash-outline /></n-icon></template>
</CommonButton>
</th>
</tr>
</thead>
<!-- TBODY 渲染 -->
<tbody
:data-node-id="structure.tbodyId"
@click.stop="editorStore.setSelectedNodeId(structure.tbodyId)"
>
<tr
v-for="(row, rIdx) in structure.tbodyRows"
:key="row.id"
:data-node-id="row.id"
class="border-b border-divider hover:bg-fill-3 group transition-colors cursor-pointer"
:class="[isNodeSelected(row.id) ? 'bg-primary/10' : '']"
@click.stop="editorStore.setSelectedNodeId(row.id)"
>
<!-- 行号选择单元格 -->
<td
class="p-2 border border-divider text-center select-none cursor-pointer transition-colors font-bold"
:class="[
isNodeSelected(row.id)
? 'bg-primary text-white'
: 'bg-fill-4 text-color3 hover:bg-fill-3'
]"
@click.stop="editorStore.setSelectedNodeId(row.id)"
>
{{ rIdx + 1 }}
</td>
<td
v-for="cell in row.cells"
:key="cell.id"
:data-node-id="cell.id"
class="p-2 border border-divider text-color2 transition-colors cursor-pointer relative"
:class="[
isNodeSelected(cell.id)
? 'bg-primary/5 outline outline-2 outline-primary outline-offset-[-2px]'
: '',
isNodeSelected(structure.tbodyId)
? 'bg-primary/5 border-primary/30'
: ''
]"
@click.stop="editorStore.setSelectedNodeId(cell.id)"
>
<div
v-for="para in cell.paragraphs"
:key="para.id"
:data-node-id="para.id"
contenteditable="true"
class="w-full min-h-[28px] px-1.5 py-1 rounded focus:outline-none focus:ring-1 focus:ring-primary focus:bg-fill-1 text-color2 leading-relaxed transition-all my-0.5"
:class="[
isNodeSelected(para.id)
? 'ring-2 ring-primary bg-primary/5'
: 'hover:bg-fill-3'
]"
@click.stop="editorStore.setSelectedNodeId(para.id)"
@blur="(e) => handleCellBlur(para.id, e)"
v-text="para.text"
></div>
</td>
<!-- 行删除按钮 -->
<td class="p-2 border border-divider text-center">
<CommonButton size="tiny" quaternary circle type="error" @click.stop="handleRowDelete(row.id)">
<template #icon><n-icon><trash-outline /></n-icon></template>
</CommonButton>
</td>
</tr>
<!-- 列操作管理辅助行 -->
<tr class="hover:bg-transparent">
<!-- 行号列占位 -->
<td class="p-1 bg-transparent border-none"></td>
<td
v-for="(_, cIdx) in structure.cols"
:key="cIdx"
class="p-1 text-center bg-transparent border-none"
>
<CommonButton
size="tiny"
quaternary
circle
type="error"
:disabled="structure.cols <= 1"
@click="handleColumnDelete(cIdx)"
>
<template #icon><n-icon><trash-outline /></n-icon></template>
</CommonButton>
</td>
<td class="p-1 bg-transparent border-none"></td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
import { AddOutline, TrashOutline } from '@vicons/ionicons5'
import type { XmlNode } from '@/types/xmlNode'
import { useTableEditor } from './functionals'
import { CELL_EDIT_TIP } from './constants'
import { useEditorStore } from '@/store/editor'
const props = defineProps<{
node: XmlNode
}>()
const editorStore = useEditorStore()
const { parseTable, updateCellText, addRow, deleteRow, addColumn, deleteColumn } = useTableEditor()
const structure = ref(parseTable(props.node))
watch(() => props.node, (newVal) => {
structure.value = parseTable(newVal)
}, { deep: true, immediate: true })
const isNodeSelected = (targetId: string): boolean => {
const selectedId = editorStore.selectedNodeId
if (!selectedId) return false
if (selectedId === targetId) return true
const findNode = (n: XmlNode, id: string): XmlNode | null => {
if (n.id === id) return n
if (n.children) {
for (const child of n.children) {
const found = findNode(child, id)
if (found) return found
}
}
return null
}
const targetNode = findNode(props.node, targetId)
if (!targetNode) return false
const isDescendant = (parent: XmlNode, id: string): boolean => {
if (parent.children) {
for (const child of parent.children) {
if (child.id === id) return true
if (isDescendant(child, id)) return true
}
}
return false
}
return isDescendant(targetNode, selectedId)
}
function handleCellBlur(cellId: string, e: FocusEvent) {
const el = e.target as HTMLElement
const val = el.innerText || ''
updateCellText(props.node, cellId, val)
}
function handleRowAdd(section: 'THEAD' | 'TBODY') {
addRow(props.node, section)
}
async function handleRowDelete(rowId: string) {
try {
await window.$dialog.warning({
title: '敏感删除确认',
content: '确定要删除这一行吗?该行内所有的单元格数据将被同时清除。'
})
deleteRow(props.node, rowId)
window.$message.success('删除行成功')
} catch (e) {
// 取消删除
}
}
function handleColumnAdd() {
addColumn(props.node)
}
async function handleColumnDelete(colIndex: number) {
try {
await window.$dialog.warning({
title: '敏感删除确认',
content: '确定要删除这一列吗?整列中所有行对应的单元格数据将被彻底清除!'
})
deleteColumn(props.node, colIndex)
window.$message.success('删除列成功')
} catch (e) {
// 取消删除
}
}
</script>
<style scoped>
</style>
...@@ -14,7 +14,7 @@ import { DEFAULT_INSERT_TEXT_VAL } from '../constants' ...@@ -14,7 +14,7 @@ import { DEFAULT_INSERT_TEXT_VAL } from '../constants'
export function useTextBlockEditor() { export function useTextBlockEditor() {
const store = useEditorStore() const store = useEditorStore()
function parseSlices(node: XmlNode): SliceItem[] { const parseSlices = (node: XmlNode): SliceItem[] => {
const isMixed = isMixedContentElement(node.tagName) const isMixed = isMixedContentElement(node.tagName)
if (!isMixed) return [] if (!isMixed) return []
...@@ -49,7 +49,7 @@ export function useTextBlockEditor() { ...@@ -49,7 +49,7 @@ export function useTextBlockEditor() {
return list return list
} }
function syncSlices(nodeId: string, slicesList: SliceItem[]): void { const syncSlices = (nodeId: string, slicesList: SliceItem[]): void => {
const newMixedContent: MixedContentItem[] = [] const newMixedContent: MixedContentItem[] = []
const newChildren: XmlNode[] = [] const newChildren: XmlNode[] = []
...@@ -80,7 +80,7 @@ export function useTextBlockEditor() { ...@@ -80,7 +80,7 @@ export function useTextBlockEditor() {
store.updateSelectedNodeMixedContent(newMixedContent, newChildren) store.updateSelectedNodeMixedContent(newMixedContent, newChildren)
} }
function insertSlice(slicesList: SliceItem[], key: string, index: number): SliceItem[] { const insertSlice = (slicesList: SliceItem[], key: string, index: number): SliceItem[] => {
const newList = [...slicesList] const newList = [...slicesList]
if (key === 'insert-text') { if (key === 'insert-text') {
newList.splice(index, 0, { newList.splice(index, 0, {
...@@ -100,7 +100,7 @@ export function useTextBlockEditor() { ...@@ -100,7 +100,7 @@ export function useTextBlockEditor() {
return newList return newList
} }
function moveSlice(slicesList: SliceItem[], index: number, direction: 'up' | 'down'): SliceItem[] { const moveSlice = (slicesList: SliceItem[], index: number, direction: 'up' | 'down'): SliceItem[] => {
const newList = [...slicesList] const newList = [...slicesList]
const target = direction === 'up' ? index - 1 : index + 1 const target = direction === 'up' ? index - 1 : index + 1
if (target < 0 || target >= newList.length) return newList if (target < 0 || target >= newList.length) return newList
......
...@@ -3,7 +3,6 @@ import { loadDtdSchema, getElementRule } from '@/utils/dtdManager' ...@@ -3,7 +3,6 @@ import { loadDtdSchema, getElementRule } from '@/utils/dtdManager'
import { parseXmlToTree, serializeTreeToXml } from '@/utils/xmlParser' import { parseXmlToTree, serializeTreeToXml } from '@/utils/xmlParser'
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
import { DEFAULT_FILE_NAME } from '../constants' import { DEFAULT_FILE_NAME } from '../constants'
import { h } from 'vue'
// 静态导入 DTD JSON // 静态导入 DTD JSON
import dtdJson from '@/assets/json/dtd.json' import dtdJson from '@/assets/json/dtd.json'
...@@ -13,10 +12,10 @@ import xmlText from '@/assets/file/AMEA-A282400-02-1_0_0.xml?raw' ...@@ -13,10 +12,10 @@ import xmlText from '@/assets/file/AMEA-A282400-02-1_0_0.xml?raw'
/** /**
* 工卡 XML 编辑器核心业务逻辑 Hook * 工卡 XML 编辑器核心业务逻辑 Hook
*/ */
export function useXmlEditor() { export function useEditor() {
const store = useEditorStore() const store = useEditorStore()
function initialize(): void { const initialize = (): void => {
try { try {
loadDtdSchema(dtdJson as any) loadDtdSchema(dtdJson as any)
const tree = parseXmlToTree(xmlText) const tree = parseXmlToTree(xmlText)
...@@ -29,14 +28,14 @@ export function useXmlEditor() { ...@@ -29,14 +28,14 @@ export function useXmlEditor() {
} }
} }
function save(): void { const save = (): void => {
if (!store.xmlTree) return if (!store.xmlTree) return
const xml = serializeTreeToXml(store.xmlTree) const xml = serializeTreeToXml(store.xmlTree)
console.log('保存的 XML 数据:\n', xml) console.log('保存的 XML 数据:\n', xml)
window.$message.success('本地修改已保存(可查看浏览器控制台输出)') window.$message.success('本地修改已保存(可查看浏览器控制台输出)')
} }
function exportXml(): void { const exportXml = (): void => {
if (!store.xmlTree) return if (!store.xmlTree) return
try { try {
const xml = serializeTreeToXml(store.xmlTree) const xml = serializeTreeToXml(store.xmlTree)
...@@ -54,7 +53,7 @@ export function useXmlEditor() { ...@@ -54,7 +53,7 @@ export function useXmlEditor() {
} }
} }
function getAllNodeKeys(): string[] { const getAllNodeKeys = (): string[] => {
if (!store.xmlTree) return [] if (!store.xmlTree) return []
const keys: string[] = [] const keys: string[] = []
const collect = (node: XmlNode) => { const collect = (node: XmlNode) => {
...@@ -65,7 +64,7 @@ export function useXmlEditor() { ...@@ -65,7 +64,7 @@ export function useXmlEditor() {
return keys return keys
} }
function validate(): void { const validate = (): void => {
if (!store.xmlTree) return if (!store.xmlTree) return
const warnings: string[] = [] const warnings: string[] = []
......
<template>
<div class="flex-1 flex flex-col min-h-0 bg-transparent">
<!-- 头部工具栏 -->
<EditorToolbar
@save="handleSave"
@validate="handleValidate"
@export="handleExport"
@expand-all="handleExpandAll"
@collapse-all="handleCollapseAll"
/>
<!-- 主体布局:自定义可拖拽分割面板 -->
<div
ref="containerRef"
class="flex-1 flex min-h-0 overflow-hidden relative"
>
<!-- 左侧:节点树 -->
<div
class="flex flex-col overflow-hidden shrink-0 transition-[width] duration-150"
:class="isCollapsed ? '' : 'border-r border-divider'"
:style="{ width: isCollapsed ? '0px' : leftWidthPx + 'px' }"
>
<NodeTree v-model:expandedKeys="expandedKeys" />
</div>
<!-- 拖动分割条 -->
<div
class="split-divider group"
:class="{ 'is-dragging': isDragging, 'is-collapsed': isCollapsed }"
@mousedown.prevent="startDrag"
@click="handleDividerClick"
>
<!-- 折叠状态:展开箭头 -->
<div v-if="isCollapsed" class="split-expand-btn">
<svg class="w-3 h-3" viewBox="0 0 12 12" fill="currentColor">
<path d="M4 2l4 4-4 4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<!-- 展开状态:可视指示线 + 手柄圆点 -->
<template v-else>
<div class="split-divider-line" />
<div class="split-divider-handle">
<div class="handle-dot" />
<div class="handle-dot" />
<div class="handle-dot" />
</div>
</template>
</div>
<!-- 右侧:编辑区 -->
<div
class="flex-1 flex flex-col overflow-hidden"
:style="{ background: themeVars.colorBg1 }"
>
<EditorPanel />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useEditorStore } from '@/store/editor'
import { useEditor } from './functionals'
import EditorToolbar from './components/EditorToolbar/index.vue'
import NodeTree from './components/NodeTree/index.vue'
import EditorPanel from './components/EditorPanel/index.vue'
const themeVars = useThemeVars()
const editorStore = useEditorStore()
const expandedKeys = ref<string[]>([])
// 实例化业务逻辑 Hook
const { initialize, save, exportXml, getAllNodeKeys, validate } = useEditor()
onMounted(() => {
initialize()
})
// ── 自定义拖动分割逻辑 ────────────────────────────────────────────────────────
const containerRef = ref<HTMLElement | null>(null)
const COLLAPSE_THRESHOLD = 150 // px:低于此宽度时自动折叠
const DEFAULT_WIDTH = 560 // px:初始/恢复宽度
const MAX_RATIO = 0.6
const leftWidthPx = ref(DEFAULT_WIDTH)
const isCollapsed = ref(false)
const isDragging = ref(false)
let startX = 0
let startWidth = 0
let hasDragged = false // 区分拖动与点击
const startDrag = (e: MouseEvent) => {
isDragging.value = true
hasDragged = false
startX = e.clientX
// 折叠状态下从 0 开始拖动
startWidth = isCollapsed.value ? 0 : leftWidthPx.value
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
window.addEventListener('mousemove', onDrag)
window.addEventListener('mouseup', stopDrag)
}
const onDrag = (e: MouseEvent) => {
if (!isDragging.value || !containerRef.value) return
const delta = e.clientX - startX
if (Math.abs(delta) > 3) hasDragged = true
const raw = startWidth + delta
const containerWidth = containerRef.value.clientWidth
const maxWidth = containerWidth * MAX_RATIO
if (raw < COLLAPSE_THRESHOLD) {
// 低于阈值:预览折叠
leftWidthPx.value = Math.max(0, raw)
isCollapsed.value = raw < COLLAPSE_THRESHOLD / 2
} else {
isCollapsed.value = false
leftWidthPx.value = Math.min(maxWidth, raw)
}
}
const stopDrag = () => {
isDragging.value = false
document.body.style.cursor = ''
document.body.style.userSelect = ''
window.removeEventListener('mousemove', onDrag)
window.removeEventListener('mouseup', stopDrag)
// 松手后:如果宽度低于阈值,完全折叠
if (!isCollapsed.value && leftWidthPx.value < COLLAPSE_THRESHOLD) {
isCollapsed.value = true
}
// 如果宽度非常小但没折叠,恢复到最小可用宽度
if (!isCollapsed.value && leftWidthPx.value < COLLAPSE_THRESHOLD) {
leftWidthPx.value = COLLAPSE_THRESHOLD
}
}
// 点击分割条:折叠时展开,展开时无操作(避免误触)
const handleDividerClick = () => {
if (hasDragged) return
if (isCollapsed.value) {
isCollapsed.value = false
leftWidthPx.value = DEFAULT_WIDTH
}
}
onUnmounted(() => {
window.removeEventListener('mousemove', onDrag)
window.removeEventListener('mouseup', stopDrag)
})
// ── 工具栏事件 ────────────────────────────────────────────────────────────────
const handleSave = () => save()
const handleExport = () => exportXml()
const handleExpandAll = () => { expandedKeys.value = getAllNodeKeys() }
const handleCollapseAll = () => {
if (editorStore.xmlTree) {
expandedKeys.value = [editorStore.xmlTree.id]
}
}
const handleValidate = () => validate()
</script>
<style scoped>
/* ── 分割条容器 ─────────────────────────────────────────────────────────────── */
.split-divider {
position: relative;
width: 10px;
flex-shrink: 0;
cursor: col-resize;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
transition: background-color 0.2s;
}
.split-divider:hover,
.split-divider.is-dragging {
background-color: var(--primary-color, #18a058)1a;
}
/* ── 可视线 ───────────────────────────────────────────────────────────────── */
.split-divider-line {
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: 1px;
transform: translateX(-50%);
background-color: var(--divider-color, rgba(0, 0, 0, 0.08));
transition: background-color 0.2s, width 0.2s;
}
.split-divider:hover .split-divider-line,
.split-divider.is-dragging .split-divider-line {
background-color: var(--primary-color, #18a058);
width: 2px;
}
/* ── 手柄圆点 ────────────────────────────────────────────────────────────── */
.split-divider-handle {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 3px;
padding: 4px 3px;
border-radius: 8px;
background: var(--fill-color-2, rgba(0, 0, 0, 0.04));
border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.08));
opacity: 0;
transform: scaleY(0.8);
transition: opacity 0.2s, transform 0.2s, background 0.2s;
}
.split-divider:hover .split-divider-handle,
.split-divider.is-dragging .split-divider-handle {
opacity: 1;
transform: scaleY(1);
background: var(--primary-color, #18a058);
border-color: var(--primary-color, #18a058);
}
.handle-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background-color: var(--divider-color, rgba(0, 0, 0, 0.2));
transition: background-color 0.2s;
}
.split-divider:hover .handle-dot,
.split-divider.is-dragging .handle-dot {
background-color: #fff;
}
/* ── 折叠状态 ─────────────────────────────────────────────────────────────── */
.split-divider.is-collapsed {
width: 16px;
cursor: pointer;
background-color: var(--fill-color-3, rgba(0, 0, 0, 0.06));
border-right: 1px solid var(--divider-color, rgba(0, 0, 0, 0.08));
}
.split-divider.is-collapsed:hover {
background-color: color-mix(in srgb, var(--primary-color, #18a058) 12%, transparent);
}
.split-expand-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: var(--text-color-3, rgba(0, 0, 0, 0.38));
transition: color 0.2s;
}
.split-divider.is-collapsed:hover .split-expand-btn {
color: var(--primary-color, #18a058);
}
</style>
<template>
<div class="flex-1 flex flex-col min-h-0 bg-transparent">
<!-- 头部工具栏 -->
<EditorToolbar
@save="handleSave"
@validate="handleValidate"
@export="handleExport"
@expand-all="handleExpandAll"
@collapse-all="handleCollapseAll"
/>
<!-- 主体布局 -->
<div class="flex-1 flex min-h-0">
<!-- 左侧节点树导航 -->
<div class="w-80 flex-shrink-0 h-full flex flex-col">
<NodeTree v-model:expandedKeys="expandedKeys" />
</div>
<!-- 右侧核心编辑区 -->
<div class="flex-1 h-full flex flex-col overflow-hidden" :style="{ background: themeVars.bodyColor }">
<EditorPanel />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useEditorStore } from '@/store/editor'
import { useXmlEditor } from './xmlEditor/functionals'
import EditorToolbar from './xmlEditor/components/EditorToolbar/index.vue'
import NodeTree from './xmlEditor/components/NodeTree/index.vue'
import EditorPanel from './xmlEditor/components/EditorPanel/index.vue'
const themeVars = useThemeVars()
const editorStore = useEditorStore()
const expandedKeys = ref<string[]>([])
// 实例化业务逻辑 Hook
const { initialize, save, exportXml, getAllNodeKeys, validate } = useXmlEditor()
onMounted(() => {
initialize()
})
// 保存操作
function handleSave() {
save()
}
// 导出文件操作
function handleExport() {
exportXml()
}
// 全部展开
function handleExpandAll() {
expandedKeys.value = getAllNodeKeys()
}
// 全部折叠
function handleCollapseAll() {
if (editorStore.xmlTree) {
expandedKeys.value = [editorStore.xmlTree.id]
}
}
// 基于 DTD 的静态语法检测
function handleValidate() {
validate()
}
</script>
<style scoped>
</style>
// 列表节点标签定义
export const ALL_LIST_TAGS = ['LIST1', 'LIST2', 'LIST3', 'UNLIST']
// 表格节点标签定义
export const CALS_TABLE_TAGS = ['TABLE', 'TGROUP']
/**
* EditorPanel 组件级业务方法
*/
export class EditorPanelClass {
// 暂无组件私有逻辑
}
<template>
<div class="flex-1 flex flex-col min-h-0 bg-transparent">
<template v-if="selectedNode">
<!-- 顶部面包屑路径 -->
<div
class="px-4 py-2 border-b border-divider bg-fill-2 flex items-center space-x-2 text-xs"
>
<span class="text-color3 select-none">当前路径:</span>
<n-breadcrumb>
<n-breadcrumb-item
v-for="n in nodePath"
:key="n.id"
@click="editorStore.setSelectedNodeId(n.id)"
class="cursor-pointer hover:text-primary transition-colors text-color2 font-medium"
>
{{ n.tagName }}
</n-breadcrumb-item>
</n-breadcrumb>
</div>
<!-- 编辑主体区域 -->
<div class="flex-1 overflow-auto p-4 space-y-4 flex flex-col min-h-0">
<!-- 属性面板 -->
<AttributeEditor :node="selectedNode" />
<!-- 内容面板 -->
<div
class="flex-1 flex flex-col rounded-lg border border-divider shadow-sm overflow-hidden bg-fill-2"
>
<div class="px-4 py-3 border-b border-divider flex items-center space-x-1">
<n-icon :color="themeVars.primaryColor"><document-text-outline /></n-icon>
<span class="text-sm font-bold text-color1">{{ editorTitle }}</span>
</div>
<!-- 可视化编辑器按节点类型动态分流 -->
<component :is="activeEditor" :node="selectedNode" class="flex-1" />
</div>
</div>
</template>
<template v-else>
<div class="flex-1 flex flex-col items-center justify-center text-color3">
<n-empty size="large" description="请在左侧菜单树选择节点以开始编辑" />
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { DocumentTextOutline } from '@vicons/ionicons5'
import { useEditorStore } from '@/store/editor'
import { getNodePath } from '@/utils/xmlParser'
import AttributeEditor from '../AttributeEditor/index.vue'
import TextBlockEditor from '../TextBlockEditor/index.vue'
import TableEditor from '../TableEditor/index.vue'
import ListEditor from '../ListEditor/index.vue'
import { CALS_TABLE_TAGS, ALL_LIST_TAGS } from './constants'
const themeVars = useThemeVars()
const editorStore = useEditorStore()
const selectedNode = computed(() => editorStore.selectedNode)
const nodePath = computed(() => {
if (!editorStore.xmlTree || !editorStore.selectedNodeId) return []
return getNodePath(editorStore.xmlTree, editorStore.selectedNodeId)
})
// 根据不同节点,展示对应样式的标题
const editorTitle = computed(() => {
if (!selectedNode.value) return '内容编辑'
const tag = selectedNode.value.tagName
if (CALS_TABLE_TAGS.includes(tag)) return 'CALS 可视化表格编辑'
if (ALL_LIST_TAGS.includes(tag)) return '层级列表可视化编辑'
return '内容编辑'
})
// 核心:分流渲染逻辑
const activeEditor = computed(() => {
if (!selectedNode.value) return TextBlockEditor
const tag = selectedNode.value.tagName
if (CALS_TABLE_TAGS.includes(tag)) {
return TableEditor
}
if (ALL_LIST_TAGS.includes(tag)) {
return ListEditor
}
return TextBlockEditor
})
</script>
<style scoped>
</style>
<template>
<div
class="flex items-center justify-between px-4 py-3 border-b border-divider bg-fill-2 transition-colors"
>
<!-- 左侧操作组 -->
<div class="flex items-center space-x-2">
<div class="flex items-center border border-divider rounded overflow-hidden">
<CommonButton type="primary" size="small" @click="emit('save')">
<template #icon>
<n-icon><save-outline /></n-icon>
</template>
保存
</CommonButton>
<CommonButton size="small" @click="emit('validate')" secondary class="border-l border-divider">
<template #icon>
<n-icon><checkmark-circle-outline /></n-icon>
</template>
验证 DTD
</CommonButton>
</div>
<n-divider vertical />
<!-- 撤销/重做 -->
<n-tooltip trigger="hover">
<template #trigger>
<CommonButton
size="small"
quaternary
:disabled="!canUndo"
@click="editorStore.undo()"
>
<template #icon>
<n-icon><arrow-undo-outline /></n-icon>
</template>
</CommonButton>
</template>
撤销 (Ctrl+Z)
</n-tooltip>
<n-tooltip trigger="hover">
<template #trigger>
<CommonButton
size="small"
quaternary
:disabled="!canRedo"
@click="editorStore.redo()"
>
<template #icon>
<n-icon><arrow-redo-outline /></n-icon>
</template>
</CommonButton>
</template>
重做 (Ctrl+Y)
</n-tooltip>
<n-divider vertical />
<!-- 展开/折叠 -->
<CommonButton size="small" secondary @click="emit('expand-all')">
展开全部
</CommonButton>
<CommonButton size="small" secondary @click="emit('collapse-all')" class="ml-2">
折叠全部
</CommonButton>
</div>
<!-- 右侧辅助组 -->
<div class="flex items-center space-x-2">
<n-tag :type="isValid ? 'success' : 'warning'" size="small" round>
{{ isValid ? 'DTD 验证通过' : '未验证 / 存在警告' }}
</n-tag>
<CommonButton size="small" tertiary @click="emit('export')">
<template #icon>
<n-icon><download-outline /></n-icon>
</template>
导出 XML
</CommonButton>
</div>
</div>
</template>
<script setup lang="ts">
import {
SaveOutline,
CheckmarkCircleOutline,
ArrowUndoOutline,
ArrowRedoOutline,
DownloadOutline
} from '@vicons/ionicons5'
import { useEditorStore } from '@/store/editor'
const emit = defineEmits(['save', 'validate', 'export', 'expand-all', 'collapse-all'])
const editorStore = useEditorStore()
const canUndo = computed(() => editorStore.undoStack.length > 0)
const canRedo = computed(() => editorStore.redoStack.length > 0)
const isValid = ref(true)
</script>
<style scoped>
</style>
// 列表/文档型叶子节点标签判定
export const DOCUMENT_LIKE_TAGS = ['PARA', 'PARAC', 'TITLE', 'TITLEC', 'WARNING', 'CAUTION', 'NOTE']
import { useEditorStore } from '@/store/editor'
import { h } from 'vue'
import type { TreeOption, DropdownOption } from 'naive-ui'
import type { XmlNode } from '@/types/xmlNode'
import { findNodeById, findParentNode } from '@/utils/xmlParser'
import {
getAllowedChildren,
getInsertableChildren,
canDeleteChild,
createDefaultAttributes
} from '@/utils/dtdManager'
/**
* 节点树 (NodeTree) 组件专用 Hook 逻辑
*/
export function useNodeTree() {
const store = useEditorStore()
/**
* 将 XML 内部节点树结构,转换为 Naive UI n-tree 组件接收的 Option 格式
*/
function convertNodeToOption(node: XmlNode): TreeOption {
let subtitle = ''
if (node.attributes.ID) {
subtitle = ` : ${node.attributes.ID}`
} else if (node.attributes.EFFECT) {
subtitle = ` : ${node.attributes.EFFECT}`
} else if (node.textContent && node.textContent.trim().length > 0) {
const clean = node.textContent.trim()
subtitle = ` : ${clean.length > 25 ? clean.substring(0, 25) + '...' : clean}`
}
const option: TreeOption = {
key: node.id,
label: node.tagName,
tagName: node.tagName,
subtitle: subtitle,
children: []
}
if (node.children && node.children.length > 0) {
option.children = node.children.map(c => convertNodeToOption(c))
}
return option
}
/**
* 自定义树节点渲染 Label 方法
*/
function renderLabel({ option }: { option: TreeOption }) {
const tagName = (option.tagName as string) || (option.label as string)
const subtitle = (option.subtitle as string) || ''
return h('div', { class: 'flex items-center space-x-1' }, [
h('span', { class: 'font-bold text-sm' }, tagName),
subtitle ? h('span', { class: 'text-xs text-secondary italic' }, subtitle) : null
])
}
/**
* 动态生成右键菜单选项,支持基于 DTD 的添加和删除校验
*/
function getDropdownOptions(nodeId: string): DropdownOption[] {
const tree = store.xmlTree
if (!tree) return []
const node = findNodeById(tree, nodeId)
if (!node) return []
const options: DropdownOption[] = []
const allowed = getAllowedChildren(node.tagName)
if (allowed.length > 0) {
const insertable = getInsertableChildren(node.tagName, node.children.map(c => c.tagName))
const subOptions = insertable.map(tag => ({
label: tag,
key: `add-child-${tag}`
}))
options.push({
label: '添加子节点',
key: 'add-child',
children: subOptions.length > 0 ? subOptions : [{ label: '无可用子节点 (超约束上限)', key: 'none', disabled: true }]
})
}
const parent = findParentNode(tree, nodeId)
if (parent) {
const childCount = parent.children.filter(c => c.tagName === node.tagName).length
const deletable = canDeleteChild(parent.tagName, node.tagName, childCount)
options.push({
label: deletable ? '删除该节点' : '删除该节点 (不可删,受 DTD 约束)',
key: 'delete-node',
disabled: !deletable
})
}
if (parent) {
const parentAllowed = getAllowedChildren(parent.tagName)
const parentInsertable = getInsertableChildren(parent.tagName, parent.children.map(c => c.tagName))
const insertableOptions = parentInsertable.map(tag => ({
label: tag,
key: `insert-sibling-${tag}`
}))
if (insertableOptions.length > 0) {
options.push({
label: '在后方插入兄弟节点',
key: 'insert-sibling',
children: insertableOptions
})
}
}
return options
}
/**
* 分发并执行右键菜单指令
*/
async function handleDropdownAction(key: string, nodeId: string): Promise<void> {
const tree = store.xmlTree
if (!tree) return
const node = findNodeById(tree, nodeId)
if (!node) return
const parent = findParentNode(tree, nodeId)
if (key.startsWith('add-child-')) {
const childTag = key.replace('add-child-', '')
const childId = crypto.randomUUID()
const newChild: XmlNode = {
id: childId,
tagName: childTag,
attributes: createDefaultAttributes(childTag),
children: [],
textContent: '',
mixedContent: [],
parentId: nodeId
}
store.addChildNode(newChild)
store.setSelectedNodeId(childId)
window.$message.success(`成功添加子节点 <${childTag}>`)
}
else if (key === 'delete-node') {
if (nodeId === tree.id) {
window.$message.warning('不能删除根节点')
return
}
try {
await window.$dialog.warning({
title: '敏感删除确认',
content: `确定要删除节点 <${node.tagName}> 吗?该操作将连带删除其所有子节点!`
})
store.deleteSelectedNode()
window.$message.success('节点删除成功')
} catch (e) {
// 取消删除
}
}
else if (key.startsWith('insert-sibling-')) {
if (!parent) return
const sibTag = key.replace('insert-sibling-', '')
const sibId = crypto.randomUUID()
const newSib: XmlNode = {
id: sibId,
tagName: sibTag,
attributes: createDefaultAttributes(sibTag),
children: [],
textContent: '',
mixedContent: [],
parentId: parent.id
}
const currIdx = parent.children.findIndex(c => c.id === nodeId)
store.saveSnapshot()
parent.children.splice(currIdx + 1, 0, newSib)
store.setSelectedNodeId(sibId)
window.$message.success(`成功在后方插入兄弟节点 <${sibTag}>`)
}
}
return {
convertNodeToOption,
renderLabel,
getDropdownOptions,
handleDropdownAction
}
}
<template>
<div class="flex flex-col h-full border-r border-divider relative">
<!-- 搜索过滤 -->
<div class="p-3 border-b border-divider">
<n-input v-model:value="pattern" placeholder="搜索节点名称..." size="small">
<template #prefix>
<n-icon><search-outline /></n-icon>
</template>
</n-input>
</div>
<!-- 树组件容器 -->
<div class="flex-1 overflow-auto p-2">
<n-tree
v-if="treeData.length > 0"
block-line
expand-on-click
:data="treeData"
:expanded-keys="expandedKeys"
:selected-keys="selectedKeys"
:pattern="pattern"
:render-label="renderLabel"
:render-prefix="renderPrefix"
@update:selected-keys="handleSelect"
@update:expanded-keys="handleExpand"
@node-contextmenu="handleContextMenu"
/>
<div v-else class="h-full flex items-center justify-center text-color3">
<n-empty description="暂无节点数据" />
</div>
</div>
<!-- 右键下拉菜单 -->
<n-dropdown
trigger="manual"
placement="bottom-start"
:show="showDropdown"
:options="dropdownOptions"
:x="dropdownX"
:y="dropdownY"
@clickoutside="showDropdown = false"
@select="handleDropdownSelect"
/>
</div>
</template>
<script setup lang="ts">
import type { TreeOption, DropdownOption } from 'naive-ui'
import { NIcon } from 'naive-ui'
import {
SearchOutline,
CodeWorkingOutline,
DocumentTextOutline,
FolderOpenOutline
} from '@vicons/ionicons5'
import { useEditorStore } from '@/store/editor'
import { useNodeTree } from './functionals'
import { DOCUMENT_LIKE_TAGS } from './constants'
const props = defineProps<{
expandedKeys: string[]
}>()
const emit = defineEmits(['update:expandedKeys'])
const editorStore = useEditorStore()
const { convertNodeToOption, renderLabel, getDropdownOptions, handleDropdownAction } = useNodeTree()
const pattern = ref('')
const selectedKeys = computed(() => editorStore.selectedNodeId ? [editorStore.selectedNodeId] : [])
// 下拉菜单状态
const showDropdown = ref(false)
const dropdownX = ref(0)
const dropdownY = ref(0)
const contextNodeId = ref<string | null>(null)
// 转换数据源,委托给 Hook
const treeData = computed(() => {
if (!editorStore.xmlTree) return []
return [convertNodeToOption(editorStore.xmlTree)]
})
// 默认展开所有一级子节点
const expandedKeys = ref<string[]>([])
watch(() => editorStore.xmlTree, (newVal) => {
if (newVal && expandedKeys.value.length === 0) {
const keys = [newVal.id, ...newVal.children.map((c: any) => c.id)]
expandedKeys.value = keys
emit('update:expandedKeys', keys)
}
}, { immediate: true })
watch(() => props.expandedKeys, (keys) => {
expandedKeys.value = keys
})
function handleSelect(keys: string[]) {
if (keys.length > 0) {
editorStore.setSelectedNodeId(keys[0])
}
}
function handleExpand(keys: string[]) {
expandedKeys.value = keys
emit('update:expandedKeys', keys)
}
function renderPrefix({ option }: { option: TreeOption }) {
const tagName = option.tagName as string
let icon = CodeWorkingOutline
if (option.children && option.children.length > 0) {
icon = FolderOpenOutline
} else if (DOCUMENT_LIKE_TAGS.includes(tagName)) {
icon = DocumentTextOutline
}
return h(NIcon, { size: 16 }, { default: () => h(icon) })
}
// 右键下拉菜单数据
const dropdownOptions = computed<DropdownOption[]>(() => {
if (!contextNodeId.value) return []
return getDropdownOptions(contextNodeId.value)
})
function handleContextMenu(e: MouseEvent, option: TreeOption) {
e.preventDefault()
showDropdown.value = false
contextNodeId.value = option.key as string
nextTick(() => {
dropdownX.value = e.clientX
dropdownY.value = e.clientY
showDropdown.value = true
})
}
// 菜单选择处理,委托给 Hook
async function handleDropdownSelect(key: string) {
showDropdown.value = false
if (!contextNodeId.value) return
await handleDropdownAction(key, contextNodeId.value)
// 如果添加了节点,确保父级节点展开状态
if (key.startsWith('add-child-')) {
if (!expandedKeys.value.includes(contextNodeId.value)) {
const nextKeys = [...expandedKeys.value, contextNodeId.value]
expandedKeys.value = nextKeys
emit('update:expandedKeys', nextKeys)
}
}
}
</script>
<style scoped>
:deep(.n-tree-node-content) {
padding: 4px 8px !important;
border-radius: 4px;
transition: all 0.2s ease;
}
:deep(.n-tree-node-content:hover) {
background-color: var(--primary-color-hover) !important;
opacity: 0.85;
}
:deep(.n-tree-node-content--selected) {
background-color: var(--primary-color) !important;
color: white !important;
}
:deep(.n-tree-node-content--selected .n-text) {
color: white !important;
}
</style>
import type { XmlNode } from '@/types/xmlNode'
export interface TableCellModel {
id: string
text: string
attributes: Record<string, string>
}
export interface TableRowModel {
id: string
cells: TableCellModel[]
}
export interface TableStructureModel {
cols: number
colSpecs: XmlNode[]
theadRows: TableRowModel[]
tbodyRows: TableRowModel[]
}
// 表格组件相关的提示文本定义
export const CELL_EDIT_TIP = '提示:表格支持直接点击单元格进行双击/聚焦修改,光标离开时自动保存文本。'
export const DEFAULT_CELL_TEXT = '新单元格'
<template>
<div class="flex-1 flex flex-col p-4 space-y-4 overflow-auto bg-transparent min-h-0">
<!-- 表格工具栏 -->
<div class="flex items-center justify-between pb-2 border-b border-divider">
<div class="flex items-center space-x-2">
<n-tag type="info" size="small">{{ node.tagName }}</n-tag>
<span class="text-xs text-color3">{{ CELL_EDIT_TIP }}</span>
</div>
<div class="flex items-center space-x-2">
<CommonButton size="tiny" secondary type="primary" @click="handleColumnAdd">
<template #icon><n-icon><add-outline /></n-icon></template>
添加列
</CommonButton>
<CommonButton size="tiny" secondary type="primary" @click="handleRowAdd('TBODY')">
<template #icon><n-icon><add-outline /></n-icon></template>
添加行
</CommonButton>
</div>
</div>
<!-- 可视化二维表格视图 -->
<div class="flex-1 overflow-auto border border-divider rounded-lg shadow-inner bg-fill-2 p-2">
<table class="w-full border-collapse text-sm table-fixed min-w-[600px]">
<!-- 表头规格 -->
<colgroup>
<col v-for="i in structure.cols" :key="i" class="min-w-[120px]" />
<col class="w-[80px]" />
</colgroup>
<!-- THEAD 渲染 -->
<thead v-if="structure.theadRows.length > 0">
<tr
v-for="row in structure.theadRows"
:key="row.id"
class="border-b border-divider hover:bg-fill-3 group"
>
<th
v-for="(cell, cIdx) in row.cells"
:key="cell.id"
class="p-2 text-left font-bold bg-fill-4 border border-divider text-color1"
>
<div
contenteditable="true"
class="w-full min-h-[28px] px-1.5 py-1 rounded focus:outline-none focus:ring-1 focus:ring-primary focus:bg-fill-1 text-color1"
@blur="(e) => handleCellBlur(cell.id, e)"
v-text="cell.text"
></div>
</th>
<!-- 表头操作栏 -->
<th class="p-2 border border-divider bg-fill-4 text-center">
<CommonButton size="tiny" quaternary circle type="error" @click="handleRowDelete(row.id)">
<template #icon><n-icon><trash-outline /></n-icon></template>
</CommonButton>
</th>
</tr>
</thead>
<!-- TBODY 渲染 -->
<tbody>
<tr
v-for="row in structure.tbodyRows"
:key="row.id"
class="border-b border-divider hover:bg-fill-3 group"
>
<td
v-for="cell in row.cells"
:key="cell.id"
class="p-2 border border-divider text-color2"
>
<div
contenteditable="true"
class="w-full min-h-[28px] px-1.5 py-1 rounded focus:outline-none focus:ring-1 focus:ring-primary focus:bg-fill-1 text-color2 leading-relaxed"
@blur="(e) => handleCellBlur(cell.id, e)"
v-text="cell.text"
></div>
</td>
<!-- 行删除按钮 -->
<td class="p-2 border border-divider text-center">
<CommonButton size="tiny" quaternary circle type="error" @click="handleRowDelete(row.id)">
<template #icon><n-icon><trash-outline /></n-icon></template>
</CommonButton>
</td>
</tr>
<!-- 列操作管理辅助行 -->
<tr class="hover:bg-transparent">
<td
v-for="(_, cIdx) in structure.cols"
:key="cIdx"
class="p-1 text-center bg-transparent border-none"
>
<CommonButton
size="tiny"
quaternary
circle
type="error"
:disabled="structure.cols <= 1"
@click="handleColumnDelete(cIdx)"
>
<template #icon><n-icon><trash-outline /></n-icon></template>
</CommonButton>
</td>
<td class="p-1 bg-transparent border-none"></td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
import { AddOutline, TrashOutline } from '@vicons/ionicons5'
import type { XmlNode } from '@/types/xmlNode'
import { useTableEditor } from './functionals'
import { CELL_EDIT_TIP } from './constants'
const props = defineProps<{
node: XmlNode
}>()
const { parseTable, updateCellText, addRow, deleteRow, addColumn, deleteColumn } = useTableEditor()
const structure = ref(parseTable(props.node))
watch(() => props.node, (newVal) => {
structure.value = parseTable(newVal)
}, { deep: true, immediate: true })
function handleCellBlur(cellId: string, e: FocusEvent) {
const el = e.target as HTMLElement
const val = el.innerText || ''
updateCellText(props.node, cellId, val)
}
function handleRowAdd(section: 'THEAD' | 'TBODY') {
addRow(props.node, section)
}
async function handleRowDelete(rowId: string) {
try {
await window.$dialog.warning({
title: '敏感删除确认',
content: '确定要删除这一行吗?该行内所有的单元格数据将被同时清除。'
})
deleteRow(props.node, rowId)
window.$message.success('删除行成功')
} catch (e) {
// 取消删除
}
}
function handleColumnAdd() {
addColumn(props.node)
}
async function handleColumnDelete(colIndex: number) {
try {
await window.$dialog.warning({
title: '敏感删除确认',
content: '确定要删除这一列吗?整列中所有行对应的单元格数据将被彻底清除!'
})
deleteColumn(props.node, colIndex)
window.$message.success('删除列成功')
} catch (e) {
// 取消删除
}
}
</script>
<style scoped>
</style>
<!-- 废弃,已迁移至 src/views/xmlEditor.vue -->
<template>
<div></div>
</template>
/**
* XML 解析 Web Worker
* 在后台线程执行耗时的 DOM 解析 + 树结构转换,避免阻塞主线程 UI
* 注意:Worker 环境中 DOMParser 与 crypto.randomUUID 均可用
*/
// ── 与 xmlParser.ts 保持一致的类型定义(Worker 不能 import 外部模块)───────
interface MixedContentItem {
type: 'text' | 'element'
text?: string
nodeId?: string
}
interface XmlNode {
id: string
tagName: string
attributes: Record<string, string>
children: XmlNode[]
textContent: string
mixedContent: MixedContentItem[]
parentId: string | null
}
// ── 工具函数 ──────────────────────────────────────────────────────────────────
function generateId(): string {
return (self as any).crypto?.randomUUID?.()
?? `node_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
}
// ── DOM → XmlNode 递归转换 ────────────────────────────────────────────────────
function domElementToXmlNode(element: Element, parentId: string | null): XmlNode {
const id = generateId()
const attributes: Record<string, string> = {}
for (const attr of Array.from(element.attributes)) {
attributes[attr.name] = attr.value
}
const children: XmlNode[] = []
const mixedContent: MixedContentItem[] = []
let textContent = ''
const childNodes = Array.from(element.childNodes)
const hasElementChildren = childNodes.some(n => n.nodeType === 1 /* ELEMENT_NODE */)
const hasTextChildren = childNodes.some(n => n.nodeType === 3 /* TEXT_NODE */ && n.textContent?.trim())
if (hasElementChildren && hasTextChildren) {
for (const child of childNodes) {
if (child.nodeType === 3) {
const text = child.textContent || ''
if (text) mixedContent.push({ type: 'text', text })
} else if (child.nodeType === 1) {
const childNode = domElementToXmlNode(child as Element, id)
children.push(childNode)
mixedContent.push({ type: 'element', nodeId: childNode.id })
}
}
} else if (hasElementChildren) {
for (const child of childNodes) {
if (child.nodeType === 1) {
children.push(domElementToXmlNode(child as Element, id))
}
}
} else {
textContent = element.textContent || ''
}
return { id, tagName: element.tagName, attributes, children, textContent, mixedContent, parentId }
}
// ── Worker 消息处理 ───────────────────────────────────────────────────────────
self.onmessage = (e: MessageEvent<{ xmlString: string }>) => {
try {
const parser = new DOMParser()
const doc = parser.parseFromString(e.data.xmlString, 'application/xml')
const parseError = doc.querySelector('parsererror')
if (parseError) {
self.postMessage({ error: `XML 解析错误: ${parseError.textContent}` })
return
}
const tree = domElementToXmlNode(doc.documentElement, null)
self.postMessage({ tree })
} catch (err: any) {
self.postMessage({ error: err?.message ?? '未知解析错误' })
}
}
import { defineConfig, loadEnv } from 'vite' import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import path from 'path' import path from 'path'
import fs from 'fs'
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd()) loadEnv(mode, process.cwd())
const proxyUrl = env.VITE_APP_PROXY_URL
return { return {
resolve: { resolve: {
...@@ -46,60 +44,7 @@ export default defineConfig(({ mode }) => { ...@@ -46,60 +44,7 @@ export default defineConfig(({ mode }) => {
], ],
// 生成 components.d.ts 类型声明文件 // 生成 components.d.ts 类型声明文件
dts: 'src/components.d.ts' dts: 'src/components.d.ts'
}), })
// 自定义中间件用于在开发阶段优先从本地 shtml 目录读取静态资源
{
name: 'serve-shtml-static',
configureServer(server) {
server.middlewares.use((req, res, next) => {
const shtmlRegex = /^\/(views|css|fonts|img|js|locales|plugins|scripts)\//
if (req.url && shtmlRegex.test(req.url)) {
const pathname = req.url.split('?')[0]
const localFilePath = path.join(__dirname, 'shtml/src/main/resources/static', pathname)
if (fs.existsSync(localFilePath) && fs.statSync(localFilePath).isFile()) {
const ext = path.extname(localFilePath).toLowerCase()
let contentType = 'text/plain'
if (ext === '.html' || ext === '.shtml') {
contentType = 'text/html; charset=utf-8'
res.setHeader('Content-Type', contentType)
const processSSI = (filePath: string): string => {
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
return `<!-- SSI Error: ${filePath} not found -->`
}
let text = fs.readFileSync(filePath, 'utf8')
const ssiRegex = /<!--#include\s+virtual="([^"]+)"\s*-->/g
return text.replace(ssiRegex, (_, includePath) => {
const resolvedPath = path.join(
__dirname,
'shtml/src/main/resources/static',
includePath.startsWith('/') ? includePath.slice(1) : includePath
)
return processSSI(resolvedPath)
})
}
res.end(processSSI(localFilePath))
return
}
if (ext === '.js') contentType = 'application/javascript; charset=utf-8'
else if (ext === '.css') contentType = 'text/css; charset=utf-8'
else if (ext === '.json') contentType = 'application/json; charset=utf-8'
else if (ext === '.png') contentType = 'image/png'
else if (ext === '.jpg' || ext === '.jpeg') contentType = 'image/jpeg'
else if (ext === '.gif') contentType = 'image/gif'
else if (ext === '.svg') contentType = 'image/svg+xml'
else if (ext === '.woff') contentType = 'font/woff'
else if (ext === '.woff2') contentType = 'font/woff2'
res.setHeader('Content-Type', contentType)
fs.createReadStream(localFilePath).pipe(res)
return
}
}
next()
})
}
}
], ],
base: './', // 打包路径 base: './', // 打包路径
server: { server: {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment