feat: 空间列表分页和搜索及数据列表优化 (#344)

1、fix: 修复因滚动条宽度影响皮肤标签的问题
2、feat: 空间列表分页和搜索及数据列表样式优化
3、feat: 对部分代码进行了优化
This commit is contained in:
Ken 2024-07-15 18:30:50 +08:00 committed by sudoooooo
parent 2044da4be9
commit 7c336e2320
13 changed files with 294 additions and 69 deletions

View File

@ -32,6 +32,7 @@ describe('WorkspaceController', () => {
useValue: {
create: jest.fn(),
findAllById: jest.fn(),
findAllByIdWithPagination: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
@ -145,20 +146,28 @@ describe('WorkspaceController', () => {
jest
.spyOn(workspaceMemberService, 'findAllByUserId')
.mockResolvedValue(memberList as unknown as Array<WorkspaceMember>);
jest
.spyOn(workspaceService, 'findAllById')
.mockResolvedValue(workspaces as Array<Workspace>);
.spyOn(workspaceService, 'findAllByIdWithPagination')
.mockResolvedValue({
list: workspaces as Array<Workspace>,
count: workspaces.length,
});
jest.spyOn(userService, 'getUserListByIds').mockResolvedValue([]);
const result = await controller.findAll(req);
const result = await controller.findAll(req, {curPage:1,pageSize:10});
expect(result.code).toEqual(200);
expect(workspaceMemberService.findAllByUserId).toHaveBeenCalledWith({
userId: req.user._id.toString(),
});
expect(workspaceService.findAllById).toHaveBeenCalledWith({
expect(workspaceService.findAllByIdWithPagination).toHaveBeenCalledWith({
workspaceIdList: memberList.map((item) => item.workspaceId),
page: 1,
limit: 10,
name: undefined
});
});
});

View File

@ -9,6 +9,7 @@ import {
Request,
SetMetadata,
HttpCode,
Query,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import moment from 'moment';
@ -31,6 +32,7 @@ import { splitMembers } from '../utils/splitMember';
import { UserService } from 'src/modules/auth/services/user.service';
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
import { Logger } from 'src/logger';
import { GetWorkspaceListDto } from '../dto/getWorkspaceList.dto';
@ApiTags('workspace')
@ApiBearerAuth()
@ -128,8 +130,21 @@ export class WorkspaceController {
@Get()
@HttpCode(200)
async findAll(@Request() req) {
async findAll(@Request() req, @Query() queryInfo: GetWorkspaceListDto) {
const { value, error } = GetWorkspaceListDto.validate(queryInfo);
if (error) {
this.logger.error(
`GetWorkspaceListDto validate failed: ${error.message}`,
{ req },
);
throw new HttpException(
`参数错误: 请联系管理员`,
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
const userId = req.user._id.toString();
const curPage = Number(value.curPage);
const pageSize = Number(value.pageSize);
// 查询当前用户参与的空间
const workspaceInfoList = await this.workspaceMemberService.findAllByUserId(
{ userId },
@ -139,9 +154,16 @@ export class WorkspaceController {
pre[cur.workspaceId] = cur;
return pre;
}, {});
// 查询当前用户的空间列表
const list = await this.workspaceService.findAllById({ workspaceIdList });
const ownerIdList = list.map((item) => item.ownerId);
const { list, count } =
await this.workspaceService.findAllByIdWithPagination({
workspaceIdList,
page: curPage,
limit: pageSize,
name: queryInfo.name,
});
const ownerIdList = list.map((item: { ownerId: any }) => item.ownerId);
const userList = await this.userService.getUserListByIds({
idList: ownerIdList,
});
@ -150,6 +172,7 @@ export class WorkspaceController {
pre[id] = cur;
return pre;
}, {});
const surveyTotalList = await Promise.all(
workspaceIdList.map((item) => {
return this.surveyMetaService.countSurveyMetaByWorkspaceId({
@ -193,6 +216,7 @@ export class WorkspaceController {
memberTotal: memberTotalMap[workspaceId] || 0,
};
}),
count,
},
};
}

View File

@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import Joi from 'joi';
export class GetWorkspaceListDto {
@ApiProperty({ description: '当前页码', required: true })
curPage: number;
@ApiProperty({ description: '分页', required: false })
pageSize: number;
@ApiProperty({ description: '空间名称', required: false })
name?: string;
static validate(data: Partial<GetWorkspaceListDto>): Joi.ValidationResult {
return Joi.object({
curPage: Joi.number().required(),
pageSize: Joi.number().allow(null).default(10),
name: Joi.string().allow(null, '').optional(),
}).validate(data);
}
}

View File

@ -8,6 +8,17 @@ import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { ObjectId } from 'mongodb';
import { RECORD_STATUS } from 'src/enums';
interface FindAllByIdWithPaginationParams {
workspaceIdList: string[];
page: number;
limit: number;
name?: string;
}
interface FindAllByIdWithPaginationResult {
list: Workspace[];
count: number;
}
@Injectable()
export class WorkspaceService {
constructor(
@ -41,15 +52,17 @@ export class WorkspaceService {
}: {
workspaceIdList: string[];
}): Promise<Workspace[]> {
return this.workspaceRepository.find({
where: {
const query = {
_id: {
$in: workspaceIdList.map((item) => new ObjectId(item)),
},
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
},
};
return this.workspaceRepository.find({
where: query,
order: {
_id: -1,
},
@ -64,6 +77,35 @@ export class WorkspaceService {
});
}
async findAllByIdWithPagination({
workspaceIdList,
page,
limit,
name,
}: FindAllByIdWithPaginationParams): Promise<FindAllByIdWithPaginationResult> {
const skip = (page - 1) * limit;
if (!Array.isArray(workspaceIdList) || workspaceIdList.length === 0) {
return { list: [], count: 0 };
}
const query = {
_id: {
$in: workspaceIdList.map((m) => new ObjectId(m)),
},
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
};
if (name) {
query['name'] = { $regex: name, $options: 'i' };
}
const [data, count] = await this.workspaceRepository.findAndCount({
where: query,
skip,
take: limit,
});
return { list: data, count };
}
update(id: string, workspace: Partial<Workspace>) {
return this.workspaceRepository.update(id, workspace);
}

View File

@ -8,8 +8,10 @@ export const updateSpace = ({ workspaceId, name, description, members }: any) =>
return axios.post(`/workspace/${workspaceId}`, { name, description, members })
}
export const getSpaceList = () => {
return axios.get('/workspace')
export const getSpaceList = (params: any) => {
return axios.get('/workspace', {
params
})
}
export const getSpaceDetail = (workspaceId: string) => {

View File

@ -9,7 +9,7 @@ export const spaceListConfig = {
name: {
title: '空间名称',
key: 'name',
width: 300
width: 200
},
surveyTotal: {
title: '问卷数',
@ -82,6 +82,16 @@ export const noListDataConfig = {
img: '/imgs/icons/list-empty.webp'
}
export const noSpaceDataConfig = {
title: '您还没有创建团队空间',
desc: '赶快点击右上角立即创建团队空间吧!',
img: '/imgs/icons/list-empty.webp'
}
export const noSpaceSearchDataConfig = {
title: '没有满足该查询条件的团队空间哦',
desc: '可以更换条件查询试试',
img: '/imgs/icons/list-empty.webp'
}
export const noSearchDataConfig = {
title: '没有满足该查询条件的问卷哦',
desc: '可以更换条件查询试试',

View File

@ -42,7 +42,7 @@
</el-table>
<el-popover
ref="popover"
popper-style="text-align: center;"
popper-style="text-align: center;font-size: 13px;"
:virtual-ref="popoverVirtualRef"
placement="top"
width="400"
@ -83,11 +83,11 @@ const setPopoverContent = (content) => {
}
const onPopoverRefOver = (scope, type) => {
let popoverContent
if (type == 'head') {
if (type === 'head') {
popoverVirtualRef.value = popoverRefMap.value[scope.column.id]
popoverContent = scope.column.label.replace(/&nbsp;/g, '')
}
if (type == 'content') {
if (type === 'content') {
popoverVirtualRef.value = popoverRefMap.value[scope.$index + scope.column.property]
popoverContent = getContent(scope.row[scope.column.property])
}
@ -99,7 +99,6 @@ const onPopoverRefOver = (scope, type) => {
.data-table-wrapper {
position: relative;
width: 100%;
padding-bottom: 20px;
min-height: v-bind('tableMinHeight');
background: #fff;
padding: 10px 20px;
@ -132,4 +131,7 @@ const onPopoverRefOver = (scope, type) => {
/* 显示省略号 */
}
}
:deep(.el-table td.el-table__cell div) {
font-size: 13px;
}
</style>

View File

@ -8,7 +8,14 @@
<NavPanel></NavPanel>
</div>
<div class="right-group">
<CooperationPanel></CooperationPanel>
<CooperationPanel>
<template #content="{ onCooper }">
<div class="btn" @click="onCooper">
<i-ep-connection class="view-icon" :size="20" />
<span class="btn-txt">协作</span>
</div>
</template>
</CooperationPanel>
<PreviewPanel></PreviewPanel>
<HistoryPanel></HistoryPanel>
<SavePanel></SavePanel>
@ -34,6 +41,12 @@ const store = useStore()
const title = computed(() => _get(store.state, 'edit.schema.metaData.title'))
</script>
<style lang="scss" scoped>
@import url('@/management/styles/edit-btn.scss');
.view-icon {
font-size: 20px;
height: 29px;
line-height: 29px;
}
.nav {
width: 100%;
height: 56px;

View File

@ -92,6 +92,10 @@ const changePreset = (banner: any) => {
border: none;
overflow-y: auto;
background-color: #fff;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
.title {
height: 40px;
line-height: 40px;

View File

@ -1,14 +1,19 @@
<template>
<div class="list-wrap">
<div class="search">
<TextSearch placeholder="请输入问卷标题" :value="searchVal" @search="onSearchText" />
</div>
<div class="list-wrap" v-if="props.total > 0">
<el-table
ref="multipleListTable"
class="list-table"
:data="dataList"
:data="data"
empty-text="暂无数据"
row-key="_id"
header-row-class-name="tableview-header"
row-class-name="tableview-row"
cell-class-name="tableview-cell"
v-loading="loading"
:height="550"
style="width: 100%"
>
<el-table-column column-key="space" width="20" />
@ -30,7 +35,6 @@
</template>
</template>
</el-table-column>
<el-table-column
label="操作"
:width="200"
@ -50,6 +54,19 @@
</el-table-column>
</el-table>
</div>
<div v-else>
<EmptyIndex :data="!searchVal ? noSpaceDataConfig : noSpaceSearchDataConfig" />
</div>
<div class="list-pagination">
<el-pagination
v-model:current-page="curPage"
background
@current-change="handleCurrentChange"
layout="prev, pager, next"
:total="props.total"
>
</el-pagination>
</div>
<SpaceModify
v-if="showSpaceModify"
:type="modifyType"
@ -58,28 +75,47 @@
/>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
import { useStore } from 'vuex'
import { ElMessageBox } from 'element-plus'
import 'element-plus/theme-chalk/src/message-box.scss'
import { get, map } from 'lodash-es'
import { spaceListConfig } from '@/management/config/listConfig'
import {
noSpaceDataConfig,
noSpaceSearchDataConfig,
spaceListConfig
} from '@/management/config/listConfig'
import SpaceModify from './SpaceModify.vue'
import TextSearch from '@/management/pages/list/components/TextSearch.vue'
import EmptyIndex from '@/management/components/EmptyIndex.vue'
import ToolBar from './ToolBar.vue'
import { UserRole } from '@/management/utils/types/workSpace'
const showSpaceModify = ref(false)
const modifyType = ref('edit')
const store = useStore()
const props = defineProps({
loading: {
type: Boolean,
default: false
},
data: {
type: Array,
default: () => []
},
total: {
type: Number,
default: 0
}
})
const emit = defineEmits(['reflush'])
const fields = ['name', 'surveyTotal', 'memberTotal', 'owner', 'createDate']
const fieldList = computed(() => {
return map(fields, (f) => {
return get(spaceListConfig, f, null)
})
})
const dataList = computed(() => {
return store.state.list.teamSpaceList
})
const isAdmin = (id: string) => {
return (
store.state.list.teamSpaceList.find((item: any) => item._id === id)?.currentUserRole ===
@ -87,6 +123,26 @@ const isAdmin = (id: string) => {
)
}
const data = computed(() => {
return props.data
})
let searchVal = ref('')
let curPage = ref(1)
const emitReflush = (page: number, name: string) => {
curPage.value = page
emit('reflush', {
curPage: page,
name
})
}
const handleCurrentChange = async (val: number) => {
emitReflush(val, searchVal.value)
}
const onSearchText = async (e: string) => {
searchVal.value = e
emitReflush(1, e)
}
const getTools = (data: any) => {
const flag = isAdmin(data._id)
const tools = [{ key: 'modify', label: flag ? '管理' : '查看' }]
@ -101,7 +157,6 @@ const handleModify = async (id: string) => {
modifyType.value = 'edit'
showSpaceModify.value = true
}
const handleDelete = (id: string) => {
ElMessageBox.confirm(
'删除团队后,团队内的问卷将同步被删除,请谨慎考虑!是否确认本次删除?',
@ -115,6 +170,7 @@ const handleDelete = (id: string) => {
.then(async () => {
await store.dispatch('list/deleteSpace', id)
await store.dispatch('list/getSpaceList')
curPage.value = 1
})
.catch(() => {})
}
@ -130,17 +186,31 @@ const handleClick = (key: string, data: any) => {
const onCloseModify = () => {
showSpaceModify.value = false
store.dispatch('list/getSpaceList')
curPage.value = 1
}
// const handleCurrentChange = (current) => {
// this.currentPage = current
// this.init()
// }
defineExpose({ onCloseModify })
</script>
<style lang="scss" scoped>
.search {
display: flex;
justify-content: flex-end;
margin-bottom: 20px;
}
.list-pagination {
margin-top: 20px;
:deep(.el-pagination) {
display: flex;
justify-content: flex-end;
}
}
.list-wrap {
padding: 20px;
background: #fff;
.search {
display: flex;
}
.list-table {
:deep(.el-table__header) {
.tableview-header .el-table__cell {

View File

@ -7,7 +7,7 @@
:placeholder="placeholder"
>
<template #suffix>
<i-ep-search class="el-input__icon" />
<i-ep-search @click="onSearch" class="el-input__icon" />
</template>
</el-input>
</div>

View File

@ -52,7 +52,14 @@
@reflush="fetchSurveyList"
v-if="spaceType !== SpaceType.Group"
></BaseList>
<SpaceList v-if="spaceType === SpaceType.Group"></SpaceList>
<SpaceList
ref="spaceListRef"
@reflush="fetchSpaceList"
:loading="loading2"
:data="spaceList"
:total="spaceTotal"
v-if="spaceType === SpaceType.Group"
></SpaceList>
</div>
</div>
<SpaceModify
@ -86,6 +93,21 @@ const surveyList = computed(() => {
const surveyTotal = computed(() => {
return store.state.list.surveyTotal
})
let spaceListRef = ref<InstanceType<typeof SpaceList> | null>(null)
const loading2 = ref(false)
const spaceList = computed(() => {
return store.state.list.teamSpaceList
})
const spaceTotal = computed(() => {
return store.state.list.teamSpaceListTotal
})
const fetchSpaceList = async (params?: any) => {
loading2.value = true
store.commit('list/changeWorkSpace', '')
await store.dispatch('list/getSpaceList', params)
loading2.value = false
}
const activeIndex = ref('1')
const spaceMenus = computed(() => {
@ -97,39 +119,37 @@ const workSpaceId = computed(() => {
const spaceType = computed(() => {
return store.state.list.spaceType
})
const handleSpaceSelect = (id: any) => {
if (id === SpaceType.Personal) {
//
if (store.state.list.spaceType === SpaceType.Personal) {
return
const handleSpaceSelect = (id: SpaceType | string) => {
if (id === store.state.list.spaceType || id === store.state.list.workSpaceId) {
return void 0
}
store.commit('list/changeSpaceType', SpaceType.Personal)
store.commit('list/changeWorkSpace', '')
} else if (id === SpaceType.Group) {
//
if (store.state.list.spaceType === SpaceType.Group) {
return
const changeSpaceType = (type: SpaceType) => {
store.commit('list/changeSpaceType', type)
}
store.commit('list/changeSpaceType', SpaceType.Group)
store.commit('list/changeWorkSpace', '')
} else if (!Object.values(SpaceType).includes(id)) {
//
if (store.state.list.workSpaceId === id) {
return
}
store.commit('list/changeSpaceType', SpaceType.Teamwork)
const changeWorkSpace = (id: string) => {
store.commit('list/changeWorkSpace', id)
}
switch (id) {
case SpaceType.Personal:
changeSpaceType(SpaceType.Personal)
changeWorkSpace('')
break
case SpaceType.Group:
changeSpaceType(SpaceType.Group)
changeWorkSpace('')
fetchSpaceList()
break
default:
changeSpaceType(SpaceType.Teamwork)
changeWorkSpace(id)
break
}
fetchSurveyList()
}
onMounted(() => {
fetchSpaceList()
fetchSurveyList()
})
const fetchSpaceList = () => {
store.dispatch('list/getSpaceList')
}
const fetchSurveyList = async (params?: any) => {
if (!params) {
params = {
@ -160,7 +180,10 @@ const onSetGroup = async () => {
const onCloseModify = (type: string) => {
showSpaceModify.value = false
if (type === 'update') fetchSpaceList()
if (type === 'update' && spaceListRef.value) {
fetchSpaceList()
spaceListRef.value.onCloseModify()
}
}
const onSpaceCreate = () => {
showSpaceModify.value = true

View File

@ -37,7 +37,9 @@ export default {
spaceType: SpaceType.Personal,
workSpaceId: '',
spaceDetail: null,
// 团队空间
teamSpaceList: [],
teamSpaceListTotal: 0,
// 列表管理
surveyList: [],
surveyTotal: 0,
@ -116,6 +118,9 @@ export default {
setTeamSpaceList(state, data) {
state.teamSpaceList = data
},
setTeamSpaceListTotal(state, teamSpaceListTotal) {
state.teamSpaceListTotal = teamSpaceListTotal
},
setSurveyList(state, list) {
state.surveyList = list
},
@ -145,18 +150,18 @@ export default {
}
},
actions: {
async getSpaceList({ commit }) {
async getSpaceList({ commit }, p = { curPage: 1 }) {
try {
const res = await getSpaceList()
const res = await getSpaceList(p)
if (res.code === CODE_MAP.SUCCESS) {
const { list } = res.data
const { list, count } = res.data
const teamSpace = list.map((item) => {
return {
id: item._id,
name: item.name
}
})
commit('setTeamSpaceListTotal', count)
commit('setTeamSpaceList', list)
commit('updateSpaceMenus', teamSpace)
} else {