OfficeSystem/components/customer-detail/ProjectsTab.vue
2025-11-07 16:51:39 +08:00

335 lines
7.6 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.

<template>
<view class="tab-content">
<view class="project-list">
<view
class="project-card"
v-for="(project, index) in projectList"
:key="project.id || index"
>
<view class="project-header">
<view class="project-status-tag" :class="getProjectStatusClass(project.status)">
<text>{{ getProjectStatusText(project.status) }}</text>
</view>
<text class="project-more" @click.stop="handleProjectMore(project)">⋯</text>
</view>
<text class="project-name">{{ project.name }}</text>
<view class="project-progress">
<view class="progress-bar">
<view
class="progress-fill"
:style="{ width: getProjectProgress(project) + '%' }"
></view>
</view>
<text class="progress-text">{{ getProjectProgress(project) }}%</text>
</view>
<view class="project-task-count">
<text class="task-count-text">{{ getTaskCountText(project) }}</text>
</view>
<view class="project-members">
<view class="member-avatars">
<image
v-for="(member, idx) in getDisplayMembers(project.memberList)"
:key="member.id || idx"
class="member-avatar"
:src="member.userAvatar || '/static/default-avatar.png'"
mode="aspectFill"
/>
</view>
<text class="members-text">{{ formatProjectMembers(project.memberList) }}</text>
</view>
<view class="project-deadline" :class="{ 'overdue': isProjectOverdue(project) }">
<text class="deadline-icon">🕐</text>
<text class="deadline-text">{{ formatProjectDeadline(project) }}</text>
</view>
</view>
<view class="empty-state" v-if="projectList.length === 0">
<text>暂无项目</text>
</view>
</view>
</view>
</template>
<script setup>
const props = defineProps({
projectList: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['project-more']);
// 获取项目状态样式类
const getProjectStatusClass = (status) => {
return {
'status-developing': status === 'developing' || status === '1' || status === 'IN_PROGRESS',
'status-expiring': status === 'expiring' || status === '2' || status === 'EXPIRING_SOON'
};
};
// 获取项目状态文本
const getProjectStatusText = (status) => {
const statusMap = {
'developing': '开发中',
'expiring': '即将到期',
'1': '开发中',
'2': '即将到期',
'IN_PROGRESS': '开发中',
'EXPIRING_SOON': '即将到期'
};
return statusMap[status] || '未知';
};
// 计算项目进度
const getProjectProgress = (project) => {
if (!project) return 0;
// 如果有任务数据,根据任务完成情况计算
if (project.taskCount && project.taskCount > 0) {
const completed = project.taskPassCount || 0;
const progress = Math.round((completed / project.taskCount) * 100);
return Math.min(100, Math.max(0, progress)); // 确保在0-100之间
}
// 如果有进度字段,使用进度字段
if (project.progress !== undefined && project.progress !== null) {
return Math.min(100, Math.max(0, project.progress));
}
// 默认返回0
return 0;
};
// 获取任务计数文本
const getTaskCountText = (project) => {
if (!project) return '0 / 0';
const completed = project.taskPassCount || 0;
const total = project.taskCount || 0;
return `${completed} / ${total}`;
};
// 获取要显示的成员头像最多4个
const getDisplayMembers = (memberList) => {
if (!memberList || !Array.isArray(memberList)) return [];
return memberList.slice(0, 4);
};
// 格式化项目成员列表
const formatProjectMembers = (memberList) => {
if (!memberList || !Array.isArray(memberList) || memberList.length === 0) {
return '暂无成员';
}
const names = memberList.map(member => member.userName || member.name).filter(Boolean);
if (names.length <= 4) {
return names.join('、');
}
const firstFour = names.slice(0, 4).join('、');
return `${firstFour}${names.length}`;
};
// 判断项目是否逾期
const isProjectOverdue = (project) => {
if (!project) return false;
// 优先使用 devOverdue 字段
if (project.devOverdue !== undefined && project.devOverdue !== null) {
return project.devOverdue;
}
// 否则根据日期判断
const deadline = project.expireTime || project.expectedCompleteDate;
if (!deadline) return false;
const deadlineDate = new Date(deadline);
const now = new Date();
return deadlineDate < now;
};
// 格式化项目截止日期
const formatProjectDeadline = (project) => {
if (!project) return '--';
// 优先使用 expireTime其次使用 expectedCompleteDate
const deadline = project.expireTime || project.expectedCompleteDate;
if (!deadline) return '--';
const deadlineDate = new Date(deadline);
const now = new Date();
const diffTime = deadlineDate - now;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
// 格式化日期显示
const year = deadlineDate.getFullYear();
const month = String(deadlineDate.getMonth() + 1).padStart(2, '0');
const day = String(deadlineDate.getDate()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}`;
if (diffDays < 0) {
return `逾期${Math.abs(diffDays)}${dateStr}`;
} else {
return `剩余${diffDays}${dateStr}`;
}
};
// 项目更多操作
const handleProjectMore = (project) => {
emit('project-more', project);
};
</script>
<style lang="scss" scoped>
.tab-content {
padding: 16px;
}
// 项目列表样式
.project-list {
padding-bottom: 16px;
}
.project-card {
background-color: #fff;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
}
.project-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.project-status-tag {
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
&.status-developing {
background-color: #1976d2;
color: #fff;
}
&.status-expiring {
background-color: #ff9800;
color: #fff;
}
}
.project-more {
font-size: 20px;
color: #999;
line-height: 1;
padding: 4px;
}
.project-name {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
display: block;
line-height: 1.4;
}
.project-progress {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.progress-bar {
flex: 1;
height: 6px;
background-color: #f0f0f0;
border-radius: 3px;
overflow: hidden;
margin-right: 8px;
}
.progress-fill {
height: 100%;
background-color: #1976d2;
border-radius: 3px;
transition: width 0.3s;
}
.progress-text {
font-size: 12px;
color: #1976d2;
font-weight: 600;
min-width: 40px;
text-align: right;
}
.project-task-count {
margin-bottom: 10px;
}
.task-count-text {
font-size: 13px;
color: #666;
}
.project-members {
display: flex;
align-items: center;
margin-bottom: 10px;
gap: 8px;
}
.member-avatars {
display: flex;
}
.member-avatar {
width: 24px;
height: 24px;
border-radius: 12px;
border: 2px solid #fff;
margin-left: -8px;
&:first-child {
margin-left: 0;
}
}
.members-text {
font-size: 13px;
color: #666;
flex: 1;
}
.project-deadline {
display: flex;
align-items: center;
gap: 4px;
}
.deadline-icon {
font-size: 12px;
color: #999;
}
.deadline-text {
font-size: 12px;
color: #1976d2;
.overdue & {
color: #f44336;
}
}
.project-deadline.overdue .deadline-text {
color: #f44336;
}
.empty-state {
text-align: center;
padding: 40px 0;
color: #999;
font-size: 14px;
}
</style>