fix(产品需求、项目需求): 按照会议所说进行修改。
This commit is contained in:
@@ -41,6 +41,7 @@
|
|||||||
"@antv/g2": "5.4.0",
|
"@antv/g2": "5.4.0",
|
||||||
"@antv/g6": "5.0.49",
|
"@antv/g6": "5.0.49",
|
||||||
"@better-scroll/core": "2.5.1",
|
"@better-scroll/core": "2.5.1",
|
||||||
|
"@iconify-vue/mingcute": "^1.0.5",
|
||||||
"@iconify/vue": "5.0.0",
|
"@iconify/vue": "5.0.0",
|
||||||
"@sa/axios": "workspace:*",
|
"@sa/axios": "workspace:*",
|
||||||
"@sa/color": "workspace:*",
|
"@sa/color": "workspace:*",
|
||||||
|
|||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -20,6 +20,9 @@ importers:
|
|||||||
'@better-scroll/core':
|
'@better-scroll/core':
|
||||||
specifier: 2.5.1
|
specifier: 2.5.1
|
||||||
version: 2.5.1
|
version: 2.5.1
|
||||||
|
'@iconify-vue/mingcute':
|
||||||
|
specifier: ^1.0.5
|
||||||
|
version: 1.0.5(vue@3.5.20(typescript@5.8.3))
|
||||||
'@iconify/vue':
|
'@iconify/vue':
|
||||||
specifier: 5.0.0
|
specifier: 5.0.0
|
||||||
version: 5.0.0(vue@3.5.20(typescript@5.8.3))
|
version: 5.0.0(vue@3.5.20(typescript@5.8.3))
|
||||||
@@ -854,6 +857,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||||
engines: {node: '>=18.18'}
|
engines: {node: '>=18.18'}
|
||||||
|
|
||||||
|
'@iconify-vue/mingcute@1.0.5':
|
||||||
|
resolution: {integrity: sha512-9g/iEU2XdobbfS6vKp01btfBlPiMqlqa+GujwYOc5WVJierhKt3dF0+tamomdk9vYcIsJiGcqOaKvrJF0g6prA==}
|
||||||
|
|
||||||
|
'@iconify/css-vue@1.0.2':
|
||||||
|
resolution: {integrity: sha512-KXG9zXTMmJLi1AF2ket+YWUGdSqFvIMSnCO789uOVpba6SZhqeUttu0JIaEcq2dNlt4oonwdtMyerkpRkAFYhw==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: '>=3.0.0'
|
||||||
|
|
||||||
'@iconify/json@2.2.380':
|
'@iconify/json@2.2.380':
|
||||||
resolution: {integrity: sha512-+Al/Q+mMB/nLz/tawmJEOkCs6+RKKVUS/Yg9I80h2yRpu0kIzxVLQRfF0NifXz/fH92vDVXbS399wio4lMVF4Q==}
|
resolution: {integrity: sha512-+Al/Q+mMB/nLz/tawmJEOkCs6+RKKVUS/Yg9I80h2yRpu0kIzxVLQRfF0NifXz/fH92vDVXbS399wio4lMVF4Q==}
|
||||||
|
|
||||||
@@ -6173,6 +6184,17 @@ snapshots:
|
|||||||
|
|
||||||
'@humanwhocodes/retry@0.4.3': {}
|
'@humanwhocodes/retry@0.4.3': {}
|
||||||
|
|
||||||
|
'@iconify-vue/mingcute@1.0.5(vue@3.5.20(typescript@5.8.3))':
|
||||||
|
dependencies:
|
||||||
|
'@iconify/css-vue': 1.0.2(vue@3.5.20(typescript@5.8.3))
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- vue
|
||||||
|
|
||||||
|
'@iconify/css-vue@1.0.2(vue@3.5.20(typescript@5.8.3))':
|
||||||
|
dependencies:
|
||||||
|
'@iconify/types': 2.0.0
|
||||||
|
vue: 3.5.20(typescript@5.8.3)
|
||||||
|
|
||||||
'@iconify/json@2.2.380':
|
'@iconify/json@2.2.380':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import type { VNode } from 'vue';
|
||||||
import { ElButton, ElCol, ElDatePicker, ElFormItem, ElInput, ElOption, ElRow, ElSelect } from 'element-plus';
|
import { ElButton, ElCol, ElDatePicker, ElFormItem, ElInput, ElOption, ElRow, ElSelect } from 'element-plus';
|
||||||
import DictSelect from './dict-select.vue';
|
import DictSelect from './dict-select.vue';
|
||||||
|
|
||||||
@@ -25,6 +26,8 @@ export interface SearchField {
|
|||||||
dictCode?: string;
|
dictCode?: string;
|
||||||
/** 占位提示文本 */
|
/** 占位提示文本 */
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
/** select 类型的自定义选项渲染函数 */
|
||||||
|
renderOption?: (option: Option) => VNode | VNode[] | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -142,7 +145,11 @@ function handleSearch() {
|
|||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||||
>
|
>
|
||||||
<ElOption v-for="opt in field.options" :key="opt.value" :label="opt.label" :value="opt.value" />
|
<ElOption v-for="opt in field.options" :key="opt.value" :label="opt.label" :value="opt.value">
|
||||||
|
<template v-if="field.renderOption" #default>
|
||||||
|
<component :is="field.renderOption(opt)" />
|
||||||
|
</template>
|
||||||
|
</ElOption>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
<ElDatePicker
|
<ElDatePicker
|
||||||
v-else-if="field.type === 'date'"
|
v-else-if="field.type === 'date'"
|
||||||
@@ -234,7 +241,11 @@ function handleSearch() {
|
|||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||||
>
|
>
|
||||||
<ElOption v-for="opt in field.options" :key="opt.value" :label="opt.label" :value="opt.value" />
|
<ElOption v-for="opt in field.options" :key="opt.value" :label="opt.label" :value="opt.value">
|
||||||
|
<template v-if="field.renderOption" #default>
|
||||||
|
<component :is="field.renderOption(opt)" />
|
||||||
|
</template>
|
||||||
|
</ElOption>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
<ElDatePicker
|
<ElDatePicker
|
||||||
v-else-if="field.type === 'date'"
|
v-else-if="field.type === 'date'"
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const RDMS_REQ_SOURCE_TYPE_DICT_CODE = 'rdms_req_source_type';
|
|||||||
* 需求优先级字典编码
|
* 需求优先级字典编码
|
||||||
*
|
*
|
||||||
* 对应业务字段:需求相关接口和页面中的 priority
|
* 对应业务字段:需求相关接口和页面中的 priority
|
||||||
* 来源口径:产品需求文档中定义,标签包括紧急、高、中、低
|
* 来源口径:产品需求文档中定义,标签包括P0、P1、P2、P3
|
||||||
*/
|
*/
|
||||||
export const RDMS_REQ_PRIORITY_DICT_CODE = 'rdms_req_priority';
|
export const RDMS_REQ_PRIORITY_DICT_CODE = 'rdms_req_priority';
|
||||||
|
|
||||||
|
|||||||
@@ -360,6 +360,25 @@ export async function fetchGetRequirementAllowedTransitions(requirementId: strin
|
|||||||
return mapServiceResult(result as ServiceRequestResult<Api.Product.RequirementLifecycleAction[]>, data => data);
|
return mapServiceResult(result as ServiceRequestResult<Api.Product.RequirementLifecycleAction[]>, data => data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 批量获取需求可执行的状态动作列表 */
|
||||||
|
export async function fetchGetRequirementAllowedTransitionsBatch(data: Api.Product.RequirementBatchReqVO) {
|
||||||
|
const result = await request<Api.Product.RequirementAllowedTransitionBatchRespVO[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${REQUIREMENT_PREFIX}/allowed-transitions/batch`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<Api.Product.RequirementAllowedTransitionBatchRespVO[]>,
|
||||||
|
data1 =>
|
||||||
|
data1.map(item => ({
|
||||||
|
requirementId: normalizeStringId(item.requirementId),
|
||||||
|
transitions: item.transitions
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取需求生命周期信息 */
|
/** 获取需求生命周期信息 */
|
||||||
export async function fetchGetRequirementLifecycle(requirementId: string, productId: string) {
|
export async function fetchGetRequirementLifecycle(requirementId: string, productId: string) {
|
||||||
const result = await request<Api.Product.RequirementLifecycleInfo>({
|
const result = await request<Api.Product.RequirementLifecycleInfo>({
|
||||||
@@ -404,6 +423,23 @@ export async function fetchHasDispatchedProjectRequirement(requirementId: string
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 批量判断产品需求是否已分流生成项目需求 */
|
||||||
|
export async function fetchHasDispatchedProjectRequirementBatch(data: Api.Product.RequirementBatchReqVO) {
|
||||||
|
const result = await request<Api.Product.RequirementHasDispatchedBatchRespVO[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${REQUIREMENT_PREFIX}/has-dispatched/batch`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<Api.Product.RequirementHasDispatchedBatchRespVO[]>, data1 =>
|
||||||
|
data1.map(item => ({
|
||||||
|
requirementId: normalizeStringId(item.requirementId),
|
||||||
|
hasDispatched: Boolean(item.hasDispatched)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** 根据当前产品需求id获取对应地,所流转到项目侧的项目需求id */
|
/** 根据当前产品需求id获取对应地,所流转到项目侧的项目需求id */
|
||||||
export async function fetchGetDispatchedProjectLink(productRequirementId: string) {
|
export async function fetchGetDispatchedProjectLink(productRequirementId: string) {
|
||||||
return request<{ projectRequirementId: string; projectId: string }>({
|
return request<{ projectRequirementId: string; projectId: string }>({
|
||||||
|
|||||||
@@ -970,6 +970,27 @@ export async function fetchGetProjectRequirementAllowedTransitions(requirementId
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 批量获取项目需求可执行状态动作列表 */
|
||||||
|
export async function fetchGetProjectRequirementAllowedTransitionsBatch(
|
||||||
|
data: Api.Project.ProjectRequirementBatchReqVO
|
||||||
|
) {
|
||||||
|
const result = await request<Api.Project.ProjectRequirementAllowedTransitionBatchRespVO[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/allowed-transitions/batch`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<Api.Project.ProjectRequirementAllowedTransitionBatchRespVO[]>,
|
||||||
|
data1 =>
|
||||||
|
data1.map(item => ({
|
||||||
|
requirementId: normalizeStringId(item.requirementId),
|
||||||
|
transitions: item.transitions
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取项目需求生命周期信息 */
|
/** 获取项目需求生命周期信息 */
|
||||||
export async function fetchGetProjectRequirementLifecycle(requirementId: string, projectId: string) {
|
export async function fetchGetProjectRequirementLifecycle(requirementId: string, projectId: string) {
|
||||||
const result = await request<Api.Project.ProjectRequirementLifecycleInfo>({
|
const result = await request<Api.Project.ProjectRequirementLifecycleInfo>({
|
||||||
|
|||||||
6
src/typings/api/dict.d.ts
vendored
6
src/typings/api/dict.d.ts
vendored
@@ -47,6 +47,8 @@ declare namespace Api {
|
|||||||
id: number;
|
id: number;
|
||||||
/** dict label */
|
/** dict label */
|
||||||
label: string;
|
label: string;
|
||||||
|
/** sign */
|
||||||
|
sign?: string | null;
|
||||||
/** dict value */
|
/** dict value */
|
||||||
value: string;
|
value: string;
|
||||||
/** dict type code */
|
/** dict type code */
|
||||||
@@ -65,6 +67,8 @@ declare namespace Api {
|
|||||||
interface FrontendDictData {
|
interface FrontendDictData {
|
||||||
/** dict label */
|
/** dict label */
|
||||||
label: string;
|
label: string;
|
||||||
|
/** sign */
|
||||||
|
sign?: string | null;
|
||||||
/** dict value */
|
/** dict value */
|
||||||
value: string;
|
value: string;
|
||||||
/** display order */
|
/** display order */
|
||||||
@@ -82,7 +86,7 @@ declare namespace Api {
|
|||||||
type DictDataSearchParams = CommonType.RecordNullable<Pick<DictData, 'label' | 'dictType' | 'status'>> & PageParams;
|
type DictDataSearchParams = CommonType.RecordNullable<Pick<DictData, 'label' | 'dictType' | 'status'>> & PageParams;
|
||||||
|
|
||||||
/** dict data save params */
|
/** dict data save params */
|
||||||
type SaveDictDataParams = Pick<DictData, 'label' | 'value' | 'dictType' | 'sort' | 'status'> & {
|
type SaveDictDataParams = Pick<DictData, 'label' | 'sign' | 'value' | 'dictType' | 'sort' | 'status'> & {
|
||||||
remark?: string | null;
|
remark?: string | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/typings/api/product.d.ts
vendored
27
src/typings/api/product.d.ts
vendored
@@ -305,12 +305,12 @@ declare namespace Api {
|
|||||||
currentHandlerUserId?: string | null;
|
currentHandlerUserId?: string | null;
|
||||||
/** 当前处理人姓名 */
|
/** 当前处理人姓名 */
|
||||||
currentHandlerUserNickname?: string | null;
|
currentHandlerUserNickname?: string | null;
|
||||||
/** 默认实现项目编号 */
|
/** 默认关联项目编号 */
|
||||||
implementProjectId?: string | null;
|
implementProjectId?: string | null;
|
||||||
/** 默认实现项目名称 */
|
/** 默认关联项目名称 */
|
||||||
implementProjectName?: string | null;
|
implementProjectName?: string | null;
|
||||||
/** 所需工时(小时) */
|
/** 预期完成日期 */
|
||||||
workHours: number;
|
expectedTime?: string | null;
|
||||||
/** 排序值 */
|
/** 排序值 */
|
||||||
sort: number;
|
sort: number;
|
||||||
/** 创建时间 */
|
/** 创建时间 */
|
||||||
@@ -378,6 +378,21 @@ declare namespace Api {
|
|||||||
availableActions: RequirementLifecycleAction[];
|
availableActions: RequirementLifecycleAction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RequirementBatchReqVO {
|
||||||
|
productId: string;
|
||||||
|
requirementIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequirementAllowedTransitionBatchRespVO {
|
||||||
|
requirementId: string;
|
||||||
|
transitions: RequirementLifecycleAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequirementHasDispatchedBatchRespVO {
|
||||||
|
requirementId: string;
|
||||||
|
hasDispatched: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 请求参数类型 ==========
|
// ========== 请求参数类型 ==========
|
||||||
|
|
||||||
/** 需求分页查询参数 */
|
/** 需求分页查询参数 */
|
||||||
@@ -408,7 +423,7 @@ declare namespace Api {
|
|||||||
| 'currentHandlerUserId'
|
| 'currentHandlerUserId'
|
||||||
| 'currentHandlerUserNickname'
|
| 'currentHandlerUserNickname'
|
||||||
| 'implementProjectId'
|
| 'implementProjectId'
|
||||||
| 'workHours'
|
| 'expectedTime'
|
||||||
| 'sort'
|
| 'sort'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@@ -447,7 +462,7 @@ declare namespace Api {
|
|||||||
| 'proposerNickname'
|
| 'proposerNickname'
|
||||||
| 'currentHandlerUserId'
|
| 'currentHandlerUserId'
|
||||||
| 'currentHandlerUserNickname'
|
| 'currentHandlerUserNickname'
|
||||||
| 'workHours'
|
| 'expectedTime'
|
||||||
| 'sort'
|
| 'sort'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|||||||
18
src/typings/api/project.d.ts
vendored
18
src/typings/api/project.d.ts
vendored
@@ -700,8 +700,8 @@ declare namespace Api {
|
|||||||
currentHandlerUserId?: string | null;
|
currentHandlerUserId?: string | null;
|
||||||
/** 当前处理人昵称 */
|
/** 当前处理人昵称 */
|
||||||
currentHandlerUserNickname?: string | null;
|
currentHandlerUserNickname?: string | null;
|
||||||
/** 所需工时 */
|
/** 预期完成日期 */
|
||||||
workHours: number;
|
expectedTime?: string | null;
|
||||||
/** 排序值 */
|
/** 排序值 */
|
||||||
sort: number;
|
sort: number;
|
||||||
/** 创建时间 */
|
/** 创建时间 */
|
||||||
@@ -763,6 +763,16 @@ declare namespace Api {
|
|||||||
availableActions: ProjectRequirementLifecycleAction[];
|
availableActions: ProjectRequirementLifecycleAction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProjectRequirementBatchReqVO {
|
||||||
|
projectId: string;
|
||||||
|
requirementIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectRequirementAllowedTransitionBatchRespVO {
|
||||||
|
requirementId: string;
|
||||||
|
transitions: ProjectRequirementLifecycleAction[];
|
||||||
|
}
|
||||||
|
|
||||||
/** 项目需求分页查询参数 */
|
/** 项目需求分页查询参数 */
|
||||||
type ProjectRequirementSearchParams = CommonType.RecordNullable<
|
type ProjectRequirementSearchParams = CommonType.RecordNullable<
|
||||||
Pick<PageParams, 'pageNo' | 'pageSize'> &
|
Pick<PageParams, 'pageNo' | 'pageSize'> &
|
||||||
@@ -790,7 +800,7 @@ declare namespace Api {
|
|||||||
| 'proposerNickname'
|
| 'proposerNickname'
|
||||||
| 'currentHandlerUserId'
|
| 'currentHandlerUserId'
|
||||||
| 'currentHandlerUserNickname'
|
| 'currentHandlerUserNickname'
|
||||||
| 'workHours'
|
| 'expectedTime'
|
||||||
| 'sort'
|
| 'sort'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@@ -828,7 +838,7 @@ declare namespace Api {
|
|||||||
| 'proposerNickname'
|
| 'proposerNickname'
|
||||||
| 'currentHandlerUserId'
|
| 'currentHandlerUserId'
|
||||||
| 'currentHandlerUserNickname'
|
| 'currentHandlerUserNickname'
|
||||||
| 'workHours'
|
| 'expectedTime'
|
||||||
| 'sort'
|
| 'sort'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|||||||
2
src/typings/api/system-manage.d.ts
vendored
2
src/typings/api/system-manage.d.ts
vendored
@@ -148,6 +148,7 @@ declare namespace Api {
|
|||||||
sex?: UserGender | null;
|
sex?: UserGender | null;
|
||||||
avatar?: string | null;
|
avatar?: string | null;
|
||||||
status: CommonStatus;
|
status: CommonStatus;
|
||||||
|
sort?: number;
|
||||||
loginIp?: string | null;
|
loginIp?: string | null;
|
||||||
resignedAt?: number | null;
|
resignedAt?: number | null;
|
||||||
loginDate?: number | null;
|
loginDate?: number | null;
|
||||||
@@ -178,6 +179,7 @@ declare namespace Api {
|
|||||||
mobile?: string | null;
|
mobile?: string | null;
|
||||||
sex?: UserGender | null;
|
sex?: UserGender | null;
|
||||||
avatar?: string | null;
|
avatar?: string | null;
|
||||||
|
sort?: number;
|
||||||
password?: string;
|
password?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ import {
|
|||||||
fetchDeleteRequirement,
|
fetchDeleteRequirement,
|
||||||
fetchGetProductMembers,
|
fetchGetProductMembers,
|
||||||
fetchGetProjectListByProductId,
|
fetchGetProjectListByProductId,
|
||||||
fetchGetRequirementAllowedTransitions,
|
fetchGetRequirementAllowedTransitionsBatch,
|
||||||
fetchGetRequirementStatusDict,
|
fetchGetRequirementStatusDict,
|
||||||
fetchGetRequirementTerminalStatusDict,
|
fetchGetRequirementTerminalStatusDict,
|
||||||
fetchGetRequirementTree,
|
fetchGetRequirementTree,
|
||||||
fetchHasDispatchedProjectRequirement
|
fetchHasDispatchedProjectRequirementBatch
|
||||||
} from '@/service/api';
|
} from '@/service/api';
|
||||||
import { useAuth } from '@/hooks/business/auth';
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
import { useDict } from '@/hooks/business/dict';
|
import { useDict } from '@/hooks/business/dict';
|
||||||
@@ -107,10 +107,10 @@ function getStatusLabel(statusCode: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const priorityTagTypeMap: Record<number, UI.ThemeColor> = {
|
const priorityTagTypeMap: Record<number, UI.ThemeColor> = {
|
||||||
0: 'info',
|
0: 'danger',
|
||||||
1: 'primary',
|
1: 'warning',
|
||||||
2: 'warning',
|
2: 'primary',
|
||||||
3: 'danger'
|
3: 'info'
|
||||||
};
|
};
|
||||||
const hasDispatchedMap = ref<Record<string, boolean>>({});
|
const hasDispatchedMap = ref<Record<string, boolean>>({});
|
||||||
|
|
||||||
@@ -122,6 +122,14 @@ function formatDateTime(value?: string | null) {
|
|||||||
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDate(value?: string | null) {
|
||||||
|
if (!value) {
|
||||||
|
return '--';
|
||||||
|
}
|
||||||
|
|
||||||
|
return dayjs(value).format('YYYY-MM-DD');
|
||||||
|
}
|
||||||
|
|
||||||
function isTerminalStatus(statusCode: string) {
|
function isTerminalStatus(statusCode: string) {
|
||||||
return terminalStatusOptions.value.includes(statusCode);
|
return terminalStatusOptions.value.includes(statusCode);
|
||||||
}
|
}
|
||||||
@@ -223,17 +231,6 @@ function flattenTree(nodes: Api.Product.Requirement[]): Api.Product.Requirement[
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectAllRequirementIds(nodes: Api.Product.Requirement[]): string[] {
|
|
||||||
const ids: string[] = [];
|
|
||||||
for (const node of nodes) {
|
|
||||||
ids.push(node.id);
|
|
||||||
if (node.children?.length) {
|
|
||||||
ids.push(...collectAllRequirementIds(node.children));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectRequirementIdsForActions(nodes: Api.Product.Requirement[]): string[] {
|
function collectRequirementIdsForActions(nodes: Api.Product.Requirement[]): string[] {
|
||||||
const ids: string[] = [];
|
const ids: string[] = [];
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
@@ -276,15 +273,18 @@ async function loadAllowedTransitionsForAll() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.all(
|
const { error, data } = await fetchGetRequirementAllowedTransitionsBatch({
|
||||||
idsToQuery.map(async id => {
|
productId: currentObjectId.value,
|
||||||
const { error, data } = await fetchGetRequirementAllowedTransitions(id, currentObjectId.value!);
|
requirementIds: idsToQuery
|
||||||
return { id, actions: error ? [] : data || [] };
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const { id, actions } of results) {
|
if (error || !data) {
|
||||||
newMap.set(id, actions);
|
allowedTransitionsMap.value = newMap;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of data) {
|
||||||
|
newMap.set(item.requirementId, item.transitions || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
allowedTransitionsMap.value = newMap;
|
allowedTransitionsMap.value = newMap;
|
||||||
@@ -304,12 +304,19 @@ async function loadHasDispatchedForAll() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(
|
const { error, data } = await fetchHasDispatchedProjectRequirementBatch({
|
||||||
idsToQuery.map(async id => {
|
productId: currentObjectId.value,
|
||||||
const { data } = await fetchHasDispatchedProjectRequirement(id, currentObjectId.value!);
|
requirementIds: idsToQuery
|
||||||
newMap[id] = Boolean(data);
|
});
|
||||||
})
|
|
||||||
);
|
if (error || !data) {
|
||||||
|
hasDispatchedMap.value = newMap;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of data) {
|
||||||
|
newMap[item.requirementId] = Boolean(item.hasDispatched);
|
||||||
|
}
|
||||||
|
|
||||||
hasDispatchedMap.value = newMap;
|
hasDispatchedMap.value = newMap;
|
||||||
}
|
}
|
||||||
@@ -339,12 +346,12 @@ const columns = computed(() => [
|
|||||||
label: '需求名称',
|
label: '需求名称',
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
formatter: (row: Api.Product.Requirement) => {
|
formatter: (row: Api.Product.Requirement) => {
|
||||||
const className = 'requirement-title';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ElButton link type="primary" class={className} onClick={() => openView(row)}>
|
<ElTooltip content={row.title} placement="top" show-after={300}>
|
||||||
{row.title}
|
<ElButton link type="primary" class="requirement-title" onClick={() => openView(row)}>
|
||||||
</ElButton>
|
{row.title}
|
||||||
|
</ElButton>
|
||||||
|
</ElTooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -360,19 +367,12 @@ const columns = computed(() => [
|
|||||||
{
|
{
|
||||||
prop: 'statusCode',
|
prop: 'statusCode',
|
||||||
label: '状态',
|
label: '状态',
|
||||||
width: 100,
|
width: 90,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
formatter: (row: Api.Product.Requirement) => (
|
formatter: (row: Api.Product.Requirement) => (
|
||||||
<ElTag type={getRequirementStatusTagType(row.statusCode)}>{getStatusLabel(row.statusCode)}</ElTag>
|
<ElTag type={getRequirementStatusTagType(row.statusCode)}>{getStatusLabel(row.statusCode)}</ElTag>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
prop: 'workHours',
|
|
||||||
label: '所需工时',
|
|
||||||
width: 75,
|
|
||||||
align: 'center',
|
|
||||||
formatter: (row: Api.Product.Requirement) => (row.workHours !== null ? `${row.workHours}h` : '--')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
prop: 'category',
|
prop: 'category',
|
||||||
label: '需求类型',
|
label: '需求类型',
|
||||||
@@ -400,13 +400,13 @@ const columns = computed(() => [
|
|||||||
{
|
{
|
||||||
prop: 'proposerNickname',
|
prop: 'proposerNickname',
|
||||||
label: '提出人',
|
label: '提出人',
|
||||||
minWidth: 70,
|
minWidth: 85,
|
||||||
formatter: (row: Api.Product.Requirement) => row.proposerNickname || '--'
|
formatter: (row: Api.Product.Requirement) => row.proposerNickname || '--'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'currentHandlerUserId',
|
prop: 'currentHandlerUserId',
|
||||||
label: '负责人',
|
label: '负责人',
|
||||||
minWidth: 70,
|
minWidth: 85,
|
||||||
formatter: (row: Api.Product.Requirement) => getMemberLabel(row.currentHandlerUserId)
|
formatter: (row: Api.Product.Requirement) => getMemberLabel(row.currentHandlerUserId)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -427,22 +427,24 @@ const columns = computed(() => [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'implementProjectId',
|
prop: 'implementProjectId',
|
||||||
label: '实现项目',
|
label: '关联项目',
|
||||||
minWidth: 140,
|
minWidth: 180,
|
||||||
formatter: (row: Api.Product.Requirement) => {
|
formatter: (row: Api.Product.Requirement) => {
|
||||||
if (!row.implementProjectId) return '--';
|
if (!row.implementProjectId) return '--';
|
||||||
const projectName = projectNameMap.value.get(row.implementProjectId) || row.implementProjectName || '--';
|
return projectNameMap.value.get(row.implementProjectId) || row.implementProjectName || '--';
|
||||||
return (
|
|
||||||
<ElButton link type="primary" class="implement-project-link" onClick={() => handleImplementProjectClick(row)}>
|
|
||||||
{projectName}
|
|
||||||
</ElButton>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'expectedTime',
|
||||||
|
label: '预期完成时间',
|
||||||
|
minWidth: 120,
|
||||||
|
align: 'center',
|
||||||
|
formatter: (row: Api.Product.Requirement) => formatDate(row.expectedTime)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: 'createTime',
|
prop: 'createTime',
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
minWidth: 180,
|
minWidth: 120,
|
||||||
formatter: (row: Api.Product.Requirement) => formatDateTime(row.createTime)
|
formatter: (row: Api.Product.Requirement) => formatDateTime(row.createTime)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -461,31 +463,39 @@ const columns = computed(() => [
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
if (canSplitRequirement(row) && hasObjectAuth('project:product:split')) {
|
if (hasObjectAuth('project:product:split')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'split',
|
key: 'split',
|
||||||
label: '拆分',
|
label: '拆分',
|
||||||
icon: ACTION_ICON_MAP.split,
|
icon: ACTION_ICON_MAP.split,
|
||||||
type: ACTION_TYPE_MAP.split,
|
type: ACTION_TYPE_MAP.split,
|
||||||
|
disabled: !canSplitRequirement(row),
|
||||||
onClick: () => openSplit(row)
|
onClick: () => openSplit(row)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (hasObjectAuth('project:product:update')) {
|
||||||
hasObjectAuth('project:product:update') &&
|
const canEdit = !isTerminalStatus(row.statusCode) && row.statusCode !== 'accepted' && !row.implementProjectId;
|
||||||
!isTerminalStatus(row.statusCode) &&
|
|
||||||
row.statusCode !== 'accepted' &&
|
|
||||||
!row.implementProjectId
|
|
||||||
) {
|
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
label: '编辑',
|
label: '编辑',
|
||||||
icon: ACTION_ICON_MAP.edit,
|
icon: ACTION_ICON_MAP.edit,
|
||||||
type: ACTION_TYPE_MAP.edit,
|
type: ACTION_TYPE_MAP.edit,
|
||||||
|
disabled: !canEdit,
|
||||||
onClick: () => openEdit(row)
|
onClick: () => openEdit(row)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (row.implementProjectId) {
|
||||||
|
actions.push({
|
||||||
|
key: 'forward',
|
||||||
|
label: '前往项目侧',
|
||||||
|
icon: ACTION_ICON_MAP.forward,
|
||||||
|
type: 'primary',
|
||||||
|
onClick: () => handleForwardToProjectRequirement(row)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const lifecycleActions = getRowActions(row);
|
const lifecycleActions = getRowActions(row);
|
||||||
const hasStatusAuth = hasObjectAuth('project:product:status');
|
const hasStatusAuth = hasObjectAuth('project:product:status');
|
||||||
|
|
||||||
@@ -534,6 +544,7 @@ const columns = computed(() => [
|
|||||||
size="small"
|
size="small"
|
||||||
class="requirement-action-icon-btn"
|
class="requirement-action-icon-btn"
|
||||||
type={action.type}
|
type={action.type}
|
||||||
|
disabled={action.disabled}
|
||||||
onClick={() => action.onClick()}
|
onClick={() => action.onClick()}
|
||||||
>
|
>
|
||||||
<IconComponent class="text-18px" />
|
<IconComponent class="text-18px" />
|
||||||
@@ -626,8 +637,7 @@ async function reloadTable() {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
await loadTreeData();
|
await loadTreeData();
|
||||||
await loadAllowedTransitionsForAll();
|
await Promise.all([loadAllowedTransitionsForAll(), loadHasDispatchedForAll()]);
|
||||||
await loadHasDispatchedForAll();
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -688,10 +698,10 @@ function openSplit(row: Api.Product.Requirement) {
|
|||||||
splitVisible.value = true;
|
splitVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleImplementProjectClick(row: Api.Product.Requirement) {
|
async function handleForwardToProjectRequirement(row: Api.Product.Requirement) {
|
||||||
if (!row.implementProjectId) return;
|
if (!row.implementProjectId) return;
|
||||||
|
|
||||||
router.push({
|
await router.replace({
|
||||||
path: '/project/project/requirement',
|
path: '/project/project/requirement',
|
||||||
query: {
|
query: {
|
||||||
objectId: row.implementProjectId
|
objectId: row.implementProjectId
|
||||||
@@ -809,8 +819,7 @@ watch(
|
|||||||
async id => {
|
async id => {
|
||||||
if (id) {
|
if (id) {
|
||||||
await Promise.all([loadMembers(), loadTreeData(), loadProjectOptions()]);
|
await Promise.all([loadMembers(), loadTreeData(), loadProjectOptions()]);
|
||||||
await loadAllowedTransitionsForAll();
|
await Promise.all([loadAllowedTransitionsForAll(), loadHasDispatchedForAll()]);
|
||||||
await loadHasDispatchedForAll();
|
|
||||||
} else {
|
} else {
|
||||||
memberOptions.value = [];
|
memberOptions.value = [];
|
||||||
treeData.value = [];
|
treeData.value = [];
|
||||||
@@ -954,6 +963,10 @@ onMounted(async () => {
|
|||||||
:deep(.requirement-title) {
|
:deep(.requirement-title) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
max-width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.requirement-title--terminal) {
|
:deep(.requirement-title--terminal) {
|
||||||
@@ -966,11 +979,6 @@ onMounted(async () => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.implement-project-link) {
|
|
||||||
padding: 0;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__row[class*='el-table__row--level-']:not(.el-table__row--level-0) td:first-child .cell) {
|
:deep(.el-table__row[class*='el-table__row--level-']:not(.el-table__row--level-0) td:first-child .cell) {
|
||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type Ref, computed, inject, ref } from 'vue';
|
import { type Ref, computed, inject, ref } from 'vue';
|
||||||
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
|
|
||||||
defineOptions({ name: 'ModuleTreeNode' });
|
defineOptions({ name: 'ModuleTreeNode' });
|
||||||
|
|
||||||
@@ -32,10 +33,23 @@ const emit = defineEmits([
|
|||||||
'updateNewChildModuleName'
|
'updateNewChildModuleName'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const { hasObjectAuth } = useAuth();
|
||||||
|
const isRootModule = computed(() => props.module.id === props.rootModuleId);
|
||||||
|
|
||||||
|
const hasAnyActionPermission = computed(() => {
|
||||||
|
if (isRootModule.value) {
|
||||||
|
return hasObjectAuth('project:product:create');
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
hasObjectAuth('project:product:create') ||
|
||||||
|
hasObjectAuth('project:product:update') ||
|
||||||
|
hasObjectAuth('project:product:delete')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const collapsedModuleIds = inject<Ref<Set<string>>>('collapsedModuleIds', ref(new Set()));
|
const collapsedModuleIds = inject<Ref<Set<string>>>('collapsedModuleIds', ref(new Set()));
|
||||||
const toggleCollapse = inject<(id: string) => void>('toggleCollapse', () => {});
|
const toggleCollapse = inject<(id: string) => void>('toggleCollapse', () => {});
|
||||||
|
|
||||||
const isRootModule = computed(() => props.module.id === props.rootModuleId);
|
|
||||||
const isSelected = computed(() => props.selectedModuleId === props.module.id);
|
const isSelected = computed(() => props.selectedModuleId === props.module.id);
|
||||||
const isEditing = computed(() => props.editingNodeId === props.module.id);
|
const isEditing = computed(() => props.editingNodeId === props.module.id);
|
||||||
const isAddingChild = computed(() => props.addingChildParentId === props.module.id);
|
const isAddingChild = computed(() => props.addingChildParentId === props.module.id);
|
||||||
@@ -141,35 +155,27 @@ function handleToggle() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isEditing" class="module-tree-item__actions" @click.stop>
|
<div v-if="!isEditing && hasAnyActionPermission" class="module-tree-item__actions" @click.stop>
|
||||||
<ElDropdown trigger="click">
|
<ElDropdown trigger="click">
|
||||||
<ElButton text size="small" class="module-tree-item__more-btn">
|
<ElButton text size="small" class="module-tree-item__more-btn">
|
||||||
<icon-mdi-dots-horizontal class="text-14px" />
|
<icon-mdi-dots-horizontal class="text-14px" />
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<ElDropdownMenu>
|
<ElDropdownMenu>
|
||||||
<ElDropdownItem
|
<ElDropdownItem v-if="hasObjectAuth('project:product:create')" @click="handleStartAddChild">
|
||||||
v-auth="{ code: 'project:product:create', source: 'object' }"
|
|
||||||
@click="handleStartAddChild"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-6px">
|
<div class="flex items-center gap-6px">
|
||||||
<icon-ic-round-plus class="text-14px" />
|
<icon-ic-round-plus class="text-14px" />
|
||||||
<span>新增子模块</span>
|
<span>新增子模块</span>
|
||||||
</div>
|
</div>
|
||||||
</ElDropdownItem>
|
</ElDropdownItem>
|
||||||
<ElDropdownItem
|
<ElDropdownItem v-if="!isRootModule && hasObjectAuth('project:product:update')" @click="handleStartEdit">
|
||||||
v-if="!isRootModule"
|
|
||||||
v-auth="{ code: 'project:product:update', source: 'object' }"
|
|
||||||
@click="handleStartEdit"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-6px">
|
<div class="flex items-center gap-6px">
|
||||||
<icon-mdi-pencil-outline class="text-14px" />
|
<icon-mdi-pencil-outline class="text-14px" />
|
||||||
<span>编辑</span>
|
<span>编辑</span>
|
||||||
</div>
|
</div>
|
||||||
</ElDropdownItem>
|
</ElDropdownItem>
|
||||||
<ElDropdownItem
|
<ElDropdownItem
|
||||||
v-if="!isRootModule && canDeleteModule"
|
v-if="!isRootModule && canDeleteModule && hasObjectAuth('project:product:delete')"
|
||||||
v-auth="{ code: 'project:product:delete', source: 'object' }"
|
|
||||||
divided
|
divided
|
||||||
@click="handleDelete"
|
@click="handleDelete"
|
||||||
>
|
>
|
||||||
@@ -241,73 +247,112 @@ function handleToggle() {
|
|||||||
.module-tree-node {
|
.module-tree-node {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item {
|
.module-tree-item {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
min-height: 42px;
|
min-height: 36px;
|
||||||
padding: 0 14px;
|
padding: 6px 12px;
|
||||||
border: 1px solid rgb(226 232 240 / 92%);
|
padding-left: 16px;
|
||||||
border-radius: 14px;
|
border-radius: 8px;
|
||||||
background-color: rgb(248 250 252 / 96%);
|
color: #475569;
|
||||||
color: rgb(71 85 105 / 94%);
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
border-color 0.2s ease,
|
background-color 0.15s ease,
|
||||||
background-color 0.2s ease,
|
color 0.15s ease;
|
||||||
color 0.2s ease,
|
}
|
||||||
transform 0.2s ease;
|
|
||||||
|
.module-tree-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 3px;
|
||||||
|
height: 0;
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
background-color: transparent;
|
||||||
|
transition:
|
||||||
|
height 0.15s ease,
|
||||||
|
background-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item:hover {
|
.module-tree-item:hover {
|
||||||
transform: translateY(-1px);
|
background-color: #f1f5f9;
|
||||||
border-color: rgb(148 163 184 / 56%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item.is-active {
|
.module-tree-item.is-active {
|
||||||
border-color: rgb(13 148 136 / 42%);
|
background-color: #f0fdfa;
|
||||||
background-color: rgb(240 253 250 / 98%);
|
color: #0d9488;
|
||||||
color: rgb(15 118 110 / 96%);
|
font-weight: 500;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item.is-root:not(.is-active) .module-tree-item__icon {
|
.module-tree-item.is-active::before {
|
||||||
color: rgb(13 148 136 / 80%);
|
height: 60%;
|
||||||
|
background-color: #14b8a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-tree-item.is-root {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-tree-item.is-root:hover {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-tree-item.is-root.is-active {
|
||||||
|
background-color: #f0fdfa;
|
||||||
|
color: #0d9488;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item--new {
|
.module-tree-item--new {
|
||||||
border-style: dashed;
|
border: 1px dashed #cbd5e1;
|
||||||
border-color: rgb(148 163 184 / 56%);
|
background-color: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item__icon {
|
.module-tree-item--new:hover {
|
||||||
display: flex;
|
background-color: #f1f5f9;
|
||||||
align-items: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: rgb(100 116 139 / 80%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item__toggle {
|
.module-tree-item__toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 20px;
|
width: 18px;
|
||||||
height: 20px;
|
height: 18px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
color: rgb(148 163 184);
|
color: #94a3b8;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-tree-item__toggle:hover {
|
||||||
|
background-color: #e2e8f0;
|
||||||
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item__toggle.is-expanded svg {
|
.module-tree-item__toggle.is-expanded svg {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-tree-item__icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-tree-item.is-active .module-tree-item__icon {
|
||||||
|
color: #14b8a6;
|
||||||
|
}
|
||||||
|
|
||||||
.module-tree-item__content {
|
.module-tree-item__content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -326,7 +371,7 @@ function handleToggle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item__input :deep(.el-input__inner) {
|
.module-tree-item__input :deep(.el-input__inner) {
|
||||||
height: 28px;
|
height: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item__actions {
|
.module-tree-item__actions {
|
||||||
@@ -334,7 +379,7 @@ function handleToggle() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item:hover .module-tree-item__actions {
|
.module-tree-item:hover .module-tree-item__actions {
|
||||||
@@ -347,5 +392,10 @@ function handleToggle() {
|
|||||||
|
|
||||||
.module-tree-item__more-btn {
|
.module-tree-item__more-btn {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-tree-item__more-btn:hover {
|
||||||
|
background-color: #e2e8f0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const rules = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isDispatchAction.value) {
|
if (isDispatchAction.value) {
|
||||||
baseRules.implementProjectId = [createRequiredRule('请选择实现项目')];
|
baseRules.implementProjectId = [createRequiredRule('请选择关联项目')];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTerminalAction.value) {
|
if (isTerminalAction.value) {
|
||||||
@@ -136,8 +136,8 @@ async function handleSubmit() {
|
|||||||
</ElRadioGroup>
|
</ElRadioGroup>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem v-if="isDispatchAction" label="实现项目" prop="implementProjectId">
|
<ElFormItem v-if="isDispatchAction" label="关联项目" prop="implementProjectId">
|
||||||
<ElSelect v-model="model.implementProjectId" class="w-full" filterable placeholder="请选择实现项目(必选)">
|
<ElSelect v-model="model.implementProjectId" class="w-full" filterable placeholder="请选择关联项目(必选)">
|
||||||
<ElOption v-for="item in projectOptions" :key="item.id" :label="item.projectName" :value="item.id" />
|
<ElOption v-for="item in projectOptions" :key="item.id" :label="item.projectName" :value="item.id" />
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, ref, watch } from 'vue';
|
import { computed, nextTick, ref, watch } from 'vue';
|
||||||
import { useResizeObserver } from '@vueuse/core';
|
import { useResizeObserver } from '@vueuse/core';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { fetchCreateRequirement, fetchGetRequirementModuleTree } from '@/service/api';
|
import { fetchCreateRequirement, fetchGetRequirementModuleTree } from '@/service/api';
|
||||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||||
import { useDict } from '@/hooks/business/dict';
|
import { useDict } from '@/hooks/business/dict';
|
||||||
@@ -47,6 +48,31 @@ const priorityOptions = computed(() => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const reviewRequiredOptions = [
|
||||||
|
{ label: '不需要', value: 0 },
|
||||||
|
{ label: '需要', value: 1 }
|
||||||
|
];
|
||||||
|
|
||||||
|
function buildExpectedTimeShortcut(text: string, mutator: (date: Date) => void) {
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
value: () => {
|
||||||
|
const date = new Date();
|
||||||
|
mutator(date);
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedTimeShortcuts = [
|
||||||
|
buildExpectedTimeShortcut('一星期', date => date.setDate(date.getDate() + 7)),
|
||||||
|
buildExpectedTimeShortcut('两星期', date => date.setDate(date.getDate() + 14)),
|
||||||
|
buildExpectedTimeShortcut('一个月', date => date.setMonth(date.getMonth() + 1)),
|
||||||
|
buildExpectedTimeShortcut('三个月', date => date.setMonth(date.getMonth() + 3)),
|
||||||
|
buildExpectedTimeShortcut('半年', date => date.setMonth(date.getMonth() + 6)),
|
||||||
|
buildExpectedTimeShortcut('一年', date => date.setFullYear(date.getFullYear() + 1))
|
||||||
|
];
|
||||||
|
|
||||||
interface Model {
|
interface Model {
|
||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
@@ -55,9 +81,9 @@ interface Model {
|
|||||||
moduleId: string;
|
moduleId: string;
|
||||||
category: string;
|
category: string;
|
||||||
priority: number | null;
|
priority: number | null;
|
||||||
|
expectedTime: string | null;
|
||||||
proposerId: string;
|
proposerId: string;
|
||||||
currentHandlerUserId: string;
|
currentHandlerUserId: string;
|
||||||
workHours: number | null;
|
|
||||||
sort: number;
|
sort: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,18 +119,12 @@ const flatModuleOptions = computed<Array<{ label: string; value: string }>>(() =
|
|||||||
return options;
|
return options;
|
||||||
});
|
});
|
||||||
|
|
||||||
const reviewRequiredOptions = [
|
|
||||||
{ label: '不需要', value: 0 },
|
|
||||||
{ label: '需要', value: 1 }
|
|
||||||
];
|
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
title: [createRequiredRule('请输入需求名称')],
|
title: [createRequiredRule('请输入需求名称')],
|
||||||
category: [createRequiredRule('请选择需求类型')],
|
category: [createRequiredRule('请选择需求类型')],
|
||||||
priority: [createRequiredRule('请选择优先级')],
|
priority: [createRequiredRule('请选择优先级')],
|
||||||
proposerId: [createRequiredRule('请选择提出人')],
|
proposerId: [createRequiredRule('请选择提出人')],
|
||||||
currentHandlerUserId: [createRequiredRule('请选择负责人')],
|
currentHandlerUserId: [createRequiredRule('请选择负责人')]
|
||||||
workHours: [createRequiredRule('请输入所需工时')]
|
|
||||||
} satisfies Record<string, App.Global.FormRule[]>;
|
} satisfies Record<string, App.Global.FormRule[]>;
|
||||||
|
|
||||||
const leftColRef = ref<HTMLElement>();
|
const leftColRef = ref<HTMLElement>();
|
||||||
@@ -146,9 +166,9 @@ function createDefaultModel(): Model {
|
|||||||
moduleId: props.defaultModuleId || '0',
|
moduleId: props.defaultModuleId || '0',
|
||||||
category: '功能需求',
|
category: '功能需求',
|
||||||
priority: 1,
|
priority: 1,
|
||||||
|
expectedTime: null,
|
||||||
proposerId: '',
|
proposerId: '',
|
||||||
currentHandlerUserId: '',
|
currentHandlerUserId: '',
|
||||||
workHours: null,
|
|
||||||
sort: 0
|
sort: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -183,12 +203,12 @@ async function handleSubmit() {
|
|||||||
attachments: [...model.value.attachments],
|
attachments: [...model.value.attachments],
|
||||||
category: model.value.category,
|
category: model.value.category,
|
||||||
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
|
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
|
||||||
|
expectedTime: model.value.expectedTime,
|
||||||
proposerId: model.value.proposerId,
|
proposerId: model.value.proposerId,
|
||||||
proposerNickname,
|
proposerNickname,
|
||||||
currentHandlerUserId: model.value.currentHandlerUserId,
|
currentHandlerUserId: model.value.currentHandlerUserId,
|
||||||
currentHandlerUserNickname,
|
currentHandlerUserNickname,
|
||||||
implementProjectId: null,
|
implementProjectId: null,
|
||||||
workHours: model.value.workHours || 0,
|
|
||||||
sort: model.value.sort
|
sort: model.value.sort
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -272,14 +292,17 @@ watch(
|
|||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="是否需要评审">
|
<ElFormItem label="是否需要评审">
|
||||||
<ElSelect v-model="model.reviewRequired" class="w-full" placeholder="请选择">
|
<ElRadioGroup v-model="model.reviewRequired">
|
||||||
<ElOption
|
<ElRadio
|
||||||
v-for="item in reviewRequiredOptions"
|
v-for="item in reviewRequiredOptions"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:label="item.label"
|
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
/>
|
border
|
||||||
</ElSelect>
|
style="width: 165px"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</ElRadio>
|
||||||
|
</ElRadioGroup>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="优先级" prop="priority">
|
<ElFormItem label="优先级" prop="priority">
|
||||||
@@ -288,17 +311,6 @@ watch(
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="所需工时" prop="workHours">
|
|
||||||
<ElInputNumber
|
|
||||||
v-model="model.workHours"
|
|
||||||
class="w-full"
|
|
||||||
:min="0"
|
|
||||||
:max="9999"
|
|
||||||
:precision="1"
|
|
||||||
placeholder="请输入所需工时"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem label="需求类型" prop="category">
|
<ElFormItem label="需求类型" prop="category">
|
||||||
<DictSelect
|
<DictSelect
|
||||||
v-model="model.category"
|
v-model="model.category"
|
||||||
@@ -334,9 +346,20 @@ watch(
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="排序值">
|
<ElFormItem label="预期完成时间">
|
||||||
<ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />
|
<ElDatePicker
|
||||||
|
v-model="model.expectedTime"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="请选择预期完成时间"
|
||||||
|
:shortcuts="expectedTimeShortcuts"
|
||||||
|
class="requirement-operate-dialog__date-picker"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
|
<!-- <ElFormItem label="排序值">-->
|
||||||
|
<!-- <ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />-->
|
||||||
|
<!-- </ElFormItem>-->
|
||||||
</BusinessFormSection>
|
</BusinessFormSection>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -397,4 +420,8 @@ watch(
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.requirement-operate-dialog__date-picker.el-date-editor.el-input) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, ref, watch } from 'vue';
|
import { computed, nextTick, ref, watch } from 'vue';
|
||||||
import { useResizeObserver } from '@vueuse/core';
|
import { useResizeObserver } from '@vueuse/core';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import {
|
import {
|
||||||
fetchGetProjectListByProductId,
|
fetchGetProjectListByProductId,
|
||||||
fetchGetRequirement,
|
fetchGetRequirement,
|
||||||
@@ -65,12 +66,12 @@ interface Model {
|
|||||||
moduleId: string;
|
moduleId: string;
|
||||||
category: string;
|
category: string;
|
||||||
priority: number | null;
|
priority: number | null;
|
||||||
|
expectedTime: string | null;
|
||||||
proposerId: string;
|
proposerId: string;
|
||||||
proposerNickname: string;
|
proposerNickname: string;
|
||||||
currentHandlerUserId: string;
|
currentHandlerUserId: string;
|
||||||
currentHandlerUserNickname: string;
|
currentHandlerUserNickname: string;
|
||||||
implementProjectId: string | null;
|
implementProjectId: string | null;
|
||||||
workHours: number | null;
|
|
||||||
sort: number;
|
sort: number;
|
||||||
lastStatusReason: string;
|
lastStatusReason: string;
|
||||||
}
|
}
|
||||||
@@ -147,6 +148,26 @@ const reviewRequiredOptions = [
|
|||||||
{ label: '需要', value: 1 }
|
{ label: '需要', value: 1 }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function buildExpectedTimeShortcut(text: string, mutator: (date: Date) => void) {
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
value: () => {
|
||||||
|
const date = new Date();
|
||||||
|
mutator(date);
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedTimeShortcuts = [
|
||||||
|
buildExpectedTimeShortcut('一星期', date => date.setDate(date.getDate() + 7)),
|
||||||
|
buildExpectedTimeShortcut('两星期', date => date.setDate(date.getDate() + 14)),
|
||||||
|
buildExpectedTimeShortcut('一个月', date => date.setMonth(date.getMonth() + 1)),
|
||||||
|
buildExpectedTimeShortcut('三个月', date => date.setMonth(date.getMonth() + 3)),
|
||||||
|
buildExpectedTimeShortcut('半年', date => date.setMonth(date.getMonth() + 6)),
|
||||||
|
buildExpectedTimeShortcut('一年', date => date.setFullYear(date.getFullYear() + 1))
|
||||||
|
];
|
||||||
|
|
||||||
const rules = computed(() => {
|
const rules = computed(() => {
|
||||||
const baseRules: Record<string, App.Global.FormRule[]> = {
|
const baseRules: Record<string, App.Global.FormRule[]> = {
|
||||||
title: isEditMode.value ? [createRequiredRule('请输入需求名称')] : [],
|
title: isEditMode.value ? [createRequiredRule('请输入需求名称')] : [],
|
||||||
@@ -198,12 +219,12 @@ function createDefaultModel(): Model {
|
|||||||
moduleId: '0',
|
moduleId: '0',
|
||||||
category: '',
|
category: '',
|
||||||
priority: 1,
|
priority: 1,
|
||||||
|
expectedTime: null,
|
||||||
proposerId: '',
|
proposerId: '',
|
||||||
proposerNickname: '',
|
proposerNickname: '',
|
||||||
currentHandlerUserId: '',
|
currentHandlerUserId: '',
|
||||||
currentHandlerUserNickname: '',
|
currentHandlerUserNickname: '',
|
||||||
implementProjectId: null,
|
implementProjectId: null,
|
||||||
workHours: null,
|
|
||||||
sort: 0,
|
sort: 0,
|
||||||
lastStatusReason: ''
|
lastStatusReason: ''
|
||||||
};
|
};
|
||||||
@@ -239,12 +260,12 @@ async function handleSubmit() {
|
|||||||
attachments: [...model.value.attachments],
|
attachments: [...model.value.attachments],
|
||||||
category: model.value.category,
|
category: model.value.category,
|
||||||
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
|
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
|
||||||
|
expectedTime: model.value.expectedTime,
|
||||||
proposerId: model.value.proposerId,
|
proposerId: model.value.proposerId,
|
||||||
proposerNickname: model.value.proposerNickname,
|
proposerNickname: model.value.proposerNickname,
|
||||||
currentHandlerUserId: model.value.currentHandlerUserId,
|
currentHandlerUserId: model.value.currentHandlerUserId,
|
||||||
currentHandlerUserNickname: handler?.userNickname || model.value.currentHandlerUserNickname,
|
currentHandlerUserNickname: handler?.userNickname || model.value.currentHandlerUserNickname,
|
||||||
implementProjectId: model.value.implementProjectId,
|
implementProjectId: model.value.implementProjectId,
|
||||||
workHours: model.value.workHours || 0,
|
|
||||||
sort: model.value.sort
|
sort: model.value.sort
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -304,17 +325,30 @@ function transformRequirementData(data: Api.Product.Requirement): typeof model.v
|
|||||||
moduleId: data.moduleId || '0',
|
moduleId: data.moduleId || '0',
|
||||||
category: data.category || '',
|
category: data.category || '',
|
||||||
priority: data.priority ?? null,
|
priority: data.priority ?? null,
|
||||||
|
expectedTime: formatExpectedTime(data.expectedTime),
|
||||||
proposerId: data.proposerId || '',
|
proposerId: data.proposerId || '',
|
||||||
proposerNickname: data.proposerNickname || '',
|
proposerNickname: data.proposerNickname || '',
|
||||||
currentHandlerUserId: data.currentHandlerUserId || '',
|
currentHandlerUserId: data.currentHandlerUserId || '',
|
||||||
currentHandlerUserNickname: data.currentHandlerUserNickname || '',
|
currentHandlerUserNickname: data.currentHandlerUserNickname || '',
|
||||||
implementProjectId: data.implementProjectId || null,
|
implementProjectId: data.implementProjectId || null,
|
||||||
workHours: data.workHours ?? null,
|
|
||||||
sort: data.sort ?? 0,
|
sort: data.sort ?? 0,
|
||||||
lastStatusReason: data.lastStatusReason || ''
|
lastStatusReason: data.lastStatusReason || ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatExpectedTime(value?: string | number[] | null): string | null {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const [year, month, day] = value;
|
||||||
|
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dayjs(value).format('YYYY-MM-DD');
|
||||||
|
}
|
||||||
|
|
||||||
async function loadRequirementDetail() {
|
async function loadRequirementDetail() {
|
||||||
if (!props.productId || !props.requirement?.id) {
|
if (!props.productId || !props.requirement?.id) {
|
||||||
return;
|
return;
|
||||||
@@ -402,21 +436,6 @@ watch(
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="所需工时">
|
|
||||||
<template v-if="isViewMode">
|
|
||||||
<ReadonlyField :value="model.workHours != null ? `${model.workHours}小时` : '--'" />
|
|
||||||
</template>
|
|
||||||
<ElInputNumber
|
|
||||||
v-else
|
|
||||||
v-model="model.workHours"
|
|
||||||
class="w-full"
|
|
||||||
:min="0"
|
|
||||||
:max="9999"
|
|
||||||
:precision="1"
|
|
||||||
placeholder="请输入所需工时"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem label="需求类型" prop="category">
|
<ElFormItem label="需求类型" prop="category">
|
||||||
<ReadonlyField :value="getCategoryLabel(model.category) || '--'" />
|
<ReadonlyField :value="getCategoryLabel(model.category) || '--'" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -447,24 +466,37 @@ watch(
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem v-if="isViewMode" label="实现项目">
|
<ElFormItem v-if="isViewMode" label="关联项目">
|
||||||
<ReadonlyField :value="projectOptionsMap.get(model.implementProjectId || '') || '--'" />
|
<ReadonlyField :value="projectOptionsMap.get(model.implementProjectId || '') || '--'" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="排序值">
|
<ElFormItem label="预期完成时间">
|
||||||
<template v-if="isViewMode">
|
<ReadonlyField v-if="isViewMode" :value="model.expectedTime || '--'" />
|
||||||
<ReadonlyField :value="model.sort" />
|
<ElDatePicker
|
||||||
</template>
|
|
||||||
<ElInputNumber
|
|
||||||
v-else
|
v-else
|
||||||
v-model="model.sort"
|
v-model="model.expectedTime"
|
||||||
class="w-full"
|
type="date"
|
||||||
:min="0"
|
value-format="YYYY-MM-DD"
|
||||||
:max="9999"
|
placeholder="请选择预期完成时间"
|
||||||
placeholder="请输入排序值"
|
:shortcuts="expectedTimeShortcuts"
|
||||||
|
class="requirement-operate-dialog__date-picker"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
|
<!-- <ElFormItem label="排序值">-->
|
||||||
|
<!-- <template v-if="isViewMode">-->
|
||||||
|
<!-- <ReadonlyField :value="model.sort" />-->
|
||||||
|
<!-- </template>-->
|
||||||
|
<!-- <ElInputNumber-->
|
||||||
|
<!-- v-else-->
|
||||||
|
<!-- v-model="model.sort"-->
|
||||||
|
<!-- class="w-full"-->
|
||||||
|
<!-- :min="0"-->
|
||||||
|
<!-- :max="9999"-->
|
||||||
|
<!-- placeholder="请输入排序值"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </ElFormItem>-->
|
||||||
|
|
||||||
<ElFormItem v-if="isViewMode && model.lastStatusReason" label="状态变更原因">
|
<ElFormItem v-if="isViewMode && model.lastStatusReason" label="状态变更原因">
|
||||||
<div class="requirement-operate-dialog__readonly-textarea">{{ model.lastStatusReason }}</div>
|
<div class="requirement-operate-dialog__readonly-textarea">{{ model.lastStatusReason }}</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -528,7 +560,7 @@ watch(
|
|||||||
.requirement-operate-dialog__readonly-textarea {
|
.requirement-operate-dialog__readonly-textarea {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100px;
|
min-height: 65px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: linear-gradient(180deg, rgb(241 245 249 / 98%), rgb(226 232 240 / 94%)), rgb(241 245 249);
|
background: linear-gradient(180deg, rgb(241 245 249 / 98%), rgb(226 232 240 / 94%)), rgb(241 245 249);
|
||||||
@@ -545,4 +577,8 @@ watch(
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.requirement-operate-dialog__date-picker.el-date-editor.el-input) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -216,15 +216,11 @@ async function handleDeleteModule(module: Api.Product.RequirementModule) {
|
|||||||
if (!currentObjectId.value) return;
|
if (!currentObjectId.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(`确定要删除模块 "${module.moduleName}" 吗?`, '删除确认', {
|
||||||
`确定要删除模块 "${module.moduleName}" 吗?该模块下的所有需求将被一并删除。`,
|
confirmButtonText: '确认删除',
|
||||||
'删除确认',
|
cancelButtonText: '取消',
|
||||||
{
|
type: 'warning'
|
||||||
confirmButtonText: '确认删除',
|
});
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -310,12 +306,12 @@ defineExpose({
|
|||||||
|
|
||||||
.requirement-module-tree-card :deep(.el-card__header) {
|
.requirement-module-tree-card :deep(.el-card__header) {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-bottom: none;
|
border-bottom: 1px solid #f1f5f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.requirement-module-tree-card :deep(.el-card__body) {
|
.requirement-module-tree-card :deep(.el-card__body) {
|
||||||
padding: 0 16px 16px;
|
padding: 12px 8px;
|
||||||
height: calc(100% - 48px);
|
height: calc(100% - 49px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,68 +322,34 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-header__title {
|
.module-tree-header__title {
|
||||||
color: rgb(15 23 42 / 94%);
|
color: #1e293b;
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-list {
|
.module-tree-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 2px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item {
|
.module-tree-list::-webkit-scrollbar {
|
||||||
display: flex;
|
width: 4px;
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 42px;
|
|
||||||
padding: 0 14px;
|
|
||||||
border: 1px solid rgb(226 232 240 / 92%);
|
|
||||||
border-radius: 14px;
|
|
||||||
background-color: rgb(248 250 252 / 96%);
|
|
||||||
color: rgb(71 85 105 / 94%);
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition:
|
|
||||||
border-color 0.2s ease,
|
|
||||||
background-color 0.2s ease,
|
|
||||||
color 0.2s ease,
|
|
||||||
transform 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item:hover {
|
.module-tree-list::-webkit-scrollbar-track {
|
||||||
transform: translateY(-1px);
|
background: transparent;
|
||||||
border-color: rgb(148 163 184 / 56%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item--new {
|
.module-tree-list::-webkit-scrollbar-thumb {
|
||||||
border-style: dashed;
|
background-color: #e2e8f0;
|
||||||
border-color: rgb(148 163 184 / 56%);
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item__icon {
|
.module-tree-list::-webkit-scrollbar-thumb:hover {
|
||||||
display: flex;
|
background-color: #cbd5e1;
|
||||||
align-items: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: rgb(100 116 139 / 80%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-tree-item__content {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-tree-item__input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-tree-item__input :deep(.el-input__inner) {
|
|
||||||
height: 28px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue';
|
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 { fetchGetRequirementStatusDict } from '@/service/api';
|
||||||
import { useDict } from '@/hooks/business/dict';
|
import { useDict } from '@/hooks/business/dict';
|
||||||
import DictSelect from '@/components/custom/dict-select.vue';
|
import TableSearchFields from '@/components/custom/table-search-fields.vue';
|
||||||
import TableSearchPanel from '@/components/custom/table-search-panel.vue';
|
|
||||||
import MemberSelectOption from './member-select-option.vue';
|
import MemberSelectOption from './member-select-option.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'RequirementSearch' });
|
defineOptions({ name: 'RequirementSearch' });
|
||||||
@@ -21,7 +20,7 @@ interface Props {
|
|||||||
priorityDictCode: string;
|
priorityDictCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'reset'): void;
|
(e: 'reset'): void;
|
||||||
@@ -45,6 +44,21 @@ const sourceTypeOptions = computed(() => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const memberSelectOptions = computed(() => {
|
||||||
|
return props.memberOptions.map(item => ({
|
||||||
|
label: item.nickname,
|
||||||
|
value: item.id,
|
||||||
|
roleName: item.roleName
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderMemberOption(option: { label: string; value: string | number; roleName?: string }) {
|
||||||
|
return h(MemberSelectOption, {
|
||||||
|
nickname: option.label,
|
||||||
|
roleName: option.roleName || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function loadStatusOptions() {
|
async function loadStatusOptions() {
|
||||||
const { error, data } = await fetchGetRequirementStatusDict();
|
const { error, data } = await fetchGetRequirementStatusDict();
|
||||||
|
|
||||||
@@ -59,77 +73,58 @@ async function loadStatusOptions() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
|
||||||
emit('reset');
|
|
||||||
}
|
|
||||||
|
|
||||||
function search() {
|
|
||||||
emit('search');
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadStatusOptions();
|
await loadStatusOptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fields = computed(() => [
|
||||||
|
{
|
||||||
|
key: 'title',
|
||||||
|
label: '需求名称',
|
||||||
|
type: 'input' as const,
|
||||||
|
placeholder: '输入需求名称'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'priority',
|
||||||
|
label: '优先级',
|
||||||
|
type: 'dict' as const,
|
||||||
|
dictCode: props.priorityDictCode,
|
||||||
|
placeholder: '筛选优先级'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'statusCode',
|
||||||
|
label: '状态',
|
||||||
|
type: 'select' as const,
|
||||||
|
placeholder: '筛选状态',
|
||||||
|
options: requirementStatusOptions.value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'category',
|
||||||
|
label: '需求类型',
|
||||||
|
type: 'dict' as const,
|
||||||
|
dictCode: props.categoryDictCode,
|
||||||
|
placeholder: '筛选需求类型'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sourceType',
|
||||||
|
label: '需求来源',
|
||||||
|
type: 'select' as const,
|
||||||
|
placeholder: '筛选需求来源',
|
||||||
|
options: sourceTypeOptions.value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'currentHandlerUserId',
|
||||||
|
label: '负责人',
|
||||||
|
type: 'select' as const,
|
||||||
|
placeholder: '筛选负责人',
|
||||||
|
options: memberSelectOptions.value,
|
||||||
|
renderOption: renderMemberOption
|
||||||
|
}
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TableSearchPanel :model="model" :action-col-lg="6" @reset="reset" @search="search">
|
<TableSearchFields v-model="model" :fields="fields" :columns="3" @search="emit('search')" @reset="emit('reset')" />
|
||||||
<ElCol :lg="6" :md="12" :sm="12">
|
|
||||||
<ElFormItem label="需求名称">
|
|
||||||
<ElInput v-model="model.title" clearable placeholder="输入需求名称" />
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
<ElCol :lg="6" :md="12" :sm="12">
|
|
||||||
<ElFormItem label="需求类型">
|
|
||||||
<DictSelect
|
|
||||||
v-model="model.category"
|
|
||||||
:dict-code="categoryDictCode"
|
|
||||||
clearable
|
|
||||||
filterable
|
|
||||||
placeholder="筛选需求类型"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
<ElCol :lg="6" :md="12" :sm="12">
|
|
||||||
<ElFormItem label="优先级">
|
|
||||||
<DictSelect v-model="model.priority" :dict-code="priorityDictCode" clearable placeholder="筛选优先级" />
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
<ElCol :lg="6" :md="12" :sm="12">
|
|
||||||
<ElFormItem label="状态">
|
|
||||||
<ElSelect v-model="model.statusCode" clearable placeholder="筛选状态">
|
|
||||||
<ElOption
|
|
||||||
v-for="item in requirementStatusOptions"
|
|
||||||
:key="item.value"
|
|
||||||
:label="item.label"
|
|
||||||
:value="item.value"
|
|
||||||
/>
|
|
||||||
</ElSelect>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
<ElCol :lg="6" :md="12" :sm="12">
|
|
||||||
<ElFormItem label="负责人">
|
|
||||||
<ElSelect
|
|
||||||
v-model="model.currentHandlerUserId"
|
|
||||||
clearable
|
|
||||||
filterable
|
|
||||||
placeholder="筛选负责人"
|
|
||||||
:filter-method="(val: string) => val"
|
|
||||||
>
|
|
||||||
<ElOption v-for="item in memberOptions" :key="item.id" :label="item.nickname" :value="item.id">
|
|
||||||
<MemberSelectOption :nickname="item.nickname" :role-name="item.roleName || ''" />
|
|
||||||
</ElOption>
|
|
||||||
</ElSelect>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
<ElCol :lg="6" :md="12" :sm="12">
|
|
||||||
<ElFormItem label="需求来源">
|
|
||||||
<ElSelect v-model="model.sourceType" clearable placeholder="筛选需求来源">
|
|
||||||
<ElOption v-for="item in sourceTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
||||||
</ElSelect>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
</TableSearchPanel>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, ref, watch } from 'vue';
|
import { computed, nextTick, ref, watch } from 'vue';
|
||||||
import { useResizeObserver } from '@vueuse/core';
|
import { useResizeObserver } from '@vueuse/core';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { fetchSplitRequirement } from '@/service/api';
|
import { fetchSplitRequirement } from '@/service/api';
|
||||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||||
import { useDict } from '@/hooks/business/dict';
|
import { useDict } from '@/hooks/business/dict';
|
||||||
@@ -46,6 +47,31 @@ const priorityOptions = computed(() => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const reviewRequiredOptions = [
|
||||||
|
{ label: '不需要', value: 0 },
|
||||||
|
{ label: '需要', value: 1 }
|
||||||
|
];
|
||||||
|
|
||||||
|
function buildExpectedTimeShortcut(text: string, mutator: (date: Date) => void) {
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
value: () => {
|
||||||
|
const date = new Date();
|
||||||
|
mutator(date);
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedTimeShortcuts = [
|
||||||
|
buildExpectedTimeShortcut('一星期', date => date.setDate(date.getDate() + 7)),
|
||||||
|
buildExpectedTimeShortcut('两星期', date => date.setDate(date.getDate() + 14)),
|
||||||
|
buildExpectedTimeShortcut('一个月', date => date.setMonth(date.getMonth() + 1)),
|
||||||
|
buildExpectedTimeShortcut('三个月', date => date.setMonth(date.getMonth() + 3)),
|
||||||
|
buildExpectedTimeShortcut('半年', date => date.setMonth(date.getMonth() + 6)),
|
||||||
|
buildExpectedTimeShortcut('一年', date => date.setFullYear(date.getFullYear() + 1))
|
||||||
|
];
|
||||||
|
|
||||||
interface Model {
|
interface Model {
|
||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
@@ -53,8 +79,8 @@ interface Model {
|
|||||||
reviewRequired: number;
|
reviewRequired: number;
|
||||||
category: string;
|
category: string;
|
||||||
priority: number | null;
|
priority: number | null;
|
||||||
|
expectedTime: string | null;
|
||||||
currentHandlerUserId: string;
|
currentHandlerUserId: string;
|
||||||
workHours: number | null;
|
|
||||||
sort: number;
|
sort: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,16 +92,10 @@ const memberUserOptions = computed(() => {
|
|||||||
return props.memberOptions.filter(m => m.status === 0);
|
return props.memberOptions.filter(m => m.status === 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
const reviewRequiredOptions = [
|
|
||||||
{ label: '不需要', value: 0 },
|
|
||||||
{ label: '需要', value: 1 }
|
|
||||||
];
|
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
title: [createRequiredRule('请输入子需求名称')],
|
title: [createRequiredRule('请输入子需求名称')],
|
||||||
priority: [createRequiredRule('请选择优先级')],
|
priority: [createRequiredRule('请选择优先级')],
|
||||||
currentHandlerUserId: [createRequiredRule('请选择负责人')],
|
currentHandlerUserId: [createRequiredRule('请选择负责人')]
|
||||||
workHours: [createRequiredRule('请输入所需工时')]
|
|
||||||
} satisfies Record<string, App.Global.FormRule[]>;
|
} satisfies Record<string, App.Global.FormRule[]>;
|
||||||
|
|
||||||
const leftColRef = ref<HTMLElement>();
|
const leftColRef = ref<HTMLElement>();
|
||||||
@@ -116,8 +136,8 @@ function createDefaultModel(): Model {
|
|||||||
reviewRequired: 0,
|
reviewRequired: 0,
|
||||||
category: '',
|
category: '',
|
||||||
priority: 1,
|
priority: 1,
|
||||||
|
expectedTime: null,
|
||||||
currentHandlerUserId: '',
|
currentHandlerUserId: '',
|
||||||
workHours: null,
|
|
||||||
sort: 0
|
sort: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -153,8 +173,8 @@ async function handleSubmit() {
|
|||||||
reviewRequired: model.value.reviewRequired as Api.Product.RequirementReviewRequired,
|
reviewRequired: model.value.reviewRequired as Api.Product.RequirementReviewRequired,
|
||||||
category: model.value.category,
|
category: model.value.category,
|
||||||
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
|
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
|
||||||
|
expectedTime: model.value.expectedTime,
|
||||||
currentHandlerUserId: model.value.currentHandlerUserId,
|
currentHandlerUserId: model.value.currentHandlerUserId,
|
||||||
workHours: model.value.workHours || 0,
|
|
||||||
sort: model.value.sort
|
sort: model.value.sort
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -192,6 +212,10 @@ watch(
|
|||||||
model.value.currentHandlerUserId = props.parentRequirement.currentHandlerUserId;
|
model.value.currentHandlerUserId = props.parentRequirement.currentHandlerUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (props.parentRequirement?.expectedTime) {
|
||||||
|
model.value.expectedTime = props.parentRequirement.expectedTime;
|
||||||
|
}
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
attachmentUploaderRef.value?.initSession();
|
attachmentUploaderRef.value?.initSession();
|
||||||
richTextEditorRef.value?.initSession();
|
richTextEditorRef.value?.initSession();
|
||||||
@@ -227,14 +251,17 @@ watch(
|
|||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="是否需要评审">
|
<ElFormItem label="是否需要评审">
|
||||||
<ElSelect v-model="model.reviewRequired" class="w-full" placeholder="请选择">
|
<ElRadioGroup v-model="model.reviewRequired">
|
||||||
<ElOption
|
<ElRadio
|
||||||
v-for="item in reviewRequiredOptions"
|
v-for="item in reviewRequiredOptions"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:label="item.label"
|
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
/>
|
border
|
||||||
</ElSelect>
|
style="width: 165px"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</ElRadio>
|
||||||
|
</ElRadioGroup>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="优先级" prop="priority">
|
<ElFormItem label="优先级" prop="priority">
|
||||||
@@ -243,17 +270,6 @@ watch(
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="所需工时" prop="workHours">
|
|
||||||
<ElInputNumber
|
|
||||||
v-model="model.workHours"
|
|
||||||
class="w-full"
|
|
||||||
:min="0"
|
|
||||||
:max="9999"
|
|
||||||
:precision="1"
|
|
||||||
placeholder="请输入所需工时"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem label="负责人" prop="currentHandlerUserId">
|
<ElFormItem label="负责人" prop="currentHandlerUserId">
|
||||||
<ElSelect v-model="model.currentHandlerUserId" class="w-full" filterable placeholder="请选择负责人">
|
<ElSelect v-model="model.currentHandlerUserId" class="w-full" filterable placeholder="请选择负责人">
|
||||||
<ElOption
|
<ElOption
|
||||||
@@ -267,9 +283,20 @@ watch(
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="排序值">
|
<ElFormItem label="预期完成时间">
|
||||||
<ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />
|
<ElDatePicker
|
||||||
|
v-model="model.expectedTime"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="请选择预期完成时间"
|
||||||
|
:shortcuts="expectedTimeShortcuts"
|
||||||
|
class="requirement-operate-dialog__date-picker"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
|
<!-- <ElFormItem label="排序值">-->
|
||||||
|
<!-- <ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />-->
|
||||||
|
<!-- </ElFormItem>-->
|
||||||
</BusinessFormSection>
|
</BusinessFormSection>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -330,4 +357,8 @@ watch(
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.requirement-operate-dialog__date-picker.el-date-editor.el-input) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { transformRecordToOption } from '@/utils/common';
|
|||||||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||||
import IconMdiCheckOutline from '~icons/mdi/check-outline';
|
import IconMdiCheckOutline from '~icons/mdi/check-outline';
|
||||||
import IconMdiCheckCircleOutline from '~icons/mdi/check-circle-outline';
|
import IconMdiCheckCircleOutline from '~icons/mdi/check-circle-outline';
|
||||||
import IconMdiSync from '~icons/mdi/sync';
|
|
||||||
import IconMdiPowerSettingsNew from '~icons/mdi/power-settings-new';
|
import IconMdiPowerSettingsNew from '~icons/mdi/power-settings-new';
|
||||||
import IconMdiShareVariant from '~icons/mdi/share-variant';
|
import IconMdiShareVariant from '~icons/mdi/share-variant';
|
||||||
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||||
@@ -12,6 +11,7 @@ import IconMdiClose from '~icons/mdi/close';
|
|||||||
import IconTablerSitemap from '~icons/tabler/sitemap';
|
import IconTablerSitemap from '~icons/tabler/sitemap';
|
||||||
import IconTablerCircleX from '~icons/tabler/circle-x';
|
import IconTablerCircleX from '~icons/tabler/circle-x';
|
||||||
import IconMaterialSymbolsDescriptionOutline from '~icons/material-symbols/description-outline';
|
import IconMaterialSymbolsDescriptionOutline from '~icons/material-symbols/description-outline';
|
||||||
|
import IconMingcuteForward2Line from '~icons/mingcute/forward-2-line';
|
||||||
|
|
||||||
export type RequirementStatusActionCode =
|
export type RequirementStatusActionCode =
|
||||||
| 'claim_to_review'
|
| 'claim_to_review'
|
||||||
@@ -55,6 +55,7 @@ export const requirementStatusActionRecord: Record<RequirementStatusActionCode,
|
|||||||
export const ACTION_ICON_MAP: Record<string, object> = {
|
export const ACTION_ICON_MAP: Record<string, object> = {
|
||||||
split: markRaw(IconTablerSitemap),
|
split: markRaw(IconTablerSitemap),
|
||||||
edit: markRaw(IconMdiPencilOutline),
|
edit: markRaw(IconMdiPencilOutline),
|
||||||
|
forward: markRaw(IconMingcuteForward2Line),
|
||||||
claim_to_review: markRaw(IconMaterialSymbolsDescriptionOutline),
|
claim_to_review: markRaw(IconMaterialSymbolsDescriptionOutline),
|
||||||
claim_to_dispatch: markRaw(IconMdiCheckOutline),
|
claim_to_dispatch: markRaw(IconMdiCheckOutline),
|
||||||
to_dispatch: markRaw(IconMdiGlasses),
|
to_dispatch: markRaw(IconMdiGlasses),
|
||||||
@@ -96,7 +97,7 @@ export function getRequirementStatusTagType(status: Api.Product.RequirementStatu
|
|||||||
pending_dispatch: 'primary',
|
pending_dispatch: 'primary',
|
||||||
implementing: 'primary',
|
implementing: 'primary',
|
||||||
accepted: 'success',
|
accepted: 'success',
|
||||||
closed: 'info',
|
closed: 'danger',
|
||||||
rejected: 'danger',
|
rejected: 'danger',
|
||||||
cancelled: 'danger'
|
cancelled: 'danger'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,29 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
|
import { OBJECT_CONTEXT_QUERY_KEY, getObjectContextDomainConfigByPath } from '@/constants/object-context';
|
||||||
import { useObjectContextStore } from '@/store/modules/object-context';
|
import { useObjectContextStore } from '@/store/modules/object-context';
|
||||||
import { normalizeCurrentProductSummary, resolveObjectIdFromQuery } from './product-context-shared';
|
import { normalizeCurrentProductSummary, resolveObjectIdFromQuery } from './product-context-shared';
|
||||||
|
|
||||||
export function useCurrentProduct() {
|
export function useCurrentProduct() {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const objectContextStore = useObjectContextStore();
|
const objectContextStore = useObjectContextStore();
|
||||||
|
const isProductDomainRoute = computed(() => getObjectContextDomainConfigByPath(route.path)?.domainKey === 'product');
|
||||||
|
|
||||||
const currentObjectId = computed(() => {
|
const currentObjectId = computed(() => {
|
||||||
|
if (!isProductDomainRoute.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
return resolveObjectIdFromQuery(route.query[OBJECT_CONTEXT_QUERY_KEY], objectContextStore.objectId);
|
return resolveObjectIdFromQuery(route.query[OBJECT_CONTEXT_QUERY_KEY], objectContextStore.objectId);
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentProduct = computed(() =>
|
const currentProduct = computed(() => {
|
||||||
normalizeCurrentProductSummary(objectContextStore.objectSummary, objectContextStore.objectName)
|
if (!isProductDomainRoute.value) {
|
||||||
);
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeCurrentProductSummary(objectContextStore.objectSummary, objectContextStore.objectName);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentObjectId,
|
currentObjectId,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { computed, reactive, ref, watch } from 'vue';
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import { ElButton, ElTag, ElTooltip } from 'element-plus';
|
import { ElButton, ElTag, ElTooltip } from 'element-plus';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import {
|
import {
|
||||||
@@ -12,7 +13,7 @@ import {
|
|||||||
fetchChangeProjectRequirementStatus,
|
fetchChangeProjectRequirementStatus,
|
||||||
fetchDeleteProjectRequirement,
|
fetchDeleteProjectRequirement,
|
||||||
fetchGetProjectMembers,
|
fetchGetProjectMembers,
|
||||||
fetchGetProjectRequirementAllowedTransitions,
|
fetchGetProjectRequirementAllowedTransitionsBatch,
|
||||||
fetchGetProjectRequirementStatusDict,
|
fetchGetProjectRequirementStatusDict,
|
||||||
fetchGetProjectRequirementTerminalStatusDict,
|
fetchGetProjectRequirementTerminalStatusDict,
|
||||||
fetchGetProjectRequirementTree
|
fetchGetProjectRequirementTree
|
||||||
@@ -23,6 +24,7 @@ import DictTag from '@/components/custom/dict-tag.vue';
|
|||||||
import DictText from '@/components/custom/dict-text.vue';
|
import DictText from '@/components/custom/dict-text.vue';
|
||||||
import { useCurrentProject } from '../../shared/use-current-project';
|
import { useCurrentProject } from '../../shared/use-current-project';
|
||||||
import {
|
import {
|
||||||
|
ACTION_ICON_MAP,
|
||||||
DEFAULT_ACTION_ICON,
|
DEFAULT_ACTION_ICON,
|
||||||
getProjectRequirementActionButtonType,
|
getProjectRequirementActionButtonType,
|
||||||
getProjectRequirementActionDisplayName,
|
getProjectRequirementActionDisplayName,
|
||||||
@@ -53,6 +55,14 @@ function formatDateTime(value?: string | null) {
|
|||||||
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDate(value?: string | null) {
|
||||||
|
if (!value) {
|
||||||
|
return '--';
|
||||||
|
}
|
||||||
|
|
||||||
|
return dayjs(value).format('YYYY-MM-DD');
|
||||||
|
}
|
||||||
|
|
||||||
function createSearchParams(): Api.Project.ProjectRequirementSearchParams {
|
function createSearchParams(): Api.Project.ProjectRequirementSearchParams {
|
||||||
return {
|
return {
|
||||||
projectId: '',
|
projectId: '',
|
||||||
@@ -76,7 +86,8 @@ const priorityTagTypeMap: Record<number, UI.ThemeColor> = {
|
|||||||
3: 'danger'
|
3: 'danger'
|
||||||
};
|
};
|
||||||
|
|
||||||
const { currentObjectId } = useCurrentProject();
|
const router = useRouter();
|
||||||
|
const { currentObjectId, currentProject } = useCurrentProject();
|
||||||
const { hasObjectAuth } = useAuth();
|
const { hasObjectAuth } = useAuth();
|
||||||
const { hasValue: canDeleteStatusHasValue } = useDict(RDMS_REQ_CAN_DELETE_STATUS_DICT_CODE);
|
const { hasValue: canDeleteStatusHasValue } = useDict(RDMS_REQ_CAN_DELETE_STATUS_DICT_CODE);
|
||||||
|
|
||||||
@@ -168,20 +179,6 @@ function flattenTree(nodes: Api.Project.ProjectRequirement[]): Api.Project.Proje
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectAllRequirementIds(nodes: Api.Project.ProjectRequirement[]): string[] {
|
|
||||||
const ids: string[] = [];
|
|
||||||
|
|
||||||
for (const node of nodes) {
|
|
||||||
ids.push(node.id);
|
|
||||||
|
|
||||||
if (node.children?.length) {
|
|
||||||
ids.push(...collectAllRequirementIds(node.children));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectRequirementIdsForActions(nodes: Api.Project.ProjectRequirement[]): string[] {
|
function collectRequirementIdsForActions(nodes: Api.Project.ProjectRequirement[]): string[] {
|
||||||
const ids: string[] = [];
|
const ids: string[] = [];
|
||||||
|
|
||||||
@@ -208,6 +205,7 @@ function buildRequirementActions(row: Api.Project.ProjectRequirement) {
|
|||||||
label: string;
|
label: string;
|
||||||
icon: object;
|
icon: object;
|
||||||
type: 'primary' | 'success' | 'danger';
|
type: 'primary' | 'success' | 'danger';
|
||||||
|
disabled?: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}> = [];
|
}> = [];
|
||||||
const hasUpdateAuth = hasObjectAuth('project:project:update');
|
const hasUpdateAuth = hasObjectAuth('project:project:update');
|
||||||
@@ -215,26 +213,39 @@ function buildRequirementActions(row: Api.Project.ProjectRequirement) {
|
|||||||
const hasStatusAuth = hasObjectAuth('project:project:status');
|
const hasStatusAuth = hasObjectAuth('project:project:status');
|
||||||
const hasSplitAuth = hasObjectAuth('project:project:split');
|
const hasSplitAuth = hasObjectAuth('project:project:split');
|
||||||
|
|
||||||
if (canSplitRequirement(row) && hasSplitAuth) {
|
if (hasSplitAuth) {
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'split',
|
key: 'split',
|
||||||
label: '拆分',
|
label: '拆分',
|
||||||
icon: getProjectRequirementActionIcon('split'),
|
icon: getProjectRequirementActionIcon('split'),
|
||||||
type: getProjectRequirementActionButtonType('split'),
|
type: getProjectRequirementActionButtonType('split'),
|
||||||
|
disabled: !canSplitRequirement(row),
|
||||||
onClick: () => openSplit(row)
|
onClick: () => openSplit(row)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdateAuth && !isTerminalStatus(row.statusCode) && row.statusCode !== 'accepted') {
|
if (hasUpdateAuth) {
|
||||||
|
const canEdit = !isTerminalStatus(row.statusCode) && row.statusCode !== 'accepted';
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
label: '编辑',
|
label: '编辑',
|
||||||
icon: getProjectRequirementActionIcon('edit'),
|
icon: getProjectRequirementActionIcon('edit'),
|
||||||
type: getProjectRequirementActionButtonType('edit'),
|
type: getProjectRequirementActionButtonType('edit'),
|
||||||
|
disabled: !canEdit,
|
||||||
onClick: () => openEdit(row)
|
onClick: () => openEdit(row)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (row.sourceType === 'product_requirement' && row.parentId === '0' && currentProject.value?.productId) {
|
||||||
|
actions.push({
|
||||||
|
key: 'back',
|
||||||
|
label: '返回产品侧',
|
||||||
|
icon: ACTION_ICON_MAP.back,
|
||||||
|
type: 'primary',
|
||||||
|
onClick: () => handleBackToProductRequirement(row)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (hasStatusAuth) {
|
if (hasStatusAuth) {
|
||||||
const lifecycleActions = getRowActions(row);
|
const lifecycleActions = getRowActions(row);
|
||||||
const nonTerminalActions: Api.Project.ProjectRequirementLifecycleAction[] = [];
|
const nonTerminalActions: Api.Project.ProjectRequirementLifecycleAction[] = [];
|
||||||
@@ -295,9 +306,11 @@ const columns = computed(() => [
|
|||||||
label: '需求名称',
|
label: '需求名称',
|
||||||
minWidth: 220,
|
minWidth: 220,
|
||||||
formatter: (row: Api.Project.ProjectRequirement) => (
|
formatter: (row: Api.Project.ProjectRequirement) => (
|
||||||
<ElButton link type="primary" class="requirement-title" onClick={() => openView(row)}>
|
<ElTooltip content={row.title} placement="top" show-after={300}>
|
||||||
{row.title}
|
<ElButton link type="primary" class="requirement-title" onClick={() => openView(row)}>
|
||||||
</ElButton>
|
{row.title}
|
||||||
|
</ElButton>
|
||||||
|
</ElTooltip>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -318,13 +331,6 @@ const columns = computed(() => [
|
|||||||
<ElTag type={getProjectRequirementStatusTagType(row.statusCode)}>{getStatusLabel(row.statusCode)}</ElTag>
|
<ElTag type={getProjectRequirementStatusTagType(row.statusCode)}>{getStatusLabel(row.statusCode)}</ElTag>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
prop: 'workHours',
|
|
||||||
label: '工时',
|
|
||||||
width: 88,
|
|
||||||
align: 'center',
|
|
||||||
formatter: (row: Api.Project.ProjectRequirement) => (row.workHours !== null ? `${row.workHours}h` : '--')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
prop: 'category',
|
prop: 'category',
|
||||||
label: '需求类型',
|
label: '需求类型',
|
||||||
@@ -364,6 +370,13 @@ const columns = computed(() => [
|
|||||||
return row.sourceBizId;
|
return row.sourceBizId;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'expectedTime',
|
||||||
|
label: '预期完成时间',
|
||||||
|
minWidth: 120,
|
||||||
|
align: 'center',
|
||||||
|
formatter: (row: Api.Project.ProjectRequirement) => formatDate(row.expectedTime)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: 'createTime',
|
prop: 'createTime',
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
@@ -388,6 +401,7 @@ const columns = computed(() => [
|
|||||||
size="small"
|
size="small"
|
||||||
class="requirement-action-icon-btn"
|
class="requirement-action-icon-btn"
|
||||||
type={action.type}
|
type={action.type}
|
||||||
|
disabled={action.disabled}
|
||||||
onClick={() => action.onClick()}
|
onClick={() => action.onClick()}
|
||||||
>
|
>
|
||||||
<IconComponent class="text-18px" />
|
<IconComponent class="text-18px" />
|
||||||
@@ -520,15 +534,18 @@ async function loadAllowedTransitionsForAll() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.all(
|
const { error, data } = await fetchGetProjectRequirementAllowedTransitionsBatch({
|
||||||
idsToQuery.map(async id => {
|
projectId: currentObjectId.value,
|
||||||
const { error, data } = await fetchGetProjectRequirementAllowedTransitions(id, currentObjectId.value!);
|
requirementIds: idsToQuery
|
||||||
return { id, actions: error ? [] : data || [] };
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
results.forEach(({ id, actions }) => {
|
if (error || !data) {
|
||||||
nextMap.set(id, actions);
|
allowedTransitionsMap.value = nextMap;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.forEach(item => {
|
||||||
|
nextMap.set(item.requirementId, item.transitions || []);
|
||||||
});
|
});
|
||||||
|
|
||||||
allowedTransitionsMap.value = nextMap;
|
allowedTransitionsMap.value = nextMap;
|
||||||
@@ -598,6 +615,18 @@ function openSplit(row: Api.Project.ProjectRequirement) {
|
|||||||
splitVisible.value = true;
|
splitVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleBackToProductRequirement(row: Api.Project.ProjectRequirement) {
|
||||||
|
const productId = currentProject.value?.productId;
|
||||||
|
if (!productId || row.sourceType !== 'product_requirement') return;
|
||||||
|
|
||||||
|
await router.replace({
|
||||||
|
path: '/product/requirement',
|
||||||
|
query: {
|
||||||
|
objectId: productId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function handleActionClick(row: Api.Project.ProjectRequirement, action: Api.Project.ProjectRequirementLifecycleAction) {
|
function handleActionClick(row: Api.Project.ProjectRequirement, action: Api.Project.ProjectRequirementLifecycleAction) {
|
||||||
if (!action.needReason) {
|
if (!action.needReason) {
|
||||||
handleDirectAction(row, action);
|
handleDirectAction(row, action);
|
||||||
@@ -869,6 +898,10 @@ Promise.all([loadStatusOptions(), loadTerminalStatusOptions()]);
|
|||||||
:deep(.requirement-title) {
|
:deep(.requirement-title) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.requirement-table-card-body) {
|
:deep(.requirement-table-card-body) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type Ref, computed, inject, ref } from 'vue';
|
import { type Ref, computed, inject, ref } from 'vue';
|
||||||
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
|
|
||||||
defineOptions({ name: 'ProjectRequirementModuleTreeNode' });
|
defineOptions({ name: 'ProjectRequirementModuleTreeNode' });
|
||||||
|
|
||||||
@@ -32,10 +33,23 @@ const emit = defineEmits([
|
|||||||
'updateNewChildModuleName'
|
'updateNewChildModuleName'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const { hasObjectAuth } = useAuth();
|
||||||
|
const isRootModule = computed(() => props.module.id === props.rootModuleId);
|
||||||
|
|
||||||
|
const hasAnyActionPermission = computed(() => {
|
||||||
|
if (isRootModule.value) {
|
||||||
|
return hasObjectAuth('project:project:create');
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
hasObjectAuth('project:project:create') ||
|
||||||
|
hasObjectAuth('project:project:update') ||
|
||||||
|
hasObjectAuth('project:project:delete')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const collapsedModuleIds = inject<Ref<Set<string>>>('collapsedModuleIds', ref(new Set()));
|
const collapsedModuleIds = inject<Ref<Set<string>>>('collapsedModuleIds', ref(new Set()));
|
||||||
const toggleCollapse = inject<(id: string) => void>('toggleCollapse', () => {});
|
const toggleCollapse = inject<(id: string) => void>('toggleCollapse', () => {});
|
||||||
|
|
||||||
const isRootModule = computed(() => props.module.id === props.rootModuleId);
|
|
||||||
const isSelected = computed(() => props.selectedModuleId === props.module.id);
|
const isSelected = computed(() => props.selectedModuleId === props.module.id);
|
||||||
const isEditing = computed(() => props.editingNodeId === props.module.id);
|
const isEditing = computed(() => props.editingNodeId === props.module.id);
|
||||||
const isAddingChild = computed(() => props.addingChildParentId === props.module.id);
|
const isAddingChild = computed(() => props.addingChildParentId === props.module.id);
|
||||||
@@ -124,25 +138,21 @@ function handleToggle() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isEditing" class="module-tree-item__actions" @click.stop>
|
<div v-if="!isEditing && hasAnyActionPermission" class="module-tree-item__actions" @click.stop>
|
||||||
<ElDropdown trigger="click">
|
<ElDropdown trigger="click">
|
||||||
<ElButton text size="small" class="module-tree-item__more-btn">
|
<ElButton text size="small" class="module-tree-item__more-btn">
|
||||||
<icon-mdi-dots-horizontal class="text-14px" />
|
<icon-mdi-dots-horizontal class="text-14px" />
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<ElDropdownMenu>
|
<ElDropdownMenu>
|
||||||
<ElDropdownItem
|
<ElDropdownItem v-if="hasObjectAuth('project:project:create')" @click="emit('addChild', module)">
|
||||||
v-auth="{ code: 'project:project:create', source: 'object' }"
|
|
||||||
@click="emit('addChild', module)"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-6px">
|
<div class="flex items-center gap-6px">
|
||||||
<icon-ic-round-plus class="text-14px" />
|
<icon-ic-round-plus class="text-14px" />
|
||||||
<span>新增子模块</span>
|
<span>新增子模块</span>
|
||||||
</div>
|
</div>
|
||||||
</ElDropdownItem>
|
</ElDropdownItem>
|
||||||
<ElDropdownItem
|
<ElDropdownItem
|
||||||
v-if="!isRootModule"
|
v-if="!isRootModule && hasObjectAuth('project:project:update')"
|
||||||
v-auth="{ code: 'project:project:update', source: 'object' }"
|
|
||||||
@click="emit('edit', module)"
|
@click="emit('edit', module)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-6px">
|
<div class="flex items-center gap-6px">
|
||||||
@@ -151,8 +161,7 @@ function handleToggle() {
|
|||||||
</div>
|
</div>
|
||||||
</ElDropdownItem>
|
</ElDropdownItem>
|
||||||
<ElDropdownItem
|
<ElDropdownItem
|
||||||
v-if="!isRootModule && canDeleteModule"
|
v-if="!isRootModule && canDeleteModule && hasObjectAuth('project:project:delete')"
|
||||||
v-auth="{ code: 'project:project:delete', source: 'object' }"
|
|
||||||
divided
|
divided
|
||||||
@click="emit('delete', module)"
|
@click="emit('delete', module)"
|
||||||
>
|
>
|
||||||
@@ -224,73 +233,112 @@ function handleToggle() {
|
|||||||
.module-tree-node {
|
.module-tree-node {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item {
|
.module-tree-item {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
min-height: 42px;
|
min-height: 36px;
|
||||||
padding: 0 14px;
|
padding: 6px 12px;
|
||||||
border: 1px solid rgb(226 232 240 / 92%);
|
padding-left: 16px;
|
||||||
border-radius: 14px;
|
border-radius: 8px;
|
||||||
background-color: rgb(248 250 252 / 96%);
|
color: #475569;
|
||||||
color: rgb(71 85 105 / 94%);
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
border-color 0.2s ease,
|
background-color 0.15s ease,
|
||||||
background-color 0.2s ease,
|
color 0.15s ease;
|
||||||
color 0.2s ease,
|
}
|
||||||
transform 0.2s ease;
|
|
||||||
|
.module-tree-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 3px;
|
||||||
|
height: 0;
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
background-color: transparent;
|
||||||
|
transition:
|
||||||
|
height 0.15s ease,
|
||||||
|
background-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item:hover {
|
.module-tree-item:hover {
|
||||||
transform: translateY(-1px);
|
background-color: #f1f5f9;
|
||||||
border-color: rgb(148 163 184 / 56%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item.is-active {
|
.module-tree-item.is-active {
|
||||||
border-color: rgb(13 148 136 / 42%);
|
background-color: #f0fdfa;
|
||||||
background-color: rgb(240 253 250 / 98%);
|
color: #0d9488;
|
||||||
color: rgb(15 118 110 / 96%);
|
font-weight: 500;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item.is-root:not(.is-active) .module-tree-item__icon {
|
.module-tree-item.is-active::before {
|
||||||
color: rgb(13 148 136 / 80%);
|
height: 60%;
|
||||||
|
background-color: #14b8a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-tree-item.is-root {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-tree-item.is-root:hover {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-tree-item.is-root.is-active {
|
||||||
|
background-color: #f0fdfa;
|
||||||
|
color: #0d9488;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item--new {
|
.module-tree-item--new {
|
||||||
border-style: dashed;
|
border: 1px dashed #cbd5e1;
|
||||||
border-color: rgb(148 163 184 / 56%);
|
background-color: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item__icon {
|
.module-tree-item--new:hover {
|
||||||
display: flex;
|
background-color: #f1f5f9;
|
||||||
align-items: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: rgb(100 116 139 / 80%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item__toggle {
|
.module-tree-item__toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 20px;
|
width: 18px;
|
||||||
height: 20px;
|
height: 18px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
color: rgb(148 163 184);
|
color: #94a3b8;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-tree-item__toggle:hover {
|
||||||
|
background-color: #e2e8f0;
|
||||||
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item__toggle.is-expanded svg {
|
.module-tree-item__toggle.is-expanded svg {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-tree-item__icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-tree-item.is-active .module-tree-item__icon {
|
||||||
|
color: #14b8a6;
|
||||||
|
}
|
||||||
|
|
||||||
.module-tree-item__content {
|
.module-tree-item__content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -309,7 +357,7 @@ function handleToggle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item__input :deep(.el-input__inner) {
|
.module-tree-item__input :deep(.el-input__inner) {
|
||||||
height: 28px;
|
height: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item__actions {
|
.module-tree-item__actions {
|
||||||
@@ -317,7 +365,7 @@ function handleToggle() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-item:hover .module-tree-item__actions {
|
.module-tree-item:hover .module-tree-item__actions {
|
||||||
@@ -330,5 +378,10 @@ function handleToggle() {
|
|||||||
|
|
||||||
.module-tree-item__more-btn {
|
.module-tree-item__more-btn {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-tree-item__more-btn:hover {
|
||||||
|
background-color: #e2e8f0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, ref, watch } from 'vue';
|
import { computed, nextTick, ref, watch } from 'vue';
|
||||||
import { useResizeObserver } from '@vueuse/core';
|
import { useResizeObserver } from '@vueuse/core';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { fetchCreateProjectRequirement, fetchGetProjectRequirementModuleTree } from '@/service/api';
|
import { fetchCreateProjectRequirement, fetchGetProjectRequirementModuleTree } from '@/service/api';
|
||||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||||
import { useDict } from '@/hooks/business/dict';
|
import { useDict } from '@/hooks/business/dict';
|
||||||
@@ -47,6 +48,26 @@ const priorityOptions = computed(() => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function buildExpectedTimeShortcut(text: string, mutator: (date: Date) => void) {
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
value: () => {
|
||||||
|
const date = new Date();
|
||||||
|
mutator(date);
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedTimeShortcuts = [
|
||||||
|
buildExpectedTimeShortcut('一星期', date => date.setDate(date.getDate() + 7)),
|
||||||
|
buildExpectedTimeShortcut('两星期', date => date.setDate(date.getDate() + 14)),
|
||||||
|
buildExpectedTimeShortcut('一个月', date => date.setMonth(date.getMonth() + 1)),
|
||||||
|
buildExpectedTimeShortcut('三个月', date => date.setMonth(date.getMonth() + 3)),
|
||||||
|
buildExpectedTimeShortcut('半年', date => date.setMonth(date.getMonth() + 6)),
|
||||||
|
buildExpectedTimeShortcut('一年', date => date.setFullYear(date.getFullYear() + 1))
|
||||||
|
];
|
||||||
|
|
||||||
interface Model {
|
interface Model {
|
||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
@@ -55,9 +76,9 @@ interface Model {
|
|||||||
moduleId: string;
|
moduleId: string;
|
||||||
category: string;
|
category: string;
|
||||||
priority: number | null;
|
priority: number | null;
|
||||||
|
expectedTime: string | null;
|
||||||
proposerId: string;
|
proposerId: string;
|
||||||
currentHandlerUserId: string;
|
currentHandlerUserId: string;
|
||||||
workHours: number | null;
|
|
||||||
sort: number;
|
sort: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,8 +123,7 @@ const rules = {
|
|||||||
category: [createRequiredRule('请选择需求类型')],
|
category: [createRequiredRule('请选择需求类型')],
|
||||||
priority: [createRequiredRule('请选择优先级')],
|
priority: [createRequiredRule('请选择优先级')],
|
||||||
proposerId: [createRequiredRule('请选择提出人')],
|
proposerId: [createRequiredRule('请选择提出人')],
|
||||||
currentHandlerUserId: [createRequiredRule('请选择负责人')],
|
currentHandlerUserId: [createRequiredRule('请选择负责人')]
|
||||||
workHours: [createRequiredRule('请输入所需工时')]
|
|
||||||
} satisfies Record<string, App.Global.FormRule[]>;
|
} satisfies Record<string, App.Global.FormRule[]>;
|
||||||
|
|
||||||
const leftColRef = ref<HTMLElement>();
|
const leftColRef = ref<HTMLElement>();
|
||||||
@@ -145,9 +165,9 @@ function createDefaultModel(): Model {
|
|||||||
moduleId: props.defaultModuleId || '0',
|
moduleId: props.defaultModuleId || '0',
|
||||||
category: '功能需求',
|
category: '功能需求',
|
||||||
priority: 1,
|
priority: 1,
|
||||||
|
expectedTime: null,
|
||||||
proposerId: '',
|
proposerId: '',
|
||||||
currentHandlerUserId: '',
|
currentHandlerUserId: '',
|
||||||
workHours: null,
|
|
||||||
sort: 0
|
sort: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -196,11 +216,11 @@ async function handleSubmit() {
|
|||||||
attachments: [...model.value.attachments],
|
attachments: [...model.value.attachments],
|
||||||
category: model.value.category,
|
category: model.value.category,
|
||||||
priority: Number(model.value.priority) as Api.Project.ProjectRequirementPriority,
|
priority: Number(model.value.priority) as Api.Project.ProjectRequirementPriority,
|
||||||
|
expectedTime: model.value.expectedTime,
|
||||||
proposerId: model.value.proposerId,
|
proposerId: model.value.proposerId,
|
||||||
proposerNickname: proposer?.userNickname || '',
|
proposerNickname: proposer?.userNickname || '',
|
||||||
currentHandlerUserId: model.value.currentHandlerUserId,
|
currentHandlerUserId: model.value.currentHandlerUserId,
|
||||||
currentHandlerUserNickname: handler?.userNickname || '',
|
currentHandlerUserNickname: handler?.userNickname || '',
|
||||||
workHours: model.value.workHours || 0,
|
|
||||||
sort: model.value.sort
|
sort: model.value.sort
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -263,14 +283,17 @@ watch(
|
|||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="是否需要评审">
|
<ElFormItem label="是否需要评审">
|
||||||
<ElSelect v-model="model.reviewRequired" class="w-full" placeholder="请选择">
|
<ElRadioGroup v-model="model.reviewRequired">
|
||||||
<ElOption
|
<ElRadio
|
||||||
v-for="item in reviewRequiredOptions"
|
v-for="item in reviewRequiredOptions"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:label="item.label"
|
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
/>
|
border
|
||||||
</ElSelect>
|
style="width: 165px"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</ElRadio>
|
||||||
|
</ElRadioGroup>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="优先级" prop="priority">
|
<ElFormItem label="优先级" prop="priority">
|
||||||
@@ -279,17 +302,6 @@ watch(
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="所需工时" prop="workHours">
|
|
||||||
<ElInputNumber
|
|
||||||
v-model="model.workHours"
|
|
||||||
class="w-full"
|
|
||||||
:min="0"
|
|
||||||
:max="9999"
|
|
||||||
:precision="1"
|
|
||||||
placeholder="请输入所需工时"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem label="需求类型" prop="category">
|
<ElFormItem label="需求类型" prop="category">
|
||||||
<DictSelect
|
<DictSelect
|
||||||
v-model="model.category"
|
v-model="model.category"
|
||||||
@@ -325,9 +337,20 @@ watch(
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="排序值">
|
<ElFormItem label="预期完成时间">
|
||||||
<ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />
|
<ElDatePicker
|
||||||
|
v-model="model.expectedTime"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="请选择预期完成时间"
|
||||||
|
:shortcuts="expectedTimeShortcuts"
|
||||||
|
class="requirement-operate-dialog__date-picker"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
|
<!-- <ElFormItem label="排序值">-->
|
||||||
|
<!-- <ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />-->
|
||||||
|
<!-- </ElFormItem>-->
|
||||||
</BusinessFormSection>
|
</BusinessFormSection>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -388,4 +411,8 @@ watch(
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.requirement-operate-dialog__date-picker.el-date-editor.el-input) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, ref, watch } from 'vue';
|
import { computed, nextTick, ref, watch } from 'vue';
|
||||||
import { useResizeObserver } from '@vueuse/core';
|
import { useResizeObserver } from '@vueuse/core';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import {
|
import {
|
||||||
fetchGetProjectRequirement,
|
fetchGetProjectRequirement,
|
||||||
fetchGetProjectRequirementModuleTree,
|
fetchGetProjectRequirementModuleTree,
|
||||||
@@ -63,11 +64,11 @@ interface Model {
|
|||||||
moduleId: string;
|
moduleId: string;
|
||||||
category: string;
|
category: string;
|
||||||
priority: number | null;
|
priority: number | null;
|
||||||
|
expectedTime: string | null;
|
||||||
proposerId: string;
|
proposerId: string;
|
||||||
proposerNickname: string;
|
proposerNickname: string;
|
||||||
currentHandlerUserId: string;
|
currentHandlerUserId: string;
|
||||||
currentHandlerUserNickname: string;
|
currentHandlerUserNickname: string;
|
||||||
workHours: number | null;
|
|
||||||
sort: number;
|
sort: number;
|
||||||
lastStatusReason: string;
|
lastStatusReason: string;
|
||||||
}
|
}
|
||||||
@@ -132,6 +133,26 @@ const reviewRequiredOptions = [
|
|||||||
{ label: '需要', value: 1 }
|
{ label: '需要', value: 1 }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function buildExpectedTimeShortcut(text: string, mutator: (date: Date) => void) {
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
value: () => {
|
||||||
|
const date = new Date();
|
||||||
|
mutator(date);
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedTimeShortcuts = [
|
||||||
|
buildExpectedTimeShortcut('一星期', date => date.setDate(date.getDate() + 7)),
|
||||||
|
buildExpectedTimeShortcut('两星期', date => date.setDate(date.getDate() + 14)),
|
||||||
|
buildExpectedTimeShortcut('一个月', date => date.setMonth(date.getMonth() + 1)),
|
||||||
|
buildExpectedTimeShortcut('三个月', date => date.setMonth(date.getMonth() + 3)),
|
||||||
|
buildExpectedTimeShortcut('半年', date => date.setMonth(date.getMonth() + 6)),
|
||||||
|
buildExpectedTimeShortcut('一年', date => date.setFullYear(date.getFullYear() + 1))
|
||||||
|
];
|
||||||
|
|
||||||
const rules = computed(() => ({
|
const rules = computed(() => ({
|
||||||
title: isEditMode.value ? [createRequiredRule('请输入需求名称')] : [],
|
title: isEditMode.value ? [createRequiredRule('请输入需求名称')] : [],
|
||||||
category: isEditMode.value ? [createRequiredRule('请选择需求类型')] : [],
|
category: isEditMode.value ? [createRequiredRule('请选择需求类型')] : [],
|
||||||
@@ -179,11 +200,11 @@ function createDefaultModel(): Model {
|
|||||||
moduleId: '0',
|
moduleId: '0',
|
||||||
category: '',
|
category: '',
|
||||||
priority: 1,
|
priority: 1,
|
||||||
|
expectedTime: null,
|
||||||
proposerId: '',
|
proposerId: '',
|
||||||
proposerNickname: '',
|
proposerNickname: '',
|
||||||
currentHandlerUserId: '',
|
currentHandlerUserId: '',
|
||||||
currentHandlerUserNickname: '',
|
currentHandlerUserNickname: '',
|
||||||
workHours: null,
|
|
||||||
sort: 0,
|
sort: 0,
|
||||||
lastStatusReason: ''
|
lastStatusReason: ''
|
||||||
};
|
};
|
||||||
@@ -228,16 +249,29 @@ async function loadRequirementDetail() {
|
|||||||
moduleId: data.moduleId || '0',
|
moduleId: data.moduleId || '0',
|
||||||
category: data.category || '',
|
category: data.category || '',
|
||||||
priority: data.priority ?? null,
|
priority: data.priority ?? null,
|
||||||
|
expectedTime: formatExpectedTime(data.expectedTime),
|
||||||
proposerId: data.proposerId || '',
|
proposerId: data.proposerId || '',
|
||||||
proposerNickname: data.proposerNickname || '',
|
proposerNickname: data.proposerNickname || '',
|
||||||
currentHandlerUserId: data.currentHandlerUserId || '',
|
currentHandlerUserId: data.currentHandlerUserId || '',
|
||||||
currentHandlerUserNickname: data.currentHandlerUserNickname || '',
|
currentHandlerUserNickname: data.currentHandlerUserNickname || '',
|
||||||
workHours: data.workHours ?? null,
|
|
||||||
sort: data.sort ?? 0,
|
sort: data.sort ?? 0,
|
||||||
lastStatusReason: data.lastStatusReason || ''
|
lastStatusReason: data.lastStatusReason || ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatExpectedTime(value?: string | number[] | null): string | null {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const [year, month, day] = value;
|
||||||
|
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dayjs(value).format('YYYY-MM-DD');
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
await validate();
|
await validate();
|
||||||
|
|
||||||
@@ -250,6 +284,11 @@ async function handleSubmit() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (props.requirement.parentId === '0' && props.requirement.sourceType === 'product_requirement') {
|
||||||
|
window.$message?.warning('来自产品需求的数据不允许编辑');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handler = memberUserOptions.value.find(item => item.userId === model.value.currentHandlerUserId);
|
const handler = memberUserOptions.value.find(item => item.userId === model.value.currentHandlerUserId);
|
||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
@@ -264,11 +303,11 @@ async function handleSubmit() {
|
|||||||
attachments: [...model.value.attachments],
|
attachments: [...model.value.attachments],
|
||||||
category: model.value.category,
|
category: model.value.category,
|
||||||
priority: Number(model.value.priority) as Api.Project.ProjectRequirementPriority,
|
priority: Number(model.value.priority) as Api.Project.ProjectRequirementPriority,
|
||||||
|
expectedTime: model.value.expectedTime,
|
||||||
proposerId: model.value.proposerId,
|
proposerId: model.value.proposerId,
|
||||||
proposerNickname: model.value.proposerNickname,
|
proposerNickname: model.value.proposerNickname,
|
||||||
currentHandlerUserId: model.value.currentHandlerUserId,
|
currentHandlerUserId: model.value.currentHandlerUserId,
|
||||||
currentHandlerUserNickname: handler?.userNickname || model.value.currentHandlerUserNickname,
|
currentHandlerUserNickname: handler?.userNickname || model.value.currentHandlerUserNickname,
|
||||||
workHours: model.value.workHours || 0,
|
|
||||||
sort: model.value.sort
|
sort: model.value.sort
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -329,8 +368,7 @@ watch(
|
|||||||
<div ref="leftColRef" class="requirement-operate-dialog__col-left">
|
<div ref="leftColRef" class="requirement-operate-dialog__col-left">
|
||||||
<BusinessFormSection title="需求信息">
|
<BusinessFormSection title="需求信息">
|
||||||
<ElFormItem label="需求名称" prop="title">
|
<ElFormItem label="需求名称" prop="title">
|
||||||
<ReadonlyField v-if="isViewMode" :value="model.title" />
|
<ReadonlyField :value="model.title" />
|
||||||
<ElInput v-else v-model="model.title" clearable maxlength="256" placeholder="请输入需求名称" />
|
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="模块">
|
<ElFormItem label="模块">
|
||||||
@@ -341,9 +379,7 @@ watch(
|
|||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="是否需要评审">
|
<ElFormItem label="是否需要评审">
|
||||||
<ReadonlyField
|
<ReadonlyField :value="reviewRequiredOptions.find(item => item.value === model.reviewRequired)?.label" />
|
||||||
:value="reviewRequiredOptions.find(item => item.value === model.reviewRequired)?.label || '--'"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="优先级" prop="priority">
|
<ElFormItem label="优先级" prop="priority">
|
||||||
@@ -356,19 +392,6 @@ watch(
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="所需工时">
|
|
||||||
<ReadonlyField v-if="isViewMode" :value="model.workHours != null ? `${model.workHours}小时` : '--'" />
|
|
||||||
<ElInputNumber
|
|
||||||
v-else
|
|
||||||
v-model="model.workHours"
|
|
||||||
class="w-full"
|
|
||||||
:min="0"
|
|
||||||
:max="9999"
|
|
||||||
:precision="1"
|
|
||||||
placeholder="请输入所需工时"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem label="需求类型" prop="category">
|
<ElFormItem label="需求类型" prop="category">
|
||||||
<ReadonlyField :value="getCategoryLabel(model.category) || '--'" />
|
<ReadonlyField :value="getCategoryLabel(model.category) || '--'" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -397,18 +420,31 @@ watch(
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="排序值">
|
<ElFormItem label="预期完成时间">
|
||||||
<ReadonlyField v-if="isViewMode" :value="model.sort" />
|
<ReadonlyField v-if="isViewMode" :value="model.expectedTime || '--'" />
|
||||||
<ElInputNumber
|
<ElDatePicker
|
||||||
v-else
|
v-else
|
||||||
v-model="model.sort"
|
v-model="model.expectedTime"
|
||||||
class="w-full"
|
type="date"
|
||||||
:min="0"
|
value-format="YYYY-MM-DD"
|
||||||
:max="9999"
|
placeholder="请选择预期完成时间"
|
||||||
placeholder="请输入排序值"
|
:shortcuts="expectedTimeShortcuts"
|
||||||
|
class="requirement-operate-dialog__date-picker"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
|
<!-- <ElFormItem label="排序值">-->
|
||||||
|
<!-- <ReadonlyField v-if="isViewMode" :value="model.sort" />-->
|
||||||
|
<!-- <ElInputNumber-->
|
||||||
|
<!-- v-else-->
|
||||||
|
<!-- v-model="model.sort"-->
|
||||||
|
<!-- class="w-full"-->
|
||||||
|
<!-- :min="0"-->
|
||||||
|
<!-- :max="9999"-->
|
||||||
|
<!-- placeholder="请输入排序值"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </ElFormItem>-->
|
||||||
|
|
||||||
<ElFormItem v-if="isViewMode && model.lastStatusReason" label="状态变更原因">
|
<ElFormItem v-if="isViewMode && model.lastStatusReason" label="状态变更原因">
|
||||||
<div class="requirement-operate-dialog__readonly-textarea">{{ model.lastStatusReason }}</div>
|
<div class="requirement-operate-dialog__readonly-textarea">{{ model.lastStatusReason }}</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -472,7 +508,7 @@ watch(
|
|||||||
.requirement-operate-dialog__readonly-textarea {
|
.requirement-operate-dialog__readonly-textarea {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100px;
|
min-height: 65px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: linear-gradient(180deg, rgb(241 245 249 / 98%), rgb(226 232 240 / 94%)), rgb(241 245 249);
|
background: linear-gradient(180deg, rgb(241 245 249 / 98%), rgb(226 232 240 / 94%)), rgb(241 245 249);
|
||||||
@@ -489,4 +525,8 @@ watch(
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.requirement-operate-dialog__date-picker.el-date-editor.el-input) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ async function handleDeleteModule(module: Api.Project.ProjectRequirementModule)
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(`确定要删除模块“${module.moduleName}”吗?该模块下的所有需求将一并删除。`, '删除确认', {
|
await ElMessageBox.confirm(`确定要删除模块“${module.moduleName}”吗?`, '删除确认', {
|
||||||
confirmButtonText: '确认删除',
|
confirmButtonText: '确认删除',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
@@ -293,12 +293,12 @@ defineExpose({
|
|||||||
|
|
||||||
.requirement-module-tree-card :deep(.el-card__header) {
|
.requirement-module-tree-card :deep(.el-card__header) {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-bottom: none;
|
border-bottom: 1px solid #f1f5f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.requirement-module-tree-card :deep(.el-card__body) {
|
.requirement-module-tree-card :deep(.el-card__body) {
|
||||||
padding: 0 16px 16px;
|
padding: 12px 8px;
|
||||||
height: calc(100% - 48px);
|
height: calc(100% - 49px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,17 +309,34 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-header__title {
|
.module-tree-header__title {
|
||||||
color: rgb(15 23 42 / 94%);
|
color: #1e293b;
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-tree-list {
|
.module-tree-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 2px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-tree-list::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-tree-list::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-tree-list::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #e2e8f0;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-tree-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #cbd5e1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue';
|
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 { fetchGetProjectRequirementStatusDict } from '@/service/api';
|
||||||
import { useDict } from '@/hooks/business/dict';
|
import { useDict } from '@/hooks/business/dict';
|
||||||
import DictSelect from '@/components/custom/dict-select.vue';
|
import TableSearchFields from '@/components/custom/table-search-fields.vue';
|
||||||
import TableSearchPanel from '@/components/custom/table-search-panel.vue';
|
|
||||||
import MemberSelectOption from './member-select-option.vue';
|
import MemberSelectOption from './member-select-option.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'ProjectRequirementSearch' });
|
defineOptions({ name: 'ProjectRequirementSearch' });
|
||||||
@@ -21,7 +20,7 @@ interface Props {
|
|||||||
priorityDictCode: string;
|
priorityDictCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'reset'): void;
|
(e: 'reset'): void;
|
||||||
@@ -43,6 +42,21 @@ const sourceTypeOptions = computed(() => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const memberSelectOptions = computed(() => {
|
||||||
|
return props.memberOptions.map(item => ({
|
||||||
|
label: item.nickname,
|
||||||
|
value: item.id,
|
||||||
|
roleName: item.roleName
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderMemberOption(option: { label: string; value: string | number; roleName?: string }) {
|
||||||
|
return h(MemberSelectOption, {
|
||||||
|
nickname: option.label,
|
||||||
|
roleName: option.roleName || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function loadStatusOptions() {
|
async function loadStatusOptions() {
|
||||||
const { error, data } = await fetchGetProjectRequirementStatusDict();
|
const { error, data } = await fetchGetProjectRequirementStatusDict();
|
||||||
|
|
||||||
@@ -57,77 +71,58 @@ async function loadStatusOptions() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
|
||||||
emit('reset');
|
|
||||||
}
|
|
||||||
|
|
||||||
function search() {
|
|
||||||
emit('search');
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadStatusOptions();
|
await loadStatusOptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fields = computed(() => [
|
||||||
|
{
|
||||||
|
key: 'title',
|
||||||
|
label: '需求名称',
|
||||||
|
type: 'input' as const,
|
||||||
|
placeholder: '输入需求名称'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'priority',
|
||||||
|
label: '优先级',
|
||||||
|
type: 'dict' as const,
|
||||||
|
dictCode: props.priorityDictCode,
|
||||||
|
placeholder: '筛选优先级'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'statusCode',
|
||||||
|
label: '状态',
|
||||||
|
type: 'select' as const,
|
||||||
|
placeholder: '筛选状态',
|
||||||
|
options: requirementStatusOptions.value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'category',
|
||||||
|
label: '需求类型',
|
||||||
|
type: 'dict' as const,
|
||||||
|
dictCode: props.categoryDictCode,
|
||||||
|
placeholder: '筛选需求类型'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sourceType',
|
||||||
|
label: '需求来源',
|
||||||
|
type: 'select' as const,
|
||||||
|
placeholder: '筛选需求来源',
|
||||||
|
options: sourceTypeOptions.value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'currentHandlerUserId',
|
||||||
|
label: '负责人',
|
||||||
|
type: 'select' as const,
|
||||||
|
placeholder: '筛选负责人',
|
||||||
|
options: memberSelectOptions.value,
|
||||||
|
renderOption: renderMemberOption
|
||||||
|
}
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TableSearchPanel :model="model" :action-col-lg="6" @reset="reset" @search="search">
|
<TableSearchFields v-model="model" :fields="fields" :columns="3" @search="emit('search')" @reset="emit('reset')" />
|
||||||
<ElCol :lg="6" :md="12" :sm="12">
|
|
||||||
<ElFormItem label="需求名称">
|
|
||||||
<ElInput v-model="model.title" clearable placeholder="输入需求名称" />
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
<ElCol :lg="6" :md="12" :sm="12">
|
|
||||||
<ElFormItem label="需求类型">
|
|
||||||
<DictSelect
|
|
||||||
v-model="model.category"
|
|
||||||
:dict-code="categoryDictCode"
|
|
||||||
clearable
|
|
||||||
filterable
|
|
||||||
placeholder="筛选需求类型"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
<ElCol :lg="6" :md="12" :sm="12">
|
|
||||||
<ElFormItem label="优先级">
|
|
||||||
<DictSelect v-model="model.priority" :dict-code="priorityDictCode" clearable placeholder="筛选优先级" />
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
<ElCol :lg="6" :md="12" :sm="12">
|
|
||||||
<ElFormItem label="状态">
|
|
||||||
<ElSelect v-model="model.statusCode" clearable placeholder="筛选状态">
|
|
||||||
<ElOption
|
|
||||||
v-for="item in requirementStatusOptions"
|
|
||||||
:key="item.value"
|
|
||||||
:label="item.label"
|
|
||||||
:value="item.value"
|
|
||||||
/>
|
|
||||||
</ElSelect>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
<ElCol :lg="6" :md="12" :sm="12">
|
|
||||||
<ElFormItem label="负责人">
|
|
||||||
<ElSelect
|
|
||||||
v-model="model.currentHandlerUserId"
|
|
||||||
clearable
|
|
||||||
filterable
|
|
||||||
placeholder="筛选负责人"
|
|
||||||
:filter-method="(val: string) => val"
|
|
||||||
>
|
|
||||||
<ElOption v-for="item in memberOptions" :key="item.id" :label="item.nickname" :value="item.id">
|
|
||||||
<MemberSelectOption :nickname="item.nickname" :role-name="item.roleName || ''" />
|
|
||||||
</ElOption>
|
|
||||||
</ElSelect>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
<ElCol :lg="6" :md="12" :sm="12">
|
|
||||||
<ElFormItem label="需求来源">
|
|
||||||
<ElSelect v-model="model.sourceType" clearable placeholder="筛选需求来源">
|
|
||||||
<ElOption v-for="item in sourceTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
||||||
</ElSelect>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
</TableSearchPanel>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, ref, watch } from 'vue';
|
import { computed, nextTick, ref, watch } from 'vue';
|
||||||
import { useResizeObserver } from '@vueuse/core';
|
import { useResizeObserver } from '@vueuse/core';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { fetchSplitProjectRequirement } from '@/service/api';
|
import { fetchSplitProjectRequirement } from '@/service/api';
|
||||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||||
import { useDict } from '@/hooks/business/dict';
|
import { useDict } from '@/hooks/business/dict';
|
||||||
@@ -46,6 +47,26 @@ const priorityOptions = computed(() => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function buildExpectedTimeShortcut(text: string, mutator: (date: Date) => void) {
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
value: () => {
|
||||||
|
const date = new Date();
|
||||||
|
mutator(date);
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedTimeShortcuts = [
|
||||||
|
buildExpectedTimeShortcut('一星期', date => date.setDate(date.getDate() + 7)),
|
||||||
|
buildExpectedTimeShortcut('两星期', date => date.setDate(date.getDate() + 14)),
|
||||||
|
buildExpectedTimeShortcut('一个月', date => date.setMonth(date.getMonth() + 1)),
|
||||||
|
buildExpectedTimeShortcut('三个月', date => date.setMonth(date.getMonth() + 3)),
|
||||||
|
buildExpectedTimeShortcut('半年', date => date.setMonth(date.getMonth() + 6)),
|
||||||
|
buildExpectedTimeShortcut('一年', date => date.setFullYear(date.getFullYear() + 1))
|
||||||
|
];
|
||||||
|
|
||||||
interface Model {
|
interface Model {
|
||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
@@ -53,8 +74,8 @@ interface Model {
|
|||||||
reviewRequired: number;
|
reviewRequired: number;
|
||||||
category: string;
|
category: string;
|
||||||
priority: number | null;
|
priority: number | null;
|
||||||
|
expectedTime: string | null;
|
||||||
currentHandlerUserId: string;
|
currentHandlerUserId: string;
|
||||||
workHours: number | null;
|
|
||||||
sort: number;
|
sort: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,8 +93,7 @@ const reviewRequiredOptions = [
|
|||||||
const rules = {
|
const rules = {
|
||||||
title: [createRequiredRule('请输入子需求名称')],
|
title: [createRequiredRule('请输入子需求名称')],
|
||||||
priority: [createRequiredRule('请选择优先级')],
|
priority: [createRequiredRule('请选择优先级')],
|
||||||
currentHandlerUserId: [createRequiredRule('请选择负责人')],
|
currentHandlerUserId: [createRequiredRule('请选择负责人')]
|
||||||
workHours: [createRequiredRule('请输入所需工时')]
|
|
||||||
} satisfies Record<string, App.Global.FormRule[]>;
|
} satisfies Record<string, App.Global.FormRule[]>;
|
||||||
|
|
||||||
const leftColRef = ref<HTMLElement>();
|
const leftColRef = ref<HTMLElement>();
|
||||||
@@ -114,8 +134,8 @@ function createDefaultModel(): Model {
|
|||||||
reviewRequired: 0,
|
reviewRequired: 0,
|
||||||
category: '',
|
category: '',
|
||||||
priority: 1,
|
priority: 1,
|
||||||
|
expectedTime: null,
|
||||||
currentHandlerUserId: '',
|
currentHandlerUserId: '',
|
||||||
workHours: null,
|
|
||||||
sort: 0
|
sort: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -144,11 +164,11 @@ async function handleSubmit() {
|
|||||||
attachments: [...model.value.attachments],
|
attachments: [...model.value.attachments],
|
||||||
category: model.value.category,
|
category: model.value.category,
|
||||||
priority: Number(model.value.priority) as Api.Project.ProjectRequirementPriority,
|
priority: Number(model.value.priority) as Api.Project.ProjectRequirementPriority,
|
||||||
|
expectedTime: model.value.expectedTime,
|
||||||
proposerId: props.parentRequirement.proposerId,
|
proposerId: props.parentRequirement.proposerId,
|
||||||
proposerNickname: props.parentRequirement.proposerNickname || '',
|
proposerNickname: props.parentRequirement.proposerNickname || '',
|
||||||
currentHandlerUserId: model.value.currentHandlerUserId,
|
currentHandlerUserId: model.value.currentHandlerUserId,
|
||||||
currentHandlerUserNickname: handler?.userNickname || '',
|
currentHandlerUserNickname: handler?.userNickname || '',
|
||||||
workHours: model.value.workHours || 0,
|
|
||||||
sort: model.value.sort
|
sort: model.value.sort
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -186,6 +206,10 @@ watch(
|
|||||||
model.value.currentHandlerUserId = props.parentRequirement.currentHandlerUserId;
|
model.value.currentHandlerUserId = props.parentRequirement.currentHandlerUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (props.parentRequirement?.expectedTime) {
|
||||||
|
model.value.expectedTime = props.parentRequirement.expectedTime;
|
||||||
|
}
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
attachmentUploaderRef.value?.initSession();
|
attachmentUploaderRef.value?.initSession();
|
||||||
richTextEditorRef.value?.initSession();
|
richTextEditorRef.value?.initSession();
|
||||||
@@ -221,14 +245,17 @@ watch(
|
|||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="是否需要评审">
|
<ElFormItem label="是否需要评审">
|
||||||
<ElSelect v-model="model.reviewRequired" class="w-full" placeholder="请选择">
|
<ElRadioGroup v-model="model.reviewRequired">
|
||||||
<ElOption
|
<ElRadio
|
||||||
v-for="item in reviewRequiredOptions"
|
v-for="item in reviewRequiredOptions"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:label="item.label"
|
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
/>
|
border
|
||||||
</ElSelect>
|
style="width: 165px"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</ElRadio>
|
||||||
|
</ElRadioGroup>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="优先级" prop="priority">
|
<ElFormItem label="优先级" prop="priority">
|
||||||
@@ -237,17 +264,6 @@ watch(
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="所需工时" prop="workHours">
|
|
||||||
<ElInputNumber
|
|
||||||
v-model="model.workHours"
|
|
||||||
class="w-full"
|
|
||||||
:min="0"
|
|
||||||
:max="9999"
|
|
||||||
:precision="1"
|
|
||||||
placeholder="请输入所需工时"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem label="负责人" prop="currentHandlerUserId">
|
<ElFormItem label="负责人" prop="currentHandlerUserId">
|
||||||
<ElSelect v-model="model.currentHandlerUserId" class="w-full" filterable placeholder="请选择负责人">
|
<ElSelect v-model="model.currentHandlerUserId" class="w-full" filterable placeholder="请选择负责人">
|
||||||
<ElOption
|
<ElOption
|
||||||
@@ -261,9 +277,20 @@ watch(
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="排序值">
|
<ElFormItem label="预期完成时间">
|
||||||
<ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />
|
<ElDatePicker
|
||||||
|
v-model="model.expectedTime"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="请选择预期完成时间"
|
||||||
|
:shortcuts="expectedTimeShortcuts"
|
||||||
|
class="requirement-operate-dialog__date-picker"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
|
<!-- <ElFormItem label="排序值">-->
|
||||||
|
<!-- <ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />-->
|
||||||
|
<!-- </ElFormItem>-->
|
||||||
</BusinessFormSection>
|
</BusinessFormSection>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -324,4 +351,8 @@ watch(
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.requirement-operate-dialog__date-picker.el-date-editor.el-input) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import IconMdiGlasses from '~icons/mdi/glasses';
|
|||||||
import IconMdiClose from '~icons/mdi/close';
|
import IconMdiClose from '~icons/mdi/close';
|
||||||
import IconTablerSitemap from '~icons/tabler/sitemap';
|
import IconTablerSitemap from '~icons/tabler/sitemap';
|
||||||
import IconTablerCircleX from '~icons/tabler/circle-x';
|
import IconTablerCircleX from '~icons/tabler/circle-x';
|
||||||
|
import IconMingcuteBack2Line from '~icons/mingcute/back-2-line';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目需求状态记录
|
* 项目需求状态记录
|
||||||
@@ -40,6 +41,7 @@ const TERMINAL_STATUS_SET = new Set<Api.Project.ProjectRequirementStatusCode>(['
|
|||||||
export const ACTION_ICON_MAP: Record<string, object> = {
|
export const ACTION_ICON_MAP: Record<string, object> = {
|
||||||
split: markRaw(IconTablerSitemap),
|
split: markRaw(IconTablerSitemap),
|
||||||
edit: markRaw(IconMdiPencilOutline),
|
edit: markRaw(IconMdiPencilOutline),
|
||||||
|
back: markRaw(IconMingcuteBack2Line),
|
||||||
claim_to_review: markRaw(IconMdiCheckOutline),
|
claim_to_review: markRaw(IconMdiCheckOutline),
|
||||||
claim_to_implement: markRaw(IconMdiCheckCircleOutline),
|
claim_to_implement: markRaw(IconMdiCheckCircleOutline),
|
||||||
pass_review: markRaw(IconMdiGlasses),
|
pass_review: markRaw(IconMdiGlasses),
|
||||||
@@ -65,7 +67,7 @@ export function getProjectRequirementStatusTagType(status: Api.Project.ProjectRe
|
|||||||
pending_review: 'warning',
|
pending_review: 'warning',
|
||||||
implementing: 'primary',
|
implementing: 'primary',
|
||||||
accepted: 'success',
|
accepted: 'success',
|
||||||
closed: 'info',
|
closed: 'danger',
|
||||||
rejected: 'danger',
|
rejected: 'danger',
|
||||||
cancelled: 'danger'
|
cancelled: 'danger'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,30 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { OBJECT_CONTEXT_QUERY_KEY, getObjectContextDomainConfigByPath } from '@/constants/object-context';
|
||||||
import { useObjectContextStore } from '@/store/modules/object-context';
|
import { useObjectContextStore } from '@/store/modules/object-context';
|
||||||
|
import { resolveObjectIdFromQuery } from '@/views/product/shared/product-context-shared';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前项目上下文
|
* 获取当前项目上下文
|
||||||
*/
|
*/
|
||||||
export function useCurrentProject() {
|
export function useCurrentProject() {
|
||||||
|
const route = useRoute();
|
||||||
const objectContextStore = useObjectContextStore();
|
const objectContextStore = useObjectContextStore();
|
||||||
|
const isProjectDomainRoute = computed(() => getObjectContextDomainConfigByPath(route.path)?.domainKey === 'project');
|
||||||
|
|
||||||
const currentObjectId = computed(() => objectContextStore.objectId);
|
const currentObjectId = computed(() => {
|
||||||
|
if (!isProjectDomainRoute.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveObjectIdFromQuery(route.query[OBJECT_CONTEXT_QUERY_KEY], objectContextStore.objectId);
|
||||||
|
});
|
||||||
|
|
||||||
const currentProject = computed(() => {
|
const currentProject = computed(() => {
|
||||||
|
if (!isProjectDomainRoute.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const summary = objectContextStore.objectSummary;
|
const summary = objectContextStore.objectSummary;
|
||||||
|
|
||||||
if (!summary) {
|
if (!summary) {
|
||||||
@@ -22,6 +37,10 @@ export function useCurrentProject() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const currentRole = computed(() => {
|
const currentRole = computed(() => {
|
||||||
|
if (!isProjectDomainRoute.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const summary = objectContextStore.objectSummary;
|
const summary = objectContextStore.objectSummary;
|
||||||
|
|
||||||
if (!summary) {
|
if (!summary) {
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ const {
|
|||||||
{ prop: 'index', type: 'index', label: $t('common.index'), width: 64 },
|
{ prop: 'index', type: 'index', label: $t('common.index'), width: 64 },
|
||||||
{ prop: 'label', label: $t('page.system.dict.dictLabel'), minWidth: 160 },
|
{ prop: 'label', label: $t('page.system.dict.dictLabel'), minWidth: 160 },
|
||||||
{ prop: 'value', label: $t('page.system.dict.dictValue'), minWidth: 180 },
|
{ prop: 'value', label: $t('page.system.dict.dictValue'), minWidth: 180 },
|
||||||
|
{ prop: 'sign', label: '标志', minWidth: 140, showOverflowTooltip: true },
|
||||||
{ prop: 'sort', label: $t('page.system.dict.sort'), width: 90, align: 'center' },
|
{ prop: 'sort', label: $t('page.system.dict.sort'), width: 90, align: 'center' },
|
||||||
{
|
{
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const currentTypeCode = computed(() => props.currentType?.type ?? model.value.di
|
|||||||
function createDefaultModel(): Model {
|
function createDefaultModel(): Model {
|
||||||
return {
|
return {
|
||||||
label: '',
|
label: '',
|
||||||
|
sign: '',
|
||||||
value: '',
|
value: '',
|
||||||
dictType: '',
|
dictType: '',
|
||||||
sort: 0,
|
sort: 0,
|
||||||
@@ -79,6 +80,7 @@ function handleInitModel() {
|
|||||||
// 编辑时直接使用表格行数据回填,保持弹框打开速度。
|
// 编辑时直接使用表格行数据回填,保持弹框打开速度。
|
||||||
Object.assign(model.value, {
|
Object.assign(model.value, {
|
||||||
label: props.rowData.label,
|
label: props.rowData.label,
|
||||||
|
sign: props.rowData.sign ?? '',
|
||||||
value: props.rowData.value,
|
value: props.rowData.value,
|
||||||
dictType: props.rowData.dictType,
|
dictType: props.rowData.dictType,
|
||||||
sort: props.rowData.sort,
|
sort: props.rowData.sort,
|
||||||
@@ -168,6 +170,11 @@ watch(visible, value => {
|
|||||||
<ElInput v-model="model.value" :placeholder="$t('page.system.dict.form.dictValue')" />
|
<ElInput v-model="model.value" :placeholder="$t('page.system.dict.form.dictValue')" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="标志" prop="sign">
|
||||||
|
<ElInput v-model="model.sign" placeholder="请输入标志" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<ElFormItem :label="$t('page.system.dict.sort')" prop="sort">
|
<ElFormItem :label="$t('page.system.dict.sort')" prop="sort">
|
||||||
<ElInputNumber
|
<ElInputNumber
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ function createDefaultModel(): Model {
|
|||||||
mobile: '',
|
mobile: '',
|
||||||
sex: 1,
|
sex: 1,
|
||||||
avatar: '',
|
avatar: '',
|
||||||
|
sort: 0,
|
||||||
password: '',
|
password: '',
|
||||||
roleIds: []
|
roleIds: []
|
||||||
};
|
};
|
||||||
@@ -141,6 +142,7 @@ async function handleInitModel() {
|
|||||||
mobile: user.mobile ?? '',
|
mobile: user.mobile ?? '',
|
||||||
sex: user.sex ?? 0,
|
sex: user.sex ?? 0,
|
||||||
avatar: user.avatar ?? '',
|
avatar: user.avatar ?? '',
|
||||||
|
sort: user.sort ?? 0,
|
||||||
password: '',
|
password: '',
|
||||||
roleIds: roleResult.error ? [] : roleResult.data
|
roleIds: roleResult.error ? [] : roleResult.data
|
||||||
};
|
};
|
||||||
@@ -163,7 +165,8 @@ async function handleSubmit() {
|
|||||||
email: getNullableText(model.value.email),
|
email: getNullableText(model.value.email),
|
||||||
mobile: getNullableText(model.value.mobile),
|
mobile: getNullableText(model.value.mobile),
|
||||||
sex: model.value.sex,
|
sex: model.value.sex,
|
||||||
avatar: getNullableText(model.value.avatar)
|
avatar: getNullableText(model.value.avatar),
|
||||||
|
sort: model.value.sort
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isEdit.value) {
|
if (!isEdit.value) {
|
||||||
@@ -276,6 +279,11 @@ watch(visible, async value => {
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="排序" prop="sort">
|
||||||
|
<ElInputNumber v-model="model.sort" class="w-full" :min="0" placeholder="请输入排序" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<ElFormItem :label="$t('page.system.user.userPhone')" prop="mobile">
|
<ElFormItem :label="$t('page.system.user.userPhone')" prop="mobile">
|
||||||
<ElInput
|
<ElInput
|
||||||
|
|||||||
Reference in New Issue
Block a user