富文本编辑器实现效果

该富文本组件支持的内容如上图标签所示
vue3+ts+elementplus组件封装
所需依赖
"@tiptap/extension-image": "^3.25.0",
"@tiptap/extension-link": "^3.25.0",
"@tiptap/extension-placeholder": "^3.25.0",
"@tiptap/extension-table": "^3.25.0",
"@tiptap/extension-table-cell": "^3.25.0",
"@tiptap/extension-table-header": "^3.25.0",
"@tiptap/extension-table-row": "^3.25.0",
"@tiptap/extension-underline": "^3.25.0",
"@tiptap/starter-kit": "^3.25.0",
"@tiptap/vue-3": "^3.25.0",
涉及的文本处理方法
javascript
/** 判断富文本 HTML 是否无实际文本内容 */
export const isRichTextEmpty = (html?: string): boolean => {
if (!html?.trim()) return true
if (typeof document !== 'undefined') {
const div = document.createElement('div')
div.innerHTML = html
const text = (div.textContent || div.innerText || '').replace(/\u00a0/g, ' ').trim()
return !text
}
const text = html
.replace(/<[^>]+>/g, '')
.replace(/ /gi, ' ')
.replace(/\s+/g, ' ')
.trim()
return !text
}
/** 内容是否包含 HTML 标签 */
export const isHtmlContent = (content?: string): boolean => /<[^>]+>/.test(content || '')
/** 提交前规范化富文本(空内容返回空字符串) */
export const normalizeRichTextForSubmit = (html?: string): string => {
if (isRichTextEmpty(html)) return ''
return html?.trim() || ''
}
/** 富文本 HTML 转纯文本 */
export const richTextToPlain = (html?: string): string => {
if (!html?.trim()) return ''
if (typeof document !== 'undefined') {
const div = document.createElement('div')
div.innerHTML = html
return (div.textContent || div.innerText || '').replace(/\u00a0/g, ' ').trim()
}
return html
.replace(/<[^>]+>/g, '')
.replace(/ /gi, ' ')
.replace(/\s+/g, ' ')
.trim()
}
组件使用方法
配置项
javascript
Prop 默认值 说明
enableHistory true 撤销/重做
enableHeading true 标题选择
enableBold true 粗体
enableItalic true 斜体
enableUnderline true 下划线
enableStrike true 删除线
enableCode true 行内代码
enableBulletList true 无序列表
enableOrderedList true 有序列表
enableBlockquote true 引用
enableCodeBlock true 代码块
enableLink true 链接
enableHorizontalRule true 分隔线
enableClearFormat true 清除格式
enableImage false 图片(需显式启用)
enableTable false 表格(需显式启用)
javascript
<RichTextEditor
v-model="form.reasonDescription"
placeholder="请输入原因描述"
:min-height="200"
:enable-image="true"
:enable-table="true"
:enable-code="false"
:enable-blockquote="false"
:enable-code-block="false"
:enable-link="false"
/>
33
组件源码
javascript
<template>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="180px"
class="fcr-form"
:validate-on-rule-change="false"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="FCR编号">
<el-input :model-value="'待生成'" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="版次 Rev." prop="revision">
<el-input v-model="form.revision" placeholder="请输入版次" maxlength="10" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="主题 Subject" prop="subject">
<el-input v-model="form.subject" placeholder="请输入主题" maxlength="500" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="发起单位 Issue Institution" prop="issueInstitution">
<el-input v-model="form.issueInstitution" placeholder="请输入发起单位" maxlength="200" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目名称 Project Name" prop="projectId">
<el-select
v-model="form.projectId"
placeholder="请选择项目"
filterable
remote
clearable
:remote-method="searchProject"
:loading="projectLoading"
style="width: 100%"
@change="handleProjectChange"
>
<el-option
v-for="item in projectOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="机组 Unit" prop="unitId">
<el-select
v-model="form.unitId"
placeholder="请选择机组"
filterable
clearable
:disabled="!form.projectId"
style="width: 100%"
@change="handleUnitChange"
>
<el-option
v-for="item in unitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="系统 System" prop="system">
<el-input v-model="form.system" placeholder="请输入系统" maxlength="100" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="设备位号 Equipment code" prop="equipmentCode">
<el-input v-model="form.equipmentCode" placeholder="请输入设备位号" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="厂房 Building" prop="building">
<el-input v-model="form.building" placeholder="请输入厂房" maxlength="100" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="区域 Area" prop="area">
<el-input v-model="form.area" placeholder="请输入区域" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="房间 Room" prop="room">
<el-input v-model="form.room" placeholder="请输入房间" maxlength="100" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="层位 Level" prop="level">
<el-input v-model="form.level" placeholder="请输入层位" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业 Discipline" prop="discipline">
<el-input v-model="form.discipline" placeholder="请输入专业" maxlength="100" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="类别 Classification" prop="classification">
<el-select
v-model="form.classification"
placeholder="请选择类别"
clearable
style="width: 100%"
>
<el-option label="设计" value="设计" />
<el-option label="采购" value="采购" />
<el-option label="施工" value="施工" />
<el-option label="调试" value="调试" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="页数 Pages" prop="pages">
<el-input v-model="form.pages" placeholder="如: 3+2附件" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="发布日期 Issue date" prop="issueDate">
<el-date-picker
v-model="form.issueDate"
type="date"
placeholder="请选择发布日期"
value-format="YYYY-MM-DD"
format="YYYY/M/D"
clearable
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="责任方代码 Responsible code" prop="responsibleCode">
<el-input v-model="form.responsibleCode" placeholder="请输入责任方代码" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="相关文件 Document Concerned" prop="documentConcerned">
<el-input
v-model="form.documentConcerned"
type="textarea"
:rows="4"
placeholder="请输入相关文件"
maxlength="2000"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="变更原因 Reason" prop="reasons">
<el-checkbox-group v-model="form.reasons">
<el-checkbox label="为适应现场特殊的设备" value="specialEquipment" />
<el-checkbox label="为适应现场特殊的材料" value="specialMaterial" />
<el-checkbox label="为适应现场特殊的环境条件" value="specialEnvironment" />
<el-checkbox label="其它" value="other" />
</el-checkbox-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="责任方 Responsible" prop="responsible">
<el-checkbox-group v-model="form.responsible">
<el-checkbox label="提出单位" value="proposingUnit" />
<el-checkbox label="相关建安承包商 Civil/Erection Contractor" value="civilContractor" />
<el-checkbox label="设备供应商 Equip-supplier" value="equipmentSupplier" />
<el-checkbox label="设计分包院 Sub-design institute" value="subDesignInstitute" />
<el-checkbox label="CNPE" value="cnpe" />
<el-checkbox label="业主 Purchaser" value="purchaser" />
<el-checkbox label="其它 Other" value="otherResponsible" />
</el-checkbox-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="是否产生费用 Cost" prop="cost">
<el-select v-model="form.cost" placeholder="请选择" clearable style="width: 100%">
<el-option label="是" value="是" />
<el-option label="否" value="否" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="金额 Amount" prop="amount">
<el-input v-model="form.amount" placeholder="请输入金额" maxlength="50">
<template #append>万元</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="是否源于CR单" prop="fromCr">
<el-select v-model="form.fromCr" placeholder="请选择" clearable style="width: 100%">
<el-option label="是" value="是" />
<el-option label="否" value="否" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="对应CR单编号" prop="crNo">
<el-input v-model="form.crNo" placeholder="CR-" maxlength="100" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="原因描述 DESC" prop="reasonDescription">
<RichTextEditor
v-model="form.reasonDescription"
placeholder="请输入原因描述"
:min-height="200"
:enable-image="true"
:enable-table="true"
:enable-code="false"
:enable-blockquote="false"
:enable-code-block="false"
:enable-link="false"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="变更方案 Scheme" prop="changeScheme">
<RichTextEditor
v-model="form.changeScheme"
placeholder="请输入变更方案"
:min-height="200"
:enable-image="true"
:enable-table="true"
:enable-code="false"
:enable-blockquote="false"
:enable-code-block="false"
:enable-link="false"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="材料替换 Material replacement" prop="materialReplacement">
<el-input
v-model="form.materialReplacement"
type="textarea"
:rows="4"
placeholder="须提供相应的变更分析材料,并说明技术影响"
maxlength="2000"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="临时措施 Temporary measure" prop="temporaryMeasure">
<el-input
v-model="form.temporaryMeasure"
type="textarea"
:rows="4"
placeholder="N/A"
maxlength="2000"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="计划开始时间" prop="planStartDate">
<el-date-picker
v-model="form.planStartDate"
type="date"
placeholder="请选择计划开始时间"
value-format="YYYY-MM-DD"
format="YYYY/M/D"
clearable
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="计划完工时间" prop="planFinishDate">
<el-date-picker
v-model="form.planFinishDate"
type="date"
placeholder="请选择计划完工时间"
value-format="YYYY-MM-DD"
format="YYYY/M/D"
clearable
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { getProjectList, getProjectDetail } from '@/api/basic-data/project'
import { extractItemsByType, findNodeByItemCode } from '@/utils/index'
import RichTextEditor from '@/components/rich-text-editor/index.vue'
/** FCR表单数据模型 */
export interface FcrFormModel {
revision: string
subject: string
issueInstitution: string
projectId: string
unitId: string
system: string
equipmentCode: string
building: string
area: string
room: string
level: string
discipline: string
classification: string
pages: string
issueDate: string
responsibleCode: string
documentConcerned: string
reasons: string[]
responsible: string[]
cost: string
amount: string
fromCr: string
crNo: string
reasonDescription: string
changeScheme: string
materialReplacement: string
temporaryMeasure: string
planStartDate: string
planFinishDate: string
}
/** 表单校验规则 */
const rules: FormRules = {
subject: [{ required: true, message: '请输入主题', trigger: 'blur' }],
issueInstitution: [{ required: true, message: '请输入发起单位', trigger: 'blur' }],
projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
unitId: [{ required: true, message: '请选择机组', trigger: 'change' }],
classification: [{ required: true, message: '请选择类别', trigger: 'change' }],
reasons: [{ required: true, message: '请选择变更原因', trigger: 'change' }]
}
/** 表单引用 */
const formRef = ref<FormInstance>()
/** 项目远程搜索 loading 状态 */
const projectLoading = ref(false)
/** 项目下拉选项 */
const projectOptions = ref<Array<{ label: string; value: string }>>([])
/** 机组下拉选项 */
const unitOptions = ref<Array<{ label: string; value: string }>>([])
/** 项目详情树形结构(用于联动) */
const projectDetailTree = ref<any[]>([])
/** 创建空表单的初始值 */
const createEmptyForm = (): FcrFormModel => ({
revision: 'A',
subject: '',
issueInstitution: '',
projectId: '',
unitId: '',
system: '',
equipmentCode: '',
building: '',
area: '',
room: '',
level: '',
discipline: '',
classification: '',
pages: '',
issueDate: '',
responsibleCode: '',
documentConcerned: '',
reasons: [],
responsible: [],
cost: '',
amount: '',
fromCr: '',
crNo: '',
reasonDescription: '',
changeScheme: '',
materialReplacement: '',
temporaryMeasure: '',
planStartDate: '',
planFinishDate: ''
})
/** 表单数据 */
const form = reactive<FcrFormModel>(createEmptyForm())
/**
* 远程搜索项目列表
* @param query 搜索关键词
*/
const searchProject = async (query?: string) => {
projectLoading.value = true
try {
const res = await getProjectList({
projectName: query?.trim() ?? '',
pageSize: 999,
pageNumber: 1
})
const data = (res?.data ?? {}) as any
const list = data.result || data.list || []
projectOptions.value = (Array.isArray(list) ? list : []).map((item: any) => ({
label: item.projectName || '',
value: String(item.projectId ?? '')
}))
} catch (error) {
console.error('获取项目列表失败:', error)
projectOptions.value = []
} finally {
projectLoading.value = false
}
}
/**
* 项目选择变化:清空机组并加载机组列表
*/
const handleProjectChange = async () => {
form.unitId = ''
unitOptions.value = []
projectDetailTree.value = []
if (form.projectId) {
try {
const res = await getProjectDetail({ projectId: form.projectId })
const detailData = (res?.data ?? {}) as any
const childItemList = detailData.childItemList || []
projectDetailTree.value = childItemList
unitOptions.value = extractItemsByType(childItemList, 'unit')
} catch (error) {
console.error('获取项目详情失败:', error)
}
}
}
/**
* 机组选择变化:联动逻辑预留
*/
const handleUnitChange = () => {}
/**
* 表单校验方法(供父组件调用)
* @returns Promise
*/
const validate = () => {
return formRef.value?.validate()
}
/**
* 获取表单数据(供父组件调用)
* @returns 当前表单数据
*/
const getFormData = (): FcrFormModel => {
return { ...form }
}
/**
* 设置表单数据(供父组件调用)
* @param data 表单数据
*/
const setFormData = (data: Partial<FcrFormModel>) => {
Object.assign(form, createEmptyForm(), data)
}
/**
* 重置表单(供父组件调用)
*/
const resetForm = () => {
Object.assign(form, createEmptyForm())
}
/** 暴露方法给父组件 */
defineExpose({
validate,
getFormData,
setFormData,
resetForm,
form
})
/** 组件挂载时加载项目列表 */
searchProject('')
/** 监听外部传入的数据变化(通过 v-model) */
const modelValue = defineModel<Partial<FcrFormModel>>()
watch(
modelValue,
(val) => {
if (val) {
setFormData(val)
}
},
{ immediate: true }
)
</script>
<style scoped lang="scss">
.fcr-form {
padding: 16px;
overflow-y: auto;
}
</style>