Merge pull request #334 from vvandk/master

feat: 表格工具栏新增列设置功能
This commit is contained in:
Archer 2023-09-17 08:37:24 +08:00 committed by GitHub
commit 9cbccfae96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 446 additions and 71 deletions

View File

@ -106,7 +106,9 @@
"vite-plugin-purge-icons": "^0.9.2", "vite-plugin-purge-icons": "^0.9.2",
"vite-plugin-style-import": "2.0.0", "vite-plugin-style-import": "2.0.0",
"vite-plugin-svg-icons": "^2.0.1", "vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^1.8.8" "vue-tsc": "^1.8.8",
"vue-draggable-plus": "^0.2.6",
"lodash": "^4.17.21"
}, },
"engines": { "engines": {
"node": ">= 14.18.0" "node": ">= 14.18.0"

View File

@ -17,6 +17,9 @@ import { getSlot } from '@/utils/tsxHelper'
import TableActions from './components/TableActions.vue' import TableActions from './components/TableActions.vue'
// import Sortable from 'sortablejs' // import Sortable from 'sortablejs'
// import { Icon } from '@/components/Icon' // import { Icon } from '@/components/Icon'
import { useAppStore } from '@/store/modules/app'
const appStore = useAppStore()
export default defineComponent({ export default defineComponent({
name: 'Table', name: 'Table',
@ -121,7 +124,9 @@ export default defineComponent({
default: () => undefined default: () => undefined
}, },
rowKey: propTypes.string.def('id'), rowKey: propTypes.string.def('id'),
emptyText: propTypes.string.def('No Data'), emptyText: propTypes.string.def('暂无数据'),
//
activeUID: propTypes.string.def(''),
defaultExpandAll: propTypes.bool.def(false), defaultExpandAll: propTypes.bool.def(false),
expandRowKeys: { expandRowKeys: {
type: Array as PropType<string[]>, type: Array as PropType<string[]>,
@ -345,7 +350,7 @@ export default defineComponent({
const renderTreeTableColumn = (columnsChildren: TableColumn[]) => { const renderTreeTableColumn = (columnsChildren: TableColumn[]) => {
const { align, headerAlign, showOverflowTooltip, preview } = unref(getProps) const { align, headerAlign, showOverflowTooltip, preview } = unref(getProps)
return columnsChildren.map((v) => { return columnsChildren.map((v) => {
if (v.hidden) return null if (v.show === false) return null
const props = { ...v } as any const props = { ...v } as any
if (props.children) delete props.children if (props.children) delete props.children
@ -417,7 +422,7 @@ export default defineComponent({
} = unref(getProps) } = unref(getProps)
return (columnsChildren || columns).map((v) => { return (columnsChildren || columns).map((v) => {
if (v.hidden) return null if (v.show === false) return null
if (v.type === 'index') { if (v.type === 'index') {
return ( return (
<ElTableColumn <ElTableColumn
@ -429,6 +434,7 @@ export default defineComponent({
headerAlign={v.headerAlign || headerAlign} headerAlign={v.headerAlign || headerAlign}
label={v.label} label={v.label}
width="65px" width="65px"
fixed="left"
></ElTableColumn> ></ElTableColumn>
) )
} else if (v.type === 'selection') { } else if (v.type === 'selection') {
@ -494,6 +500,7 @@ export default defineComponent({
if (getSlot(slots, 'append')) { if (getSlot(slots, 'append')) {
tableSlots['append'] = (...args: any[]) => getSlot(slots, 'append', args) tableSlots['append'] = (...args: any[]) => getSlot(slots, 'append', args)
} }
const toolbar = getSlot(slots, 'toolbar')
// const { sortable } = unref(getProps) // const { sortable } = unref(getProps)
@ -511,14 +518,31 @@ export default defineComponent({
return ( return (
<div v-loading={unref(getProps).loading}> <div v-loading={unref(getProps).loading}>
<div class="flex justify-between mb-1">
<div>{toolbar}</div>
<div class="pt-2">
{unref(getProps).showAction ? ( {unref(getProps).showAction ? (
<TableActions <TableActions
activeUID={unref(getProps).activeUID}
columns={unref(getProps).columns} columns={unref(getProps).columns}
el-table-ref={elTableRef}
onChangSize={changSize} onChangSize={changSize}
onRefresh={refresh} onRefresh={refresh}
/> />
) : null} ) : null}
<ElTable ref={elTableRef} data={unref(getProps).data} {...unref(getBindValue)}> </div>
</div>
<ElTable
ref={elTableRef}
data={unref(getProps).data}
{...unref(getBindValue)}
header-cell-style={
appStore.getIsDark
? { color: '#CFD3DC', 'background-color': '#000' }
: { color: '#000', 'background-color': '#f5f7fa' }
}
>
{{ {{
default: () => renderTableColumn(), default: () => renderTableColumn(),
...tableSlots ...tableSlots

View File

@ -1,24 +1,34 @@
<script lang="tsx"> <script lang="tsx">
import { defineComponent, unref, computed, PropType, watch } from 'vue' import { defineComponent, unref, computed, PropType, watch, ref, nextTick } from 'vue'
import { import {
ElTooltip, ElTooltip,
ElDropdown, ElDropdown,
ElDropdownMenu, ElDropdownMenu,
ElDropdownItem, ElDropdownItem,
ComponentSize ComponentSize,
// ElPopover, ElPopover,
// ElTree ElCheckbox,
ElScrollbar,
ElButton,
ElTable,
ElDivider
} from 'element-plus' } from 'element-plus'
import { Icon } from '@/components/Icon' import { Icon } from '@/components/Icon'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
import { TableColumn } from '../types' import { TableColumn } from '../types'
import { cloneDeep } from 'lodash-es' import { VueDraggable } from 'vue-draggable-plus'
// import { eachTree } from '@/utils/tree' import { useRouter } from 'vue-router'
import { useStorage } from '@/hooks/web/useStorage'
import cloneDeep from 'lodash/cloneDeep'
import { propTypes } from '@/utils/propTypes'
import { moveElementToIndex } from '@/utils/index'
const appStore = useAppStore() const appStore = useAppStore()
const sizeMap = computed(() => appStore.sizeMap) const sizeMap = computed(() => appStore.sizeMap)
const { setStorage, getStorage, removeStorage } = useStorage()
const { t } = useI18n() const { t } = useI18n()
export default defineComponent({ export default defineComponent({
@ -27,7 +37,13 @@ export default defineComponent({
columns: { columns: {
type: Array as PropType<TableColumn[]>, type: Array as PropType<TableColumn[]>,
default: () => [] default: () => []
} },
elTableRef: {
type: Object as PropType<ComponentRef<typeof ElTable>>,
default: () => {}
},
//
activeUID: propTypes.string.def('')
}, },
emits: ['refresh', 'changSize'], emits: ['refresh', 'changSize'],
setup(props, { emit }) { setup(props, { emit }) {
@ -39,25 +55,142 @@ export default defineComponent({
emit('changSize', size) emit('changSize', size)
} }
const columns = computed(() => { const tableColumns = ref(props.columns)
return cloneDeep(props.columns).filter((v) => { const elTableRef = ref(props.elTableRef)
// typeselectionexpand const activeUID = ref(props.activeUID)
if (v.type !== 'selection' && v.type !== 'expand') { const numberColumnStatus = ref(false)
return v
// table columns
const numberColumnField = tableColumns.value.find((item) => item.type === 'index')
if (numberColumnField === undefined) {
tableColumns.value.unshift({
field: '_serial_number',
label: '序号',
type: 'index',
show: false,
disabled: true
})
} else {
numberColumnStatus.value = numberColumnField.show
}
// table columns
const oldTableColumns = cloneDeep(unref(tableColumns))
const checkAll = ref(false)
// True
const isIndeterminate = ref(true)
//
const handleCheckAllChange = (val: boolean) => {
tableColumns.value.forEach((item) => {
if (item.disabled !== true) {
item.show = val
} }
}) })
isIndeterminate.value = tableColumns.value
.filter((item) => !item.disabled)
.some((item) => item.show)
}
//
const handleCheckChange = () => {
checkAll.value = tableColumns.value
.filter((item) => !item.disabled)
.every((item) => item.show)
if (checkAll.value) {
isIndeterminate.value = false
} else {
isIndeterminate.value = tableColumns.value
.filter((item) => !item.disabled)
.some((item) => item.show)
}
}
// table columns
const updateNumberColumnStatus = (syns: boolean, status: boolean = false) => {
const numberColumnField = tableColumns.value.find((item) => item.type === 'index')
if (numberColumnField) {
if (syns) {
numberColumnStatus.value = numberColumnField.show
} else {
numberColumnField.show = status
}
}
}
const { currentRoute } = useRouter()
const fullPath = currentRoute.value.fullPath
const cacheTableHeadersKey = `${fullPath}_${activeUID.value}`
if (cacheTableHeadersKey) {
// table columns table columns
const cacheData = JSON.parse(getStorage(cacheTableHeadersKey))
if (cacheData) {
tableColumns.value.forEach((item) => {
const fieldData = cacheData[item.field]
item._index = fieldData.index
item.show = fieldData.show
item.fixed = fieldData.fixed
}) })
tableColumns.value.sort((a, b) => a._index - b._index)
updateNumberColumnStatus(true)
}
}
watch( watch(
() => columns.value, () => tableColumns.value,
(newColumns) => { async (val) => {
console.log('columns change', newColumns) const cacheData = {}
for (let i = 0; i < val.length; i++) {
const item = val[i]
cacheData[item.field] = {
show: item.show,
index: i,
fixed: item.fixed
}
}
setStorage(cacheTableHeadersKey, JSON.stringify(cacheData))
handleCheckChange()
await nextTick()
elTableRef.value?.doLayout()
}, },
{ {
deep: true deep: true
} }
) )
watch(
() => numberColumnStatus.value,
async (val) => {
updateNumberColumnStatus(false, val)
await nextTick()
elTableRef.value?.doLayout()
},
{
deep: true
}
)
//
const resetTableColumns = async () => {
Object.assign(tableColumns.value, cloneDeep(oldTableColumns))
updateNumberColumnStatus(true)
await nextTick()
//
removeStorage(cacheTableHeadersKey)
}
//
const updateColumnsIndex = (val) => {
Object.assign(
tableColumns.value,
cloneDeep(moveElementToIndex(tableColumns.value, val.oldIndex, val.newIndex))
)
}
handleCheckChange()
return () => ( return () => (
<> <>
<div class="text-right h-28px flex items-center justify-end"> <div class="text-right h-28px flex items-center justify-end">
@ -71,7 +204,7 @@ export default defineComponent({
</span> </span>
</ElTooltip> </ElTooltip>
<ElTooltip content={t('common.size')} placement="top"> <ElTooltip content={t('common.density')} placement="top">
<ElDropdown trigger="click" onCommand={changSize}> <ElDropdown trigger="click" onCommand={changSize}>
{{ {{
default: () => { default: () => {
@ -106,28 +239,76 @@ export default defineComponent({
</ElDropdown> </ElDropdown>
</ElTooltip> </ElTooltip>
{/* <ElTooltip content={t('common.columnSetting')} placement="top"> */} <ElTooltip content={t('common.columnSetting')} placement="top">
{/* <ElPopover trigger="click" placement="left"> <ElPopover trigger="click" placement="bottom" width="300px">
{{ {{
default: () => { default: () => {
return ( return (
<div> <div>
<ElTree <div style="border-bottom: 1px solid #d4d7de" class="flex justify-between">
data={unref(columns)} <div>
show-checkbox <ElCheckbox
default-checked-keys={unref(defaultCheckeds)} v-model={checkAll.value}
draggable indeterminate={isIndeterminate.value}
node-key="field" onChange={handleCheckAllChange}
allow-drop={(_draggingNode: any, _dropNode: any, type: string) => { >
if (type === 'inner') { {t('common.selectAll')}
return false </ElCheckbox>
} else { <ElCheckbox v-model={numberColumnStatus.value}>
return true {t('common.SerialNumberColumn')}
} </ElCheckbox>
</div>
<ElButton type="primary" link onClick={resetTableColumns}>
{t('common.reset')}
</ElButton>
</div>
<ElScrollbar max-height="400px">
<VueDraggable
modelValue={tableColumns.value}
onEnd={updateColumnsIndex}
handle=".cursor-move"
>
{tableColumns.value.map((element) => {
if (element.type === 'index') return null
return (
<div class="flex justify-between">
<div>
<span class="cursor-move mr-10px">
<Icon icon="akar-icons:drag-vertical" />
</span>
<ElCheckbox
v-model={element.show}
disabled={element.disabled === true}
onChange={handleCheckChange}
>
{element.label}
</ElCheckbox>
</div>
<div class="mt-7px mr-9px">
<span
class={element.fixed === 'left' ? 'color-[#409eff]' : ''}
onClick={() => {
element.fixed = element.fixed === 'left' ? undefined : 'left'
}} }}
onNode-drag-end={onNodeDragEnd} >
onCheck-change={onCheckChange} <Icon icon="radix-icons:pin-left" class="cursor-pointer" />
/> </span>
<ElDivider direction="vertical" />
<span
class={element.fixed === 'right' ? 'color-[#409eff]' : ''}
onClick={() => {
element.fixed =
element.fixed === 'right' ? undefined : 'right'
}}
>
<Icon icon="radix-icons:pin-right" class="cursor-pointer" />
</span>
</div>
</div>
)
})}
</VueDraggable>
</ElScrollbar>
</div> </div>
) )
}, },
@ -141,8 +322,8 @@ export default defineComponent({
) )
} }
}} }}
</ElPopover> */} </ElPopover>
{/* </ElTooltip> */} </ElTooltip>
</div> </div>
</> </>
) )

View File

@ -4,9 +4,9 @@ export interface TableColumn {
label?: string label?: string
type?: string type?: string
/** /**
* *
*/ */
hidden?: boolean show: boolean
children?: TableColumn[] children?: TableColumn[]
slots?: { slots?: {
default?: (...args: any[]) => JSX.Element | JSX.Element[] | null default?: (...args: any[]) => JSX.Element | JSX.Element[] | null

View File

@ -44,11 +44,14 @@ export default {
refresh: 'Refresh', refresh: 'Refresh',
fullscreen: 'Fullscreen', fullscreen: 'Fullscreen',
size: 'Size', size: 'Size',
density: 'Density',
columnSetting: 'Column setting', columnSetting: 'Column setting',
lengthRange: 'The length should be between {min} and {max}', lengthRange: 'The length should be between {min} and {max}',
notSpace: 'Spaces are not allowed', notSpace: 'Spaces are not allowed',
notSpecialCharacters: 'Special characters are not allowed', notSpecialCharacters: 'Special characters are not allowed',
isEqual: 'The two are not equal' isEqual: 'The two are not equal',
selectAll: 'Select all',
SerialNumberColumn: 'Index column'
}, },
lock: { lock: {
lockScreen: 'Lock screen', lockScreen: 'Lock screen',
@ -108,18 +111,27 @@ export default {
welcome: 'Welcome to the system', welcome: 'Welcome to the system',
message: 'Backstage management system', message: 'Backstage management system',
username: 'Username', username: 'Username',
telephone: 'Telephone',
password: 'Password', password: 'Password',
register: 'Register', register: 'Register',
checkPassword: 'Confirm password', checkPassword: 'Confirm password',
login: 'Sign in', login: 'Sign in',
passwordLogin: 'Password login',
smsLogin: 'SMS code login',
otherLogin: 'Sign in with', otherLogin: 'Sign in with',
remember: 'Remember me', remember: 'Remember me',
hasUser: 'Existing account? Go to login', hasUser: 'Existing account? Go to login',
forgetPassword: 'Forget password', forgetPassword: 'Forget password',
usernamePlaceholder: 'Please input username', usernamePlaceholder: 'Please input username',
telephonePlaceholder: 'Please input telephone',
passwordPlaceholder: 'Please input password', passwordPlaceholder: 'Please input password',
code: 'Verification code', code: 'Verification code',
codePlaceholder: 'Please input verification code' getCode: 'Get code',
codePlaceholder: 'Please input verification code',
SMSCode: 'sms code',
getSMSCode: 'get sms code',
SMSCodePlaceholder: 'Please input sms code',
SMSCodeRetry: 'S retry'
}, },
router: { router: {
login: 'Login', login: 'Login',
@ -442,6 +454,7 @@ export default {
changeTitle: 'Change title', changeTitle: 'Change title',
header: 'Header', header: 'Header',
selectAllNone: 'Select all / none', selectAllNone: 'Select all / none',
selectAll: 'Select all',
delOrAddAction: 'Delete or add action', delOrAddAction: 'Delete or add action',
showOrHiddenStripe: 'Show or hidden stripe', showOrHiddenStripe: 'Show or hidden stripe',
showOrHiddenBorder: 'Show or hidden border', showOrHiddenBorder: 'Show or hidden border',

View File

@ -44,11 +44,14 @@ export default {
refresh: '刷新', refresh: '刷新',
fullscreen: '全屏', fullscreen: '全屏',
size: '尺寸', size: '尺寸',
density: '密度',
columnSetting: '列设置', columnSetting: '列设置',
lengthRange: '长度在 {min} 到 {max} 个字符', lengthRange: '长度在 {min} 到 {max} 个字符',
notSpace: '不能包含空格', notSpace: '不能包含空格',
notSpecialCharacters: '不能包含特殊字符', notSpecialCharacters: '不能包含特殊字符',
isEqual: '两次输入不一致' isEqual: '两次输入不一致',
selectAll: '全选',
SerialNumberColumn: '序号列'
}, },
lock: { lock: {
lockScreen: '锁定屏幕', lockScreen: '锁定屏幕',
@ -107,18 +110,27 @@ export default {
welcome: '欢迎使用本系统', welcome: '欢迎使用本系统',
message: '开箱即用的中后台管理系统', message: '开箱即用的中后台管理系统',
username: '用户名', username: '用户名',
telephone: '手机号',
password: '密码', password: '密码',
register: '注册', register: '注册',
checkPassword: '确认密码', checkPassword: '确认密码',
login: '登录', login: '登录',
passwordLogin: '密码登录',
smsLogin: '短信验证码登录',
otherLogin: '其它登录方式', otherLogin: '其它登录方式',
remember: '记住我', remember: '记住我',
hasUser: '已有账号?去登录', hasUser: '已有账号?去登录',
forgetPassword: '忘记密码', forgetPassword: '忘记密码',
usernamePlaceholder: '请输入用户名', usernamePlaceholder: '请输入用户名',
telephonePlaceholder: '请输入手机号',
passwordPlaceholder: '请输入密码', passwordPlaceholder: '请输入密码',
code: '验证码', code: '验证码',
codePlaceholder: '请输入验证码' getCode: '获取验证码',
codePlaceholder: '请输入验证码',
SMSCode: '短信验证码',
getSMSCode: '获取短信验证码',
SMSCodePlaceholder: '请输入短信验证码',
SMSCodeRetry: 'S后重新'
}, },
router: { router: {
login: '登录', login: '登录',
@ -435,6 +447,7 @@ export default {
changeTitle: '修改标题', changeTitle: '修改标题',
header: '头部', header: '头部',
selectAllNone: '全选/全不选', selectAllNone: '全选/全不选',
selectAll: '全选',
delOrAddAction: '删除/添加操作列', delOrAddAction: '删除/添加操作列',
showOrHiddenStripe: '显示/隐藏斑马纹', showOrHiddenStripe: '显示/隐藏斑马纹',
showOrHiddenBorder: '显示/隐藏边框', showOrHiddenBorder: '显示/隐藏边框',

View File

@ -122,3 +122,145 @@ export function toAnyString() {
export function firstUpperCase(str: string) { export function firstUpperCase(str: string) {
return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase()) return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase())
} }
// 根据当前时间获取祝福语
export const getGreeting = (): string => {
const now = new Date()
const hour = now.getHours()
if (hour >= 6 && hour < 10) {
return '早上好'
} else if (hour >= 10 && hour < 13) {
return '中午好'
} else if (hour >= 13 && hour < 18) {
return '下午好'
} else {
return '晚上好'
}
}
// 获取当前星期几
export const getDayOfWeek = (): string => {
const daysOfWeek: string[] = [
'星期日',
'星期一',
'星期二',
'星期三',
'星期四',
'星期五',
'星期六'
]
const date: Date = new Date()
const dayOfWeekIndex: number = date.getDay()
return daysOfWeek[dayOfWeekIndex]
}
// 数字转金额
// 作者:时光足迹
// 链接https://juejin.cn/post/7028086399601475591
// 来源:稀土掘金
export const formatMoney = (amount, currency = true): string => {
const formatter = new Intl.NumberFormat('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
useGrouping: true
})
const formattedAmount = formatter.format(amount)
if (currency) {
return `${formattedAmount}`
}
return formattedAmount
}
/**
*
* 0.85 -> 8.5
* 0.5 -> 5
*/
export const convertToDiscount = (decimal: number | undefined): string => {
if (decimal === undefined) {
return ''
}
const discount = decimal * 10
if (discount === 10) {
return '无折扣'
}
return discount % 1 === 0 ? `${discount}` : `${discount.toFixed(1)}`
}
/**
*
* yyyy-MM-dd HH:mm:ss
*/
export const getCurrentDateTime = (): string => {
const now: Date = new Date()
const year: number = now.getFullYear()
const month: number = now.getMonth() + 1
const day: number = now.getDate()
const hours: number = now.getHours()
const minutes: number = now.getMinutes()
const seconds: number = now.getSeconds()
// 格式化为字符串
const formattedDateTime = `${year}-${padZero(month)}-${padZero(day)} ${padZero(hours)}:${padZero(
minutes
)}:${padZero(seconds)}`
return formattedDateTime
}
/**
*
* yyyy-MM-dd HH:mm:ss
*/
export const getCurrentDate = (): string => {
const now: Date = new Date()
const year: number = now.getFullYear()
const month: number = now.getMonth() + 1
const day: number = now.getDate()
// 格式化为字符串
const formattedDate = `${year}-${padZero(month)}-${padZero(day)}`
return formattedDate
}
// 辅助函数在数字小于10时在前面补零
export const padZero = (num: number): string => {
return num < 10 ? `0${num}` : `${num}`
}
// 将base64编码的字符串转换为文件
export const base64ToFile = (dataURI, filename): File => {
const arr = dataURI.split(',')
const mime = arr[0].match(/:(.*?);/)[1]
const bstr = atob(arr[1])
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new File([u8arr], filename, { type: mime })
}
// 将指定索引的元素移动到目标索引的函数
export const moveElementToIndex = (array: any[], fromIndex: number, toIndex: number) => {
const clonedArray = [...array] // 克隆数组以避免修改原始数组
if (
fromIndex >= 0 &&
fromIndex < clonedArray.length &&
toIndex >= 0 &&
toIndex < clonedArray.length
) {
const [element] = clonedArray.splice(fromIndex, 1) // 移除指定索引的元素
clonedArray.splice(toIndex, 0, element) // 将元素插入目标索引位置
}
return clonedArray
}