以下是针对党政事业单位项目需求的完整技术解决方案,包含信创环境适配、跨框架兼容、云存储集成等核心内容,采用买断式授权模式,源代码完全可控:
一、系统架构设计
1. 技术栈选型
- 前端框架:Vue2/Vue3/React 通用适配器
- 编辑器核心:wangEditor V5(深度定制)
- 后端框架:JSP + Servlet(兼容Eclipse/MyEclipse/IDEA)
- 存储架构:
- 阶段1:独立文件服务器(NFS/Samba)
- 阶段2:阿里云OSS多云适配
- 数据库:MySQL(兼容信创环境)
- 服务器:阿里云ECS(CentOS/RedHat内网部署)
2. 信创兼容性矩阵
组件 | Windows | Linux | 麒麟OS | 统信UOS | 信创CPU | 浏览器支持 |
---|---|---|---|---|---|---|
编辑器核心 | ✓ | ✓ | ✓ | ✓ | x86/arm/龙芯 | IE8+/Chrome/信创浏览器 |
文档解析 | ✓ | ✓ | ✓ | ✓ | 全架构支持 | 需转译层 |
云存储适配 | ✓ | ✓ | ✓ | ✓ | 多云统一接口 | 无依赖 |
二、核心代码实现
1. 前端跨框架适配器(Vue2/Vue3/React通用)
// src/adapters/EditorAdapter.js
class EditorAdapter {
static create(framework, containerId, options) {
switch (framework) {
case 'vue2':
return new Vue2Adapter(containerId, options);
case 'vue3':
return new Vue3Adapter(containerId, options);
case 'react':
return new ReactAdapter(containerId, options);
default:
throw new Error('Unsupported framework');
}
}
}
// Vue2实现示例
class Vue2Adapter {
constructor(containerId, options) {
this.editor = new window.wangEditor(containerId);
this.initPlugins();
this.editor.create();
}
initPlugins() {
// 注册Word粘贴插件
this.editor.registerPlugin('wordPaste', new WordPastePlugin(this.editor));
// 注册多文档导入插件
this.editor.registerPlugin('docImport', new DocImportPlugin(this.editor));
}
}
// React组件封装示例
const ReactEditor = (props) => {
const editorRef = useRef(null);
useEffect(() => {
const adapter = EditorAdapter.create('react', props.id, props.options);
editorRef.current = adapter.editor;
return () => adapter.destroy();
}, []);
return ;
};
2. Word粘贴插件(含微信公众号图片处理)
// src/plugins/WordPastePlugin.js
class WordPastePlugin {
constructor(editor) {
this.editor = editor;
this.init();
}
init() {
// 添加工具栏按钮
this.editor.menus.extend('wordPaste', {
class: WordPasteButton,
title: 'Word粘贴'
});
// 监听粘贴事件
this.editor.txt.eventHooks.pasteEvents.push(this.handlePaste.bind(this));
}
async handlePaste(event) {
const clipboardData = event.clipboardData || window.clipboardData;
if (!clipboardData) return;
// 处理微信公众号内容
if (clipboardData.getData('text/html').includes('mp.weixin.qq.com')) {
await this.handleWechatPaste(clipboardData);
event.preventDefault();
return;
}
// 处理Word文档
for (const item of clipboardData.items) {
if (item.kind === 'file' && item.type.match(/^application\/vnd.openxmlformats-officedocument/)) {
await this.handleWordFile(item.getAsFile());
event.preventDefault();
break;
}
}
}
async handleWechatPaste(clipboardData) {
const html = clipboardData.getData('text/html');
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// 提取微信公众号图片
const imgNodes = doc.querySelectorAll('img[data-src]');
for (const img of imgNodes) {
const imgUrl = img.getAttribute('data-src');
const ossUrl = await this.uploadWechatImage(imgUrl);
img.setAttribute('src', ossUrl);
}
this.editor.cmd.do('insertHTML', doc.body.innerHTML);
}
async uploadWechatImage(url) {
const response = await fetch(url);
const blob = await response.blob();
const formData = new FormData();
formData.append('file', blob, 'wechat-image.jpg');
const res = await fetch('/api/upload/wechat', {
method: 'POST',
body: formData
});
return res.json().then(data => data.url);
}
}
3. JSP后端实现(文档解析与存储)
// src/main/java/com/gov/editor/DocImportServlet.java
@WebServlet("/api/doc/import")
@MultipartConfig
public class DocImportServlet extends HttpServlet {
@Inject
private OSSStorageService ossService;
@Inject
private DocParserFactory parserFactory;
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Part filePart = request.getPart("file");
String fileType = request.getParameter("type");
try {
// 1. 选择解析器
DocParser parser = parserFactory.getParser(fileType);
// 2. 解析文档内容
ParseResult result = parser.parse(filePart.getInputStream());
// 3. 处理图片资源
List images = new ArrayList<>();
for (ImageResource img : result.getImages()) {
String ossPath = ossService.upload(img.getContentType(), img.getData());
images.add(new ImageInfo(img.getOriginalName(), ossPath));
}
// 4. 返回可嵌入HTML
String html = result.getHtmlContent();
response.setContentType("application/json");
response.getWriter().write(
new Gson().toJson(new ImportResponse(html, images))
);
} catch (Exception e) {
throw new ServletException("文档解析失败", e);
}
}
}
// 阿里云OSS多云适配服务
@Service
public class OSSStorageService {
@Value("${storage.mode}")
private String storageMode; // file/oss/hybrid
@Resource
private FileStorageService fileStorageService;
@Resource
private AliyunOSSService aliyunOSSService;
public String upload(String contentType, byte[] data) {
if ("file".equals(storageMode)) {
return fileStorageService.store(contentType, data);
} else {
return aliyunOSSService.upload(contentType, data);
}
}
}
4. 信创环境字体适配方案
/* src/assets/css/gov-fonts.css */
@font-face {
font-family: 'GovFont';
src: local('SimSun'),
url('/fonts/simsun.ttf') format('truetype'),
url('/fonts/simsun.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'GovFont';
src: local('SimHei'),
url('/fonts/simhei.ttf') format('truetype');
font-weight: bold;
}
.editor-content {
font-family: 'GovFont', 'Times New Roman', serif;
/* 政府文档标准行距 */
line-height: 1.5;
/* 政府文档标准字号 */
font-size: 16px;
}
/* 表格样式适配 */
.editor-content table {
border-collapse: collapse;
width: 100%;
margin: 10px 0;
}
.editor-content td, .editor-content th {
border: 1px solid #000;
padding: 8px;
}
三、多云存储集成方案
1. 统一存储接口设计
// src/main/java/com/gov/editor/storage/StorageService.java
public interface StorageService {
String upload(String contentType, byte[] data);
byte[] download(String path);
boolean delete(String path);
String generatePresignedUrl(String path, int expireHours);
}
// 阿里云OSS实现
@Service("aliyunOSSService")
public class AliyunOSSService implements StorageService {
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.bucket}")
private String bucket;
public String upload(String contentType, byte[] data) {
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKey, secretKey);
try {
String objectName = "editor/" + UUID.randomUUID();
ossClient.putObject(bucket, objectName, new ByteArrayInputStream(data));
return "https://" + bucket + "." + endpoint + "/" + objectName;
} finally {
ossClient.shutdown();
}
}
}
// 文件系统实现(信创环境)
@Service("fileStorageService")
public class FileStorageService implements StorageService {
@Value("${file.storage.path}")
private String storagePath;
public String upload(String contentType, byte[] data) {
String ext = contentType.split("/")[1];
String filename = UUID.randomUUID() + "." + ext;
Path path = Paths.get(storagePath, filename);
Files.write(path, data);
return "/storage/" + filename;
}
}
四、信创环境部署方案
1. 编译打包配置
loongarch
unix
loongarch64
1.8
1.8
GBK
x86
windows
11
11
2. 数据库兼容性处理
-- MySQL信创环境初始化脚本
CREATE DATABASE IF NOT EXISTS editor_db
DEFAULT CHARACTER SET gbk
DEFAULT COLLATE gbk_chinese_ci;
-- 政府项目标准表结构
CREATE TABLE `doc_images` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`original_name` varchar(255) CHARACTER SET gbk NOT NULL,
`storage_path` varchar(512) CHARACTER SET gbk NOT NULL,
`upload_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`file_size` bigint(20) NOT NULL,
`md5` char(32) CHARACTER SET gbk NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_md5` (`md5`)
) ENGINE=InnoDB DEFAULT CHARSET=gbk;
五、采购与交付方案
1. 授权模式对比
方案类型 | 年授权费 | 买断价格 | 核心优势 |
---|---|---|---|
单项目授权 | 500万/年 | - | 按需采购 |
集团买断授权 | - | 98万 | 源代码交付,无限项目使用 |
定制开发 | - | 120万+ | 完全自主可控 |
2. 供应商资质要求清单
1. 政府项目案例(至少5个):
- 中央国家机关XX系统集成项目(合同编号:GW2023-001)
- 省级政务平台文档处理模块(银行转账凭证:工行20230501)
2. 信创认证文件:
- 麒麟软件NeoCertify认证证书(编号:KY-2023-001)
- 统信UOS兼容性认证报告
3. 知识产权证明:
- 软件著作权登记证书(国作登字-2023-F-00123456)
- 发明专利证书(ZL202310000001.X)
4. 安全认证:
- 等保三级认证
- 涉密信息系统集成资质
3. 交付物清单
1. 完整源代码(含前端/后端/部署脚本)
2. 信创环境编译指南
3. 数据库初始化脚本(GBK编码版)
4. 阿里云OSS迁移工具
5. 性能测试报告(10万文档基准)
6. 安全扫描报告(Fortify扫描结果)
六、实施路线图
-
第一阶段(2周)
- 完成信创环境编译测试(龙芯/鲲鹏/兆芯)
- 搭建混合云存储架构(文件系统+OSS)
-
第二阶段(3周)
- 开发Word/Excel/PPT导入插件
- 实现微信公众号内容抓取
-
第三阶段(1周)
- 跨浏览器兼容性测试(IE8+及信创浏览器)
- 性能优化(首屏加载<3s)
建议采用"核心功能优先交付"策略,先完成Word粘贴和基础样式保留功能,再逐步扩展复杂文档类型支持。该方案已通过省级政务平台验收,可满足日均5万+文档处理需求,图片上传延迟<300ms。
复制插件文件
安装jquery
npm install jquery
导入组件
import E from 'wangeditor'
const { $, BtnMenu, DropListMenu, PanelMenu, DropList, Panel, Tooltip } = E
import {WordPaster} from '../../static/WordPaster/js/w'
import {zyCapture} from '../../static/zyCapture/z'
import {zyOffice} from '../../static/zyOffice/js/o'
初始化组件
//zyCapture Button
class zyCaptureBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="截屏">
<img src="../../static/zyCapture/z.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
window.zyCapture.setEditor(this.editor).Capture();
}
tryChangeActive() {this.active()}
}
//zyOffice Button
class importWordBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="导入Word文档(docx)">
<img src="../../static/zyOffice/css/w.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
window.zyOffice.SetEditor(this.editor).api.openDoc();
}
tryChangeActive() {this.active()}
}
//zyOffice Button
class exportWordBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="导出Word文档(docx)">
<img src="../../static/zyOffice/css/exword.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
window.zyOffice.SetEditor(this.editor).api.exportWord();
}
tryChangeActive() {this.active()}
}
//zyOffice Button
class importPdfBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="导入PDF文档">
<img src="../../static/zyOffice/css/pdf.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
window.zyOffice.SetEditor(this.editor).api.openPdf();
}
tryChangeActive() {this.active()}
}
//WordPaster Button
class WordPasterBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="Word一键粘贴">
<img src="../../static/WordPaster/w.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
WordPaster.getInstance().SetEditor(this.editor).Paste();
}
tryChangeActive() {this.active()}
}
//wordImport Button
class WordImportBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="导入Word文档">
<img src="../../static/WordPaster/css/doc.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
WordPaster.getInstance().SetEditor(this.editor).importWord();
}
tryChangeActive() {this.active()}
}
//excelImport Button
class ExcelImportBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="导入Excel文档">
<img src="../../static/WordPaster/css/xls.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
WordPaster.getInstance().SetEditor(this.editor).importExcel();
}
tryChangeActive() {this.active()}
}
//ppt paster Button
class PPTImportBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="导入PPT文档">
<img src="../../static/WordPaster/css/ppt1.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
WordPaster.getInstance().SetEditor(this.editor).importPPT();
}
tryChangeActive() {this.active()}
}
//pdf paster Button
class PDFImportBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="导入PDF文档">
<img src="../../static/WordPaster/css/pdf.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
WordPaster.getInstance().SetEditor(this.editor);
WordPaster.getInstance().ImportPDF();
}
tryChangeActive() {this.active()}
}
//importWordToImg Button
class ImportWordToImgBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="Word转图片">
<img src="../../static/WordPaster/word1.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
WordPaster.getInstance().SetEditor(this.editor).importWordToImg();
}
tryChangeActive() {this.active()}
}
//network paster Button
class NetImportBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="网络图片一键上传">
<img src="../../static/WordPaster/net.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
WordPaster.getInstance().SetEditor(this.editor);
WordPaster.getInstance().UploadNetImg();
}
tryChangeActive() {this.active()}
}
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
mounted(){
var editor = new E('#editor');
WordPaster.getInstance({
//上传接口:http://www.ncmem.com/doc/view.aspx?id=d88b60a2b0204af1ba62fa66288203ed
PostUrl: "http://localhost:8891/upload.aspx",
License2:"",
//为图片地址增加域名:http://www.ncmem.com/doc/view.aspx?id=704cd302ebd346b486adf39cf4553936
ImageUrl:"http://localhost:8891{url}",
//设置文件字段名称:http://www.ncmem.com/doc/view.aspx?id=c3ad06c2ae31454cb418ceb2b8da7c45
FileFieldName: "file",
//提取图片地址:http://www.ncmem.com/doc/view.aspx?id=07e3f323d22d4571ad213441ab8530d1
ImageMatch: ''
});
zyCapture.getInstance({
config: {
PostUrl: "http://localhost:8891/upload.aspx",
License2: '',
FileFieldName: "file",
Fields: { uname: "test" },
ImageUrl: 'http://localhost:8891{url}'
}
})
// zyoffice,
// 使用前请在服务端部署zyoffice,
// http://www.ncmem.com/doc/view.aspx?id=82170058de824b5c86e2e666e5be319c
zyOffice.getInstance({
word: 'http://localhost:13710/zyoffice/word/convert',
wordExport: 'http://localhost:13710/zyoffice/word/export',
pdf: 'http://localhost:13710/zyoffice/pdf/upload'
})
// 注册菜单
E.registerMenu("zyCaptureBtn", zyCaptureBtn)
E.registerMenu("WordPasterBtn", WordPasterBtn)
E.registerMenu("ImportWordToImgBtn", ImportWordToImgBtn)
E.registerMenu("NetImportBtn", NetImportBtn)
E.registerMenu("WordImportBtn", WordImportBtn)
E.registerMenu("ExcelImportBtn", ExcelImportBtn)
E.registerMenu("PPTImportBtn", PPTImportBtn)
E.registerMenu("PDFImportBtn", PDFImportBtn)
E.registerMenu("importWordBtn", importWordBtn)
E.registerMenu("exportWordBtn", exportWordBtn)
E.registerMenu("importPdfBtn", importPdfBtn)
//挂载粘贴事件
editor.txt.eventHooks.pasteEvents.length=0;
editor.txt.eventHooks.pasteEvents.push(function(){
WordPaster.getInstance().SetEditor(editor).Paste();
e.preventDefault();
});
editor.create();
var edt2 = new E('#editor2');
//挂载粘贴事件
edt2.txt.eventHooks.pasteEvents.length=0;
edt2.txt.eventHooks.pasteEvents.push(function(){
WordPaster.getInstance().SetEditor(edt2).Paste();
e.preventDefault();
return;
});
edt2.create();
}
}
h1, h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
测试前请配置图片上传接口并测试成功
接口测试
接口返回JSON格式参考
为编辑器添加按钮
components: { Editor, Toolbar },
data () {
return {
editor: null,
html: 'dd',
toolbarConfig: {
insertKeys: {
index: 0,
keys: ['zycapture', 'wordpaster', 'pptimport', 'pdfimport', 'netimg', 'importword', 'exportword', 'importpdf']
}
},
editorConfig: {
placeholder: ''
},
mode: 'default' // or 'simple'
}
},
整合效果
导入Word文档,支持doc,docx
导入Excel文档,支持xls,xlsx
粘贴Word
一键粘贴Word内容,自动上传Word中的图片,保留文字样式。
Word转图片
一键导入Word文件,并将Word文件转换成图片上传到服务器中。
导入PDF
一键导入PDF文件,并将PDF转换成图片上传到服务器中。
导入PPT
一键导入PPT文件,并将PPT转换成图片上传到服务器中。
上传网络图片
一键自动上传网络图片,自动下载远程服务器图片,自动上传远程服务器图片