feat: 拖拽表格
This commit is contained in:
parent
cfb3b3a5ce
commit
b69b8ed1bd
|
@ -1,7 +1,7 @@
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- release
|
||||
|
||||
name: Release
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)')
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: '富文本',
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue