【升级】前端表格组件优化、新图标选择器优化并加入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> </template>
<script setup> <script setup>
import Draggable from 'vuedraggable-es' import Draggable from 'vuedraggable-es'
import { ref, watch } from 'vue'
const emit = defineEmits(['columnChange']) const emit = defineEmits(['columnChange'])
const props = defineProps({ const props = defineProps({
columns: { columns: {
@ -35,25 +37,47 @@
const indeterminate = ref(false) const indeterminate = ref(false)
const checkAll = ref(true) const checkAll = ref(true)
const columnsSetting = ref([]) const columnsSetting = ref([])
const originColumns = ref() const originColumns = ref([])
onMounted(() => { const emitColumnChange = () => {
columnsSetting.value = props.columns.map((value) => { //
if (value.checked === undefined) { const updatedColumns = columnsSetting.value.map((col) => ({
return { ...col,
...value, hidden: !col.checked,
checked: true checked: col.checked,
show: col.checked // show
}))
//
emit('columnChange', updatedColumns)
} }
} else return value
})
// 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 })) originColumns.value = columnsSetting.value.map((value) => ({ ...value }))
// //
const notCheckedList = columnsSetting.value.filter((value) => !value.checked) 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 = () => { const reset = () => {
columnsSetting.value = originColumns.value.map((value) => ({ ...value })) columnsSetting.value = originColumns.value.map((value) => ({ ...value }))
@ -83,11 +107,6 @@
})) }))
emitColumnChange() emitColumnChange()
} }
const emitColumnChange = () => {
// eslint-disable-next-line vue/require-explicit-emits
emit('columnChange', columnsSetting.value)
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.s-tool-column-item { .s-tool-column-item {

View File

@ -52,7 +52,6 @@
</span> </span>
</div> </div>
</div> </div>
<!-- 统计列数据 --> <!-- 统计列数据 -->
<a-alert showIcon class="s-table-alert mb-4" v-if="props.alert"> <a-alert showIcon class="s-table-alert mb-4" v-if="props.alert">
<template #message> <template #message>
@ -65,7 +64,7 @@
}} }}
</a> </a>
</span> </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 }} 总计{{ ' ' }} {{ item.title }} 总计{{ ' ' }}
<a className="font-6">{{ !item.customRender ? item.total : item.customRender(item.total) }}</a> <a className="font-6">{{ !item.customRender ? item.total : item.customRender(item.total) }}</a>
</span> </span>
@ -75,13 +74,11 @@
" "
className="ml-6" className="ml-6"
@click=" @click="
rowClear(
typeof props.alert === 'boolean' && props.alert typeof props.alert === 'boolean' && props.alert
? clearSelected() ? clearSelected()
: props.alert.clear && typeof props.alert.clear === 'function' : props.alert.clear && typeof props.alert.clear === 'function'
? props.alert.clear() ? props.alert.clear()
: null : null
)
" "
> >
{{ ' ' }} {{ ' ' }}
@ -121,10 +118,11 @@
import columnSetting from './columnSetting.vue' import columnSetting from './columnSetting.vue'
import { useSlots } from 'vue' import { useSlots } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { get } from 'lodash-es' import { cloneDeep, get } from 'lodash-es'
const slots = useSlots() const slots = useSlots()
const route = useRoute() const route = useRoute()
const emit = defineEmits(['onExpand']) const emit = defineEmits(['onExpand', 'onSelectionChange'])
const renderSlots = Object.keys(slots) const renderSlots = Object.keys(slots)
const props = defineProps( const props = defineProps(
@ -207,7 +205,17 @@
localSettings: { localSettings: {
rowClassName: props.rowClassName, rowClassName: props.rowClassName,
rowClassNameSwitch: Boolean(props.rowClassName) rowClassNameSwitch: Boolean(props.rowClassName)
} },
renderTableProps: {
...props,
columns: [],
dataSource: [],
pagination: {},
loading: false,
size: props.compSize
},
selectedRows: [],
selectedRowKeys: []
}) })
watch( 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( watch(
() => props.columns, () => props.columns,
(newVal) => { (newVal) => {
data.columnsSetting = newVal data.columnsSetting = newVal.map((col) => ({
} ...col,
checked: col.checked === undefined ? true : col.checked
}))
},
{ deep: true, immediate: true }
) )
// props // 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 = [ const tool = [
{ {
@ -291,13 +411,9 @@
// //
const columnChange = (v) => { const columnChange = (v) => {
data.columnsSetting = v data.columnsSetting = v
getTableProps() data.localColumns = v.filter((value) => value.checked === undefined || value.checked)
getTableProps() // getTableProps
} }
//
const rowClear = (callback) => {
clearSelected()
}
//
const init = () => { const init = () => {
const { current } = route.params const { current } = route.params
const localPageNum = (current && parseInt(current)) || props.pageNum const localPageNum = (current && parseInt(current)) || props.pageNum
@ -305,7 +421,7 @@
(['auto', true].includes(props.showPagination) && (['auto', true].includes(props.showPagination) &&
Object.assign({}, data.localPagination, { Object.assign({}, data.localPagination, {
current: localPageNum, current: localPageNum,
pageSize: props.size, //props.compSize, size// pageSize: props.size,
showSizeChanger: props.showSizeChanger, showSizeChanger: props.showSizeChanger,
defaultPageSize: props.defaultPageSize, defaultPageSize: props.defaultPageSize,
pageSizeOptions: props.pageSizeOptions, pageSizeOptions: props.pageSizeOptions,
@ -316,6 +432,16 @@
false false
data.needTotalList = initTotalList(props.columns) data.needTotalList = initTotalList(props.columns)
data.columnsSetting = 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() loadData()
} }
@ -453,83 +579,44 @@
: null : null
return 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[k] = props[k]
}) })
renderProps = { renderProps = {
...renderProps, ...renderProps,
size: data.customSize, // sizea-tablecompSize size: data.customSize,
columns: data.columnsSetting.filter((value) => value.checked === undefined || value.checked), columns: data.columnsSetting.filter((value) => value.checked === undefined || value.checked),
...data.localSettings ...data.localSettings
} }
// undefined null tableprops // 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 // total
const updateSelect = (selectedRowKeys, selectedRows) => { const updateSelect = (selectedRowKeys, selectedRows) => {
if (props.rowSelection) { if (props.rowSelection) {
//
data.selectedRows = cloneDeep(selectedRows)
data.selectedRowKeys = cloneDeep(selectedRowKeys)
// rowSelection
// eslint-disable-next-line vue/no-mutating-props // 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 // eslint-disable-next-line vue/no-mutating-props
props.rowSelection.selectedRowKeys = selectedRowKeys props.rowSelection.selectedRowKeys = cloneDeep(selectedRowKeys)
props.rowSelection.onChange(selectedRowKeys, selectedRows) //
emit('onSelectionChange', selectedRowKeys, selectedRows)
//
getTableProps() getTableProps()
} //
const list = data.needTotalList data.needTotalList = initTotalList(props.columns)
data.needTotalList = list.map((item) => { data.needTotalList.forEach((item) => {
return { item.total = selectedRows.reduce((sum, val) => {
...item, const value = get(val, item.dataIndex)
total: selectedRows.reduce((sum, val) => { return addNumbers(sum, value)
return addNumbers(sum, get(val, item.dataIndex))
}, 0) }, 0)
}
}) })
} }
// needTotal }
//
const addNumbers = (num1, num2) => { const addNumbers = (num1, num2) => {
// //
let num1Value = Number(num1) let num1Value = Number(num1)
@ -554,10 +641,19 @@
// table // table
const clearSelected = () => { const clearSelected = () => {
if (props.rowSelection) { 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([], []) props.rowSelection.onChange([], [])
updateSelect([], []) //
getTableProps() getTableProps()
//
updateSelect([], [])
} }
data.needTotalList = initTotalList(props.columns)
} }
// //
const clearRefreshSelected = (bool = false) => { 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" trigger="click"
placement="bottomLeft" placement="bottomLeft"
:overlayStyle="{ width: '500px' }" :overlayStyle="{ width: '500px' }"
@visibleChange="handleVisibleChange" @openChange="handleOpenChange"
> >
<template #content> <template #content>
<div class="icon-selector-content"> <div class="icon-selector-content">
<a-tabs v-model:activeKey="activeKey" tab-position="left" size="small" @change="handleTabChange"> <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"> <a-tab-pane v-for="group in iconData" :key="group.key" :tab="group.name">
<div v-if="group.iconItem.length > 1" class="icon-category"> <div v-if="group.iconItem.length > 1" class="icon-category">
<a-form-item-rest>
<a-radio-group v-model:value="currentCategory" @change="handleCategoryChange" size="small"> <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"> <a-radio-button v-for="item in group.iconItem" :key="item.key" :value="item.key">
{{ item.name }} {{ item.name }}
</a-radio-button> </a-radio-button>
</a-radio-group> </a-radio-group>
</a-form-item-rest>
</div> </div>
<div class="icon-grid"> <div class="icon-grid">
<template v-for="iconGroup in group.iconItem" :key="iconGroup.key"> <template v-for="iconGroup in group.iconItem" :key="iconGroup.key">
<template v-if="iconGroup.key === currentCategory"> <template v-if="iconGroup.key === currentCategory">
@ -59,7 +60,7 @@
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue' import { ref } from 'vue'
import config from '@/config/iconSelect' import config from '@/config/iconSelect'
import { SearchOutlined, CloseCircleOutlined } from '@ant-design/icons-vue' import { SearchOutlined, CloseCircleOutlined } from '@ant-design/icons-vue'
@ -85,10 +86,10 @@
default: true default: true
} }
}) })
const formRef = defineModel('formRef')
const emit = defineEmits(['update:value', 'change']) const emit = defineEmits(['update:value', 'change'])
const selectedIcon = ref(props.value) const selectedIcon = ref()
const iconData = ref(config.icons) const iconData = ref(config.icons)
const visible = ref(false) const visible = ref(false)
const activeKey = ref(iconData.value[0]?.key || '') const activeKey = ref(iconData.value[0]?.key || '')
@ -101,9 +102,9 @@
} }
) )
const handleVisibleChange = (isVisible) => { const handleOpenChange = (isOpen) => {
if (!props.disabled) { if (!props.disabled) {
visible.value = isVisible visible.value = isOpen
} }
} }
@ -112,16 +113,15 @@
} }
const handleIconSelect = (icon) => { const handleIconSelect = (icon) => {
selectedIcon.value = icon
emit('update:value', icon) emit('update:value', icon)
emit('change', icon) formRef.value?.validateFields('icon')
visible.value = false visible.value = false
} }
const handleClear = () => { const handleClear = () => {
selectedIcon.value = ''
emit('update:value', '') emit('update:value', '')
emit('change', '') selectedIcon.value = ''
formRef.value?.validateFields('icon')
} }
const handleTabChange = (key) => { const handleTabChange = (key) => {
@ -144,6 +144,7 @@
color: rgba(0, 0, 0, 0.25); color: rgba(0, 0, 0, 0.25);
transition: color 0.3s; transition: color 0.3s;
font-size: 12px; font-size: 12px;
&:hover { &:hover {
color: rgba(0, 0, 0, 0.45); color: rgba(0, 0, 0, 0.45);
} }
@ -210,6 +211,7 @@
background-color: rgba(0, 0, 0, 0.2); background-color: rgba(0, 0, 0, 0.2);
border-radius: 4px; border-radius: 4px;
} }
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background-color: transparent; background-color: transparent;
} }
@ -240,12 +242,15 @@
font-size: 14px; font-size: 14px;
} }
} }
:deep(.ant-tabs-tab) { :deep(.ant-tabs-tab) {
padding: 8px !important; padding: 8px !important;
} }
:deep(.ant-tabs-nav) { :deep(.ant-tabs-nav) {
width: 60px !important; width: 60px !important;
} }
:deep(.ant-tabs-tabpane) { :deep(.ant-tabs-tabpane) {
padding-left: 0 !important; padding-left: 0 !important;
} }