feat(projects): 新增项目、执行、任务等功能

This commit is contained in:
2026-05-09 11:30:34 +08:00
parent f4f43814b3
commit 824392b564
106 changed files with 13060 additions and 1049 deletions

View File

@@ -0,0 +1,115 @@
<script setup lang="tsx">
import { computed } from 'vue';
import { getProjectStatusLabel, getProjectStatusTagType } from './project-master-data';
import type { CurrentProjectSummary } from './project-context-shared';
defineOptions({ name: 'ProjectContextBanner' });
interface Props {
project: CurrentProjectSummary | null;
caption?: string;
}
const props = withDefaults(defineProps<Props>(), {
caption: ''
});
const projectStatusCode = computed(() => props.project?.statusCode as Api.Project.ProjectStatusCode | undefined);
const summaryItems = computed(() => {
if (!props.project) {
return [];
}
return [
{ label: '项目 ID', value: props.project.id || '--' },
{ label: '项目编码', value: props.project.projectCode || '--' },
{ label: '项目类型', value: props.project.projectType || '--' },
{ label: '项目负责人', value: props.project.managerUserId || '--' }
];
});
</script>
<template>
<ElCard class="project-context-banner card-wrapper">
<template v-if="project">
<div class="flex flex-col gap-20px lg:flex-row lg:items-start lg:justify-between">
<div class="min-w-0 flex-1">
<div class="mb-12px flex flex-wrap items-center gap-10px">
<span class="project-context-banner__code">{{ project.projectCode }}</span>
<ElTag v-if="projectStatusCode" :type="getProjectStatusTagType(projectStatusCode)" effect="light" round>
{{ getProjectStatusLabel(projectStatusCode) }}
</ElTag>
</div>
<div class="mb-10px flex flex-wrap items-center gap-12px">
<h2 class="text-24px text-[#0f172a] font-700">{{ project.projectName }}</h2>
<span v-if="caption" class="text-14px text-[#64748b]">{{ caption }}</span>
</div>
<div class="flex flex-wrap gap-x-18px gap-y-8px text-13px text-[#64748b] leading-22px">
<span>对象 ID{{ project.id || '--' }}</span>
<span>类型{{ project.projectType || '--' }}</span>
<span>项目负责人{{ project.managerUserId || '--' }}</span>
</div>
</div>
<div class="project-context-banner__stats">
<div v-for="item in summaryItems" :key="item.label" class="project-context-banner__stat-card">
<span class="project-context-banner__stat-label">{{ item.label }}</span>
<strong class="project-context-banner__stat-value">{{ item.value }}</strong>
</div>
</div>
</div>
</template>
<ElEmpty v-else description="未获取到当前项目上下文" :image-size="84" />
</ElCard>
</template>
<style scoped>
.project-context-banner {
overflow: hidden;
border: 1px solid rgb(226 232 240 / 88%);
background:
radial-gradient(circle at top left, rgb(14 165 233 / 10%), transparent 32%),
linear-gradient(135deg, rgb(255 255 255 / 98%), rgb(248 250 252 / 96%));
}
.project-context-banner__code {
display: inline-flex;
align-items: center;
height: 28px;
padding: 0 10px;
border-radius: 999px;
background-color: rgb(15 23 42 / 88%);
color: #fff;
font-size: 12px;
letter-spacing: 0.08em;
}
.project-context-banner__stats {
display: grid;
flex-shrink: 0;
grid-template-columns: repeat(2, minmax(132px, 1fr));
gap: 12px;
width: min(100%, 320px);
}
.project-context-banner__stat-card {
display: flex;
flex-direction: column;
gap: 6px;
padding: 14px;
border: 1px solid rgb(148 163 184 / 18%);
border-radius: 16px;
background-color: rgb(255 255 255 / 72%);
}
.project-context-banner__stat-label {
color: rgb(100 116 139 / 90%);
font-size: 12px;
}
.project-context-banner__stat-value {
color: rgb(15 23 42 / 94%);
font-size: 20px;
line-height: 1.2;
}
</style>

View File

@@ -0,0 +1,9 @@
export interface CurrentProjectSummary {
id: string;
projectCode: string;
projectName: string;
projectType: string;
productId: string | null;
managerUserId: string;
statusCode: string;
}

View File

@@ -0,0 +1,87 @@
import { transformRecordToOption } from '@/utils/common';
/** 项目状态编码与中文标签映射 */
export const projectStatusRecord: Record<Api.Project.ProjectStatusCode, string> = {
pending: '待开始',
active: '进行中',
paused: '已暂停',
completed: '已完成',
cancelled: '已取消',
archived: '已归档'
};
export const projectStatusOptions = transformRecordToOption(projectStatusRecord);
/** 项目状态动作编码与中文标签映射 */
export const projectStatusActionRecord: Record<Api.Project.ProjectStatusActionCode, string> = {
auto_start: '自动开始',
pause: '暂停项目',
resume: '恢复项目',
complete: '完成项目',
cancel: '取消项目',
reopen: '重新开启',
archive: '归档项目'
};
export function getProjectStatusLabel(status: Api.Project.ProjectStatusCode) {
return projectStatusRecord[status];
}
/** 根据项目状态返回对应的 Tag 类型,用于 ElTag 组件的颜色映射 */
export function getProjectStatusTagType(status: Api.Project.ProjectStatusCode): UI.ThemeColor {
const statusTagTypeMap: Record<Api.Project.ProjectStatusCode, UI.ThemeColor> = {
pending: 'info',
active: 'success',
paused: 'warning',
completed: 'success',
cancelled: 'info',
archived: 'info'
};
return statusTagTypeMap[status];
}
/** 判断项目是否可编辑pending / active / paused 状态允许编辑 */
export function isProjectEditable(status: Api.Project.ProjectStatusCode) {
return status === 'active' || status === 'pending';
}
/** 判断项目编辑是否受限paused / completed 状态只能编辑部分字段 */
export function isProjectEditLimited(status: Api.Project.ProjectStatusCode) {
return status === 'paused' || status === 'completed';
}
/** 根据当前状态获取允许的状态动作列表 */
export function getAllowedProjectStatusActions(
status: Api.Project.ProjectStatusCode
): Array<Exclude<Api.Project.ProjectStatusActionCode, 'auto_start'>> {
const actionMap: Record<
Api.Project.ProjectStatusCode,
Array<Exclude<Api.Project.ProjectStatusActionCode, 'auto_start'>>
> = {
pending: ['cancel'],
active: ['pause', 'complete', 'cancel'],
paused: ['resume', 'cancel'],
completed: ['reopen', 'archive'],
cancelled: [],
archived: []
};
return actionMap[status];
}
export function getProjectStatusActionLabel(actionCode: Api.Project.ProjectStatusActionCode) {
return projectStatusActionRecord[actionCode];
}
export function getProjectStatusActionOptions(status: Api.Project.ProjectStatusCode) {
return getAllowedProjectStatusActions(status).map(actionCode => ({
value: actionCode,
label: getProjectStatusActionLabel(actionCode)
}));
}
/** 判断状态动作是否必须填写原因resume 和 auto_start 不需要原因 */
export function isProjectActionReasonRequired(actionCode: Api.Project.ProjectStatusActionCode) {
return actionCode !== 'resume' && actionCode !== 'auto_start';
}

View File

@@ -0,0 +1,42 @@
import { computed } from 'vue';
import { useObjectContextStore } from '@/store/modules/object-context';
/**
* 获取当前项目上下文
*/
export function useCurrentProject() {
const objectContextStore = useObjectContextStore();
const currentObjectId = computed(() => objectContextStore.objectId);
const currentProject = computed(() => {
const summary = objectContextStore.objectSummary;
if (!summary) {
return null;
}
return ((summary as unknown as Api.Project.ProjectContext).currentProject || null) as
| Api.Project.ProjectContext['currentProject']
| null;
});
const currentRole = computed(() => {
const summary = objectContextStore.objectSummary;
if (!summary) {
return null;
}
return (summary as unknown as Api.Project.ProjectContext).currentRole || null;
});
const isGuest = computed(() => currentRole.value?.guestFlag ?? true);
return {
currentObjectId,
currentProject,
currentRole,
isGuest
};
}