Files
IETM-web/src/components/wang.vue
2025-07-28 17:01:13 +08:00

762 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="editor-container">
<div class="toolbar">
<button @click="insertImage">上传图片</button>
<button @click="insertVideo">上传视频</button>
<button @click="loadAllContent">加载所有内容</button>
<button @click="generateTOC">生成目录</button>
<input type="color">
<!-- <button @click="deleteAllVideos">删除所有视频</button> -->
<button @click="loadFwb">模拟加载</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>
<div id="editor2" ref="editor2"></div>
<!-- <button @click="callWpfMethod">调用WPF方法</button> -->
<input type="text" v-model="val1">
<input type="text" v-model="val2">
<button @click="sendMessageToHost('李四','张三')">传递</button>
</div>
</template>
<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);
// }
export default {
name: 'RichTextEditor',
data() {
return {
val1:'',
val2:'',
wpfData: '',
message: '',
editor: null,
editor2:null,
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() {
this.initEditor()
// this.initEditor2();
// window.receiveMessageFromWpf = this.receiveMessageFromWpf;
console.log('chrome对象是否存在:', !!window.chrome);
console.log('webview对象是否存在:', !!window.chrome?.webview);
},
beforeDestroy() {
// 销毁编辑器
if (this.editor) {
this.editor.destroy()
}
},
methods: {
test() {
},
sendMessageToHost() {
this.$sendToDotNet(this.val1,this.val2);
},
loadFwb() {
this.editor2.txt.html(this.editorContent)
// 创建编辑器
},
initEditor() {
this.editor = new WangEditor(this.$refs.editor)
// 配置编辑器
this.editor.config.uploadImgShowBase64 = true // 使用 base64 保存图片
this.editor.config.onchange = (html) => {
this.editorContent = html
}
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', // 图片
'video', // 视频
'head', // 标题
'bold', // 粗体
// 'fontSize',//字号
// 'fontName',//字体
'italic', // 斜体
'underline', // 下划线
'strikeThrough', // 删除线
// 'line',//行高
'lineHeight',//
'foreColor', // 文字颜色
'backColor', // 背景颜色
// 'link', // 链接
'list', // 列表
// 'todo',//
'justify', // 对齐方式
// 'quote', // 引用
// 'emoticon',//表情
'table', // 表格
// 'code', // 代码
'splitLine',//分割线
'undo', // 撤销
'redo', // 重做
]
// 创建编辑器
this.editor.create();
this.editor2 = new WangEditor(this.$refs.editor2)
// 配置编辑器
this.editor2.config.uploadImgShowBase64 = true // 使用 base64 保存图片
this.editor2.config.onchange = (html) => {
console.log("22222",html)
// this.editorContent2 = html
}
this.editor2.create()
},
// 上传图片
insertImage() {
const imgUrl = 'http://youneed.top:10017/uploads/1.jpg'
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()}`
// 创建视频HTML
const videoHtml = `
<div class="video-wrapper" data-video-id="${videoId}">
<video controls width="50%" style='margin:auto' data-video-id="${videoId}">
<source src="${videoUrl}" type="video/mp4">
</video>
<div class="video-controls" style='display:none;'>
<span class="video-delete" data-video-id="${videoId}">× 删除</span>
</div>
</div>
<p><br></p>
`
// 使用编辑器命令插入
this.editor.cmd.do('insertHTML', videoHtml)
// 添加删除事件监听
this.$nextTick(() => {
const btn = document.querySelector(`button[data-video-id="${videoId}"]`)
if (btn) {
btn.onclick = (e) => {
e.preventDefault()
this.deleteVideoById(videoId)
}
}
})
},
// 根据ID删除视频修正版
deleteVideoById(videoId) {
const container = document.querySelector(`.video-container[data-video-id="${videoId}"]`)
if (container) {
container.remove()
this.editor.txt.html(this.editor.txt.html()) // 刷新编辑器
}
},
deleteAllVideos() {
const videoWrappers = document.querySelectorAll('.video-wrapper')
if (videoWrappers.length === 0) {
console.log('没有找到可删除的视频')
return
}
videoWrappers.forEach(wrapper => {
wrapper.remove()
})
// 删除可能残留的空段落
const editor = this.$refs.editor
const paragraphs = editor.querySelectorAll('p')
paragraphs.forEach(p => {
if (p.textContent.trim() === '' && p.children.length === 0) {
p.remove()
}
})
// 使用正确的方式通知内容变更
if (this.editor.txt) {
this.editor.txt.html(this.editor.txt.html()) // 强制更新编辑器内容
}
console.log(`已删除 ${videoWrappers.length} 个视频`)
},
// 加载所有内容到 main 容器
loadAllContent() {
const mainContainer = document.getElementById('main')
mainContainer.innerHTML = this.editorContent;
console.log("this.editorContent",this.editorContent)
// 为main容器中的标题添加ID
this.addHeadingIds(mainContainer)
},
// 为标题元素添加ID
addHeadingIds(container) {
const headings = container.querySelectorAll('h1, h2, h3, h4, h5, h6')
headings.forEach((heading, index) => {
if (!heading.id) {
heading.id = `heading-${index}-${Date.now()}`
}
})
},
// 生成带锚点的目录
generateTOC() {
const navContainer = document.getElementById('nav')
navContainer.innerHTML = '' // 清空原有目录
// 确保main容器已加载内容
if (!document.getElementById('main').innerHTML) {
this.loadAllContent()
}
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
}
const tocList = document.createElement('ul')
tocList.style.listStyleType = 'none'
tocList.style.paddingLeft = '0'
headings.forEach(heading => {
const level = parseInt(heading.tagName.substring(1))
const listItem = document.createElement('li')
listItem.style.marginLeft = `${(level - 1) * 15}px`
listItem.style.marginBottom = '5px'
const link = document.createElement('a')
link.href = `#${heading.id}`
link.textContent = heading.textContent
link.style.textDecoration = 'none'
link.style.color = '#333'
// 添加平滑滚动效果
link.addEventListener('click', (e) => {
e.preventDefault()
document.getElementById(heading.id).scrollIntoView({
behavior: 'smooth'
})
})
listItem.appendChild(link)
tocList.appendChild(listItem)
})
const tocContainer = document.createElement('div')
tocContainer.style.border = '1px solid #ddd'
tocContainer.style.padding = '15px'
tocContainer.style.marginBottom = '20px'
tocContainer.style.background = '#f9f9f9'
tocContainer.style.borderRadius = '4px'
const tocTitle = document.createElement('h2')
tocTitle.textContent = '目录'
tocTitle.style.marginTop = '0'
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}`
};
}
}
},
created() {
// 方式1直接访问全局变量
this.$watch(
() => this.$dotNetMessage,
(newMsg) => {
console.log('收到消息:', newMsg)
}
)
// 方式2监听全局事件
window.addEventListener('dotnet-message', (e) => {
console.log('通过事件收到:', e.detail)
})
}
}
</script>
<style>
.editor-container {
width: 80%;
margin: 0 auto;
padding: 20px;
}
.toolbar {
margin-bottom: 10px;
}
.toolbar button {
margin-right: 10px;
padding: 5px 10px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.toolbar button:hover {
background: #66b1ff;
}
#editor,#editor2 {
border: 1px solid #ddd;
min-height: 300px;
padding: 10px;
text-align: left;
}
.content-preview {
margin-top: 20px;
border: 1px solid #eee;
padding: 15px;
background: #fafafa;
text-align: left;
}
#main th,
#main td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
#main th {
background-color: #f2f2f2;
}
/* 视频容器样式 */
.video-wrapper {
position: relative;
margin: 15px 0;
/* border: 1px solid #ddd; */
border-radius: 4px;
overflow: hidden;
}
.video-controls {
position: absolute;
top: 5px;
right: 5px;
z-index: 10;
}
.video-delete {
display: inline-block;
padding: 2px 8px;
background: rgba(255, 0, 0, 0.7);
color: white;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
user-select: none;
}
.video-delete:hover {
background: rgba(255, 0, 0, 0.9);
}
/* 确保视频响应式 */
video {
max-width: 100%;
display: block;
background: #000;
}
/* 禁用菜单项样式 */
.disabled-menu-item {
opacity: 0.5 !important;
cursor: not-allowed !important;
pointer-events: none !important;
}
/* 如果需要工具提示也禁用 */
.w-e-toolbar .w-e-menu:nth-child(1),
.w-e-toolbar .w-e-menu:nth-child(2) {
/* 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>