207 lines
5.6 KiB
Vue
207 lines
5.6 KiB
Vue
<template>
|
|
<Teleport to="body">
|
|
<ul
|
|
v-if="menu.visible"
|
|
ref="menuRef"
|
|
class="table-copy-menu"
|
|
:style="{ left: menu.x + 'px', top: menu.y + 'px' }"
|
|
@click.stop
|
|
@contextmenu.prevent
|
|
>
|
|
<li class="tcm-title">复制{{ menu.rows.length }}{{ entityLabel }}记录的列值</li>
|
|
<li
|
|
v-for="col in shownColumns"
|
|
:key="col.key"
|
|
class="tcm-item"
|
|
@click="handleCopy(col)"
|
|
>
|
|
{{ col.label }}
|
|
</li>
|
|
<li v-if="!shownColumns.length" class="tcm-empty">暂无可复制的列</li>
|
|
</ul>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup name="TableCopyMenu">
|
|
/**
|
|
* 表格右键复制列值菜单
|
|
*
|
|
* 用法:
|
|
* <el-table @row-contextmenu="(row, col, ev) => copyMenuRef.open(row, ev)">
|
|
* <table-copy-menu ref="copyMenuRef" :columns="copyColumns" :selected-rows="selectedRows" />
|
|
*
|
|
* columns: [{ key: '字段名', label: '列名', get?: (row) => string }]
|
|
* - get 缺省时取 row[key]
|
|
* visibleKeys: 传入时只显示其中的列(配合「列设置」当前可见列)
|
|
* selectedRows: 表格勾选行;非空时复制勾选行,否则只复制当前右键行
|
|
*/
|
|
const props = defineProps({
|
|
columns: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
selectedRows: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
visibleKeys: {
|
|
type: Array,
|
|
default: null
|
|
},
|
|
// 量词:客户/供应商用「条」,收款单用「笔」
|
|
entityLabel: {
|
|
type: String,
|
|
default: '条'
|
|
}
|
|
})
|
|
|
|
const { proxy } = getCurrentInstance()
|
|
|
|
const MENU_WIDTH = 190
|
|
const ITEM_HEIGHT = 34
|
|
const TITLE_HEIGHT = 46
|
|
const MENU_MAX_HEIGHT = 340
|
|
|
|
const menuRef = ref(null)
|
|
const menu = reactive({ visible: false, x: 0, y: 0, rows: [] })
|
|
|
|
const shownColumns = computed(() => {
|
|
if (!props.visibleKeys) return props.columns
|
|
return props.columns.filter(c => props.visibleKeys.includes(c.key))
|
|
})
|
|
|
|
/** 打开右键菜单:有勾选行则复制勾选行,否则只复制当前右键行 */
|
|
function open(row, event) {
|
|
// 阻止浏览器原生右键菜单
|
|
event.preventDefault()
|
|
menu.rows = props.selectedRows && props.selectedRows.length ? props.selectedRows : [row]
|
|
const height = Math.min(TITLE_HEIGHT + shownColumns.value.length * ITEM_HEIGHT, MENU_MAX_HEIGHT)
|
|
menu.x = Math.min(event.clientX, window.innerWidth - MENU_WIDTH - 8)
|
|
menu.y = Math.min(event.clientY, window.innerHeight - height - 8)
|
|
menu.visible = true
|
|
addListeners()
|
|
}
|
|
|
|
function close() {
|
|
if (!menu.visible) return
|
|
menu.visible = false
|
|
removeListeners()
|
|
}
|
|
|
|
function onDocPointerDown(e) {
|
|
if (e.target.closest && e.target.closest('.table-copy-menu')) return
|
|
close()
|
|
}
|
|
function onDocKeydown(e) {
|
|
if (e.key === 'Escape') close()
|
|
}
|
|
function onWindowScroll() {
|
|
close()
|
|
}
|
|
|
|
function addListeners() {
|
|
document.addEventListener('mousedown', onDocPointerDown, true)
|
|
document.addEventListener('keydown', onDocKeydown)
|
|
window.addEventListener('scroll', onWindowScroll, true)
|
|
}
|
|
function removeListeners() {
|
|
document.removeEventListener('mousedown', onDocPointerDown, true)
|
|
document.removeEventListener('keydown', onDocKeydown)
|
|
window.removeEventListener('scroll', onWindowScroll, true)
|
|
}
|
|
|
|
/** 兼容非安全上下文(http)的剪贴板写入 */
|
|
async function copyToClipboard(text) {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
try {
|
|
await navigator.clipboard.writeText(text)
|
|
return true
|
|
} catch (err) {
|
|
// 走降级方案
|
|
}
|
|
}
|
|
const textarea = document.createElement('textarea')
|
|
textarea.value = text
|
|
textarea.style.position = 'fixed'
|
|
textarea.style.top = '-9999px'
|
|
textarea.style.opacity = '0'
|
|
document.body.appendChild(textarea)
|
|
textarea.focus()
|
|
textarea.select()
|
|
let ok = false
|
|
try {
|
|
ok = document.execCommand('copy')
|
|
} catch (err) {
|
|
ok = false
|
|
}
|
|
document.body.removeChild(textarea)
|
|
return ok
|
|
}
|
|
|
|
async function handleCopy(col) {
|
|
const getter = typeof col.get === 'function' ? col.get : row => (row[col.key] === null || row[col.key] === undefined ? '' : row[col.key])
|
|
const values = menu.rows
|
|
.map(getter)
|
|
.map(v => String(v == null ? '' : v).trim())
|
|
.filter(v => v !== '' && v !== '-')
|
|
close()
|
|
if (!values.length) {
|
|
proxy.$modal.msgWarning(`所选记录的「${col.label}」均为空,无可复制内容`)
|
|
return
|
|
}
|
|
const ok = await copyToClipboard(values.join('\n'))
|
|
if (ok) {
|
|
proxy.$modal.msgSuccess(`已复制 ${values.length} ${props.entityLabel}记录的「${col.label}」`)
|
|
} else {
|
|
proxy.$modal.msgError('复制失败,请检查浏览器剪贴板权限')
|
|
}
|
|
}
|
|
|
|
// keep-alive 页面切走时关闭菜单(菜单 teleport 在 body,否则会残留在页面上)
|
|
onDeactivated(close)
|
|
onBeforeUnmount(removeListeners)
|
|
|
|
defineExpose({ open, close })
|
|
</script>
|
|
|
|
<style scoped>
|
|
.table-copy-menu {
|
|
position: fixed;
|
|
z-index: 3000;
|
|
min-width: 170px;
|
|
max-height: 340px;
|
|
margin: 0;
|
|
padding: 4px 0;
|
|
overflow-y: auto;
|
|
list-style: none;
|
|
background: var(--el-bg-color);
|
|
border: 1px solid var(--el-border-color-light);
|
|
border-radius: 4px;
|
|
box-shadow: var(--el-box-shadow-light);
|
|
user-select: none;
|
|
}
|
|
.table-copy-menu .tcm-title {
|
|
padding: 4px 14px 6px;
|
|
font-size: 12px;
|
|
color: var(--el-text-color-secondary);
|
|
pointer-events: none;
|
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
margin-bottom: 4px;
|
|
}
|
|
.table-copy-menu .tcm-item {
|
|
padding: 7px 16px;
|
|
font-size: 13px;
|
|
color: var(--el-text-color-regular);
|
|
cursor: pointer;
|
|
}
|
|
.table-copy-menu .tcm-item:hover {
|
|
background: var(--el-color-primary-light-9);
|
|
color: var(--el-color-primary);
|
|
}
|
|
.table-copy-menu .tcm-empty {
|
|
padding: 8px 16px;
|
|
font-size: 12px;
|
|
color: var(--el-text-color-secondary);
|
|
}
|
|
</style>
|