Browse Source

data-table

master
zhangtao 2 years ago
parent
commit
590ddc8076
13 changed files with 704 additions and 44 deletions
  1. +7
    -1
      mock/system/index.js
  2. +1
    -0
      package.json
  3. +58
    -7
      src/components/DataTable/index.vue
  4. +39
    -0
      src/components/DataTable/tools/props.js
  5. +111
    -0
      src/components/DataTable/tools/useDataSource.js
  6. +37
    -0
      src/components/DataTable/tools/usePagination.js
  7. +220
    -22
      src/layout/components/Tags/index.vue
  8. +2
    -2
      src/router/guard/permission-guard.js
  9. +23
    -1
      src/router/routes/index.js
  10. +10
    -0
      src/utils/index.js
  11. +20
    -0
      src/views/redirect/index.vue
  12. +10
    -9
      src/views/system/menu/index.vue
  13. +166
    -2
      src/views/system/role/index.vue

+ 7
- 1
mock/system/index.js View File

@@ -82,7 +82,13 @@ export default [
return true
})
const List = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1))
return resultSuccess(List)
const data = {
list: List,
page: Number(page),
limit: Number(limit),
total: count
}
return resultSuccess(data)
}
}
]

+ 1
- 0
package.json View File

@@ -12,6 +12,7 @@
"@vicons/antd": "^0.10.0",
"@vicons/ionicons5": "^0.10.0",
"axios": "^0.26.1",
"dayjs": "^1.11.0",
"mockjs": "^1.1.0",
"pinia": "^2.0.13",
"vue": "^3.2.16",

+ 58
- 7
src/components/DataTable/index.vue View File

@@ -13,27 +13,78 @@
</div>
</div>
<div class="s-table">
<n-data-table v-bind="getBindProps" />
<n-data-table
ref="tableElRef"
v-bind="getBindProps"
:pagination="pagination"
@update:page="updatePage"
@update:page-size="updatePageSize"
/>
</div>
</template>

<script>
import { NDataTable } from 'naive-ui'
import { unref, computed } from 'vue'
import { tableProps } from './tools/props.js'
import { useDataSource } from './tools/useDataSource.js'
import { usePagination } from './tools/usePagination.js'
import { ref, unref, computed, toRaw, provide } from 'vue'
export default {
name: 'DataTable',
props: {
...NDataTable.props
...tableProps
},
setup(props, { emit }) {
const getProps = computed(() => {
return { ...props }
})

/* loading--start */
const loadingRef = ref(unref(getProps).loading)
const getLoading = computed(() => unref(loadingRef))
function setLoading(loading) {
loadingRef.value = loading
}
/* loading--end */

/* pagination-start */
const { getPaginationInfo, setPagination } = usePagination(getProps)
const pagination = computed(() => toRaw(unref(getPaginationInfo)))

/* 页码切换 */
function updatePage(page) {
setPagination({ page: page })
reload()
}

/* 分页数量切换 */
function updatePageSize(size) {
setPagination({ page: 1, pageSize: size })
reload()
}
/* pagination-end */

/* tableData-start */
const tableData = ref([])
const { getDataSourceRef, getRowKey, reload } = useDataSource(getProps, { getPaginationInfo, setPagination, tableData, setLoading }, emit)
const getBindProps = computed(() => {
return {
...unref(props)
...unref(getProps),
loading: unref(getLoading),
rowKey: unref(getRowKey),
data: unref(getDataSourceRef),
remote: true
}
})
console.log(getBindProps)

const key = Symbol('s-table')
provide(key, { getBindProps })
/* tableData-end */

return {
getBindProps
getBindProps,
pagination,
updatePage,
updatePageSize
}
}
}

+ 39
- 0
src/components/DataTable/tools/props.js View File

@@ -0,0 +1,39 @@
import { NDataTable } from 'naive-ui'

export const tableProps = {
...NDataTable.props,
/* 初始化接口请求 */
request: {
type: Function,
default: null
},
/* 分页信息 */
pagination: {
type: [Object, Boolean],
default: () => {}
},
/* 分页设置信息 */
paginationSetting: {
type: Object,
default: () => {
return {
// 当前页的字段名
pageField: 'page',
// 每页数量字段名
sizeField: 'limit',
// 接口返回的数据字段名
listField: 'list',
// 接口返回总页数字段名
totalField: 'total',
// 默认分页数量
pageSize: 10,
// 可切换每页数量集合
pageSizes: [10, 20, 30, 40, 50],
// 是否显示每页条数的选择器
showSizePicker: false,
// 是否显示快速跳转
showQuickJumper: false
}
}
}
}

+ 111
- 0
src/components/DataTable/tools/useDataSource.js View File

@@ -0,0 +1,111 @@
import { ref, unref, computed, onMounted } from 'vue'
import { isBoolean } from '@/utils/is'

export function useDataSource(propsRef, { getPaginationInfo, setPagination, setLoading, tableData }, emit) {
const dataSourceRef = ref([])

async function fetch(opt) {
try {
/* 设置loading */
setLoading(true)
const { request, pagination, paginationSetting } = unref(propsRef)
/* 无接口请求中断 */
if (!request) return
/* 获取分页信息 */
const pageField = paginationSetting.pageField
const sizeField = paginationSetting.sizeField
const totalField = paginationSetting.totalField
const listField = paginationSetting.listField

let pageParams = {}
const { page = 1, pageSize = 10 } = unref(getPaginationInfo)
/* 判断是否需要分页信息 */
if ((isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)) {
pageParams = {}
} else {
pageParams[pageField] = (opt && opt[pageField]) || page
pageParams[sizeField] = pageSize
}
const params = {
...pageParams
}
const response = await request(params)
const res = response.data
const resultTotal = res[totalField] || 0
const currentPage = res[pageField]
// 如果数据异常,需获取正确的页码再次执行
if (resultTotal) {
if (page > Math.ceil(resultTotal / pageSize)) {
setPagination({
[pageField]: Math.ceil(resultTotal / pageSize)
})
fetch(opt)
}
}
const resultInfo = res[listField] ? res[listField] : []
dataSourceRef.value = resultInfo
setPagination({
[pageField]: currentPage,
[totalField]: Math.ceil(resultTotal / pageSize)
})
/* 更新页码数据 */
if (opt && opt[pageField]) {
setPagination({
[pageField]: opt[pageField] || 1
})
}
emit('fetch-success', {
items: unref(resultInfo),
resultTotal
})
} catch (error) {
console.error(error)
// emit('fetch-error', error)
dataSourceRef.value = []
} finally {
setLoading(false)
}
}

const getDataSourceRef = computed(() => {
const dataSource = unref(dataSourceRef)
if (!dataSource || dataSource.length === 0) {
return unref(dataSourceRef)
}
return unref(dataSourceRef)
})

function getDataSource() {
return getDataSourceRef.value
}

function setTableData(values) {
dataSourceRef.value = values
}

const getRowKey = computed(() => {
const { rowKey } = unref(propsRef)
return rowKey || (() => {
return 'key'
})
})

async function reload(opt) {
await fetch(opt)
}

onMounted(() => {
setTimeout(() => {
fetch()
}, 15)
})

return {
fetch,
getDataSourceRef,
getDataSource,
setTableData,
getRowKey,
reload
}
}

+ 37
- 0
src/components/DataTable/tools/usePagination.js View File

@@ -0,0 +1,37 @@
import { computed, unref, ref } from 'vue'
import { isBoolean } from '@/utils/is'

export function usePagination(refProps) {
const configRef = ref({})
const getPaginationInfo = computed(() => {
const { pagination, paginationSetting } = unref(refProps)
/* 判断是否需要展示分页 */
if ((isBoolean(pagination) && !pagination)) {
return false
}
/* 返回配置的分页信息 */
return {
pageSize: paginationSetting.pageSize,
pageSizes: paginationSetting.pageSizes,
showSizePicker: paginationSetting.showSizePicker,
showQuickJumper: paginationSetting.showQuickJumper,
...(isBoolean(pagination) ? {} : pagination),
...unref(configRef),
pageCount: unref(configRef)[paginationSetting.totalField]
}
})

function setPagination(info) {
const paginationInfo = unref(getPaginationInfo)
configRef.value = {
...(!isBoolean(paginationInfo) ? paginationInfo : {}),
...info
}
}

function getPagination() {
return unref(getPaginationInfo)
}

return { getPaginationInfo, setPagination, getPagination }
}

+ 220
- 22
src/layout/components/Tags/index.vue View File

@@ -1,13 +1,13 @@
<template>
<div class="tabs-view" :class="{'tabs-view-fix': tagsMenuSetting.fixed,}">
<div class="tabs-view" :class="{'tabs-view-fix': tagsMenuSetting.fixed,}" :style="getChangeStyle">
<div class="tabs-view-main">
<div ref="navWrap" class="tabs-card" :class="{ 'tabs-card-scrollable': scrollable }">
<span class="tabs-card-prev" :class="{ 'tabs-card-prev-hide': !scrollable }" @click="scrollPrev">
<div ref="navWrap" class="tabs-card" :class="{ 'tabs-card-scrollable': state.scrollable }">
<span class="tabs-card-prev" :class="{ 'tabs-card-prev-hide': !state.scrollable }" @click="scrollPrev">
<n-icon size="16" color="#515a6e">
<LeftOutlined />
</n-icon>
</span>
<span class="tabs-card-next" :class="{ 'tabs-card-next-hide': !scrollable }" @click="scrollNext">
<span class="tabs-card-next" :class="{ 'tabs-card-next-hide': !state.scrollable }" @click="scrollNext">
<n-icon size="16" color="#515a6e">
<RightOutlined />
</n-icon>
@@ -31,6 +31,31 @@
</Draggable>
</div>
</div>

<div class="tabs-close">
<n-dropdown
trigger="hover"
placement="bottom-end"
:options="tagsMemuOptions"
@select="closeHandleSelect"
>
<div class="tabs-close-btn">
<n-icon size="16" color="#515a6e">
<DownOutlined />
</n-icon>
</div>
</n-dropdown>
</div>

<n-dropdown
:show="state.showDropdown"
:x="state.dropdownX"
:y="state.dropdownY"
placement="bottom-start"
:options="tagsMemuOptions"
@clickoutside="onClickOutside"
@select="closeHandleSelect"
/>
</div>
</div>
</template>
@@ -39,26 +64,59 @@
import {
LeftOutlined,
RightOutlined,
CloseOutlined
CloseOutlined,
DownOutlined,
ReloadOutlined,
ColumnWidthOutlined,
MinusOutlined
} from '@vicons/antd'
import Draggable from 'vuedraggable'
import { reactive, computed, watch } from 'vue'
import { reactive, computed, watch, defineProps, ref, unref, provide, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTagsMenuStore } from '@/store/modules/tagsMenu.js'
import { useSettingStore } from '@/store/modules/setting.js'
import { getTags, setTags } from '@/utils/tags.js'
import { isString } from '@/utils/is.js'
import { renderIcon } from '@/utils'

const props = defineProps({
collapsed: {
type: Boolean
},
menuMode: {
type: String,
default: 'sidebar'
}
})

/* 白名单 */
const whiteList = ['Login', 'NOT_FOUND']
/* 基础页面 */
const baseMenu = '/home'

/* 获取路由器 */
const route = useRoute()
const router = useRouter()
const { push, replace } = router

/* 使用store数据 */
const tagsMenuStore = useTagsMenuStore()
const settingStore = useSettingStore()

const tabsList = computed(() => tagsMenuStore.tabsList)
const tagsMenuSetting = computed(() => settingStore.getTagsMenuSetting)

/* 组装样式 */
const getChangeStyle = computed(() => {
const { collapsed, menuMode } = props
const { cWidth, width } = unref(settingStore.getSidebarSetting)
const { fixed } = unref(settingStore.getTagsMenuSetting)
const lenNum = menuMode === 'header' ? '0px' : collapsed ? `${cWidth}px` : `${width}px`
return {
left: lenNum,
width: `calc(100% - ${!fixed ? '0px' : lenNum})`
}
})

let cacheRoutes = []
const simpleRoute = getSimpleRoute(route)
try {
@@ -82,13 +140,15 @@ tagsMenuStore.initTabs(cacheRoutes)

const state = reactive({
activeKey: route.fullPath,
scrollable: false
scrollable: false,
showDropdown: false,
dropdownX: 0,
dropdownY: 0
})

watch(
() => route.fullPath,
(to) => {
console.log('to', to)
// if (whiteList.includes(route.name)) return
state.activeKey = to
tagsMenuStore.addTabs(getSimpleRoute(route))
@@ -143,27 +203,147 @@ function goPage(e) {
* @description: 删除项
* @return {*}
*/
// 删除tab
function closeTabItem(e) {
// const { fullPath } = e
// const routeInfo = tabsList.value.find((item) => item.fullPath == fullPath)
// removeTab(routeInfo)
const { fullPath } = e
const routeInfo = tabsList.value.find((item) => item.fullPath === fullPath)
removeTab(routeInfo)
}

/**
* @description:
* @return {*}
*/

const removeTab = (route) => {
if (tabsList.value.length === 1) {
// return message.warning('这已经是最后一页,不能再关闭了!')
}
// delKeepAliveCompName()
tagsMenuStore.closeCurrentTab(route)
// 如果关闭的是当前页
if (state.activeKey === route.fullPath) {
const currentRoute = tabsList.value[Math.max(0, tabsList.value.length - 1)]
state.activeKey = currentRoute.fullPath
router.push(currentRoute)
}
// updateNavScroll()
}

// const delKeepAliveCompName = () => {
// if (route.meta.keepAlive) {
// const name = router.currentRoute.value.matched.find((item) => item.name === route.name)
// ?.components?.default.name
// if (name) {
// asyncRouteStore.keepAliveComponents = asyncRouteStore.keepAliveComponents.filter(
// (item) => item != name
// )
// }
// }
// }

/* dropdown--start */
/* 右侧下拉菜单 */
const isCurrent = ref(false)
const tagsMemuOptions = computed(() => {
const isDisabled = unref(tabsList).length <= 1
return [
{
label: '刷新当前',
key: '1',
icon: renderIcon(ReloadOutlined)
},
{
label: `关闭当前`,
key: '2',
disabled: unref(isCurrent) || isDisabled,
icon: renderIcon(CloseOutlined)
},
{
label: '关闭其他',
key: '3',
disabled: isDisabled,
icon: renderIcon(ColumnWidthOutlined)
},
{
label: '关闭全部',
key: '4',
disabled: isDisabled,
icon: renderIcon(MinusOutlined)
}
]
})
/* 操作右侧下拉菜单 */
const closeHandleSelect = (key) => {
switch (key) {
// 刷新
case '1':
reloadPage()
break
// 关闭
case '2':
removeTab(route)
break
// 关闭其他
case '3':
closeOther(route)
break
// 关闭所有
case '4':
closeAll()
break
}
// updateNavScroll()
state.showDropdown = false
}
/* 刷新页面 */
const reloadPage = () => {
// delKeepAliveCompName()
router.push({
path: '/redirect' + unref(route).fullPath
})
}
/* 注入刷新页面方法 */
provide('reloadPage', reloadPage)
/* 关闭其他 */
const closeOther = (route) => {
tagsMenuStore.closeOtherTabs(route)
state.activeKey = route.fullPath
router.replace(route.fullPath)
// updateNavScroll()
}

/* 关闭全部 */
const closeAll = () => {
tagsMenuStore.closeAllTabs()
router.replace(baseMenu)
// updateNavScroll()
}
/* dropdown--end */
/* contextMenu--start */
/**
* @description: 右键菜单
* @return {*}
*/
function handleContextMenu(e, item) {
// e.preventDefault()
// isCurrent.value = PageEnum.BASE_HOME_REDIRECT === item.path
// state.showDropdown = false
// nextTick().then(() => {
// state.showDropdown = true
// state.dropdownX = e.clientX
// state.dropdownY = e.clientY
// })
e.preventDefault()
isCurrent.value = baseMenu === item.path
state.showDropdown = false
nextTick().then(() => {
state.showDropdown = true
state.dropdownX = e.clientX
state.dropdownY = e.clientY
})
}

function onClickOutside() {
state.showDropdown = false
}
/* contextMenu--end */
/**
* @description: 页面跳转
* @param undefined
* @param undefined
* @return {*}
*/
function go(opt, isReplace = false) {
if (!opt) {
return
@@ -258,6 +438,24 @@ function go(opt, isReplace = false) {
}
}
}

.tabs-close {
min-width: 32px;
width: 32px;
height: 32px;
line-height: 32px;
text-align: center;
// background: var(--color);
border-radius: 2px;
cursor: pointer;
.tabs-close-btn {
// color: var(--color);
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
}

.tabs-view-fix {

+ 2
- 2
src/router/guard/permission-guard.js View File

@@ -1,6 +1,6 @@
import { useUserStore } from '@/store/modules/user'
import { usePermissionStore } from '@/store/modules/permission'
import { NOT_FOUND_ROUTE } from '@/router/routes'
import { NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '@/router/routes'
import { getToken } from '@/utils/token'

const WHITE_LIST = ['/login', '/redirect']
@@ -14,7 +14,6 @@ export function createPermissionGuard(router) {
next({ path: '/' })
} else {
const hasRoutes = !!permissionStore.permissionRoutes.length
console.log(permissionStore.permissionRoutes)
if (hasRoutes) {
next()
} else {
@@ -23,6 +22,7 @@ export function createPermissionGuard(router) {
const routes = await permissionStore.generateRoutes()
router.addRoute(routes[0])
router.addRoute(NOT_FOUND_ROUTE)
router.addRoute(REDIRECT_ROUTE)
next({ ...to, replace: true })
} catch (error) {
// removeToken()

+ 23
- 1
src/router/routes/index.js View File

@@ -25,7 +25,8 @@ export const basicRoutes = [
name: 'Home',
component: Home,
meta: {
title: '首页'
title: '首页',
affix: true
}
}
]
@@ -39,6 +40,27 @@ export const NOT_FOUND_ROUTE = {
isHidden: true
}

export const REDIRECT_ROUTE = {
path: '/redirect',
name: 'Redirect',
component: Layout,
meta: {
title: 'Redirect',
hideBreadcrumb: true
},
children: [
{
path: '/redirect/:path(.*)',
name: 'Redirect',
component: () => import('@/views/redirect/index.vue'),
meta: {
title: 'Redirect',
hideBreadcrumb: true
}
}
]
}

const modules = import.meta.globEager('./modules/*.js')
const asyncRoutes = []
Object.keys(modules).forEach((key) => {

+ 10
- 0
src/utils/index.js View File

@@ -1,3 +1,5 @@
import { h } from 'vue'
import { NIcon } from 'naive-ui'
import dayjs from 'dayjs'

/**
@@ -74,3 +76,11 @@ export function debounce(method, wait, immediate) {
}
}
}

/**
* @description: 渲染图标
* @return {*}
*/
export function renderIcon(icon) {
return () => h(NIcon, null, { default: () => h(icon) })
}

+ 20
- 0
src/views/redirect/index.vue View File

@@ -0,0 +1,20 @@
<script>
import { defineComponent, onBeforeMount } from 'vue'
import { useRoute, useRouter } from 'vue-router'

export default defineComponent({
name: 'RedirectPage',
setup() {
const route = useRoute()
const router = useRouter()
onBeforeMount(() => {
const { params, query } = route
const { path } = params
router.replace({
path: '/' + (Array.isArray(path) ? path.join('/') : path),
query
})
})
}
})
</script>

+ 10
- 9
src/views/system/menu/index.vue View File

@@ -1,19 +1,20 @@
<template>
<div>
<data-table :columns="data.columns" :data="data.data" size="large">
<template #tableTitle>
<n-button type="primary">
添加角色
</n-button>
</template>
</data-table>

<n-card>
<data-table :columns="data.columns" :pagination="false" :data="data.data" size="large">
<template #tableTitle>
<n-button type="primary">
添加角色
</n-button>
</template>
</data-table>
</n-card>
</div>
</template>

<script>
import dataTable from '@/components/DataTable/index.vue'
import Action from '@/components/DataTable/tools/action.vue'
import Action from '@/components/DataTable/tools/Action.vue'
import { getMenuList } from '@/api/system/index.js'
import { h, onMounted } from 'vue'
import { reactive } from 'vue'

+ 166
- 2
src/views/system/role/index.vue View File

@@ -1,17 +1,181 @@
<template>
<div>
1
<n-card>
<data-table
:columns="data.columns"
:row-key="(row) => row.id"
:request="loadDataTable"
size="large"
scroll-x="1200"
>
<template #tableTitle>
<n-button type="primary">
新建
</n-button>
<n-button type="primary">
删除
</n-button>
</template>
</data-table>
</n-card>
</div>
</template>

<script>
import dataTable from '@/components/DataTable/index.vue'
import TableAction from '@/components/DataTable/tools/Action.vue'
import TableImage from '@/components/DataTable/tools/Image.vue'
import { getUserList } from '@/api/system/index.js'
import { h, onMounted, unref } from 'vue'
import { reactive } from 'vue'
export default {
name: '',
name: 'MenuPage',
components: { dataTable },
setup() {
const data = reactive({
columns: [
{
title: '用户编号',
key: 'code',
align: 'center'
},
{
title: '头像',
key: 'avatar',
align: 'center',
render(row) {
return h(TableImage, {
images: {
width: 36,
height: 36,
src: row.avatar
}
})
}
},
{
title: '用户账号',
key: 'username',
align: 'center'
},
{
title: '用户姓名',
key: 'realname',
align: 'center'
},
{
title: '用户类型',
key: 'type',
align: 'center',
width: 100
},
{
title: '角色',
key: 'roles',
align: 'center'

},
{
title: '状态',
key: 'status',
align: 'center',
width: 100
},
{
title: '部门',
key: 'deptName',
align: 'center'
},
{
title: '创建时间',
key: 'createTime',
align: 'center',
width: 160
},
{
title: '更新时间',
key: 'updateTime',
align: 'center',
width: 160
},
{
title: '操作',
align: 'center',
width: 150,
fixed: 'right',
render(row) {
return h(TableAction, {
actions: [
{
label: '添加',
type: 'button',
props: {
type: 'primary',
onClick: play.bind(null, row)
},
auth: 'basic_list'
},
{
label: '修改',
auth: 'basic_list'
},
{
label: '删除',
type: 'popconfirm',
auth: 'basic_list'
}
],
align: 'center'
})
}
}
],
data: [],
pagination: {
pageSize: 10
}
})

function play(row) {
console.log(row)
}

/**
* @description: 获取用户数据
* @return {*}
*/
async function fetchList() {
const params = {
page: 1,
limit: 10
}
const res = await getUserList(params)
data.data = res.data
}

const params = reactive({
name: 'xiaoMa'
})

const loadDataTable = async(res) => {
const _params = {
...unref(params),
...res
}
return await getUserList(_params)
}

onMounted(() => {
fetchList()
})

return { data, loadDataTable }
}
}

</script>
<style scoped lang='scss'>
.n-button+.n-button{
margin-left: 10px;
}
</style>

Loading…
Cancel
Save