- 移除 VITE_SERVICE_LOGOUT_CODES 中的 1002023000 状态码 - 将 VITE_SERVICE_EXPIRED_TOKEN_CODES 从 1002023001 改为 1002023000 - 修改 fetchRefreshToken 函数使用 params 传递 refreshToken 并设置 skipAuth - 添加 skipAuth 配置选项避免给公开接口带上过期 access 头 - 实现 notifySessionExpired 函数确保并发请求只弹一次会话失效提示 - 在登录成功后复位会话失效标志以支持下次正常提示 - 更新 handleExpiredRequest 使用 refreshTokenPromise 替代 refreshTokenFn
380 lines
11 KiB
Vue
380 lines
11 KiB
Vue
<script setup lang="tsx">
|
|
import { computed, onMounted, reactive, ref } from 'vue';
|
|
import { ElButton, ElTag } from 'element-plus';
|
|
import dayjs from 'dayjs';
|
|
import { RDMS_OBJECT_DIRECTION_DICT_CODE, RDMS_PROJECT_TYPE_DICT_CODE } from '@/constants/dict';
|
|
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
|
|
import { fetchGetProjectOverviewSummary, fetchGetProjectPage, fetchGetUserSimpleList } from '@/service/api';
|
|
import { useDict } from '@/hooks/business/dict';
|
|
import { useRouterPush } from '@/hooks/common/router';
|
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
|
import { getProjectStatusLabel, getProjectStatusTagType } from '../shared/project-master-data';
|
|
import ProjectOperateDialog from './modules/project-operate-dialog.vue';
|
|
import ProjectOverviewCard from './modules/project-overview-card.vue';
|
|
import ProjectSearch from './modules/project-search.vue';
|
|
|
|
defineOptions({ name: 'ProjectList' });
|
|
|
|
type ProjectPageResponse = Awaited<ReturnType<typeof fetchGetProjectPage>>;
|
|
|
|
const PROJECT_ENTRY_ROUTE_PATH = '/project/list';
|
|
|
|
function getInitSearchParams(): Api.Project.ProjectSearchParams {
|
|
return {
|
|
pageNo: 1,
|
|
pageSize: 10,
|
|
keyword: '',
|
|
directionCode: undefined,
|
|
projectType: undefined,
|
|
productId: undefined,
|
|
managerUserId: undefined,
|
|
statusCode: undefined,
|
|
updateTime: undefined
|
|
};
|
|
}
|
|
|
|
function transformProjectPage(response: ProjectPageResponse, 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
|
|
};
|
|
}
|
|
|
|
function sortManagerOptions(list: Api.SystemManage.UserSimple[]) {
|
|
return list.slice().sort((left, right) => left.nickname.localeCompare(right.nickname, 'zh-CN'));
|
|
}
|
|
|
|
function formatDateTime(value?: string | null) {
|
|
if (!value) {
|
|
return '--';
|
|
}
|
|
|
|
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
|
}
|
|
|
|
const searchParams = reactive(getInitSearchParams());
|
|
const selectedStatus = ref<Api.Project.ProjectStatusCode>('active');
|
|
const managerFilterOptions = ref<Api.SystemManage.UserSimple[]>([]);
|
|
const managerUserOptions = ref<Api.SystemManage.UserSimple[]>([]);
|
|
const operateVisible = ref(false);
|
|
const editingRow = ref<Api.Project.Project | null>(null);
|
|
const { routerPush } = useRouterPush();
|
|
|
|
const { getLabel: getDirectionDictLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE);
|
|
const { getLabel: getProjectTypeLabel } = useDict(RDMS_PROJECT_TYPE_DICT_CODE);
|
|
|
|
const statusCounts = ref<Record<string, number>>({
|
|
pending: 0,
|
|
active: 0,
|
|
paused: 0,
|
|
completed: 0,
|
|
cancelled: 0,
|
|
archived: 0
|
|
});
|
|
|
|
const statusOptions = computed(() => [
|
|
{ value: 'pending', label: '待开始' },
|
|
{ value: 'active', label: '进行中' },
|
|
{ value: 'paused', label: '已暂停' },
|
|
{ value: 'completed', label: '已完成' },
|
|
{ value: 'cancelled', label: '作废项目' },
|
|
{ value: 'archived', label: '归档项目' }
|
|
]);
|
|
|
|
const managerLabelMap = computed(() => {
|
|
return new Map(managerUserOptions.value.map(item => [String(item.id), item.nickname]));
|
|
});
|
|
|
|
function getDirectionLabel(directionCode?: string | null) {
|
|
return getDirectionDictLabel(directionCode, '--');
|
|
}
|
|
|
|
function getProjectTypeLabelByCode(projectType?: string | null) {
|
|
return getProjectTypeLabel(projectType, '--');
|
|
}
|
|
|
|
function getManagerLabel(managerUserId?: string | null) {
|
|
if (!managerUserId) {
|
|
return '--';
|
|
}
|
|
|
|
return managerLabelMap.value.get(String(managerUserId)) || String(managerUserId);
|
|
}
|
|
|
|
function createRequestParams(): Api.Project.ProjectSearchParams {
|
|
return {
|
|
...searchParams,
|
|
keyword: searchParams.keyword?.trim() || undefined,
|
|
statusCode: selectedStatus.value
|
|
};
|
|
}
|
|
|
|
const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
|
ProjectPageResponse,
|
|
Api.Project.Project
|
|
>({
|
|
paginationProps: {
|
|
currentPage: searchParams.pageNo,
|
|
pageSize: searchParams.pageSize
|
|
},
|
|
api: () => fetchGetProjectPage(createRequestParams()),
|
|
transform: response => transformProjectPage(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
|
onPaginationParamsChange: params => {
|
|
searchParams.pageNo = params.currentPage ?? 1;
|
|
searchParams.pageSize = params.pageSize ?? 10;
|
|
},
|
|
columns: () => [
|
|
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
|
{
|
|
prop: 'projectName',
|
|
label: '项目名称',
|
|
minWidth: 220,
|
|
formatter: row => (
|
|
<ElButton link type="primary" class="project-name-link" onClick={() => enterProjectContext(row)}>
|
|
{row.projectName}
|
|
</ElButton>
|
|
)
|
|
},
|
|
{ prop: 'projectCode', label: '项目编码', minWidth: 140, showOverflowTooltip: true },
|
|
{
|
|
prop: 'directionCode',
|
|
label: '项目方向',
|
|
minWidth: 140,
|
|
showOverflowTooltip: true,
|
|
formatter: row => getDirectionLabel(row.directionCode)
|
|
},
|
|
{
|
|
prop: 'projectType',
|
|
label: '项目类型',
|
|
minWidth: 140,
|
|
showOverflowTooltip: true,
|
|
formatter: row => getProjectTypeLabelByCode(row.projectType)
|
|
},
|
|
{
|
|
prop: 'managerUserId',
|
|
label: '项目经理',
|
|
minWidth: 120,
|
|
formatter: row => getManagerLabel(row.managerUserId)
|
|
},
|
|
{
|
|
prop: 'progressRate',
|
|
label: '进度',
|
|
width: 100,
|
|
align: 'center',
|
|
formatter: () => '--'
|
|
},
|
|
{
|
|
prop: 'statusCode',
|
|
label: '状态',
|
|
width: 100,
|
|
align: 'center',
|
|
formatter: row => (
|
|
<ElTag type={getProjectStatusTagType(row.statusCode)}>{getProjectStatusLabel(row.statusCode)}</ElTag>
|
|
)
|
|
},
|
|
{
|
|
prop: 'updateTime',
|
|
label: '最近更新',
|
|
width: 170,
|
|
align: 'center',
|
|
formatter: row => formatDateTime(row.updateTime)
|
|
}
|
|
],
|
|
immediate: false
|
|
});
|
|
|
|
async function loadManagerOptions() {
|
|
const { error, data: userList } = await fetchGetUserSimpleList();
|
|
|
|
if (error || !userList) {
|
|
managerUserOptions.value = [];
|
|
managerFilterOptions.value = [];
|
|
return;
|
|
}
|
|
|
|
const userSimpleList = sortManagerOptions(userList);
|
|
managerUserOptions.value = userSimpleList;
|
|
managerFilterOptions.value = userSimpleList;
|
|
}
|
|
|
|
async function loadOverviewData() {
|
|
const { error, data: overviewSummary } = await fetchGetProjectOverviewSummary();
|
|
|
|
if (error || !overviewSummary) {
|
|
statusCounts.value = {};
|
|
return;
|
|
}
|
|
|
|
statusCounts.value = overviewSummary.statusCounts || {};
|
|
}
|
|
|
|
async function reloadProjectTable(page = searchParams.pageNo ?? 1) {
|
|
await getDataByPage(page);
|
|
}
|
|
|
|
async function refreshPageData(page = searchParams.pageNo ?? 1) {
|
|
await Promise.all([loadManagerOptions(), loadOverviewData(), reloadProjectTable(page)]);
|
|
}
|
|
|
|
async function handleSearch() {
|
|
await reloadProjectTable(1);
|
|
}
|
|
|
|
async function handleResetSearch() {
|
|
const pageSize = searchParams.pageSize ?? 10;
|
|
|
|
Object.assign(searchParams, getInitSearchParams(), {
|
|
pageSize
|
|
});
|
|
|
|
await reloadProjectTable(1);
|
|
}
|
|
|
|
async function handleStatusChange(status: Api.Project.ProjectStatusCode) {
|
|
selectedStatus.value = status;
|
|
await Promise.all([loadOverviewData(), reloadProjectTable(1)]);
|
|
}
|
|
|
|
function openCreate() {
|
|
editingRow.value = null;
|
|
operateVisible.value = true;
|
|
}
|
|
|
|
async function enterProjectContext(row: Api.Project.Project) {
|
|
await routerPush({
|
|
path: PROJECT_ENTRY_ROUTE_PATH,
|
|
query: {
|
|
[OBJECT_CONTEXT_QUERY_KEY]: row.id
|
|
}
|
|
});
|
|
}
|
|
|
|
async function handleProjectSubmitted(projectId?: string) {
|
|
const isEditing = Boolean(projectId && editingRow.value?.id === projectId);
|
|
|
|
await refreshPageData(isEditing ? (searchParams.pageNo ?? 1) : 1);
|
|
|
|
if (isEditing) {
|
|
editingRow.value = null;
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await refreshPageData();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="min-h-560px gap-16px overflow-hidden xl:grid xl:grid-cols-[396px_minmax(0,1fr)] lt-xl:flex lt-xl:flex-col lt-xl:overflow-auto"
|
|
>
|
|
<div class="flex-col-stretch gap-16px xl:min-h-0">
|
|
<ProjectOverviewCard
|
|
:status-counts="statusCounts"
|
|
:selected-status="selectedStatus"
|
|
@status-change="handleStatusChange"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex-col-stretch gap-16px xl:min-h-0">
|
|
<ProjectSearch
|
|
v-model:model="searchParams"
|
|
:manager-options="managerFilterOptions"
|
|
@reset="handleResetSearch"
|
|
@search="handleSearch"
|
|
/>
|
|
|
|
<ElCard class="card-wrapper xl:flex-1-hidden" body-class="business-table-card-body">
|
|
<template #header>
|
|
<div class="project-card-header">
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex flex-wrap items-center gap-8px">
|
|
<p class="truncate text-16px font-600">项目列表</p>
|
|
<ElTag effect="plain" :type="getProjectStatusTagType(selectedStatus)">
|
|
{{
|
|
statusOptions.find(item => item.value === selectedStatus)?.label ||
|
|
getProjectStatusLabel(selectedStatus)
|
|
}}
|
|
</ElTag>
|
|
<ElTag effect="plain">{{ mobilePagination.total || data.length }}</ElTag>
|
|
</div>
|
|
</div>
|
|
<TableHeaderOperation
|
|
v-model:columns="columnChecks"
|
|
:disabled-delete="true"
|
|
:loading="loading"
|
|
@refresh="refreshPageData"
|
|
>
|
|
<template #default>
|
|
<ElButton plain type="primary" @click="openCreate">
|
|
<template #icon>
|
|
<icon-ic-round-plus class="text-icon" />
|
|
</template>
|
|
新建
|
|
</ElButton>
|
|
</template>
|
|
</TableHeaderOperation>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="flex-1">
|
|
<ElTable v-loading="loading" height="100%" border row-key="id" :data="data">
|
|
<ElTableColumn v-for="col in columns" :key="String(col.prop)" v-bind="col" />
|
|
|
|
<template #empty>
|
|
<ElEmpty description="当前筛选条件下暂无项目" />
|
|
</template>
|
|
</ElTable>
|
|
</div>
|
|
<div class="mt-20px flex justify-end">
|
|
<ElPagination
|
|
v-if="mobilePagination.total"
|
|
layout="total,prev,pager,next,sizes"
|
|
v-bind="mobilePagination"
|
|
@current-change="mobilePagination['current-change']"
|
|
@size-change="mobilePagination['size-change']"
|
|
/>
|
|
</div>
|
|
</ElCard>
|
|
</div>
|
|
|
|
<ProjectOperateDialog
|
|
v-model:visible="operateVisible"
|
|
:manager-user-options="managerUserOptions"
|
|
:row-data="editingRow"
|
|
@submitted="handleProjectSubmitted"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.project-card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
.project-name-link {
|
|
padding: 0;
|
|
}
|
|
|
|
@media (width <= 1280px) {
|
|
.project-card-header {
|
|
align-items: flex-start;
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
</style>
|