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>
|