feat(产品需求、项目需求): 支持手动录入"来源业务编号",前端的sourceBizId改为sourceBizCode,也支持按"来源业务编号"搜索。

This commit is contained in:
dk
2026-06-15 11:30:07 +08:00
parent 3c1cf6c7fa
commit 622d8d5a4d
12 changed files with 155 additions and 105 deletions

View File

@@ -210,7 +210,7 @@ type RequirementResponse = Omit<
| 'proposerId'
| 'currentHandlerUserId'
| 'implementProjectId'
| 'sourceBizId'
| 'sourceBizCode'
| 'attachments'
> & {
id: string | number;
@@ -220,7 +220,7 @@ type RequirementResponse = Omit<
currentHandlerUserId?: string | number | null;
implementProjectId?: string | number | null;
implementProjectName?: string | null;
sourceBizId?: string | number | null;
sourceBizCode?: string | null;
attachments?: AttachmentItemResponse[] | null;
children?: RequirementResponse[];
};
@@ -292,7 +292,7 @@ function normalizeRequirement(requirement: RequirementResponse): Api.Product.Req
currentHandlerUserId: normalizeNullableStringId(requirement.currentHandlerUserId),
implementProjectId: normalizeNullableStringId(requirement.implementProjectId),
implementProjectName: requirement.implementProjectName ?? null,
sourceBizId: normalizeNullableStringId(requirement.sourceBizId),
sourceBizCode: requirement.sourceBizCode ?? null,
attachments: normalizeAttachments(requirement.attachments),
children: requirement.children?.map(normalizeRequirement)
};

View File

@@ -1026,7 +1026,7 @@ const PROJECT_REQUIREMENT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project/requir
type ProjectRequirementResponse = Omit<
Api.Project.ProjectRequirement,
'id' | 'projectId' | 'parentId' | 'moduleId' | 'proposerId' | 'currentHandlerUserId' | 'sourceBizId' | 'attachments'
'id' | 'projectId' | 'parentId' | 'moduleId' | 'proposerId' | 'currentHandlerUserId' | 'sourceBizCode' | 'attachments'
> & {
id: string | number;
projectId: string | number;
@@ -1034,7 +1034,7 @@ type ProjectRequirementResponse = Omit<
moduleId: string | number;
proposerId: string | number;
currentHandlerUserId?: string | number | null;
sourceBizId?: string | number | null;
sourceBizCode?: string | null;
attachments?: AttachmentItemResponse[] | null;
children?: ProjectRequirementResponse[];
};
@@ -1090,7 +1090,7 @@ function normalizeProjectRequirement(requirement: ProjectRequirementResponse): A
moduleId: normalizeStringId(requirement.moduleId),
proposerId: normalizeStringId(requirement.proposerId),
currentHandlerUserId: normalizeNullableStringId(requirement.currentHandlerUserId),
sourceBizId: normalizeNullableStringId(requirement.sourceBizId),
sourceBizCode: requirement.sourceBizCode ?? null,
attachments: normalizeAttachments(requirement.attachments),
progressRate: typeof requirement.progressRate === 'number' ? requirement.progressRate : 0,
children: requirement.children?.map(normalizeProjectRequirement)

View File

@@ -332,8 +332,8 @@ declare namespace Api {
categoryName?: string | null;
/** 需求来源类型 */
sourceType: RequirementSourceType;
/** 需求来源业务ID */
sourceBizId?: string | null;
/** 来源业务编号 */
sourceBizCode?: string | null;
/** 优先级0低 1中 2高 3紧急 */
priority: RequirementPriority;
/** 优先级名称 */
@@ -508,7 +508,7 @@ declare namespace Api {
Pick<PageParams, 'pageNo' | 'pageSize'> &
Pick<
Requirement,
'moduleId' | 'category' | 'priority' | 'statusCode' | 'currentHandlerUserId' | 'sourceType'
'moduleId' | 'category' | 'priority' | 'statusCode' | 'currentHandlerUserId' | 'sourceBizCode'
> & {
productId: string;
title?: string;
@@ -526,6 +526,7 @@ declare namespace Api {
| 'attachments'
| 'category'
| 'priority'
| 'sourceBizCode'
| 'proposerId'
| 'proposerNickname'
| 'currentHandlerUserId'

View File

@@ -1160,8 +1160,8 @@ declare namespace Api {
categoryName?: string | null;
/** 需求来源类型 */
sourceType: ProjectRequirementSourceType;
/** 来源业务 ID */
sourceBizId?: string | null;
/** 来源业务编号 */
sourceBizCode?: string | null;
/** 优先级 */
priority: ProjectRequirementPriority;
/** 优先级名称 */
@@ -1285,7 +1285,7 @@ declare namespace Api {
Pick<PageParams, 'pageNo' | 'pageSize'> &
Pick<
ProjectRequirement,
'moduleId' | 'parentId' | 'category' | 'priority' | 'statusCode' | 'currentHandlerUserId' | 'sourceType'
'moduleId' | 'parentId' | 'category' | 'priority' | 'statusCode' | 'currentHandlerUserId' | 'sourceBizCode'
> & {
projectId: string;
title: string;
@@ -1303,6 +1303,7 @@ declare namespace Api {
| 'attachments'
| 'category'
| 'priority'
| 'sourceBizCode'
| 'proposerId'
| 'proposerNickname'
| 'currentHandlerUserId'

View File

@@ -7,8 +7,7 @@ import dayjs from 'dayjs';
import {
RDMS_REQ_CAN_DELETE_STATUS_DICT_CODE,
RDMS_REQ_CATEGORY_DICT_CODE,
RDMS_REQ_PRIORITY_DICT_CODE,
RDMS_REQ_SOURCE_TYPE_DICT_CODE
RDMS_REQ_PRIORITY_DICT_CODE
} from '@/constants/dict';
import { getStatusTagType } from '@/constants/status-tag';
import {
@@ -25,7 +24,6 @@ import {
import { useAuth } from '@/hooks/business/auth';
import { useDict } from '@/hooks/business/dict';
import DictTag from '@/components/custom/dict-tag.vue';
import DictText from '@/components/custom/dict-text.vue';
import { useCurrentProduct } from '../shared/use-current-product';
import {
ACTION_ICON_MAP,
@@ -181,7 +179,7 @@ const searchParams = reactive({
priority: undefined as Api.Product.RequirementPriority | undefined,
statusCode: undefined as Api.Product.RequirementStatusCode | undefined,
currentHandlerUserId: undefined as string | undefined,
sourceType: undefined as Api.Product.RequirementSourceType | undefined
sourceBizCode: undefined as string | undefined
});
const createVisible = ref(false);
@@ -384,15 +382,15 @@ const columns = computed(() => [
minWidth: 80,
formatter: (row: Api.Product.Requirement) => row.category
},
{
prop: 'sourceType',
label: '需求来源',
minWidth: 80,
align: 'center',
formatter: (row: Api.Product.Requirement) => (
<DictText dictCode={RDMS_REQ_SOURCE_TYPE_DICT_CODE} value={row.sourceType} />
)
},
// {
// prop: 'sourceType',
// label: '需求来源',
// minWidth: 80,
// align: 'center',
// formatter: (row: Api.Product.Requirement) => (
// <DictText dictCode={RDMS_REQ_SOURCE_TYPE_DICT_CODE} value={row.sourceType} />
// )
// },
// {
// prop: 'description',
// label: '内容',
@@ -415,19 +413,15 @@ const columns = computed(() => [
formatter: (row: Api.Product.Requirement) => getMemberLabel(row.currentHandlerUserId)
},
{
prop: 'sourceBizId',
prop: 'sourceBizCode',
label: '来源业务编号',
minWidth: 140,
formatter: (row: Api.Product.Requirement) => {
if (!row.sourceBizId || row.sourceType === 'manual') {
if (!row.sourceBizCode) {
return '--';
}
return (
<ElButton link type="primary" class="requirement-source-link">
{row.sourceBizId}
</ElButton>
);
return row.sourceBizCode;
}
},
{
@@ -649,7 +643,7 @@ async function loadTreeData() {
priority: searchParams.priority,
statusCode: searchParams.statusCode,
currentHandlerUserId: searchParams.currentHandlerUserId,
sourceType: searchParams.sourceType
sourceBizCode: searchParams.sourceBizCode?.trim() || undefined
});
if (error || !data) {
@@ -705,7 +699,7 @@ function handleResetSearch() {
searchParams.priority = undefined;
searchParams.statusCode = undefined;
searchParams.currentHandlerUserId = undefined;
searchParams.sourceType = undefined;
searchParams.sourceBizCode = undefined;
pagination.pageNo = 1;
reloadTable();
}
@@ -1057,10 +1051,6 @@ onMounted(async () => {
opacity: 0.6;
}
:deep(.requirement-source-link) {
padding: 0;
}
:deep(.el-table__row[class*='el-table__row--level-']:not(.el-table__row--level-0) td:first-child .cell) {
color: transparent;
}

View File

@@ -74,6 +74,7 @@ interface Model {
moduleId: string | null;
category: string;
priority: string | null;
sourceBizCode: string;
expectedTime: string | null;
proposerId: string;
currentHandlerUserId: string;
@@ -139,6 +140,7 @@ function createDefaultModel(): Model {
moduleId: props.defaultModuleId || null,
category: '功能需求',
priority: '3',
sourceBizCode: '',
expectedTime: null,
proposerId: '',
currentHandlerUserId: '',
@@ -186,6 +188,7 @@ async function handleSubmit() {
attachments: [...model.value.attachments],
category: model.value.category,
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
sourceBizCode: model.value.sourceBizCode.trim() || null,
expectedTime: model.value.expectedTime,
proposerId: model.value.proposerId,
proposerNickname,
@@ -315,6 +318,10 @@ watch(
/>
</ElFormItem>
<ElFormItem label="来源业务编号">
<ElInput v-model="model.sourceBizCode" clearable maxlength="128" placeholder="请输入来源业务编号" />
</ElFormItem>
<ElFormItem label="提出人" prop="proposerId">
<ElSelect v-model="model.proposerId" class="w-full" filterable placeholder="请选择提出人">
<ElOption v-for="item in allUserOptions" :key="item.id" :label="item.nickname" :value="item.id" />

View File

@@ -64,6 +64,7 @@ interface Model {
moduleId: string | null;
category: string;
priority: string | null;
sourceBizCode: string;
expectedTime: string | null;
proposerId: string;
proposerNickname: string;
@@ -201,6 +202,7 @@ function createDefaultModel(): Model {
moduleId: null,
category: '',
priority: '3',
sourceBizCode: '',
expectedTime: null,
proposerId: '',
proposerNickname: '',
@@ -252,6 +254,7 @@ async function handleSubmit() {
attachments: [...model.value.attachments],
category: model.value.category,
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
sourceBizCode: model.value.sourceBizCode.trim() || null,
expectedTime: model.value.expectedTime,
proposerId: model.value.proposerId,
proposerNickname: model.value.proposerNickname,
@@ -317,6 +320,7 @@ function transformRequirementData(data: Api.Product.Requirement): typeof model.v
moduleId: data.moduleId || null,
category: data.category || '',
priority: data.priority === null || data.priority === undefined ? null : String(data.priority),
sourceBizCode: data.sourceBizCode || '',
expectedTime: formatExpectedTime(data.expectedTime),
proposerId: data.proposerId || '',
proposerNickname: data.proposerNickname || '',
@@ -446,6 +450,17 @@ watch(
<ReadonlyField :value="getCategoryLabel(model.category) || '--'" />
</ElFormItem>
<ElFormItem label="来源业务编号">
<ReadonlyField v-if="isViewMode" :value="model.sourceBizCode || '--'" />
<ElInput
v-else
v-model="model.sourceBizCode"
clearable
maxlength="128"
placeholder="请输入来源业务编号"
/>
</ElFormItem>
<ElFormItem label="提出人" prop="proposerId">
<ReadonlyField :value="allUserLabelMap.get(model.proposerId) || '--'" />
</ElFormItem>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed, h, onMounted, ref } from 'vue';
import { RDMS_REQ_SOURCE_TYPE_DICT_CODE } from '@/constants/dict';
// import { RDMS_REQ_SOURCE_TYPE_DICT_CODE } from '@/constants/dict';
import { fetchGetRequirementStatusDict } from '@/service/api';
import { useDict } from '@/hooks/business/dict';
// import { useDict } from '@/hooks/business/dict';
import TableSearchFields from '@/components/custom/table-search-fields.vue';
import MemberSelectOption from './member-select-option.vue';
@@ -32,17 +32,15 @@ const emit = defineEmits<Emits>();
const model = defineModel<Api.Product.RequirementSearchParams>('model', { required: true });
const requirementStatusOptions = ref<Array<{ label: string; value: string }>>([]);
const { enabledDictData: sourceTypeDictData } = useDict(RDMS_REQ_SOURCE_TYPE_DICT_CODE);
const sourceTypeOptions = computed(() => {
return sourceTypeDictData.value
.filter(item => item.value !== 'product_requirement')
.map(item => ({
label: item.label,
value: item.value
}));
});
// const { enabledDictData: sourceTypeDictData } = useDict(RDMS_REQ_SOURCE_TYPE_DICT_CODE);
// const sourceTypeOptions = computed(() => {
// return sourceTypeDictData.value
// .filter(item => item.value !== 'product_requirement')
// .map(item => ({
// label: item.label,
// value: item.value
// }));
// });
const memberSelectOptions = computed(() => {
return props.memberOptions.map(item => ({
@@ -105,12 +103,18 @@ const fields = computed(() => [
dictCode: props.categoryDictCode,
placeholder: '筛选需求类型'
},
// {
// key: 'sourceType',
// label: '需求来源',
// type: 'select' as const,
// placeholder: '筛选需求来源',
// options: sourceTypeOptions.value
// },
{
key: 'sourceType',
label: '需求来源',
type: 'select' as const,
placeholder: '筛选需求来源',
options: sourceTypeOptions.value
key: 'sourceBizCode',
label: '业务编号',
type: 'input' as const,
placeholder: '输入来源业务编号'
},
{
key: 'currentHandlerUserId',

View File

@@ -6,8 +6,7 @@ import dayjs from 'dayjs';
import {
RDMS_REQ_CAN_DELETE_STATUS_DICT_CODE,
RDMS_REQ_CATEGORY_DICT_CODE,
RDMS_REQ_PRIORITY_DICT_CODE,
RDMS_REQ_SOURCE_TYPE_DICT_CODE
RDMS_REQ_PRIORITY_DICT_CODE
} from '@/constants/dict';
import { getStatusTagType } from '@/constants/status-tag';
import {
@@ -21,7 +20,6 @@ import {
import { useAuth } from '@/hooks/business/auth';
import { useDict } from '@/hooks/business/dict';
import DictTag from '@/components/custom/dict-tag.vue';
import DictText from '@/components/custom/dict-text.vue';
import { useCurrentProject } from '../../shared/use-current-project';
import {
ACTION_ICON_MAP,
@@ -78,7 +76,7 @@ function createSearchParams(): Api.Project.ProjectRequirementSearchParams {
priority: undefined,
statusCode: undefined,
currentHandlerUserId: undefined,
sourceType: undefined
sourceBizCode: undefined
};
}
@@ -386,15 +384,15 @@ const columns = computed(() => [
minWidth: 120,
formatter: (row: Api.Project.ProjectRequirement) => row.categoryName || row.category || '--'
},
{
prop: 'sourceType',
label: '需求来源',
minWidth: 110,
align: 'center',
formatter: (row: Api.Project.ProjectRequirement) => (
<DictText dictCode={RDMS_REQ_SOURCE_TYPE_DICT_CODE} value={row.sourceType} />
)
},
// {
// prop: 'sourceType',
// label: '需求来源',
// minWidth: 110,
// align: 'center',
// formatter: (row: Api.Project.ProjectRequirement) => (
// <DictText dictCode={RDMS_REQ_SOURCE_TYPE_DICT_CODE} value={row.sourceType} />
// )
// },
{
prop: 'proposerNickname',
label: '提出人',
@@ -408,15 +406,15 @@ const columns = computed(() => [
formatter: (row: Api.Project.ProjectRequirement) => getMemberLabel(row.currentHandlerUserId)
},
{
prop: 'sourceBizId',
prop: 'sourceBizCode',
label: '来源业务编号',
minWidth: 140,
formatter: (row: Api.Project.ProjectRequirement) => {
if (!row.sourceBizId || row.sourceType === 'manual') {
if (!row.sourceBizCode) {
return '--';
}
return row.sourceBizId;
return row.sourceBizCode;
}
},
{
@@ -552,7 +550,7 @@ async function loadTreeData() {
priority: searchParams.priority,
statusCode: searchParams.statusCode,
currentHandlerUserId: searchParams.currentHandlerUserId,
sourceType: searchParams.sourceType
sourceBizCode: searchParams.sourceBizCode?.trim() || undefined
});
if (error || !data) {

View File

@@ -73,6 +73,7 @@ interface Model {
moduleId: string | null;
category: string;
priority: string | null;
sourceBizCode: string;
expectedTime: string | null;
proposerId: string;
currentHandlerUserId: string;
@@ -141,6 +142,7 @@ function createDefaultModel(): Model {
moduleId: props.defaultModuleId || null,
category: '功能需求',
priority: '3',
sourceBizCode: '',
expectedTime: null,
proposerId: '',
currentHandlerUserId: '',
@@ -202,6 +204,7 @@ async function handleSubmit() {
attachments: [...model.value.attachments],
category: model.value.category,
priority: Number(model.value.priority) as Api.Project.ProjectRequirementPriority,
sourceBizCode: model.value.sourceBizCode.trim() || null,
expectedTime: model.value.expectedTime,
proposerId: model.value.proposerId,
proposerNickname: proposer?.nickname || '',
@@ -309,6 +312,10 @@ watch(
/>
</ElFormItem>
<ElFormItem label="来源业务编号">
<ElInput v-model="model.sourceBizCode" clearable maxlength="128" placeholder="请输入来源业务编号" />
</ElFormItem>
<ElFormItem label="提出人" prop="proposerId">
<ElSelect v-model="model.proposerId" class="w-full" filterable placeholder="请选择提出人">
<ElOption v-for="item in allUserOptions" :key="item.id" :label="item.nickname" :value="item.id" />

View File

@@ -62,6 +62,7 @@ interface Model {
moduleId: string | null;
category: string;
priority: string | null;
sourceBizCode: string;
expectedTime: string | null;
proposerId: string;
proposerNickname: string;
@@ -181,6 +182,7 @@ function createDefaultModel(): Model {
moduleId: null,
category: '',
priority: '3',
sourceBizCode: '',
expectedTime: null,
proposerId: '',
proposerNickname: '',
@@ -201,6 +203,30 @@ function mapModuleTree(modules: Api.Project.ProjectRequirementModule[]): Require
}));
}
function normalizePriorityValue(priority: Api.Project.ProjectRequirement['priority']) {
return priority === null || priority === undefined ? null : String(priority);
}
function transformRequirementDetail(data: Api.Project.ProjectRequirement): Model {
return {
title: data.title || '',
description: data.description || null,
attachments: data.attachments ? [...data.attachments] : [],
reviewRequired: data.reviewRequired ?? 0,
moduleId: data.moduleId || null,
category: data.category || '',
priority: normalizePriorityValue(data.priority),
sourceBizCode: data.sourceBizCode || '',
expectedTime: formatExpectedTime(data.expectedTime),
proposerId: data.proposerId || '',
proposerNickname: data.proposerNickname || '',
currentHandlerUserId: data.currentHandlerUserId || '',
currentHandlerUserNickname: data.currentHandlerUserNickname || '',
sort: data.sort ?? 0,
lastStatusReason: data.lastStatusReason || ''
};
}
async function loadModuleTree() {
if (!props.projectId) {
moduleTree.value = [];
@@ -232,22 +258,7 @@ async function loadRequirementDetail() {
return;
}
model.value = {
title: data.title || '',
description: data.description || null,
attachments: data.attachments ? [...data.attachments] : [],
reviewRequired: data.reviewRequired ?? 0,
moduleId: data.moduleId || null,
category: data.category || '',
priority: data.priority === null || data.priority === undefined ? null : String(data.priority),
expectedTime: formatExpectedTime(data.expectedTime),
proposerId: data.proposerId || '',
proposerNickname: data.proposerNickname || '',
currentHandlerUserId: data.currentHandlerUserId || '',
currentHandlerUserNickname: data.currentHandlerUserNickname || '',
sort: data.sort ?? 0,
lastStatusReason: data.lastStatusReason || ''
};
model.value = transformRequirementDetail(data);
}
function formatExpectedTime(value?: string | number[] | null): string | null {
@@ -294,6 +305,7 @@ async function handleSubmit() {
attachments: [...model.value.attachments],
category: model.value.category,
priority: Number(model.value.priority) as Api.Project.ProjectRequirementPriority,
sourceBizCode: model.value.sourceBizCode.trim() || null,
expectedTime: model.value.expectedTime,
proposerId: model.value.proposerId,
proposerNickname: model.value.proposerNickname,
@@ -401,6 +413,17 @@ watch(
<ReadonlyField :value="getCategoryLabel(model.category) || '--'" />
</ElFormItem>
<ElFormItem label="来源业务编号">
<ReadonlyField v-if="isViewMode" :value="model.sourceBizCode || '--'" />
<ElInput
v-else
v-model="model.sourceBizCode"
clearable
maxlength="128"
placeholder="请输入来源业务编号"
/>
</ElFormItem>
<ElFormItem label="提出人" prop="proposerId">
<ReadonlyField :value="allUserLabelMap.get(model.proposerId) || '--'" />
</ElFormItem>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed, h, onMounted, ref } from 'vue';
import { RDMS_REQ_SOURCE_TYPE_DICT_CODE } from '@/constants/dict';
// import { RDMS_REQ_SOURCE_TYPE_DICT_CODE } from '@/constants/dict';
import { fetchGetProjectRequirementStatusDict } from '@/service/api';
import { useDict } from '@/hooks/business/dict';
// import { useDict } from '@/hooks/business/dict';
import TableSearchFields from '@/components/custom/table-search-fields.vue';
import MemberSelectOption from './member-select-option.vue';
@@ -32,15 +32,13 @@ const emit = defineEmits<Emits>();
const model = defineModel<Api.Project.ProjectRequirementSearchParams>('model', { required: true });
const requirementStatusOptions = ref<Array<{ label: string; value: string }>>([]);
const { enabledDictData: sourceTypeDictData } = useDict(RDMS_REQ_SOURCE_TYPE_DICT_CODE);
const sourceTypeOptions = computed(() => {
return sourceTypeDictData.value.map(item => ({
label: item.label,
value: item.value
}));
});
// const { enabledDictData: sourceTypeDictData } = useDict(RDMS_REQ_SOURCE_TYPE_DICT_CODE);
// const sourceTypeOptions = computed(() => {
// return sourceTypeDictData.value.map(item => ({
// label: item.label,
// value: item.value
// }));
// });
const memberSelectOptions = computed(() => {
return props.memberOptions.map(item => ({
@@ -103,12 +101,18 @@ const fields = computed(() => [
dictCode: props.categoryDictCode,
placeholder: '筛选需求类型'
},
// {
// key: 'sourceType',
// label: '需求来源',
// type: 'select' as const,
// placeholder: '筛选需求来源',
// options: sourceTypeOptions.value
// },
{
key: 'sourceType',
label: '需求来源',
type: 'select' as const,
placeholder: '筛选需求来源',
options: sourceTypeOptions.value
key: 'sourceBizCode',
label: '业务编号',
type: 'input' as const,
placeholder: '输入来源业务编号'
},
{
key: 'currentHandlerUserId',