承接上一篇《Vue3 项目集成 OnlyOffice 在线编辑 + 自定义插件开发(一)》,本文将聚焦 OnlyOffice 自定义插件的核心功能实现------包括表格插入、HTML 格式内容插入,以及通过正则匹配实现特殊字符自动识别与一键填充(如公司信息、日期等),全程附完整代码及实操说明,新手可直接复制复用。
核心目标:通过 Vue3 前端触发操作,结合 OnlyOffice 插件桥接层,实现对在线文档的灵活插入操作,解决实际开发中"批量填充""格式保留"的核心需求,提升文档编辑效率。
一、插入表格:一键插入行数据到文档表格
场景说明:在 Vue 页面中点击按钮,将表格行数据提取并插入到 OnlyOffice 文档的指定表格中,支持自动匹配单元格顺序,无需手动输入。
1.1 Vue 页面:按钮触发与数据提取
通过自定义按钮触发插入操作,提取当前行的有效字段(过滤隐藏列和操作列),拼接后传递给父组件,由父组件与桥接层通信。
javascript
// 按钮配置(表格操作列按钮)
{
render: 'tipButton',
name: 'info',
title: '插入行数据',
text: '',
type: 'success',
icon: 'fa fa-file-text-o',
class: 'table-row-edit',
disabledTip: false,
click: (row: TableRow) => {
handleInsertRow(row) // 点击触发行数据插入
},
}
// 提取当前行有效内容(过滤隐藏列和操作列)
const handleInsertRow = (row: any) => {
// 筛选显示的列,排除操作列(operation)
const columns = baTable.table.column.filter((item) => item.show !== false && item.prop !== 'operation')
// 拼接行内容,用$作为分隔符(后续拆分插入单元格)
const rowContent = columns.map((item) => row[item.prop]).join('$')
// 调用插入表格方法,传递拼接后的内容
handleInsertTable(rowContent)
}
// 向父组件发送插入指令(携带内容和类型)
const handleInsertTable = (content: any) => {
const textToInsert = content || ''
emit('insert', textToInsert, 'table') // 触发父组件insert事件,类型标记为table
}
1.2 父组件:接收指令并与桥接层通信
父组件通过监听子组件的 insert 事件,根据插入类型(table)组装请求参数,通过 window.parent.postMessage 向 OnlyOffice 桥接层发送插入指令。
javascript
<!-- 父组件中引入子组件,监听insert事件 -->
<KeepAlive>
<component :is="currentComponent" @insert="insertIntoDoc" />
</KeepAlive>
<script setup lang="ts">
// 接收子组件插入指令,向桥接层发送消息
const insertIntoDoc = (text: string, type?: string) => {
let action = ''
// 根据插入类型匹配对应的OnlyOffice操作
switch (type) {
case 'table':
action = 'insertTable' // 插入表格标识
break
case 'html':
action = 'insertHtml' // 插入HTML标识
break
default:
action = 'insertText' // 默认插入纯文本
break
}
// 组装请求参数,转为JSON字符串(OnlyOffice要求格式)
const payload = JSON.stringify({
action,
text: String(text),
})
// 向桥接层发送消息(*表示允许所有源,实际开发可限制具体域名)
window.parent.postMessage(payload, '*')
// ElMessage.success('已发送插入指令') // 可选:添加操作提示
}
</script>
1.3 桥接层核心代码(关键实现)
在上一篇提到的 public/tender-plugin/index.html(OnlyOffice 插件桥接层)中,添加表格插入的核心逻辑,解析 Vue 发送的指令,调用 OnlyOffice 原生 API 实现单元格数据填充。
先补充桥接层完整页面结构(含加载遮罩,提升用户体验),再重点实现表格插入方法:
javascript
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>ONLYOFFICE 桥接层</title>
<!-- 引入OnlyOffice插件SDK(本地引入,避免跨域问题) -->
<script src="./plugins.js"></script>
<style>
body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; }
iframe { border: none; width: 100%; height: 100%; }
/* 加载遮罩:插入操作时显示,提升体验 */
.loading-mask {
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
background: rgba(0, 0, 0, 0.6); display: none; justify-content: center; align-items: center; z-index: 9999;
}
.loading-mask.show { display: flex; }
.loading-box {
display: flex; align-items: center; gap: 10px;
background: #fff; padding: 14px 24px; border-radius: 8px; font-size: 16px; color: #333;
}
.loading-spin {
width: 18px; height: 18px; border: 2px solid #999; border-top-color: #1677ff;
border-radius: 50%; animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<iframe id="office-plugin-resource"></iframe>
<div class="loading-mask" id="loadingMask">
<div class="loading-box">
<div class="loading-spin"></div>
<span>正在处理中...</span>
</div>
</div>
<script>
// 1. 与 ONLYOFFICE 底层握手初始化
window.Asc.plugin.init = function () {
console.log('🌉 [桥接层] ONLYOFFICE 底层握手成功,权限已就绪!')
}
// 配置iframe路径(适配项目部署路径,避免404)
const urlPrefix = location.pathname.includes('/web-client') ? '/web-client' : ''
document.querySelector('#office-plugin-resource').src = `${location.origin}${urlPrefix}/#/plugin-panel`
// 2. 监听 Vue 发来的消息,执行对应插入操作
window.addEventListener('message', function (event) {
// 确保收到的是纯字符串(防止OnlyOffice崩溃,过滤无效消息)
if (typeof event.data === 'string') {
try {
var data = JSON.parse(event.data)
// 插入表格逻辑
if (data && data.action === 'insertTable') {
console.log('🌉 [桥接层] 行数据', data.text)
insertDataToCells(data.text) // 调用表格插入方法
}
// 其他插入逻辑(后续章节补充)
} catch (e) {
// 忽略系统自带的杂乱消息,避免插件报错
}
}
})
// 3. 表格插入核心方法:将拼接的行数据拆分,插入到对应单元格
function insertDataToCells(rowText) {
// 存储行数据到OnlyOffice作用域(跨方法访问)
Asc.scope.rowText = rowText
// 调用OnlyOffice原生命令,操作文档(异步执行)
window.Asc.plugin.callCommand(
function () {
const rowText = Asc.scope.rowText
const oDocument = Api.GetDocument() // 获取当前文档对象
const oParagraph = oDocument.GetCurrentParagraph() // 获取当前段落
// 异常处理:如果当前位置没有段落,直接返回
if (!oParagraph) return
// 判定当前是否在表格单元格内(不在则直接插入文本)
if (!oParagraph.GetParentTableCell || !oParagraph.GetParentTableCell()) {
oParagraph.AddText(rowText.replace(/\$/g, ' '))
return
}
// 拆分行数据(按$分隔,对应表格列顺序)
const rowItems = rowText.split('$')
let currentCell = oParagraph.GetParentTableCell() // 获取当前单元格
// 依次将数据插入到当前单元格及后续单元格
for (let i = 0; i < rowItems.length; i++) {
if (!currentCell) break // 没有更多单元格,终止循环
// 清空单元格原有内容,插入新数据并居中显示
const oCellContent = currentCell.GetContent()
const oCellParagraph = oCellContent.GetElement(0)
oCellParagraph.RemoveAllElements() // 清空原有内容
oCellParagraph.SetJc('center') // 文本居中
oCellParagraph.AddText(rowItems[i]) // 插入当前列数据
currentCell = currentCell.GetNext() // 切换到下一个单元格
}
},
false,
false,
function (result) {} // 回调函数(可添加操作结果提示)
)
}
// 插件关闭时清理资源
window.Asc.plugin.button = function (id) {
this.executeCommand('close', '')
}
</script>
</body>
</html>
1.4 效果说明
点击 Vue 页面表格中的"插入行数据"按钮,即可将当前行的所有有效字段,按顺序插入到 OnlyOffice 文档中当前选中的表格单元格内,自动清空原有内容并居中显示,无需手动调整格式。
二、插入HTML文本:保留格式插入富文本内容
场景说明:插入带有自定义格式(如字体、颜色、换行)的 HTML 内容到 OnlyOffice 文档,利用 OnlyOffice 的PasteHtml API 实现格式完美保留,适用于插入模板化内容、带样式的文本等场景。
2.1 Vue 页面:按钮触发与HTML内容提取
与表格插入逻辑类似,通过按钮触发,提取需要插入的 HTML 内容(如模板内容),传递给父组件并指定插入类型为 html。
javascript
// 按钮配置(与表格插入按钮类似,可复用样式)
{
render: 'tipButton',
name: 'info',
title: '插入模板内容',
text: '',
type: 'success',
icon: 'fa fa-file-text-o',
class: 'table-row-edit',
disabledTip: false,
click: (row: TableRow) => {
handleInsertRow(row)
},
}
// 提取当前行的HTML内容(假设content字段存储HTML模板)
const handleInsertRow = (row: any) => {
handleInsertHtml(row['content']) // 传递HTML内容
}
// 向父组件发送插入指令,类型标记为html
const handleInsertHtml = (content: any) => {
const textToInsert = content || ''
emit('insert', textToInsert, 'html')
}
2.2 桥接层核心代码(补充HTML插入逻辑)
在桥接层的 message 监听事件中,添加 insertHtml 分支,调用 OnlyOffice 的 PasteHtml API,直接解析 HTML 内容并插入到文档中(格式自动保留)。
javascript
// 继续在window.addEventListener('message', ...)中添加如下逻辑
else if (data && data.action === 'insertHtml') {
console.log('🌉 [桥接层] 插入 HTML ')
// OnlyOffice 核心API:PasteHtml,直接解析并插入富文本内容
window.Asc.plugin.executeMethod('PasteHtml', [data.text])
}
2.3 效果说明
插入的 HTML 内容(如带有字体颜色、换行、加粗的文本)会完美保留原有格式,直接渲染到 OnlyOffice 文档中,无需手动调整样式,适用于插入固定模板、带格式的说明文本等场景。
三、一键填充:正则匹配特殊字符,自动插入固定内容
核心场景:通过正则表达式匹配文档中的特殊字符(如"日期:""公司名称:"),一键插入预设内容(如当前日期、公司信息),支持多字段同时填充,大幅提升文档编辑效率(本文以日期填充为例,其他字段可直接复用逻辑)。
3.1 Vue 页面:一键填充交互界面与逻辑
设计交互界面(复选框选择填充字段、日期选择器自定义日期),点击"一键填充"按钮,组装填充参数,发送给桥接层。
javascript
<template>
<div class="ba-table-box">
<!-- 填充字段选择 -->
<el-form :model="form" label-width="auto" style="max-width: 600px">
<el-form-item label="">
<el-checkbox-group v-model="form.type" class="checkbox-wrapper">
<el-checkbox value="date" name="type"> 签署日期 </el-checkbox>
</el-checkbox-group>
</el-form-item>
<!-- 自定义签署日期(选中日期字段时显示) -->
<el-form-item label="自定义签署日期" v-show="form.type.includes('date')">
<el-date-picker
v-model="selectedDate"
type="date"
placeholder="请选择签署日期"
:clearable="false"
style="width: 200px"
/>
</el-form-item>
</el-form>
<!-- 一键填充按钮 -->
<el-row justify="start" style="margin-top: 16px">
<el-button type="primary" size="small" plain @click="handleAutoFill">一键填充</el-button>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { getCompanyInfo } from '/@/api/backend/business' // 接口:获取公司信息(自定义)
defineOptions({
name: 'autoFill',
})
// 响应式日期变量,默认初始化为今天
const selectedDate = ref<Date>(new Date())
/*
* 匹配模式说明:
* mode: 1 → 匹配项后直接插入(有下划线则保留下划线格式)
* mode: 2 → 居中雷达探测模式(适配"年 月 日"分散填空场景)
*/
const distObj = reactive({
date: {
match: [
// 匹配"编制日期:____年__月__日"这类格式(含可选编制二字、空格、下划线)
{
keyword: '(?:编[^\\S\\n\\r]*制[^\\S\\n\\r]*)?日[^\\S\\n\\r]*期[^\\S\\n\\r]*[::](?:[^\\S\\n\\r]|[__\\----])*(年(?:[^\\S\\n\\r]|[__\\----])*月(?:[^\\S\\n\\r]|[__\\----])*日)',
isRegex: true,
mode: '2',
},
// 匹配"____年__月__日"这类纯日期填空格式
{
keyword: '(?:[^\\S\\n\\r]|[__\\----])*年(?:[^\\S\\n\\r]|[__\\----])*月(?:[^\\S\\n\\r]|[__\\----])*日',
isRegex: true,
mode: '2',
},
// 匹配"日期:"后无内容的场景(直接在后面插入)
{
keyword: '(?:编[^\\S\\n\\r]*制[^\\S\\n\\r]*)?日[^\\S\\n\\r]*期[^\\S\\n\\r]*[::](?![^\\S\\n\\r]*[0-9一二三四五六七八九十壹贰叁肆伍陆柒捌玖拾])',
isRegex: true,
mode: '1',
},
],
value: '', // 填充值(动态计算)
valueObj: { year: '', month: '', day: '' }, // 日期拆分(适配mode:2)
},
})
// 选中的填充字段
const form = reactive({
type: ['companyName', 'date', 'address', 'phone', 'postcode'],
})
// 一键填充核心逻辑:组装参数,发送给桥接层
const handleAutoFill = () => {
// 动态解析用户选择的日期,赋值给distObj
if (form.type.includes('date') && selectedDate.value) {
const d = selectedDate.value
const year = d.getFullYear().toString()
const month = (d.getMonth() + 1).toString()
const day = d.getDate().toString()
// 覆盖日期填充值(年/月/日拆分,适配mode:2的分散填空)
distObj.date.value = `${year}年${month}月${day}日`
distObj.date.valueObj = { year, month, day }
}
// 组装需要填充的字段(筛选选中的字段)
const fields: any[] = []
Object.keys(distObj).forEach((key) => {
if (form.type.includes(key)) {
fields.push(distObj[key as keyof typeof distObj])
}
})
// 向桥接层发送一键填充指令
const payload = JSON.stringify({
action: 'autoFill',
fields: fields,
})
window.parent.postMessage(payload, '*')
}
// 初始化:获取公司信息(填充公司名称、地址等字段,可根据实际接口调整)
const fetchData = async () => {
try {
const res = await getCompanyInfo()
const resData = res?.data ?? {}
// 将接口返回的公司信息赋值给distObj
Object.keys(distObj).forEach((key) => {
if (key in resData) {
distObj[key].value = resData[key]
} else if (distObj[key].alias) {
// 适配自定义字段(如接口返回的customFields数组)
const aliasArr = distObj[key].alias.split('.')
if (aliasArr[0] == 'customFields') {
distObj[key].value = resData['customFields'].find((item) => item.fieldName == aliasArr[1])?.fieldValue || ''
}
}
})
} catch (error) {
console.error('获取公司信息失败:', error)
}
}
// 页面挂载时获取公司信息
onMounted(() => {
fetchData()
})
</script>
<style scoped lang="scss">
.checkbox-wrapper {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 16px;
}
</style>
3.2 桥接层核心代码:正则匹配与自动填充
在桥接层中添加 autoFill 指令处理逻辑,实现正则匹配文档中的特殊字符,根据匹配模式插入对应内容,支持下划线格式保留、分散填空(如日期"年/月/日")等场景。
javascript
// 1. 在message监听事件中添加autoFill分支
else if (data && data.action === 'autoFill') {
console.log('🌉 [桥接层] 自动填充', data)
handleAutoFill(data.fields) // 调用一键填充核心方法
}
// 2. 一键填充核心方法(含正则匹配、格式处理、内容插入)
function handleAutoFill(fields) {
// 显示加载遮罩,提升用户体验
const loadingMask = document.getElementById('loadingMask')
if (loadingMask) {
loadingMask.classList.add('show')
}
// 存储填充字段到OnlyOffice作用域
Asc.scope.autoFillFields = fields
// 调用OnlyOffice原生命令,操作文档(异步执行)
window.Asc.plugin.callCommand(
() => {
try {
const autoFillFields = Asc.scope.autoFillFields
const oDocument = Api.GetDocument() // 获取当前文档对象
const filledDateTags = new Set() // 去重:避免同一匹配项重复填充
// 遍历所有需要填充的字段
autoFillFields.forEach((item) => {
const insertString = item.value // 要插入的内容
const matchFields = generateMatchKeywords(oDocument, item.match) // 生成匹配规则
// 遍历每个匹配规则,执行填充
matchFields.forEach((match) => {
const { keyword, mode } = match
if (filledDateTags.has(keyword)) return // 跳过已填充的匹配项
filledDateTags.add(keyword)
// 正则匹配文档中的目标内容
const aSearchResults = oDocument.Search(keyword)
if (aSearchResults.length <= 0) return // 无匹配项,跳过
// 从后往前处理(避免修改文档后,前面的匹配位置失效)
for (let i = aSearchResults.length - 1; i >= 0; i--) {
const oRange = aSearchResults[i]
const keywordStartPos = oRange.GetStartPos()
const keywordEndPos = oRange.GetEndPos()
// 根据匹配模式执行不同的插入逻辑
switch (mode) {
// 模式1:匹配项后直接插入(处理下划线、单元格等场景)
case '1':
// 检测是否属于模式2的场景(避免误判)
let isMode2Territory = false
try {
// 向后侦测20个字符,判断是否有"年"字(模式2的特征)
const checkRange = oDocument.GetRange(keywordEndPos, keywordEndPos + 20)
if (checkRange) {
const checkText = checkRange.GetText() || ''
// 20字符内有"年"字且无换行,判定为模式2场景,跳过
if (checkText.indexOf('年') !== -1 && checkText.indexOf('\r') === -1 && checkText.indexOf('\n') === -1) {
isMode2Territory = true
}
}
} catch (e) {}
if (isMode2Territory) break // 属于模式2,跳过当前匹配项
// 检测匹配项后是否有下划线
const isNextCharUnderline = checkUnderline(oDocument, keywordEndPos)
// 场景1:在单元格中填充(匹配项在单元格内)
if (match.mbCell) {
const currentCell = checkTableCell(oRange)
if (currentCell) {
const nextCell = currentCell.GetNext()
const curCellText = currentCell.GetContent().GetText().trim()
if (nextCell && curCellText == keyword) {
const oCellContent = nextCell.GetContent()
const oCellParagraph = oCellContent.GetElement(0)
oCellParagraph.RemoveAllElements()
oCellParagraph.AddText(insertString)
}
}
}
// 场景2:向前匹配(如"(日期)"格式)
else if (match.isForward) {
const isPrevCharUnderline = checkUnderlineForward(oDocument, keywordStartPos - 1)
if (isPrevCharUnderline) {
const underlineRangeForward = findUnderlineRangeForward(oDocument, keywordStartPos - 1)
if (underlineRangeForward) {
insertIntoUnderline(oDocument, underlineRangeForward, insertString, true)
}
}
}
// 场景3:匹配项后有下划线,在下划线中插入
else if (isNextCharUnderline) {
const underlineRange = findUnderlineRange(oDocument, keywordEndPos)
if (underlineRange) {
insertIntoUnderline(oDocument, underlineRange, insertString, true)
} else {
insertPosUnderline(oDocument, keywordEndPos, insertString)
}
}
// 场景4:无下划线,直接在匹配项后插入
else {
oRange.AddText(insertString)
}
break
// 模式2:分散填空(如"____年__月__日",分别插入年、月、日)
case '2':
if (!item.valueObj) break // 无日期拆分数据,跳过
const rStart = oRange.GetStartPos()
const rEnd = oRange.GetEndPos()
// 匹配当前范围内的"年""月""日"(确定插入位置)
const allYears = oDocument.Search('年')
const allMonths = oDocument.Search('月')
const allDays = oDocument.Search('日')
let targetYear = null, targetMonth = null, targetDay = null
// 筛选当前匹配范围内的"年"
if (allYears) {
for (let j = 0; j < allYears.length; j++) {
if (allYears[j].GetStartPos() >= rStart && allYears[j].GetEndPos() <= rEnd)
targetYear = allYears[j]
}
}
// 筛选当前匹配范围内的"月"
if (allMonths) {
for (let j = 0; j < allMonths.length; j++) {
if (allMonths[j].GetStartPos() >= rStart && allMonths[j].GetEndPos() <= rEnd)
targetMonth = allMonths[j]
}
}
// 筛选当前匹配范围内的"日"
if (allDays) {
for (let j = 0; j < allDays.length; j++) {
if (allDays[j].GetStartPos() >= rStart && allDays[j].GetEndPos() <= rEnd)
targetDay = allDays[j]
}
}
// 关键:从右往左插入(日→月→年),避免修改后坐标失效
const dayGap = insertDatePart(oDocument, targetDay, item.valueObj.day)
const monthGap = insertDatePart(oDocument, targetMonth, item.valueObj.month, dayGap > 0 ? dayGap + 2 : null)
const safeYearLimit = monthGap > 0 ? monthGap + 2 : 4
insertDatePart(oDocument, targetYear, item.valueObj.year, safeYearLimit)
break
default:
console.log('缺少匹配模式,无法填充')
}
}
})
})
} catch (e) {
console.error('插件执行出错:', e) // 捕获异常,避免插件崩溃
}
// 辅助函数:构建完整文档文本(用于正则匹配)
function buildFullText(container) {
let text = ''
if (!container || !container.GetElementsCount) return text
for (let i = 0; i < container.GetElementsCount(); i++) {
const elem = container.GetElement(i)
// 段落:直接获取文本
if (elem.GetText && !elem.GetRowsCount) {
text += elem.GetText() + '\n'
}
// 表格:遍历单元格,递归获取内容
else if (elem.GetRowsCount) {
for (let r = 0; r < elem.GetRowsCount(); r++) {
const row = elem.GetRow(r)
for (let c = 0; c < row.GetCellsCount(); c++) {
const cell = row.GetCell(c)
const cellContent = cell.GetContent()
text += buildFullText(cellContent)
}
}
}
// 其他容器:递归获取内容
else if (elem.GetElementsCount) {
text += buildFullText(elem)
}
}
return text
}
// 辅助函数:正则匹配文档中的目标内容
function findRegexKeyword(doc, complexRegex) {
const matchTexts = []
if (!gFullDocText) {
gFullDocText = buildFullText(doc) // 初始化完整文档文本
}
if (!complexRegex) return []
// 执行正则匹配,获取所有匹配项
let matches = gFullDocText.matchAll(complexRegex)
for (let match of matches) {
matchTexts.push(match[0])
}
return matchTexts
}
// 辅助函数:处理正则匹配项,去重并生成匹配规则
function handleMatchKeyword(result, match) {
const arr = findRegexKeyword(doc, new RegExp(match.keyword, 'g'))
// 去重:避免重复匹配
const newArr = arr.filter((item, index) => arr.indexOf(item) === index)
// 生成最终匹配规则
newArr.forEach((item) => {
result.push({ ...match, keyword: item, isRegex: false })
})
}
// 辅助函数:生成所有匹配规则(处理正则和非正则匹配)
function generateMatchKeywords(doc, matchArr) {
if (!matchArr || !Array.isArray(matchArr)) return []
let resultArr = []
matchArr.forEach((match) => {
if (match.isRegex) {
handleMatchKeyword(resultArr, { ...match })
const exclusion = match.exclusion || {}
// 处理单元格内匹配场景
if (exclusion.mbCell) {
const mKeyword = match.keyword.replace(/\\s\*\[::\]$/, '')
handleMatchKeyword(resultArr, { ...match, keyword: mKeyword, mbCell: true })
}
// 处理向前匹配场景(如"(日期)")
if (exclusion.isForward) {
let fKeyword = match.keyword.replace(/\\s\*\[::\]$/, '')
fKeyword = `[((]${fKeyword}[))]`
handleMatchKeyword(resultArr, { ...match, keyword: fKeyword, isForward: true })
}
} else {
resultArr.push({ ...match })
}
})
return resultArr
}
// 辅助函数:判断当前匹配范围是否在表格单元格内
function checkTableCell(oRange) {
const oParagraph = oRange.GetParagraph(0)
if (!oParagraph || !oParagraph.GetParentTableCell || !oParagraph.GetParentTableCell()) {
return false
}
return oParagraph.GetParentTableCell()
}
// 辅助函数:检测指定位置是否有下划线(向后检测)
function checkUnderline(oDocument, pos) {
const range = oDocument.GetRange(pos, pos + 1)
try {
const textPr = range.GetTextPr()
const char = range.GetText()
// 有下划线属性且为空白字符,判定为下划线
if (textPr && textPr.GetUnderline && /^\s*$/.test(char)) {
return textPr.GetUnderline() === true
}
return false
} catch (e) {
return false
}
}
// 辅助函数:找到连续下划线范围(向后查找)
function findUnderlineRange(doc, startPos) {
let currentPos = startPos
const maxSearchLength = 100 // 限制搜索长度,避免性能问题
let searchCount = 0
// 查找下划线结束位置
while (searchCount < maxSearchLength) {
try {
if (!checkUnderline(doc, currentPos)) break
currentPos++
searchCount++
} catch (e) {
break
}
}
// 存在连续下划线,返回下划线范围
if (currentPos > startPos + 1) {
return doc.GetRange(startPos, currentPos)
}
return null
}
// 辅助函数:检测指定位置是否有下划线(向前检测)
function checkUnderlineForward(oDocument, pos) {
const range = oDocument.GetRange(pos - 1, pos)
try {
const textPr = range.GetTextPr()
const char = range.GetText()
if (textPr && textPr.GetUnderline && /^\s*$/.test(char)) {
return textPr.GetUnderline() === true
}
return false
} catch (e) {
return false
}
}
// 辅助函数:找到连续下划线范围(向前查找)
function findUnderlineRangeForward(doc, startPos) {
let currentPos = startPos
const maxSearchLength = 100
let searchCount = 0
while (searchCount < maxSearchLength) {
try {
if (!checkUnderlineForward(doc, currentPos)) break
currentPos--
searchCount++
} catch (e) {
break
}
}
if (currentPos < startPos) {
return doc.GetRange(currentPos, startPos)
}
return null
}
// 辅助函数:在下划线中间插入内容(保留下划线格式)
function insertIntoUnderline(doc, underlineRange, content, isExceedDelete) {
let underlineStart = underlineRange.GetStartPos()
let underlineEnd = underlineRange.GetEndPos()
let oRangeToDelete = null
const contentLength = content.length
let contentWidth = null
// 根据内容长度,删除多余的下划线(避免内容溢出)
if (isExceedDelete) {
if (underlineEnd - underlineStart > contentLength * 2) {
contentWidth = contentLength * 2
} else if (underlineEnd - underlineStart > contentLength * 1.5) {
contentWidth = Math.floor(contentLength * 1.5)
} else if (underlineEnd - underlineStart > contentLength) {
contentWidth = contentLength
} else if (underlineEnd - underlineStart <= contentLength) {
contentWidth = underlineEnd - underlineStart - 2
}
} else if (underlineEnd - underlineStart > content.length) {
contentWidth = content.length
}
// 删除多余下划线
if (contentWidth) {
oRangeToDelete = doc.GetRange(underlineStart, underlineStart + contentWidth)
oRangeToDelete.Delete()
underlineStart = underlineRange.GetStartPos()
underlineEnd = underlineRange.GetEndPos()
}
// 在下划线中间插入内容,保持下划线格式
const insertPos = Math.floor((underlineStart + underlineEnd) / 2)
const textPr = Api.CreateTextPr()
textPr.SetUnderline(true) // 保留下划线格式
const insertRange = doc.GetRange(insertPos, insertPos)
if (insertRange) {
insertRange.AddText(content, textPr)
}
}
// 辅助函数:在指定位置插入带下划线的内容
function insertPosUnderline(doc, start, content) {
const textPr = Api.CreateTextPr()
textPr.SetUnderline(true)
const insertRange = doc.GetRange(start, start + 1)
if (insertRange) {
insertRange.AddText(content, textPr)
}
}
// 辅助函数:插入日期拆分内容(适配mode:2,保留原有格式)
function insertDatePart(oDocument, targetNode, textContent, maxSpaceLimit = null) {
if (!targetNode) return 0
const pos = targetNode.GetStartPos() // 获取"年/月/日"的位置
let gapStart = pos
let needsUnderline = false
let parentTextPr = null
let expectedGapType = null
let finalGapLength = 0
// 向前查找下划线/空格(确定插入起点)
for (let i = 1; i <= 30; i++) {
if (pos - i < 0) break
const prevRange = oDocument.GetRange(pos - i, pos - i + 1)
if (!prevRange) break
const char = prevRange.GetText() || ''
if (char === '\r' || char === '\n') break // 遇到换行,停止查找
const textPr = prevRange.GetTextPr()
if (i === 1) parentTextPr = textPr // 记录原有样式
let isGap = false
let currentGapType = null
// 判定下划线(包括下划线属性和下划线字符)
if (/^[__\------]+$/.test(char)) {
currentGapType = 'underline'
} else if (textPr && textPr.GetUnderline && /^\s*$/.test(char)) {
currentGapType = 'underline'
} else if (/^[\s\t\xA0\u3000]*$/.test(char)) {
currentGapType = 'space' // 空白字符
} else {
break
}
// 匹配间隙类型,限制查找范围
if (currentGapType) {
if (i === 1) expectedGapType = currentGapType
if (currentGapType === expectedGapType) {
if (maxSpaceLimit !== null && i > maxSpaceLimit) break
isGap = true
if (currentGapType === 'underline') needsUnderline = true
} else {
isGap = false
}
}
if (isGap) {
gapStart = pos - i
finalGapLength = i // 记录间隙长度
} else {
break
}
}
// 插入内容(保留原有样式)
if (gapStart < pos) {
const midPos = Math.floor((gapStart + pos) / 2)
const insertRange = oDocument.GetRange(midPos, midPos)
const newPr = Api.CreateTextPr()
// 继承原有字体、字号(避免样式突变)
if (parentTextPr) {
if (parentTextPr.GetFontSize) newPr.SetFontSize(parentTextPr.GetFontSize())
if (parentTextPr.GetFontName) newPr.SetFontName(parentTextPr.GetFontName())
}
// 有下划线,保留下划线格式
if (needsUnderline) {
newPr.SetUnderline('single') // OnlyOffice规范:single表示单下划线
insertRange.AddText(textContent, newPr)
} else {
insertRange.AddText(textContent)
}
} else {
// 无间隙,紧贴插入
oDocument.GetRange(pos, pos).AddText(textContent)
}
return finalGapLength
}
},
false,
false,
// 填充完成后,隐藏加载遮罩
(returnValue) => {
const loadingMask = document.getElementById('loadingMask')
if (loadingMask) {
loadingMask.classList.remove('show')
}
}
)
}
3.3 效果说明
在 Vue 页面选择需要填充的字段(如日期),选择自定义日期后点击"一键填充",桥接层会通过正则匹配文档中所有符合规则的"日期"相关字段,自动插入选中的日期,支持两种场景:
-
场景1:"日期:"后无内容 → 直接在后面插入日期(保留原有格式);
-
场景2:"____年__月__日" → 分别在"年""月""日"前面的下划线中插入对应数字,完美适配分散填空格式。
其他字段(公司名称、地址等)可直接复用此逻辑,只需在 distObj 中添加对应匹配规则和接口请求即可。
四、总结与注意事项
4.1 核心总结
本文实现了 OnlyOffice 自定义插件的三大核心插入功能,核心逻辑是"Vue 前端触发 → 父组件通信 → 桥接层解析 → OnlyOffice API 执行",关键要点:
-
表格插入:通过
callCommand操作文档表格,拆分拼接内容插入对应单元格; -
HTML 插入:利用
PasteHtmlAPI 直接解析富文本,保留原有格式; -
一键填充:通过正则匹配文档特殊字符,根据匹配模式插入内容,支持下划线保留、分散填空等复杂场景。
4.2 注意事项
-
桥接层的
plugins.js建议本地引入(避免跨域问题),可从 OnlyOffice 官方 SDK 下载; -
正则匹配规则可根据实际文档格式调整,避免匹配过宽或过窄;
-
操作文档时尽量使用
callCommand异步执行,避免阻塞插件; -
添加加载遮罩和异常捕获,提升用户体验,避免插件崩溃。