<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,当修改后如果再选中一段文本,点击修改文本颜色或者背景色,之前的背景色修改会消失
最新发布