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

1425 lines
35 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
团队成员管理页面
功能特性:
1. 团队成员列表展示
2. 新增团队成员
3. 编辑团队成员信息
4. 删除团队成员
5. 导入导出团队成员数据
-->
<template>
<GiPageLayout 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>