feat(projects): 新增项目、执行、任务等功能
This commit is contained in:
115
src/views/project/shared/project-context-banner.vue
Normal file
115
src/views/project/shared/project-context-banner.vue
Normal 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>
|
||||
9
src/views/project/shared/project-context-shared.ts
Normal file
9
src/views/project/shared/project-context-shared.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface CurrentProjectSummary {
|
||||
id: string;
|
||||
projectCode: string;
|
||||
projectName: string;
|
||||
projectType: string;
|
||||
productId: string | null;
|
||||
managerUserId: string;
|
||||
statusCode: string;
|
||||
}
|
||||
87
src/views/project/shared/project-master-data.ts
Normal file
87
src/views/project/shared/project-master-data.ts
Normal 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';
|
||||
}
|
||||
42
src/views/project/shared/use-current-project.ts
Normal file
42
src/views/project/shared/use-current-project.ts
Normal 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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user