【升级】前端表格组件优化、新图标选择器优化并加入readme说明

pull/249/head
俞宝山 2025-03-09 00:48:21 +08:00
parent 9650871e90
commit f88765a34c
4 changed files with 284 additions and 118 deletions

View File

@ -24,6 +24,8 @@
</template>
<script setup>
import Draggable from 'vuedraggable-es'
import { ref, watch } from 'vue'
const emit = defineEmits(['columnChange'])
const props = defineProps({
columns: {
@ -35,25 +37,47 @@
const indeterminate = ref(false)
const checkAll = ref(true)
const columnsSetting = ref([])
const originColumns = ref()
const originColumns = ref([])
onMounted(() => {
columnsSetting.value = props.columns.map((value) => {
if (value.checked === undefined) {
return {
...value,
checked: true
}
} else return value
})
const emitColumnChange = () => {
//
const updatedColumns = columnsSetting.value.map((col) => ({
...col,
hidden: !col.checked,
checked: col.checked,
show: col.checked // show
}))
//
emit('columnChange', updatedColumns)
}
// originColumns
//
const initializeColumns = (columns) => {
columnsSetting.value = columns.map((value) => ({
...value,
checked: value.checked !== undefined ? value.checked : !value.hidden
}))
//
originColumns.value = columnsSetting.value.map((value) => ({ ...value }))
//
//
const notCheckedList = columnsSetting.value.filter((value) => !value.checked)
if (notCheckedList.length) checkAll.value = false
})
checkAll.value = !notCheckedList.length
indeterminate.value = notCheckedList.length > 0 && notCheckedList.length < columnsSetting.value.length
//
emitColumnChange()
}
// props.columns
watch(
() => props.columns,
(newColumns) => {
initializeColumns(newColumns)
},
{ immediate: true }
)
const reset = () => {
columnsSetting.value = originColumns.value.map((value) => ({ ...value }))
@ -83,11 +107,6 @@
}))
emitColumnChange()
}
const emitColumnChange = () => {
// eslint-disable-next-line vue/require-explicit-emits
emit('columnChange', columnsSetting.value)
}
</script>
<style lang="less" scoped>
.s-tool-column-item {

View File

@ -52,7 +52,6 @@
</span>
</div>
</div>
<!-- 统计列数据 -->
<a-alert showIcon class="s-table-alert mb-4" v-if="props.alert">
<template #message>
@ -65,7 +64,7 @@
}}
</a>
</span>
<span className="mr-3" v-for="item in data.needTotalList">
<span className="mr-3" v-for="item in data.needTotalList" :key="item">
{{ item.title }} 总计{{ ' ' }}
<a className="font-6">{{ !item.customRender ? item.total : item.customRender(item.total) }}</a>
</span>
@ -75,13 +74,11 @@
"
className="ml-6"
@click="
rowClear(
typeof props.alert === 'boolean' && props.alert
? clearSelected()
: props.alert.clear && typeof props.alert.clear === 'function'
? props.alert.clear()
: null
)
typeof props.alert === 'boolean' && props.alert
? clearSelected()
: props.alert.clear && typeof props.alert.clear === 'function'
? props.alert.clear()
: null
"
>
{{ ' ' }}
@ -121,10 +118,11 @@
import columnSetting from './columnSetting.vue'
import { useSlots } from 'vue'
import { useRoute } from 'vue-router'
import { get } from 'lodash-es'
import { cloneDeep, get } from 'lodash-es'
const slots = useSlots()
const route = useRoute()
const emit = defineEmits(['onExpand'])
const emit = defineEmits(['onExpand', 'onSelectionChange'])
const renderSlots = Object.keys(slots)
const props = defineProps(
@ -207,7 +205,17 @@
localSettings: {
rowClassName: props.rowClassName,
rowClassNameSwitch: Boolean(props.rowClassName)
}
},
renderTableProps: {
...props,
columns: [],
dataSource: [],
pagination: {},
loading: false,
size: props.compSize
},
selectedRows: [],
selectedRowKeys: []
})
watch(
@ -234,15 +242,127 @@
})
}
)
// showPagination
watch(
() => props.rowSelection,
(newVal) => {
if (!newVal) {
// rowSelectionnull
data.selectedRows = []
data.selectedRowKeys = []
}
//
getTableProps()
},
{ deep: true }
)
watch(
() => props.showPagination,
(newVal) => {
//
data.localPagination = newVal === false ? false : Object.assign({}, data.localPagination)
//
loadData()
getTableProps()
}
)
watch(
() => props.columns,
(newVal) => {
data.columnsSetting = newVal
}
data.columnsSetting = newVal.map((col) => ({
...col,
checked: col.checked === undefined ? true : col.checked
}))
},
{ deep: true, immediate: true }
)
// props
const renderTableProps = ref([])
const renderTableProps = computed(() => {
const tableProps = {
...props,
columns: data.localColumns || props.columns,
dataSource: data.localDataSource,
pagination: data.localPagination,
loading: data.localLoading,
size: data.customSize
}
if (props.rowSelection) {
tableProps.rowSelection = {
...props.rowSelection,
selectedRowKeys: data.selectedRowKeys,
selectedRows: data.selectedRows,
onChange: (selectedRowKeys, selectedRows) => {
updateSelect(selectedRowKeys, selectedRows)
props.rowSelection.onChange?.(selectedRowKeys, selectedRows)
}
}
}
if (props.lineSelection && props.rowSelection) {
tableProps.customRow = (record, index) => {
const customRowProps = typeof props.customRow === 'function' ? props.customRow(record, index) : {}
return {
...customRowProps,
onClick: (event) => {
// onClick
if (customRowProps && typeof customRowProps.onClick === 'function') {
customRowProps.onClick(event)
}
//
const rowDisabled =
typeof props.rowSelection.getCheckboxProps === 'function' &&
props.rowSelection.getCheckboxProps(record).disabled
if (rowDisabled) return
//
if (
event.target?.tagName.toLowerCase() === 'button' ||
event.target?.tagName.toLowerCase() === 'a' ||
event.target?.closest('button') ||
event.target?.closest('a') ||
event.target?.closest('.ant-checkbox-wrapper') ||
event.target?.closest('.ant-radio-wrapper')
) {
return
}
// key
const key = (typeof props.rowKey === 'function' && props.rowKey(record)) || record[props.rowKey] || index
//
let selectedRowKeys = [...data.selectedRowKeys]
let selectedRows = [...data.selectedRows]
const rowType = props.rowSelection?.type || 'checkbox'
if (rowType === 'radio') {
//
selectedRowKeys = [key]
selectedRows = [record]
} else {
//
const existingIndex = selectedRowKeys.indexOf(key)
if (existingIndex === -1) {
selectedRowKeys.push(key)
selectedRows.push(record)
} else {
selectedRowKeys.splice(existingIndex, 1)
selectedRows.splice(existingIndex, 1)
}
}
//
updateSelect(selectedRowKeys, selectedRows)
props.rowSelection.onChange?.(selectedRowKeys, selectedRows)
}
}
}
}
return tableProps
})
//
const tool = [
{
@ -291,13 +411,9 @@
//
const columnChange = (v) => {
data.columnsSetting = v
getTableProps()
data.localColumns = v.filter((value) => value.checked === undefined || value.checked)
getTableProps() // getTableProps
}
//
const rowClear = (callback) => {
clearSelected()
}
//
const init = () => {
const { current } = route.params
const localPageNum = (current && parseInt(current)) || props.pageNum
@ -305,7 +421,7 @@
(['auto', true].includes(props.showPagination) &&
Object.assign({}, data.localPagination, {
current: localPageNum,
pageSize: props.size, //props.compSize, size//
pageSize: props.size,
showSizeChanger: props.showSizeChanger,
defaultPageSize: props.defaultPageSize,
pageSizeOptions: props.pageSizeOptions,
@ -316,6 +432,16 @@
false
data.needTotalList = initTotalList(props.columns)
data.columnsSetting = props.columns
//
if (props.rowSelection && props.rowSelection.selectedRowKeys) {
data.selectedRowKeys = [...props.rowSelection.selectedRowKeys]
// selectedRows
if (props.rowSelection.selectedRows) {
data.selectedRows = cloneDeep(props.rowSelection.selectedRows)
}
}
loadData()
}
@ -449,87 +575,48 @@
updateSelect(selectedRowKeys, selectedRows)
typeof props[k].onChange !== 'undefined' && props[k].onChange(selectedRowKeys, selectedRows)
}
}
}
: null
return
}
// ,
if (k === 'customRow') {
if (props.lineSelection && props.rowSelection) {
// customRow
renderProps[k] = (record, index) => {
return {
...(typeof props.customRow !== 'undefined' && props.customRow(record, index)),
onClick: (event) => {
// onClick
typeof data[k] !== 'undefined' &&
typeof data[k](record, index).onClick !== 'undefined' &&
data[k](record, index).onClick(event)
// disabled
const rowDisabled =
typeof props.rowSelection.getCheckboxProps !== 'undefined' &&
props.rowSelection.getCheckboxProps(record).disabled
if (rowDisabled) return
//
const classList = event.target?.classList
if (!classList.contains('ant-table-cell')) return
const key = (typeof props.rowKey === 'function' && props.rowKey(record)) || props.rowKey || index
let selectedRows = props.rowSelection.selectedRows
let selectedRowKeys = props.rowSelection.selectedRowKeys
const rowType = props.rowSelection?.type || 'checkbox'
if (rowType === 'radio' || props.rowSelection.selectedRowKeys === undefined) {
selectedRowKeys = [key]
selectedRows = [record]
} else if (!props.rowSelection.selectedRowKeys?.includes(key)) {
selectedRowKeys.push(key)
selectedRows.push(record)
} else {
const index = props.rowSelection.selectedRowKeys?.findIndex((itemKey) => itemKey === key)
selectedRows.splice(index, 1)
selectedRowKeys.splice(index, 1)
}
updateSelect(selectedRowKeys, selectedRows)
}
}
}
return renderProps[k]
}
}
renderProps[k] = props[k]
})
renderProps = {
...renderProps,
size: data.customSize, // sizea-tablecompSize
size: data.customSize,
columns: data.columnsSetting.filter((value) => value.checked === undefined || value.checked),
...data.localSettings
}
// undefined null tableprops
renderTableProps.value = Object.entries(renderProps).reduce((x, [y, z]) => (z == null ? x : ((x[y] = z), x)), {})
data.renderTableProps = Object.entries(renderProps).reduce((x, [y, z]) => (z == null ? x : ((x[y] = z), x)), {})
}
// total
const updateSelect = (selectedRowKeys, selectedRows) => {
if (props.rowSelection) {
//
data.selectedRows = cloneDeep(selectedRows)
data.selectedRowKeys = cloneDeep(selectedRowKeys)
// rowSelection
// eslint-disable-next-line vue/no-mutating-props
props.rowSelection.selectedRows = selectedRows
props.rowSelection.selectedRows = cloneDeep(selectedRows)
// eslint-disable-next-line vue/no-mutating-props
props.rowSelection.selectedRowKeys = selectedRowKeys
props.rowSelection.onChange(selectedRowKeys, selectedRows)
props.rowSelection.selectedRowKeys = cloneDeep(selectedRowKeys)
//
emit('onSelectionChange', selectedRowKeys, selectedRows)
//
getTableProps()
}
const list = data.needTotalList
data.needTotalList = list.map((item) => {
return {
...item,
total: selectedRows.reduce((sum, val) => {
return addNumbers(sum, get(val, item.dataIndex))
//
data.needTotalList = initTotalList(props.columns)
data.needTotalList.forEach((item) => {
item.total = selectedRows.reduce((sum, val) => {
const value = get(val, item.dataIndex)
return addNumbers(sum, value)
}, 0)
}
})
})
}
}
// needTotal
//
const addNumbers = (num1, num2) => {
//
let num1Value = Number(num1)
@ -554,10 +641,19 @@
// table
const clearSelected = () => {
if (props.rowSelection) {
//
// eslint-disable-next-line vue/no-mutating-props
props.rowSelection.selectedRowKeys = []
// eslint-disable-next-line vue/no-mutating-props
props.rowSelection.selectedRows = []
// onChange
props.rowSelection.onChange([], [])
updateSelect([], [])
//
getTableProps()
//
updateSelect([], [])
}
data.needTotalList = initTotalList(props.columns)
}
//
const clearRefreshSelected = (bool = false) => {

View File

@ -0,0 +1,46 @@
## 小诺图标选择器
### 说明
改组件为小诺人员选择器可返回id用逗号隔离的字符串或id数组类型的数据格式
@author yubaoshan
@data 2024年4月13日23:59:23
### props定义
| 序号 | 编码 | 类型 | 说明 | 默认 |
|-----|-------------------|---------------|---------------------|-------|
| 1 | value | String | 值跟v-model绑定 | '' |
| 2 | formRef | - | 表单dom为了取消绑定的验证 | - |
### 示例
```vue
<template>
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
<a-form-item label="图标选择:" name="icon">
<xn-icon-selector
v-model:value="formData.icon"
v-model:formRef="formRef"
placeholder="请选择图标"
allow-clear
/>
</a-form-item>
</a-form>
</template>
<script setup>
import { required } from '@/utils/formRules'
// 定义表单的dom
const formRef = ref()
// 表单数据
const formData = ref({
icon: ''
})
// 默认要校验的
const formRules = {
icon: [required('请选择图标')]
}
</script>
```

View File

@ -5,20 +5,21 @@
trigger="click"
placement="bottomLeft"
:overlayStyle="{ width: '500px' }"
@visibleChange="handleVisibleChange"
@openChange="handleOpenChange"
>
<template #content>
<div class="icon-selector-content">
<a-tabs v-model:activeKey="activeKey" tab-position="left" size="small" @change="handleTabChange">
<a-tab-pane v-for="group in iconData" :key="group.key" :tab="group.name">
<div v-if="group.iconItem.length > 1" class="icon-category">
<a-radio-group v-model:value="currentCategory" @change="handleCategoryChange" size="small">
<a-radio-button v-for="item in group.iconItem" :key="item.key" :value="item.key">
{{ item.name }}
</a-radio-button>
</a-radio-group>
<a-form-item-rest>
<a-radio-group v-model:value="currentCategory" @change="handleCategoryChange" size="small">
<a-radio-button v-for="item in group.iconItem" :key="item.key" :value="item.key">
{{ item.name }}
</a-radio-button>
</a-radio-group>
</a-form-item-rest>
</div>
<div class="icon-grid">
<template v-for="iconGroup in group.iconItem" :key="iconGroup.key">
<template v-if="iconGroup.key === currentCategory">
@ -59,7 +60,7 @@
</template>
<script setup>
import { ref, watch } from 'vue'
import { ref } from 'vue'
import config from '@/config/iconSelect'
import { SearchOutlined, CloseCircleOutlined } from '@ant-design/icons-vue'
@ -85,10 +86,10 @@
default: true
}
})
const formRef = defineModel('formRef')
const emit = defineEmits(['update:value', 'change'])
const selectedIcon = ref(props.value)
const selectedIcon = ref()
const iconData = ref(config.icons)
const visible = ref(false)
const activeKey = ref(iconData.value[0]?.key || '')
@ -101,9 +102,9 @@
}
)
const handleVisibleChange = (isVisible) => {
const handleOpenChange = (isOpen) => {
if (!props.disabled) {
visible.value = isVisible
visible.value = isOpen
}
}
@ -112,16 +113,15 @@
}
const handleIconSelect = (icon) => {
selectedIcon.value = icon
emit('update:value', icon)
emit('change', icon)
formRef.value?.validateFields('icon')
visible.value = false
}
const handleClear = () => {
selectedIcon.value = ''
emit('update:value', '')
emit('change', '')
selectedIcon.value = ''
formRef.value?.validateFields('icon')
}
const handleTabChange = (key) => {
@ -144,6 +144,7 @@
color: rgba(0, 0, 0, 0.25);
transition: color 0.3s;
font-size: 12px;
&:hover {
color: rgba(0, 0, 0, 0.45);
}
@ -210,6 +211,7 @@
background-color: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
@ -240,12 +242,15 @@
font-size: 14px;
}
}
:deep(.ant-tabs-tab) {
padding: 8px !important;
}
:deep(.ant-tabs-nav) {
width: 60px !important;
}
:deep(.ant-tabs-tabpane) {
padding-left: 0 !important;
}