feat(projects): 新增项目、执行、任务等功能
This commit is contained in:
@@ -6,7 +6,7 @@ import dayjs from 'dayjs';
|
||||
import { CircleCheckFilled, DeleteFilled, FolderOpened, VideoPause } from '@element-plus/icons-vue';
|
||||
import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict';
|
||||
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
|
||||
import { fetchGetProductPage, fetchGetUserSimpleList } from '@/service/api';
|
||||
import { fetchGetProductOverviewSummary, fetchGetProductPage, fetchGetUserSimpleList } from '@/service/api';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||
@@ -27,7 +27,6 @@ interface StatusNavMeta {
|
||||
|
||||
type ProductPageResponse = Awaited<ReturnType<typeof fetchGetProductPage>>;
|
||||
|
||||
const PRODUCT_OPTION_PAGE_SIZE = 200;
|
||||
const PRODUCT_ENTRY_ROUTE_PATH = '/product/list';
|
||||
|
||||
function getInitSearchParams(): Api.Product.ProductSearchParams {
|
||||
@@ -72,59 +71,6 @@ function formatDateTime(value?: string | null) {
|
||||
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
async function fetchProductTotal(params: Api.Product.ProductSearchParams) {
|
||||
const { error, data } = await fetchGetProductPage({
|
||||
...params,
|
||||
pageNo: 1,
|
||||
pageSize: 1
|
||||
});
|
||||
|
||||
if (error || !data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return data.total;
|
||||
}
|
||||
|
||||
async function fetchAllProducts() {
|
||||
async function collect(pageNo: number, list: Api.Product.Product[]): Promise<Api.Product.Product[] | null> {
|
||||
const { error, data } = await fetchGetProductPage({
|
||||
pageNo,
|
||||
pageSize: PRODUCT_OPTION_PAGE_SIZE
|
||||
});
|
||||
|
||||
if (error || !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextList = list.concat(data.list);
|
||||
|
||||
if (nextList.length >= data.total || data.list.length === 0) {
|
||||
return nextList;
|
||||
}
|
||||
|
||||
return collect(pageNo + 1, nextList);
|
||||
}
|
||||
|
||||
return collect(1, []);
|
||||
}
|
||||
|
||||
function createManagerOptions(products: Api.Product.Product[], users: Api.SystemManage.UserSimple[]) {
|
||||
const managerIdSet = new Set(products.map(item => String(item.managerUserId)).filter(Boolean));
|
||||
const userMap = new Map(users.map(item => [String(item.id), item]));
|
||||
|
||||
const options = Array.from(managerIdSet).map(managerUserId => {
|
||||
return (
|
||||
userMap.get(managerUserId) || {
|
||||
id: managerUserId,
|
||||
nickname: String(managerUserId)
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return sortManagerOptions(options);
|
||||
}
|
||||
|
||||
const statusNavMetas: StatusNavMeta[] = [
|
||||
{
|
||||
key: 'active',
|
||||
@@ -166,15 +112,13 @@ const { routerPush } = useRouterPush();
|
||||
|
||||
const { dictData: directionOptions, getLabel: getDirectionDictLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE);
|
||||
|
||||
const statusCounts = ref<Record<Api.Product.ProductStatusCode, number>>({
|
||||
const statusCounts = ref<Record<string, number>>({
|
||||
active: 0,
|
||||
archived: 0,
|
||||
paused: 0,
|
||||
abandoned: 0
|
||||
});
|
||||
|
||||
const recentUpdatedCount = ref(0);
|
||||
|
||||
const managerLabelMap = computed(() => {
|
||||
return new Map(managerUserOptions.value.map(item => [String(item.id), item.nickname]));
|
||||
});
|
||||
@@ -182,7 +126,7 @@ const managerLabelMap = computed(() => {
|
||||
const statusItems = computed(() =>
|
||||
statusNavMetas.map(item => ({
|
||||
...item,
|
||||
count: statusCounts.value[item.key]
|
||||
count: statusCounts.value[item.key] ?? 0
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -194,7 +138,7 @@ const overviewMetrics = computed(() => [
|
||||
},
|
||||
{
|
||||
label: '当前启用',
|
||||
value: statusCounts.value.active,
|
||||
value: statusCounts.value.active ?? 0,
|
||||
hint: '正在持续服务和维护的产品'
|
||||
},
|
||||
{
|
||||
@@ -203,9 +147,9 @@ const overviewMetrics = computed(() => [
|
||||
hint: '已加载的方向字典项数量'
|
||||
},
|
||||
{
|
||||
label: '30天内更新',
|
||||
value: recentUpdatedCount.value,
|
||||
hint: '最近 30 天内发生过更新的产品'
|
||||
label: '废弃产品',
|
||||
value: statusCounts.value.abandoned ?? 0,
|
||||
hint: '已明确停止建设的产品'
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -312,44 +256,33 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination }
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
],
|
||||
immediate: false
|
||||
});
|
||||
|
||||
async function loadManagerOptions() {
|
||||
const [allProducts, userSimpleResult] = await Promise.all([fetchAllProducts(), fetchGetUserSimpleList()]);
|
||||
const { error, data: userList } = await fetchGetUserSimpleList();
|
||||
|
||||
const userSimpleList =
|
||||
userSimpleResult.error || !userSimpleResult.data ? [] : sortManagerOptions(userSimpleResult.data);
|
||||
|
||||
managerUserOptions.value = userSimpleList;
|
||||
|
||||
if (!allProducts) {
|
||||
if (error || !userList) {
|
||||
managerUserOptions.value = [];
|
||||
managerFilterOptions.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
managerFilterOptions.value = createManagerOptions(allProducts, userSimpleList);
|
||||
const userSimpleList = sortManagerOptions(userList);
|
||||
managerUserOptions.value = userSimpleList;
|
||||
managerFilterOptions.value = userSimpleList;
|
||||
}
|
||||
|
||||
async function loadOverviewData() {
|
||||
const end = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
|
||||
const start = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss');
|
||||
const { error, data: overviewSummary } = await fetchGetProductOverviewSummary();
|
||||
|
||||
const [activeTotal, archivedTotal, pausedTotal, abandonedTotal, recentTotal] = await Promise.all([
|
||||
fetchProductTotal({ pageNo: 1, pageSize: 1, statusCode: 'active' }),
|
||||
fetchProductTotal({ pageNo: 1, pageSize: 1, statusCode: 'archived' }),
|
||||
fetchProductTotal({ pageNo: 1, pageSize: 1, statusCode: 'paused' }),
|
||||
fetchProductTotal({ pageNo: 1, pageSize: 1, statusCode: 'abandoned' }),
|
||||
fetchProductTotal({ pageNo: 1, pageSize: 1, updateTime: [start, end] })
|
||||
]);
|
||||
if (error || !overviewSummary) {
|
||||
statusCounts.value = {};
|
||||
return;
|
||||
}
|
||||
|
||||
statusCounts.value = {
|
||||
active: activeTotal,
|
||||
archived: archivedTotal,
|
||||
paused: pausedTotal,
|
||||
abandoned: abandonedTotal
|
||||
};
|
||||
recentUpdatedCount.value = recentTotal;
|
||||
statusCounts.value = overviewSummary.statusCounts || {};
|
||||
}
|
||||
|
||||
async function reloadProductTable(page = searchParams.pageNo ?? 1) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict';
|
||||
import { fetchCreateProduct, fetchGetProduct, fetchUpdateProduct } from '@/service/api';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessUserSelect from '@/components/custom/business-user-select.vue';
|
||||
import DictSelect from '@/components/custom/dict-select.vue';
|
||||
|
||||
defineOptions({ name: 'ProductOperateDialog' });
|
||||
@@ -166,14 +167,14 @@ watch(visible, async value => {
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="dialogTitle"
|
||||
preset="lg"
|
||||
preset="sm"
|
||||
:loading="loading"
|
||||
:confirm-loading="submitting"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem v-if="isEditMode" label="产品编码" prop="code">
|
||||
<ElInput
|
||||
:model-value="model.code"
|
||||
@@ -186,12 +187,12 @@ watch(visible, async value => {
|
||||
<ElInput v-model="model.code" clearable placeholder="不填则由后端自动生成" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="产品名称" prop="name">
|
||||
<ElInput v-model="model.name" clearable maxlength="128" placeholder="请输入产品名称" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="产品方向" prop="directionCode">
|
||||
<DictSelect
|
||||
v-model="model.directionCode"
|
||||
@@ -201,7 +202,7 @@ watch(visible, async value => {
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem v-if="isEditMode">
|
||||
<template #label>
|
||||
<span class="business-form-label-with-tip">
|
||||
@@ -225,9 +226,11 @@ watch(visible, async value => {
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-else label="产品经理" prop="managerUserId">
|
||||
<ElSelect v-model="model.managerUserId" clearable filterable placeholder="请选择产品经理">
|
||||
<ElOption v-for="item in managerUserOptions" :key="item.id" :label="item.nickname" :value="item.id" />
|
||||
</ElSelect>
|
||||
<BusinessUserSelect
|
||||
v-model="model.managerUserId"
|
||||
:options="managerUserOptions"
|
||||
placeholder="请选择产品经理"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict';
|
||||
import DictSelect from '@/components/custom/dict-select.vue';
|
||||
import TableSearchPanel from '@/components/custom/table-search-panel.vue';
|
||||
import TableSearchFields, { type SearchField } from '@/components/custom/table-search-fields.vue';
|
||||
|
||||
defineOptions({ name: 'ProductSearch' });
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
managerOptions: Api.SystemManage.UserSimple[];
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'reset'): void;
|
||||
@@ -20,6 +20,32 @@ const emit = defineEmits<Emits>();
|
||||
|
||||
const model = defineModel<Api.Product.ProductSearchParams>('model', { required: true });
|
||||
|
||||
const fields = computed<SearchField[]>(() => [
|
||||
{
|
||||
key: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '产品名称 / 编号'
|
||||
},
|
||||
{
|
||||
key: 'managerUserId',
|
||||
label: '产品经理',
|
||||
type: 'select',
|
||||
options: props.managerOptions.map(item => ({
|
||||
label: item.nickname,
|
||||
value: item.id
|
||||
})),
|
||||
placeholder: '筛选产品经理'
|
||||
},
|
||||
{
|
||||
key: 'directionCode',
|
||||
label: '产品方向',
|
||||
type: 'dict',
|
||||
dictCode: RDMS_OBJECT_DIRECTION_DICT_CODE,
|
||||
placeholder: '筛选产品方向'
|
||||
}
|
||||
]);
|
||||
|
||||
function reset() {
|
||||
emit('reset');
|
||||
}
|
||||
@@ -30,30 +56,7 @@ function search() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableSearchPanel :model="model" :action-col-lg="6" @reset="reset" @search="search">
|
||||
<ElCol :lg="6" :md="12" :sm="12">
|
||||
<ElFormItem label="关键词">
|
||||
<ElInput v-model="model.keyword" clearable placeholder="产品名称 / 编号" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :lg="6" :md="12" :sm="12">
|
||||
<ElFormItem label="产品经理">
|
||||
<ElSelect v-model="model.managerUserId" clearable filterable placeholder="筛选产品经理">
|
||||
<ElOption v-for="item in managerOptions" :key="item.id" :label="item.nickname" :value="item.id" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :lg="6" :md="12" :sm="12">
|
||||
<ElFormItem label="产品方向">
|
||||
<DictSelect
|
||||
v-model="model.directionCode"
|
||||
:dict-code="RDMS_OBJECT_DIRECTION_DICT_CODE"
|
||||
filterable
|
||||
placeholder="筛选产品方向"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</TableSearchPanel>
|
||||
<TableSearchFields v-model="model" :fields="fields" :columns="3" @reset="reset" @search="search" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
Reference in New Issue
Block a user