261 lines
7.1 KiB
Vue
261 lines
7.1 KiB
Vue
<script setup lang="ts">
|
|
import { computed, reactive, watch } from 'vue';
|
|
import { fetchGetProjectExecutionMemberLogPage } from '@/service/api';
|
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
|
import BusinessUserSelect from '@/components/custom/business-user-select.vue';
|
|
import { formatDateTime, getExecutionMemberActionName, getExecutionMemberActionTagType } from '../shared';
|
|
|
|
defineOptions({ name: 'ProjectExecutionMemberLogPanel' });
|
|
|
|
interface Props {
|
|
projectId: string;
|
|
executionId: string;
|
|
userOptions: Api.SystemManage.UserSimple[];
|
|
active: boolean;
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
|
|
type ActionType = Api.Project.ExecutionMemberActionType;
|
|
|
|
const ACTION_TYPE_OPTIONS: Array<{ label: string; value: ActionType }> = [
|
|
{ label: getExecutionMemberActionName('join'), value: 'join' },
|
|
{ label: getExecutionMemberActionName('inactive'), value: 'inactive' },
|
|
{ label: getExecutionMemberActionName('owner_transfer_in'), value: 'owner_transfer_in' },
|
|
{ label: getExecutionMemberActionName('owner_transfer_out'), value: 'owner_transfer_out' }
|
|
];
|
|
|
|
const searchParams = reactive<{
|
|
pageNo: number;
|
|
pageSize: number;
|
|
actionTypes?: ActionType[];
|
|
userId?: string;
|
|
}>({
|
|
pageNo: 1,
|
|
pageSize: 5,
|
|
actionTypes: undefined,
|
|
userId: undefined
|
|
});
|
|
|
|
const canLoad = computed(() => Boolean(props.projectId && props.executionId));
|
|
|
|
type LogPageResponse = Awaited<ReturnType<typeof fetchGetProjectExecutionMemberLogPage>>;
|
|
|
|
function buildRequestParams(): Api.Project.ExecutionMemberLogSearchParams {
|
|
return {
|
|
pageNo: searchParams.pageNo,
|
|
pageSize: searchParams.pageSize,
|
|
actionTypes: searchParams.actionTypes?.length ? searchParams.actionTypes : undefined,
|
|
userId: searchParams.userId || undefined
|
|
};
|
|
}
|
|
|
|
function transformLogPage(response: LogPageResponse, pageNo: number, pageSize: number) {
|
|
if (!response.error && response.data) {
|
|
return {
|
|
data: response.data.list,
|
|
pageNum: pageNo,
|
|
pageSize,
|
|
total: response.data.total
|
|
};
|
|
}
|
|
|
|
return {
|
|
data: [],
|
|
pageNum: pageNo,
|
|
pageSize,
|
|
total: 0
|
|
};
|
|
}
|
|
|
|
const { data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
|
LogPageResponse,
|
|
Api.Project.ExecutionMemberLog
|
|
>({
|
|
paginationProps: {
|
|
currentPage: searchParams.pageNo,
|
|
pageSize: searchParams.pageSize
|
|
},
|
|
api: () => {
|
|
if (!canLoad.value) {
|
|
return Promise.resolve({
|
|
data: { total: 0, list: [] },
|
|
error: null
|
|
} as unknown as LogPageResponse);
|
|
}
|
|
|
|
return fetchGetProjectExecutionMemberLogPage(props.projectId, props.executionId, buildRequestParams());
|
|
},
|
|
transform: response => transformLogPage(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 5),
|
|
onPaginationParamsChange: params => {
|
|
searchParams.pageNo = params.currentPage ?? 1;
|
|
searchParams.pageSize = params.pageSize ?? 5;
|
|
},
|
|
immediate: false,
|
|
columns: () => [{ prop: 'actionTime', label: '时间' }]
|
|
});
|
|
|
|
// 每次切到该 tab 都按当前筛选条件重拉第 1 页,确保用户在"当前成员" tab 操作后回到这里能看到最新事件
|
|
watch(
|
|
() => props.active,
|
|
active => {
|
|
if (active && canLoad.value) {
|
|
getDataByPage(1);
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
// 切换到不同执行项时完全清空筛选条件
|
|
watch(
|
|
() => props.executionId,
|
|
() => {
|
|
resetSearchParams();
|
|
}
|
|
);
|
|
|
|
function resetSearchParams() {
|
|
searchParams.pageNo = 1;
|
|
searchParams.actionTypes = undefined;
|
|
searchParams.userId = undefined;
|
|
}
|
|
|
|
async function handleSearch() {
|
|
await getDataByPage(1);
|
|
}
|
|
|
|
async function handleReset() {
|
|
resetSearchParams();
|
|
await getDataByPage(1);
|
|
}
|
|
|
|
function getMemberDisplay(row: Api.Project.ExecutionMemberLog) {
|
|
return row.userNicknameSnapshot?.trim() || row.userId || '--';
|
|
}
|
|
|
|
function getOperatorDisplay(row: Api.Project.ExecutionMemberLog) {
|
|
return row.operatorNicknameSnapshot?.trim() || row.operatorUserId || '--';
|
|
}
|
|
|
|
function refresh() {
|
|
return getDataByPage(1);
|
|
}
|
|
|
|
defineExpose({ refresh });
|
|
</script>
|
|
|
|
<template>
|
|
<div class="member-log-panel">
|
|
<div class="member-log-panel__toolbar">
|
|
<ElSelect
|
|
v-model="searchParams.actionTypes"
|
|
multiple
|
|
collapse-tags
|
|
collapse-tags-tooltip
|
|
clearable
|
|
placeholder="全部事件"
|
|
class="member-log-panel__action-select"
|
|
>
|
|
<ElOption v-for="item in ACTION_TYPE_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
|
|
</ElSelect>
|
|
<BusinessUserSelect
|
|
v-model="searchParams.userId"
|
|
:options="userOptions"
|
|
placeholder="全部成员"
|
|
clearable
|
|
class="member-log-panel__user-select"
|
|
/>
|
|
<div class="member-log-panel__actions">
|
|
<ElButton @click="handleReset">重置</ElButton>
|
|
<ElButton type="primary" @click="handleSearch">查询</ElButton>
|
|
</div>
|
|
</div>
|
|
|
|
<ElTable v-loading="loading" :data="data" :height="247" border size="default">
|
|
<ElTableColumn label="时间" width="170" align="center">
|
|
<template #default="{ row }">
|
|
{{ formatDateTime(row.actionTime) }}
|
|
</template>
|
|
</ElTableColumn>
|
|
<ElTableColumn label="事件类型" width="130" align="center">
|
|
<template #default="{ row }">
|
|
<ElTag :type="getExecutionMemberActionTagType(row.actionType)" effect="light">
|
|
{{ getExecutionMemberActionName(row.actionType) }}
|
|
</ElTag>
|
|
</template>
|
|
</ElTableColumn>
|
|
<ElTableColumn label="成员" min-width="120" show-overflow-tooltip>
|
|
<template #default="{ row }">
|
|
{{ getMemberDisplay(row) }}
|
|
</template>
|
|
</ElTableColumn>
|
|
<ElTableColumn label="操作人" min-width="120" show-overflow-tooltip>
|
|
<template #default="{ row }">
|
|
{{ getOperatorDisplay(row) }}
|
|
</template>
|
|
</ElTableColumn>
|
|
<ElTableColumn label="原因" min-width="200" show-overflow-tooltip>
|
|
<template #default="{ row }">
|
|
<span v-if="row.reason">{{ row.reason }}</span>
|
|
<span v-else class="member-log-panel__empty">--</span>
|
|
</template>
|
|
</ElTableColumn>
|
|
|
|
<template #empty>
|
|
<ElEmpty description="暂无变更记录" :image-size="80" />
|
|
</template>
|
|
</ElTable>
|
|
|
|
<div class="member-log-panel__pagination">
|
|
<ElPagination
|
|
v-if="mobilePagination.total"
|
|
background
|
|
layout="total, prev, pager, next"
|
|
small
|
|
v-bind="mobilePagination"
|
|
@current-change="mobilePagination['current-change']"
|
|
@size-change="mobilePagination['size-change']"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.member-log-panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.member-log-panel__toolbar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.member-log-panel__action-select {
|
|
width: 200px;
|
|
}
|
|
|
|
.member-log-panel__user-select {
|
|
width: 200px;
|
|
}
|
|
|
|
.member-log-panel__actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.member-log-panel__empty {
|
|
color: var(--el-text-color-placeholder);
|
|
}
|
|
|
|
.member-log-panel__pagination {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
min-height: 32px;
|
|
}
|
|
</style>
|