Files
yihuiyong-ui/apps/web-ele/src/components/db-hst/index.vue

478 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref, nextTick, createVNode, render, watch, computed } from 'vue'
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
import Handsontable from "handsontable";
import { HotTable } from '@handsontable/vue3'
import { registerLanguageDictionary, zhCN } from 'handsontable/i18n'
import { registerAllModules } from 'handsontable/registry'
import 'handsontable/styles/handsontable.css'
// import 'handsontable/styles/ht-theme-main.css'
import 'handsontable/styles/ht-theme-classic.css';
registerAllModules()
registerLanguageDictionary(zhCN)
import { handlerDropdownRenderer } from './dropdown'
import { handlerTableRenderer } from './table'
import { handlerDuplicateCodeRenderer } from './text'
import { computeCodeColWidth,codeRenderer } from './tree'
import ContextMenu from './contextmenu.vue'
import { handleRowOperation } from '#/components/db-hst/tree'
// import { sourceDataObject } from './mockData'
// const language = ref('zh-CN')
defineOptions({ name: 'DbHst' });
const componentProps = defineProps<{
settings?: any
contextMenuItems?: Array<{
key: string
name: string
callback?: (hotInstance: any) => void
separator?: boolean
}>
}>()
// 导入和注册插件和单元格类型
// import { registerCellType, NumericCellType } from 'handsontable/cellTypes';
// import { registerPlugin, UndoRedo } from 'handsontable/plugins';
// registerCellType(NumericCellType);
// registerPlugin(UndoRedo);
// const tableHeight = computed(() => componentProps.height ?? 0)
// const defaultData = ref<any[][]>(Array.from({ length: 30 }, () => Array(componentProps.columns?.length ?? 0).fill('')))
const hotTableComponent = ref<any>(null)
const selectedRow = ref<number | null>(null) // 记录当前选中的行
const codeColWidth = ref<number>(120)
// const colHeaders = ref<string[]>([])
let defaultSettings = {
// themeName: 'ht-theme-main',
themeName: 'ht-theme-classic',
language: 'zh-CN',
// data: sourceDataObject,
// colWidths: [100, 120, 100, 100, 100, 100],
// rowHeights: [30, 30, 30, 30, 30, 30],
colWidths: 120, // 固定列宽
// colWidths(index) {
// return (index + 1) * 40;
// },
// colWidths: undefined,
rowHeights: 23, // 固定行高
wordWrap: false,// 禁止单元格内容自动换行
//manualColumnMove: true,
manualColumnResize: true,
autoRowSize: false,
autoColumnSize: false,
renderAllRows: false,
viewportColumnRenderingOffset: 12,//渲染列数
viewportRowRenderingOffset: 12,//渲染行数
// colHeaders: componentProps.colHeaders ?? [],
rowHeaders: false,
// columns: componentProps.columns ?? [],
autoWrapRow: true,
autoWrapCol: true,
width: '100%',
// height: 'auto',
// height: tableHeight.value,
// height: 200,
// stretchH: 'all',
// loading: true,
//contextMenu: true,
// dialog: true,
// dialog: {
// content: 'This dialog can be controlled programmatically.',
// closable: true,
// contentBackground: true,
// background: 'semi-transparent',
// },
licenseKey: '424fc-f3b67-5905b-a191b-9b809',
// 如果使用第一行作为列头colHeaders: false添加以下配置
// cells: function(row: number, col: number) {
// const cellProperties: any = {};
// // 如果 colHeaders 为 false将第一行设置为列头样式
// if (row === 0) {
// cellProperties.readOnly = true; // 不可编辑
// cellProperties.className = 'custom-header-row'; // 自定义样式类
// cellProperties.renderer = function(instance: any, td: HTMLTableCellElement, row: number, col: number, prop: any, value: any, cellProperties: any) {
// Handsontable.renderers.TextRenderer.apply(this, arguments as any);
// td.style.fontWeight = 'bold';
// td.style.backgroundColor = '#f5f5f5';
// td.style.textAlign = 'center';
// td.style.borderBottom = '2px solid #ddd';
// };
// }
// return cellProperties;
// },
// afterSelection(row1: number, _col1: number, _row2: number, _col2: number) {
// const hot = this as any
// if (selectedRow.value !== null && selectedRow.value !== row1) {
// const colCount = hot.countCols()
// for (let c = 0; c < colCount; c++) {
// const meta = hot.getCellMeta(selectedRow.value, c)
// const classes = (meta.className || '').split(' ').filter(Boolean)
// const idx = classes.indexOf('row-highlight')
// if (idx !== -1) classes.splice(idx, 1)
// hot.setCellMeta(selectedRow.value, c, 'className', classes.join(' '))
// }
// }
// selectedRow.value = row1
// const colCount = hot.countCols()
// for (let c = 0; c < colCount; c++) {
// const meta = hot.getCellMeta(row1, c)
// const classes = (meta.className || '').split(' ').filter(Boolean)
// if (!classes.includes('row-highlight')) classes.push('row-highlight')
// hot.setCellMeta(row1, c, 'className', classes.join(' '))
// }
// hot.render()
// },
//afterDeselect() {
// const hot = this as any
// if (selectedRow.value === null) return
// const colCount = hot.countCols()
// for (let c = 0; c < colCount; c++) {
// const meta = hot.getCellMeta(selectedRow.value, c)
// const classes = (meta.className || '').split(' ').filter(Boolean)
// const idx = classes.indexOf('row-highlight')
// if (idx !== -1) classes.splice(idx, 1)
// hot.setCellMeta(selectedRow.value, c, 'className', classes.join(' '))
// }
// selectedRow.value = null
// hot.render()
//},
// modifyColWidth(width: number, col: number) {
// const hot = hotInstance.value
// if (!hot) return width
// const codeCol = hot.propToCol('code')
// // console.log('modifyColWidth',codeCol,width)
// return col === codeCol ? (codeColWidth.value ?? width) : width
// },
// afterChange(changes: any, source: string) {
// if (!changes || !hotInstance.value) return
// if (source !== 'edit' && source !== 'Autofill' && source !== 'UndoRedo') return
// const hot = hotInstance.value
// const codeCol = hot.propToCol('code')
// const hasCodeEdit = changes.some((c: any) => c && (c[1] === 'code' || c[1] === codeCol))
// // console.log('afterChange',changes,hasCodeEdit, codeCol)
// if (!hasCodeEdit) return
// codeColWidth.value = computeCodeColWidth(hot)
// // console.log('afterChange',codeColWidth.value)
// hot.render()
// // console.log('afterChange',codeColWidth.value)
// },
afterOnCellContextMenu: () => {
// Handsontable 内置右键菜单打开后,关闭自定义菜单
contextMenuRef.value?.hideContextMenu?.()
},
}
// 合并外部 settings 和默认配置
let hotSettings = {}
// 保留必要的回调函数
const hotInstance = ref<any>(null)
const contextMenuRef = ref<any>(null)
// 处理右键菜单事件
const handleContextMenu = (event: MouseEvent) => {
contextMenuRef.value?.handleContextMenu(event)
}
onMounted(() => {
hotInstance.value = hotTableComponent.value?.hotInstance
})
onUnmounted(() => {
})
watch(
() => componentProps.settings,
(newSettings) => {
if (!newSettings) return
const merged = {
...defaultSettings,
...newSettings,
}
Object.assign(hotSettings, merged)
hotSettings = merged
// console.log(merged)
},
{ immediate: true,deep:true }
)
const loadData = (rows: any[][]) => {
if (!hotInstance.value) return
// hotInstance.value.loadData(rows.length === 0?defaultData.value:rows)
hotInstance.value.loadData(rows)
//console.log('Source Data:', hotInstance.value.getSourceData());
}
const updateCodeColWidth = () => {
if (!hotInstance.value) return
const newWidth = computeCodeColWidth(hotInstance.value)
// 如果宽度没有变化,不需要更新
if (newWidth === codeColWidth.value) return
codeColWidth.value = newWidth
// 查找配置了 code: true 的列
const currentSettings = hotInstance.value.getSettings()
const columns = currentSettings.columns || []
const codeColIndex = columns.findIndex((col: any) => col.code === true)
if (codeColIndex !== null && codeColIndex >= 0) {
// 获取当前列数
const colCount = hotInstance.value.countCols()
const currentColWidths = currentSettings.colWidths
// 构建新的列宽数组
const newColWidths: number[] = []
for (let i = 0; i < colCount; i++) {
if (i === codeColIndex) {
newColWidths[i] = codeColWidth.value
} else {
// 获取其他列的当前宽度
if (Array.isArray(currentColWidths)) {
newColWidths[i] = currentColWidths[i] || 100
} else if (typeof currentColWidths === 'function') {
newColWidths[i] = 100
} else {
newColWidths[i] = currentColWidths || 100
}
}
}
console.log(newColWidths)
// 更新列宽
hotInstance.value.updateSettings({
colWidths: newColWidths
})
}
hotInstance.value.render()
}
defineExpose({ loadData, hotTableComponent, hotInstance, updateCodeColWidth, codeColWidth })
Handsontable.renderers.registerRenderer("db-table", handlerTableRenderer);
Handsontable.renderers.registerRenderer("db-dropdown", handlerDropdownRenderer);
Handsontable.renderers.registerRenderer("db-duplicate", handlerDuplicateCodeRenderer);
</script>
<template>
<div @contextmenu="handleContextMenu">
<hot-table ref="hotTableComponent" :settings="hotSettings"></hot-table>
<ContextMenu ref="contextMenuRef" :hotTableComponent="hotTableComponent" :menuItems="componentProps.contextMenuItems" />
</div>
<!-- <div id="hot-dialog-container" style="display:none">
<div class="ht-dialog-content">
<h3>执行操作</h3>
<el-table :data="gridData">
<el-table-column property="date" label="Date" width="150" />
<el-table-column property="name" label="Name" width="200" />
<el-table-column property="address" label="Address" />
</el-table>
<div style="margin-top:12px;display:flex;gap:8px;justify-content:flex-end">
<button class="el-button el-button--default el-button--small btn-cancel">取消</button>
<button class="el-button el-button--primary el-button--small btn-ok">确定</button>
</div>
</div>
</div>
</div> -->
<!-- <el-popover
ref="popoverRef"
:virtual-ref="popoverButtonRef"
v-model:visible="isPopoverOpen"
trigger="manual"
virtual-triggering
width="auto"
>
<el-table :data="gridData" size="small" @row-click="onClickOutside" border>
<el-table-column width="150" property="date" label="date" />
<el-table-column width="100" property="name" label="name" />
<el-table-column width="300" property="address" label="address" />
</el-table>
</el-popover> -->
</template>
<style lang="css">
/* 禁止单元格内容换行 */
.handsontable td {
white-space: nowrap !important;
overflow: hidden;
text-overflow: ellipsis;
}
/* 自定义滚动条样式 */
/* .ht_master .wtHolder::-webkit-scrollbar {
width: 10px;
background-color: #f1f1f1;
}
.ht_master .wtHolder::-webkit-scrollbar-thumb {
background-color: #888;
border-radius: 6px;
border: 3px solid #ffffff;
}
.ht_master .wtHolder::-webkit-scrollbar-thumb:hover {
background-color: #555;
} */
/* 滚动条width */
.ht_master .wtHolder{
/* overflow: hidden !important; */
scrollbar-width: thin;
scrollbar-color: #a6a8ac #ecf0f1;
}
/* dropdown */
.ht-cell-dropdown { display: flex; align-items: center; justify-content: center; width: 100%; position: relative; box-sizing: border-box; height: 100%; }
.ht-cell-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ht-cell-value:empty::after { content: "\200b"; }
.ht-cell-caret { position: absolute; right: 0; top: 50%; transform: translateY(-50%); width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 5px solid #979797; }
.ht-dropdown-menu { position: absolute; background: #fff; border: 1px solid var(--el-border-color-light); border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); min-width: 160px; max-height:300px; overflow: auto; z-index: 10000; }
.ht-dropdown-menu.is-bottom { margin-top: 4px; }
.ht-dropdown-menu.is-top { margin-bottom: 4px; }
.ht-dropdown-item { padding: 8px 12px; cursor: pointer; font-size: 13px; }
.ht-dropdown-item:hover { background-color: #f5f7fa; }
.ht-dropdown-item.is-selected { background-color: #eef3ff; }
.ht-dropdown-item.is-disabled { color: #c0c4cc; cursor: not-allowed; }
/* tree */
.handsontable .text-relative span.ht_nestingLevel_empty{
position: relative;
display: inline-block;
width: 5px;
height: 1px;
order: -2;
}
.handsontable .text-relative span:last-child {
padding-left: calc(var(--ht-icon-size) + 5px);
}
/* table */
/* 自定义下拉渲染样式 */
.hot-cell-dropdown { display: flex; align-items: center; gap: 6px; padding: 0 6px; }
.hot-dropdown-display { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hot-dropdown-text { font-size: 12px; color: #1f2328; }
.hot-dropdown-placeholder { font-size: 12px; color: #a0a0a0; }
.hot-dropdown-trigger {
margin-left: auto;
border: none;
background: transparent;
position: relative;
cursor: pointer;
width: 16px;
height: 16px;
font-size: 0;
}
.hot-dropdown-trigger::after {
width: 16px;
height: 16px;
-webkit-mask-size: contain;
-webkit-mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cpath d='m21 21-4.35-4.35'%3E%3C/path%3E%3C/svg%3E");
background-color: currentColor;
}
.hot-dropdown-trigger::after {
content: "";
display: block;
position: absolute;
top: 0;
left: 0;
opacity: 0.6;
}
.hot-dropdown { position: fixed; z-index: 10000; background: #fff; border: 1px solid #e5e7eb; box-shadow: 0 8px 24px rgba(0,0,0,0.12); border-radius: 6px; max-height: 260px; overflow: hidden; will-change: top, left; display: flex; flex-direction: column; }
.hot-dropdown--up { border-top-left-radius: 6px; border-top-right-radius: 6px; }
.hot-dropdown--down { border-bottom-left-radius: 6px; border-bottom-right-radius: 6px; }
.hot-dropdown-search { padding: 0px; border-bottom: 1px solid #e5e7eb; background: #fff; position: sticky; top: 0; z-index: 1; }
.hot-dropdown-search-input { width: 100%; padding: 6px 10px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 12px; outline: none; transition: border-color 0.2s; }
.hot-dropdown-search-input:focus { border-color: #3b82f6; }
.hot-dropdown-table-wrapper { overflow: auto; flex: 1; }
.hot-dropdown-table { width: 100%; border-collapse: collapse; font-size: 12px; text-align: center; }
.hot-dropdown-table thead th { position: sticky; top: 0; background: #f9fafb; font-weight: 600; color: #374151; padding: 8px; border-bottom: 1px solid #e5e7eb; text-align: center; }
.hot-dropdown-table tbody td { padding: 8px; border-bottom: 1px solid #f3f4f6; color: #374151; }
.hot-dropdown-row { cursor: pointer; }
.hot-dropdown-row:hover { background: #f3f4f6; }
/* 整行高亮样式 */
.row-highlight {
background-color: #e9ecfc !important; /* 浅蓝色背景 */
}
/* 确保 Handsontable 右键菜单在 ElDialog 之上 - 必须是全局样式 */
.handsontable.htDropdownMenu:not(.htGhostTable),
.handsontable.htContextMenu:not(.htGhostTable),
.handsontable.htFiltersConditionsMenu:not(.htGhostTable) {
z-index: 9999 !important;
}
.ht-id-cell {
position: relative !important;
z-index: 3 !important;
overflow: visible !important;
}
.ht-id-icon {
position: absolute;
top: 0px;
right: -14px;
width: 14px;
height: 14px;
display: none;
cursor: pointer;
z-index: 4;
}
.ht-id-cell.current .ht-id-icon,
.ht-id-cell.area .ht-id-icon {
display: inline-flex;
}
.handsontable {
--ht-tree-line-color: #7c7c7c;
--ht-tree-line-style: solid;
--ht-tree-line-width: 1px;
--ht-tree-indent: 14px;
}
/** 新树形连接线 */
.handsontable .ht_treeCell {
display: flex;
align-items: stretch;
height: 100%;
}
.handsontable .ht_treeIndentLayer {
flex: 0 0 auto;
height: 100%;
}
.handsontable .ht_treeContent {
display: flex;
align-items: center;
gap: 6px;
}
.handsontable .ht_treeToggleSpacer {
display: inline-block;
width: var(--ht-icon-size);
height: var(--ht-icon-size);
flex: 0 0 auto;
}
.handsontable .ht_treeContent {
min-width: 0;
}
/* 整行高亮样式 */
.handsontable td.ht_rowHighlight:not(.current):not(.area) {
background-color: #e9ecfc;
}
</style>