Commit 868627e6 by pangchong

feat(editor): 集成 CodeMirror 6 实现高级 XML 编辑器组件

- 新增 CommonXmlEditor 组件,基于 CodeMirror 6 构建
- 支持自定义行号与差异行高亮功能
- 集成 XML 语法高亮、代码折叠、括号匹配、历史记录和搜索
- 实现基于 DTD 的自动补全,支持标签名、属性和枚举值提示
- 自适应亮暗主题,样式完全基于 CSS 变量
- 支持格式化粘贴的 XML 内容,提升编辑体验
- 拦截组件内 Ctrl+F 快捷键优先显示查找面板,避免冲突
- 添加依赖 codemirror 相关库,更新 package.json 和 package-lock.json
parent 05264f0b
...@@ -9,9 +9,15 @@ ...@@ -9,9 +9,15 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@alova/scene-vue": "^1.6.2", "@alova/scene-vue": "^1.6.2",
"@codemirror/autocomplete": "^6.20.3",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/language": "^6.12.4",
"@codemirror/state": "^6.7.1",
"@lezer/highlight": "^1.2.3",
"@vicons/ionicons5": "^0.13.0", "@vicons/ionicons5": "^0.13.0",
"@vueuse/core": "^14.3.0", "@vueuse/core": "^14.3.0",
"alova": "^3.5.0", "alova": "^3.5.0",
"codemirror": "^6.0.2",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"diff": "^7.0.0", "diff": "^7.0.0",
"less": "^4.5.1", "less": "^4.5.1",
...@@ -157,6 +163,101 @@ ...@@ -157,6 +163,101 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@codemirror/autocomplete": {
"version": "6.20.3",
"resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
"integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.10.4",
"resolved": "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.10.4.tgz",
"integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.7.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-xml": {
"version": "6.1.0",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
"integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/xml": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.12.4",
"resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.12.4.tgz",
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.9.7",
"resolved": "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.9.7.tgz",
"integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.42.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.7.1",
"resolved": "https://registry.npmmirror.com/@codemirror/search/-/search-6.7.1.tgz",
"integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.37.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.7.1",
"resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.7.1.tgz",
"integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.43.6",
"resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.43.6.tgz",
"integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.7.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@commitlint/cli": { "node_modules/@commitlint/cli": {
"version": "19.8.1", "version": "19.8.1",
"resolved": "https://registry.npmmirror.com/@commitlint/cli/-/cli-19.8.1.tgz", "resolved": "https://registry.npmmirror.com/@commitlint/cli/-/cli-19.8.1.tgz",
...@@ -988,6 +1089,47 @@ ...@@ -988,6 +1089,47 @@
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@lezer/common": {
"version": "1.5.2",
"resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.5.2.tgz",
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
"license": "MIT"
},
"node_modules/@lezer/highlight": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.3.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.10",
"resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.10.tgz",
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/xml": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/@lezer/xml/-/xml-1.0.6.tgz",
"integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
"integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
"license": "MIT"
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
...@@ -2135,6 +2277,21 @@ ...@@ -2135,6 +2277,21 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/codemirror": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/codemirror/-/codemirror-6.0.2.tgz",
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
...@@ -2300,6 +2457,12 @@ ...@@ -2300,6 +2457,12 @@
"jiti": "lib/jiti-cli.mjs" "jiti": "lib/jiti-cli.mjs"
} }
}, },
"node_modules/crelt": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/crelt/-/crelt-1.0.7.tgz",
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
"license": "MIT"
},
"node_modules/css-render": { "node_modules/css-render": {
"version": "0.15.14", "version": "0.15.14",
"resolved": "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz", "resolved": "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz",
...@@ -4418,6 +4581,12 @@ ...@@ -4418,6 +4581,12 @@
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
} }
}, },
"node_modules/style-mod": {
"version": "4.1.3",
"resolved": "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.3.tgz",
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"license": "MIT"
},
"node_modules/sucrase": { "node_modules/sucrase": {
"version": "3.35.1", "version": "3.35.1",
"resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz", "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz",
...@@ -5267,6 +5436,12 @@ ...@@ -5267,6 +5436,12 @@
"vue": "^3.0.11" "vue": "^3.0.11"
} }
}, },
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/webpack-virtual-modules": { "node_modules/webpack-virtual-modules": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
......
...@@ -17,9 +17,15 @@ ...@@ -17,9 +17,15 @@
}, },
"dependencies": { "dependencies": {
"@alova/scene-vue": "^1.6.2", "@alova/scene-vue": "^1.6.2",
"@codemirror/autocomplete": "^6.20.3",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/language": "^6.12.4",
"@codemirror/state": "^6.7.1",
"@lezer/highlight": "^1.2.3",
"@vicons/ionicons5": "^0.13.0", "@vicons/ionicons5": "^0.13.0",
"@vueuse/core": "^14.3.0", "@vueuse/core": "^14.3.0",
"alova": "^3.5.0", "alova": "^3.5.0",
"codemirror": "^6.0.2",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"diff": "^7.0.0", "diff": "^7.0.0",
"less": "^4.5.1", "less": "^4.5.1",
......
<template>
<div
class="xml-editor-container relative w-full border border-divider rounded-lg overflow-hidden bg-fill-2 flex flex-col"
:style="editorStyle"
@paste.capture="handlePasteCapture"
@keydown="handleKeyDown"
>
<div ref="editorContainerRef" class="flex-1 w-full h-full"></div>
</div>
</template>
<script setup lang="ts">
import { EditorState, StateField, StateEffect, RangeSetBuilder } from '@codemirror/state'
import { xml } from '@codemirror/lang-xml'
import { autocompletion, CompletionContext, startCompletion, closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete'
import { HighlightStyle, syntaxHighlighting, bracketMatching, foldGutter, foldKeymap, indentOnInput } from '@codemirror/language'
import { tags as t } from '@lezer/highlight'
import { useEditorStore } from '@/store/editor'
import {
EditorView,
Decoration,
DecorationSet,
lineNumbers,
highlightActiveLineGutter,
highlightSpecialChars,
drawSelection,
dropCursor,
rectangularSelection,
crosshairCursor,
highlightActiveLine,
keymap
} from '@codemirror/view'
import { history, defaultKeymap, historyKeymap } from '@codemirror/commands'
import { searchKeymap, highlightSelectionMatches, openSearchPanel, search, SearchQuery, getSearchQuery } from '@codemirror/search'
import { lintKeymap } from '@codemirror/lint'
interface Props {
modelValue: string
disabled?: boolean
height?: string | number
maxHeight?: string | number
minHeight?: string | number
lineClasses?: string[]
lineNumbers?: (string | number)[]
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
disabled: false,
minHeight: 200,
lineClasses: () => [],
lineNumbers: () => []
})
// ── 差异高亮与自定义行号的 CodeMirror 6 拓展实现 ──
const setLineClassesEffect = StateEffect.define<string[]>()
function getLineDecorations(lineClasses: string[], doc: any) {
const builder = new RangeSetBuilder<Decoration>()
for (let i = 1; i <= doc.lines; i++) {
const cls = lineClasses[i - 1]
if (cls) {
const line = doc.line(i)
builder.add(
line.from,
line.from,
Decoration.line({
attributes: { class: cls }
})
)
}
}
return builder.finish()
}
const lineClassesField = StateField.define<DecorationSet>({
create(state) {
return getLineDecorations(props.lineClasses || [], state.doc)
},
update(deco, tr) {
deco = deco.map(tr.changes)
for (const effect of tr.effects) {
if (effect.is(setLineClassesEffect)) {
deco = getLineDecorations(effect.value, tr.state.doc)
}
}
return deco
},
provide: (f) => EditorView.decorations.from(f)
})
const customLineNumbers = lineNumbers({
formatNumber: (lineNum) => {
if (props.lineNumbers && props.lineNumbers.length > 0) {
return String(props.lineNumbers[lineNum - 1] ?? '')
}
return String(lineNum)
}
})
const emit = defineEmits(['update:modelValue'])
const editorStyle = computed(() => {
const styles: Record<string, string> = {}
if (props.maxHeight !== undefined) {
const mh = props.maxHeight
const mhStr = typeof mh === 'number' || /^\d+$/.test(String(mh)) ? `${mh}px` : String(mh)
styles['--editor-max-height'] = mhStr
styles['--editor-height'] = 'auto'
} else {
styles['--editor-max-height'] = 'none'
}
if (props.height !== undefined) {
const h = props.height
const hStr = typeof h === 'number' || /^\d+$/.test(String(h)) ? `${h}px` : String(h)
styles['--editor-height'] = hStr
} else if (props.maxHeight === undefined) {
styles['--editor-height'] = '320px'
}
const minh = props.minHeight
const minhStr = typeof minh === 'number' || /^\d+$/.test(String(minh)) ? `${minh}px` : String(minh)
styles['--editor-min-height'] = minhStr
styles.minHeight = minhStr
styles.height = styles['--editor-height']
return styles
})
const editorContainerRef = ref<HTMLElement | null>(null)
let editorView: EditorView | null = null
const editorStore = useEditorStore()
// 自定义 CodeMirror 高亮样式(完全基于系统 CSS 变量)
const customHighlightStyle = HighlightStyle.define([
{ tag: t.tagName, color: 'var(--primary-color, #165DFF)', fontWeight: '600' },
{ tag: t.attributeName, color: 'var(--warning-color, #FF7D00)' },
{ tag: t.attributeValue, color: 'var(--success-color, #00B42A)' },
{ tag: t.comment, color: 'var(--textColor3, #999999)', fontStyle: 'italic' },
{ tag: t.angleBracket, color: 'var(--textColor3, #888888)' },
{ tag: t.content, color: 'var(--textColor1, #333333)' }
])
// 自定义 CodeMirror 主题样式(自适应亮暗主题)
const customTheme = EditorView.theme({
'&': {
backgroundColor: 'var(--card-color, #ffffff)',
color: 'var(--textColor1, #333333)',
height: 'var(--editor-height, 100%)',
minHeight: 'var(--editor-min-height, 300px)',
fontFamily: 'Fira Code, Consolas, Monaco, monospace',
fontSize: '13px'
},
'.cm-scroller': {
maxHeight: 'var(--editor-max-height, none)',
minHeight: 'inherit',
overflow: 'auto'
},
'.cm-content': {
caretColor: 'var(--primary-color, #165DFF)'
},
'.cm-cursor, .cm-dropCursor': {
borderLeftColor: 'var(--primary-color, #165DFF)'
},
'&.cm-focused': {
outline: 'none'
},
'.cm-gutters': {
backgroundColor: 'var(--colorFill2, #f5f7fa)',
color: 'var(--textColor3, #999999)',
borderRight: '1px solid var(--divider-color, #e8eaed)'
},
'.cm-activeLineGutter': {
backgroundColor: 'color-mix(in srgb, var(--primary-color, #165DFF) 12%, transparent)'
},
'.cm-activeLine': {
backgroundColor: 'color-mix(in srgb, var(--primary-color, #165DFF) 5%, transparent)'
},
'.cm-tooltip': {
backgroundColor: 'var(--card-color, #ffffff)',
border: '1px solid var(--divider-color, #e8eaed)',
borderRadius: '4px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'
},
'.cm-tooltip-autocomplete > ul': {
fontFamily: 'inherit',
fontSize: '12px',
color: 'var(--textColor2, #555555)'
},
'.cm-tooltip-autocomplete ul li[aria-selected]': {
backgroundColor: 'var(--primary-color, #165DFF)',
color: '#ffffff'
},
// 搜索面板自适应深浅主题样式
'.cm-panels': {
backgroundColor: 'var(--colorFill1, #fafafa) !important',
color: 'var(--textColor1, #333333)',
borderTop: '1px solid var(--divider-color, #e8eaed) !important',
borderBottom: '1px solid var(--divider-color, #e8eaed) !important'
},
'.cm-panels-top': {
borderBottom: '1px solid var(--divider-color, #e8eaed) !important'
},
'.cm-panels-bottom': {
borderTop: '1px solid var(--divider-color, #e8eaed) !important'
},
'.cm-search': {
display: 'flex',
alignItems: 'center',
padding: '5px 12px !important',
gap: '6px',
flexWrap: 'wrap',
position: 'relative'
},
'.cm-search label': {
display: 'inline-flex',
alignItems: 'center',
fontSize: '12px',
color: 'var(--textColor2, #555555)',
cursor: 'pointer',
gap: '2px',
margin: '0 2px !important'
},
'.cm-search input[type="checkbox"]': {
accentColor: 'var(--primary-color, #165DFF)',
marginRight: '2px',
cursor: 'pointer'
},
'.cm-textfield': {
backgroundColor: 'var(--card-color, #ffffff) !important',
color: 'var(--textColor1, #333333) !important',
border: '1px solid var(--border-color, #dee2e6) !important',
borderRadius: '4px !important',
padding: '3px 6px !important',
outline: 'none !important',
fontSize: '12px !important',
transition: 'border-color 0.15s ease, box-shadow 0.15s ease',
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.02) !important'
},
'.cm-textfield:focus': {
borderColor: 'var(--primary-color, #165DFF) !important',
boxShadow: '0 0 0 2px color-mix(in srgb, var(--primary-color, #165DFF) 15%, transparent) !important'
},
'.cm-button': {
backgroundColor: 'var(--colorFill3, #e8eaed) !important',
color: 'var(--textColor1, #333333) !important',
backgroundImage: 'none !important',
border: 'none !important',
borderRadius: '4px !important',
padding: '3px 8px !important',
fontSize: '12px !important',
fontWeight: '500 !important',
cursor: 'pointer !important',
transition: 'all 0.15s ease',
margin: '0 1px !important'
},
'.cm-button:hover': {
backgroundColor: 'var(--primary-color, #165DFF) !important',
color: '#ffffff !important',
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05) !important'
},
'.cm-button:active': {
opacity: '0.85'
},
'.cm-search button[name="close"]': {
position: 'absolute',
top: '50% !important',
transform: 'translateY(-50%)',
right: '8px !important',
color: 'var(--textColor3, #999999) !important',
backgroundColor: 'transparent !important',
border: 'none !important',
cursor: 'pointer !important',
width: '22px !important',
height: '22px !important',
display: 'inline-flex !important',
alignItems: 'center !important',
justifyContent: 'center !important',
borderRadius: '50% !important',
transition: 'all 0.15s ease',
fontSize: '14px !important',
padding: '0 !important',
margin: '0 !important'
},
'.cm-search button[name="close"]:hover': {
backgroundColor: 'var(--colorFill3, rgba(128, 128, 128, 0.15)) !important',
color: 'var(--textColor1, #333333) !important'
}
})
// 基于 DTD 的自动补全源
const xmlCompletionSource = (context: CompletionContext) => {
// 1. 如果正在输入标签名称,即匹配当前以 '<' 开头的标签单词部分,但匹配区间从 '<' 之后开始
const wordMatch = context.matchBefore(/[a-zA-Z0-9_-]*$/)
if (wordMatch) {
const charBefore = context.state.sliceDoc(wordMatch.from - 1, wordMatch.from)
if (charBefore === '<') {
const allTags = getAllElementNames()
// 获取当前选中节点允许的子节点列表,并提高推荐优先级
let allowedTags: string[] = []
const selectedNodeId = editorStore.selectedNodeId
if (selectedNodeId) {
const mapped = editorStore.nodeMap.get(selectedNodeId)
if (mapped) {
allowedTags = getAllowedChildren(mapped.node.tagName)
}
}
const options = allTags.map((tag) => {
const isAllowed = allowedTags.includes(tag)
const rule = getElementRule(tag)
return {
label: tag,
type: 'type',
detail: isAllowed ? ' 推荐' : '标签',
info: `XML 标签: ${tag}\nDTD 约束: ${rule?.contentModel.humanReadable || '(#PCDATA)'}`,
// 自动闭合标签并将光标放置在中间
apply: (view: EditorView, completion: any, from: number, to: number) => {
const insertText = `${tag}></${tag}>`
view.dispatch({
changes: { from, to, insert: insertText },
selection: { anchor: from + tag.length + 2 }
})
},
boost: isAllowed ? 99 : 0
}
})
return {
from: wordMatch.from,
options
}
}
}
// 2. 如果在标签内部(可能已有其他属性),获取当前光标前面的文本解析出所属的标签名
const textBefore = context.state.sliceDoc(Math.max(0, context.pos - 500), context.pos)
const lastLt = textBefore.lastIndexOf('<')
const lastGt = textBefore.lastIndexOf('>')
if (lastLt !== -1 && lastLt > lastGt) {
const tagText = textBefore.substring(lastLt)
const tagNameMatch = tagText.match(/^<([a-zA-Z0-9_-]+)/)
if (tagNameMatch) {
const tagName = tagNameMatch[1]
// 统计引号数量,确保当前光标不在属性值字符串字面量内部
const doubleQuotesCount = (tagText.match(/"/g) || []).length
const singleQuotesCount = (tagText.match(/'/g) || []).length
if (doubleQuotesCount % 2 === 0 && singleQuotesCount % 2 === 0) {
// 2.1 光标位于属性值外部,匹配目前正在输入的属性前缀(空格或等号后面的字母数字)
const lastWordMatch = tagText.match(/[\s=]([a-zA-Z0-9_-]*)$/)
if (lastWordMatch) {
const typedAttr = lastWordMatch[1]
// 提取当前标签内已经完整键入的属性名,避免在提示中重复出现
const typedAttrs = new Set<string>()
const attrRegex = /\s+([a-zA-Z0-9_-]+)\s*=\s*(['"])(.*?)\2/g
let m
while ((m = attrRegex.exec(tagText)) !== null) {
typedAttrs.add(m[1])
}
const attrs = getElementAttributes(tagName)
const options = Object.entries(attrs)
.filter(([name]) => !typedAttrs.has(name))
.map(([name, def]) => {
const isRequired = def.requirement === '#REQUIRED'
return {
label: name,
type: 'property',
detail: isRequired ? '🔴 必填' : '可选',
info: `属性: ${name}\n类型: ${def.typeDefinition}\n默认值: ${def.defaultValue || '无'}\n是否必填: ${isRequired ? '是' : '否'}`,
apply: `${name}=""`
}
})
return {
from: context.pos - typedAttr.length,
options
}
}
} else {
// 2.2 光标在属性值引号内部!我们来获取对应的属性名,并提示 DtdAttribute.enumValues 中的候选值
const isDouble = doubleQuotesCount % 2 === 1
const quoteChar = isDouble ? '"' : "'"
// 找到最后一个该引号字符的位置之前的内容
const lastQuoteIdx = tagText.lastIndexOf(quoteChar)
const textBeforeQuote = tagText.substring(0, lastQuoteIdx)
// 提取等号前的属性名称
const attrNameMatch = textBeforeQuote.match(/([a-zA-Z0-9_-]+)\s*=\s*$/)
if (attrNameMatch) {
const attrName = attrNameMatch[1]
const attrs = getElementAttributes(tagName)
const attrDef = attrs[attrName]
if (attrDef && attrDef.enumValues && attrDef.enumValues.length > 0) {
const typedVal = tagText.substring(lastQuoteIdx + 1)
const options = attrDef.enumValues.map((val) => {
return {
label: val,
type: 'value',
detail: '属性值',
apply: val
}
})
return {
from: context.pos - typedVal.length,
options
}
}
}
}
}
}
return null
}
const updateSearchCount = (view: EditorView) => {
const searchPanel = view.dom.querySelector('.cm-search')
if (!searchPanel) return
let countEl = searchPanel.querySelector('.cm-search-count') as HTMLElement
if (!countEl) {
countEl = document.createElement('span')
countEl.className = 'cm-search-count'
const searchInput = searchPanel.querySelector('input[name="search"]')
if (searchInput && searchInput.nextSibling) {
searchPanel.insertBefore(countEl, searchInput.nextSibling)
} else if (searchInput) {
searchPanel.appendChild(countEl)
}
}
const querySpec = getSearchQuery(view.state)
if (!querySpec || !querySpec.search || !querySpec.valid) {
countEl.textContent = ''
countEl.style.display = 'none'
return
}
try {
const cursor = querySpec.getCursor(view.state)
const matches: { from: number; to: number }[] = []
let next = cursor.next()
while (!next.done && matches.length < 1000) {
matches.push(next.value)
next = cursor.next()
}
const total = matches.length
if (total === 0) {
countEl.textContent = '0 / 0'
countEl.style.color = 'var(--error-color, #f5222d)'
} else {
const mainSelection = view.state.selection.main
let current = 0
for (let i = 0; i < total; i++) {
const m = matches[i]
if (mainSelection.from === m.from && mainSelection.to === m.to) {
current = i + 1
break
}
}
countEl.textContent = `${current} / ${total}`
countEl.style.color = 'var(--textColor3, #999999)'
}
countEl.style.display = 'inline-flex'
} catch (e) {
countEl.textContent = ''
countEl.style.display = 'none'
}
}
const initCodeMirror = () => {
if (!editorContainerRef.value) return
const startState = EditorState.create({
doc: props.modelValue,
extensions: [
customLineNumbers,
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
bracketMatching(),
closeBrackets(),
rectangularSelection(),
crosshairCursor(),
highlightActiveLine(),
highlightSelectionMatches(),
search({ top: true }),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap,
...searchKeymap
]),
lineClassesField,
xml(),
syntaxHighlighting(customHighlightStyle),
customTheme,
EditorState.readOnly.of(props.disabled),
EditorView.editable.of(!props.disabled),
...(!props.disabled ? [autocompletion({ override: [xmlCompletionSource] })] : []),
EditorView.updateListener.of((update) => {
updateSearchCount(update.view)
if (update.docChanged) {
const newValue = update.state.doc.toString()
if (props.modelValue !== newValue) {
emit('update:modelValue', newValue)
}
// 如果是用户输入导致的变化,且当前正处于可能需要提示的上下文中,自动激活提示
const isType = update.transactions.some((tr) => tr.isUserEvent('input.type'))
if (isType) {
const mainSelection = update.state.selection.main
if (mainSelection.empty) {
const pos = mainSelection.head
const textBefore = update.state.sliceDoc(Math.max(0, pos - 500), pos)
const lastLt = textBefore.lastIndexOf('<')
const lastGt = textBefore.lastIndexOf('>')
const isTypingTagName = /<[a-zA-Z0-9_-]*$/.test(textBefore)
const isInsideTag = lastLt !== -1 && lastLt > lastGt
if (isTypingTagName || isInsideTag) {
setTimeout(() => {
startCompletion(update.view)
}, 0)
}
}
}
}
// 如果光标位置改变,或者编辑器聚焦且选区为空(普通闪烁光标)
if (update.selectionSet || update.focusChanged) {
const mainSelection = update.state.selection.main
if (mainSelection.empty && update.view.hasFocus) {
const pos = mainSelection.head
const beforeStr = update.state.sliceDoc(Math.max(0, pos - 50), pos)
const afterStr = update.state.sliceDoc(pos, Math.min(update.state.doc.length, pos + 50))
// 检查光标是否正好落在两个引号之间 "" 或 ''
const isBetweenQuotes =
(beforeStr.endsWith('"') && afterStr.startsWith('"')) || (beforeStr.endsWith("'") && afterStr.startsWith("'"))
if (isBetweenQuotes) {
const matchAttr = beforeStr.match(/([a-zA-Z0-9_-]+)\s*=\s*['"]$/)
if (matchAttr) {
setTimeout(() => {
startCompletion(update.view)
}, 0)
}
}
}
}
})
]
})
editorView = new EditorView({
state: startState,
parent: editorContainerRef.value
})
}
watch(
() => props.modelValue,
(newVal) => {
if (editorView && editorView.state.doc.toString() !== newVal) {
editorView.dispatch({
changes: { from: 0, to: editorView.state.doc.length, insert: newVal }
})
if (props.lineClasses && props.lineClasses.length > 0) {
editorView.dispatch({
effects: setLineClassesEffect.of(props.lineClasses)
})
}
}
}
)
watch(
() => props.disabled,
() => {
if (editorView) {
editorView.destroy()
initCodeMirror()
}
}
)
watch(
() => props.lineClasses,
(newClasses) => {
if (editorView && newClasses) {
editorView.dispatch({
effects: setLineClassesEffect.of(newClasses)
})
}
},
{ deep: true }
)
watch(
() => props.lineNumbers,
() => {
if (editorView) {
editorView.dispatch({})
}
},
{ deep: true }
)
const handleFormat = () => {
const currentVal = props.modelValue
if (!currentVal) return
const formatted = formatXmlText(currentVal)
if (editorView && formatted !== currentVal) {
editorView.dispatch({
changes: { from: 0, to: editorView.state.doc.length, insert: formatted }
})
emit('update:modelValue', formatted)
}
}
// 在 Vue 层捕获阶段拦截粘贴事件,优先于 CodeMirror 内部任何处理
const handlePasteCapture = (event: ClipboardEvent) => {
const text = event.clipboardData?.getData('text')
if (!text || (!text.includes('<') && !text.includes('>'))) return
event.preventDefault()
event.stopPropagation()
const formatted = formatXmlText(text)
if (editorView) {
editorView.dispatch({
changes: editorView.state.selection.ranges.map((range) => ({
from: range.from,
to: range.to,
insert: formatted
}))
})
emit('update:modelValue', editorView.state.doc.toString())
}
}
// 阻止快捷键事件冒泡至 window,避免与主编辑器的全局 Ctrl+F 或 Escape 冲突
const handleKeyDown = (event: KeyboardEvent) => {
const isMod = event.ctrlKey || event.metaKey
const key = event.key.toLowerCase()
if ((isMod && key === 'f') || key === 'escape') {
event.stopPropagation()
}
}
// 在捕获阶段拦截 Ctrl+F:只要本组件已挂载到 DOM,就优先使用组件内置查找面板
// 注意:useKeyboardShortcuts 会通过 .xml-editor-container 选择器跳过主编辑器的查找
const handleGlobalCtrlF = (event: KeyboardEvent) => {
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 'f') return
if (!editorView || !editorContainerRef.value?.isConnected) return
event.preventDefault()
openSearchPanel(editorView)
}
onMounted(() => {
initCodeMirror()
window.addEventListener('keydown', handleGlobalCtrlF, true)
})
onBeforeUnmount(() => {
window.removeEventListener('keydown', handleGlobalCtrlF, true)
if (editorView) {
editorView.destroy()
}
})
defineExpose({
handleFormat
})
</script>
...@@ -27,8 +27,10 @@ export function useKeyboardShortcuts(options: ShortcutOptions = {}, enableGlobal ...@@ -27,8 +27,10 @@ export function useKeyboardShortcuts(options: ShortcutOptions = {}, enableGlobal
} }
const target = e.target as HTMLElement | null const target = e.target as HTMLElement | null
const isInput = target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') const isInputOrEditor =
if (isInput) return target &&
(target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable || target.closest('.cm-editor'))
if (isInputOrEditor) return
if (ctrl) { if (ctrl) {
const key = e.key.toLowerCase() const key = e.key.toLowerCase()
...@@ -65,6 +67,8 @@ export function useKeyboardShortcuts(options: ShortcutOptions = {}, enableGlobal ...@@ -65,6 +67,8 @@ export function useKeyboardShortcuts(options: ShortcutOptions = {}, enableGlobal
if (options.onFind) { if (options.onFind) {
onKeyStroke(['f', 'F'], (e: KeyboardEvent) => { onKeyStroke(['f', 'F'], (e: KeyboardEvent) => {
if (e.ctrlKey || e.metaKey) { if (e.ctrlKey || e.metaKey) {
// 若页面上存在已挂载的 CommonXmlEditor,则让它自己处理 Ctrl+F,跳过主编辑器的查找
if (document.querySelector('.xml-editor-container')) return
e.preventDefault() e.preventDefault()
options.onFind!() options.onFind!()
} }
......
...@@ -156,12 +156,53 @@ export function serializeTreeToXml(node: XmlNode, indent: number = 0, compact: b ...@@ -156,12 +156,53 @@ export function serializeTreeToXml(node: XmlNode, indent: number = 0, compact: b
*/ */
export function formatXmlText(xmlStr: string): string { export function formatXmlText(xmlStr: string): string {
if (!xmlStr) return '' if (!xmlStr) return ''
try {
const tree = parseXmlToTree(xmlStr) // 规范化空白字符,先去除标签之间的所有换行和缩进,以重新对其进行对齐排版
return serializeTreeToXml(tree, 0, false) const cleanXml = xmlStr.trim().replace(/>\s+</g, '><')
} catch (e) {
return xmlStr // 正则分割标签与文本内容
const tokens = cleanXml.split(/(<[^>]+>)/g).filter(t => t.trim() !== '')
let indentLevel = 0
const lines: string[] = []
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i].trim()
if (!token) continue
if (token.startsWith('</')) {
// 闭合标签,减少缩进
indentLevel = Math.max(0, indentLevel - 1)
lines.push(' '.repeat(indentLevel) + token)
} else if (token.startsWith('<') && token.endsWith('/>')) {
// 自闭合标签
lines.push(' '.repeat(indentLevel) + token)
} else if (token.startsWith('<?') || token.startsWith('<!')) {
// XML 声明、注释、或者 DTD 声明
lines.push(' '.repeat(indentLevel) + token)
} else if (token.startsWith('<')) {
// 开始标签
const matchTag = token.match(/^<([a-zA-Z0-9_:-]+)/)
const tagName = matchTag ? matchTag[1] : null
const nextToken = tokens[i + 1]?.trim()
const afterNextToken = tokens[i + 2]?.trim()
// 检查这是否是一个单行标签(即:开始标签 + 纯文本内容 + 结束标签)
if (tagName && nextToken && !nextToken.startsWith('<') && afterNextToken === `</${tagName}>`) {
lines.push(' '.repeat(indentLevel) + token + nextToken + afterNextToken)
i += 2 // 跳过这两个 token
} else {
lines.push(' '.repeat(indentLevel) + token)
indentLevel++
}
} else {
// 纯文本内容
lines.push(' '.repeat(indentLevel) + token)
} }
}
return lines.join('\n')
} }
/** /**
......
...@@ -6,6 +6,7 @@ export function useInsertFragmentModal() { ...@@ -6,6 +6,7 @@ export function useInsertFragmentModal() {
const visible = ref(false) const visible = ref(false)
const xmlContent = ref('') const xmlContent = ref('')
const isSaving = ref(false) const isSaving = ref(false)
const hasClipboardContent = ref(false)
const insertModeSetting = ref<'above' | 'below' | 'inside'>('below') const insertModeSetting = ref<'above' | 'below' | 'inside'>('below')
const targetNodeIdSetting = ref<string | undefined>(undefined) const targetNodeIdSetting = ref<string | undefined>(undefined)
...@@ -17,11 +18,15 @@ export function useInsertFragmentModal() { ...@@ -17,11 +18,15 @@ export function useInsertFragmentModal() {
} }
targetNodeIdSetting.value = targetId targetNodeIdSetting.value = targetId
xmlContent.value = '' xmlContent.value = ''
hasClipboardContent.value = false
try { try {
if (navigator.clipboard && navigator.clipboard.readText) { if (navigator.clipboard && navigator.clipboard.readText) {
const text = await navigator.clipboard.readText() const text = await navigator.clipboard.readText()
if (text && text.trim()) { if (text && text.trim()) {
xmlContent.value = text.trim() // 如果剪贴板内容含有 XML 标签,自动格式化后再填入
const raw = text.trim()
xmlContent.value = (raw.includes('<') || raw.includes('>')) ? formatXmlText(raw) : raw
hasClipboardContent.value = true
} }
} }
} catch (err) { } catch (err) {
...@@ -53,6 +58,7 @@ export function useInsertFragmentModal() { ...@@ -53,6 +58,7 @@ export function useInsertFragmentModal() {
xmlContent, xmlContent,
isSaving, isSaving,
open, open,
handleConfirm handleConfirm,
hasClipboardContent
} }
} }
<template> <template>
<CommonModal v-model="visible" title="插入 XML 片段" :width="600" :loading="isSaving" confirm-text="插入" @confirm="handleConfirm"> <CommonModal
:scrollable="false"
v-model="visible"
title="插入 XML 片段"
:width="900"
:loading="isSaving"
confirm-text="插入"
@confirm="handleConfirm"
>
<div class="flex flex-col space-y-3 p-1"> <div class="flex flex-col space-y-3 p-1">
<!-- 头部说明性描述 -->
<div class="text-xs text-color3"> <div class="text-xs text-color3">
请输入从其他地方复制的 XML 节点片段,系统将自动解析节点结构,并根据当前选中节点及 DTD 架构规则进行兼容性校验: 请输入从其他地方复制的 XML 节点片段,系统将自动解析节点结构,并根据当前选中节点及 DTD 架构规则进行兼容性校验:
</div> </div>
<n-input v-model:value="xmlContent" type="textarea" rows="10" placeholder="例如:<PARAC>测试记录行</PARAC>" />
<!-- 编辑器组件 -->
<CommonXmlEditor ref="xmlEditorRef" v-model="xmlContent" :max-height="hasClipboardContent ? '70vh' : undefined" />
<!-- 底部快捷键或帮助提示 -->
<div class="text-[10px] text-color3 flex items-center justify-between">
<span>提示: 输入 &lt; 可唤起 XML 节点建议;在标签内输入空格可唤起属性建议。</span>
<span>支持高亮与折叠</span>
</div>
</div> </div>
<template #footer-extra>
<CommonButton secondary @click="handleFormat" :disabled="!xmlContent.trim()">格式化</CommonButton>
</template>
</CommonModal> </CommonModal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useInsertFragmentModal } from './functionals' import { useInsertFragmentModal } from './functionals'
const { visible, xmlContent, isSaving, open, handleConfirm } = useInsertFragmentModal() const { visible, xmlContent, isSaving, open, handleConfirm, hasClipboardContent } = useInsertFragmentModal()
const xmlEditorRef = ref<any>(null)
const handleFormat = () => {
xmlEditorRef.value?.handleFormat()
}
defineExpose({ defineExpose({
open, open,
......
import { useStashStore, type StashItem } from '@/store/stash' import { useStashStore, type StashItem } from '@/store/stash'
import { useEditorStore } from '@/store/editor' import { useEditorStore } from '@/store/editor'
import { serializeTreeToXml, parseXmlToTreeAsync } from '@/utils/xmlParser'
import { openDownloadModal } from '@/utils/render'
export function useStashModal() { export function useStashModal() {
const visible = ref(false) const visible = ref(false)
......
...@@ -137,17 +137,19 @@ ...@@ -137,17 +137,19 @@
</div> </div>
<!-- 代码预览区 --> <!-- 代码预览区 -->
<div <div v-if="isXmlTextLoading" class="flex-grow flex flex-col gap-2 animate-pulse p-4 border border-divider rounded-lg bg-card">
class="flex-1 overflow-auto border border-divider rounded-lg bg-card p-3 relative font-mono text-[11px] leading-5 select-text custom-scrollbar"
>
<div v-if="isXmlTextLoading" class="flex flex-col gap-2 animate-pulse py-1">
<div class="h-3 bg-fill-3 rounded w-4/5"></div> <div class="h-3 bg-fill-3 rounded w-4/5"></div>
<div class="h-3 bg-fill-3 rounded w-full"></div> <div class="h-3 bg-fill-3 rounded w-full"></div>
<div class="h-3 bg-fill-3 rounded w-3/4"></div> <div class="h-3 bg-fill-3 rounded w-3/4"></div>
<div class="h-3 bg-fill-3 rounded w-5/6"></div> <div class="h-3 bg-fill-3 rounded w-5/6"></div>
<div class="h-3 bg-fill-3 rounded w-2/3"></div> <div class="h-3 bg-fill-3 rounded w-2/3"></div>
</div> </div>
<pre v-else class="m-0 text-color2 whitespace-pre-wrap break-all">{{ activeItemXmlText }}</pre> <div v-else class="flex-grow min-h-0 flex flex-col">
<CommonXmlEditor
v-model="activeItemXmlText"
disabled
height="100%"
/>
</div> </div>
<!-- 操作面板 --> <!-- 操作面板 -->
......
<template> <template>
<CommonModal v-model="viewXmlVisible" :title="viewXmlTitle" :width="900" :show-confirm="false" cancel-text="关闭"> <CommonModal :scrollable="false" v-model="viewXmlVisible" :title="viewXmlTitle" :width="900" :show-confirm="false" cancel-text="关闭">
<div class="flex flex-col space-y-4"> <div class="flex flex-col space-y-4">
<!-- XML 渲染面板,带优雅网格背景与代码字体 --> <!-- 使用全局 CommonXmlEditor 组件展示只读的 XML 内容 -->
<div <CommonXmlEditor :model-value="formattedXmlContent" disabled max-height="70vh" />
class="rounded-xl overflow-hidden border border-divider shadow-lg relative bg-fill-4"
style="background-image: radial-gradient(circle, rgba(0, 0, 0, 0.02) 1px, transparent 1px); background-size: 16px 16px"
>
<div class="p-6 font-mono text-sm leading-relaxed overflow-x-auto max-h-[500px] scrollbar-thin select-all">
<pre class="text-color1" @copy.prevent="handleCopyXml"><code class="xml-content-pre">{{ formattedXmlContent }}</code></pre>
</div>
</div>
</div> </div>
<template #footer-extra> <template #footer-extra>
...@@ -32,10 +25,4 @@ const { viewXmlVisible, viewXmlTitle, formattedXmlContent, handleCopyXml, open } ...@@ -32,10 +25,4 @@ const { viewXmlVisible, viewXmlTitle, formattedXmlContent, handleCopyXml, open }
defineExpose({ open }) defineExpose({ open })
</script> </script>
<style scoped> <style scoped></style>
.xml-content-pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
white-space: pre-wrap;
word-break: break-all;
}
</style>
...@@ -68,7 +68,7 @@ export default defineConfig(({ mode }) => { ...@@ -68,7 +68,7 @@ export default defineConfig(({ mode }) => {
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
headers: { headers: {
Cookie: '_udid=264b2935-6cad-4d00-8a65-7b54d1ecf2db; JSESSIONID=DE507139FF27D7D5BA18A673DCD3EC71; _amro_sk=d2bf7778-f4b7-497a-a8c5-b6fa9cfe3369' Cookie: '_udid=5bf34532-a66b-477d-81dc-8c0f4079f6bc; JSESSIONID=48C6C0153A0F552336259B30A2D4D8DC; _amro_sk=b51dbda2-701b-4a17-983d-3770fad9966f'
} }
}, },
'/mnt': { '/mnt': {
......
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