1425 lines
35 KiB
Vue
1425 lines
35 KiB
Vue
<!--
|
||
团队成员管理页面
|
||
功能特性:
|
||
1. 团队成员列表展示
|
||
2. 新增团队成员
|
||
3. 编辑团队成员信息
|
||
4. 删除团队成员
|
||
5. 导入导出团队成员数据
|
||
-->
|
||
<template>
|
||
<GiPageLayout class="construction-personnel-page">
|
||
<!-- 页面头部 -->
|
||
<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>
|
||
<a-form-item label="项目岗位">
|
||
<a-select
|
||
v-model="searchForm.roleType"
|
||
placeholder="请选择项目岗位"
|
||
style="width: 150px"
|
||
allow-clear
|
||
>
|
||
<a-option
|
||
v-for="option in ROLE_TYPE_OPTIONS"
|
||
: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>
|
||
<a-button type="primary" @click="handleSearch">
|
||
<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"
|
||
:scroll="{ x: '100%', y: 'calc(100vh - 400px)', minWidth: 1200 }"
|
||
:pagination="pagination"
|
||
:disabled-tools="['size']"
|
||
@page-change="onPageChange"
|
||
@page-size-change="onPageSizeChange"
|
||
@refresh="loadData"
|
||
>
|
||
<!-- 项目岗位列 -->
|
||
<template #roleType="{ record }">
|
||
<span class="role-type-text">
|
||
{{ getRoleTypeText(record.roleType) }}
|
||
</span>
|
||
</template>
|
||
|
||
<!-- 状态列 -->
|
||
<template #status="{ record }">
|
||
<a-tag :color="getStatusColor(record.status || 'INACTIVE')">
|
||
{{ getStatusText(record.status || 'INACTIVE') }}
|
||
</a-tag>
|
||
</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>
|
||
<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>
|
||
</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-auto-complete
|
||
v-model="memberForm.name"
|
||
placeholder="请输入姓名进行搜索"
|
||
:data="userSearchOptions"
|
||
:loading="userSearchLoading"
|
||
@search="handleUserSearch"
|
||
@select="handleUserSelect"
|
||
allow-clear
|
||
:filter-option="false"
|
||
:trigger-on-focus="false"
|
||
/>
|
||
</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">
|
||
<label>项目岗位 <span class="required">*</span></label>
|
||
<a-select v-model="memberForm.roleType" placeholder="请选择项目岗位">
|
||
<a-option
|
||
v-for="option in ROLE_TYPE_OPTIONS"
|
||
: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, onUnmounted } 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'
|
||
import { searchUserByName } from '@/apis/system/user'
|
||
import type { UserNewResp } from '@/apis/system/type'
|
||
|
||
// 获取路由参数
|
||
const route = useRoute()
|
||
const projectId = route.query.projectId as string
|
||
|
||
// 选项数据常量 - 项目岗位选项
|
||
|
||
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' }
|
||
]
|
||
|
||
const STATUS_OPTIONS = [
|
||
{ label: '可用', value: 'ACTIVE' },
|
||
{ label: '忙碌', value: 'SUSPENDEN' },
|
||
{ label: '离线', value: 'INACTIVE' }
|
||
]
|
||
|
||
// 响应式数据
|
||
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
|
||
roleType: string
|
||
status: string
|
||
}>({
|
||
name: '',
|
||
roleType: '',
|
||
status: ''
|
||
})
|
||
|
||
// 表格列配置
|
||
const tableColumns = [
|
||
{ title: '姓名', dataIndex: 'name', width: 100, fixed: 'left' },
|
||
{ title: '联系电话', dataIndex: 'phone', width: 120 },
|
||
{ title: '邮箱', dataIndex: 'email', width: 150 },
|
||
{ title: '项目岗位', dataIndex: 'roleType', width: 120, slotName: 'roleType' },
|
||
{ 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: '',
|
||
roleType: '', // 项目岗位
|
||
status: 'ACTIVE',
|
||
email: '',
|
||
joinDate: '',
|
||
remark: ''
|
||
})
|
||
|
||
const statusForm = reactive<{
|
||
id: string | number
|
||
name: string
|
||
currentStatus: string
|
||
newStatus: 'ACTIVE' | 'SUSPENDEN' | 'INACTIVE'
|
||
}>({
|
||
id: '',
|
||
name: '',
|
||
currentStatus: '',
|
||
newStatus: 'ACTIVE'
|
||
})
|
||
|
||
const fileList = ref<any[]>([])
|
||
|
||
// 用户搜索相关数据
|
||
const userSearchResults = ref<UserNewResp[]>([])
|
||
const userSearchOptions = ref<Array<{ label: string; value: string; user: UserNewResp }>>([])
|
||
const userSearchLoading = ref(false)
|
||
const searchTimeout = ref<NodeJS.Timeout | null>(null)
|
||
|
||
// 方法
|
||
const loadData = async () => {
|
||
if (!projectId) {
|
||
console.warn('未获取到项目ID,无法加载团队成员数据')
|
||
Message.warning('未获取到项目信息,请从项目详情页面进入')
|
||
return
|
||
}
|
||
|
||
loading.value = true
|
||
try {
|
||
console.log('正在加载项目团队成员数据,项目ID:', projectId)
|
||
|
||
// 构建查询参数
|
||
const queryParams: TeamMemberQuery = {
|
||
projectId: projectId,
|
||
page: pagination.current,
|
||
pageSize: pagination.pageSize,
|
||
name: searchForm.name || undefined,
|
||
position: searchForm.roleType || undefined,
|
||
status: searchForm.status || undefined
|
||
}
|
||
|
||
const response = await getProjectTeamMembers(queryParams)
|
||
|
||
console.log('API响应数据:', response.data)
|
||
|
||
// 确保response.data是数组
|
||
const rawData = Array.isArray(response.data) ? response.data : [response.data]
|
||
|
||
console.log('处理后的原始数据:', rawData)
|
||
|
||
// 处理后端返回的数据,将后端字段映射到前端期望的字段
|
||
const mappedData = rawData.map((item: BackendTeamMemberResp) => {
|
||
const mappedItem: TeamMemberResp = {
|
||
id: item.memberId,
|
||
name: item.name || '',
|
||
phone: item.phone || '',
|
||
email: item.email || '',
|
||
roleType: item.roleType || '', // 映射项目岗位
|
||
status: (item.status === 'ACTIVE' ? 'ACTIVE' : item.status === 'SUSPENDEN' ? 'SUSPENDEN' : 'INACTIVE') as 'ACTIVE' | 'SUSPENDEN' | 'INACTIVE',
|
||
joinDate: item.joinDate || '',
|
||
remark: item.remark || '',
|
||
avatar: item.userAvatar || ''
|
||
}
|
||
console.log('映射后的数据项:', mappedItem)
|
||
return mappedItem
|
||
})
|
||
|
||
dataList.value = mappedData
|
||
pagination.total = mappedData.length
|
||
|
||
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: '',
|
||
roleType: '',
|
||
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 handleUserSearch = async (value: string) => {
|
||
console.log('开始搜索用户,输入值:', value)
|
||
|
||
// 清除之前的定时器
|
||
if (searchTimeout.value) {
|
||
clearTimeout(searchTimeout.value)
|
||
}
|
||
|
||
// 如果输入为空,清空结果
|
||
if (!value || value.trim().length < 1) {
|
||
userSearchResults.value = []
|
||
console.log('输入为空,清空搜索结果')
|
||
return
|
||
}
|
||
|
||
// 设置防抖延迟(300ms)
|
||
searchTimeout.value = setTimeout(async () => {
|
||
console.log('执行搜索,搜索值:', value.trim())
|
||
userSearchLoading.value = true
|
||
try {
|
||
const response = await searchUserByName(value.trim())
|
||
console.log('API响应完整数据:', response)
|
||
// 根据后端返回的数据结构,使用 response.rows 而不是 response.data
|
||
const users = response.rows || []
|
||
userSearchResults.value = users
|
||
|
||
// 转换为 a-auto-complete 需要的格式
|
||
userSearchOptions.value = users.map(user => ({
|
||
label: `${user.name} | ${user.deptName || '未分配部门'}`,
|
||
value: user.name,
|
||
user: user
|
||
}))
|
||
|
||
console.log('设置搜索结果:', userSearchResults.value)
|
||
console.log('设置搜索选项:', userSearchOptions.value)
|
||
} catch (error) {
|
||
console.error('搜索用户失败:', error)
|
||
userSearchResults.value = []
|
||
userSearchOptions.value = []
|
||
Message.error('搜索用户失败')
|
||
} finally {
|
||
userSearchLoading.value = false
|
||
}
|
||
}, 300)
|
||
}
|
||
|
||
// 用户选择处理函数
|
||
const handleUserSelect = (value: string, option: any) => {
|
||
const selectedOption = userSearchOptions.value.find(option => option.value === value)
|
||
if (selectedOption) {
|
||
const selectedUser = selectedOption.user
|
||
// 自动填充用户信息
|
||
memberForm.name = selectedUser.name
|
||
memberForm.phone = selectedUser.mobile || ''
|
||
// 后端返回的数据中没有email字段,所以不自动填充邮箱
|
||
// memberForm.email = selectedUser.email || ''
|
||
// 清空搜索结果
|
||
userSearchResults.value = []
|
||
userSearchOptions.value = []
|
||
console.log('选择的用户:', selectedUser)
|
||
}
|
||
}
|
||
|
||
const openEditModal = (record: TeamMemberResp) => {
|
||
isEdit.value = true
|
||
Object.assign(memberForm, {
|
||
id: record.id,
|
||
projectId: projectId,
|
||
name: record.name,
|
||
phone: record.phone || '',
|
||
roleType: record.roleType || '',
|
||
status: record.status || 'ACTIVE',
|
||
email: record.email || '',
|
||
joinDate: record.joinDate || '',
|
||
remark: record.remark || ''
|
||
})
|
||
memberModalVisible.value = true
|
||
}
|
||
|
||
const saveMember = async () => {
|
||
if (!memberForm.name || !memberForm.phone || !memberForm.roleType) {
|
||
Message.error('请填写必填项')
|
||
return
|
||
}
|
||
|
||
try {
|
||
if (isEdit.value && memberForm.id) {
|
||
// 更新团队成员
|
||
const updateData: UpdateTeamMemberForm = {
|
||
name: memberForm.name,
|
||
phone: memberForm.phone,
|
||
roleType: memberForm.roleType,
|
||
status: memberForm.status,
|
||
email: memberForm.email,
|
||
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,
|
||
roleType: memberForm.roleType,
|
||
status: memberForm.status,
|
||
email: memberForm.email,
|
||
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()
|
||
// 清空用户搜索结果
|
||
userSearchResults.value = []
|
||
userSearchOptions.value = []
|
||
}
|
||
|
||
const resetMemberForm = () => {
|
||
Object.assign(memberForm, {
|
||
id: undefined,
|
||
projectId: projectId,
|
||
name: '',
|
||
phone: '',
|
||
roleType: '',
|
||
status: 'ACTIVE',
|
||
email: '',
|
||
joinDate: '',
|
||
remark: ''
|
||
})
|
||
// 清空用户搜索结果
|
||
userSearchResults.value = []
|
||
userSearchOptions.value = []
|
||
}
|
||
|
||
const openStatusModal = (record: TeamMemberResp) => {
|
||
Object.assign(statusForm, {
|
||
id: record.id,
|
||
name: record.name,
|
||
currentStatus: record.status || 'ACTIVE',
|
||
newStatus: record.status || 'ACTIVE'
|
||
})
|
||
statusModalVisible.value = true
|
||
}
|
||
|
||
const saveStatus = async () => {
|
||
try {
|
||
await updateTeamMember(statusForm.id, {
|
||
status: statusForm.newStatus as 'ACTIVE' | 'SUSPENDEN' | 'INACTIVE'
|
||
})
|
||
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,
|
||
position: searchForm.roleType || undefined,
|
||
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('导出失败')
|
||
}
|
||
}
|
||
|
||
// 工具方法
|
||
const getRoleTypeText = (roleType: string) => {
|
||
const option = ROLE_TYPE_OPTIONS.find(opt => opt.value === roleType)
|
||
return option ? option.label : roleType
|
||
}
|
||
|
||
const getStatusColor = (status: string) => {
|
||
const colorMap: Record<string, string> = {
|
||
ACTIVE: '#52c41a', // 绿色 - 可用
|
||
SUSPENDEN: '#faad14', // 橙色 - 忙碌
|
||
INACTIVE: '#ff4d4f' // 红色 - 离线
|
||
}
|
||
return colorMap[status] || '#ff4d4f'
|
||
}
|
||
|
||
const getStatusText = (status: string) => {
|
||
const textMap: Record<string, string> = {
|
||
ACTIVE: '可用',
|
||
SUSPENDEN: '忙碌',
|
||
INACTIVE: '离线'
|
||
}
|
||
return textMap[status] || '未知'
|
||
}
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
console.log('团队成员管理页面加载,项目ID:', projectId)
|
||
if (projectId) {
|
||
loadData()
|
||
} else {
|
||
console.warn('未获取到项目ID,无法加载团队成员数据')
|
||
Message.warning('未获取到项目信息,请从项目详情页面进入')
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
// 清理搜索定时器
|
||
if (searchTimeout.value) {
|
||
clearTimeout(searchTimeout.value)
|
||
}
|
||
})
|
||
</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;
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
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;
|
||
}
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
position: relative;
|
||
z-index: 1;
|
||
|
||
.back-btn {
|
||
border: none;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
color: white;
|
||
backdrop-filter: blur(10px);
|
||
border-radius: 8px;
|
||
padding: 8px 16px;
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
transform: translateX(-2px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||
}
|
||
}
|
||
|
||
.header-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
|
||
.title-icon {
|
||
font-size: 28px;
|
||
color: white;
|
||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
||
}
|
||
|
||
h1 {
|
||
margin: 0;
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: white;
|
||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.project-info {
|
||
font-size: 14px;
|
||
color: rgba(255, 255, 255, 0.9);
|
||
background: rgba(255, 255, 255, 0.2);
|
||
padding: 6px 12px;
|
||
border-radius: 20px;
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
}
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.search-section {
|
||
margin-bottom: 16px;
|
||
|
||
.search-card {
|
||
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);
|
||
|
||
:deep(.arco-card-body) {
|
||
padding: 24px;
|
||
}
|
||
|
||
:deep(.arco-form-item) {
|
||
margin-bottom: 0;
|
||
margin-right: 20px;
|
||
}
|
||
|
||
:deep(.arco-form-item-label) {
|
||
font-weight: 600;
|
||
color: #4e5969;
|
||
font-size: 14px;
|
||
}
|
||
|
||
:deep(.arco-input),
|
||
:deep(.arco-select) {
|
||
width: 100%;
|
||
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) {
|
||
border-radius: 8px;
|
||
font-weight: 600;
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 表格区域样式
|
||
:deep(.gi-table) {
|
||
background: white;
|
||
border-radius: 12px;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||
margin-bottom: 24px;
|
||
overflow: hidden;
|
||
|
||
.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%);
|
||
|
||
.arco-table-th {
|
||
background: transparent !important;
|
||
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
|
||
|
||
.arco-table-th-item-title {
|
||
color: white !important;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 表格行样式
|
||
.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;
|
||
}
|
||
}
|
||
|
||
// 斑马纹效果
|
||
.arco-table-tr:nth-child(even) {
|
||
background: #fafbfc;
|
||
|
||
&:hover {
|
||
background: linear-gradient(135deg, #f8f9ff 0%, #f0f2ff 100%);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 固定列样式
|
||
.arco-table-fixed-left,
|
||
.arco-table-fixed-right {
|
||
.arco-table-td {
|
||
background: white;
|
||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
.remark-content {
|
||
max-width: 180px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
color: #4e5969;
|
||
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);
|
||
}
|
||
}
|
||
|
||
.no-remark {
|
||
color: #c9cdd4;
|
||
font-style: italic;
|
||
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 {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 页面滚动修复
|
||
.construction-personnel-page {
|
||
height: auto;
|
||
min-height: 100vh;
|
||
overflow: visible;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
:deep(.gi-page-layout) {
|
||
height: auto;
|
||
min-height: 100vh;
|
||
overflow: visible;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
:deep(.gi-page-layout-content) {
|
||
flex: 1;
|
||
overflow: visible;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
|
||
// 响应式设计
|
||
@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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 全局动画效果
|
||
@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);
|
||
}
|
||
}
|
||
|
||
// 用户搜索选项样式
|
||
.user-option {
|
||
.user-name {
|
||
font-weight: 500;
|
||
color: var(--color-text-1);
|
||
}
|
||
}
|
||
|
||
// 自动完成组件样式优化
|
||
:deep(.arco-auto-complete) {
|
||
.arco-input {
|
||
transition: all 0.3s ease;
|
||
|
||
&:focus {
|
||
border-color: var(--color-primary-6);
|
||
box-shadow: 0 0 0 2px rgba(var(--color-primary-6), 0.1);
|
||
}
|
||
}
|
||
}
|
||
</style> |