Files
cn-rdms-web/src/views/project/project/requirement/index.vue

1073 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>