Industrial-image-management.../src/views/project-management/personnel-dispatch/index.vue

1539 lines
42 KiB
Vue
Raw Normal View History

2025-08-08 14:26:22 +08:00
<!--
人员调度管理页面 - 项目状态看板
功能特性:
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">
2025-08-10 20:41:24 +08:00
<div class="section-header team-members-header">
<div class="header-left">
<h3>团队成员明细</h3>
<span class="member-count" v-if="currentProject.teamMembers && currentProject.teamMembers.length > 0">
({{ currentProject.teamMembers.length }})
</span>
</div>
<a-button size="small" @click="openPersonnelManagement" class="add-member-btn">
2025-08-08 14:26:22 +08:00
<template #icon><icon-user-group /></template>
添加成员
</a-button>
</div>
<div class="team-members">
<div
2025-08-10 20:41:24 +08:00
v-for="(member, index) in currentProject.teamMembers"
2025-08-08 14:26:22 +08:00
:key="member.id"
class="member-item"
2025-08-10 20:41:24 +08:00
:style="{ animationDelay: `${index * 0.1}s` }"
2025-08-08 14:26:22 +08:00
@click="editMemberPosition(member)"
>
<div class="member-avatar">
<icon-user />
</div>
<div class="member-info">
2025-08-10 20:41:24 +08:00
<div class="member-name">{{ member.name || '未设置姓名' }}</div>
<div class="member-position">{{ member.position || '未设置岗位' }}</div>
2025-08-08 14:26:22 +08:00
<div class="member-details">
<span class="member-status" :class="member.status">
{{ member.status === 'available' ? '在线' : '离线' }}
</span>
2025-08-10 20:41:24 +08:00
<span class="member-date">入职: {{ member.joinDate || '未设置' }}</span>
2025-08-08 14:26:22 +08:00
</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) // 添加调试日志
2025-08-10 20:41:24 +08:00
console.log('成员userName字段:', member.userName)
console.log('成员name字段:', member.name)
2025-08-08 14:26:22 +08:00
const mappedMember = {
id: member.memberId,
2025-08-10 20:41:24 +08:00
name: member.userName || member.name || '未设置姓名', // 修复优先使用userName字段
position: member.roleTypeDesc || member.jobCodeDesc || '未设置岗位',
2025-08-08 14:26:22 +08:00
phone: member.phone || '', // 后端数据中的电话字段
email: member.email || '', // 后端数据中的邮箱字段
status: member.status === 'ACTIVE' ? 'available' : 'offline',
skills: [], // 后端数据中没有技能字段
2025-08-10 20:41:24 +08:00
joinDate: member.joinDate || '未设置',
2025-08-08 14:26:22 +08:00
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 || [])
]
2025-08-10 20:41:24 +08:00
console.log('后端返回的所有项目数据:', allProjects)
console.log('后端返回的preparingProjects:', response.data.preparingProjects)
console.log('后端返回的ongoingProjects:', response.data.ongoingProjects)
console.log('后端返回的inProgressProjects:', response.data.inProgressProjects)
console.log('后端返回的pendingProjects:', response.data.pendingProjects)
2025-08-08 14:26:22 +08:00
// 根据项目状态分类
allProjects.forEach(project => {
2025-08-10 20:41:24 +08:00
console.log('处理项目:', project.projectName || project.name)
console.log('项目团队成员:', project.teamMembers)
console.log('项目状态:', project.status, typeof project.status)
2025-08-08 14:26:22 +08:00
const mappedProject = mapProjectRespToProjectCard(project)
2025-08-10 20:41:24 +08:00
console.log('映射后的项目:', mappedProject.name)
console.log('映射后的团队成员:', mappedProject.teamMembers)
2025-08-08 14:26:22 +08:00
// 确保状态比较时类型一致,将字符串转换为数字进行比较
const status = typeof project.status === 'string' ? parseInt(project.status) : project.status
if (status === 0) {
// status: 0 表示准备中
2025-08-10 20:41:24 +08:00
console.log('添加到准备中项目:', mappedProject.name)
2025-08-08 14:26:22 +08:00
preparingProjects.value.push(mappedProject)
} else if (status === 1) {
// status: 1 表示进行中
2025-08-10 20:41:24 +08:00
console.log('添加到进行中项目:', mappedProject.name)
2025-08-08 14:26:22 +08:00
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)
2025-08-10 20:41:24 +08:00
console.log('传入的项目数据:', project)
2025-08-08 14:26:22 +08:00
// 首先检查传入的项目数据是否已经包含团队成员信息
if (project.teamMembers && project.teamMembers.length > 0) {
console.log('项目数据已包含团队成员信息,直接使用')
2025-08-10 20:41:24 +08:00
console.log('团队成员数据:', project.teamMembers)
2025-08-08 14:26:22 +08:00
currentProject.value = project
projectDetailVisible.value = true
return
}
// 如果项目数据中没有团队成员信息则调用API获取
const response = await getProjectDetail(project.id)
if (response.data) {
// 使用映射函数处理后端数据
2025-08-10 20:41:24 +08:00
console.log('API返回的原始数据:', response.data)
2025-08-08 14:26:22 +08:00
currentProject.value = mapProjectRespToProjectCard(response.data)
2025-08-10 20:41:24 +08:00
console.log('映射后的项目数据:', currentProject.value)
2025-08-08 14:26:22 +08:00
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({
2025-08-08 15:23:57 +08:00
path: '/project-management/personnel-dispatch/construction-personnel',
2025-08-08 14:26:22 +08:00
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;
}
}
}
2025-08-10 20:41:24 +08:00
.team-members-header {
.header-left {
display: flex;
align-items: center;
gap: 8px;
h3 {
margin: 0;
color: #1d2129;
font-size: 18px;
font-weight: 600;
}
.member-count {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
}
.add-member-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
}
}
2025-08-08 14:26:22 +08:00
.team-members {
2025-08-10 20:41:24 +08:00
max-height: 400px;
overflow-y: auto;
padding-right: 8px;
// 自定义滚动条样式
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 3px;
&:hover {
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
}
}
2025-08-08 14:26:22 +08:00
.member-item {
display: flex;
align-items: center;
2025-08-10 20:41:24 +08:00
gap: 16px;
padding: 16px;
border: 1px solid #e5e6eb;
border-radius: 12px;
margin-bottom: 12px;
2025-08-08 14:26:22 +08:00
cursor: pointer;
2025-08-10 20:41:24 +08:00
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
animation: fadeInUp 0.6s ease-out forwards;
opacity: 0;
transform: translateY(20px);
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
2025-08-08 14:26:22 +08:00
&:hover {
2025-08-10 20:41:24 +08:00
background: linear-gradient(135deg, #f8f9ff 0%, #e8f2ff 100%);
2025-08-08 14:26:22 +08:00
border-color: #667eea;
2025-08-10 20:41:24 +08:00
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.15);
transform: translateY(-2px);
}
&:active {
transform: translateY(0);
2025-08-08 14:26:22 +08:00
}
.member-avatar {
2025-08-10 20:41:24 +08:00
width: 48px;
height: 48px;
2025-08-08 14:26:22 +08:00
border-radius: 50%;
2025-08-10 20:41:24 +08:00
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
2025-08-08 14:26:22 +08:00
display: flex;
align-items: center;
justify-content: center;
2025-08-10 20:41:24 +08:00
color: white;
font-size: 20px;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
position: relative;
&::after {
content: '';
position: absolute;
top: -2px;
right: -2px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #52c41a;
border: 2px solid white;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
2025-08-08 14:26:22 +08:00
}
.member-info {
flex: 1;
2025-08-10 20:41:24 +08:00
min-width: 0;
2025-08-08 14:26:22 +08:00
.member-name {
2025-08-10 20:41:24 +08:00
font-weight: 600;
2025-08-08 14:26:22 +08:00
color: #1d2129;
2025-08-10 20:41:24 +08:00
margin-bottom: 4px;
font-size: 16px;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
2025-08-08 14:26:22 +08:00
}
.member-position {
2025-08-10 20:41:24 +08:00
font-size: 13px;
color: #4e5969;
margin-bottom: 6px;
font-weight: 500;
background: linear-gradient(135deg, #f0f2f5 0%, #e5e6eb 100%);
padding: 2px 8px;
border-radius: 6px;
display: inline-block;
2025-08-08 14:26:22 +08:00
}
.member-details {
display: flex;
align-items: center;
2025-08-10 20:41:24 +08:00
gap: 12px;
2025-08-08 14:26:22 +08:00
font-size: 12px;
color: #86909c;
.member-status {
2025-08-10 20:41:24 +08:00
padding: 4px 8px;
border-radius: 6px;
2025-08-08 14:26:22 +08:00
font-weight: 500;
2025-08-10 20:41:24 +08:00
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
2025-08-08 14:26:22 +08:00
&.available {
2025-08-10 20:41:24 +08:00
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
color: #1890ff;
border: 1px solid #91d5ff;
2025-08-08 14:26:22 +08:00
}
&.offline {
2025-08-10 20:41:24 +08:00
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
color: #8c8c8c;
border: 1px solid #d9d9d9;
2025-08-08 14:26:22 +08:00
}
}
2025-08-10 20:41:24 +08:00
.member-date {
background: #f7f8fa;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid #e5e6eb;
}
2025-08-08 14:26:22 +08:00
}
}
.member-actions {
.arco-btn {
font-size: 12px;
2025-08-10 20:41:24 +08:00
border-radius: 8px;
padding: 6px 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
transition: all 0.3s ease;
&:hover {
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
2025-08-08 14:26:22 +08:00
}
}
}
.empty-state {
text-align: center;
2025-08-10 20:41:24 +08:00
padding: 60px 20px;
2025-08-08 14:26:22 +08:00
color: #86909c;
2025-08-10 20:41:24 +08:00
background: linear-gradient(135deg, #fafbfc 0%, #f0f2f5 100%);
border-radius: 12px;
border: 2px dashed #d9d9d9;
margin: 20px 0;
2025-08-08 14:26:22 +08:00
.empty-icon {
2025-08-10 20:41:24 +08:00
font-size: 64px;
margin-bottom: 20px;
color: #bfbfbf;
opacity: 0.6;
2025-08-08 14:26:22 +08:00
}
.empty-text {
2025-08-10 20:41:24 +08:00
font-size: 20px;
font-weight: 600;
margin-bottom: 12px;
color: #595959;
2025-08-08 14:26:22 +08:00
}
.empty-desc {
font-size: 14px;
2025-08-10 20:41:24 +08:00
color: #8c8c8c;
line-height: 1.6;
2025-08-08 14:26:22 +08:00
}
}
}
.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>