1073 lines
29 KiB
Vue
1073 lines
29 KiB
Vue
<script setup lang="tsx">
|
||
import { computed, markRaw, reactive, ref, watch } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
import { ElButton, ElTag, ElTooltip } from 'element-plus';
|
||
import dayjs from 'dayjs';
|
||
import {
|
||
RDMS_REQ_CAN_DELETE_STATUS_DICT_CODE,
|
||
RDMS_REQ_CATEGORY_DICT_CODE,
|
||
RDMS_REQ_PRIORITY_DICT_CODE,
|
||
RDMS_REQ_SOURCE_TYPE_DICT_CODE
|
||
} from '@/constants/dict';
|
||
import { getStatusTagType } from '@/constants/status-tag';
|
||
import {
|
||
fetchChangeProjectRequirementStatus,
|
||
fetchDeleteProjectRequirement,
|
||
fetchGetProjectMembers,
|
||
fetchGetProjectRequirementAllowedTransitionsBatch,
|
||
fetchGetProjectRequirementStatusDict,
|
||
fetchGetProjectRequirementTree
|
||
} from '@/service/api';
|
||
import { useAuth } from '@/hooks/business/auth';
|
||
import { useDict } from '@/hooks/business/dict';
|
||
import DictTag from '@/components/custom/dict-tag.vue';
|
||
import DictText from '@/components/custom/dict-text.vue';
|
||
import { useCurrentProject } from '../../shared/use-current-project';
|
||
import {
|
||
ACTION_ICON_MAP,
|
||
DEFAULT_ACTION_ICON,
|
||
getProjectRequirementActionButtonType,
|
||
getProjectRequirementActionDisplayName,
|
||
getProjectRequirementActionIcon,
|
||
isProjectRequirementActionTerminal
|
||
} from './shared/requirement-master-data';
|
||
import RequirementActionDialog from './modules/requirement-action-dialog.vue';
|
||
import RequirementCreateDialog from './modules/requirement-create-dialog.vue';
|
||
import RequirementDetailDialog from './modules/requirement-detail-dialog.vue';
|
||
import RequirementModuleTree from './modules/requirement-module-tree.vue';
|
||
import RequirementSearch from './modules/requirement-search.vue';
|
||
import RequirementSplitDialog from './modules/requirement-split-dialog.vue';
|
||
import RequirementReviewDialog from './modules/requirement-review-dialog.vue';
|
||
import RequirementReviewRecordDialog from './modules/requirement-review-record-dialog.vue';
|
||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||
|
||
defineOptions({ name: 'ProjectRequirement' });
|
||
|
||
interface MemberUserOption {
|
||
id: string;
|
||
nickname: string;
|
||
roleName: string;
|
||
}
|
||
|
||
function formatDateTime(value?: string | null) {
|
||
if (!value) {
|
||
return '--';
|
||
}
|
||
|
||
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 {
|
||
return {
|
||
projectId: '',
|
||
pageNo: 1,
|
||
pageSize: 10,
|
||
title: '',
|
||
moduleId: undefined,
|
||
parentId: undefined,
|
||
category: undefined,
|
||
priority: undefined,
|
||
statusCode: undefined,
|
||
currentHandlerUserId: undefined,
|
||
sourceType: undefined
|
||
};
|
||
}
|
||
|
||
const router = useRouter();
|
||
const { currentObjectId, currentProject } = useCurrentProject();
|
||
const { hasObjectAuth } = useAuth();
|
||
const { hasValue: canDeleteStatusHasValue } = useDict(RDMS_REQ_CAN_DELETE_STATUS_DICT_CODE);
|
||
|
||
const statusDict = ref<Api.Project.ProjectRequirementStatusDict[]>([]);
|
||
const memberOptions = ref<Api.Project.ProjectMember[]>([]);
|
||
const treeData = ref<Api.Project.ProjectRequirement[]>([]);
|
||
const requirementDisplayTotal = ref(0);
|
||
const loading = ref(false);
|
||
const selectedModuleId = ref<string | undefined>('');
|
||
const searchParams = reactive(createSearchParams());
|
||
const pagination = reactive({
|
||
pageNo: 1,
|
||
pageSize: 10,
|
||
total: 0
|
||
});
|
||
|
||
const createVisible = ref(false);
|
||
const detailVisible = ref(false);
|
||
const detailMode = ref<'view' | 'edit'>('view');
|
||
const selectedRequirement = ref<Api.Project.ProjectRequirement | null>(null);
|
||
const splitVisible = ref(false);
|
||
const splitParentRequirement = ref<Api.Project.ProjectRequirement | null>(null);
|
||
const actionVisible = ref(false);
|
||
const actionRequirement = ref<Api.Project.ProjectRequirement | null>(null);
|
||
const currentAction = ref<Api.Project.ProjectRequirementLifecycleAction | null>(null);
|
||
const allowedTransitionsMap = ref<Map<string, Api.Project.ProjectRequirementLifecycleAction[]>>(new Map());
|
||
const columnChecks = ref<UI.TableColumnCheck[]>([]);
|
||
const reviewVisible = ref(false);
|
||
const reviewRequirement = ref<Api.Project.ProjectRequirement | null>(null);
|
||
const reviewRecordVisible = ref(false);
|
||
const reviewRecordRequirement = ref<Api.Project.ProjectRequirement | null>(null);
|
||
|
||
const statusOptions = computed(() => {
|
||
return statusDict.value.map(item => ({
|
||
label: item.statusName,
|
||
value: item.statusCode
|
||
}));
|
||
});
|
||
|
||
const statusMetaMap = computed(() => {
|
||
return new Map(statusDict.value.map(item => [item.statusCode, item]));
|
||
});
|
||
|
||
const memberUserOptions = computed<MemberUserOption[]>(() => {
|
||
return memberOptions.value
|
||
.filter(item => item.status === 0)
|
||
.map(item => ({
|
||
id: item.userId,
|
||
nickname: item.userNickname,
|
||
roleName: item.roleName
|
||
}));
|
||
});
|
||
|
||
const memberLabelMap = computed(() => {
|
||
return new Map(memberUserOptions.value.map(item => [item.id, item.nickname]));
|
||
});
|
||
|
||
function getMemberLabel(userId?: string | null) {
|
||
if (!userId) {
|
||
return '--';
|
||
}
|
||
|
||
return memberLabelMap.value.get(userId) || userId;
|
||
}
|
||
|
||
function getStatusLabel(statusCode: string) {
|
||
const item = statusOptions.value.find(option => option.value === statusCode);
|
||
return item ? item.label : statusCode;
|
||
}
|
||
|
||
function isTerminalStatus(statusCode: string) {
|
||
return Boolean(statusMetaMap.value.get(statusCode)?.terminalFlag);
|
||
}
|
||
|
||
// 要么是后端数据库中项目需求的某状态的allowEdit字段是true,要么是需求来源是产品需求、且为父需求、状态是implementing
|
||
function canEditRequirement(row: Api.Project.ProjectRequirement) {
|
||
return (
|
||
Boolean(statusMetaMap.value.get(row.statusCode)?.allowEdit) ||
|
||
(row.sourceType === 'product_requirement' && row.parentId === '0' && row.statusCode === 'implementing')
|
||
);
|
||
}
|
||
|
||
function isReviewAction(row: Api.Project.ProjectRequirement, action: Api.Project.ProjectRequirementLifecycleAction) {
|
||
return row.statusCode === 'pending_review' && ['pass_review', 'reject_review'].includes(action.actionCode);
|
||
}
|
||
|
||
function isReviewTransitionAction(actionCode: string) {
|
||
return ['pass_review', 'reject_review'].includes(actionCode);
|
||
}
|
||
|
||
function canViewReviewRecord(row: Api.Project.ProjectRequirement) {
|
||
return row.reviewRequired === 1 && !['pending_claim', 'pending_review'].includes(row.statusCode);
|
||
}
|
||
|
||
function canSplitRequirement(row: Api.Project.ProjectRequirement) {
|
||
return ['reviewed', 'implementing'].includes(row.statusCode);
|
||
}
|
||
|
||
function canDeleteRequirement(row: Api.Project.ProjectRequirement) {
|
||
const isStatusAllowed = canDeleteStatusHasValue(row.statusCode);
|
||
const hasNoChildren = !row.children || row.children.length === 0;
|
||
return isStatusAllowed && hasNoChildren;
|
||
}
|
||
|
||
function flattenTree(nodes: Api.Project.ProjectRequirement[]): Api.Project.ProjectRequirement[] {
|
||
const result: Api.Project.ProjectRequirement[] = [];
|
||
|
||
for (const node of nodes) {
|
||
result.push(node);
|
||
|
||
if (node.children?.length) {
|
||
result.push(...flattenTree(node.children));
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
function countRequirementTreeNodes(nodes: Api.Project.ProjectRequirement[]) {
|
||
return flattenTree(nodes).length;
|
||
}
|
||
|
||
function collectRequirementIdsForActions(nodes: Api.Project.ProjectRequirement[]): string[] {
|
||
const ids: string[] = [];
|
||
|
||
for (const node of nodes) {
|
||
if (!isTerminalStatus(node.statusCode)) {
|
||
ids.push(node.id);
|
||
}
|
||
|
||
if (node.children?.length) {
|
||
ids.push(...collectRequirementIdsForActions(node.children));
|
||
}
|
||
}
|
||
|
||
return ids;
|
||
}
|
||
|
||
function getRowActions(row: Api.Project.ProjectRequirement) {
|
||
return allowedTransitionsMap.value.get(row.id) || [];
|
||
}
|
||
|
||
function buildRequirementActions(row: Api.Project.ProjectRequirement) {
|
||
const actions: Array<{
|
||
key: string;
|
||
label: string;
|
||
icon: object;
|
||
type: 'primary' | 'success' | 'danger';
|
||
disabled?: boolean;
|
||
onClick: () => void;
|
||
}> = [];
|
||
|
||
const hasUpdateAuth = hasObjectAuth('project:project:update');
|
||
const hasDeleteAuth = hasObjectAuth('project:project:delete');
|
||
const hasStatusAuth = hasObjectAuth('project:project:status');
|
||
const hasSplitAuth = hasObjectAuth('project:project:split');
|
||
const hasQueryAuth = hasObjectAuth('project:project:query');
|
||
const hasReviewAuth = hasObjectAuth('project:project:review');
|
||
const lifecycleActions = getRowActions(row);
|
||
|
||
if (hasQueryAuth && canViewReviewRecord(row)) {
|
||
actions.push({
|
||
key: 'reviewRecord',
|
||
label: '查看评审记录',
|
||
icon: markRaw(IconMdiEyeOutline),
|
||
type: 'primary',
|
||
onClick: () => handleViewReviewRecord(row)
|
||
});
|
||
}
|
||
|
||
if (hasSplitAuth && canSplitRequirement(row)) {
|
||
actions.push({
|
||
key: 'split',
|
||
label: '拆分',
|
||
icon: getProjectRequirementActionIcon('split'),
|
||
type: getProjectRequirementActionButtonType('split'),
|
||
onClick: () => openSplit(row)
|
||
});
|
||
}
|
||
|
||
if (hasUpdateAuth && canEditRequirement(row)) {
|
||
actions.push({
|
||
key: 'edit',
|
||
label: '编辑',
|
||
icon: getProjectRequirementActionIcon('edit'),
|
||
type: getProjectRequirementActionButtonType('edit'),
|
||
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 (hasReviewAuth && lifecycleActions.some(action => isReviewAction(row, action))) {
|
||
actions.push({
|
||
key: 'review',
|
||
label: '评审',
|
||
icon: ACTION_ICON_MAP.pass_review,
|
||
type: 'primary',
|
||
onClick: () => openReview(row)
|
||
});
|
||
}
|
||
|
||
if (hasStatusAuth) {
|
||
const nonTerminalActions: Api.Project.ProjectRequirementLifecycleAction[] = [];
|
||
const terminalActions: Api.Project.ProjectRequirementLifecycleAction[] = [];
|
||
|
||
lifecycleActions
|
||
.filter(action => !isReviewTransitionAction(action.actionCode))
|
||
.forEach(action => {
|
||
if (isProjectRequirementActionTerminal(action.actionCode)) {
|
||
terminalActions.push(action);
|
||
} else {
|
||
nonTerminalActions.push(action);
|
||
}
|
||
});
|
||
|
||
[...nonTerminalActions, ...terminalActions].forEach(action => {
|
||
actions.push({
|
||
key: `action-${action.actionCode}`,
|
||
label: getProjectRequirementActionDisplayName(action),
|
||
icon: getProjectRequirementActionIcon(action.actionCode) ?? DEFAULT_ACTION_ICON,
|
||
type: getProjectRequirementActionButtonType(action.actionCode),
|
||
onClick: () => handleActionClick(row, action)
|
||
});
|
||
});
|
||
}
|
||
|
||
if (hasDeleteAuth && canDeleteRequirement(row)) {
|
||
actions.push({
|
||
key: 'delete',
|
||
label: '删除',
|
||
icon: getProjectRequirementActionIcon('delete'),
|
||
type: getProjectRequirementActionButtonType('delete'),
|
||
onClick: () => handleDelete(row)
|
||
});
|
||
}
|
||
|
||
return actions;
|
||
}
|
||
|
||
const columns = computed(() => [
|
||
{
|
||
type: 'index',
|
||
label: '序号',
|
||
width: 64,
|
||
align: 'center',
|
||
index: (index: number): number => {
|
||
const flatList = flattenTree(treeData.value);
|
||
const row = flatList[index];
|
||
|
||
if (!row || row.parentId !== '0') {
|
||
return 0;
|
||
}
|
||
|
||
const parentIndex = treeData.value.findIndex(item => item.id === row.id);
|
||
return parentIndex >= 0 ? (pagination.pageNo - 1) * pagination.pageSize + parentIndex + 1 : 0;
|
||
}
|
||
},
|
||
{
|
||
prop: 'title',
|
||
label: '需求名称',
|
||
minWidth: 220,
|
||
formatter: (row: Api.Project.ProjectRequirement) => (
|
||
<ElTooltip content={row.title} placement="top" show-after={300}>
|
||
<ElButton link type="primary" class="requirement-title" onClick={() => openView(row)}>
|
||
{row.title}
|
||
</ElButton>
|
||
</ElTooltip>
|
||
)
|
||
},
|
||
{
|
||
prop: 'priority',
|
||
label: '优先级',
|
||
width: 88,
|
||
align: 'center',
|
||
formatter: (row: Api.Project.ProjectRequirement) => (
|
||
<DictTag dictCode={RDMS_REQ_PRIORITY_DICT_CODE} value={row.priority} />
|
||
)
|
||
},
|
||
{
|
||
prop: 'statusCode',
|
||
label: '状态',
|
||
width: 110,
|
||
align: 'center',
|
||
formatter: (row: Api.Project.ProjectRequirement) => (
|
||
<ElTag type={getStatusTagType('projectRequirement', row.statusCode)}>{getStatusLabel(row.statusCode)}</ElTag>
|
||
)
|
||
},
|
||
{
|
||
prop: 'category',
|
||
label: '需求类型',
|
||
minWidth: 120,
|
||
formatter: (row: Api.Project.ProjectRequirement) => row.categoryName || row.category || '--'
|
||
},
|
||
{
|
||
prop: 'sourceType',
|
||
label: '需求来源',
|
||
minWidth: 110,
|
||
align: 'center',
|
||
formatter: (row: Api.Project.ProjectRequirement) => (
|
||
<DictText dictCode={RDMS_REQ_SOURCE_TYPE_DICT_CODE} value={row.sourceType} />
|
||
)
|
||
},
|
||
{
|
||
prop: 'proposerNickname',
|
||
label: '提出人',
|
||
minWidth: 90,
|
||
formatter: (row: Api.Project.ProjectRequirement) => row.proposerNickname || '--'
|
||
},
|
||
{
|
||
prop: 'currentHandlerUserId',
|
||
label: '负责人',
|
||
minWidth: 90,
|
||
formatter: (row: Api.Project.ProjectRequirement) => getMemberLabel(row.currentHandlerUserId)
|
||
},
|
||
{
|
||
prop: 'sourceBizId',
|
||
label: '来源业务编号',
|
||
minWidth: 140,
|
||
formatter: (row: Api.Project.ProjectRequirement) => {
|
||
if (!row.sourceBizId || row.sourceType === 'manual') {
|
||
return '--';
|
||
}
|
||
|
||
return row.sourceBizId;
|
||
}
|
||
},
|
||
{
|
||
prop: 'expectedTime',
|
||
label: '预期完成时间',
|
||
minWidth: 120,
|
||
align: 'center',
|
||
formatter: (row: Api.Project.ProjectRequirement) => formatDate(row.expectedTime)
|
||
},
|
||
{
|
||
prop: 'createTime',
|
||
label: '创建时间',
|
||
minWidth: 180,
|
||
formatter: (row: Api.Project.ProjectRequirement) => formatDateTime(row.createTime)
|
||
},
|
||
{
|
||
prop: 'operate',
|
||
label: '操作',
|
||
width: 200,
|
||
align: 'center',
|
||
fixed: 'right',
|
||
formatter: (row: Api.Project.ProjectRequirement) => {
|
||
const actions = buildRequirementActions(row);
|
||
|
||
return (
|
||
<div class="requirement-action-cell" onClick={event => event.stopPropagation()}>
|
||
{actions.length === 0 ? (
|
||
<ElButton link size="small" class="requirement-action-icon-btn" type="primary" disabled>
|
||
<IconMdiPencilOutline class="text-15px" />
|
||
</ElButton>
|
||
) : (
|
||
actions.map(action => {
|
||
const IconComponent = action.icon as any;
|
||
|
||
return (
|
||
<ElTooltip key={action.key} content={action.label} placement="top">
|
||
<ElButton
|
||
link
|
||
size="small"
|
||
class="requirement-action-icon-btn"
|
||
type={action.type}
|
||
disabled={action.disabled}
|
||
onClick={() => action.onClick()}
|
||
>
|
||
<IconComponent class="text-15px" />
|
||
</ElButton>
|
||
</ElTooltip>
|
||
);
|
||
})
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
}
|
||
]);
|
||
|
||
watch(
|
||
() => columns.value,
|
||
value => {
|
||
const existingMap = new Map(columnChecks.value.map(item => [item.prop, item.checked]));
|
||
|
||
columnChecks.value = value
|
||
.filter(column => column.prop && column.prop !== 'operate')
|
||
.map(column => ({
|
||
prop: String(column.prop),
|
||
label: String(column.label || ''),
|
||
checked: existingMap.has(String(column.prop)) ? existingMap.get(String(column.prop))! : true,
|
||
visible: true
|
||
}));
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
const visibleColumns = computed(() => {
|
||
if (!columnChecks.value.length) {
|
||
return columns.value;
|
||
}
|
||
|
||
const visibleSet = new Set(columnChecks.value.filter(item => item.checked).map(item => item.prop));
|
||
|
||
return columns.value.filter(column => {
|
||
const prop = String(column.prop || '');
|
||
|
||
if (!prop || prop === 'operate') {
|
||
return true;
|
||
}
|
||
|
||
return visibleSet.has(prop);
|
||
});
|
||
});
|
||
|
||
async function loadStatusOptions() {
|
||
const { error, data } = await fetchGetProjectRequirementStatusDict();
|
||
|
||
if (error || !data) {
|
||
statusDict.value = [];
|
||
return;
|
||
}
|
||
|
||
statusDict.value = data;
|
||
}
|
||
|
||
async function loadMembers() {
|
||
if (!currentObjectId.value) {
|
||
memberOptions.value = [];
|
||
return;
|
||
}
|
||
|
||
const { error, data } = await fetchGetProjectMembers(currentObjectId.value);
|
||
|
||
if (error || !data) {
|
||
memberOptions.value = [];
|
||
return;
|
||
}
|
||
|
||
memberOptions.value = data;
|
||
}
|
||
|
||
async function loadTreeData() {
|
||
if (!currentObjectId.value) {
|
||
treeData.value = [];
|
||
pagination.total = 0;
|
||
return;
|
||
}
|
||
|
||
const { error, data } = await fetchGetProjectRequirementTree({
|
||
projectId: currentObjectId.value,
|
||
moduleId: selectedModuleId.value,
|
||
pageNo: pagination.pageNo,
|
||
pageSize: pagination.pageSize,
|
||
title: searchParams.title?.trim() || undefined,
|
||
category: searchParams.category,
|
||
priority: searchParams.priority,
|
||
statusCode: searchParams.statusCode,
|
||
currentHandlerUserId: searchParams.currentHandlerUserId,
|
||
sourceType: searchParams.sourceType
|
||
});
|
||
|
||
if (error || !data) {
|
||
treeData.value = [];
|
||
pagination.total = 0;
|
||
return;
|
||
}
|
||
|
||
treeData.value = data.list;
|
||
pagination.total = data.total;
|
||
}
|
||
|
||
async function loadRequirementDisplayTotal() {
|
||
if (!currentObjectId.value) {
|
||
requirementDisplayTotal.value = 0;
|
||
return;
|
||
}
|
||
|
||
const baseParams = {
|
||
projectId: currentObjectId.value,
|
||
pageNo: 1
|
||
};
|
||
const rootTotalResult = await fetchGetProjectRequirementTree({
|
||
...baseParams,
|
||
pageSize: 1
|
||
});
|
||
|
||
if (rootTotalResult.error || !rootTotalResult.data) {
|
||
requirementDisplayTotal.value = pagination.total;
|
||
return;
|
||
}
|
||
|
||
const rootTotal = rootTotalResult.data.total;
|
||
|
||
if (rootTotal <= 0) {
|
||
requirementDisplayTotal.value = 0;
|
||
return;
|
||
}
|
||
|
||
const allTreeResult = await fetchGetProjectRequirementTree({
|
||
...baseParams,
|
||
pageSize: rootTotal
|
||
});
|
||
|
||
if (allTreeResult.error || !allTreeResult.data) {
|
||
requirementDisplayTotal.value = rootTotal;
|
||
return;
|
||
}
|
||
|
||
requirementDisplayTotal.value = countRequirementTreeNodes(allTreeResult.data.list);
|
||
}
|
||
|
||
async function loadAllowedTransitionsForAll() {
|
||
if (!currentObjectId.value) {
|
||
allowedTransitionsMap.value = new Map();
|
||
return;
|
||
}
|
||
|
||
const idsToQuery = collectRequirementIdsForActions(treeData.value);
|
||
const nextMap = new Map<string, Api.Project.ProjectRequirementLifecycleAction[]>();
|
||
|
||
if (idsToQuery.length === 0) {
|
||
allowedTransitionsMap.value = nextMap;
|
||
return;
|
||
}
|
||
|
||
const { error, data } = await fetchGetProjectRequirementAllowedTransitionsBatch({
|
||
projectId: currentObjectId.value,
|
||
requirementIds: idsToQuery
|
||
});
|
||
|
||
if (error || !data) {
|
||
allowedTransitionsMap.value = nextMap;
|
||
return;
|
||
}
|
||
|
||
data.forEach(item => {
|
||
nextMap.set(item.requirementId, item.transitions || []);
|
||
});
|
||
|
||
allowedTransitionsMap.value = nextMap;
|
||
}
|
||
|
||
async function reloadTable() {
|
||
loading.value = true;
|
||
|
||
try {
|
||
await loadTreeData();
|
||
await Promise.all([loadAllowedTransitionsForAll(), loadRequirementDisplayTotal()]);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function handleModuleSelect(moduleId: string | undefined) {
|
||
selectedModuleId.value = moduleId;
|
||
pagination.pageNo = 1;
|
||
reloadTable();
|
||
}
|
||
|
||
function handleSearch() {
|
||
pagination.pageNo = 1;
|
||
reloadTable();
|
||
}
|
||
|
||
function handleResetSearch() {
|
||
Object.assign(searchParams, createSearchParams(), {
|
||
projectId: currentObjectId.value || '',
|
||
pageSize: pagination.pageSize
|
||
});
|
||
pagination.pageNo = 1;
|
||
reloadTable();
|
||
}
|
||
|
||
function handlePageChange(page: number) {
|
||
pagination.pageNo = page;
|
||
reloadTable();
|
||
}
|
||
|
||
function handleSizeChange(size: number) {
|
||
pagination.pageNo = 1;
|
||
pagination.pageSize = size;
|
||
reloadTable();
|
||
}
|
||
|
||
function openCreate() {
|
||
selectedRequirement.value = null;
|
||
createVisible.value = true;
|
||
}
|
||
|
||
function openView(row: Api.Project.ProjectRequirement) {
|
||
selectedRequirement.value = row;
|
||
detailMode.value = 'view';
|
||
detailVisible.value = true;
|
||
}
|
||
|
||
function openEdit(row: Api.Project.ProjectRequirement) {
|
||
selectedRequirement.value = row;
|
||
detailMode.value = 'edit';
|
||
detailVisible.value = true;
|
||
}
|
||
|
||
function openSplit(row: Api.Project.ProjectRequirement) {
|
||
splitParentRequirement.value = row;
|
||
splitVisible.value = true;
|
||
}
|
||
|
||
function openReview(row: Api.Project.ProjectRequirement) {
|
||
reviewRequirement.value = row;
|
||
reviewVisible.value = true;
|
||
}
|
||
|
||
function handleViewReviewRecord(row: Api.Project.ProjectRequirement) {
|
||
if (!canViewReviewRecord(row)) {
|
||
return;
|
||
}
|
||
|
||
reviewRecordRequirement.value = row;
|
||
reviewRecordVisible.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) {
|
||
if (!action.needReason) {
|
||
handleDirectAction(row, action);
|
||
return;
|
||
}
|
||
|
||
actionRequirement.value = row;
|
||
currentAction.value = action;
|
||
actionVisible.value = true;
|
||
}
|
||
|
||
async function handleDirectAction(
|
||
row: Api.Project.ProjectRequirement,
|
||
action: Api.Project.ProjectRequirementLifecycleAction
|
||
) {
|
||
if (!currentObjectId.value) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await window.$messageBox?.confirm(`确定要执行“${action.actionName}”操作吗?`, '确认操作', {
|
||
confirmButtonText: '确认',
|
||
cancelButtonText: '取消',
|
||
type: 'info'
|
||
});
|
||
} catch {
|
||
return;
|
||
}
|
||
|
||
const { error } = await fetchChangeProjectRequirementStatus({
|
||
id: row.id,
|
||
projectId: currentObjectId.value,
|
||
actionCode: action.actionCode
|
||
});
|
||
|
||
if (error) {
|
||
return;
|
||
}
|
||
|
||
window.$message?.success(`${action.actionName}成功`);
|
||
await reloadTable();
|
||
}
|
||
|
||
async function handleActionSubmitted(payload: { actionCode: string; reason?: string }) {
|
||
if (!currentObjectId.value || !actionRequirement.value) {
|
||
return;
|
||
}
|
||
|
||
const { error } = await fetchChangeProjectRequirementStatus({
|
||
id: actionRequirement.value.id,
|
||
projectId: currentObjectId.value,
|
||
actionCode: payload.actionCode,
|
||
reason: payload.reason
|
||
});
|
||
|
||
if (error) {
|
||
return;
|
||
}
|
||
|
||
window.$message?.success('操作成功');
|
||
actionVisible.value = false;
|
||
await reloadTable();
|
||
}
|
||
|
||
async function handleDelete(row: Api.Project.ProjectRequirement) {
|
||
if (!currentObjectId.value) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await window.$messageBox?.confirm('确定要删除该需求吗?删除后不可恢复。', '删除确认', {
|
||
confirmButtonText: '确认删除',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
});
|
||
} catch {
|
||
return;
|
||
}
|
||
|
||
const { error } = await fetchDeleteProjectRequirement({
|
||
id: row.id,
|
||
projectId: currentObjectId.value
|
||
});
|
||
|
||
if (error) {
|
||
return;
|
||
}
|
||
|
||
window.$message?.success('需求删除成功');
|
||
await reloadTable();
|
||
}
|
||
|
||
async function handleCreateSubmitted() {
|
||
createVisible.value = false;
|
||
await reloadTable();
|
||
}
|
||
|
||
async function handleDetailSubmitted() {
|
||
detailVisible.value = false;
|
||
await reloadTable();
|
||
}
|
||
|
||
async function handleSplitSubmitted() {
|
||
splitVisible.value = false;
|
||
await reloadTable();
|
||
}
|
||
|
||
async function handleReviewSubmitted() {
|
||
reviewVisible.value = false;
|
||
await reloadTable();
|
||
}
|
||
|
||
watch(
|
||
() => currentObjectId.value,
|
||
async id => {
|
||
Object.assign(searchParams, createSearchParams(), {
|
||
projectId: id || '',
|
||
pageSize: pagination.pageSize
|
||
});
|
||
selectedModuleId.value = '';
|
||
pagination.pageNo = 1;
|
||
|
||
if (!id) {
|
||
memberOptions.value = [];
|
||
treeData.value = [];
|
||
allowedTransitionsMap.value = new Map();
|
||
pagination.total = 0;
|
||
requirementDisplayTotal.value = 0;
|
||
return;
|
||
}
|
||
|
||
await Promise.all([loadMembers(), loadTreeData()]);
|
||
await Promise.all([loadAllowedTransitionsForAll(), loadRequirementDisplayTotal()]);
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
Promise.all([loadStatusOptions()]);
|
||
</script>
|
||
|
||
<template>
|
||
<div v-if="currentObjectId" class="project-requirement-page">
|
||
<div class="flex-col-stretch gap-16px xl:min-h-0">
|
||
<RequirementModuleTree :requirement-tree="treeData" @select="handleModuleSelect" @refresh="reloadTable" />
|
||
</div>
|
||
|
||
<div class="flex-col-stretch gap-16px xl:min-h-0">
|
||
<RequirementSearch
|
||
v-model:model="searchParams"
|
||
:member-options="memberUserOptions"
|
||
:category-dict-code="RDMS_REQ_CATEGORY_DICT_CODE"
|
||
:priority-dict-code="RDMS_REQ_PRIORITY_DICT_CODE"
|
||
@reset="handleResetSearch"
|
||
@search="handleSearch"
|
||
/>
|
||
|
||
<ElCard class="card-wrapper xl:flex-1-hidden" body-class="requirement-table-card-body">
|
||
<template #header>
|
||
<div class="project-requirement-card-header">
|
||
<div class="min-w-0 flex-1">
|
||
<div class="flex flex-wrap items-center gap-8px">
|
||
<p class="truncate text-16px font-600">项目需求列表</p>
|
||
<ElTag effect="plain">{{ requirementDisplayTotal }}</ElTag>
|
||
</div>
|
||
</div>
|
||
<TableHeaderOperation v-model:columns="columnChecks" :loading="loading" @refresh="reloadTable">
|
||
<template #default>
|
||
<ElButton
|
||
v-auth="{ code: 'project:project:create', source: 'object' }"
|
||
plain
|
||
type="primary"
|
||
@click="openCreate"
|
||
>
|
||
<template #icon>
|
||
<icon-ic-round-plus class="text-icon" />
|
||
</template>
|
||
新增
|
||
</ElButton>
|
||
</template>
|
||
</TableHeaderOperation>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="flex-1">
|
||
<ElTable
|
||
v-loading="loading"
|
||
border
|
||
lazy
|
||
row-key="id"
|
||
:indent="32"
|
||
height="100%"
|
||
:data="treeData"
|
||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||
>
|
||
<ElTableColumn v-for="column in visibleColumns" :key="String(column.prop || 'index')" v-bind="column" />
|
||
|
||
<template #empty>
|
||
<ElEmpty description="当前筛选条件下暂无项目需求" />
|
||
</template>
|
||
</ElTable>
|
||
</div>
|
||
|
||
<div class="mt-16px flex justify-end">
|
||
<ElPagination
|
||
v-model:current-page="pagination.pageNo"
|
||
v-model:page-size="pagination.pageSize"
|
||
:total="pagination.total"
|
||
:page-sizes="[10, 15, 20, 25, 30]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@current-change="handlePageChange"
|
||
@size-change="handleSizeChange"
|
||
/>
|
||
</div>
|
||
</ElCard>
|
||
</div>
|
||
|
||
<RequirementCreateDialog
|
||
v-model:visible="createVisible"
|
||
:project-id="currentObjectId || ''"
|
||
:default-module-id="selectedModuleId"
|
||
:member-options="memberOptions"
|
||
:category-dict-code="RDMS_REQ_CATEGORY_DICT_CODE"
|
||
:priority-dict-code="RDMS_REQ_PRIORITY_DICT_CODE"
|
||
@submitted="handleCreateSubmitted"
|
||
/>
|
||
|
||
<RequirementDetailDialog
|
||
v-model:visible="detailVisible"
|
||
:mode="detailMode"
|
||
:requirement="selectedRequirement"
|
||
:project-id="currentObjectId || ''"
|
||
:member-options="memberOptions"
|
||
:category-dict-code="RDMS_REQ_CATEGORY_DICT_CODE"
|
||
:priority-dict-code="RDMS_REQ_PRIORITY_DICT_CODE"
|
||
@submitted="handleDetailSubmitted"
|
||
/>
|
||
|
||
<RequirementSplitDialog
|
||
v-model:visible="splitVisible"
|
||
:parent-requirement="splitParentRequirement"
|
||
:project-id="currentObjectId || ''"
|
||
:member-options="memberOptions"
|
||
:category-dict-code="RDMS_REQ_CATEGORY_DICT_CODE"
|
||
:priority-dict-code="RDMS_REQ_PRIORITY_DICT_CODE"
|
||
@submitted="handleSplitSubmitted"
|
||
/>
|
||
|
||
<RequirementActionDialog
|
||
v-model:visible="actionVisible"
|
||
:action="currentAction"
|
||
:requirement-title="actionRequirement?.title || ''"
|
||
@submitted="handleActionSubmitted"
|
||
/>
|
||
|
||
<RequirementReviewDialog
|
||
v-model:visible="reviewVisible"
|
||
:project-id="currentObjectId || ''"
|
||
:requirement="reviewRequirement"
|
||
:member-options="memberOptions"
|
||
@submitted="handleReviewSubmitted"
|
||
/>
|
||
|
||
<RequirementReviewRecordDialog
|
||
v-model:visible="reviewRecordVisible"
|
||
:project-id="currentObjectId || ''"
|
||
:requirement="reviewRecordRequirement"
|
||
:member-options="memberOptions"
|
||
/>
|
||
</div>
|
||
|
||
<ElEmpty v-else description="未获取到当前项目上下文,请返回项目列表重新选择项目" />
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
.project-requirement-page {
|
||
display: grid;
|
||
min-height: 560px;
|
||
gap: 16px;
|
||
overflow: hidden;
|
||
grid-template-columns: 280px minmax(0, 1fr);
|
||
}
|
||
|
||
.project-requirement-card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
|
||
:deep(.requirement-title) {
|
||
padding: 0;
|
||
font-weight: 500;
|
||
max-width: 200px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
line-height: 1.5;
|
||
height: auto;
|
||
}
|
||
|
||
:deep(.requirement-table-card-body) {
|
||
height: calc(100% - 56px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
:deep(.requirement-action-cell) {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
:deep(.requirement-action-cell .el-button + .el-button) {
|
||
margin-left: 0;
|
||
}
|
||
|
||
:deep(.requirement-action-icon-btn) {
|
||
padding: 3px;
|
||
height: auto;
|
||
min-width: auto;
|
||
line-height: 1;
|
||
}
|
||
|
||
:deep(.requirement-action-icon-btn:hover) {
|
||
background-color: var(--el-fill-color-light);
|
||
}
|
||
|
||
:deep(.el-table__row[class*='el-table__row--level-']:not(.el-table__row--level-0) td:first-child .cell) {
|
||
color: transparent;
|
||
}
|
||
|
||
@media (width <= 1280px) {
|
||
.project-requirement-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: auto;
|
||
}
|
||
|
||
.project-requirement-card-header {
|
||
align-items: flex-start;
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
</style>
|