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

1310 lines
32 KiB
Vue
Raw Normal View History

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>
<a-form-item label="项目岗位">
2025-08-08 14:26:22 +08:00
<a-select
v-model="searchForm.roleType"
placeholder="请选择项目岗位"
2025-08-08 14:26:22 +08:00
style="width: 150px"
allow-clear
>
<a-option
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>
<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"
: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"
>
<!-- 项目岗位列 -->
<template #roleType="{ record }">
<span class="role-type-text">
{{ getRoleTypeText(record.roleType) }}
</span>
</template>
2025-08-08 14:26:22 +08:00
<!-- 状态列 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status || 'INACTIVE')">
{{ getStatusText(record.status || 'INACTIVE') }}
</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>
<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">
<label>项目岗位 <span class="required">*</span></label>
<a-select v-model="memberForm.roleType" placeholder="请选择项目岗位">
2025-08-08 14:26:22 +08:00
<a-option
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
// 选项数据常量 - 项目岗位选项
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 = [
{ label: '可用', value: 'ACTIVE' },
{ label: '忙碌', value: 'SUSPENDEN' },
{ 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
roleType: string
2025-08-08 14:26:22 +08:00
status: string
}>({
name: '',
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 },
{ 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: '',
roleType: '', // 项目岗位
status: 'ACTIVE',
2025-08-08 14:26:22 +08:00
email: '',
joinDate: '',
remark: ''
})
const statusForm = reactive<{
id: string | number
name: string
currentStatus: string
newStatus: 'ACTIVE' | 'SUSPENDEN' | 'INACTIVE'
2025-08-08 14:26:22 +08:00
}>({
id: '',
name: '',
currentStatus: '',
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-08 14:26:22 +08:00
const queryParams: TeamMemberQuery = {
projectId: projectId,
page: pagination.current,
pageSize: pagination.pageSize,
name: searchForm.name || undefined,
position: searchForm.roleType || undefined,
2025-08-08 14:26:22 +08:00
status: searchForm.status || undefined
}
const response = await getProjectTeamMembers(queryParams)
2025-08-08 14:26:22 +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
console.log('处理后的原始数据:', rawData)
2025-08-08 14:26:22 +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 || '',
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 || ''
}
console.log('映射后的数据项:', mappedItem)
return mappedItem
})
2025-08-08 14:26:22 +08:00
dataList.value = mappedData
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: '',
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 || '',
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 () => {
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,
roleType: memberForm.roleType,
2025-08-08 14:26:22 +08:00
status: memberForm.status,
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,
roleType: memberForm.roleType,
2025-08-08 14:26:22 +08:00
status: memberForm.status,
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: '',
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,
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, {
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,
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('导出失败')
}
}
// 工具方法
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> = {
ACTIVE: 'success',
SUSPENDEN: 'warning',
INACTIVE: 'danger'
2025-08-08 14:26:22 +08:00
}
return colorMap[status] || 'danger'
2025-08-08 14:26:22 +08:00
}
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
ACTIVE: '可用',
SUSPENDEN: '忙碌',
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;
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;
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;
gap: 20px;
position: relative;
z-index: 1;
2025-08-08 14:26:22 +08:00
.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;
2025-08-08 14:26:22 +08:00
&:hover {
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;
gap: 12px;
2025-08-08 14:26:22 +08:00
.title-icon {
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;
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;
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;
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 {
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) {
padding: 24px;
}
2025-08-08 14:26:22 +08:00
:deep(.arco-form-item) {
margin-bottom: 0;
margin-right: 20px;
2025-08-08 14:26:22 +08:00
}
:deep(.arco-form-item-label) {
font-weight: 600;
2025-08-08 14:26:22 +08:00
color: #4e5969;
font-size: 14px;
2025-08-08 14:26:22 +08:00
}
: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);
}
2025-08-08 14:26:22 +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
.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);
}
}
2025-08-08 14:26:22 +08:00
}
2025-08-10 20:41:24 +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;
}
}
// 斑马纹效果
.arco-table-tr:nth-child(even) {
background: #fafbfc;
&:hover {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f2ff 100%);
}
}
2025-08-10 20:41:24 +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-08 14:26:22 +08:00
.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);
}
2025-08-08 14:26:22 +08:00
}
.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);
}
}
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 {
height: auto;
min-height: 100vh;
overflow: visible;
2025-08-10 20:41:24 +08:00
display: flex;
flex-direction: column;
:deep(.gi-page-layout) {
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;
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;
}
}
}
// 全局动画效果
@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>