feat: 拖拽表格

This commit is contained in:
kailong321200875 2023-07-20 16:37:18 +08:00
parent cfb3b3a5ce
commit b69b8ed1bd
18 changed files with 254 additions and 29 deletions

View File

@ -1,7 +1,7 @@
on: on:
push: push:
branches: branches:
- master - release
name: Release name: Release

View File

@ -147,7 +147,15 @@ const adminList = [
component: 'views/Components/Table/TreeTable', component: 'views/Components/Table/TreeTable',
name: 'TreeTable', name: 'TreeTable',
meta: { meta: {
title: 'TreeTable' title: 'router.TreeTable'
}
},
{
path: 'table-image-preview',
component: 'views/Components/Table/TableImagePreview',
name: 'TableImagePreview',
meta: {
title: 'router.PicturePreview'
} }
}, },
{ {
@ -490,6 +498,7 @@ const testList: string[] = [
'/components/table/default-table', '/components/table/default-table',
'/components/table/use-table', '/components/table/use-table',
'/components/table/tree-table', '/components/table/tree-table',
'/components/table/table-image-preview',
'/components/table/ref-table', '/components/table/ref-table',
'/components/editor-demo', '/components/editor-demo',
'/components/editor-demo/editor', '/components/editor-demo/editor',

View File

@ -20,6 +20,7 @@ interface ListProps {
importance: number importance: number
display_time: string display_time: string
pageviews: number pageviews: number
image_uri: string
} }
interface TreeListProps { interface TreeListProps {
@ -45,8 +46,8 @@ for (let i = 0; i < count; i++) {
content: baseContent, content: baseContent,
importance: '@integer(1, 3)', importance: '@integer(1, 3)',
display_time: '@datetime', display_time: '@datetime',
pageviews: '@integer(300, 5000)' pageviews: '@integer(300, 5000)',
// image_uri image_uri: Mock.Random.image('@integer(300, 5000)x@integer(300, 5000)')
}) })
) )
} }

View File

@ -47,6 +47,7 @@
"pinia-plugin-persist": "^1.0.0", "pinia-plugin-persist": "^1.0.0",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"qs": "^6.11.2", "qs": "^6.11.2",
"sortablejs": "^1.15.0",
"url": "^0.11.1", "url": "^0.11.1",
"vue": "3.3.4", "vue": "3.3.4",
"vue-i18n": "9.2.2", "vue-i18n": "9.2.2",
@ -66,6 +67,7 @@
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.5.1", "@types/qrcode": "^1.5.1",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"@types/sortablejs": "^1.15.1",
"@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0", "@typescript-eslint/parser": "^6.1.0",
"@unocss/transformer-variant-group": "^0.53.5", "@unocss/transformer-variant-group": "^0.53.5",

View File

@ -78,7 +78,7 @@ export default defineComponent({
validateOnRuleChange: propTypes.bool.def(true), validateOnRuleChange: propTypes.bool.def(true),
size: { size: {
type: String as PropType<ComponentSize>, type: String as PropType<ComponentSize>,
default: 'small' default: undefined
}, },
disabled: propTypes.bool.def(false), disabled: propTypes.bool.def(false),
scrollToError: propTypes.bool.def(false), scrollToError: propTypes.bool.def(false),

View File

@ -104,7 +104,9 @@ const getPasswordStrength = computed(() => {
height: inherit; height: inherit;
background-color: transparent; background-color: transparent;
border-radius: inherit; border-radius: inherit;
transition: width 0.5s ease-in-out, background 0.25s; transition:
width 0.5s ease-in-out,
background 0.25s;
&[data-score='0'] { &[data-score='0'] {
width: 20%; width: 20%;

View File

@ -1,6 +1,13 @@
<script lang="tsx"> <script lang="tsx">
import { ElTable, ElTableColumn, ElPagination, ComponentSize, ElTooltipProps } from 'element-plus' import {
import { defineComponent, PropType, ref, computed, unref, watch, onMounted } from 'vue' ElTable,
ElTableColumn,
ElPagination,
ComponentSize,
ElTooltipProps,
ElImage
} from 'element-plus'
import { defineComponent, PropType, ref, computed, unref, watch, onMounted, nextTick } from 'vue'
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
import { setIndex } from './helper' import { setIndex } from './helper'
import type { TableProps, TableColumn, Pagination, TableSetProps } from './types' import type { TableProps, TableColumn, Pagination, TableSetProps } from './types'
@ -8,6 +15,8 @@ import { set } from 'lodash-es'
import { CSSProperties } from 'vue' import { CSSProperties } from 'vue'
import { getSlot } from '@/utils/tsxHelper' import { getSlot } from '@/utils/tsxHelper'
import TableActions from './components/TableActions.vue' import TableActions from './components/TableActions.vue'
import Sortable from 'sortablejs'
import { Icon } from '@/components/Icon'
export default defineComponent({ export default defineComponent({
name: 'Table', name: 'Table',
@ -48,6 +57,12 @@ export default defineComponent({
type: Array as PropType<Recordable[]>, type: Array as PropType<Recordable[]>,
default: () => [] default: () => []
}, },
//
preview: {
type: Array as PropType<string[]>,
default: () => []
},
sortable: propTypes.bool.def(false),
height: propTypes.oneOfType([Number, String]), height: propTypes.oneOfType([Number, String]),
maxHeight: propTypes.oneOfType([Number, String]), maxHeight: propTypes.oneOfType([Number, String]),
stripe: propTypes.bool.def(false), stripe: propTypes.bool.def(false),
@ -173,7 +188,7 @@ export default defineComponent({
scrollbarAlwaysOn: propTypes.bool.def(false), scrollbarAlwaysOn: propTypes.bool.def(false),
flexible: propTypes.bool.def(false) flexible: propTypes.bool.def(false)
}, },
emits: ['update:pageSize', 'update:currentPage', 'register', 'refresh'], emits: ['update:pageSize', 'update:currentPage', 'register', 'refresh', 'sortable-change'],
setup(props, { attrs, emit, slots, expose }) { setup(props, { attrs, emit, slots, expose }) {
const elTableRef = ref<ComponentRef<typeof ElTable>>() const elTableRef = ref<ComponentRef<typeof ElTable>>()
@ -198,6 +213,33 @@ export default defineComponent({
return propsObj return propsObj
}) })
const sortableEl = ref()
//
const initDropTable = () => {
const el = unref(elTableRef)?.$el.querySelector('.el-table__body tbody')
if (!el) return
if (unref(sortableEl)) unref(sortableEl).destroy()
sortableEl.value = Sortable.create(el, {
handle: '.table-move',
animation: 180,
onEnd(e: any) {
emit('sortable-change', e)
}
})
}
watch(
() => getProps.value.sortable,
async (v) => {
await nextTick()
v && initDropTable()
},
{
immediate: true
}
)
const setProps = (props: TableProps = {}) => { const setProps = (props: TableProps = {}) => {
mergeProps.value = Object.assign(unref(mergeProps), props) mergeProps.value = Object.assign(unref(mergeProps), props)
outsideProps.value = { ...props } as any outsideProps.value = { ...props } as any
@ -301,7 +343,7 @@ export default defineComponent({
}) })
const renderTreeTableColumn = (columnsChildren: TableColumn[]) => { const renderTreeTableColumn = (columnsChildren: TableColumn[]) => {
const { align, headerAlign, showOverflowTooltip } = unref(getProps) const { align, headerAlign, showOverflowTooltip, preview } = unref(getProps)
return columnsChildren.map((v) => { return columnsChildren.map((v) => {
if (v.hidden) return null if (v.hidden) return null
const props = { ...v } as any const props = { ...v } as any
@ -312,12 +354,20 @@ export default defineComponent({
const slots = { const slots = {
default: (...args: any[]) => { default: (...args: any[]) => {
const data = args[0] const data = args[0]
let isImageUrl = false
if (preview.length) {
isImageUrl = preview.some((item) => (item as string) === v.field)
}
return children && children.length return children && children.length
? renderTreeTableColumn(children) ? renderTreeTableColumn(children)
: props?.slots?.default : props?.slots?.default
? props.slots.default(args) ? props.slots.default(args)
: v?.formatter?.(data.row, data.column, data.row[v.field], data.$index) || : v?.formatter
data.row[v.field] ? v?.formatter?.(data.row, data.column, data.row[v.field], data.$index)
: isImageUrl
? renderPreview(data.row[v.field])
: data.row[v.field]
} }
} }
if (props?.slots?.header) { if (props?.slots?.header) {
@ -338,6 +388,21 @@ export default defineComponent({
}) })
} }
const renderPreview = (url: string) => {
return (
<div class="flex items-center">
<ElImage
src={url}
fit="cover"
class="w-[100%] h-100px"
lazy
preview-src-list={[url]}
preview-teleported
/>
</div>
)
}
const renderTableColumn = (columnsChildren?: TableColumn[]) => { const renderTableColumn = (columnsChildren?: TableColumn[]) => {
const { const {
columns, columns,
@ -347,7 +412,8 @@ export default defineComponent({
align, align,
headerAlign, headerAlign,
showOverflowTooltip, showOverflowTooltip,
reserveSelection reserveSelection,
preview
} = unref(getProps) } = unref(getProps)
return (columnsChildren || columns).map((v) => { return (columnsChildren || columns).map((v) => {
@ -384,12 +450,21 @@ export default defineComponent({
const slots = { const slots = {
default: (...args: any[]) => { default: (...args: any[]) => {
const data = args[0] const data = args[0]
let isImageUrl = false
if (preview.length) {
isImageUrl = preview.some((item) => (item as string) === v.field)
}
return children && children.length return children && children.length
? renderTreeTableColumn(children) ? renderTreeTableColumn(children)
: props?.slots?.default : props?.slots?.default
? props.slots.default(args) ? props.slots.default(args)
: v?.formatter?.(data.row, data.column, data.row[v.field], data.$index) || : v?.formatter
data.row[v.field] ? v?.formatter?.(data.row, data.column, data.row[v.field], data.$index)
: isImageUrl
? renderPreview(data.row[v.field])
: data.row[v.field]
} }
} }
if (props?.slots?.header) { if (props?.slots?.header) {
@ -419,6 +494,21 @@ 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 { sortable } = unref(getProps)
const sortableEl = sortable ? (
<ElTableColumn
className="table-move cursor-move"
type="sortable"
prop="sortable"
width="60px"
align="center"
>
<Icon icon="ant-design:drag-outlined" />
</ElTableColumn>
) : null
return ( return (
<div v-loading={unref(getProps).loading}> <div v-loading={unref(getProps).loading}>
{unref(getProps).showAction ? ( {unref(getProps).showAction ? (
@ -426,7 +516,7 @@ export default defineComponent({
) : null} ) : null}
<ElTable ref={elTableRef} data={unref(getProps).data} {...unref(getBindValue)}> <ElTable ref={elTableRef} data={unref(getProps).data} {...unref(getBindValue)}>
{{ {{
default: () => renderTableColumn(), default: () => [sortableEl, ...renderTableColumn()],
...tableSlots ...tableSlots
}} }}
</ElTable> </ElTable>

View File

@ -91,5 +91,7 @@ export interface TableProps extends Omit<Partial<ElTableProps<any[]>>, 'data'> {
align?: 'left' | 'center' | 'right' align?: 'left' | 'center' | 'right'
// 表头对齐方式 // 表头对齐方式
headerAlign?: 'left' | 'center' | 'right' headerAlign?: 'left' | 'center' | 'right'
preview?: string[]
sortable?: boolean
data?: Recordable data?: Recordable
} }

View File

@ -94,7 +94,9 @@ const toDocument = () => {
<style scoped lang="less"> <style scoped lang="less">
.fade-bottom-enter-active, .fade-bottom-enter-active,
.fade-bottom-leave-active { .fade-bottom-leave-active {
transition: opacity 0.25s, transform 0.3s; transition:
opacity 0.25s,
transform 0.3s;
} }
.fade-bottom-enter-from { .fade-bottom-enter-from {

View File

@ -154,6 +154,11 @@ export const useTable = (config: UseTableConfig) => {
refresh: () => { refresh: () => {
methods.getList() methods.getList()
},
sortableChange: (e: any) => {
const { oldIndex, newIndex } = e
dataList.value.splice(newIndex, 0, dataList.value.splice(oldIndex, 1)[0])
} }
// // 删除数据 // // 删除数据
// delList: async (ids: string[] | number[], multiple: boolean, message = true) => { // delList: async (ids: string[] | number[], multiple: boolean, message = true) => {

View File

@ -158,7 +158,9 @@ export default {
role: 'Role management', role: 'Role management',
document: 'Document', document: 'Document',
inputPassword: 'InputPassword', inputPassword: 'InputPassword',
sticky: 'Sticky' sticky: 'Sticky',
treeTable: 'Tree table',
PicturePreview: 'Table Image Preview'
}, },
permission: { permission: {
hasPermission: 'Please set the operation permission value' hasPermission: 'Please set the operation permission value'
@ -426,7 +428,9 @@ export default {
showOrHiddenStripe: 'Show or hidden stripe', showOrHiddenStripe: 'Show or hidden stripe',
showOrHiddenBorder: 'Show or hidden border', showOrHiddenBorder: 'Show or hidden border',
fixedHeaderOrAuto: 'Fixed header or auto', fixedHeaderOrAuto: 'Fixed header or auto',
getSelections: 'Get selections' getSelections: 'Get selections',
preview: 'Preview',
showOrHiddenSortable: 'Show or hidden sortable'
}, },
richText: { richText: {
richText: 'Rich text', richText: 'Rich text',

View File

@ -158,7 +158,9 @@ export default {
role: '角色管理', role: '角色管理',
document: '文档', document: '文档',
inputPassword: '密码输入框', inputPassword: '密码输入框',
sticky: '黏性' sticky: '黏性',
treeTable: '树形表格',
PicturePreview: '表格图片预览'
}, },
permission: { permission: {
hasPermission: '请设置操作权限值' hasPermission: '请设置操作权限值'
@ -421,7 +423,9 @@ export default {
showOrHiddenStripe: '显示/隐藏斑马纹', showOrHiddenStripe: '显示/隐藏斑马纹',
showOrHiddenBorder: '显示/隐藏边框', showOrHiddenBorder: '显示/隐藏边框',
fixedHeaderOrAuto: '固定头部/自动', fixedHeaderOrAuto: '固定头部/自动',
getSelections: '获取多选数据' getSelections: '获取多选数据',
preview: '封面',
showOrHiddenSortable: '显示/隐藏排序'
}, },
richText: { richText: {
richText: '富文本', richText: '富文本',

View File

@ -190,7 +190,15 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
component: () => import('@/views/Components/Table/TreeTable.vue'), component: () => import('@/views/Components/Table/TreeTable.vue'),
name: 'TreeTable', name: 'TreeTable',
meta: { meta: {
title: 'TreeTable' title: t('router.treeTable')
}
},
{
path: 'table-image-preview',
component: () => import('@/views/Components/Table/TableImagePreview.vue'),
name: 'TableImagePreview',
meta: {
title: t('router.PicturePreview')
} }
} }
] ]

View File

@ -103,3 +103,8 @@ export const isUrl = (path: string): boolean => {
export const isDark = (): boolean => { export const isDark = (): boolean => {
return window.matchMedia('(prefers-color-scheme: dark)').matches return window.matchMedia('(prefers-color-scheme: dark)').matches
} }
// 是否是图片链接
export const isImgPath = (path: string): boolean => {
return /(https?:\/\/|data:image\/).*?\.(png|jpg|jpeg|gif|svg|webp|ico)/gi.test(path)
}

View File

@ -0,0 +1,82 @@
<script setup lang="tsx">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { Table, TableColumn } from '@/components/Table'
import { getTableListApi } from '@/api/table'
import { TableData } from '@/api/table/types'
import { ref } from 'vue'
import { ElTag } from 'element-plus'
interface Params {
pageIndex?: number
pageSize?: number
}
const { t } = useI18n()
const columns: TableColumn[] = [
{
field: 'title',
label: t('tableDemo.title')
},
{
field: 'image_uri',
label: t('tableDemo.preview')
},
{
field: 'author',
label: t('tableDemo.author')
},
{
field: 'display_time',
label: t('tableDemo.displayTime')
},
{
field: 'importance',
label: t('tableDemo.importance'),
formatter: (_: Recordable, __: TableColumn, cellValue: number) => {
return (
<ElTag type={cellValue === 1 ? 'success' : cellValue === 2 ? 'warning' : 'danger'}>
{cellValue === 1
? t('tableDemo.important')
: cellValue === 2
? t('tableDemo.good')
: t('tableDemo.commonly')}
</ElTag>
)
}
},
{
field: 'pageviews',
label: t('tableDemo.pageviews')
}
]
const loading = ref(true)
let tableDataList = ref<TableData[]>([])
const getTableList = async (params?: Params) => {
const res = await getTableListApi(
params || {
pageIndex: 1,
pageSize: 10
}
)
.catch(() => {})
.finally(() => {
loading.value = false
})
if (res) {
tableDataList.value = res.data.list
}
}
getTableList()
</script>
<template>
<ContentWrap :title="t('router.PicturePreview')">
<Table :columns="columns" :data="tableDataList" :loading="loading" :preview="['image_uri']" />
</ContentWrap>
</template>

View File

@ -92,7 +92,7 @@ const actionFn = (data: TableSlotDefault) => {
</script> </script>
<template> <template>
<ContentWrap :title="`TreeTable ${t('tableDemo.example')}`"> <ContentWrap :title="`${t('router.treeTable')} ${t('tableDemo.example')}`">
<Table <Table
v-model:pageSize="pageSize" v-model:pageSize="pageSize"
v-model:currentPage="currentPage" v-model:currentPage="currentPage"

View File

@ -21,7 +21,8 @@ const { tableRegister, tableMethods, tableState } = useTable({
} }
}) })
const { loading, dataList, total, currentPage, pageSize } = tableState const { loading, dataList, total, currentPage, pageSize } = tableState
const { setProps, setColumn, getElTableExpose, addColumn, delColumn, refresh } = tableMethods const { setProps, setColumn, getElTableExpose, addColumn, delColumn, refresh, sortableChange } =
tableMethods
const { t } = useI18n() const { t } = useI18n()
@ -213,6 +214,11 @@ const getSelections = async () => {
const selections = elTableRef?.getSelectionRows() const selections = elTableRef?.getSelectionRows()
console.log(selections) console.log(selections)
} }
const sortable = ref(false)
const showOrHiddenSortable = () => {
sortable.value = !unref(sortable)
}
</script> </script>
<template> <template>
@ -244,6 +250,8 @@ const getSelections = async () => {
<ElButton @click="fixedHeaderOrAuto">{{ t('tableDemo.fixedHeaderOrAuto') }}</ElButton> <ElButton @click="fixedHeaderOrAuto">{{ t('tableDemo.fixedHeaderOrAuto') }}</ElButton>
<ElButton @click="getSelections">{{ t('tableDemo.getSelections') }}</ElButton> <ElButton @click="getSelections">{{ t('tableDemo.getSelections') }}</ElButton>
<ElButton @click="showOrHiddenSortable">{{ t('tableDemo.showOrHiddenSortable') }}</ElButton>
</ContentWrap> </ContentWrap>
<ContentWrap :title="`UseTable ${t('tableDemo.example')}`"> <ContentWrap :title="`UseTable ${t('tableDemo.example')}`">
<Table <Table
@ -253,6 +261,7 @@ const getSelections = async () => {
:columns="columns" :columns="columns"
:data="dataList" :data="dataList"
:loading="loading" :loading="loading"
:sortable="sortable"
:pagination=" :pagination="
canShowPagination canShowPagination
? { ? {
@ -262,6 +271,7 @@ const getSelections = async () => {
" "
@register="tableRegister" @register="tableRegister"
@refresh="refresh" @refresh="refresh"
@sortable-change="sortableChange"
/> />
</ContentWrap> </ContentWrap>
</template> </template>

View File

@ -10,7 +10,6 @@ import { viteMockServe } from 'vite-plugin-mock'
import PurgeIcons from 'vite-plugin-purge-icons' import PurgeIcons from 'vite-plugin-purge-icons'
import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite" import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite"
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
// @ts-expect-error
import DefineOptions from "unplugin-vue-define-options/vite" import DefineOptions from "unplugin-vue-define-options/vite"
import { createStyleImportPlugin, ElementPlusResolve } from 'vite-plugin-style-import' import { createStyleImportPlugin, ElementPlusResolve } from 'vite-plugin-style-import'
import UnoCSS from 'unocss/vite' import UnoCSS from 'unocss/vite'
@ -50,10 +49,10 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
} }
}] }]
}), }),
// EslintPlugin({ EslintPlugin({
// cache: false, cache: false,
// include: ['src/**/*.vue', 'src/**/*.ts', 'src/**/*.tsx'] // 检查的文件 include: ['src/**/*.vue', 'src/**/*.ts', 'src/**/*.tsx'] // 检查的文件
// }), }),
VueI18nPlugin({ VueI18nPlugin({
runtimeOnly: true, runtimeOnly: true,
compositionOnly: true, compositionOnly: true,