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

1539 lines
42 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.

<!--
人员调度管理页面 - 项目状态看板
功能特性:
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 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">
<template #icon><icon-user-group /></template>
添加成员
</a-button>
</div>
<div class="team-members">
<div
v-for="(member, index) in currentProject.teamMembers"
:key="member.id"
class="member-item"
:style="{ animationDelay: `${index * 0.1}s` }"
@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 === 'ACTIVE' ? '在线' : '离线' }}
</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) // 添加调试日志
console.log('成员userName字段:', member.userName)
console.log('成员name字段:', member.name)
const mappedMember = {
id: member.memberId,
name: member.userName || member.name || '未设置姓名', // 修复优先使用userName字段
position: member.roleTypeDesc || member.jobCodeDesc || '未设置岗位',
phone: member.phone || '', // 后端数据中的电话字段
email: member.email || '', // 后端数据中的邮箱字段
status: member.status === 'ACTIVE' ? 'ACTIVE' : 'INACTIVE',
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 || [])
]
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)
// 根据项目状态分类
allProjects.forEach(project => {
console.log('处理项目:', project.projectName || project.name)
console.log('项目团队成员:', project.teamMembers)
console.log('项目状态:', project.status, typeof project.status)
const mappedProject = mapProjectRespToProjectCard(project)
console.log('映射后的项目:', mappedProject.name)
console.log('映射后的团队成员:', mappedProject.teamMembers)
// 确保状态比较时类型一致,将字符串转换为数字进行比较
const status = typeof project.status === 'string' ? parseInt(project.status) : project.status
if (status === 0) {
// status: 0 表示准备中
console.log('添加到准备中项目:', mappedProject.name)
preparingProjects.value.push(mappedProject)
} else if (status === 1) {
// status: 1 表示进行中
console.log('添加到进行中项目:', mappedProject.name)
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)
console.log('传入的项目数据:', project)
// 首先检查传入的项目数据是否已经包含团队成员信息
if (project.teamMembers && project.teamMembers.length > 0) {
console.log('项目数据已包含团队成员信息,直接使用')
console.log('团队成员数据:', project.teamMembers)
currentProject.value = project
projectDetailVisible.value = true
return
}
// 如果项目数据中没有团队成员信息则调用API获取
const response = await getProjectDetail(project.id)
if (response.data) {
// 使用映射函数处理后端数据
console.log('API返回的原始数据:', response.data)
currentProject.value = mapProjectRespToProjectCard(response.data)
console.log('映射后的项目数据:', currentProject.value)
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/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-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);
}
}
}
.team-members {
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%);
}
}
.member-item {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border: 1px solid #e5e6eb;
border-radius: 12px;
margin-bottom: 12px;
cursor: pointer;
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);
}
}
&:hover {
background: linear-gradient(135deg, #f8f9ff 0%, #e8f2ff 100%);
border-color: #667eea;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.15);
transform: translateY(-2px);
}
&:active {
transform: translateY(0);
}
.member-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
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);
}
}
.member-info {
flex: 1;
min-width: 0;
.member-name {
font-weight: 600;
color: #1d2129;
margin-bottom: 4px;
font-size: 16px;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.member-position {
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;
}
.member-details {
display: flex;
align-items: center;
gap: 12px;
font-size: 12px;
color: #86909c;
.member-status {
padding: 4px 8px;
border-radius: 6px;
font-weight: 500;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
&.ACTIVE {
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
color: #1890ff;
border: 1px solid #91d5ff;
}
&.INACTIVE {
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
color: #8c8c8c;
border: 1px solid #d9d9d9;
}
}
.member-date {
background: #f7f8fa;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid #e5e6eb;
}
}
}
.member-actions {
.arco-btn {
font-size: 12px;
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);
}
}
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #86909c;
background: linear-gradient(135deg, #fafbfc 0%, #f0f2f5 100%);
border-radius: 12px;
border: 2px dashed #d9d9d9;
margin: 20px 0;
.empty-icon {
font-size: 64px;
margin-bottom: 20px;
color: #bfbfbf;
opacity: 0.6;
}
.empty-text {
font-size: 20px;
font-weight: 600;
margin-bottom: 12px;
color: #595959;
}
.empty-desc {
font-size: 14px;
color: #8c8c8c;
line-height: 1.6;
}
}
}
.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>