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:
push:
branches:
- master
- release
name: Release

View File

@ -147,7 +147,15 @@ const adminList = [
component: 'views/Components/Table/TreeTable',
name: 'TreeTable',
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/use-table',
'/components/table/tree-table',
'/components/table/table-image-preview',
'/components/table/ref-table',
'/components/editor-demo',
'/components/editor-demo/editor',

View File

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

View File

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

View File

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

View File

@ -104,7 +104,9 @@ const getPasswordStrength = computed(() => {
height: inherit;
background-color: transparent;
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'] {
width: 20%;

View File

@ -1,6 +1,13 @@
<script lang="tsx">
import { ElTable, ElTableColumn, ElPagination, ComponentSize, ElTooltipProps } from 'element-plus'
import { defineComponent, PropType, ref, computed, unref, watch, onMounted } from 'vue'
import {
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 { setIndex } from './helper'
import type { TableProps, TableColumn, Pagination, TableSetProps } from './types'
@ -8,6 +15,8 @@ import { set } from 'lodash-es'
import { CSSProperties } from 'vue'
import { getSlot } from '@/utils/tsxHelper'
import TableActions from './components/TableActions.vue'
import Sortable from 'sortablejs'
import { Icon } from '@/components/Icon'
export default defineComponent({
name: 'Table',
@ -48,6 +57,12 @@ export default defineComponent({
type: Array as PropType<Recordable[]>,
default: () => []
},
//
preview: {
type: Array as PropType<string[]>,
default: () => []
},
sortable: propTypes.bool.def(false),
height: propTypes.oneOfType([Number, String]),
maxHeight: propTypes.oneOfType([Number, String]),
stripe: propTypes.bool.def(false),
@ -173,7 +188,7 @@ export default defineComponent({
scrollbarAlwaysOn: 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 }) {
const elTableRef = ref<ComponentRef<typeof ElTable>>()
@ -198,6 +213,33 @@ export default defineComponent({
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 = {}) => {
mergeProps.value = Object.assign(unref(mergeProps), props)
outsideProps.value = { ...props } as any
@ -301,7 +343,7 @@ export default defineComponent({
})
const renderTreeTableColumn = (columnsChildren: TableColumn[]) => {
const { align, headerAlign, showOverflowTooltip } = unref(getProps)
const { align, headerAlign, showOverflowTooltip, preview } = unref(getProps)
return columnsChildren.map((v) => {
if (v.hidden) return null
const props = { ...v } as any
@ -312,12 +354,20 @@ export default defineComponent({
const slots = {
default: (...args: any[]) => {
const data = args[0]
let isImageUrl = false
if (preview.length) {
isImageUrl = preview.some((item) => (item as string) === v.field)
}
return children && children.length
? renderTreeTableColumn(children)
: props?.slots?.default
? props.slots.default(args)
: v?.formatter?.(data.row, data.column, data.row[v.field], data.$index) ||
data.row[v.field]
: v?.formatter
? 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) {
@ -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 {
columns,
@ -347,7 +412,8 @@ export default defineComponent({
align,
headerAlign,
showOverflowTooltip,
reserveSelection
reserveSelection,
preview
} = unref(getProps)
return (columnsChildren || columns).map((v) => {
@ -384,12 +450,21 @@ export default defineComponent({
const slots = {
default: (...args: any[]) => {
const data = args[0]
let isImageUrl = false
if (preview.length) {
isImageUrl = preview.some((item) => (item as string) === v.field)
}
return children && children.length
? renderTreeTableColumn(children)
: props?.slots?.default
? props.slots.default(args)
: v?.formatter?.(data.row, data.column, data.row[v.field], data.$index) ||
data.row[v.field]
: v?.formatter
? 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) {
@ -419,6 +494,21 @@ export default defineComponent({
if (getSlot(slots, 'append')) {
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 (
<div v-loading={unref(getProps).loading}>
{unref(getProps).showAction ? (
@ -426,7 +516,7 @@ export default defineComponent({
) : null}
<ElTable ref={elTableRef} data={unref(getProps).data} {...unref(getBindValue)}>
{{
default: () => renderTableColumn(),
default: () => [sortableEl, ...renderTableColumn()],
...tableSlots
}}
</ElTable>

View File

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

View File

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

View File

@ -154,6 +154,11 @@ export const useTable = (config: UseTableConfig) => {
refresh: () => {
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) => {

View File

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

View File

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

View File

@ -190,7 +190,15 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
component: () => import('@/views/Components/Table/TreeTable.vue'),
name: 'TreeTable',
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 => {
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>
<template>
<ContentWrap :title="`TreeTable ${t('tableDemo.example')}`">
<ContentWrap :title="`${t('router.treeTable')} ${t('tableDemo.example')}`">
<Table
v-model:pageSize="pageSize"
v-model:currentPage="currentPage"

View File

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

View File

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