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

View File

@ -487,7 +487,14 @@ export default {
role: 'Role',
remark: 'Remark',
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: {
title: 'InputPassword',

View File

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

View File

@ -311,14 +311,6 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
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: {
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({

View File

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

View File

@ -87,12 +87,12 @@ export const useTagsViewStore = defineStore('tagsView', {
// const affixTags = this.visitedViews.filter((tag) => tag.meta.affix)
this.visitedViews = []
},
// 删除其
// 删除其
delOthersViews(view: RouteLocationNormalizedLoaded) {
this.delOthersVisitedViews(view)
this.addCachedView()
},
// 删除其tag
// 删除其tag
delOthersVisitedViews(view: RouteLocationNormalizedLoaded) {
this.visitedViews = this.visitedViews.filter((v) => {
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 { useI18n } from '@/hooks/web/useI18n'
import { Table } from '@/components/Table'
import { getUserListApi } from '@/api/login'
import { UserType } from '@/api/login/types'
import { ref, h } from 'vue'
import { ElButton } from 'element-plus'
import { TableColumn, TableSlotDefault } from '@/types/table'
interface Params {
pageIndex?: number
pageSize?: number
}
import { Table, TableColumn } from '@/components/Table'
import { ref, unref, nextTick, watch } from 'vue'
import { ElButton, ElTree, ElInput, ElDivider } from 'element-plus'
import { getDepartmentApi, getUserByIdApi } from '@/api/department'
import type { DepartmentItem, DepartmentUserItem } from '@/api/department/types'
import { useTable } from '@/hooks/web/useTable'
import { Search } from '@/components/Search'
import { FormSchema } from '@/components/Form'
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[] = [
{
field: 'index',
@ -26,65 +41,141 @@ const columns: TableColumn[] = [
label: t('userDemo.username')
},
{
field: 'password',
label: t('userDemo.password')
field: 'account',
label: t('userDemo.account')
},
{
field: 'role',
label: t('userDemo.role')
},
{
field: 'remark',
label: t('userDemo.remark'),
formatter: (row: UserType) => {
return h(
'span',
row.username === 'admin' ? t('userDemo.remarkMessage1') : t('userDemo.remarkMessage2')
)
}
field: 'email',
label: t('userDemo.email')
},
{
field: 'createTime',
label: t('userDemo.createTime')
},
{
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)
let tableDataList = ref<UserType[]>([])
const getTableList = async (params?: Params) => {
const res = await getUserListApi({
params: params || {
pageIndex: 1,
pageSize: 10
}
})
// .catch(() => {})
// .finally(() => {
// loading.value = false
// })
if (res) {
tableDataList.value = res.data.list
loading.value = false
const searchSchema: FormSchema[] = [
{
field: 'username',
label: t('userDemo.username'),
component: 'Input'
},
{
field: 'account',
label: t('userDemo.account'),
component: 'Input'
}
]
const searchParams = ref({})
const setSearchParams = (params: any) => {
currentPage.value = 1
searchParams.value = params
getList()
}
getTableList()
const actionFn = (data: TableSlotDefault) => {
const actionFn = (data: DepartmentUserItem) => {
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>
<template>
<ContentWrap :title="t('userDemo.title')" :message="t('userDemo.message')">
<Table :columns="columns" :data="tableDataList" :loading="loading" :selection="false">
<template #action="data">
<ElButton type="primary" @click="actionFn(data as TableSlotDefault)">
{{ t('tableDemo.action') }}
</ElButton>
</template>
</Table>
</ContentWrap>
<div class="flex w-100% h-100%">
<ContentWrap class="flex-1">
<div class="flex justify-center items-center">
<div class="flex-1">{{ t('userDemo.departmentList') }}</div>
<ElInput
v-model="currentDepartment"
class="flex-[2]"
:placeholder="t('userDemo.searchDepartment')"
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>

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>