2025-08-08 14:26:22 +08:00
|
|
|
|
<!--
|
|
|
|
|
团队成员管理页面
|
|
|
|
|
功能特性:
|
|
|
|
|
1. 团队成员列表展示
|
|
|
|
|
2. 新增团队成员
|
|
|
|
|
3. 编辑团队成员信息
|
|
|
|
|
4. 删除团队成员
|
|
|
|
|
5. 导入导出团队成员数据
|
|
|
|
|
-->
|
|
|
|
|
<template>
|
2025-08-10 20:41:24 +08:00
|
|
|
|
<GiPageLayout class="construction-personnel-page">
|
2025-08-08 14:26:22 +08:00
|
|
|
|
<!-- 页面头部 -->
|
|
|
|
|
<div class="page-header">
|
|
|
|
|
<div class="header-left">
|
|
|
|
|
<a-button @click="$router.back()" class="back-btn">
|
|
|
|
|
<template #icon><icon-left /></template>
|
|
|
|
|
返回
|
|
|
|
|
</a-button>
|
|
|
|
|
<div class="header-title">
|
|
|
|
|
<icon-user-group class="title-icon" />
|
|
|
|
|
<h1>团队成员管理</h1>
|
|
|
|
|
<span v-if="projectId" class="project-info">项目ID: {{ projectId }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="header-actions">
|
|
|
|
|
<a-button type="primary" @click="openAddModal">
|
|
|
|
|
<template #icon><icon-plus /></template>
|
|
|
|
|
新增成员
|
|
|
|
|
</a-button>
|
|
|
|
|
<a-button @click="openImportModal">
|
|
|
|
|
<template #icon><icon-upload /></template>
|
|
|
|
|
导入
|
|
|
|
|
</a-button>
|
|
|
|
|
<a-button @click="exportData">
|
|
|
|
|
<template #icon><icon-download /></template>
|
|
|
|
|
导出
|
|
|
|
|
</a-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 简洁搜索区域 -->
|
|
|
|
|
<div class="search-section">
|
|
|
|
|
<a-card :bordered="false" class="search-card">
|
|
|
|
|
<a-form layout="inline" :model="searchForm">
|
|
|
|
|
<a-form-item label="姓名">
|
|
|
|
|
<a-input
|
|
|
|
|
v-model="searchForm.name"
|
|
|
|
|
placeholder="请输入姓名"
|
|
|
|
|
style="width: 180px"
|
|
|
|
|
allow-clear
|
|
|
|
|
@press-enter="handleSearch"
|
|
|
|
|
/>
|
|
|
|
|
</a-form-item>
|
2025-08-11 15:54:44 +08:00
|
|
|
|
<a-form-item label="项目岗位">
|
2025-08-08 14:26:22 +08:00
|
|
|
|
<a-select
|
2025-08-11 15:54:44 +08:00
|
|
|
|
v-model="searchForm.roleType"
|
|
|
|
|
placeholder="请选择项目岗位"
|
2025-08-08 14:26:22 +08:00
|
|
|
|
style="width: 150px"
|
|
|
|
|
allow-clear
|
|
|
|
|
>
|
|
|
|
|
<a-option
|
2025-08-11 15:54:44 +08:00
|
|
|
|
v-for="option in ROLE_TYPE_OPTIONS"
|
2025-08-08 14:26:22 +08:00
|
|
|
|
:key="option.value"
|
|
|
|
|
:value="option.value"
|
|
|
|
|
>
|
|
|
|
|
{{ option.label }}
|
|
|
|
|
</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
<a-form-item label="状态">
|
|
|
|
|
<a-select
|
|
|
|
|
v-model="searchForm.status"
|
|
|
|
|
placeholder="请选择状态"
|
|
|
|
|
style="width: 120px"
|
|
|
|
|
allow-clear
|
|
|
|
|
>
|
|
|
|
|
<a-option
|
|
|
|
|
v-for="option in STATUS_OPTIONS"
|
|
|
|
|
:key="option.value"
|
|
|
|
|
:value="option.value"
|
|
|
|
|
>
|
|
|
|
|
{{ option.label }}
|
|
|
|
|
</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
<a-form-item>
|
|
|
|
|
<a-space>
|
2025-08-12 17:02:50 +08:00
|
|
|
|
<a-button type="primary" @click="handleSearch">
|
2025-08-08 14:26:22 +08:00
|
|
|
|
<template #icon><icon-search /></template>
|
|
|
|
|
搜索
|
|
|
|
|
</a-button>
|
|
|
|
|
<a-button @click="handleReset">
|
|
|
|
|
<template #icon><icon-refresh /></template>
|
|
|
|
|
重置
|
|
|
|
|
</a-button>
|
|
|
|
|
</a-space>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-form>
|
|
|
|
|
</a-card>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 数据表格 -->
|
|
|
|
|
<GiTable
|
|
|
|
|
row-key="id"
|
|
|
|
|
:data="dataList"
|
|
|
|
|
:columns="tableColumns"
|
|
|
|
|
:loading="loading"
|
2025-08-11 15:54:44 +08:00
|
|
|
|
:scroll="{ x: '100%', y: 'calc(100vh - 400px)', minWidth: 1200 }"
|
2025-08-08 14:26:22 +08:00
|
|
|
|
:pagination="pagination"
|
|
|
|
|
:disabled-tools="['size']"
|
|
|
|
|
@page-change="onPageChange"
|
|
|
|
|
@page-size-change="onPageSizeChange"
|
|
|
|
|
@refresh="loadData"
|
|
|
|
|
>
|
2025-08-11 15:54:44 +08:00
|
|
|
|
<!-- 项目岗位列 -->
|
|
|
|
|
<template #roleType="{ record }">
|
|
|
|
|
<span class="role-type-text">
|
|
|
|
|
{{ getRoleTypeText(record.roleType) }}
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
|
2025-08-08 14:26:22 +08:00
|
|
|
|
<!-- 状态列 -->
|
|
|
|
|
<template #status="{ record }">
|
2025-08-12 17:02:50 +08:00
|
|
|
|
<a-tag :color="getStatusColor(record.status || 'INACTIVE')">
|
2025-08-11 15:54:44 +08:00
|
|
|
|
{{ getStatusText(record.status || 'INACTIVE') }}
|
2025-08-12 17:02:50 +08:00
|
|
|
|
</a-tag>
|
2025-08-08 14:26:22 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 备注列 -->
|
|
|
|
|
<template #remark="{ record }">
|
|
|
|
|
<div class="remark-content" v-if="record.remark">
|
|
|
|
|
{{ record.remark }}
|
|
|
|
|
</div>
|
|
|
|
|
<span v-else class="no-remark">暂无备注</span>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- 操作列 -->
|
|
|
|
|
<template #action="{ record }">
|
|
|
|
|
<a-space>
|
2025-08-11 15:54:44 +08:00
|
|
|
|
<a-link
|
|
|
|
|
title="编辑"
|
|
|
|
|
@click="openEditModal(record)"
|
|
|
|
|
class="action-link edit-link"
|
|
|
|
|
>
|
|
|
|
|
<template #icon><icon-edit /></template>
|
|
|
|
|
编辑
|
|
|
|
|
</a-link>
|
|
|
|
|
<a-link
|
|
|
|
|
title="状态调整"
|
|
|
|
|
@click="openStatusModal(record)"
|
|
|
|
|
class="action-link status-link"
|
|
|
|
|
>
|
|
|
|
|
<template #icon><icon-settings /></template>
|
|
|
|
|
状态
|
|
|
|
|
</a-link>
|
|
|
|
|
<a-link
|
|
|
|
|
status="danger"
|
|
|
|
|
title="删除"
|
|
|
|
|
@click="confirmDelete(record)"
|
|
|
|
|
class="action-link delete-link"
|
|
|
|
|
>
|
|
|
|
|
<template #icon><icon-delete /></template>
|
|
|
|
|
删除
|
|
|
|
|
</a-link>
|
2025-08-08 14:26:22 +08:00
|
|
|
|
</a-space>
|
|
|
|
|
</template>
|
|
|
|
|
</GiTable>
|
|
|
|
|
|
|
|
|
|
<!-- 新增/编辑弹窗 -->
|
|
|
|
|
<a-modal
|
|
|
|
|
v-model:visible="memberModalVisible"
|
|
|
|
|
:title="isEdit ? '编辑团队成员' : '新增团队成员'"
|
|
|
|
|
width="600px"
|
|
|
|
|
@ok="saveMember"
|
|
|
|
|
@cancel="cancelMember"
|
|
|
|
|
>
|
|
|
|
|
<div class="member-form">
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label>姓名 <span class="required">*</span></label>
|
|
|
|
|
<a-input v-model="memberForm.name" placeholder="请输入姓名" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label>联系电话 <span class="required">*</span></label>
|
|
|
|
|
<a-input v-model="memberForm.phone" placeholder="请输入联系电话" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<div class="form-item">
|
2025-08-11 15:54:44 +08:00
|
|
|
|
<label>项目岗位 <span class="required">*</span></label>
|
|
|
|
|
<a-select v-model="memberForm.roleType" placeholder="请选择项目岗位">
|
2025-08-08 14:26:22 +08:00
|
|
|
|
<a-option
|
2025-08-11 15:54:44 +08:00
|
|
|
|
v-for="option in ROLE_TYPE_OPTIONS"
|
2025-08-08 14:26:22 +08:00
|
|
|
|
:key="option.value"
|
|
|
|
|
:value="option.value"
|
|
|
|
|
>
|
|
|
|
|
{{ option.label }}
|
|
|
|
|
</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label>状态</label>
|
|
|
|
|
<a-select v-model="memberForm.status" placeholder="请选择状态">
|
|
|
|
|
<a-option
|
|
|
|
|
v-for="option in STATUS_OPTIONS"
|
|
|
|
|
:key="option.value"
|
|
|
|
|
:value="option.value"
|
|
|
|
|
>
|
|
|
|
|
{{ option.label }}
|
|
|
|
|
</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label>邮箱</label>
|
|
|
|
|
<a-input v-model="memberForm.email" placeholder="请输入邮箱" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label>入职日期</label>
|
|
|
|
|
<a-date-picker v-model="memberForm.joinDate" placeholder="请选择入职日期" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label>备注信息</label>
|
|
|
|
|
<a-textarea
|
|
|
|
|
v-model="memberForm.remark"
|
|
|
|
|
placeholder="请输入备注信息,格式:主要负责:项目经理,次要负责:安全员等"
|
|
|
|
|
:rows="3"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
|
|
|
<!-- 状态调整弹窗 -->
|
|
|
|
|
<a-modal
|
|
|
|
|
v-model:visible="statusModalVisible"
|
|
|
|
|
title="调整状态"
|
|
|
|
|
width="400px"
|
|
|
|
|
@ok="saveStatus"
|
|
|
|
|
@cancel="cancelStatus"
|
|
|
|
|
>
|
|
|
|
|
<div class="status-form">
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label>成员姓名:</label>
|
|
|
|
|
<span>{{ statusForm.name }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label>当前状态:</label>
|
|
|
|
|
<span>{{ getStatusText(statusForm.currentStatus) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label>新状态:</label>
|
|
|
|
|
<a-select v-model="statusForm.newStatus" placeholder="请选择新状态">
|
|
|
|
|
<a-option
|
|
|
|
|
v-for="option in STATUS_OPTIONS"
|
|
|
|
|
:key="option.value"
|
|
|
|
|
:value="option.value"
|
|
|
|
|
>
|
|
|
|
|
{{ option.label }}
|
|
|
|
|
</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
|
|
|
<!-- 导入弹窗 -->
|
|
|
|
|
<a-modal
|
|
|
|
|
v-model:visible="importModalVisible"
|
|
|
|
|
title="导入团队成员"
|
|
|
|
|
width="500px"
|
|
|
|
|
@ok="confirmImport"
|
|
|
|
|
@cancel="cancelImport"
|
|
|
|
|
>
|
|
|
|
|
<div class="import-form">
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label>选择文件:</label>
|
|
|
|
|
<a-upload
|
|
|
|
|
v-model:file-list="fileList"
|
|
|
|
|
:custom-request="customUpload"
|
|
|
|
|
:show-file-list="true"
|
|
|
|
|
accept=".xlsx,.xls,.csv"
|
|
|
|
|
:limit="1"
|
|
|
|
|
>
|
|
|
|
|
<a-button>
|
|
|
|
|
<template #icon><icon-upload /></template>
|
|
|
|
|
选择文件
|
|
|
|
|
</a-button>
|
|
|
|
|
</a-upload>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label>下载模板:</label>
|
|
|
|
|
<a-button type="text" @click="downloadTemplate">
|
|
|
|
|
<template #icon><icon-download /></template>
|
|
|
|
|
下载导入模板
|
|
|
|
|
</a-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</a-modal>
|
|
|
|
|
</GiPageLayout>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, reactive, onMounted } from 'vue'
|
|
|
|
|
import { Message, Modal } from '@arco-design/web-vue'
|
|
|
|
|
import { useRoute } from 'vue-router'
|
|
|
|
|
import type { TeamMemberResp, TeamMemberQuery, TeamMemberExportQuery, CreateTeamMemberForm, UpdateTeamMemberForm, BackendTeamMemberResp } from '@/apis/project/type'
|
|
|
|
|
import {
|
|
|
|
|
getProjectTeamMembers,
|
|
|
|
|
createTeamMember,
|
|
|
|
|
updateTeamMember,
|
|
|
|
|
deleteTeamMembers,
|
|
|
|
|
importTeamMembers,
|
|
|
|
|
exportTeamMembers,
|
|
|
|
|
downloadImportTemplate
|
|
|
|
|
} from '@/apis/project/personnel-dispatch'
|
|
|
|
|
|
|
|
|
|
// 获取路由参数
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
const projectId = route.query.projectId as string
|
|
|
|
|
|
2025-08-11 15:54:44 +08:00
|
|
|
|
// 选项数据常量 - 项目岗位选项
|
|
|
|
|
|
|
|
|
|
const ROLE_TYPE_OPTIONS = [
|
|
|
|
|
{ label: '项目经理', value: 'PROJECT_MANAGER' },
|
|
|
|
|
{ label: '项目负责人', value: 'TEAM_LEADER' },
|
|
|
|
|
{ label: '技术负责人', value: 'TECH_LEADER' },
|
|
|
|
|
{ label: '安全员', value: 'SAFETY_OFFICER' },
|
|
|
|
|
{ label: '质量员', value: 'QUALITY_OFFICER' },
|
|
|
|
|
{ label: '施工员', value: 'CONSTRUCTOR' },
|
|
|
|
|
{ label: '材料员', value: 'MATERIAL_MANAGER' },
|
|
|
|
|
{ label: '资料员', value: 'DOCUMENT_MANAGER' },
|
|
|
|
|
{ label: '实习生', value: 'INTERN' },
|
|
|
|
|
{ label: '技术工人', value: 'TECH_WORKER' },
|
|
|
|
|
{ label: '普通工人', value: 'WORKER' }
|
2025-08-08 14:26:22 +08:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const STATUS_OPTIONS = [
|
2025-08-11 15:54:44 +08:00
|
|
|
|
{ label: '可用', value: 'ACTIVE' },
|
2025-08-12 17:02:50 +08:00
|
|
|
|
{ label: '忙碌', value: 'SUSPENDEN' },
|
2025-08-11 15:54:44 +08:00
|
|
|
|
{ label: '离线', value: 'INACTIVE' }
|
2025-08-08 14:26:22 +08:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// 响应式数据
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
const dataList = ref<TeamMemberResp[]>([])
|
|
|
|
|
const pagination = reactive({
|
|
|
|
|
current: 1,
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
total: 0,
|
|
|
|
|
showTotal: true,
|
|
|
|
|
showJumper: true,
|
|
|
|
|
showPageSize: true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 搜索表单
|
|
|
|
|
const searchForm = reactive<{
|
|
|
|
|
name: string
|
2025-08-11 15:54:44 +08:00
|
|
|
|
roleType: string
|
2025-08-08 14:26:22 +08:00
|
|
|
|
status: string
|
|
|
|
|
}>({
|
|
|
|
|
name: '',
|
2025-08-11 15:54:44 +08:00
|
|
|
|
roleType: '',
|
2025-08-08 14:26:22 +08:00
|
|
|
|
status: ''
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 表格列配置
|
|
|
|
|
const tableColumns = [
|
|
|
|
|
{ title: '姓名', dataIndex: 'name', width: 100, fixed: 'left' },
|
|
|
|
|
{ title: '联系电话', dataIndex: 'phone', width: 120 },
|
|
|
|
|
{ title: '邮箱', dataIndex: 'email', width: 150 },
|
2025-08-11 15:54:44 +08:00
|
|
|
|
{ title: '项目岗位', dataIndex: 'roleType', width: 120, slotName: 'roleType' },
|
2025-08-08 14:26:22 +08:00
|
|
|
|
{ title: '状态', dataIndex: 'status', width: 100, slotName: 'status' },
|
|
|
|
|
{ title: '入职日期', dataIndex: 'joinDate', width: 120 },
|
|
|
|
|
{ title: '备注', dataIndex: 'remark', width: 200, slotName: 'remark' },
|
|
|
|
|
{ title: '操作', dataIndex: 'action', width: 180, fixed: 'right', slotName: 'action' }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// 弹窗状态
|
|
|
|
|
const memberModalVisible = ref(false)
|
|
|
|
|
const statusModalVisible = ref(false)
|
|
|
|
|
const importModalVisible = ref(false)
|
|
|
|
|
const isEdit = ref(false)
|
|
|
|
|
|
|
|
|
|
// 表单数据
|
|
|
|
|
const memberForm = reactive<CreateTeamMemberForm & { id?: string | number }>({
|
|
|
|
|
projectId: projectId,
|
|
|
|
|
name: '',
|
|
|
|
|
phone: '',
|
2025-08-11 15:54:44 +08:00
|
|
|
|
roleType: '', // 项目岗位
|
|
|
|
|
status: 'ACTIVE',
|
2025-08-08 14:26:22 +08:00
|
|
|
|
email: '',
|
|
|
|
|
joinDate: '',
|
|
|
|
|
remark: ''
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const statusForm = reactive<{
|
|
|
|
|
id: string | number
|
|
|
|
|
name: string
|
|
|
|
|
currentStatus: string
|
2025-08-12 17:02:50 +08:00
|
|
|
|
newStatus: 'ACTIVE' | 'SUSPENDEN' | 'INACTIVE'
|
2025-08-08 14:26:22 +08:00
|
|
|
|
}>({
|
|
|
|
|
id: '',
|
|
|
|
|
name: '',
|
|
|
|
|
currentStatus: '',
|
2025-08-11 15:54:44 +08:00
|
|
|
|
newStatus: 'ACTIVE'
|
2025-08-08 14:26:22 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const fileList = ref<any[]>([])
|
|
|
|
|
|
|
|
|
|
// 方法
|
|
|
|
|
const loadData = async () => {
|
|
|
|
|
if (!projectId) {
|
|
|
|
|
console.warn('未获取到项目ID,无法加载团队成员数据')
|
|
|
|
|
Message.warning('未获取到项目信息,请从项目详情页面进入')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
try {
|
|
|
|
|
console.log('正在加载项目团队成员数据,项目ID:', projectId)
|
|
|
|
|
|
2025-08-12 17:02:50 +08:00
|
|
|
|
// 构建查询参数
|
2025-08-08 14:26:22 +08:00
|
|
|
|
const queryParams: TeamMemberQuery = {
|
|
|
|
|
projectId: projectId,
|
|
|
|
|
page: pagination.current,
|
|
|
|
|
pageSize: pagination.pageSize,
|
|
|
|
|
name: searchForm.name || undefined,
|
2025-08-12 17:02:50 +08:00
|
|
|
|
position: searchForm.roleType || undefined,
|
2025-08-08 14:26:22 +08:00
|
|
|
|
status: searchForm.status || undefined
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-11 15:54:44 +08:00
|
|
|
|
const response = await getProjectTeamMembers(queryParams)
|
2025-08-08 14:26:22 +08:00
|
|
|
|
|
2025-08-12 17:02:50 +08:00
|
|
|
|
console.log('API响应数据:', response.data)
|
|
|
|
|
|
|
|
|
|
// 确保response.data是数组
|
|
|
|
|
const rawData = Array.isArray(response.data) ? response.data : [response.data]
|
2025-08-08 14:26:22 +08:00
|
|
|
|
|
2025-08-12 17:02:50 +08:00
|
|
|
|
console.log('处理后的原始数据:', rawData)
|
2025-08-08 14:26:22 +08:00
|
|
|
|
|
2025-08-12 17:02:50 +08:00
|
|
|
|
// 处理后端返回的数据,将后端字段映射到前端期望的字段
|
|
|
|
|
const mappedData = rawData.map((item: BackendTeamMemberResp) => {
|
|
|
|
|
const mappedItem: TeamMemberResp = {
|
2025-08-08 14:26:22 +08:00
|
|
|
|
id: item.memberId,
|
|
|
|
|
name: item.name || '',
|
|
|
|
|
phone: item.phone || '',
|
|
|
|
|
email: item.email || '',
|
2025-08-12 17:02:50 +08:00
|
|
|
|
roleType: item.roleType || '', // 映射项目岗位
|
|
|
|
|
status: (item.status === 'ACTIVE' ? 'ACTIVE' : item.status === 'SUSPENDEN' ? 'SUSPENDEN' : 'INACTIVE') as 'ACTIVE' | 'SUSPENDEN' | 'INACTIVE',
|
2025-08-08 14:26:22 +08:00
|
|
|
|
joinDate: item.joinDate || '',
|
|
|
|
|
remark: item.remark || '',
|
|
|
|
|
avatar: item.userAvatar || ''
|
|
|
|
|
}
|
2025-08-12 17:02:50 +08:00
|
|
|
|
console.log('映射后的数据项:', mappedItem)
|
|
|
|
|
return mappedItem
|
|
|
|
|
})
|
2025-08-08 14:26:22 +08:00
|
|
|
|
|
|
|
|
|
dataList.value = mappedData
|
2025-08-12 17:02:50 +08:00
|
|
|
|
pagination.total = mappedData.length
|
2025-08-08 14:26:22 +08:00
|
|
|
|
|
|
|
|
|
console.log('团队成员数据加载完成,显示数据:', dataList.value.length, '条,总计:', pagination.total, '条')
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('团队成员数据加载失败:', error)
|
|
|
|
|
Message.error('团队成员数据加载失败')
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSearch = async () => {
|
|
|
|
|
pagination.current = 1
|
|
|
|
|
await loadData()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleReset = () => {
|
|
|
|
|
console.log('重置搜索表单')
|
|
|
|
|
Object.assign(searchForm, {
|
|
|
|
|
name: '',
|
2025-08-11 15:54:44 +08:00
|
|
|
|
roleType: '',
|
2025-08-08 14:26:22 +08:00
|
|
|
|
status: ''
|
|
|
|
|
})
|
|
|
|
|
pagination.current = 1
|
|
|
|
|
loadData()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onPageChange = (page: number) => {
|
|
|
|
|
pagination.current = page
|
|
|
|
|
loadData()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onPageSizeChange = (pageSize: number) => {
|
|
|
|
|
pagination.pageSize = pageSize
|
|
|
|
|
pagination.current = 1
|
|
|
|
|
loadData()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const openAddModal = () => {
|
|
|
|
|
isEdit.value = false
|
|
|
|
|
resetMemberForm()
|
|
|
|
|
memberModalVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const openEditModal = (record: TeamMemberResp) => {
|
|
|
|
|
isEdit.value = true
|
|
|
|
|
Object.assign(memberForm, {
|
|
|
|
|
id: record.id,
|
|
|
|
|
projectId: projectId,
|
|
|
|
|
name: record.name,
|
|
|
|
|
phone: record.phone || '',
|
2025-08-11 15:54:44 +08:00
|
|
|
|
roleType: record.roleType || '',
|
|
|
|
|
status: record.status || 'ACTIVE',
|
2025-08-08 14:26:22 +08:00
|
|
|
|
email: record.email || '',
|
|
|
|
|
joinDate: record.joinDate || '',
|
|
|
|
|
remark: record.remark || ''
|
|
|
|
|
})
|
|
|
|
|
memberModalVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const saveMember = async () => {
|
2025-08-11 15:54:44 +08:00
|
|
|
|
if (!memberForm.name || !memberForm.phone || !memberForm.roleType) {
|
2025-08-08 14:26:22 +08:00
|
|
|
|
Message.error('请填写必填项')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (isEdit.value && memberForm.id) {
|
|
|
|
|
// 更新团队成员
|
|
|
|
|
const updateData: UpdateTeamMemberForm = {
|
|
|
|
|
name: memberForm.name,
|
|
|
|
|
phone: memberForm.phone,
|
2025-08-11 15:54:44 +08:00
|
|
|
|
roleType: memberForm.roleType,
|
2025-08-08 14:26:22 +08:00
|
|
|
|
status: memberForm.status,
|
2025-08-11 15:54:44 +08:00
|
|
|
|
email: memberForm.email,
|
2025-08-08 14:26:22 +08:00
|
|
|
|
joinDate: memberForm.joinDate,
|
|
|
|
|
remark: memberForm.remark
|
|
|
|
|
}
|
|
|
|
|
await updateTeamMember(memberForm.id, updateData)
|
|
|
|
|
Message.success('更新成功')
|
|
|
|
|
} else {
|
|
|
|
|
// 创建团队成员
|
|
|
|
|
const createData: CreateTeamMemberForm = {
|
|
|
|
|
projectId: projectId,
|
|
|
|
|
name: memberForm.name,
|
|
|
|
|
phone: memberForm.phone,
|
2025-08-11 15:54:44 +08:00
|
|
|
|
roleType: memberForm.roleType,
|
2025-08-08 14:26:22 +08:00
|
|
|
|
status: memberForm.status,
|
2025-08-11 15:54:44 +08:00
|
|
|
|
email: memberForm.email,
|
2025-08-08 14:26:22 +08:00
|
|
|
|
joinDate: memberForm.joinDate,
|
|
|
|
|
remark: memberForm.remark
|
|
|
|
|
}
|
|
|
|
|
await createTeamMember(createData)
|
|
|
|
|
Message.success('添加成功')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
memberModalVisible.value = false
|
|
|
|
|
loadData()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('保存团队成员失败:', error)
|
|
|
|
|
Message.error(isEdit.value ? '更新失败' : '添加失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cancelMember = () => {
|
|
|
|
|
memberModalVisible.value = false
|
|
|
|
|
resetMemberForm()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resetMemberForm = () => {
|
|
|
|
|
Object.assign(memberForm, {
|
|
|
|
|
id: undefined,
|
|
|
|
|
projectId: projectId,
|
|
|
|
|
name: '',
|
|
|
|
|
phone: '',
|
2025-08-11 15:54:44 +08:00
|
|
|
|
roleType: '',
|
|
|
|
|
status: 'ACTIVE',
|
2025-08-08 14:26:22 +08:00
|
|
|
|
email: '',
|
|
|
|
|
joinDate: '',
|
|
|
|
|
remark: ''
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const openStatusModal = (record: TeamMemberResp) => {
|
|
|
|
|
Object.assign(statusForm, {
|
|
|
|
|
id: record.id,
|
|
|
|
|
name: record.name,
|
2025-08-11 15:54:44 +08:00
|
|
|
|
currentStatus: record.status || 'ACTIVE',
|
|
|
|
|
newStatus: record.status || 'ACTIVE'
|
2025-08-08 14:26:22 +08:00
|
|
|
|
})
|
|
|
|
|
statusModalVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const saveStatus = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await updateTeamMember(statusForm.id, {
|
2025-08-12 17:02:50 +08:00
|
|
|
|
status: statusForm.newStatus as 'ACTIVE' | 'SUSPENDEN' | 'INACTIVE'
|
2025-08-08 14:26:22 +08:00
|
|
|
|
})
|
|
|
|
|
Message.success('状态更新成功')
|
|
|
|
|
statusModalVisible.value = false
|
|
|
|
|
loadData()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('状态更新失败:', error)
|
|
|
|
|
Message.error('状态更新失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cancelStatus = () => {
|
|
|
|
|
statusModalVisible.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const confirmDelete = (record: TeamMemberResp) => {
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
title: '确认删除',
|
|
|
|
|
content: `确定要删除团队成员"${record.name}"吗?`,
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
try {
|
|
|
|
|
await deleteTeamMembers(record.id)
|
|
|
|
|
Message.success('删除成功')
|
|
|
|
|
loadData()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('删除失败:', error)
|
|
|
|
|
Message.error('删除失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const openImportModal = () => {
|
|
|
|
|
importModalVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const customUpload = (options: any) => {
|
|
|
|
|
const { file } = options
|
|
|
|
|
fileList.value = [file]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const confirmImport = async () => {
|
|
|
|
|
if (fileList.value.length === 0) {
|
|
|
|
|
Message.error('请选择要导入的文件')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const file = fileList.value[0].originFileObj
|
|
|
|
|
await importTeamMembers(projectId, file)
|
|
|
|
|
Message.success('导入成功')
|
|
|
|
|
importModalVisible.value = false
|
|
|
|
|
fileList.value = []
|
|
|
|
|
loadData()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('导入失败:', error)
|
|
|
|
|
Message.error('导入失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cancelImport = () => {
|
|
|
|
|
importModalVisible.value = false
|
|
|
|
|
fileList.value = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const downloadTemplate = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await downloadImportTemplate()
|
|
|
|
|
const blob = new Blob([response.data], {
|
|
|
|
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
|
|
|
})
|
|
|
|
|
const url = window.URL.createObjectURL(blob)
|
|
|
|
|
const link = document.createElement('a')
|
|
|
|
|
link.href = url
|
|
|
|
|
link.download = '团队成员导入模板.xlsx'
|
|
|
|
|
link.click()
|
|
|
|
|
window.URL.revokeObjectURL(url)
|
|
|
|
|
Message.success('模板下载成功')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('模板下载失败:', error)
|
|
|
|
|
Message.error('模板下载失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const exportData = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const queryParams: TeamMemberExportQuery = {
|
|
|
|
|
projectId: projectId,
|
|
|
|
|
name: searchForm.name || undefined,
|
2025-08-11 15:54:44 +08:00
|
|
|
|
position: searchForm.roleType || undefined,
|
2025-08-08 14:26:22 +08:00
|
|
|
|
status: searchForm.status || undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await exportTeamMembers(queryParams)
|
|
|
|
|
const blob = new Blob([response.data], {
|
|
|
|
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
|
|
|
})
|
|
|
|
|
const url = window.URL.createObjectURL(blob)
|
|
|
|
|
const link = document.createElement('a')
|
|
|
|
|
link.href = url
|
|
|
|
|
link.download = `团队成员数据_${new Date().toISOString().split('T')[0]}.xlsx`
|
|
|
|
|
link.click()
|
|
|
|
|
window.URL.revokeObjectURL(url)
|
|
|
|
|
Message.success('导出成功')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('导出失败:', error)
|
|
|
|
|
Message.error('导出失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 工具方法
|
2025-08-11 15:54:44 +08:00
|
|
|
|
const getRoleTypeText = (roleType: string) => {
|
|
|
|
|
const option = ROLE_TYPE_OPTIONS.find(opt => opt.value === roleType)
|
|
|
|
|
return option ? option.label : roleType
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 14:26:22 +08:00
|
|
|
|
const getStatusColor = (status: string) => {
|
|
|
|
|
const colorMap: Record<string, string> = {
|
2025-08-11 15:54:44 +08:00
|
|
|
|
ACTIVE: 'success',
|
2025-08-12 17:02:50 +08:00
|
|
|
|
SUSPENDEN: 'warning',
|
2025-08-11 15:54:44 +08:00
|
|
|
|
INACTIVE: 'danger'
|
2025-08-08 14:26:22 +08:00
|
|
|
|
}
|
2025-08-11 15:54:44 +08:00
|
|
|
|
return colorMap[status] || 'danger'
|
2025-08-08 14:26:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getStatusText = (status: string) => {
|
|
|
|
|
const textMap: Record<string, string> = {
|
2025-08-11 15:54:44 +08:00
|
|
|
|
ACTIVE: '可用',
|
2025-08-12 17:02:50 +08:00
|
|
|
|
SUSPENDEN: '忙碌',
|
2025-08-11 15:54:44 +08:00
|
|
|
|
INACTIVE: '离线'
|
2025-08-08 14:26:22 +08:00
|
|
|
|
}
|
|
|
|
|
return textMap[status] || '未知'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生命周期
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
console.log('团队成员管理页面加载,项目ID:', projectId)
|
|
|
|
|
if (projectId) {
|
|
|
|
|
loadData()
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('未获取到项目ID,无法加载团队成员数据')
|
|
|
|
|
Message.warning('未获取到项目信息,请从项目详情页面进入')
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
// 确保页面可以正常滚动
|
|
|
|
|
:deep(.gi-page-layout) {
|
|
|
|
|
height: auto !important;
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
overflow-y: visible;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.gi-page-layout__body) {
|
|
|
|
|
height: auto !important;
|
|
|
|
|
overflow-y: visible;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 确保全局滚动正常
|
|
|
|
|
:deep(body), :deep(html) {
|
|
|
|
|
overflow-y: auto !important;
|
|
|
|
|
height: auto !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 确保页面容器可以滚动
|
|
|
|
|
:deep(.app-main), :deep(.main-content), :deep(.layout-content) {
|
|
|
|
|
overflow-y: auto !important;
|
|
|
|
|
height: auto !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 确保表格容器可以滚动
|
|
|
|
|
:deep(.arco-table-container) {
|
|
|
|
|
overflow-x: auto;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 确保表格内容可以正常滚动
|
|
|
|
|
:deep(.arco-table-body) {
|
|
|
|
|
overflow-y: auto !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.arco-table-tbody) {
|
|
|
|
|
overflow-y: auto !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 确保分页器不会影响滚动
|
|
|
|
|
:deep(.arco-pagination) {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 操作链接样式
|
|
|
|
|
.action-link {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.edit-link {
|
|
|
|
|
color: #1677ff;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background: rgba(22, 119, 255, 0.1);
|
|
|
|
|
color: #0958d9;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.status-link {
|
|
|
|
|
color: #52c41a;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background: rgba(82, 196, 26, 0.1);
|
|
|
|
|
color: #389e0d;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.delete-link {
|
|
|
|
|
color: #ff4d4f;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background: rgba(255, 77, 79, 0.1);
|
|
|
|
|
color: #d9363e;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-08 14:26:22 +08:00
|
|
|
|
}
|
|
|
|
|
.page-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 24px;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
padding: 24px;
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
&::before {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="75" cy="75" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="50" cy="10" r="0.5" fill="rgba(255,255,255,0.1)"/><circle cx="10" cy="60" r="0.5" fill="rgba(255,255,255,0.1)"/><circle cx="90" cy="40" r="0.5" fill="rgba(255,255,255,0.1)"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
|
|
|
|
|
opacity: 0.3;
|
|
|
|
|
}
|
2025-08-08 14:26:22 +08:00
|
|
|
|
|
|
|
|
|
.header-left {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
gap: 20px;
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 1;
|
2025-08-08 14:26:22 +08:00
|
|
|
|
|
|
|
|
|
.back-btn {
|
|
|
|
|
border: none;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
color: white;
|
|
|
|
|
backdrop-filter: blur(10px);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
transition: all 0.3s ease;
|
2025-08-08 14:26:22 +08:00
|
|
|
|
|
|
|
|
|
&:hover {
|
2025-08-11 15:54:44 +08:00
|
|
|
|
background: rgba(255, 255, 255, 0.3);
|
|
|
|
|
transform: translateX(-2px);
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
2025-08-08 14:26:22 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-title {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
gap: 12px;
|
2025-08-08 14:26:22 +08:00
|
|
|
|
|
|
|
|
|
.title-icon {
|
2025-08-11 15:54:44 +08:00
|
|
|
|
font-size: 28px;
|
|
|
|
|
color: white;
|
|
|
|
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
2025-08-08 14:26:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h1 {
|
|
|
|
|
margin: 0;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
font-size: 24px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: white;
|
|
|
|
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
2025-08-08 14:26:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.project-info {
|
|
|
|
|
font-size: 14px;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
color: rgba(255, 255, 255, 0.9);
|
|
|
|
|
background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
backdrop-filter: blur(10px);
|
2025-08-08 14:26:22 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 12px;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
|
|
|
|
.arco-btn {
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-08 14:26:22 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-section {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
.search-card {
|
2025-08-11 15:54:44 +08:00
|
|
|
|
background: linear-gradient(135deg, #f8f9ff 0%, #f0f2ff 100%);
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.1);
|
|
|
|
|
border: 1px solid rgba(102, 126, 234, 0.1);
|
2025-08-08 14:26:22 +08:00
|
|
|
|
|
|
|
|
|
:deep(.arco-card-body) {
|
2025-08-11 15:54:44 +08:00
|
|
|
|
padding: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 14:26:22 +08:00
|
|
|
|
:deep(.arco-form-item) {
|
|
|
|
|
margin-bottom: 0;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
margin-right: 20px;
|
2025-08-08 14:26:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.arco-form-item-label) {
|
2025-08-11 15:54:44 +08:00
|
|
|
|
font-weight: 600;
|
2025-08-08 14:26:22 +08:00
|
|
|
|
color: #4e5969;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
font-size: 14px;
|
2025-08-08 14:26:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.arco-input),
|
|
|
|
|
:deep(.arco-select) {
|
|
|
|
|
width: 100%;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid rgba(102, 126, 234, 0.2);
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
border-color: rgba(102, 126, 234, 0.4);
|
|
|
|
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&:focus {
|
|
|
|
|
border-color: #667eea;
|
|
|
|
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.arco-btn) {
|
2025-08-12 17:02:50 +08:00
|
|
|
|
border-radius: 8px;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
font-weight: 600;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
|
|
}
|
2025-08-08 14:26:22 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-12 17:02:50 +08:00
|
|
|
|
// 表格区域样式
|
|
|
|
|
:deep(.gi-table) {
|
|
|
|
|
background: white;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
overflow: hidden;
|
2025-08-08 14:26:22 +08:00
|
|
|
|
|
2025-08-12 17:02:50 +08:00
|
|
|
|
.arco-table-container {
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.arco-table {
|
|
|
|
|
overflow: visible;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.arco-table-body {
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 表格头部样式
|
|
|
|
|
.arco-table-thead {
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
2025-08-11 15:54:44 +08:00
|
|
|
|
|
2025-08-12 17:02:50 +08:00
|
|
|
|
.arco-table-th {
|
|
|
|
|
background: transparent !important;
|
|
|
|
|
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
|
2025-08-11 15:54:44 +08:00
|
|
|
|
|
2025-08-12 17:02:50 +08:00
|
|
|
|
.arco-table-th-item-title {
|
|
|
|
|
color: white !important;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
font-weight: 600;
|
2025-08-12 17:02:50 +08:00
|
|
|
|
font-size: 14px;
|
|
|
|
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
2025-08-11 15:54:44 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-08 14:26:22 +08:00
|
|
|
|
}
|
2025-08-10 20:41:24 +08:00
|
|
|
|
|
2025-08-12 17:02:50 +08:00
|
|
|
|
// 表格行样式
|
|
|
|
|
.arco-table-tbody {
|
|
|
|
|
.arco-table-tr {
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background: linear-gradient(135deg, #f8f9ff 0%, #f0f2ff 100%);
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.arco-table-td {
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
padding: 16px 12px;
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
}
|
2025-08-11 15:54:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-08-12 17:02:50 +08:00
|
|
|
|
// 斑马纹效果
|
|
|
|
|
.arco-table-tr:nth-child(even) {
|
|
|
|
|
background: #fafbfc;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
|
2025-08-12 17:02:50 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
background: linear-gradient(135deg, #f8f9ff 0%, #f0f2ff 100%);
|
2025-08-11 15:54:44 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-10 20:41:24 +08:00
|
|
|
|
}
|
2025-08-12 17:02:50 +08:00
|
|
|
|
|
|
|
|
|
// 固定列样式
|
|
|
|
|
.arco-table-fixed-left,
|
|
|
|
|
.arco-table-fixed-right {
|
|
|
|
|
.arco-table-td {
|
|
|
|
|
background: white;
|
|
|
|
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-08 14:26:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-08-12 17:02:50 +08:00
|
|
|
|
|
|
|
|
|
|
2025-08-08 14:26:22 +08:00
|
|
|
|
.remark-content {
|
|
|
|
|
max-width: 180px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
color: #4e5969;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
background: rgba(102, 126, 234, 0.05);
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border-left: 3px solid #667eea;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background: rgba(102, 126, 234, 0.1);
|
|
|
|
|
transform: translateX(2px);
|
|
|
|
|
white-space: normal;
|
|
|
|
|
max-width: 250px;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
position: relative;
|
|
|
|
|
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.2);
|
|
|
|
|
}
|
2025-08-08 14:26:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.no-remark {
|
|
|
|
|
color: #c9cdd4;
|
|
|
|
|
font-style: italic;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
background: rgba(201, 205, 212, 0.1);
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px dashed #c9cdd4;
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.role-type-text {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
color: white;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
min-width: 80px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
|
|
|
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
2025-08-12 17:02:50 +08:00
|
|
|
|
transform: translateY(-2px);
|
2025-08-11 15:54:44 +08:00
|
|
|
|
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 14:26:22 +08:00
|
|
|
|
.member-form {
|
|
|
|
|
.form-row {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-item {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
label {
|
|
|
|
|
display: block;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #4e5969;
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
|
|
|
|
.required {
|
|
|
|
|
color: #f53f3f;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.arco-input,
|
|
|
|
|
.arco-select,
|
|
|
|
|
.arco-textarea,
|
|
|
|
|
.arco-date-picker {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-form {
|
|
|
|
|
.form-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
label {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #4e5969;
|
|
|
|
|
min-width: 80px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.arco-select {
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.import-form {
|
|
|
|
|
.form-item {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
label {
|
|
|
|
|
display: block;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #4e5969;
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-10 20:41:24 +08:00
|
|
|
|
// 页面滚动修复
|
|
|
|
|
.construction-personnel-page {
|
2025-08-11 15:54:44 +08:00
|
|
|
|
height: auto;
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
overflow: visible;
|
2025-08-10 20:41:24 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
|
|
:deep(.gi-page-layout) {
|
2025-08-11 15:54:44 +08:00
|
|
|
|
height: auto;
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
overflow: visible;
|
2025-08-10 20:41:24 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.gi-page-layout-content) {
|
|
|
|
|
flex: 1;
|
2025-08-11 15:54:44 +08:00
|
|
|
|
overflow: visible;
|
2025-08-10 20:41:24 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 14:26:22 +08:00
|
|
|
|
// 响应式设计
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.page-header {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|
|
|
|
|
.header-actions {
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-section {
|
|
|
|
|
.search-card {
|
|
|
|
|
:deep(.arco-form) {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.arco-form-item) {
|
|
|
|
|
margin-right: 0;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.member-form {
|
|
|
|
|
.form-row {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-11 15:54:44 +08:00
|
|
|
|
|
|
|
|
|
// 全局动画效果
|
|
|
|
|
@keyframes fadeInUp {
|
|
|
|
|
from {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateY(20px);
|
|
|
|
|
}
|
|
|
|
|
to {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes slideInLeft {
|
|
|
|
|
from {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateX(-20px);
|
|
|
|
|
}
|
|
|
|
|
to {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 页面加载动画
|
|
|
|
|
.construction-personnel-page {
|
|
|
|
|
animation: fadeInUp 0.6s ease-out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.page-header {
|
|
|
|
|
animation: slideInLeft 0.8s ease-out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-section {
|
|
|
|
|
animation: fadeInUp 0.8s ease-out 0.2s both;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.gi-table) {
|
|
|
|
|
animation: fadeInUp 0.8s ease-out 0.4s both;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 表格行进入动画
|
|
|
|
|
:deep(.arco-table-tbody .arco-table-tr) {
|
|
|
|
|
animation: fadeInUp 0.6s ease-out;
|
|
|
|
|
animation-fill-mode: both;
|
|
|
|
|
|
|
|
|
|
@for $i from 1 through 20 {
|
|
|
|
|
&:nth-child(#{$i}) {
|
|
|
|
|
animation-delay: #{0.1 * $i}s;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 悬停效果增强
|
|
|
|
|
:deep(.arco-table-tbody .arco-table-tr:hover) {
|
|
|
|
|
.role-type-text {
|
|
|
|
|
transform: scale(1.05);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-link {
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 状态标签悬停效果
|
|
|
|
|
:deep(.arco-tag) {
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
transform: scale(1.05);
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-08 14:26:22 +08:00
|
|
|
|
</style>
|