1372 lines
36 KiB
Vue
1372 lines
36 KiB
Vue
|
<!--
|
|||
|
人员调度管理页面 - 项目状态看板
|
|||
|
功能特性:
|
|||
|
1. 以项目状态分类的图形化看板
|
|||
|
2. 项目卡片展示基本信息
|
|||
|
3. 项目明细弹窗
|
|||
|
4. 人员需求发布
|
|||
|
5. 团队成员岗位编辑
|
|||
|
-->
|
|||
|
<template>
|
|||
|
<GiPageLayout>
|
|||
|
<!-- 页面头部 -->
|
|||
|
<div class="personnel-dispatch-header">
|
|||
|
<div class="header-content">
|
|||
|
<div class="header-title">
|
|||
|
<icon-dashboard class="title-icon" />
|
|||
|
<h1>项目调度看板</h1>
|
|||
|
<a-button
|
|||
|
size="small"
|
|||
|
type="text"
|
|||
|
@click="refreshData"
|
|||
|
:loading="loading"
|
|||
|
class="refresh-btn"
|
|||
|
>
|
|||
|
<template #icon><icon-refresh /></template>
|
|||
|
刷新
|
|||
|
</a-button>
|
|||
|
</div>
|
|||
|
<div class="header-stats">
|
|||
|
<div class="stat-card" :class="{ 'loading': loading }">
|
|||
|
<div class="stat-number">
|
|||
|
<a-spin v-if="loading" size="small" />
|
|||
|
<span v-else>{{ stats.totalProjectsCount }}</span>
|
|||
|
</div>
|
|||
|
<div class="stat-label">总项目数</div>
|
|||
|
</div>
|
|||
|
<div class="stat-card" :class="{ 'loading': loading }">
|
|||
|
<div class="stat-number">
|
|||
|
<a-spin v-if="loading" size="small" />
|
|||
|
<span v-else>{{ stats.inProgressProjectCount }}</span>
|
|||
|
</div>
|
|||
|
<div class="stat-label">进行中</div>
|
|||
|
</div>
|
|||
|
<div class="stat-card" :class="{ 'loading': loading }">
|
|||
|
<div class="stat-number">
|
|||
|
<a-spin v-if="loading" size="small" />
|
|||
|
<span v-else>{{ stats.pendingProjectCount }}</span>
|
|||
|
</div>
|
|||
|
<div class="stat-label">准备中</div>
|
|||
|
</div>
|
|||
|
<div class="stat-card" :class="{ 'loading': loading }">
|
|||
|
<div class="stat-number">
|
|||
|
<a-spin v-if="loading" size="small" />
|
|||
|
<span v-else>{{ stats.completedProjectCount }}</span>
|
|||
|
</div>
|
|||
|
<div class="stat-label">已完成</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
|
|||
|
|
|||
|
<!-- 项目状态看板 -->
|
|||
|
<div class="project-kanban">
|
|||
|
<!-- 准备中项目 -->
|
|||
|
<div class="kanban-column">
|
|||
|
<div class="column-header preparing">
|
|||
|
<div class="column-title">
|
|||
|
<icon-clock-circle class="status-icon" />
|
|||
|
<h2>准备中</h2>
|
|||
|
<span class="project-count">{{ preparingProjects.length }}</span>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<div class="column-content">
|
|||
|
<div
|
|||
|
v-for="project in preparingProjects"
|
|||
|
:key="project.id"
|
|||
|
class="project-card preparing"
|
|||
|
@click="openProjectDetail(project)"
|
|||
|
>
|
|||
|
<div class="project-header">
|
|||
|
<h3 class="project-name">{{ project.name }}</h3>
|
|||
|
<div class="project-status preparing">准备中</div>
|
|||
|
</div>
|
|||
|
<div class="project-info">
|
|||
|
<div class="info-item">
|
|||
|
<icon-money class="info-icon" />
|
|||
|
<span>预算: {{ formatBudget(project.budget) }}</span>
|
|||
|
</div>
|
|||
|
<div class="info-item">
|
|||
|
<icon-user class="info-icon" />
|
|||
|
<span>负责人: {{ project.manager }}</span>
|
|||
|
</div>
|
|||
|
<div class="info-item">
|
|||
|
<icon-team class="info-icon" />
|
|||
|
<span>团队: {{ project.teamSize }}人</span>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<div class="project-footer">
|
|||
|
<div class="progress-info">
|
|||
|
<span>准备进度</span>
|
|||
|
<span class="progress-text">{{ project.preparationProgress }}%</span>
|
|||
|
</div>
|
|||
|
<div class="progress-bar">
|
|||
|
<div class="progress-fill" :style="{ width: `${project.preparationProgress}%` }"></div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<!-- 已开工项目 -->
|
|||
|
<div class="kanban-column">
|
|||
|
<div class="column-header ongoing">
|
|||
|
<div class="column-title">
|
|||
|
<icon-play-circle class="status-icon" />
|
|||
|
<h2>已开工</h2>
|
|||
|
<span class="project-count">{{ ongoingProjects.length }}</span>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<div class="column-content">
|
|||
|
<div
|
|||
|
v-for="project in ongoingProjects"
|
|||
|
:key="project.id"
|
|||
|
class="project-card ongoing"
|
|||
|
@click="openProjectDetail(project)"
|
|||
|
>
|
|||
|
<div class="project-header">
|
|||
|
<h3 class="project-name">{{ project.name }}</h3>
|
|||
|
<div class="project-status ongoing">进行中</div>
|
|||
|
</div>
|
|||
|
<div class="project-info">
|
|||
|
<div class="info-item">
|
|||
|
<icon-money class="info-icon" />
|
|||
|
<span>预算: {{ formatBudget(project.budget) }}</span>
|
|||
|
</div>
|
|||
|
<div class="info-item">
|
|||
|
<icon-user class="info-icon" />
|
|||
|
<span>负责人: {{ project.manager }}</span>
|
|||
|
</div>
|
|||
|
<div class="info-item">
|
|||
|
<icon-team class="info-icon" />
|
|||
|
<span>团队: {{ project.teamSize }}人</span>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<div class="project-footer">
|
|||
|
<div class="progress-info">
|
|||
|
<span>项目进度</span>
|
|||
|
<span class="progress-text">{{ project.progress }}%</span>
|
|||
|
</div>
|
|||
|
<div class="progress-bar">
|
|||
|
<div class="progress-fill" :style="{ width: `${project.progress}%` }"></div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<div class="project-alerts" v-if="project.alerts.length > 0">
|
|||
|
<div
|
|||
|
v-for="alert in project.alerts"
|
|||
|
:key="alert.type"
|
|||
|
class="alert-item"
|
|||
|
:class="alert.type"
|
|||
|
>
|
|||
|
<icon-exclamation-circle />
|
|||
|
{{ alert.message }}
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
|
|||
|
</div>
|
|||
|
|
|||
|
<!-- 项目明细弹窗 -->
|
|||
|
<a-modal
|
|||
|
v-model:visible="projectDetailVisible"
|
|||
|
:title="currentProject?.name"
|
|||
|
width="800px"
|
|||
|
:footer="false"
|
|||
|
@cancel="closeProjectDetail"
|
|||
|
>
|
|||
|
<div v-if="currentProject" class="project-detail">
|
|||
|
<!-- 项目基本信息 -->
|
|||
|
<div class="detail-section">
|
|||
|
<h3>项目基本信息</h3>
|
|||
|
<div class="info-grid">
|
|||
|
<div class="info-item">
|
|||
|
<label>项目名称:</label>
|
|||
|
<span>{{ currentProject.name }}</span>
|
|||
|
</div>
|
|||
|
<div class="info-item">
|
|||
|
<label>项目状态:</label>
|
|||
|
<span class="status-badge" :class="currentProject.status">{{ getStatusText(currentProject.status) }}</span>
|
|||
|
</div>
|
|||
|
<div class="info-item">
|
|||
|
<label>项目预算:</label>
|
|||
|
<span>{{ formatBudget(currentProject.budget) }}</span>
|
|||
|
</div>
|
|||
|
<div class="info-item">
|
|||
|
<label>项目负责人:</label>
|
|||
|
<span>{{ currentProject.manager }}</span>
|
|||
|
</div>
|
|||
|
<div class="info-item">
|
|||
|
<label>团队人数:</label>
|
|||
|
<span>{{ currentProject.teamSize }}人</span>
|
|||
|
</div>
|
|||
|
<div class="info-item">
|
|||
|
<label>开始时间:</label>
|
|||
|
<span>{{ formatDate(currentProject.startDate) }}</span>
|
|||
|
</div>
|
|||
|
<div class="info-item" v-if="currentProject.endDate">
|
|||
|
<label>结束时间:</label>
|
|||
|
<span>{{ formatDate(currentProject.endDate) }}</span>
|
|||
|
</div>
|
|||
|
<div class="info-item" v-if="currentProject.progress !== undefined">
|
|||
|
<label>项目进度:</label>
|
|||
|
<span>{{ currentProject.progress }}%</span>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<!-- 异常状态 -->
|
|||
|
<div class="detail-section" v-if="currentProject.alerts && currentProject.alerts.length > 0">
|
|||
|
<h3>异常状态</h3>
|
|||
|
<div class="alerts-list">
|
|||
|
<div
|
|||
|
v-for="alert in currentProject.alerts"
|
|||
|
:key="alert.type"
|
|||
|
class="alert-item"
|
|||
|
:class="alert.type"
|
|||
|
>
|
|||
|
<icon-exclamation-circle />
|
|||
|
<span>{{ alert.message }}</span>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<!-- 团队成员明细 -->
|
|||
|
<div class="detail-section">
|
|||
|
<div class="section-header">
|
|||
|
<h3>团队成员明细</h3>
|
|||
|
<a-button size="small" @click="openPersonnelManagement">
|
|||
|
<template #icon><icon-user-group /></template>
|
|||
|
添加成员
|
|||
|
</a-button>
|
|||
|
</div>
|
|||
|
<div class="team-members">
|
|||
|
<div
|
|||
|
v-for="member in currentProject.teamMembers"
|
|||
|
:key="member.id"
|
|||
|
class="member-item"
|
|||
|
@click="editMemberPosition(member)"
|
|||
|
>
|
|||
|
<div class="member-avatar">
|
|||
|
<icon-user />
|
|||
|
</div>
|
|||
|
<div class="member-info">
|
|||
|
<div class="member-name">{{ member.name }}</div>
|
|||
|
<div class="member-position">{{ member.position }}</div>
|
|||
|
<div class="member-details">
|
|||
|
<span class="member-status" :class="member.status">
|
|||
|
{{ member.status === 'available' ? '在线' : '离线' }}
|
|||
|
</span>
|
|||
|
<span class="member-date">入职: {{ member.joinDate }}</span>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<div class="member-actions">
|
|||
|
<a-button size="mini" @click.stop="editMemberRemark(member)">
|
|||
|
<template #icon><icon-edit /></template>
|
|||
|
添加备注
|
|||
|
</a-button>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<!-- 空状态提示 -->
|
|||
|
<div v-if="!currentProject.teamMembers || currentProject.teamMembers.length === 0" class="empty-state">
|
|||
|
<div class="empty-icon">
|
|||
|
<icon-user-group />
|
|||
|
</div>
|
|||
|
<div class="empty-text">暂无团队成员</div>
|
|||
|
<div class="empty-desc">点击"添加成员"按钮来添加项目团队成员</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<!-- 项目需求发布 -->
|
|||
|
<div class="detail-section">
|
|||
|
<div class="section-header">
|
|||
|
<h3>项目需求</h3>
|
|||
|
<a-button type="primary" size="small" @click="openProjectRequirement">
|
|||
|
<template #icon><icon-plus /></template>
|
|||
|
发布需求
|
|||
|
</a-button>
|
|||
|
</div>
|
|||
|
<div class="requirements-list">
|
|||
|
<div
|
|||
|
v-for="requirement in currentProject.requirements"
|
|||
|
:key="requirement.id"
|
|||
|
class="requirement-item"
|
|||
|
>
|
|||
|
<div class="requirement-info">
|
|||
|
<div class="requirement-title">{{ requirement.title }}</div>
|
|||
|
<div class="requirement-details">
|
|||
|
<span>需求类型: {{ requirement.type === 'personnel' ? '人员需求' : '设备需求' }}</span>
|
|||
|
<span v-if="requirement.type === 'personnel'">需求人数: {{ requirement.count }}人</span>
|
|||
|
<span v-if="requirement.type === 'equipment'">设备数量: {{ requirement.count }}台</span>
|
|||
|
<span>技能要求: {{ requirement.skills.join(', ') }}</span>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<div class="requirement-status" :class="requirement.status">
|
|||
|
{{ getRequirementStatusText(requirement.status) }}
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</a-modal>
|
|||
|
|
|||
|
<!-- 备注编辑弹窗 -->
|
|||
|
<a-modal
|
|||
|
v-model:visible="remarkEditVisible"
|
|||
|
title="添加备注"
|
|||
|
width="600px"
|
|||
|
@ok="saveRemarkEdit"
|
|||
|
@cancel="cancelRemarkEdit"
|
|||
|
>
|
|||
|
<div v-if="editingMember" class="remark-edit">
|
|||
|
<div class="edit-item">
|
|||
|
<label>成员姓名:</label>
|
|||
|
<span>{{ editingMember.name }}</span>
|
|||
|
</div>
|
|||
|
<div class="edit-item">
|
|||
|
<label>当前岗位:</label>
|
|||
|
<span>{{ editingMember.position }}</span>
|
|||
|
</div>
|
|||
|
<div class="edit-item">
|
|||
|
<label>备注信息:</label>
|
|||
|
<a-textarea
|
|||
|
v-model="newRemark"
|
|||
|
placeholder="请输入备注信息,格式:主要负责:项目经理,次要负责:安全员等"
|
|||
|
:rows="4"
|
|||
|
/>
|
|||
|
</div>
|
|||
|
<div class="edit-item">
|
|||
|
<label>备注示例:</label>
|
|||
|
<div class="remark-example">
|
|||
|
主要负责:项目经理,次要负责:安全员,协助:质量员
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</a-modal>
|
|||
|
|
|||
|
<!-- 项目需求发布弹窗 -->
|
|||
|
<a-modal
|
|||
|
v-model:visible="requirementVisible"
|
|||
|
title="发布项目需求"
|
|||
|
width="500px"
|
|||
|
@ok="saveProjectRequirement"
|
|||
|
@cancel="cancelProjectRequirement"
|
|||
|
>
|
|||
|
<div class="requirement-form">
|
|||
|
<div class="form-item">
|
|||
|
<label>需求描述:</label>
|
|||
|
<a-textarea v-model="newRequirement.description" :rows="3" placeholder="请描述具体需求" />
|
|||
|
</div>
|
|||
|
<div class="form-item">
|
|||
|
<label>紧急程度:</label>
|
|||
|
<a-select v-model="newRequirement.priority" placeholder="请选择紧急程度">
|
|||
|
<a-option value="low">一般</a-option>
|
|||
|
<a-option value="medium">紧急</a-option>
|
|||
|
<a-option value="high">非常紧急</a-option>
|
|||
|
</a-select>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</a-modal>
|
|||
|
</GiPageLayout>
|
|||
|
</template>
|
|||
|
|
|||
|
<script setup lang="ts">
|
|||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
|||
|
import { useRouter } from 'vue-router'
|
|||
|
import { Message } from '@arco-design/web-vue'
|
|||
|
import { getProjectKanbanStats, getProjectKanbanData, getProjectDetail } from '@/apis/project/personnel-dispatch'
|
|||
|
|
|||
|
const router = useRouter()
|
|||
|
|
|||
|
// 响应式数据
|
|||
|
const stats = reactive({
|
|||
|
totalProjectsCount: '0',
|
|||
|
pendingProjectCount: '0',
|
|||
|
inProgressProjectCount: '0',
|
|||
|
completedProjectCount: '0',
|
|||
|
auditedProjectCount: '0',
|
|||
|
acceptedProjectCount: '0',
|
|||
|
totalTurbineCount: '0',
|
|||
|
pendingTurbineCount: '0',
|
|||
|
inProgressTurbineCount: '0',
|
|||
|
completedTurbineCount: '0',
|
|||
|
auditedTurbineCount: '0',
|
|||
|
acceptedTurbineCount: '0',
|
|||
|
totalTaskCount: '0',
|
|||
|
pendingTaskCount: '0',
|
|||
|
inProgressTaskCount: '0',
|
|||
|
completedTaskCount: '0',
|
|||
|
totalMemberCount: '0',
|
|||
|
projectManagerCount: '0',
|
|||
|
safetyOfficerCount: '0',
|
|||
|
qualityOfficerCount: '0',
|
|||
|
constructorCount: '0',
|
|||
|
teamLeaderCount: '0'
|
|||
|
})
|
|||
|
|
|||
|
// 加载状态
|
|||
|
const loading = ref(false)
|
|||
|
|
|||
|
// 项目数据
|
|||
|
const preparingProjects = ref<any[]>([])
|
|||
|
const ongoingProjects = ref<any[]>([])
|
|||
|
|
|||
|
|
|||
|
|
|||
|
// 弹窗状态
|
|||
|
const projectDetailVisible = ref(false)
|
|||
|
const remarkEditVisible = ref(false)
|
|||
|
const requirementVisible = ref(false)
|
|||
|
|
|||
|
// 当前选中的项目
|
|||
|
const currentProject = ref<any>(null)
|
|||
|
|
|||
|
// 编辑相关
|
|||
|
const editingMember = ref<any>(null)
|
|||
|
const newRemark = ref('')
|
|||
|
|
|||
|
// 需求相关
|
|||
|
const newRequirement = reactive({
|
|||
|
description: '',
|
|||
|
priority: 'medium'
|
|||
|
})
|
|||
|
|
|||
|
// 计算属性
|
|||
|
const totalProjects = computed(() => {
|
|||
|
return preparingProjects.value.length + ongoingProjects.value.length
|
|||
|
})
|
|||
|
|
|||
|
// 方法
|
|||
|
const mapProjectRespToProjectCard = (projectResp: any): any => {
|
|||
|
// 计算团队人数:根据constructorNames的人数 + 项目经理 + 安全员 + 质量员
|
|||
|
let teamSize = 3 // 基础人数:项目经理、安全员、质量员
|
|||
|
if (projectResp.constructorNames) {
|
|||
|
const constructorCount = projectResp.constructorNames.split(',').length
|
|||
|
teamSize += constructorCount
|
|||
|
}
|
|||
|
|
|||
|
// 处理团队成员数据 - 使用后端返回的真实数据
|
|||
|
const teamMembers = projectResp.teamMembers ? projectResp.teamMembers.map((member: any) => {
|
|||
|
console.log('处理团队成员数据:', member) // 添加调试日志
|
|||
|
const mappedMember = {
|
|||
|
id: member.memberId,
|
|||
|
name: member.name, // 修复:使用正确的姓名字段
|
|||
|
position: member.roleTypeDesc || member.jobCodeDesc,
|
|||
|
phone: member.phone || '', // 后端数据中的电话字段
|
|||
|
email: member.email || '', // 后端数据中的邮箱字段
|
|||
|
status: member.status === 'ACTIVE' ? 'available' : 'offline',
|
|||
|
skills: [], // 后端数据中没有技能字段
|
|||
|
joinDate: member.joinDate,
|
|||
|
remark: member.remark || member.jobDesc || '',
|
|||
|
// 保留原始数据用于后续处理
|
|||
|
originalData: member
|
|||
|
}
|
|||
|
console.log('映射后的团队成员数据:', mappedMember) // 添加调试日志
|
|||
|
return mappedMember
|
|||
|
}) : []
|
|||
|
|
|||
|
// 根据后端返回的ProjectResp格式映射到前端ProjectCard格式
|
|||
|
// 确保状态比较时类型一致
|
|||
|
const status = typeof projectResp.status === 'string' ? parseInt(projectResp.status) : projectResp.status
|
|||
|
|
|||
|
const mappedProject = {
|
|||
|
id: projectResp.projectId || projectResp.id,
|
|||
|
name: projectResp.projectName || projectResp.name,
|
|||
|
status: status === 0 ? 'preparing' : status === 1 ? 'ongoing' : 'pending',
|
|||
|
budget: projectResp.budget || 500000, // 使用后端返回的预算数据
|
|||
|
manager: projectResp.projectManagerName || projectResp.manager || '未指定',
|
|||
|
teamSize: projectResp.teamSize || teamSize, // 使用后端返回的团队人数
|
|||
|
preparationProgress: status === 0 ? 50 : 100, // 准备中项目进度50%,其他100%
|
|||
|
progress: projectResp.progressPercentage || 0,
|
|||
|
startDate: projectResp.startDate,
|
|||
|
endDate: projectResp.endDate,
|
|||
|
plannedStartDate: projectResp.plannedStartDate || projectResp.startDate,
|
|||
|
alerts: [], // 暂时为空数组
|
|||
|
teamMembers: teamMembers, // 使用处理后的团队成员数据
|
|||
|
requirements: [] // 暂时为空数组,后续可以通过API获取
|
|||
|
}
|
|||
|
|
|||
|
return mappedProject
|
|||
|
}
|
|||
|
|
|||
|
const loadStats = async () => {
|
|||
|
loading.value = true
|
|||
|
try {
|
|||
|
const response = await getProjectKanbanStats()
|
|||
|
if (response.data) {
|
|||
|
Object.assign(stats, response.data)
|
|||
|
}
|
|||
|
} catch (error) {
|
|||
|
console.error('获取项目看板统计数据失败:', error)
|
|||
|
Message.error('获取统计数据失败,请稍后重试')
|
|||
|
// 如果API调用失败,使用本地数据作为后备
|
|||
|
stats.totalProjectsCount = totalProjects.value.toString()
|
|||
|
stats.inProgressProjectCount = ongoingProjects.value.length.toString()
|
|||
|
stats.pendingProjectCount = '0'
|
|||
|
stats.completedProjectCount = '0'
|
|||
|
} finally {
|
|||
|
loading.value = false
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const loadKanbanData = async () => {
|
|||
|
loading.value = true
|
|||
|
try {
|
|||
|
const response = await getProjectKanbanData()
|
|||
|
console.log('后端返回的原始数据:', response.data)
|
|||
|
|
|||
|
if (response.data) {
|
|||
|
// 清空现有数据
|
|||
|
preparingProjects.value = []
|
|||
|
ongoingProjects.value = []
|
|||
|
|
|||
|
// 处理所有项目数据,根据status分类
|
|||
|
const allProjects = [
|
|||
|
...(response.data.preparingProjects || []),
|
|||
|
...(response.data.ongoingProjects || []),
|
|||
|
...(response.data.inProgressProjects || []), // 添加inProgressProjects
|
|||
|
...(response.data.pendingProjects || [])
|
|||
|
]
|
|||
|
|
|||
|
// 根据项目状态分类
|
|||
|
allProjects.forEach(project => {
|
|||
|
const mappedProject = mapProjectRespToProjectCard(project)
|
|||
|
|
|||
|
// 确保状态比较时类型一致,将字符串转换为数字进行比较
|
|||
|
const status = typeof project.status === 'string' ? parseInt(project.status) : project.status
|
|||
|
|
|||
|
if (status === 0) {
|
|||
|
// status: 0 表示准备中
|
|||
|
preparingProjects.value.push(mappedProject)
|
|||
|
} else if (status === 1) {
|
|||
|
// status: 1 表示进行中
|
|||
|
ongoingProjects.value.push(mappedProject)
|
|||
|
}
|
|||
|
// status: 其他值表示未开工,我们不需要显示
|
|||
|
})
|
|||
|
|
|||
|
console.log('分类后的准备中项目:', preparingProjects.value)
|
|||
|
console.log('分类后的进行中项目:', ongoingProjects.value)
|
|||
|
}
|
|||
|
} catch (error) {
|
|||
|
console.error('获取项目看板数据失败:', error)
|
|||
|
Message.error('获取看板数据失败,请稍后重试')
|
|||
|
|
|||
|
// 如果API调用失败,使用模拟数据作为后备
|
|||
|
const mockPreparingProjects = [
|
|||
|
{
|
|||
|
projectId: 'mock-001',
|
|||
|
projectName: '三峡能源阿城万兴风电场防雷通道检测项目',
|
|||
|
projectManagerName: '张大川',
|
|||
|
turbineCount: '1',
|
|||
|
status: 0,
|
|||
|
startDate: '2025-07-01',
|
|||
|
endDate: '2025-07-31',
|
|||
|
progressPercentage: 0
|
|||
|
}
|
|||
|
]
|
|||
|
|
|||
|
const mockOngoingProjects = [
|
|||
|
{
|
|||
|
projectId: '0b71a1259c49918c6595c9720ad1db5d',
|
|||
|
projectName: '三峡能源辽宁分公司庄河海上风电场防雷通道检测项目',
|
|||
|
projectManagerName: '张大川,董冰',
|
|||
|
constructorNames: '牟晓松,cs,谢小兵,高阳,韩凌柱,朱贤进',
|
|||
|
turbineCount: '0',
|
|||
|
status: 1,
|
|||
|
startDate: '2025-06-05',
|
|||
|
endDate: null,
|
|||
|
progressPercentage: 0
|
|||
|
}
|
|||
|
]
|
|||
|
|
|||
|
preparingProjects.value = mockPreparingProjects.map(mapProjectRespToProjectCard)
|
|||
|
ongoingProjects.value = mockOngoingProjects.map(mapProjectRespToProjectCard)
|
|||
|
} finally {
|
|||
|
loading.value = false
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const refreshData = async () => {
|
|||
|
await Promise.all([
|
|||
|
loadStats(),
|
|||
|
loadKanbanData()
|
|||
|
])
|
|||
|
}
|
|||
|
|
|||
|
const formatBudget = (budget: number) => {
|
|||
|
return `¥${(budget / 10000).toFixed(0)}万`
|
|||
|
}
|
|||
|
|
|||
|
const formatDate = (date: string) => {
|
|||
|
return new Date(date).toLocaleDateString('zh-CN')
|
|||
|
}
|
|||
|
|
|||
|
const getStatusText = (status: string) => {
|
|||
|
const statusMap = {
|
|||
|
preparing: '准备中',
|
|||
|
ongoing: '进行中',
|
|||
|
pending: '未开工'
|
|||
|
}
|
|||
|
return statusMap[status] || status
|
|||
|
}
|
|||
|
|
|||
|
const getRequirementStatusText = (status: string) => {
|
|||
|
const statusMap = {
|
|||
|
pending: '待解决',
|
|||
|
recruiting: '解决中',
|
|||
|
completed: '已解决'
|
|||
|
}
|
|||
|
return statusMap[status] || status
|
|||
|
}
|
|||
|
|
|||
|
const openProjectDetail = async (project: any) => {
|
|||
|
try {
|
|||
|
loading.value = true
|
|||
|
console.log('正在获取项目详情,项目ID:', project.id)
|
|||
|
|
|||
|
// 首先检查传入的项目数据是否已经包含团队成员信息
|
|||
|
if (project.teamMembers && project.teamMembers.length > 0) {
|
|||
|
console.log('项目数据已包含团队成员信息,直接使用')
|
|||
|
currentProject.value = project
|
|||
|
projectDetailVisible.value = true
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
// 如果项目数据中没有团队成员信息,则调用API获取
|
|||
|
const response = await getProjectDetail(project.id)
|
|||
|
|
|||
|
if (response.data) {
|
|||
|
// 使用映射函数处理后端数据
|
|||
|
currentProject.value = mapProjectRespToProjectCard(response.data)
|
|||
|
projectDetailVisible.value = true
|
|||
|
} else {
|
|||
|
// 如果API没有返回数据,使用传入的项目数据作为后备
|
|||
|
currentProject.value = project
|
|||
|
projectDetailVisible.value = true
|
|||
|
}
|
|||
|
} catch (error) {
|
|||
|
console.error('获取项目详情失败:', error)
|
|||
|
Message.error('获取项目详情失败,请稍后重试')
|
|||
|
// 如果API调用失败,使用传入的项目数据作为后备
|
|||
|
currentProject.value = project
|
|||
|
projectDetailVisible.value = true
|
|||
|
} finally {
|
|||
|
loading.value = false
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const closeProjectDetail = () => {
|
|||
|
projectDetailVisible.value = false
|
|||
|
currentProject.value = null
|
|||
|
}
|
|||
|
|
|||
|
const openPersonnelManagement = () => {
|
|||
|
if (currentProject.value && currentProject.value.id) {
|
|||
|
router.push({
|
|||
|
path: '/project-management/projects/personnel-dispatch/construction-personnel',
|
|||
|
query: { projectId: currentProject.value.id }
|
|||
|
})
|
|||
|
} else {
|
|||
|
Message.error('项目信息不完整,无法进入团队成员管理')
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const editMemberPosition = (member: any) => {
|
|||
|
// 这里可以实现编辑成员岗位的逻辑
|
|||
|
console.log('编辑成员岗位:', member)
|
|||
|
Message.info('编辑成员岗位功能开发中...')
|
|||
|
}
|
|||
|
|
|||
|
const editMemberRemark = (member: any) => {
|
|||
|
editingMember.value = member
|
|||
|
newRemark.value = member.remark || ''
|
|||
|
remarkEditVisible.value = true
|
|||
|
}
|
|||
|
|
|||
|
const saveRemarkEdit = () => {
|
|||
|
if (editingMember.value && newRemark.value) {
|
|||
|
editingMember.value.remark = newRemark.value
|
|||
|
Message.success('备注更新成功')
|
|||
|
remarkEditVisible.value = false
|
|||
|
editingMember.value = null
|
|||
|
newRemark.value = ''
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const cancelRemarkEdit = () => {
|
|||
|
remarkEditVisible.value = false
|
|||
|
editingMember.value = null
|
|||
|
newRemark.value = ''
|
|||
|
}
|
|||
|
|
|||
|
const openProjectRequirement = () => {
|
|||
|
requirementVisible.value = true
|
|||
|
}
|
|||
|
|
|||
|
const saveProjectRequirement = () => {
|
|||
|
if (newRequirement.description && newRequirement.priority) {
|
|||
|
const requirement = {
|
|||
|
id: Date.now(),
|
|||
|
title: '项目需求',
|
|||
|
description: newRequirement.description,
|
|||
|
priority: newRequirement.priority,
|
|||
|
status: 'pending'
|
|||
|
}
|
|||
|
if (currentProject.value) {
|
|||
|
currentProject.value.requirements.push(requirement)
|
|||
|
}
|
|||
|
Message.success('项目需求发布成功')
|
|||
|
requirementVisible.value = false
|
|||
|
// 重置表单
|
|||
|
Object.assign(newRequirement, {
|
|||
|
description: '',
|
|||
|
priority: 'medium'
|
|||
|
})
|
|||
|
} else {
|
|||
|
Message.error('请填写完整信息')
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const cancelProjectRequirement = () => {
|
|||
|
requirementVisible.value = false
|
|||
|
Object.assign(newRequirement, {
|
|||
|
description: '',
|
|||
|
priority: 'medium'
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
// 生命周期
|
|||
|
onMounted(async () => {
|
|||
|
console.log('页面开始加载数据...')
|
|||
|
await Promise.all([
|
|||
|
loadStats(),
|
|||
|
loadKanbanData()
|
|||
|
])
|
|||
|
console.log('数据加载完成,当前状态:')
|
|||
|
console.log('准备中项目数量:', preparingProjects.value.length)
|
|||
|
console.log('进行中项目数量:', ongoingProjects.value.length)
|
|||
|
})
|
|||
|
</script>
|
|||
|
|
|||
|
<style lang="scss" scoped>
|
|||
|
.personnel-dispatch-header {
|
|||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|||
|
color: white;
|
|||
|
padding: 24px;
|
|||
|
border-radius: 12px;
|
|||
|
margin-bottom: 24px;
|
|||
|
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.15);
|
|||
|
|
|||
|
.header-content {
|
|||
|
display: flex;
|
|||
|
justify-content: space-between;
|
|||
|
align-items: center;
|
|||
|
}
|
|||
|
|
|||
|
.header-title {
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
gap: 12px;
|
|||
|
|
|||
|
.title-icon {
|
|||
|
font-size: 32px;
|
|||
|
color: rgba(255, 255, 255, 0.9);
|
|||
|
}
|
|||
|
|
|||
|
h1 {
|
|||
|
margin: 0;
|
|||
|
font-size: 28px;
|
|||
|
font-weight: 600;
|
|||
|
color: white;
|
|||
|
}
|
|||
|
|
|||
|
.refresh-btn {
|
|||
|
color: rgba(255, 255, 255, 0.8);
|
|||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|||
|
background: rgba(255, 255, 255, 0.1);
|
|||
|
|
|||
|
&:hover {
|
|||
|
color: white;
|
|||
|
background: rgba(255, 255, 255, 0.2);
|
|||
|
border-color: rgba(255, 255, 255, 0.3);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.header-stats {
|
|||
|
display: flex;
|
|||
|
gap: 24px;
|
|||
|
|
|||
|
.stat-card {
|
|||
|
text-align: center;
|
|||
|
padding: 16px 24px;
|
|||
|
background: rgba(255, 255, 255, 0.1);
|
|||
|
border-radius: 8px;
|
|||
|
backdrop-filter: blur(10px);
|
|||
|
transition: all 0.3s ease;
|
|||
|
|
|||
|
&.loading {
|
|||
|
opacity: 0.7;
|
|||
|
background: rgba(255, 255, 255, 0.05);
|
|||
|
}
|
|||
|
|
|||
|
.stat-number {
|
|||
|
font-size: 32px;
|
|||
|
font-weight: 700;
|
|||
|
margin-bottom: 4px;
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
justify-content: center;
|
|||
|
gap: 8px;
|
|||
|
min-height: 32px;
|
|||
|
}
|
|||
|
|
|||
|
.stat-label {
|
|||
|
font-size: 14px;
|
|||
|
opacity: 0.9;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.project-kanban {
|
|||
|
display: grid;
|
|||
|
grid-template-columns: repeat(2, 1fr);
|
|||
|
gap: 24px;
|
|||
|
margin-bottom: 24px;
|
|||
|
|
|||
|
.kanban-column {
|
|||
|
background: #f7f8fa;
|
|||
|
border-radius: 12px;
|
|||
|
padding: 20px;
|
|||
|
min-height: 600px;
|
|||
|
|
|||
|
.column-header {
|
|||
|
margin-bottom: 20px;
|
|||
|
padding: 16px;
|
|||
|
border-radius: 8px;
|
|||
|
color: white;
|
|||
|
|
|||
|
&.preparing {
|
|||
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
|||
|
}
|
|||
|
|
|||
|
&.ongoing {
|
|||
|
background: linear-gradient(135deg, #51cf66 0%, #40c057 100%);
|
|||
|
}
|
|||
|
|
|||
|
&.pending {
|
|||
|
background: linear-gradient(135deg, #868e96 0%, #6c757d 100%);
|
|||
|
}
|
|||
|
|
|||
|
.column-title {
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
gap: 12px;
|
|||
|
|
|||
|
.status-icon {
|
|||
|
font-size: 24px;
|
|||
|
}
|
|||
|
|
|||
|
h2 {
|
|||
|
margin: 0;
|
|||
|
font-size: 18px;
|
|||
|
font-weight: 600;
|
|||
|
}
|
|||
|
|
|||
|
.project-count {
|
|||
|
background: rgba(255, 255, 255, 0.2);
|
|||
|
padding: 4px 8px;
|
|||
|
border-radius: 12px;
|
|||
|
font-size: 12px;
|
|||
|
font-weight: 600;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.column-content {
|
|||
|
display: flex;
|
|||
|
flex-direction: column;
|
|||
|
gap: 16px;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.project-card {
|
|||
|
background: white;
|
|||
|
border-radius: 8px;
|
|||
|
padding: 16px;
|
|||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|||
|
cursor: pointer;
|
|||
|
transition: all 0.3s ease;
|
|||
|
border-left: 4px solid transparent;
|
|||
|
|
|||
|
&:hover {
|
|||
|
transform: translateY(-2px);
|
|||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
|||
|
}
|
|||
|
|
|||
|
&.preparing {
|
|||
|
border-left-color: #ff6b6b;
|
|||
|
}
|
|||
|
|
|||
|
&.ongoing {
|
|||
|
border-left-color: #51cf66;
|
|||
|
}
|
|||
|
|
|||
|
&.pending {
|
|||
|
border-left-color: #868e96;
|
|||
|
}
|
|||
|
|
|||
|
.project-header {
|
|||
|
display: flex;
|
|||
|
justify-content: space-between;
|
|||
|
align-items: center;
|
|||
|
margin-bottom: 12px;
|
|||
|
|
|||
|
.project-name {
|
|||
|
margin: 0;
|
|||
|
font-size: 16px;
|
|||
|
font-weight: 600;
|
|||
|
color: #1d2129;
|
|||
|
}
|
|||
|
|
|||
|
.project-status {
|
|||
|
padding: 4px 8px;
|
|||
|
border-radius: 4px;
|
|||
|
font-size: 12px;
|
|||
|
font-weight: 500;
|
|||
|
|
|||
|
&.preparing {
|
|||
|
background: #fff5f5;
|
|||
|
color: #ff6b6b;
|
|||
|
}
|
|||
|
|
|||
|
&.ongoing {
|
|||
|
background: #f0fff4;
|
|||
|
color: #51cf66;
|
|||
|
}
|
|||
|
|
|||
|
&.pending {
|
|||
|
background: #f8f9fa;
|
|||
|
color: #868e96;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.project-info {
|
|||
|
margin-bottom: 12px;
|
|||
|
|
|||
|
.info-item {
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
gap: 8px;
|
|||
|
margin-bottom: 6px;
|
|||
|
font-size: 14px;
|
|||
|
color: #4e5969;
|
|||
|
|
|||
|
.info-icon {
|
|||
|
font-size: 14px;
|
|||
|
color: #86909c;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.project-footer {
|
|||
|
.progress-info {
|
|||
|
display: flex;
|
|||
|
justify-content: space-between;
|
|||
|
align-items: center;
|
|||
|
margin-bottom: 6px;
|
|||
|
font-size: 12px;
|
|||
|
color: #86909c;
|
|||
|
|
|||
|
.progress-text {
|
|||
|
font-weight: 600;
|
|||
|
color: #1d2129;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.progress-bar {
|
|||
|
height: 6px;
|
|||
|
background: #f0f0f0;
|
|||
|
border-radius: 3px;
|
|||
|
overflow: hidden;
|
|||
|
|
|||
|
.progress-fill {
|
|||
|
height: 100%;
|
|||
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
|||
|
border-radius: 3px;
|
|||
|
transition: width 0.3s ease;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.start-date {
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
gap: 8px;
|
|||
|
font-size: 12px;
|
|||
|
color: #86909c;
|
|||
|
|
|||
|
.info-icon {
|
|||
|
font-size: 12px;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.project-alerts {
|
|||
|
margin-top: 12px;
|
|||
|
|
|||
|
.alert-item {
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
gap: 6px;
|
|||
|
padding: 6px 8px;
|
|||
|
border-radius: 4px;
|
|||
|
font-size: 12px;
|
|||
|
margin-bottom: 4px;
|
|||
|
|
|||
|
&.cost {
|
|||
|
background: #fff5f5;
|
|||
|
color: #ff6b6b;
|
|||
|
}
|
|||
|
|
|||
|
&.personnel {
|
|||
|
background: #fff8e1;
|
|||
|
color: #ffa000;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.project-detail {
|
|||
|
.detail-section {
|
|||
|
margin-bottom: 24px;
|
|||
|
|
|||
|
h3 {
|
|||
|
margin: 0 0 16px 0;
|
|||
|
font-size: 16px;
|
|||
|
font-weight: 600;
|
|||
|
color: #1d2129;
|
|||
|
border-bottom: 2px solid #f0f0f0;
|
|||
|
padding-bottom: 8px;
|
|||
|
}
|
|||
|
|
|||
|
.section-header {
|
|||
|
display: flex;
|
|||
|
justify-content: space-between;
|
|||
|
align-items: center;
|
|||
|
margin-bottom: 16px;
|
|||
|
|
|||
|
h3 {
|
|||
|
margin: 0;
|
|||
|
border-bottom: none;
|
|||
|
padding-bottom: 0;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.info-grid {
|
|||
|
display: grid;
|
|||
|
grid-template-columns: repeat(2, 1fr);
|
|||
|
gap: 16px;
|
|||
|
|
|||
|
.info-item {
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
gap: 8px;
|
|||
|
|
|||
|
label {
|
|||
|
font-weight: 500;
|
|||
|
color: #4e5969;
|
|||
|
min-width: 80px;
|
|||
|
}
|
|||
|
|
|||
|
.status-badge {
|
|||
|
padding: 4px 8px;
|
|||
|
border-radius: 4px;
|
|||
|
font-size: 12px;
|
|||
|
font-weight: 500;
|
|||
|
|
|||
|
&.preparing {
|
|||
|
background: #fff5f5;
|
|||
|
color: #ff6b6b;
|
|||
|
}
|
|||
|
|
|||
|
&.ongoing {
|
|||
|
background: #f0fff4;
|
|||
|
color: #51cf66;
|
|||
|
}
|
|||
|
|
|||
|
&.pending {
|
|||
|
background: #f8f9fa;
|
|||
|
color: #868e96;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.alerts-list {
|
|||
|
.alert-item {
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
gap: 8px;
|
|||
|
padding: 12px;
|
|||
|
border-radius: 6px;
|
|||
|
margin-bottom: 8px;
|
|||
|
|
|||
|
&.cost {
|
|||
|
background: #fff5f5;
|
|||
|
color: #ff6b6b;
|
|||
|
border: 1px solid #ffebee;
|
|||
|
}
|
|||
|
|
|||
|
&.personnel {
|
|||
|
background: #fff8e1;
|
|||
|
color: #ffa000;
|
|||
|
border: 1px solid #fff3cd;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.team-members {
|
|||
|
.member-item {
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
gap: 12px;
|
|||
|
padding: 12px;
|
|||
|
border: 1px solid #f0f0f0;
|
|||
|
border-radius: 6px;
|
|||
|
margin-bottom: 8px;
|
|||
|
cursor: pointer;
|
|||
|
transition: all 0.3s ease;
|
|||
|
|
|||
|
&:hover {
|
|||
|
background: #f8f9fa;
|
|||
|
border-color: #667eea;
|
|||
|
}
|
|||
|
|
|||
|
.member-avatar {
|
|||
|
width: 40px;
|
|||
|
height: 40px;
|
|||
|
border-radius: 50%;
|
|||
|
background: #f0f0f0;
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
justify-content: center;
|
|||
|
color: #86909c;
|
|||
|
}
|
|||
|
|
|||
|
.member-info {
|
|||
|
flex: 1;
|
|||
|
|
|||
|
.member-name {
|
|||
|
font-weight: 500;
|
|||
|
color: #1d2129;
|
|||
|
margin-bottom: 2px;
|
|||
|
}
|
|||
|
|
|||
|
.member-position {
|
|||
|
font-size: 12px;
|
|||
|
color: #86909c;
|
|||
|
}
|
|||
|
|
|||
|
.member-details {
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
gap: 8px;
|
|||
|
font-size: 12px;
|
|||
|
color: #86909c;
|
|||
|
|
|||
|
.member-status {
|
|||
|
padding: 2px 6px;
|
|||
|
border-radius: 4px;
|
|||
|
font-weight: 500;
|
|||
|
|
|||
|
&.available {
|
|||
|
background: #e3f2fd;
|
|||
|
color: #2196f3;
|
|||
|
}
|
|||
|
|
|||
|
&.offline {
|
|||
|
background: #f8f9fa;
|
|||
|
color: #868e96;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.member-actions {
|
|||
|
.arco-btn {
|
|||
|
font-size: 12px;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.empty-state {
|
|||
|
text-align: center;
|
|||
|
padding: 40px 0;
|
|||
|
color: #86909c;
|
|||
|
|
|||
|
.empty-icon {
|
|||
|
font-size: 48px;
|
|||
|
margin-bottom: 16px;
|
|||
|
}
|
|||
|
|
|||
|
.empty-text {
|
|||
|
font-size: 18px;
|
|||
|
font-weight: 500;
|
|||
|
margin-bottom: 8px;
|
|||
|
}
|
|||
|
|
|||
|
.empty-desc {
|
|||
|
font-size: 14px;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.requirements-list {
|
|||
|
.requirement-item {
|
|||
|
display: flex;
|
|||
|
justify-content: space-between;
|
|||
|
align-items: center;
|
|||
|
padding: 12px;
|
|||
|
border: 1px solid #f0f0f0;
|
|||
|
border-radius: 6px;
|
|||
|
margin-bottom: 8px;
|
|||
|
|
|||
|
.requirement-info {
|
|||
|
.requirement-title {
|
|||
|
font-weight: 500;
|
|||
|
color: #1d2129;
|
|||
|
margin-bottom: 4px;
|
|||
|
}
|
|||
|
|
|||
|
.requirement-details {
|
|||
|
font-size: 12px;
|
|||
|
color: #86909c;
|
|||
|
|
|||
|
span {
|
|||
|
margin-right: 16px;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.requirement-status {
|
|||
|
padding: 4px 8px;
|
|||
|
border-radius: 4px;
|
|||
|
font-size: 12px;
|
|||
|
font-weight: 500;
|
|||
|
|
|||
|
&.pending {
|
|||
|
background: #fff8e1;
|
|||
|
color: #ffa000;
|
|||
|
}
|
|||
|
|
|||
|
&.recruiting {
|
|||
|
background: #e3f2fd;
|
|||
|
color: #2196f3;
|
|||
|
}
|
|||
|
|
|||
|
&.completed {
|
|||
|
background: #f0fff4;
|
|||
|
color: #51cf66;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.remark-edit {
|
|||
|
.edit-item {
|
|||
|
display: flex;
|
|||
|
align-items: flex-start;
|
|||
|
gap: 12px;
|
|||
|
margin-bottom: 16px;
|
|||
|
|
|||
|
label {
|
|||
|
font-weight: 500;
|
|||
|
color: #4e5969;
|
|||
|
min-width: 80px;
|
|||
|
margin-top: 4px;
|
|||
|
}
|
|||
|
|
|||
|
.arco-textarea {
|
|||
|
flex: 1;
|
|||
|
}
|
|||
|
|
|||
|
.remark-example {
|
|||
|
flex: 1;
|
|||
|
padding: 8px 12px;
|
|||
|
background: #f7f8fa;
|
|||
|
border-radius: 4px;
|
|||
|
color: #86909c;
|
|||
|
font-size: 12px;
|
|||
|
border: 1px solid #e5e6eb;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.requirement-form {
|
|||
|
.form-item {
|
|||
|
margin-bottom: 16px;
|
|||
|
|
|||
|
label {
|
|||
|
display: block;
|
|||
|
font-weight: 500;
|
|||
|
color: #4e5969;
|
|||
|
margin-bottom: 6px;
|
|||
|
}
|
|||
|
|
|||
|
.arco-select,
|
|||
|
.arco-input-number,
|
|||
|
.arco-textarea {
|
|||
|
width: 100%;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 滚动修复
|
|||
|
.project-kanban {
|
|||
|
overflow-y: auto;
|
|||
|
max-height: calc(100vh - 300px);
|
|||
|
}
|
|||
|
|
|||
|
.project-detail {
|
|||
|
max-height: calc(100vh - 200px);
|
|||
|
overflow-y: auto;
|
|||
|
}
|
|||
|
|
|||
|
// 响应式设计
|
|||
|
@media (max-width: 1200px) {
|
|||
|
.project-kanban {
|
|||
|
grid-template-columns: 1fr;
|
|||
|
gap: 16px;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
@media (max-width: 768px) {
|
|||
|
.personnel-dispatch-header {
|
|||
|
.header-content {
|
|||
|
flex-direction: column;
|
|||
|
gap: 20px;
|
|||
|
text-align: center;
|
|||
|
}
|
|||
|
|
|||
|
.header-stats {
|
|||
|
flex-wrap: wrap;
|
|||
|
justify-content: center;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.info-grid {
|
|||
|
grid-template-columns: 1fr;
|
|||
|
}
|
|||
|
}
|
|||
|
</style>
|