feat: 用户列表重构

This commit is contained in:
kailong321200875 2023-07-23 19:34:09 +08:00
parent 9a0259de5c
commit 755cea0990
14 changed files with 355 additions and 318 deletions

105
mock/department/index.ts Normal file
View File

@ -0,0 +1,105 @@
import config from '@/config/axios/config'
import { MockMethod } from 'vite-plugin-mock'
import { toAnyString } from '@/utils'
import Mock from 'mockjs'
const { code } = config
const departmentList: any = []
const citys = ['厦门总公司', '北京分公司', '上海分公司', '福州分公司', '深圳分公司', '杭州分公司']
for (let i = 0; i < 5; i++) {
departmentList.push({
// 部门名称
departmentName: citys[i],
id: toAnyString(),
children: [
{
// 部门名称
departmentName: '研发部',
id: toAnyString()
},
{
// 部门名称
departmentName: '产品部',
id: toAnyString()
},
{
// 部门名称
departmentName: '运营部',
id: toAnyString()
},
{
// 部门名称
departmentName: '市场部',
id: toAnyString()
},
{
// 部门名称
departmentName: '销售部',
id: toAnyString()
},
{
// 部门名称
departmentName: '客服部',
id: toAnyString()
}
]
})
}
export default [
// 列表接口
{
url: '/department/list',
method: 'get',
response: () => {
return {
data: {
code: code,
data: {
list: departmentList
}
}
}
}
},
{
url: '/department/users',
method: 'get',
timeout: 1000,
response: ({ query }) => {
const { pageSize } = query
// 根据pageSize来创建数据
const mockList: any = []
for (let i = 0; i < pageSize; i++) {
mockList.push(
Mock.mock({
// 用户名
username: '@cname',
// 账号
account: '@first',
// 邮箱
email: '@EMAIL',
// 创建时间
createTime: '@datetime',
// 角色
role: '@first',
// 用户id
id: toAnyString()
})
)
}
return {
data: {
code: code,
data: {
total: 100,
list: mockList
}
}
}
}
}
] as MockMethod[]

View File

@ -0,0 +1,10 @@
import request from '@/config/axios'
import { DepartmentListResponse, DepartmentUserParams, DepartmentUserResponse } from './types'
export const getDepartmentApi = () => {
return request.get<DepartmentListResponse>({ url: '/department/list' })
}
export const getUserByIdApi = (params: DepartmentUserParams) => {
return request.get<DepartmentUserResponse>({ url: '/department/users', params })
}

View File

@ -0,0 +1,31 @@
export interface DepartmentItem {
id: string
departmentName: string
children?: DepartmentItem[]
}
export interface DepartmentListResponse {
list: DepartmentItem[]
}
export interface DepartmentUserParams {
pageSize: number
pageIndex: number
id: string
username?: string
account?: string
}
export interface DepartmentUserItem {
id: string
username: string
account: string
email: string
createTime: string
role: string
}
export interface DepartmentUserResponse {
list: DepartmentUserItem[]
total: number
}

View File

@ -255,7 +255,7 @@ export default defineComponent({
const renderFormItem = (item: FormSchema) => { const renderFormItem = (item: FormSchema) => {
// optionApi使optionApi // optionApi使optionApi
if (item.optionApi) { if (item.optionApi) {
// //
getOptions(item.optionApi, item) getOptions(item.optionApi, item)
} }
const formItemSlots: Recordable = { const formItemSlots: Recordable = {

View File

@ -1,3 +0,0 @@
import Sticky from './src/Sticky.vue'
export { Sticky }

View File

@ -1,141 +0,0 @@
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes'
import { ref, onMounted, onActivated, shallowRef } from 'vue'
import { useEventListener, useWindowSize, isClient } from '@vueuse/core'
import type { CSSProperties } from 'vue'
const props = defineProps({
// (px)
offset: propTypes.number.def(0),
//
zIndex: propTypes.number.def(999),
// class
className: propTypes.string.def(''),
// (top)topbottom
position: {
type: String,
validator: function (value: string) {
return ['top', 'bottom'].indexOf(value) !== -1
},
default: 'top'
}
})
const width = ref('auto' as string)
const height = ref('auto' as string)
const isSticky = ref(false)
const refSticky = shallowRef<HTMLElement>()
const scrollContainer = shallowRef<HTMLElement | Window>()
const { height: windowHeight } = useWindowSize()
onMounted(() => {
height.value = refSticky.value?.getBoundingClientRect().height + 'px'
scrollContainer.value = getScrollContainer(refSticky.value!, true)
useEventListener(scrollContainer, 'scroll', handleScroll)
useEventListener('resize', handleReize)
handleScroll()
})
onActivated(() => {
handleScroll()
})
const camelize = (str: string): string => {
return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
}
const getStyle = (element: HTMLElement, styleName: keyof CSSProperties): string => {
if (!isClient || !element || !styleName) return ''
let key = camelize(styleName)
if (key === 'float') key = 'cssFloat'
try {
const style = element.style[styleName]
if (style) return style
const computed = document.defaultView?.getComputedStyle(element, '')
return computed ? computed[styleName] : ''
} catch {
return element.style[styleName]
}
}
const isScroll = (el: HTMLElement, isVertical?: boolean): boolean => {
if (!isClient) return false
const key = (
{
undefined: 'overflow',
true: 'overflow-y',
false: 'overflow-x'
} as const
)[String(isVertical)]!
const overflow = getStyle(el, key)
return ['scroll', 'auto', 'overlay'].some((s) => overflow.includes(s))
}
const getScrollContainer = (
el: HTMLElement,
isVertical: boolean
): Window | HTMLElement | undefined => {
if (!isClient) return
let parent = el
while (parent) {
if ([window, document, document.documentElement].includes(parent)) return window
if (isScroll(parent, isVertical)) return parent
parent = parent.parentNode as HTMLElement
}
return parent
}
const handleScroll = () => {
width.value = refSticky.value!.getBoundingClientRect().width! + 'px'
if (props.position === 'top') {
const offsetTop = refSticky.value?.getBoundingClientRect().top
if (offsetTop !== undefined && offsetTop < props.offset) {
sticky()
return
}
reset()
} else {
const offsetBottom = refSticky.value?.getBoundingClientRect().bottom
if (offsetBottom !== undefined && offsetBottom > windowHeight.value - props.offset) {
sticky()
return
}
reset()
}
}
const handleReize = () => {
if (isSticky.value && refSticky.value) {
width.value = refSticky.value.getBoundingClientRect().width + 'px'
}
}
const sticky = () => {
if (isSticky.value) {
return
}
isSticky.value = true
}
const reset = () => {
if (!isSticky.value) {
return
}
width.value = 'auto'
isSticky.value = false
}
</script>
<template>
<div :style="{ height: height, zIndex: zIndex }" ref="refSticky">
<div
:class="className"
:style="{
top: position === 'top' ? offset + 'px' : '',
bottom: position !== 'top' ? offset + 'px' : '',
zIndex: zIndex,
position: isSticky ? 'fixed' : 'static',
width: width,
height: height
}"
>
<slot>
<div>sticky</div>
</slot>
</div>
</div>
</template>

View File

@ -75,7 +75,7 @@ const closeAllTags = () => {
toLastView() toLastView()
} }
// //
const closeOthersTags = () => { const closeOthersTags = () => {
tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded) tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
} }
@ -482,7 +482,8 @@ watch(
&__tool { &__tool {
position: relative; position: relative;
&:before {
&::before {
position: absolute; position: absolute;
top: 1px; top: 1px;
left: 0; left: 0;
@ -493,14 +494,14 @@ watch(
} }
&--first { &--first {
&:before { &::before {
position: absolute; position: absolute;
top: 1px; top: 1px;
left: 0; left: 0;
width: 100%; width: 100%;
height: calc(~'100% - 1px'); height: calc(~'100% - 1px');
border-left: none;
border-right: 1px solid var(--el-border-color); border-right: 1px solid var(--el-border-color);
border-left: none;
content: ''; content: '';
} }
} }
@ -553,7 +554,7 @@ watch(
.@{prefix-cls} { .@{prefix-cls} {
&__tool { &__tool {
&--first { &--first {
&:after { &::after {
display: none; display: none;
} }
} }

View File

@ -487,7 +487,14 @@ export default {
role: 'Role', role: 'Role',
remark: 'Remark', remark: 'Remark',
remarkMessage1: 'Back end control routing permission', remarkMessage1: 'Back end control routing permission',
remarkMessage2: 'Front end control routing permission' remarkMessage2: 'Front end control routing permission',
// 部门列表
departmentList: 'Department list',
// 搜索部门
searchDepartment: 'Search department',
account: 'Account',
email: 'Email',
createTime: 'Create time'
}, },
inputPasswordDemo: { inputPasswordDemo: {
title: 'InputPassword', title: 'InputPassword',

View File

@ -17,7 +17,7 @@ export default {
closeTab: '关闭标签页', closeTab: '关闭标签页',
closeTheLeftTab: '关闭左侧标签页', closeTheLeftTab: '关闭左侧标签页',
closeTheRightTab: '关闭右侧标签页', closeTheRightTab: '关闭右侧标签页',
closeOther: '关闭其标签页', closeOther: '关闭其标签页',
closeAll: '关闭全部标签页', closeAll: '关闭全部标签页',
prevLabel: '上一步', prevLabel: '上一步',
nextLabel: '下一步', nextLabel: '下一步',
@ -106,7 +106,7 @@ export default {
register: '注册', register: '注册',
checkPassword: '确认密码', checkPassword: '确认密码',
login: '登录', login: '登录',
otherLogin: '其登录方式', otherLogin: '其登录方式',
remember: '记住我', remember: '记住我',
hasUser: '已有账号?去登录', hasUser: '已有账号?去登录',
forgetPassword: '忘记密码', forgetPassword: '忘记密码',
@ -480,7 +480,13 @@ export default {
role: '角色', role: '角色',
remark: '备注', remark: '备注',
remarkMessage1: '后端控制路由权限', remarkMessage1: '后端控制路由权限',
remarkMessage2: '前端控制路由权限' remarkMessage2: '前端控制路由权限',
// 部门列表
departmentList: '部门列表',
searchDepartment: '搜索部门',
account: '账号',
email: '邮箱',
createTime: '创建时间'
}, },
inputPasswordDemo: { inputPasswordDemo: {
title: '密码输入框', title: '密码输入框',

View File

@ -311,14 +311,6 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
title: t('router.inputPassword') title: t('router.inputPassword')
} }
} }
// {
// path: 'sticky',
// component: () => import('@/views/Components/Sticky.vue'),
// name: 'Sticky',
// meta: {
// title: t('router.sticky')
// }
// }
] ]
}, },
{ {
@ -339,15 +331,15 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
meta: { meta: {
title: 'useWatermark' title: 'useWatermark'
} }
},
{
path: 'useCrudSchemas',
component: () => import('@/views/hooks/useCrudSchemas.vue'),
name: 'UseCrudSchemas',
meta: {
title: 'useCrudSchemas'
}
} }
// {
// path: 'useCrudSchemas',
// component: () => import('@/views/hooks/useCrudSchemas.vue'),
// name: 'UseCrudSchemas',
// meta: {
// title: 'useCrudSchemas'
// }
// }
] ]
}, },
{ {
@ -513,36 +505,36 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
} }
} }
] ]
},
{
path: '/authorization',
component: Layout,
redirect: '/authorization/user',
name: 'Authorization',
meta: {
title: t('router.authorization'),
icon: 'eos-icons:role-binding',
alwaysShow: true
},
children: [
{
path: 'user',
component: () => import('@/views/Authorization/User.vue'),
name: 'User',
meta: {
title: t('router.user')
}
},
{
path: 'role',
component: () => import('@/views/Authorization/Role.vue'),
name: 'Role',
meta: {
title: t('router.role')
}
}
]
} }
// {
// path: '/authorization',
// component: Layout,
// redirect: '/authorization/user',
// name: 'Authorization',
// meta: {
// title: t('router.authorization'),
// icon: 'eos-icons:role-binding',
// alwaysShow: true
// },
// children: [
// {
// path: 'user',
// component: () => import('@/views/Authorization/User.vue'),
// name: 'User',
// meta: {
// title: t('router.user')
// }
// },
// {
// path: 'role',
// component: () => import('@/views/Authorization/Role.vue'),
// name: 'Role',
// meta: {
// title: t('router.role')
// }
// }
// ]
// }
] ]
const router = createRouter({ const router = createRouter({

View File

@ -37,7 +37,7 @@ interface AppState {
export const useAppStore = defineStore('app', { export const useAppStore = defineStore('app', {
state: (): AppState => { state: (): AppState => {
return { return {
userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其项目冲突 userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其项目冲突
sizeMap: ['default', 'large', 'small'], sizeMap: ['default', 'large', 'small'],
mobile: false, // 是否是移动端 mobile: false, // 是否是移动端
title: import.meta.env.VITE_APP_TITLE, // 标题 title: import.meta.env.VITE_APP_TITLE, // 标题
@ -225,7 +225,7 @@ export const useAppStore = defineStore('app', {
}, },
setLayout(layout: LayoutType) { setLayout(layout: LayoutType) {
if (this.mobile && layout !== 'classic') { if (this.mobile && layout !== 'classic') {
ElMessage.warning('移动端模式下不支持切换其布局') ElMessage.warning('移动端模式下不支持切换其布局')
return return
} }
this.layout = layout this.layout = layout

View File

@ -87,12 +87,12 @@ export const useTagsViewStore = defineStore('tagsView', {
// const affixTags = this.visitedViews.filter((tag) => tag.meta.affix) // const affixTags = this.visitedViews.filter((tag) => tag.meta.affix)
this.visitedViews = [] this.visitedViews = []
}, },
// 删除其 // 删除其
delOthersViews(view: RouteLocationNormalizedLoaded) { delOthersViews(view: RouteLocationNormalizedLoaded) {
this.delOthersVisitedViews(view) this.delOthersVisitedViews(view)
this.addCachedView() this.addCachedView()
}, },
// 删除其tag // 删除其tag
delOthersVisitedViews(view: RouteLocationNormalizedLoaded) { delOthersVisitedViews(view: RouteLocationNormalizedLoaded) {
this.visitedViews = this.visitedViews.filter((v) => { this.visitedViews = this.visitedViews.filter((v) => {
return v?.meta?.affix || v.path === view.path return v?.meta?.affix || v.path === view.path

View File

@ -1,20 +1,35 @@
<script setup lang="ts"> <script setup lang="tsx">
import { ContentWrap } from '@/components/ContentWrap' import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { Table } from '@/components/Table' import { Table, TableColumn } from '@/components/Table'
import { getUserListApi } from '@/api/login' import { ref, unref, nextTick, watch } from 'vue'
import { UserType } from '@/api/login/types' import { ElButton, ElTree, ElInput, ElDivider } from 'element-plus'
import { ref, h } from 'vue' import { getDepartmentApi, getUserByIdApi } from '@/api/department'
import { ElButton } from 'element-plus' import type { DepartmentItem, DepartmentUserItem } from '@/api/department/types'
import { TableColumn, TableSlotDefault } from '@/types/table' import { useTable } from '@/hooks/web/useTable'
import { Search } from '@/components/Search'
interface Params { import { FormSchema } from '@/components/Form'
pageIndex?: number
pageSize?: number
}
const { t } = useI18n() const { t } = useI18n()
const { tableRegister, tableState, tableMethods } = useTable({
fetchDataApi: async () => {
const { pageSize, currentPage } = tableState
const res = await getUserByIdApi({
id: unref(currentNodeKey),
pageIndex: unref(currentPage),
pageSize: unref(pageSize),
...unref(searchParams)
})
return {
list: res.data.list || [],
total: res.data.total || 0
}
}
})
const { total, loading, dataList, pageSize, currentPage } = tableState
const { getList } = tableMethods
const columns: TableColumn[] = [ const columns: TableColumn[] = [
{ {
field: 'index', field: 'index',
@ -26,65 +41,141 @@ const columns: TableColumn[] = [
label: t('userDemo.username') label: t('userDemo.username')
}, },
{ {
field: 'password', field: 'account',
label: t('userDemo.password') label: t('userDemo.account')
}, },
{ {
field: 'role', field: 'role',
label: t('userDemo.role') label: t('userDemo.role')
}, },
{ {
field: 'remark', field: 'email',
label: t('userDemo.remark'), label: t('userDemo.email')
formatter: (row: UserType) => { },
return h( {
'span', field: 'createTime',
row.username === 'admin' ? t('userDemo.remarkMessage1') : t('userDemo.remarkMessage2') label: t('userDemo.createTime')
)
}
}, },
{ {
field: 'action', field: 'action',
label: t('userDemo.action') label: t('userDemo.action'),
slots: {
default: (data) => {
return (
<ElButton type="primary" onClick={() => actionFn(data[0].row)}>
{t('tableDemo.action')}
</ElButton>
)
}
}
} }
] ]
const loading = ref(true) const searchSchema: FormSchema[] = [
{
let tableDataList = ref<UserType[]>([]) field: 'username',
label: t('userDemo.username'),
const getTableList = async (params?: Params) => { component: 'Input'
const res = await getUserListApi({ },
params: params || { {
pageIndex: 1, field: 'account',
pageSize: 10 label: t('userDemo.account'),
} component: 'Input'
})
// .catch(() => {})
// .finally(() => {
// loading.value = false
// })
if (res) {
tableDataList.value = res.data.list
loading.value = false
} }
]
const searchParams = ref({})
const setSearchParams = (params: any) => {
currentPage.value = 1
searchParams.value = params
getList()
} }
getTableList() const actionFn = (data: DepartmentUserItem) => {
const actionFn = (data: TableSlotDefault) => {
console.log(data) console.log(data)
} }
const treeEl = ref<typeof ElTree>()
const currentNodeKey = ref('')
const departmentList = ref<DepartmentItem[]>([])
const fetchDepartment = async () => {
const res = await getDepartmentApi()
departmentList.value = res.data.list
currentNodeKey.value =
(res.data.list[0] && res.data.list[0]?.children && res.data.list[0].children[0].id) || ''
await nextTick()
unref(treeEl)?.setCurrentKey(currentNodeKey.value)
}
fetchDepartment()
const currentDepartment = ref('')
watch(
() => currentDepartment.value,
(val) => {
unref(treeEl)!.filter(val)
}
)
const currentChange = (data: DepartmentItem) => {
if (data.children) return
currentNodeKey.value = data.id
currentPage.value = 1
getList()
}
const filterNode = (value: string, data: DepartmentItem) => {
if (!value) return true
return data.departmentName.includes(value)
}
</script> </script>
<template> <template>
<ContentWrap :title="t('userDemo.title')" :message="t('userDemo.message')"> <div class="flex w-100% h-100%">
<Table :columns="columns" :data="tableDataList" :loading="loading" :selection="false"> <ContentWrap class="flex-1">
<template #action="data"> <div class="flex justify-center items-center">
<ElButton type="primary" @click="actionFn(data as TableSlotDefault)"> <div class="flex-1">{{ t('userDemo.departmentList') }}</div>
{{ t('tableDemo.action') }} <ElInput
</ElButton> v-model="currentDepartment"
</template> class="flex-[2]"
</Table> :placeholder="t('userDemo.searchDepartment')"
</ContentWrap> clearable
/>
</div>
<ElDivider />
<ElTree
ref="treeEl"
:data="departmentList"
default-expand-all
node-key="id"
:current-node-key="currentNodeKey"
:props="{
label: 'departmentName'
}"
:filter-node-method="filterNode"
@current-change="currentChange"
/>
</ContentWrap>
<ContentWrap class="flex-[2] ml-20px">
<Search
:schema="searchSchema"
@reset="setSearchParams"
@search="setSearchParams"
:search-loading="loading"
/>
<div>
<Table
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:columns="columns"
:data="dataList"
:loading="loading"
@register="tableRegister"
:pagination="{
total
}"
/>
</div>
</ContentWrap>
</div>
</template> </template>

View File

@ -1,62 +0,0 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { Sticky } from '@/components/Sticky'
import { ElAffix } from 'element-plus'
const { t } = useI18n()
</script>
<template>
<ContentWrap :title="t('stickyDemo.sticky')">
<Sticky :offset="90">
<div style="padding: 10px; background-color: lightblue"> Sticky 距离顶部90px </div>
</Sticky>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<el-affix :offset="150">
<div style="padding: 10px; background-color: lightblue">Affix 距离顶部150px </div>
</el-affix>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<el-affix :offset="150" position="bottom">
<div style="padding: 10px; background-color: lightblue">Affix 距离底部150px </div>
</el-affix>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<Sticky :offset="90" position="bottom">
<div style="padding: 10px; background-color: lightblue"> Sticky 距离底部90px </div>
</Sticky>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
</ContentWrap>
</template>