VxeTable 手写实现区域复制、粘贴功能(非Pro或付费插件实现)
此组件是用Cursor生成的,VxeTable版本对应4.19.16,其它版本未试过。
VxeTableAreaClip
为 vxe-grid 表格提供 Excel 风格的区域框选、复制与粘贴 能力。通过包裹 vxe-grid,用户可像操作电子表格一样拖拽选区,并使用 Ctrl+C / Ctrl+V(macOS 为 Cmd+C / Cmd+V)在单元格间批量复制数据。
依赖
- Vue 3
- vxe-table(
vxe-grid)
将整个 VxeTableAreaClip 目录复制到目标项目的组件目录即可使用,无额外 npm 依赖。
功能特性
- 鼠标拖拽框选矩形区域,支持
Shift + 点击扩展选区 - 选区高亮显示(锚点单元格与选中区域样式区分)
- 复制为 TSV(制表符分隔)格式写入系统剪贴板,可直接粘贴到 Excel
- 从剪贴板粘贴 TSV 数据,按锚点单元格向右、向下填充
- 支持固定列(left / right)场景下的单元格定位
- 可配置单元格是否允许选中 / 粘贴
- 支持自定义复制格式化与粘贴解析(如 ID ↔ 名称互转)
- 编辑控件(input、select 等)内保留原生文本选中行为
目录结构
VxeTableAreaClip/
├── index.vue # 组件入口,包裹 slot 并绑定事件
├── useVxeTableAreaClip.js # 核心 composable(选区、复制、粘贴逻辑)
├── utils.js # TSV 序列化 / 解析、选区归一化工具
├── vxe-table-area-clip.scss # 选区高亮与 user-select 样式
└── README.md # 本说明文件
基本用法
将 vxe-grid 作为默认插槽内容包裹在组件内,并通过 grid-ref 传入表格实例引用。
<template>
<VxeTableAreaClip
ref="clipRef"
:grid-ref="getGridRef"
:can-select-cell="canClipCell"
:can-paste-cell="canClipCell"
:copy-formatter="clipCopyFormatter"
:paste-parser="clipPasteParser"
>
<vxe-grid ref="gridRef" v-bind="gridOptions" />
</VxeTableAreaClip>
</template>
<script setup>
import { ref } from 'vue'
import VxeTableAreaClip from './components/VxeTableAreaClip/index.vue'
const gridRef = ref()
const clipRef = ref()
/** 返回 vxe-grid 实例(推荐 getter 函数,避免实例未挂载时取值为空) */
function getGridRef() {
return gridRef.value
}
/** 仅数据行且存在 field 的列允许框选 */
function canClipCell({ row, column }) {
return !row._isGroup && !!column?.field
}
/** 复制时将内部值转为可读文本 */
function clipCopyFormatter(row, column, value) {
if (value == null || value === '') return ''
if (column.field === 'status') {
return value === '1' ? '启用' : '禁用'
}
return String(value)
}
/** 粘贴时将可读文本解析回内部值 */
function clipPasteParser(row, column, text) {
const raw = text == null ? '' : String(text).trim()
if (raw === '') return null
if (column.field === 'status') {
if (raw === '启用' || raw === '1') return '1'
if (raw === '禁用' || raw === '0') return '0'
}
return raw
}
</script>
注意:包裹容器需有明确高度(如
height: 100%或flex: 1),否则表格区域可能无法正常展示与框选。
Props
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
gridRef |
Object | Function |
null |
vxe-grid 实例引用。支持直接传 ref 对象,或传返回实例的 getter 函数(推荐) |
canSelectCell |
Function |
() => true |
判断单元格是否允许被选入选区。参数:{ row, column } |
canPasteCell |
Function |
() => true |
判断单元格是否允许接收粘贴。参数:{ row, column, value } |
copyFormatter |
Function |
null |
复制时格式化单元格值。参数:(row, column, value) => string。未传时默认 String(value) |
pasteParser |
Function |
null |
粘贴时解析剪贴板文本。参数:(row, column, text) => any。未传时原样写入 |
Events
| 事件名 | 参数 | 说明 |
|---|---|---|
selection-change |
range: { r1, r2, c1, c2 } |
选区变化时触发,表示归一化后的行列索引范围 |
pasted |
{ changes: Array<{ row, column, value }> } |
粘贴完成后触发,包含所有被修改的单元格 |
暴露方法
通过 ref 可调用以下方法(defineExpose):
| 方法 | 说明 |
|---|---|
remount() |
卸载并重新挂载事件监听。表格列变更、数据重载后若框选失效,应调用此方法 |
refreshHighlight() |
仅刷新选区高亮,不重新绑定事件 |
典型调用时机——数据加载完成后:
import { nextTick } from 'vue'
nextTick(() => {
gridRef.value?.recalculate?.()
clipRef.value?.remount?.()
})
交互说明
| 操作 | 行为 |
|---|---|
| 单击单元格 | 以该单元格为锚点,选区收缩为单格 |
| 拖拽 | 框选矩形区域 |
Shift + 单击 |
以已有锚点为起点,扩展选区至目标单元格 |
Ctrl+C / Cmd+C |
复制选区内容到剪贴板(TSV 格式) |
Ctrl+V / Cmd+V |
从剪贴板粘贴,以锚点单元格为左上角向右、向下填充 |
| 单元格内编辑 | 点击 input / select 等编辑控件时,不触发框选,保留原生输入行为 |
自定义复制 / 粘贴
当列存储的是 ID、枚举值等内部数据,而用户期望在 Excel 中看到可读文本时,需实现 copyFormatter 与 pasteParser 配对使用。
复制:将 categoryId: 3 格式化为 "电子产品"
粘贴:将 "电子产品" 解析回 3
未实现 pasteParser 时,粘贴的是原始文本;未实现 copyFormatter 时,复制的是单元格原始值。
注意事项
grid-ref推荐使用 getter 函数(如:grid-ref="getGridRef"),确保表格挂载后能取到最新实例。- 仅
field不为空且visible !== false的列参与复制粘贴。 - 粘贴直接修改行对象(
row[field] = value),并调用vxe-grid的updateStatus刷新编辑状态;不会自动触发保存,需由业务页面自行处理。 - 剪贴板读写依赖浏览器权限(
navigator.clipboard),非 HTTPS 或权限被拒时复制/粘贴可能静默失败。 - 表格列动态变更后(如切换查询条件重载 columns),需调用
clipRef.remount()重新绑定事件。 - 树形表格中,通常通过
canSelectCell限制仅数据行可参与框选,分组行、汇总行等应排除在外。
移植说明
- 将
VxeTableAreaClip整个目录复制到目标项目。 - 按目标项目的路径别名调整
import路径(示例中使用相对路径./components/VxeTableAreaClip/index.vue)。 - 确保目标项目已安装并注册 vxe-table。
- 若目标项目使用不同的 CSS 变量体系,可按需调整
vxe-table-area-clip.scss中的高亮颜色(默认使用--el-color-primary)。
代码
点击查看 index.vue 代码
<template>
<div ref="wrapRef" class="vxe-table-area-clip-wrap">
<slot/>
</div>
</template>
<script setup>
import {nextTick, onBeforeUnmount, onMounted, ref, watch} from 'vue'
import {useVxeTableAreaClip} from './useVxeTableAreaClip'
import './vxe-table-area-clip.scss'
const props = defineProps({
/** vxe-grid 实例引用,支持 ref 对象或返回实例的 getter 函数 */
gridRef: {
type: [Object, Function],
default: null
},
/** 判断单元格是否允许被选入选区,参数为 { row, column } */
canSelectCell: {
type: Function,
default: () => true
},
/** 判断单元格是否允许接收粘贴,参数为 { row, column, value } */
canPasteCell: {
type: Function,
default: () => true
},
/** 复制时将单元格值格式化为剪贴板文本,参数为 (row, column, value) */
copyFormatter: {
type: Function,
default: null
},
/** 粘贴时将剪贴板文本解析为单元格值,参数为 (row, column, text) */
pasteParser: {
type: Function,
default: null
}
})
/** 组件事件定义 */
const emit = defineEmits(['pasted', 'selection-change'])
/** 包裹容器 DOM 引用 */
const wrapRef = ref()
/** 区域复制粘贴 composable 实例 */
const clip = useVxeTableAreaClip(() => resolveGridRef(props.gridRef), {
canSelectCell: (...args) => props.canSelectCell(...args),
canPasteCell: (...args) => props.canPasteCell(...args),
copyFormatter: (...args) => (props.copyFormatter || ((row, column, value) => (value == null ? '' : String(value))))(...args),
pasteParser: (...args) => (props.pasteParser || ((row, column, text) => text))(...args),
onSelectionChange: range => emit('selection-change', range),
onPasted: payload => emit('pasted', payload)
})
/** 解析 gridRef(支持 ref 对象或 getter 函数) */
function resolveGridRef(gridRef) {
const raw = typeof gridRef === 'function' ? gridRef() : gridRef
return raw?.value ?? raw
}
/** 卸载并重新挂载剪贴板事件监听 */
function remountClip() {
clip.unmount()
const unbind = clip.mount(wrapRef.value)
clip.setCleanup(unbind)
}
watch(() => resolveGridRef(props.gridRef), (val) => {
if (val) nextTick(remountClip)
})
onMounted(() => {
nextTick(remountClip)
})
onBeforeUnmount(() => {
clip.unmount()
})
defineExpose({refreshHighlight: clip.refreshHighlight, remount: remountClip})
</script>
<style scoped>
.vxe-table-area-clip-wrap {
outline: none;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
</style>
点击查看 useVxeTableAreaClip.js 代码
import { ref } from 'vue'
import { normalizeRange, parseTsv, serializeTsv } from './utils'
/** 选区单元格高亮 CSS 类名 */
const SELECTED = 'vxe-area-clip--selected'
/** 选区锚点单元格高亮 CSS 类名 */
const ANCHOR = 'vxe-area-clip--anchor'
/** 从 grid 引用解析 vxe-table 实例 */
function getVxeTable(gridRef) {
const inst = typeof gridRef === 'function' ? gridRef() : gridRef?.value ?? gridRef
if (!inst) return null
return inst.$refs?.xTable || inst
}
/** 从 td 元素提取列 ID(colid) */
function extractColId(td) {
if (!td) return null
const cls = typeof td.className === 'string' ? td.className : td.classList?.toString?.() || ''
const fromClass = cls.match(/\bcol_\d+\b/)?.[0]
if (fromClass) return fromClass
return td.getAttribute?.('colid') || null
}
/** 获取列的固定方向类型(left / right / 空) */
function getColumnFixedType(column) {
const fixed = column?.fixed
if (fixed === true || fixed === 'left') return 'left'
if (fixed === 'right') return 'right'
return ''
}
/** 判断 body wrapper 是否匹配指定固定列类型 */
function matchBodyWrapper(body, fixedType) {
if (!body?.classList) return false
if (fixedType === 'left') return body.classList.contains('fixed-left--wrapper')
if (fixedType === 'right') return body.classList.contains('fixed-right--wrapper')
return body.classList.contains('body--wrapper')
&& !body.classList.contains('fixed-left--wrapper')
&& !body.classList.contains('fixed-right--wrapper')
}
/** 判断 td 是否为可见的数据单元格 */
function isVisibleBodyTd(td) {
return td && !td.classList.contains('fixed--hidden')
}
/** 从 td 元素提取行 ID(rowid) */
function extractRowId(td) {
const rowidEl = td?.querySelector?.('[rowid]') || td?.closest?.('tr')?.querySelector?.('[rowid]')
return rowidEl?.getAttribute('rowid') || null
}
/** 根据鼠标事件解析对应的表格单元格信息 */
function resolveCellFromEvent(event, gridRef) {
const $table = getVxeTable(gridRef)
if (!$table) return null
let td = event.target.closest('td')
if (!td) return null
const colid = extractColId(td)
if (!colid) return null
const column = $table.getColumnById?.(colid)
if (!column?.field) return null
const rowid = extractRowId(td)
if (!rowid) return null
const row = $table.getRowById?.(rowid)
if (!row) return null
if (!isVisibleBodyTd(td)) {
const visibleTd = findTd($table, row, column.field)
if (visibleTd) td = visibleTd
}
return { row, column, field: column.field, td, tr: td.closest('tr') }
}
/** 获取可参与复制粘贴的可见列列表 */
function getClipColumns(gridRef) {
const $table = getVxeTable(gridRef)
if (!$table) return []
return ($table.getColumns?.() || []).filter(c => c.field && c.visible !== false)
}
/** 获取当前可见行数据 */
function getVisibleRows(gridRef) {
const $table = getVxeTable(gridRef)
return $table?.getTableData?.().visibleData ?? []
}
/** 根据行列索引解析单元格信息 */
function resolveCellByIndex(gridRef, rowIndex, colIndex, clipColumns, visibleRows) {
const row = visibleRows[rowIndex]
const column = clipColumns[colIndex]
if (!row || !column?.field) return null
return { row, column, field: column.field }
}
/** 在指定 body 区域内查找可见的 td 元素 */
function findTdInBody(body, rowid, colid) {
const markers = body.querySelectorAll(`[rowid="${rowid}"]`)
for (const marker of markers) {
const tr = marker.closest('tr')
const td = tr?.querySelector(`td.${colid}`)
if (isVisibleBodyTd(td)) return td
}
return null
}
/** 根据行和字段名查找对应的可见 td 元素 */
function findTd($table, row, field) {
const column = $table.getColumnByField?.(field)
const colid = column?.id
const rowid = row?.rowKey
if (!colid || !rowid) return null
const fixedType = getColumnFixedType(column)
const wrappers = $table.$el?.querySelectorAll('.vxe-table--body-wrapper') || []
for (const body of wrappers) {
if (!matchBodyWrapper(body, fixedType)) continue
const td = findTdInBody(body, rowid, colid)
if (td) return td
}
for (const body of wrappers) {
const td = findTdInBody(body, rowid, colid)
if (td) return td
}
return null
}
/** vxe-table 区域复制粘贴 composable */
export function useVxeTableAreaClip(gridRef, options = {}) {
const {
canSelectCell = () => true,
canPasteCell = () => true,
copyFormatter = (row, column, value) => (value == null ? '' : String(value)),
pasteParser = (row, column, text) => text,
onSelectionChange,
onPasted
} = options
/** 选区锚点单元格索引 */
const anchor = ref(null)
/** 选区焦点单元格索引 */
const focus = ref(null)
/** 是否正在拖拽框选 */
const isDragging = ref(false)
/** 键盘事件聚焦元素 */
let focusEl = null
/** 事件解绑清理函数 */
let cleanup = null
/** 清除所有选区高亮样式 */
function clearHighlight() {
const $table = getVxeTable(gridRef)
$table?.$el?.querySelectorAll(`.${SELECTED}, .${ANCHOR}`).forEach(el => {
el.classList.remove(SELECTED, ANCHOR)
})
}
/** 根据当前锚点与焦点重新绘制选区高亮 */
function applyHighlight() {
clearHighlight()
const range = normalizeRange(anchor.value, focus.value)
if (!range) return
const $table = getVxeTable(gridRef)
if (!$table) return
const clipColumns = getClipColumns(gridRef)
const visibleRows = getVisibleRows(gridRef)
for (let r = range.r1; r <= range.r2; r++) {
for (let c = range.c1; c <= range.c2; c++) {
const cell = resolveCellByIndex(gridRef, r, c, clipColumns, visibleRows)
if (!cell || !canSelectCell({ row: cell.row, column: cell.column })) continue
const td = findTd($table, cell.row, cell.field)
if (!td) continue
td.classList.add(SELECTED)
if (anchor.value && r === anchor.value.rowIndex && c === anchor.value.colIndex) {
td.classList.add(ANCHOR)
}
}
}
onSelectionChange?.(range)
}
/** 将单元格信息转换为行列索引 */
function cellToIndex(cell) {
const clipColumns = getClipColumns(gridRef)
const visibleRows = getVisibleRows(gridRef)
const rowIndex = visibleRows.findIndex(r => r.rowKey === cell.row.rowKey)
const colIndex = clipColumns.findIndex(c => c.field === cell.field)
if (rowIndex < 0 || colIndex < 0) return null
return { rowIndex, colIndex }
}
/** 清除浏览器原生文本选区 */
function clearNativeSelection() {
window.getSelection?.()?.removeAllRanges?.()
}
/** 判断事件目标是否为可编辑控件 */
function isEditableTarget(target) {
return !!target?.closest?.('input, textarea, select, .vxe-input, .vxe-select, .vxe-tree-select, .vxe-number-input, .vxe-textarea')
}
/** 阻止表格区域的原生文本选中 */
function onSelectStart(e) {
if (isEditableTarget(e.target)) return
const cell = resolveCellFromEvent(e, gridRef)
if (!cell) return
if (!canSelectCell({ row: cell.row, column: cell.column })) return
e.preventDefault()
}
/** 鼠标按下时初始化或扩展选区 */
function onMouseDown(e) {
if (e.button !== 0) return
if (isEditableTarget(e.target)) return
const cell = resolveCellFromEvent(e, gridRef)
if (!cell) return
if (!canSelectCell({ row: cell.row, column: cell.column })) return
const idx = cellToIndex(cell)
if (!idx) return
clearNativeSelection()
focusEl?.focus?.({ preventScroll: true })
if (e.shiftKey && anchor.value) {
focus.value = idx
applyHighlight()
return
}
anchor.value = idx
focus.value = idx
isDragging.value = true
document.addEventListener('mousemove', onDocumentMouseMove, { passive: false })
applyHighlight()
}
/** 鼠标移动时更新选区焦点 */
function onMouseMove(e) {
if (!isDragging.value) return
e.preventDefault()
clearNativeSelection()
const cell = resolveCellFromEvent(e, gridRef)
if (!cell) return
if (!canSelectCell({ row: cell.row, column: cell.column })) return
const idx = cellToIndex(cell)
if (!idx) return
focus.value = idx
applyHighlight()
}
/** document 级鼠标移动代理(拖拽框选) */
function onDocumentMouseMove(e) {
if (!isDragging.value) return
onMouseMove(e)
}
/** 鼠标松开时结束拖拽框选 */
function onMouseUp() {
if (isDragging.value) {
document.removeEventListener('mousemove', onDocumentMouseMove)
clearNativeSelection()
}
isDragging.value = false
}
/** 构建当前选区的复制矩阵 */
function buildCopyMatrix() {
const range = normalizeRange(anchor.value, focus.value)
if (!range) return null
const clipColumns = getClipColumns(gridRef)
const visibleRows = getVisibleRows(gridRef)
const matrix = []
for (let r = range.r1; r <= range.r2; r++) {
const rowArr = []
for (let c = range.c1; c <= range.c2; c++) {
const cell = resolveCellByIndex(gridRef, r, c, clipColumns, visibleRows)
if (!cell || !canSelectCell({ row: cell.row, column: cell.column })) {
rowArr.push('')
continue
}
rowArr.push(copyFormatter(cell.row, cell.column, cell.row[cell.field]))
}
matrix.push(rowArr)
}
return matrix
}
/** 将当前选区内容复制到剪贴板 */
async function copySelection() {
const matrix = buildCopyMatrix()
if (!matrix?.length) return
const text = serializeTsv(matrix)
try {
await navigator.clipboard.writeText(text)
} catch {
}
}
/** 从剪贴板读取内容并粘贴到选区 */
async function pasteSelection() {
if (!anchor.value) return
let text = ''
try {
text = await navigator.clipboard.readText()
} catch {
return
}
const matrix = parseTsv(text)
if (!matrix.length) return
const clipColumns = getClipColumns(gridRef)
const visibleRows = getVisibleRows(gridRef)
const $table = getVxeTable(gridRef)
const changes = []
for (let ri = 0; ri < matrix.length; ri++) {
for (let ci = 0; ci < matrix[ri].length; ci++) {
const rowIndex = anchor.value.rowIndex + ri
const colIndex = anchor.value.colIndex + ci
const cell = resolveCellByIndex(gridRef, rowIndex, colIndex, clipColumns, visibleRows)
if (!cell) continue
const raw = matrix[ri][ci]
const value = pasteParser(cell.row, cell.column, raw)
if (!canPasteCell({ row: cell.row, column: cell.column, value })) continue
cell.row[cell.field] = value
changes.push({ row: cell.row, column: cell.column, value })
}
}
if (changes.length && $table?.updateStatus) {
changes.forEach(({ row }) => $table.updateStatus({ row }))
}
onPasted?.({ changes })
applyHighlight()
}
/** 处理 Ctrl/Cmd+C/V 快捷键 */
function onKeyDown(e) {
if (e.target.closest('input, textarea, select, .vxe-input, .vxe-select, .vxe-tree-select, .vxe-number-input, .vxe-textarea')) return
if (!anchor.value) return
const mod = e.ctrlKey || e.metaKey
if (!mod) return
if (e.key === 'c' || e.key === 'C') {
e.preventDefault()
copySelection()
} else if (e.key === 'v' || e.key === 'V') {
e.preventDefault()
pasteSelection()
}
}
/** 阻止表格区域的原生拖拽行为 */
function onDragStart(e) {
if (isEditableTarget(e.target)) return
const cell = resolveCellFromEvent(e, gridRef)
if (!cell) return
if (!canSelectCell({ row: cell.row, column: cell.column })) return
e.preventDefault()
}
/** 绑定鼠标、键盘等事件监听 */
function bindEvents(rootEl, wrapEl) {
if (!rootEl) return
focusEl = wrapEl || rootEl
rootEl.addEventListener('mousedown', onMouseDown)
rootEl.addEventListener('mousemove', onMouseMove, { passive: false })
rootEl.addEventListener('selectstart', onSelectStart)
rootEl.addEventListener('dragstart', onDragStart)
document.addEventListener('mouseup', onMouseUp)
document.addEventListener('keydown', onKeyDown)
}
/** 解绑所有事件监听 */
function unbindEvents(rootEl) {
if (!rootEl) return
rootEl.removeEventListener('mousedown', onMouseDown)
rootEl.removeEventListener('mousemove', onMouseMove)
rootEl.removeEventListener('selectstart', onSelectStart)
rootEl.removeEventListener('dragstart', onDragStart)
document.removeEventListener('mousemove', onDocumentMouseMove)
document.removeEventListener('mouseup', onMouseUp)
document.removeEventListener('keydown', onKeyDown)
isDragging.value = false
focusEl = null
}
/** 挂载事件监听并返回解绑函数 */
function mount(rootEl) {
const el = rootEl?.value || rootEl
if (!el) return
el.setAttribute('tabindex', '0')
const $table = getVxeTable(gridRef)
const tableRoot = $table?.$el || el
bindEvents(tableRoot, el)
return () => unbindEvents(tableRoot)
}
/** 刷新选区高亮(表格数据或列变化后调用) */
function refreshHighlight() {
applyHighlight()
}
return {
anchor,
focus,
mount,
unmount: () => {
cleanup?.()
cleanup = null
clearHighlight()
},
refreshHighlight,
setCleanup: fn => { cleanup = fn },
remount: (rootEl) => {
cleanup?.()
cleanup = mount(rootEl)
}
}
}
export default useVxeTableAreaClip
点击查看 utils.js 代码
/** 将锚点与焦点单元格索引归一化为矩形选区(r1/r2/c1/c2) */
export function normalizeRange(anchor, focus) {
if (!anchor || !focus) return null
return {
r1: Math.min(anchor.rowIndex, focus.rowIndex),
r2: Math.max(anchor.rowIndex, focus.rowIndex),
c1: Math.min(anchor.colIndex, focus.colIndex),
c2: Math.max(anchor.colIndex, focus.colIndex)
}
}
/** 将二维矩阵序列化为制表符分隔的 TSV 文本 */
export function serializeTsv(matrix) {
return matrix.map(row => row.map(v => v == null ? '' : String(v)).join('\t')).join('\n')
}
/** 将剪贴板 TSV 文本解析为二维矩阵 */
export function parseTsv(text) {
if (!text) return []
const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n')
while (lines.length && lines[lines.length - 1] === '') {
lines.pop()
}
return lines.map(line => line.split('\t'))
}
点击查看 vxe-table-area-clip.scss 代码
.vxe-table-area-clip-wrap {
user-select: none;
-webkit-user-select: none;
.vxe-table,
.vxe-cell,
.vxe-cell--label {
user-select: none;
-webkit-user-select: none;
}
input,
textarea,
select,
.vxe-input--inner,
.vxe-number-input--inner,
.vxe-textarea--inner {
user-select: text;
-webkit-user-select: text;
}
}
.vxe-area-clip--selected {
background-color: rgba(64, 158, 255, 0.18) !important;
box-shadow: inset 0 0 0 1px var(--el-color-primary);
}
.vxe-area-clip--anchor {
background-color: rgba(64, 158, 255, 0.28) !important;
}
浙公网安备 33010602011771号