You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

445 lines
13 KiB
TypeScript

1 year ago
import {
SettingOutlined,
VerticalAlignMiddleOutlined,
VerticalAlignTopOutlined,
VerticalAlignBottomOutlined
} from '@ant-design/icons-vue'
import { Checkbox, Tree, Popover, Tooltip, Space } from 'ant-design-vue'
import 'ant-design-vue/es/checkbox/style/index.less'
import 'ant-design-vue/es/tree/style/index.less'
import 'ant-design-vue/es/popover/style/index.less'
import 'ant-design-vue/es/tooltip/style/index.less'
import 'ant-design-vue/es/space/style/index.less'
import type { TableColumnType } from 'ant-design-vue'
import type { DataNode } from 'ant-design-vue/es/tree'
import { genColumnKey } from '../../utils'
import type { ProColumns } from '../../typing'
import './index.less'
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface'
import type { FunctionalComponent, PropType } from 'vue'
import { useIntl } from '#/provider'
import { getPrefixCls } from '#/layout/RouteContext'
import type { VueKey, VueNode } from '#/types'
import type { ColumnsState } from '#/table/container'
import { useContainer } from '#/table/container'
import { computed, ref, watchEffect } from 'vue'
import omit from 'ant-design-vue/es/_util/omit'
type ColumnSettingProps<T = any> = {
columns: TableColumnType<T>[]
draggable?: boolean
checkable?: boolean
extra?: VueNode
checkedReset?: boolean
listsHeight?: number
}
const ToolTipIcon: FunctionalComponent<{
title: string
columnKey: string | number
show: boolean
fixed: 'left' | 'right' | undefined
}> = ({ title, show, columnKey, fixed }, { slots }) => {
const { columnsMap, setColumnsMap } = useContainer()!
if (!show) {
return null
}
return (
<Tooltip title={title}>
<span
onClick={e => {
e.stopPropagation()
e.preventDefault()
const config = columnsMap.value[columnKey] || {}
const disableIcon = typeof config.disable === 'boolean' && config.disable
if (disableIcon) return
const columnKeyMap = {
...columnsMap.value,
[columnKey]: { ...config, fixed } as ColumnsState
}
setColumnsMap(columnKeyMap)
}}
>
{slots.default?.()}
</span>
</Tooltip>
)
}
const CheckboxListItem: FunctionalComponent<{
columnKey: string | number
className?: string
title?: VueNode
fixed?: boolean | 'left' | 'right'
hasParent?: boolean // 等于 react 版本的 isLeaf 属性,由于 ant-design-vue 会默认给 isLeaf 属性赋值,这里需要一个额外的属性来处理
}> = ({ columnKey, hasParent, title, className, fixed }) => {
const intl = useIntl()
const dom = (
<span class={`${className}-list-item-option`}>
<ToolTipIcon
columnKey={columnKey}
fixed="left"
title={intl.getMessage('tableToolBar.leftPin', '固定在列首')}
show={fixed !== 'left'}
>
<VerticalAlignTopOutlined />
</ToolTipIcon>
<ToolTipIcon
columnKey={columnKey}
fixed={undefined}
title={intl.getMessage('tableToolBar.noPin', '不固定')}
show={!!fixed}
>
<VerticalAlignMiddleOutlined />
</ToolTipIcon>
<ToolTipIcon
columnKey={columnKey}
fixed="right"
title={intl.getMessage('tableToolBar.rightPin', '固定在列尾')}
show={fixed !== 'right'}
>
<VerticalAlignBottomOutlined />
</ToolTipIcon>
</span>
)
return (
<span class={`${className}-list-item`} key={columnKey}>
<div class={`${className}-list-item-title`}>{title}</div>
{!hasParent ? dom : null}
</span>
)
}
const CheckboxList = defineComponent({
name: 'CheckboxList',
props: {
list: {
type: Array as PropType<(ProColumns<any> & { index?: number })[]>,
default: undefined
},
className: { type: String, default: undefined },
title: { type: String, default: undefined },
draggable: { type: Boolean, default: undefined },
checkable: { type: Boolean, default: undefined },
showTitle: { type: Boolean, default: true },
listHeight: { type: Number, default: 280 }
},
setup(props) {
const { columnsMap, setColumnsMap, sortKeyColumns, setSortKeyColumns } = useContainer()!
const show = computed(() => props.list && props.list.length > 0)
const treeDataConfig = computed(() => {
if (!show.value) return {}
const checkedKeys: string[] = []
const loopData = (data: any[], parentConfig?: ColumnsState): DataNode[] =>
data.map(({ key, children, ...rest }) => {
const columnKey = genColumnKey(key, rest.index)
const config = columnsMap.value[columnKey || 'null'] || { show: true }
if (config.show !== false && parentConfig?.show !== false && !children) {
checkedKeys.push(columnKey)
}
const item: DataNode = {
key: columnKey,
...omit(rest, ['className']),
selectable: false,
disabled: config.disable === true,
disableCheckbox:
typeof config.disable === 'boolean' ? config.disable : config.disable?.checkbox,
hasParent: parentConfig ? true : undefined
}
if (children) {
item.children = loopData(children, config)
}
return item
})
return { list: loopData(props.list as any), keys: checkedKeys }
})
/** 移动到指定的位置 */
const move = (id: VueKey, targetId: VueKey, dropPosition: number) => {
const newMap = { ...columnsMap.value }
// @ts-ignore
const newColumns = [...sortKeyColumns.value]
const findIndex = newColumns.findIndex(columnKey => columnKey === id)
const targetIndex = newColumns.findIndex(columnKey => columnKey === targetId)
const isDownWord = dropPosition > findIndex
if (findIndex < 0) return
const targetItem = newColumns[findIndex]
newColumns.splice(findIndex, 1)
if (dropPosition === 0) {
newColumns.unshift(targetItem)
} else {
newColumns.splice(isDownWord ? targetIndex : targetIndex + 1, 0, targetItem)
}
// 重新生成排序数组
newColumns.forEach((key, order) => {
newMap[key] = { ...(newMap[key] || {}), order }
})
// 更新数组
setColumnsMap(newMap)
setSortKeyColumns(newColumns)
}
/** 选中反选功能 */
const onCheckTree = (e: any) => {
const columnKey = e.node.key
const newSetting = { ...columnsMap.value[columnKey] }
newSetting.show = e.checked
setColumnsMap({
...columnsMap.value,
[columnKey]: newSetting
})
}
return () => {
if (!show.value) {
return null
}
const listDom = (
<Tree
itemHeight={24}
draggable={
props.draggable &&
!!treeDataConfig.value.list?.length &&
treeDataConfig.value.list?.length > 1
}
checkable={props.checkable}
onDrop={info => {
const dropKey = info.node.key
const dragKey = info.dragNode.key
const { dropPosition, dropToGap } = info
const position = dropPosition === -1 || !dropToGap ? dropPosition + 1 : dropPosition
move(dragKey, dropKey, position)
}}
blockNode
onCheck={(_, e) => onCheckTree(e)}
checkedKeys={treeDataConfig.value.keys}
showLine={false}
height={props.listHeight}
treeData={treeDataConfig.value.list}
>
{{
title: (_node: DataNode) => {
const node = { ..._node, children: undefined }
return <CheckboxListItem className={props.className} {...node} columnKey={node.key} />
}
}}
</Tree>
)
return (
<>
{props.showTitle && <span class={`${props.className}-list-title`}>{props.title}</span>}
{listDom}
</>
)
}
}
})
const GroupCheckboxList: FunctionalComponent<{
localColumns: (ProColumns<any> & { index?: number })[]
className?: string
draggable: boolean
checkable: boolean
listsHeight?: number
}> = ({ localColumns, className, draggable, checkable, listsHeight }) => {
const rightList: (ProColumns<any> & { index?: number })[] = []
const leftList: (ProColumns<any> & { index?: number })[] = []
const list: (ProColumns<any> & { index?: number })[] = []
const intl = useIntl()
localColumns.forEach(item => {
/** 不在 setting 中展示的 */
if (item.hideInSetting) {
return
}
const { fixed } = item
if (fixed === 'left') {
leftList.push(item)
return
}
if (fixed === 'right') {
rightList.push(item)
return
}
list.push(item)
})
const showRight = rightList && rightList.length > 0
const showLeft = leftList && leftList.length > 0
return (
<div
class={[
`${className}-list`,
{
[`${className}-list-group`]: showRight || showLeft
}
]}
>
<CheckboxList
title={intl.getMessage('tableToolBar.leftFixedTitle', '固定在左侧')}
list={leftList}
draggable={draggable}
checkable={checkable}
className={className}
listHeight={listsHeight}
/>
{/* 如果没有任何固定不需要显示title */}
<CheckboxList
list={list}
draggable={draggable}
checkable={checkable}
title={intl.getMessage('tableToolBar.noFixedTitle', '不固定')}
showTitle={showLeft || showRight}
className={className}
listHeight={listsHeight}
/>
<CheckboxList
title={intl.getMessage('tableToolBar.rightFixedTitle', '固定在右侧')}
list={rightList}
draggable={draggable}
checkable={checkable}
className={className}
listHeight={listsHeight}
/>
</div>
)
}
const ColumnSetting: FunctionalComponent<ColumnSettingProps> = (
props: ColumnSettingProps,
{ slots }
) => {
const columnRef = ref({})
const counter = useContainer()!
const localColumns: TableColumnType &
{
index?: number
fixed?: any
key?: any
}[] = props.columns
const { checkedReset = true } = props
const { columnsMap, setColumnsMap, clearPersistenceStorage } = counter
watchEffect(() => {
if (counter.propsRef.value?.columnsState?.value) {
columnRef.value = JSON.parse(
JSON.stringify(counter.propsRef.value?.columnsState?.value || {})
)
}
})
/**
*
*
* @param show
*/
const setAllSelectAction = (show = true) => {
const columnKeyMap = {}
const loopColumns = (columns: any) => {
columns.forEach(({ key, fixed, index, children }: any) => {
const columnKey = genColumnKey(key, index)
if (columnKey) {
// @ts-ignore
columnKeyMap[columnKey] = {
show,
fixed
}
}
if (children) {
loopColumns(children)
}
})
}
loopColumns(localColumns)
setColumnsMap(columnKeyMap)
}
/** 全选和反选 */
const checkedAll = (e: CheckboxChangeEvent) => {
if (e.target.checked) {
setAllSelectAction()
} else {
setAllSelectAction(false)
}
}
/** 重置项目 */
const clearClick = () => {
clearPersistenceStorage?.()
setColumnsMap(columnRef.value)
}
// 未选中的 key 列表
const unCheckedKeys = computed(() =>
Object.values(columnsMap.value).filter(
// @ts-ignore
value => !value || value.show === false
)
)
// 是否已经选中
const indeterminate = computed(
() => unCheckedKeys.value.length > 0 && unCheckedKeys.value.length !== localColumns.length
)
const intl = useIntl()
const className = getPrefixCls('pro-table-column-setting')
return (
<Popover
arrowPointAtCenter
title={
<div class={`${className}-title`}>
<Checkbox
indeterminate={indeterminate.value}
checked={
unCheckedKeys.value.length === 0 && unCheckedKeys.value.length !== localColumns.length
}
onChange={e => checkedAll(e)}
>
{intl.getMessage('tableToolBar.columnDisplay', '列展示')}
</Checkbox>
{checkedReset ? (
<a onClick={clearClick} class={`${className}-action-rest-button`}>
{intl.getMessage('tableToolBar.reset', '重置')}
</a>
) : null}
{props?.extra ? (
<Space size={12} align="center">
{props.extra}
</Space>
) : null}
</div>
}
overlayClassName={`${className}-overlay`}
trigger="click"
placement="bottomRight"
content={
<GroupCheckboxList
checkable={props.checkable ?? true}
draggable={props.draggable ?? true}
className={className}
localColumns={localColumns}
listsHeight={props.listsHeight}
/>
}
>
{slots.default?.() || (
<Tooltip title={intl.getMessage('tableToolBar.columnSetting', '列设置')}>
<SettingOutlined />
</Tooltip>
)}
</Popover>
)
}
export default ColumnSetting