This commit is contained in:
2025-07-28 17:01:13 +08:00
parent f88caa57e2
commit 05b849470f
5 changed files with 508 additions and 198 deletions

View File

@@ -1,209 +1,95 @@
<template>
<div class="xml-importer">
<input type="file" @change="handleFileUpload" accept=".xml" />
<button @click="importToEditor" :disabled="!xmlData">导入到编辑器</button>
<div ref="editor"></div>
<div>
<div ref="editor" style="text-align:left"></div>
<button @click="exportToXml">导出为XML</button>
<input type="file" accept=".xml" @change="handleXmlUpload">
</div>
</template>
<script>
import E from 'wangeditor'
import E from 'wangeditor';
import { js2xml, xml2js } from 'xml-js';
export default {
name: 'XmlImporter',
data() {
return {
editor: null,
xmlData: null,
parsedContent: ''
}
editor: null
};
},
mounted() {
this.initEditor()
this.editor = new E(this.$refs.editor);
this.editor.create();
},
methods: {
initEditor() {
this.editor = new E(this.$refs.editor)
this.editor.create()
},
// 处理XML文件上传
handleFileUpload(event) {
const file = event.target.files[0]
if (!file) return
convertHtmlToXml(htmlContent) {
const xmlObject = {
declaration: {
attributes: {
version: '1.0',
encoding: 'UTF-8'
}
},
elements: [{
type: 'element',
name: 'richText',
elements: [{
type: 'text',
text: htmlContent
}]
}]
};
const reader = new FileReader()
reader.onload = (e) => {
this.xmlData = e.target.result
this.parseS1000DXml(this.xmlData)
}
reader.readAsText(file)
return js2xml(xmlObject, { compact: false, spaces: 4 });
},
// 解析S1000D XML
parseS1000DXml(xmlString) {
downloadXml(content, fileName = 'content.xml') {
const blob = new Blob([content], { type: 'application/xml' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = fileName;
link.click();
URL.revokeObjectURL(link.href);
},
exportToXml() {
const html = this.editor.txt.html();
const xml = this.convertHtmlToXml(html);
this.downloadXml(xml);
},
parseXmlToHtml(xmlString) {
try {
const parser = new DOMParser()
const xmlDoc = parser.parseFromString(xmlString, "text/xml")
// 检查是否是有效的S1000D文档
if (xmlDoc.getElementsByTagName('parsererror').length > 0) {
throw new Error('XML解析错误: ' + xmlDoc.getElementsByTagName('parsererror')[0].textContent)
const result = xml2js(xmlString, { compact: false });
const richTextElement = result.elements.find(el => el.name === 'richText');
if (richTextElement && richTextElement.elements && richTextElement.elements.length > 0) {
return richTextElement.elements[0].text;
}
// 提取内容部分
const contentElement = xmlDoc.querySelector('content description')
if (!contentElement) {
throw new Error('未找到content/description元素')
}
// 将S1000D XML转换为HTML
this.parsedContent = this.s1000dToHtml(contentElement)
} catch (error) {
console.error('XML解析失败:', error)
alert(`解析失败: ${error.message}`)
return '';
} catch (e) {
console.error('XML解析错误:', e);
return '';
}
},
// 将S1000D元素转换为HTML
s1000dToHtml(s1000dElement) {
let html = ''
handleXmlUpload(event) {
const file = event.target.files[0];
if (!file) return;
// 递归处理子节点
const processNode = (node) => {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent
const reader = new FileReader();
reader.onload = (e) => {
const xmlString = e.target.result;
const html = this.parseXmlToHtml(xmlString);
if (html) {
this.editor.txt.html(html);
}
if (node.nodeType !== Node.ELEMENT_NODE) return ''
const tag = node.tagName.toLowerCase()
const children = Array.from(node.childNodes).map(processNode).join('')
const attrs = Array.from(node.attributes)
.map(attr => ` ${attr.name}="${this.escapeHtml(attr.value)}"`)
.join('')
// 将S1000D标签映射回HTML
switch(tag) {
case 'para': {
return `<p${attrs}>${children}</p>`
}
case 'emphasis': {
const role = node.getAttribute('role') || 'bold'
switch(role) {
case 'bold': return `<strong${attrs}>${children}</strong>`
case 'italic': return `<em${attrs}>${children}</em>`
case 'underline': return `<u${attrs}>${children}</u>`
default: return `<span class="emphasis-${role}"${attrs}>${children}</span>`
}
}
case 'orderedlist': {
return `<ol${attrs}>${children}</ol>`
}
case 'itemizedlist': {
return `<ul${attrs}>${children}</ul>`
}
case 'listitem': {
return `<li${attrs}>${children}</li>`
}
case 'figure': {
const graphic = node.querySelector('graphic')
if (graphic) {
const imgAttrs = Array.from(graphic.attributes)
.filter(attr => attr.name !== 'infoentityid')
.map(attr => ` ${attr.name}="${this.escapeHtml(attr.value)}"`)
.join('')
return `<div class="figure"><img${imgAttrs}>${children}</div>`
}
return children
}
case 'table': {
return `<table${attrs}>${children}</table>`
}
case 'row': {
return `<tr${attrs}>${children}</tr>`
}
case 'entry': {
const isHeader = node.getAttribute('thead') === 'yes'
return isHeader
? `<th${attrs}>${children}</th>`
: `<td${attrs}>${children}</td>`
}
case 'title': {
const level = node.getAttribute('level') || '1'
return `<h${level}${attrs}>${children}</h${level}>`
}
case 'xref': {
const href = node.getAttribute('xrefid') || '#'
return `<a href="${href}"${attrs}>${children}</a>`
}
default: {
// 未知标签作为div处理
return `<div class="s1000d-${tag}"${attrs}>${children}</div>`
}
}
}
// 处理所有子节点
html = Array.from(s1000dElement.childNodes).map(processNode).join('')
return html
},
// HTML特殊字符转义
escapeHtml(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
},
// 将解析后的内容导入编辑器
importToEditor() {
if (this.parsedContent && this.editor) {
this.editor.txt.html(this.parsedContent)
alert('内容已成功导入编辑器')
}
};
reader.readAsText(file);
}
},
beforeDestroy() {
if (this.editor) {
this.editor.destroy()
this.editor.destroy();
}
}
}
</script>
<style>
.xml-importer {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.xml-importer input[type="file"] {
margin-bottom: 15px;
display: block;
}
.xml-importer button {
padding: 8px 16px;
background: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.xml-importer button:disabled {
background: #cccccc;
cursor: not-allowed;
}
.figure {
margin: 15px 0;
text-align: center;
}
</style>
};
</script>

View File

@@ -5,11 +5,36 @@
<button @click="insertVideo">上传视频</button>
<button @click="loadAllContent">加载所有内容</button>
<button @click="generateTOC">生成目录</button>
<button @click="deleteAllVideos">删除所有视频</button>
<input type="color">
<!-- <button @click="deleteAllVideos">删除所有视频</button> -->
<button @click="loadFwb">模拟加载</button>
<button @click='test'>测试</button>
<!-- <button @click='test'>测试</button> -->
</div>
<div class="s1000d-editor-container">
<!-- 工具栏 -->
<div class="toolbar">
<button @click="exportToS1000D" class="tool-btn">导出S1000D XML</button>
<label for="xml-upload" class="tool-btn">导入S1000D XML</label>
<input
id="xml-upload"
type="file"
accept=".xml"
@change="handleS1000DUpload"
style="display: none"
/>
<button @click="validateCurrentXml" class="tool-btn">验证XML</button>
</div>
<!-- 编辑器区域 -->
<div class="editor-area" style="display:none;">
<div ref="editor" class="editor"></div>
<div class="xml-preview" style="display: none">
<h3>XML预览</h3>
<pre>{{ formattedXmlPreview }}</pre>
</div>
</div>
</div>
<div id="main" class="content-preview"></div>
<div id="nav" class="toc-container"></div>
<div id="editor" ref="editor"></div>
@@ -23,6 +48,8 @@
<script>
import WangEditor from 'wangeditor'
// import '@yaireo/colorpicker/dist/colorpicker.min.css';
// import '@yaireo/colorpicker'
// import axios from 'axios';
// window.handleMessageFromDotNet = function(msg) {
// alert("Received message from C#: " + msg);
@@ -40,6 +67,47 @@ export default {
editorContent: '',
editorContent2:'',
hasVideoSelected: false,
xmlPreview: '',
showValidationModal: false,
validationResult: {
valid: false,
message: '',
details: ''
},
// 简单的S1000D模板配置
dmConfig: {
dmc: {
modelIdentCode: 'AAA',
systemDiffCode: 'BBB',
systemCode: 'CCC',
subSystemCode: 'DDD',
subSubSystemCode: 'EEE',
assyCode: 'FFF',
disassyCode: 'GGG',
disassyCodeVariant: 'HHH',
infoCode: 'III',
infoCodeVariant: 'JJJ',
itemLocationCode: 'KKK'
},
issueInfo: {
issueNumber: '001',
inWork: '01',
issueDate: new Date().toISOString().split('T')[0]
},
language: 'zh-CN'
}
}
},
computed: {
formattedXmlPreview() {
if (!this.xmlPreview) return '暂无XML预览';
// 简单格式化XML显示
return this.xmlPreview
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>')
.replace(/\s/g, '&nbsp;');
}
},
mounted() {
@@ -82,6 +150,14 @@ console.log('webview对象是否存在:', !!window.chrome?.webview);
let dm = this.editor.config.menus;
console.log("默认菜单",dm)
// 启用颜色选择功能
this.editor.config.colors = [
'#000000', '#ffffff', '#eeeef1',
'#ff0000', '#ff5e5e', '#ffbbbb',
'#0033ff', '#0055ff', '#3d7eff',
'red',"#096","#9cf"
]
// 完全自定义菜单
this.editor.config.menus = [
'image', // 图片
@@ -95,8 +171,8 @@ console.log('webview对象是否存在:', !!window.chrome?.webview);
'strikeThrough', // 删除线
// 'line',//行高
'lineHeight',//
// 'foreColor', // 文字颜色
// 'backColor', // 背景颜色
'foreColor', // 文字颜色
'backColor', // 背景颜色
// 'link', // 链接
'list', // 列表
// 'todo',//
@@ -107,7 +183,7 @@ console.log('webview对象是否存在:', !!window.chrome?.webview);
// 'code', // 代码
'splitLine',//分割线
'undo', // 撤销
'redo' // 重做
'redo', // 重做
]
// 创建编辑器
@@ -135,7 +211,7 @@ console.log('webview对象是否存在:', !!window.chrome?.webview);
this.editor.cmd.do('insertHTML', `<img src="${imgUrl}" style="max-width: 100%;" alt="图片">`)
},
// 插入视频(可靠版本)
// 插入视频
insertVideo() {
const videoUrl = 'http://youneed.top:10017/uploads/video.mp4'
const videoId = `video-${Date.now()}`
@@ -183,16 +259,12 @@ console.log('webview对象是否存在:', !!window.chrome?.webview);
},
bindDel(){
},
deleteAllVideos() {
const videoWrappers = document.querySelectorAll('.video-wrapper')
if (videoWrappers.length === 0) {
alert('没有找到可删除的视频')
console.log('没有找到可删除的视频')
return
}
@@ -209,7 +281,7 @@ console.log('webview对象是否存在:', !!window.chrome?.webview);
}
})
// 修正:使用正确的方式通知内容变更
// 使用正确的方式通知内容变更
if (this.editor.txt) {
this.editor.txt.html(this.editor.txt.html()) // 强制更新编辑器内容
}
@@ -251,7 +323,7 @@ console.log('webview对象是否存在:', !!window.chrome?.webview);
const mainContainer = document.getElementById('main')
const headings = mainContainer.querySelectorAll('h1, h2, h3, h4, h5, h6')
console.log("headings.length",headings.length)
if (headings.length === 0) {
navContainer.innerHTML = '<p>没有找到标题元素,无法生成目录。</p>'
return
@@ -299,6 +371,182 @@ console.log('webview对象是否存在:', !!window.chrome?.webview);
tocContainer.appendChild(tocTitle)
tocContainer.appendChild(tocList)
navContainer.appendChild(tocContainer)
},
// 生成S1000D XML模板
generateS1000DTemplate(content) {
const { dmc, issueInfo, language } = this.dmConfig;
return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dmodule [
<!ENTITY % ISOEntities PUBLIC "ISO 8879-1986//ENTITIES ISO Character Entities 20030531//EN//XML" "http://www.s1000d.org/S1000D_4-1/ent/ISOEntities">
%ISOEntities;
]>
<dmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.s1000d.org/S1000D_4-1/xml_schema_flat/descript.xsd">
<idstatus>
<dmaddress>
<dmc>
<avee>${dmc.modelIdentCode}-${dmc.systemDiffCode}-${dmc.systemCode}-${dmc.subSystemCode}-${dmc.subSubSystemCode}-${dmc.assyCode}-${dmc.disassyCode}-${dmc.disassyCodeVariant}</avee>
<avee>${dmc.infoCode}-${dmc.infoCodeVariant}-${dmc.itemLocationCode}</avee>
</dmc>
</dmaddress>
<issueinfo>
<issue number="${issueInfo.issueNumber}" inwork="${issueInfo.inWork}" date="${issueInfo.issueDate}"/>
<language country="${language.split('-')[1]}" language="${language.split('-')[0]}"/>
</issueinfo>
</idstatus>
<content>
<description>
${this.escapeXml(content)}
</description>
</content>
</dmodule>`;
},
// XML特殊字符转义
escapeXml(unsafe) {
return unsafe.replace(/[<>&'"]/g, (c) => {
switch (c) {
case '<': return '&lt;';
case '>': return '&gt;';
case '&': return '&amp;';
case '\'': return '&apos;';
case '"': return '&quot;';
default: return c;
}
});
},
// 更新XML预览
updateXmlPreview(html) {
try {
this.xmlPreview = this.generateS1000DTemplate(html);
} catch (e) {
console.error('生成XML预览失败:', e);
this.xmlPreview = `生成XML预览时出错: ${e.message}`;
}
},
// 导出为S1000D XML文件
exportToS1000D() {
const html = this.editor.txt.html();
const xmlContent = this.generateS1000DTemplate(html);
// 下载文件
const blob = new Blob([xmlContent], { type: 'application/xml' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `s1000d-${this.dmConfig.dmc.modelIdentCode}-${this.dmConfig.issueInfo.issueNumber}.xml`;
link.click();
URL.revokeObjectURL(link.href);
},
// 从S1000D XML导入
handleS1000DUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const xmlString = e.target.result;
const html = this.extractContentFromS1000D(xmlString);
if (html) {
this.editor.txt.html(html);
this.updateXmlPreview(html);
}
// 重置input值允许重复选择同一文件
event.target.value = '';
};
reader.readAsText(file);
},
// 从S1000D XML提取内容
extractContentFromS1000D(xmlString) {
try {
// 简单提取description内容
const descriptionMatch = xmlString.match(/<description>([\s\S]*?)<\/description>/i);
if (!descriptionMatch || !descriptionMatch[1]) {
throw new Error('未找到description内容');
}
// 反转义XML特殊字符
let content = descriptionMatch[1]
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/&apos;/g, "'")
.replace(/&quot;/g, '"');
// 移除可能的多余空格和换行
content = content.trim();
return content;
} catch (e) {
console.error('S1000D导入错误:', e);
alert(`导入S1000D XML失败: ${e.message}`);
return '';
}
},
// 验证当前XML
validateCurrentXml() {
const validationResult = this.validateS1000DXml(this.xmlPreview);
this.validationResult = validationResult;
this.showValidationModal = true;
},
// 基本S1000D XML验证
validateS1000DXml(xmlString) {
try {
// 检查基本结构
const hasDmodule = xmlString.includes('<dmodule');
const hasIdstatus = xmlString.includes('<idstatus');
const hasContent = xmlString.includes('<content');
if (!hasDmodule) {
return {
valid: false,
message: '无效的S1000D文档: 缺少dmodule根元素'
};
}
if (!hasIdstatus) {
return {
valid: false,
message: '缺少必需的S1000D元素: idstatus'
};
}
if (!hasContent) {
return {
valid: false,
message: '缺少必需的S1000D元素: content'
};
}
// 检查dmc结构
const hasDmc = xmlString.includes('<dmc>');
const hasAvee = xmlString.includes('<avee>');
if (!hasDmc || !hasAvee) {
return {
valid: false,
message: '无效的dmc结构: 缺少dmc或avee元素'
};
}
return {
valid: true,
message: 'XML文档符合S1000D基本结构要求'
};
} catch (e) {
return {
valid: false,
message: `验证过程中发生错误: ${e.message}`
};
}
}
},
@@ -357,6 +605,7 @@ console.log('webview对象是否存在:', !!window.chrome?.webview);
border: 1px solid #eee;
padding: 15px;
background: #fafafa;
text-align: left;
}
#main th,
#main td {
@@ -418,4 +667,96 @@ video {
/* background-color:#096; */
display: none !important;
}
.s1000d-editor-container {
display: flex;
flex-direction: column;
/*height: 100vh;*/
padding: 20px;
box-sizing: border-box;
}
.toolbar {
margin-bottom: 15px;
}
.tool-btn {
padding: 8px 15px;
margin-right: 10px;
background-color: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.tool-btn:hover {
background-color: #66b1ff;
}
.editor-area {
display: flex;
flex: 1;
gap: 20px;
}
.editor {
flex: 1;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.xml-preview {
flex: 1;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 10px;
overflow: auto;
background-color: #f5f7fa;
}
.xml-preview pre {
white-space: pre-wrap;
font-family: Consolas, Monaco, monospace;
margin: 0;
}
.modal {
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
padding: 20px;
border-radius: 5px;
width: 60%;
max-height: 80%;
overflow: auto;
}
.close {
float: right;
font-size: 24px;
cursor: pointer;
}
.valid {
color: #67c23a;
}
.invalid {
color: #f56c6c;
}
/deep/ .editor .w-e-text{
text-align: left;
}
</style>