createElement与createDocumentFragment的点点区别

本文通过两个按钮触发事件,分别演示了如何使用JavaScript创建元素和文档片段,并将它们添加到DOM树中,随后移除这些元素,展示了基本的DOM操作。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <style type="text/css">
        #outer{ height: 200px; border: 1px solid #006400;}
    </style>
</head>
<body>
<div id="outer">
</div>
<input type="button" value="createElement" id="btn_1"/><input type="button" value="createDocumentFragment" id="btn_2"/>
<script type="text/javascript">
    function $(id){
        return document.getElementById(id);
    }
    var outer = $('outer');
    var inner = $('inner'); 
    $('btn_1').onclick = function(){
        var div = document.createElement('div');
            div.innerHTML = '<p>测试createElement</p>';
        document.body.appendChild(div);
        setTimeout(function(){
            outer.appendChild(div);
            setTimeout(function(){
                outer.removeChild(div);
            },1000)
        },1000)
    }
    $('btn_2').onclick = function(){
        var p = document.createElement('p');
            p.innerHTML = '测试DocumentFragment';
        var fragment = document.createDocumentFragment();
            fragment.appendChild(p);
            fragment.innerHTML = '<p>测试DocumentFragment</p>';
            fragment.innerHTML = '<span>测试DocumentFragment</span>';
        document.body.appendChild(fragment);
        setTimeout(function(){
            outer.appendChild(fragment);//报错,因为此时文档内部已经能够不存在fragment了
            setTimeout(function(){
                outer.removeChild(fragment);
            },1000)
        },1000)
    }
</script>
</body>
</html>
 转载之: http://www.cnblogs.com/xesam/archive/2011/12/19/2293876.html
<template> <div class="hot-table-wps"> <!-- 工具栏 --> <div class="toolbar" v-if="flag"> <select v-model="fontFamily" @change="applyStyle('fontFamily')"> <option value="Arial">Arial</option> <option value="Times New Roman">Times New Roman</option> <option value="Courier New">Courier New</option> <option value="Verdana">Verdana</option> <option value="SimSun">宋体</option> <option value="Microsoft YaHei">微软雅黑</option> </select> <select v-model="fontSize" @change="applyStyle('fontSize')"> <option v-for="size in [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]" :value="size" :key="size">{{ size }} </option> </select> <button @click="toggleBold" :class="{ 'active': fontWeight === 'bold' }"> <b>B</b> </button> <button @click="toggleItalic" :class="{ 'active': fontStyle === 'italic' }"> <i>I</i> </button> <button @click="toggleUnderline" :class="{ 'active': textDecoration === 'underline' }"> <u>U</u> </button> <div class="color-picker"> <label>文字颜色</label> <!-- <input type="color" v-model="color" @change="applyStyle('color')"> --> <el-color-picker v-model="color" show-alpha :predefine="predefineColors" @change="applyStyle('color')"/> </div> <div class="color-picker"> <label>背景颜色</label> <!-- <input type="color" v-model="backgroundColor" @change="applyStyle('backgroundColor')"> --> {{ backgroundColor }} <el-color-picker v-model="backgroundColor" show-alpha :predefine="predefineColors" @change="applyStyle('backgroundColor')"/> </div> <select v-model="textAlign" @change="applyStyle('textAlign')"> <option value="left">左对齐</option> <option value="center">居中</option> <option value="right">右对齐</option> </select> <select v-model="verticalAlign" @change="applyStyle('verticalAlign')"> <option value="top">顶部对齐</option> <option value="middle">垂直居中</option> <option value="bottom">底部对齐</option> </select> <!-- <button @click="exportToExcel" class="export-btn"> <span>导出Excel</span> </button> --> <button class="export-btn" type="success" @click="flag = false"> <span>取消</span> </button> </div> <div class="toolbar" v-if="rowFlage"> <!-- 添加部分文字字体和字号选择 --> <select v-model="partialFontFamily" @change="applyPartialStyle('fontFamily')"> <option value="Arial">Arial</option> <option value="Times New Roman">Times New Roman</option> <option value="Courier New">Courier New</option> <option value="Verdana">Verdana</option> <option value="SimSun">宋体</option> <option value="Microsoft YaHei">微软雅黑</option> </select> <select v-model="partialFontSize" @change="applyPartialStyle('fontSize')"> <option v-for="size in [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]" :value="size" :key="size">{{ size }} </option> </select> <button @click="applyPartialStyle('fontWeight', 'bold')" :class="{ 'active': partialFontWeight === 'bold' }"> <b>B</b> </button> <button @click="applyPartialStyle('fontStyle', 'italic')" :class="{ 'active': partialFontStyle === 'italic' }"> <i>I</i> </button> <button @click="applyPartialStyle('textDecoration', 'underline')" :class="{ 'active': partialTextDecoration === 'underline' }"> <u>U</u> </button> <div class="color-picker"> <label>文字颜色</label> <input type="color" v-model="partialColor" @change="applyPartialStyle('color')"> </div> <div class="color-picker"> <label>背景颜色</label> <input type="color" v-model="partialBackgroundColor" @change="applyPartialStyle('backgroundColor')"> </div> <select v-model="partialTextAlign" @change="applyPartialStyle('textAlign')"> <option value="left">左对齐</option> <option value="center">居中</option> <option value="right">右对齐</option> </select> <!-- <select v-model="partialVerticalAlign" @change="applyPartialStyle('verticalAlign')">--> <!-- <option value="top">顶部对齐</option>--> <!-- <option value="middle">垂直居中</option>--> <!-- <option value="bottom">底部对齐</option>--> <!-- </select>--> <button class="export-btn" type="success" @click="rowFlage = false"> <span>取消</span> </button> </div> <!-- Handsontable 表格容器 --> <div ref="hotTable" class="hot-table-container"></div> </div> </template> <script> import Handsontable from 'handsontable'; import 'handsontable/dist/handsontable.full.css'; import { exportToExcel } from './exporter'; // 导入中文语言包 import { registerLanguageDictionary, zhCN } from 'handsontable/i18n'; // 注册中文语言包 registerLanguageDictionary('zh-CN', zhCN); export default { name: 'HotTableWPS', props: { colHeaders: { type: [Boolean, Array], default: true }, rowHeaders: { type: Boolean, default: true }, mergeSameStation: { type: Boolean, default: true } , tableData: { type: Object, default: () => { } }, active: { type: Number, default: 0 }, }, data() { return { predefineColors:[ '#ff4500', '#ff8c00', '#ffd700', '#90ee90', '#00ced1', '#1e90ff', '#c71585', 'rgba(255, 69, 0, 0.68)', 'rgb(255, 120, 0)', 'hsv(51, 100, 98)', 'hsva(120, 40, 94, 0.5)', 'hsl(181, 100%, 37%)', 'hsla(209, 100%, 56%, 0.73)', '#c7158577', ], flag: false, rowFlage: false, partialColor: '#000000', partialBackgroundColor: '#ffffff', partialFontWeight: 'normal', partialFontStyle: 'normal', partialTextDecoration: 'none', partialFontFamily: 'Arial', partialTextAlign: 'left', partialVerticalAlign: 'middle', partialFontSize: 14, currentTextSelection: { text: '', start: -1, end: -1, row: -1, col: -1 }, partialTextStyles: {}, hot: null, cellStyles: {}, // 添加撤销/重做历史记录 historyStack: [], historyIndex: 0, maxHistorySize: 50, fontFamily: 'Arial', fontSize: 14, fontWeight: 'normal', fontStyle: 'normal', textDecoration: 'none', color: '#000000', backgroundColor: '#ffffff', textAlign: 'center', verticalAlign: 'middle', selectedCells: [], height: 'auto', tableList: [], newRemarks: '', // 推荐的数据格式 // tableData: { // // 表头数据(日期列) // headers: [ // "时间", "", "21日", '22日', '23日', '24日', '25日', '26日', '27日', '28日', // '29日', '30日', '31日', '1日', '2日', '3日', '4日', '昨日实况', '昨日预报' // ], // // 行数据(按水电站和指标分组) // rows: [ // { // stationName: '桐子林', // type: '出库', // values: [1950, 2050, 2200, 2250, 2150, 2000, 1900, 1900, 1800, 1800, 1800, 1800, 1800, 1800, 1830, 1850, 1850, 1850] // // }, // { // stationName: '观音岩', // type: '出库', // values: [3100, 3200, 3400, 3500, 3500, 3500, 3400, 3400, 3000, 3000, 3000, 2800, 2800, 2800, 3030, 3400] // }, // { // stationName: '观音岩', // type: '区间', // values: [450, 450, 500, 650, 750, 600, 500, 200, 600, 500, 400, 600, 400, 400, 450, 350] // }, // { // stationName: '观音岩', // type: '入库', // values: [5500, 5700, 6100, 6400, 6400, 6100, 5800, 5500, 5400, 5300, 5200, 5200, 5000, 5000, 5310, 5600] // }, // { // stationName: '观音岩', // type: '出库', // values: [5450, 5450, 5480, 5450, 6080, 6070, 6070, 6100, 6130, 5480, 5490, 5170, 4740, 4730, 4730] // }, // { // stationName: '乌东德', // type: '时段初水位', // values: [960.03, 960.07, 960.29, 960.82, 961.63, 961.90, 961.93, 961.69, 961.19, 960.57, 960.41, 960.16, 960.41, 960.64, 960.64] // }, // { // stationName: '乌东德', // type: '时段末水位', // values: [960.07, 960.29, 960.82, 961.63, 961.90, 961.93, 961.69, 961.19, 960.57, 960.41, 960.16, 960.19, 960.41, 960.41, 960.87] // }, // { // stationName: '乌东德', // type: '日变幅', // values: [0.04, 0.22, 0.53, 0.81, 0.27, 0.03, -0.24, -0.50, -0.62, -0.16, -0.25, 0.03, 0.22, 0.23, 0.23] // }, // { // stationName: '乌东德', // type: '耗水率', // values: [2.89, 2.89, 2.87, 2.85, 2.87, 2.86, 2.87, 2.87, 2.89, 2.87, 2.83, 2.86, 2.84, 2.84, 2.84] // }, // { // stationName: '乌东德', // type: '建议电量', // values: [1.63, 1.63, 1.65, 1.65, 1.83, 1.83, 1.83, 1.83, 1.83, 1.65, 1.65, 1.56, 1.44, 1.44, 1.44] // }, // { // stationName: '白鹤滩', // type: '交易电量', // values: [1.63, 1.63, 1.65, 1.65, 1.83, 1.83, 1.83, 1.83, 1.65, 1.65, '/', '/', '/', '/', '/'] // }, // { // stationName: '白鹤滩', // type: '入库', // values: [5850, 5950, 6080, 5850, 6380, 6370, 6370, 6400] // } // ] // } }; }, mounted() { this.initializeHotTable(); // 添加键盘事件监听 this.addKeyboardListeners(); }, beforeDestroy() { if (this.hot) { this.hot.destroy(); } document.removeEventListener('selectionchange', this.handleTextSelection); // 移除键盘事件监听 this.removeKeyboardListeners(); }, methods: { /** * 检查文本选择范围的样式状态 * 判断选择范围是完全应用、部分应用还是未应用目标样式,用于决定后续是应用还是移除样式 * @param {string} cellKey 单元格标识 * @param {number} start 选择起始索引 * @param {number} end 选择结束索引 * @param {string} property 样式属性 * @param {any} value 目标样式值 * @returns {Object} 样式状态对象(fullyStyled: 是否完全应用, mixed: 是否部分应用) */ findExactStyle(cellKey, start, end, property, value) { if (!this.partialTextStyles[cellKey] || this.partialTextStyles[cellKey].length === 0) { return { fullyStyled: false, start: start, end: end, property: property, value: value !== null ? value : this.getStyleValue(property) }; } const targetValue = value !== null ? value : this.getStyleValue(property); let fullyStyled = true;// 是否完全应用目标样式 let partiallyStyled = false;// 是否部分应用目标样式 // 逐位置检查选择范围内的样式状态 for (let pos = start; pos < end; pos++) { let hasStyleAtPos = false; // 检查当前位置是否有目标样式 for (const style of this.partialTextStyles[cellKey]) { if (pos >= style.start && pos < style.end && style[property] === targetValue) { hasStyleAtPos = true; break; } } if (!hasStyleAtPos) { fullyStyled = false;// 存在未应用样式的位置,标记为非完全应用 } else { partiallyStyled = true;// 存在已应用样式的位置,标记为部分应用 } } // 返回判断结果 if (fullyStyled) { return { fullyStyled: true, start: start, end: end, property: property, value: targetValue }; } else if (partiallyStyled) { return { fullyStyled: false, start: start, end: end, property: property, value: targetValue, mixed: true// 混合状态(部分应用) }; } else { return { fullyStyled: false, start: start, end: end, property: property, value: targetValue }; } }, getStyleValue(property) { switch (property) { case 'fontFamily': return this.partialFontFamily; case 'fontSize': return this.partialFontSize; case 'color': return this.partialColor; case 'backgroundColor': return this.partialBackgroundColor; case 'fontWeight': // 切换加粗状态 return this.partialFontWeight === 'bold' ? 'normal' : 'bold'; case 'fontStyle': // 切换斜体状态 return this.partialFontStyle === 'italic' ? 'normal' : 'italic'; case 'textDecoration': // 切换下划线状态 return this.partialTextDecoration === 'underline' ? 'none' : 'underline'; case 'textAlign': return this.partialTextAlign; case 'verticalAlign': return this.partialVerticalAlign; default: return null; } }, /** * 移除部分文本样式(取消已应用的样式) * 逻辑:拆分包含目标样式的片段 -> 移除目标属性 -> 保留其他属性 -> 合并剩余片段 * @param {string} cellKey 单元格标识 * @param {Object} foundStyle 要移除的样式信息(包含start, end, property, value) * @param {string} property 要移除的样式属性 */ removePartialStyle(cellKey, foundStyle, property) { const { start, end, value } = foundStyle; const styles = this.partialTextStyles[cellKey]; // 收集需要处理的样式(选择范围重叠且属性值匹配的样式) const stylesToProcess = []; for (let i = 0; i < styles.length; i++) { const style = styles[i]; // 检查范围是否重叠且属性值匹配 if (start < style.end && end > style.start && style[property] === value) { const overlapStart = Math.max(start, style.start); const overlapEnd = Math.min(end, style.end); if (overlapStart < overlapEnd) { stylesToProcess.push({ index: i, style: { ...style }, overlapStart, overlapEnd }); } } } // 从后往前处理,避免索引偏移问题 stylesToProcess.sort((a, b) => b.index - a.index); for (const { index, style, overlapStart, overlapEnd } of stylesToProcess) { // 移除原样式 styles.splice(index, 1); // 创建新的样式片段(拆分原样式,移除目标属性) const newStyles = []; // 1. 重叠部分之前的片段(保留所有属性) if (style.start < overlapStart) { const beforeStyle = { ...style }; beforeStyle.end = overlapStart; newStyles.push(beforeStyle); } // 2. 重叠部分(移除目标属性,保留其他属性) const middleStyle = { ...style }; delete middleStyle[property]; // 核心:移除目标样式属性 middleStyle.start = overlapStart; middleStyle.end = overlapEnd; // 仅当还有其他样式属性时保留此片段(避免空样式) const hasOtherProperties = Object.keys(middleStyle).some( key => !['start', 'end'].includes(key) && middleStyle[key] !== undefined ); if (hasOtherProperties) { newStyles.push(middleStyle); } // 3. 重叠部分之后的片段(保留所有属性) if (style.end > overlapEnd) { const afterStyle = { ...style }; afterStyle.start = overlapEnd; newStyles.push(afterStyle); } // 添加所有新片段到样式数组 styles.push(...newStyles); } // 重新渲染单元格,使样式变更生效 const [row, col] = cellKey.split('-').map(Number); this.renderCellWithPartialStyles(row, col); }, /** * 合并相邻且样式完全相同的片段(优化方法) * 减少冗余样式片段,提升渲染性能和减少DOM节点数量 * @param {string} cellKey 单元格标识 */ mergeAdjacentStyles(cellKey) { if (!this.partialTextStyles[cellKey] || this.partialTextStyles[cellKey].length <= 1) { return; // 无需合并:无样式或只有一个样式片段 } const styles = this.partialTextStyles[cellKey]; // 按起始位置排序(确保从左到右处理) styles.sort((a, b) => a.start - b.start); let i = 0; while (i < styles.length - 1) { const current = styles[i]; const next = styles[i + 1]; // 检查是否相邻(当前结束=下一个开始)且所有样式属性完全相同 if (current.end === next.start && this.areStylesCompletelyEqual(current, next)) { // 合并样式:扩展当前样式的结束位置,移除下一个样式 current.end = next.end; styles.splice(i + 1, 1); } else { i++; // 不满足合并条件,移动到下一个 } } }, areStylesCompletelyEqual(style1, style2) { const properties = ['color', 'backgroundColor', 'fontWeight', 'fontStyle', 'textDecoration', 'fontFamily', 'fontSize','textAlign', 'verticalAlign']; for (const prop of properties) { const val1 = style1[prop]; const val2 = style2[prop]; // 处理 undefined 和 null 的情况 if ((val1 === undefined || val1 === null) && (val2 === undefined || val2 === null)) { continue; } if (val1 !== val2) { return false; } } return true; }, /** * 应用部分文本样式(主入口方法) * @param {string} property 样式属性名(如 'color', 'fontWeight') * @param {any} value 样式值(如 '#ff0000', 'bold',为null时自动切换状态) */ applyPartialStyle(property, value = null) { if (!this.hot) return; // 保存当前状态到历史记录,支持撤销操作 this.saveToHistory(); // 检查是否有文本选择 if (!this.currentTextSelection.text) { alert('请先选择要修改样式的文本:\n1. 双击单元格\n2. 选择部分文本\n3. 点击样式按钮'); return; } const { start, end, row, col } = this.currentTextSelection; const cellValue = this.hot.getDataAtCell(row, col) || ''; // 验证选择范围 if (start < 0 || end > cellValue.length || start >= end) { alert('选择范围无效'); return; } const cellKey = `${row}-${col}`; const styleValue = value !== null ? value : this.getStyleValue(property); // 检查当前选择范围的样式状态 const foundStyle = this.findExactStyle(cellKey, start, end, property, styleValue); if (foundStyle.fullyStyled) { // 如果已经完全应用了相同的样式,则移除它(取消样式) this.removePartialStyle(cellKey, foundStyle, property); } else { // 应用新样式 this.applyNewPartialStyle(cellKey, start, end, property, styleValue); } // 重新渲染单元格 this.renderCellWithPartialStyles(row, col); // 清除选择 // this.currentTextSelection = { // text: '', // start: -1, // end: -1, // row: -1, // col: -1 // }; }, /** * 应用新的部分文本样式(核心实现) * 处理逻辑:拆分重叠样式 -> 保留非重叠部分 -> 合并新样式 -> 优化相邻样式 * @param {string} cellKey 单元格唯一标识(格式:"row-col") * @param {number} start 文本选择起始索引 * @param {number} end 文本选择结束索引 * @param {string} property 样式属性名 * @param {any} value 样式值 */ applyNewPartialStyle(cellKey, start, end, property, value, overlappingStyles = []) { if (!this.partialTextStyles[cellKey]) { this.partialTextStyles[cellKey] = []; } const styles = this.partialTextStyles[cellKey]; // 第一步:收集所有受影响的样式(当前选择范围重叠的样式) const affectedStyles = []; for (let i = 0; i < styles.length; i++) { const style = styles[i]; if (style.start < end && style.end > start) { affectedStyles.push({ index: i, style: { ...style } }); } } // 第二步:从后往前移除受影响的样式(避免索引偏移问题) affectedStyles.sort((a, b) => b.index - a.index); for (const { index } of affectedStyles) { styles.splice(index, 1); } // 第三步:重新创建所有样式片段(拆分原样式为:前半部分+重叠部分+后半部分) const allFragments = []; // 处理受影响的样式,拆分为非重叠部分和重叠部分(重叠部分应用新样式 for (const { style } of affectedStyles) { // 样式在新范围之前的部分(保留原样式) if (style.start < start) { const beforeFragment = { ...style }; beforeFragment.end = Math.min(style.end, start); allFragments.push(beforeFragment); } // 样式新范围重叠的部分(应用新属性值) const overlapStart = Math.max(style.start, start); const overlapEnd = Math.min(style.end, end); if (overlapStart < overlapEnd) { const overlapFragment = { ...style, [property]: value, // 同时支持textAlign和verticalAlign的父元素对齐 ...(property === 'textAlign' ? { parentAlign: value } : {}), ...(property === 'verticalAlign' ? { parentVerticalAlign: value } : {}) }; overlapFragment.start = overlapStart; overlapFragment.end = overlapEnd; allFragments.push(overlapFragment); } // 样式在新范围之后的部分(保留原样式) if (style.end > end) { const afterFragment = { ...style }; afterFragment.start = Math.max(style.start, end); allFragments.push(afterFragment); } } // 第四步:处理新范围中未被任何现有样式覆盖的空白区域(直接应用新样式) let coveredRanges = allFragments.map(f => ({ start: f.start, end: f.end })); coveredRanges.sort((a, b) => a.start - b.start); let lastCoveredEnd = start; for (const range of coveredRanges) { if (range.start > lastCoveredEnd) { // 添加未被覆盖的部分 const newFragment = { start: lastCoveredEnd, end: range.start, [property]: value }; allFragments.push(newFragment); } lastCoveredEnd = Math.max(lastCoveredEnd, range.end); } // 检查最后一段未覆盖的空白区域 if (lastCoveredEnd < end) { const newFragment = { start: lastCoveredEnd, end: end, [property]: value }; allFragments.push(newFragment); } // 第五步:添加所有不受影响的现有样式和新创建的片段 for (const { style } of affectedStyles) { if (style.end <= start || style.start >= end) { // 添加完全不受影响的样式 styles.push(style); } } // 添加新创建的片段 styles.push(...allFragments); // 合并相邻且样式完全相同的片段(优化渲染性能) this.mergeAdjacentStyles(cellKey); }, /** * 渲染带部分样式的单元格文本 * 将存储的样式片段转换为带<span>标签的HTML,实现局部文本样式 * @param {number} row 行索引 * @param {number} col 列索引 */ renderCellWithPartialStyles(row, col) { const cell = this.hot.getCell(row, col); if (!cell) return; let cellValue = this.hot.getDataAtCell(row, col); // 确保cellValue是字符串(处理数字/undefined等情况) cellValue = cellValue != null ? cellValue.toString() : ''; const cellKey = `${row}-${col}`; const styles = this.partialTextStyles[cellKey] || []; // 如果没有样式,直接显示纯文本 if (styles.length === 0) { cell.textContent = cellValue; return; } // 按起始位置排序样式(确保从左到右渲染) const sortedStyles = [...styles].sort((a, b) => a.start - b.start); // 过滤无效样式(范围超出文本长度或起始>=结束的样式) const validStyles = sortedStyles.filter(style => style.start >= 0 && style.end <= cellValue.length && style.start < style.end ); // 构建带样式的HTML内容 let htmlContent = ''; let lastIndex = 0; validStyles.forEach(style => { // 添加样式片段前的普通文本(未被样式覆盖的部分) if (style.start > lastIndex) { htmlContent += this.escapeHtml(cellValue.substring(lastIndex, style.start)); } // 添加带样式的文本(确保范围正确) if (style.start < cellValue.length) { // 计算安全的文本范围(避免超出字符串长度) const styledText = cellValue.substring( Math.max(style.start, 0), Math.min(style.end, cellValue.length) ); // 构建样式字符串 let styleString = ''; if (style.color) styleString += `color: ${style.color};`; if (style.backgroundColor) styleString += `background-color: ${style.backgroundColor};`; if (style.fontWeight) styleString += `font-weight: ${style.fontWeight};`; if (style.fontStyle) styleString += `font-style: ${style.fontStyle};`; if (style.textDecoration) styleString += `text-decoration: ${style.textDecoration};`; if (style.fontFamily) styleString += `font-family: ${style.fontFamily};`; if (style.fontSize) styleString += `font-size: ${style.fontSize}px;`; if (style.textAlign) { styleString += `text-align: ${style.textAlign};`; // 对于内联元素,需要设置display为inline-block才能应用textAlign styleString += `display: inline-block; width: 100%;`; } if (style.verticalAlign) { styleString += `vertical-align: ${style.verticalAlign};`; styleString += `display: inline-block; height: 100%;`; } // 添加带样式的span标签(使用escapeHtml防止XSS和HTML解析问题) htmlContent += `<span style="${styleString}">${this.escapeHtml(styledText)}</span>`; } // 更新最后处理的索引,避免重复渲染 lastIndex = Math.min(style.end, cellValue.length); }); // 添加剩余未处理的文本(样式片段之后的部分) if (lastIndex < cellValue.length) { htmlContent += this.escapeHtml(cellValue.substring(lastIndex)); } // 更新单元格内容 cell.innerHTML = htmlContent; }, escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }, setupTextSelectionListener() { // 移除旧的监听器(如果存在) document.removeEventListener('selectionchange', this.handleTextSelection); // 添加新的选择变化监听 document.addEventListener('selectionchange', this.handleTextSelection); console.log('文本选择监听器已启用'); }, handleTextSelection() { const selection = window.getSelection(); const selectedText = selection.toString().trim(); if (selectedText) { // 获取当前激活的单元格 const selectedRange = this.hot.getSelectedLast(); if (!selectedRange) return; const [row, col] = selectedRange; let cellValue = this.hot.getDataAtCell(row, col); // 确保cellValue是字符串 cellValue = cellValue != null ? cellValue.toString() : ''; // 修复:直接获取文本框选择位置而非使用indexOf const activeElement = document.activeElement; if (activeElement && activeElement.tagName === 'TEXTAREA') { const start = activeElement.selectionStart; const end = activeElement.selectionEnd; if (start !== end) { // 确保有实际选择 this.currentTextSelection = { text: selectedText, start: start, end: end, row: row, col: col }; return; } } // 仅在无法获取文本框选择位置时使用indexOf作为回退方案 const start = cellValue.indexOf(selectedText); if (start !== -1) { this.currentTextSelection = { text: selectedText, start: start, end: start + selectedText.length, row: row, col: col }; } } }, initStyles() { const styles = {}; const { data } = this.generateTableFromTableData(); data.forEach((row, rowIndex) => { row.forEach((cell, colIndex) => { styles[`${rowIndex}-${colIndex}`] = { fontFamily: 'Arial', fontSize: '14px', fontWeight: 'normal', fontStyle: 'normal', textDecoration: 'none', textAlign: 'center', verticalAlign: 'middle', backgroundColor: '#ffffff', color: '#000000', padding: '5px', // minWidth: "100px", }; }); }); this.cellStyles = styles; }, initializeHotTable() { const container = this.$refs.hotTable; const self = this; // 初始化样式 this.initStyles(); // 从 tableData 生成表格数据 const { data, columns, nestedHeaders, mergeCells } = this.generateTableFromTableData(); console.log('mergeCells', mergeCells) this.hot = new Handsontable(container, { language: 'zh-CN', data: this.tableList, columns: columns, nestedHeaders: nestedHeaders, width: '100%', autoColumnSize: true, // 自动调整列宽以适应内容 stretchH: 'all', mergeCells: mergeCells, // 动态设置mergeCells // colHeaders: true, // rowHeaders: true, height: 700, licenseKey: 'non-commercial-and-evaluation', contextMenu: { items: { // alignment: true, // 自定义菜单项 custom_insert_above: { name: '插入计算行', callback: function (key, selection) { // 您的自定义插入逻辑 // console.log('自定义上方插入行', key, selection, data); self.$emit('changeRowDialogVisible', true); self.$emit('custom-insert-row', key, selection, data); }.bind(this) }, // 自定义菜单项 custom_insert_itself: { name: '插入单独行', callback: function (key, selection) { // 您的自定义插入逻辑 // console.log('自定义上方插入行', key, selection, JSON.stringify(data)); self.$emit('changeRowDialogVisible', true); // 插入单独行 self.$emit('custom-insert-row', key, selection, data, 'itself'); }.bind(this) }, // 自定义菜单项 custom_insert_column: { name: '插入列', callback: function (key, selection) { // 您的自定义插入逻辑 console.log('自定义上方插入行', key, selection, data); self.$emit('changeRowDialogVisible', true); self.$emit('custom-insert-row', key, selection, data, "column"); }.bind(this) }, } }, // dropdownMenu: true, // multiColumnSorting: true, // filters: true, // manualRowResize: true, // manualColumnResize: true, // autoWrapRow: true, // autoWrapCol: true, afterBeginEditing: () => { // 开始编辑时设置监听 self.flag = false self.rowFlage = true setTimeout(() => this.setupTextSelectionListener(), 100); }, afterSelectionEnd: (r1, c1, r2, c2) => { // 选择结束时如果是单元格选择而非文本选择,清除文本选择信息 if (r1 !== r2 || c1 !== c2) { this.currentTextSelection = { text: '', start: -1, end: -1, row: -1, col: -1 }; } }, // 在 initializeHotTable 方法的 cells 渲染器中更新部分文字样式处理 cells: function (row, col) { const cellMeta = {}; const style = self.cellStyles[`${row}-${col}`]; const cellKey = `${row}-${col}`; const partialStyles = self.partialTextStyles[cellKey] || []; if (style) { cellMeta.renderer = function (instance, td, row, col, prop, value, cellProperties) { // 调用基础渲染器 Handsontable.renderers.TextRenderer.apply(this, arguments); // 应用基本样式 td.style.fontFamily = style.fontFamily || 'Arial'; td.style.fontSize = style.fontSize || '12px'; td.style.fontWeight = style.fontWeight || 'normal'; td.style.fontStyle = style.fontStyle || 'normal'; td.style.textDecoration = style.textDecoration || 'none'; td.style.color = style.color || '#000000'; td.style.backgroundColor = style.backgroundColor || '#ffffff'; td.style.textAlign = style.textAlign || 'center'; td.style.verticalAlign = style.verticalAlign || 'middle'; td.style.padding = style.padding || '5px'; // td.style.minWidth = "50px"; // 在 cells 渲染器中更新部分文字样式处理 if (partialStyles.length > 0 && value != null) { // 确保value是字符串 const stringValue = value.toString(); // 清空单元格内容 td.innerHTML = ''; // 按起始位置排序样式 const sortedStyles = [...partialStyles].sort((a, b) => a.start - b.start); // 验证样式范围 const validStyles = sortedStyles.filter(style => style.start >= 0 && style.end <= stringValue.length && style.start < style.end ); let lastIndex = 0; let newContent = document.createDocumentFragment(); validStyles.forEach(style => { // 添加样式前的文本 if (style.start > lastIndex) { const normalText = document.createTextNode( stringValue.substring(lastIndex, style.start) ); newContent.appendChild(normalText); } // 添加带样式的文本 if (style.start < stringValue.length) { const styledSpan = document.createElement('span'); const styledText = stringValue.substring( Math.max(style.start, 0), Math.min(style.end, stringValue.length) ); // 应用所有部分文字样式 if (style.color) styledSpan.style.color = style.color; if (style.fontWeight) styledSpan.style.fontWeight = style.fontWeight; if (style.fontStyle) styledSpan.style.fontStyle = style.fontStyle; if (style.textDecoration) styledSpan.style.textDecoration = style.textDecoration; if (style.fontFamily) styledSpan.style.fontFamily = style.fontFamily; if (style.fontSize) styledSpan.style.fontSize = `${style.fontSize}px`; if (style.verticalAlign) styledSpan.style.verticalAlign = style.verticalAlign; styledSpan.textContent = styledText; newContent.appendChild(styledSpan); } lastIndex = Math.min(style.end, stringValue.length); }); // 添加剩余文本 if (lastIndex < stringValue.length) { const remainingText = document.createTextNode( stringValue.substring(lastIndex) ); newContent.appendChild(remainingText); } td.appendChild(newContent); } }; } return cellMeta; }, afterSelection: (r1, c1, r2, c2) => { // console.log('r1, c1, r2, c2',r1, c1, r2, c2) this.handleSelection(r1, c1, r2, c2); }, // 设置表头样式类名 headerClassName: 'custom-header', afterChange: (changes, source) => { if (source !== 'loadData' && changes && source !== 'MergeCells') { console.log('2222', source) console.log('afterChange', source, JSON.stringify(changes)) // console.log('this.tableList',this.tableList) this.calculateWudongdeInterval(); // this.$emit('data-change', this.hot.getData()); } } }); }, generateTableFromTableData() { const that = this const { headers, rows, updateTime, remarks } = this.tableData; console.log('this.tableData',this.tableData) if (!headers || !headers.length) return { data: [], columns: [], nestedHeaders: [], mergeCells: [] } // 生成列配置 const columns = headers.length && headers.map((header, index) => ({ data: index, title: header, // type: 'numeric', numericFormat: { pattern: '0,0' } })); // heaert("","") // 生成嵌套表头 - 修正这里让列数匹配 const nestedHeaders = [ [{ label: '时间', colspan: 2 }, ...headers.slice(2).map(header => ({ label: header, colspan: 0 }))], ]; // console.log('consconscons', headers, nestedHeaders) // 生成表格数据(支持相同station行合并) let data = []; const mergeCells = []; let firstOutgoingRowIndex = -1; //记录第一个"出库"行索引 const targetCol = headers.indexOf('昨日实况'); if (this.mergeSameStation) { // 按station分组 const groupedRows = {}; rows.forEach((row, index) => { if (!groupedRows[row.stationName]) { groupedRows[row.stationName] = []; } groupedRows[row.stationName].push({ ...row, originalIndex: index }); }); // 生成合并后的数据 Object.keys(groupedRows).forEach(stationName => { const stationRows = groupedRows[stationName]; stationRows.forEach((row, rowIndex) => { const rowData = []; console.log(' data',data) // 只在第一行显示station名称 if (rowIndex === 0) { rowData.push(row.stationName || ''); // rowData.push(updateTime); // 添加合并单元格配置 if (stationRows.length > 0) { if (row.configName === row.stationName) { mergeCells.push({ row: data.length, col: 0, rowspan: 1, colspan: 2 }); } else { mergeCells.push({ row: data.length, col: 0, rowspan: stationRows.length, colspan: 1 }); } } } else { rowData.push(row.stationName); // 合并行后续行不显示station } rowData.push(row.configName); row.values.forEach(value => { rowData.push(value); }); console.log('rowData',rowData) if (this.active == 2) { if (row && row.configName === '出库' && firstOutgoingRowIndex === -1) { firstOutgoingRowIndex = data.length; } } data.push(rowData); }); }); } else { console.log('3333333333') // 不启用合并的原始逻辑 data = rows.map(row => { const rowData = []; if (row.stationName) { rowData.push(row.stationName); } else { rowData.push(row.stationName); } rowData.push(row.configName); row.values.forEach(value => { rowData.push(value); }); if (this.active == 2) { if (row && row.type && row.configName.toString().trim() === '出库' && firstOutgoingRowIndex === -1) { firstOutgoingRowIndex = index; } } return rowData; }); } if (this.active == 2) { if (firstOutgoingRowIndex !== -1 && targetCol !== -1) { mergeCells.push({ row: firstOutgoingRowIndex, col: targetCol, rowspan: data.length - firstOutgoingRowIndex, colspan: headers.length - targetCol }); // console.log('nestedHeaders', nestedHeaders); // console.log('mergeCells', mergeCells); if (data[firstOutgoingRowIndex]) { data[firstOutgoingRowIndex][targetCol] = remarks; const cellKey = `${firstOutgoingRowIndex}-${targetCol}`; if (!this.cellStyles[cellKey]) { this.cellStyles[cellKey] = { ...this.cellStyles[`0-0`] }; } this.cellStyles[cellKey].textAlign = 'left'; } } } data && data.length && data[0].push(updateTime) if (remarks && this.active === 15) { let newRemarks = remarks const length = data.length; const tzOutRow = rows.find(row => row.configName === '桐子林出库'); const gyOutRow = rows.find(row => row.configName === '观音岩出库'); if (gyOutRow && gyOutRow.values && gyOutRow.values.length > 0) { const gyTodayValue = gyOutRow.values[0]; const gyLaterValues = gyOutRow.values.slice(1,-3).filter(value => value !== null); const gyMaxValue = gyLaterValues.length > 0 ?Math.max(...gyLaterValues):0; const gyMinValue = gyLaterValues.length > 0 ?Math.min(...gyLaterValues):0; newRemarks = remarks.replace('观音岩今日出库预计?', `观音岩今日出库预计${gyTodayValue?? 0}`) .replace('观音岩今日出库预计?立方米每秒', `观音岩今日出库预计${gyTodayValue}立方米每秒`) .replace('预计出库?-?立方米每秒', `预计出库${gyMinValue}-${gyMaxValue}立方米每秒`); } if (tzOutRow && tzOutRow.values && tzOutRow.values.length > 0) { const tzTodayValue = tzOutRow.values[0]; const tzLaterValues = tzOutRow.values.slice(1,-3).filter(value => value !== null); const tzMaxValue = tzLaterValues.length > 0 ?Math.max(...tzLaterValues):0; const tzMinValue = tzLaterValues.length > 0 ?Math.min(...tzLaterValues):0; newRemarks = newRemarks.replace('桐子林今日出库?', `桐子林今日出库${tzTodayValue?? 0}`) .replace('桐子林今日出库?立方米每秒', `桐子林今日出库${tzTodayValue}立方米每秒`) .replace('出库?-?立方米每秒', `出库${tzMinValue}-${tzMaxValue}立方米每秒`); } that.newRemarks = newRemarks for (let i = 0; i < 3; i++) { const newRow = Array(headers.length).fill(''); if (i === 0 && remarks) { newRow[0] = newRemarks; } data.push(newRow); } mergeCells.push({ row: length, col: 0, rowspan: 3, colspan: headers.length }); const startRow = length; const endRow = length + 2; for (let row = startRow; row <= endRow; row++) { for (let col = 0; col < headers.length; col++) { const cellKey = `${row}-${col}`; if (!this.cellStyles[cellKey]) { this.cellStyles[cellKey] = { ...this.cellStyles[`0-0`] }; } this.cellStyles[cellKey].textAlign = 'left'; } } // 更新到数据中 if (firstOutgoingRowIndex !== -1 && targetCol !== -1) { data[firstOutgoingRowIndex][targetCol] = newRemarks; } } this.tableList = data console.log('this.tableList', this.tableList) // return return { data, columns, nestedHeaders, mergeCells: mergeCells }; }, handleSelection(r1, c1, r2, c2) { this.selectedCells = []; const startRow = Math.min(r1, r2); const endRow = Math.max(r1, r2); const startCol = Math.min(c1, c2); const endCol = Math.max(c1, c2); for (let row = startRow; row <= endRow; row++) { for (let col = startCol; col <= endCol; col++) { this.selectedCells.push({ row, col }); } } this.updateToolbarState(); }, // 在methods部分添加乌东德区间计算方法 // 修改乌东德区间计算方法,使其更新所有相关列 calculateWudongdeInterval() { try { // 遍历表格数据,查找相关行 let wudongdeInRowIndex = -1; // 乌东德入库行索引 let guanyinyanOutRowIndex = -1; // 观音岩出库行索引 let tongzilinOutRowIndex = -1; // 桐子林出库行索引 let intervalRowIndex = -1; // 区间行索引 const list =this.tableData.rows for (let i = 0; i < this.tableList.length; i++) { const row = list[i]; // 检查是否存在第二列数据 console.log('row',row) if (row ) { // 查找乌东德入库行 if (row.stationName === '乌东德' && row.configName === '入库') { wudongdeInRowIndex = i; } // 查找观音岩出库行 else if (row.stationName === "乌东德" && row.configName === '观音岩出库') { guanyinyanOutRowIndex = i; } // 查找桐子林出库行 else if (row.stationName === "乌东德" && row.configName === '桐子林出库') { tongzilinOutRowIndex = i; } // 查找乌东德区间行 else if (row.stationName === "乌东德" && row.configName === '区间') { intervalRowIndex = i; } } } console.log('intervalValue', wudongdeInRowIndex, guanyinyanOutRowIndex, tongzilinOutRowIndex, intervalRowIndex) // 检查是否找到了所有需要的行 if (wudongdeInRowIndex !== -1 && guanyinyanOutRowIndex !== -1 && tongzilinOutRowIndex !== -1 && intervalRowIndex !== -1) { // 获取区间行和各源数据行的长度,确保有足够的列可以处理 const intervalRowLength = this.tableList[intervalRowIndex]?.length || null; const wudongdeInRowLength = this.tableList[wudongdeInRowIndex]?.length || null; const guanyinyanOutRowLength = this.tableList[guanyinyanOutRowIndex]?.length || null; const tongzilinOutRowLength = this.tableList[tongzilinOutRowIndex]?.length || null; // 确定需要处理的最大列数(从第3列开始,索引为2) const maxColumnIndex = Math.min(intervalRowLength, wudongdeInRowLength, guanyinyanOutRowLength, tongzilinOutRowLength); // 遍历所有相关列(从索引2开始,对应第3列及以后的列) for (let colIndex = 2; colIndex < maxColumnIndex; colIndex++) { // 获取当前列的相关数据值 const wudongdeInValue = parseFloat(this.tableList[wudongdeInRowIndex][colIndex]) || null; const guanyinyanOutValue = parseFloat(this.tableList[guanyinyanOutRowIndex][colIndex]) || null; const tongzilinOutValue = parseFloat(this.tableList[tongzilinOutRowIndex][colIndex]) || null; // 计算区间值:区间 = 入库 - 观音岩出库 - 桐子林出库 const intervalValue = (guanyinyanOutValue === null && tongzilinOutValue === null && wudongdeInValue === null) ? null : wudongdeInValue - guanyinyanOutValue - tongzilinOutValue; // 更新区间单元格值 if (this.tableList[intervalRowIndex] && colIndex < this.tableList[intervalRowIndex].length) { this.tableList[intervalRowIndex][colIndex] = intervalValue; } } // 触发表格重新渲染 if (this.hot) { this.hot.render(); } console.log('乌东德区间所有列计算完成'); } else { console.warn('未找到计算乌东德区间所需的所有行'); } } catch (error) { console.error('计算乌东德区间时出错:', error); } }, updateToolbarState() { // console.log('this.selectedCells',JSON.stringify(this.tableList),JSON.stringify(this.selectedCells)) if (this.selectedCells.length === 0) return; this.flag = true this.rowFlage = false const firstCell = this.selectedCells[0]; const style = this.cellStyles[`${firstCell.row}-${firstCell.col}`] || {}; this.fontFamily = style.fontFamily || 'Arial'; this.fontSize = parseInt(style.fontSize || '12'); this.fontWeight = style.fontWeight || 'normal'; this.fontStyle = style.fontStyle || 'normal'; this.textDecoration = style.textDecoration || 'none'; this.color = style.color || '#000000'; this.backgroundColor = style.backgroundColor || '#ffffff'; this.textAlign = style.textAlign || 'left'; this.verticalAlign = style.verticalAlign || 'middle'; }, // 添加键盘事件监听 addKeyboardListeners() { this.keyboardHandler = (event) => { // 在键盘事件监听器中添加错误处理 try { if (event.ctrlKey || event.metaKey) { if (event.key === 'z' && !event.shiftKey) { event.preventDefault(); this.undo(); } else if ((event.key === 'z' && event.shiftKey) || event.key === 'y') { event.preventDefault(); this.redo(); } } } catch (error) { console.warn('Keyboard event error:', error); // 静默处理扩展相关的错误 } }; document.addEventListener('keydown', this.keyboardHandler); }, removeKeyboardListeners() { document.removeEventListener('keydown', this.keyboardHandler); }, // 保存当前状态到历史记录 saveToHistory() { // 移除当前索引之后的历史记录 if (this.historyIndex < this.historyStack.length - 1) { this.historyStack = this.historyStack.slice(0, this.historyIndex + 1); } // 添加新状态(同时保存 cellStyles 和 partialTextStyles) this.historyStack.push({ cellStyles: JSON.parse(JSON.stringify(this.cellStyles)), partialTextStyles: JSON.parse(JSON.stringify(this.partialTextStyles)) }); // 限制历史记录大小 if (this.historyStack.length > this.maxHistorySize) { this.historyStack.shift(); } // console.log('this.historyStack.this.historyStack.222222222222', this.historyStack.length) this.historyIndex = this.historyStack.length; }, // 撤销 undo() { console.log('this.historyIndex', this.historyIndex) if (this.historyIndex > 0) { this.historyIndex--; const historyState = this.historyStack[this.historyIndex]; console.log('historyState', historyState.cellStyles) // 只恢复当前历史状态对应的样式,不同时恢复两种样式 if (historyState.cellStyles) { this.cellStyles = JSON.parse(JSON.stringify(historyState.cellStyles)); } if (historyState.partialTextStyles) { this.partialTextStyles = JSON.parse(JSON.stringify(historyState.partialTextStyles)); } this.hot.render(); } }, // 重做 redo() { // console.log('this.historyIndex < this.historyStack.length', this.historyStack) if (this.historyIndex < this.historyStack.length - 1) { this.historyIndex++; const historyState = this.historyStack[this.historyIndex]; // 只恢复当前历史状态对应的样式,不同时恢复两种样式 if (historyState.cellStyles) { this.cellStyles = JSON.parse(JSON.stringify(historyState.cellStyles)); } if (historyState.partialTextStyles) { this.partialTextStyles = JSON.parse(JSON.stringify(historyState.partialTextStyles)); } this.hot.render(); } }, applyStyle(property) { // 保存当前状态到历史记录 this.saveToHistory(); const newStyles = { ...this.cellStyles }; this.selectedCells.forEach(({ row, col }) => { const key = `${row}-${col}`; if (!newStyles[key]) { newStyles[key] = {}; } switch (property) { case 'fontFamily': newStyles[key].fontFamily = this.fontFamily; break; case 'fontSize': newStyles[key].fontSize = `${this.fontSize}px`; break; case 'fontWeight': newStyles[key].fontWeight = this.fontWeight; break; case 'fontStyle': newStyles[key].fontStyle = this.fontStyle; break; case 'textDecoration': newStyles[key].textDecoration = this.textDecoration; break; case 'color': newStyles[key].color = this.color; break; case 'backgroundColor': newStyles[key].backgroundColor = this.backgroundColor; break; case 'textAlign': newStyles[key].textAlign = this.textAlign; break; case 'verticalAlign': newStyles[key].verticalAlign = this.verticalAlign; break; } }); this.cellStyles = newStyles; this.hot.render(); // 强制重新渲染 }, toggleBold() { this.fontWeight = this.fontWeight === 'bold' ? 'normal' : 'bold'; this.applyStyle('fontWeight'); }, toggleItalic() { this.fontStyle = this.fontStyle === 'italic' ? 'normal' : 'italic'; this.applyStyle('fontStyle'); }, toggleUnderline() { this.textDecoration = this.textDecoration === 'underline' ? 'none' : 'underline'; this.applyStyle('textDecoration'); }, exportToExcel(getFileName) { const that = this const data = this.hot.getData(); // 获取表头信息 const headers = this.tableData.headers || this.initialData[0]; // 获取合并单元格信息 const mergeCells = this.hot.getPlugin('mergeCells').mergedCellsCollection.mergedCells; // 获取嵌套表头信息 const nestedHeaders = this.hot.getSettings().nestedHeaders; // console.log('nestedHeaders', mergeCells, headers, nestedHeaders); exportToExcel(data, this.cellStyles, headers, mergeCells, nestedHeaders, getFileName); } }, watch: { initialData: { handler(newVal) { if (this.hot) { this.hot.loadData(JSON.parse(JSON.stringify(newVal))); // this.initStyles(); } }, deep: true } } }; </script> <style scoped> .hot-table-wps { font-family: Arial, sans-serif; display: flex; flex-direction: column; /* height: 100vh; */ } /* 自定义表头样式 */ :deep(.custom-header) { height: 32px; /* 可 columnHeaderHeight 保持一致 */ line-height: 32px; /* 垂直居中 */ background-color: #47a8c8; /* 自定义背景色 */ font-size: 16px; color: #fff; /* 自定义字体大小 */ } .toolbar { display: flex; gap: 8px; padding: 8px; background: #f0f0f0; border-bottom: 1px solid #ddd; flex-wrap: wrap; align-items: center; } .toolbar select, .toolbar input, .toolbar button { padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; background: white; height: 30px; } .toolbar button { min-width: 30px; cursor: pointer; } .toolbar button.active { background: #e0e0e0; } .color-picker { display: flex; align-items: center; gap: 4px; } .color-picker label { font-size: 12px; } .color-picker input[type="color"] { width: 30px; height: 30px; padding: 0; border: 1px solid #ccc; } .export-btn { background: #4CAF50 !important; color: white; border: none; padding: 6px 12px; margin-left: auto; } .export-btn:hover { background: #45a049; } .hot-table-container { width: 100%; height: 400px; /* overflow: hidden; */ } </style> 代码可以实现双击后选中部分文本修改背景色功能,但是有一个bug,当修改后如果再选中一段文本,点击修改文本颜色或者背景色,之前的背景色修改会消失
最新发布
09-18
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值