Industrial-image-management.../src/views/project/index.vue

871 lines
27 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. 项目列表查询 (listProject) - 支持分页和条件查询
2. 项目新增 (addProject)
3. 项目修改 (updateProject)
4. 项目删除 (deleteProject)
5. 项目导出 (exportProject)
6. 项目导入 (importProject)
所有API调用都已添加错误处理和类型安全检查
-->
<template>
<GiPageLayout>
<GiTable row-key="id" :data="dataList" :columns="tableColumns" :loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }" :pagination="pagination" :disabled-tools="['size']"
@page-change="onPageChange" @page-size-change="onPageSizeChange" @refresh="search">
<template #top>
<GiForm v-model="searchForm" search :columns="queryFormColumns" size="medium" @search="search" @reset="reset">
</GiForm>
</template>
<template #toolbar-left>
<a-button v-permission="['project:create']" type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新增项目</template>
</a-button>
<a-button v-permission="['project:import']" @click="openImportModal">
<template #icon><icon-upload /></template>
<template #default>导入</template>
</a-button>
</template>
<template #toolbar-right>
<a-button v-permission="['project:export']" @click="exportData">
<template #icon><icon-download /></template>
<template #default>导出</template>
</a-button>
</template>
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">{{ PROJECT_STATUS_MAP[record.status] || '未知状态' }}</a-tag>
</template>
<template #fieldInfo="{ record }">
<div>{{ record.fieldName }}</div>
<div class="text-xs text-gray-500">{{ record.fieldLocation }}</div>
</template>
<template #commissionInfo="{ record }">
<div>{{ record.commissionContact }}</div>
<div class="text-xs text-gray-500">{{ record.commissionPhone }}</div>
</template>
<template #inspectionInfo="{ record }">
<div>{{ record.inspectionContact }}</div>
<div class="text-xs text-gray-500">{{ record.inspectionPhone }}</div>
</template>
<template #projectPeriod="{ record }">
<span v-if="record.projectPeriod?.length === 2">
{{ record.projectPeriod[0] }} 至 {{ record.projectPeriod[1] }}
</span>
</template>
<template #projectManager="{ record }">
<div>{{ record.projectManager }}</div>
<div class="text-xs text-gray-500">{{ record.projectStaff?.join(', ') }}</div>
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['project:detail']" title="详情" @click="viewDetail(record)">详情</a-link>
<a-link v-permission="['project:update']" title="修改" @click="openEditModal(record)">修改</a-link>
<a-link v-permission="['project:delete']" status="danger" title="删除" @click="confirmDelete(record)">
删除
</a-link>
</a-space>
</template>
</GiTable>
<!-- 新增/编辑项目弹窗 -->
<a-modal v-model:visible="addModalVisible" :title="modalTitle" @cancel="resetForm"
:ok-button-props="{ loading: submitLoading }" @ok="handleSubmit" width="800px" modal-class="project-form-modal">
<a-form ref="formRef" :model="form" :rules="formRules" layout="vertical"
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }">
<!-- 基本信息 -->
<a-divider orientation="left">基本信息</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="projectName" label="项目名称" required>
<a-input v-model="form.projectName" placeholder="请输入项目名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="farmAddress" label="地址">
<a-input v-model="form.farmAddress" placeholder="请输入地址" />
</a-form-item>
</a-col>
<a-col><a-button size="mini" @click="() => { Message.info(`待开发`) }">
<template #icon><icon-location /></template>
地图选点
</a-button></a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="projectManagerId" label="项目经理" required>
<a-select v-model="form.projectManagerId" placeholder="请选择项目经理" :loading="userLoading">
<a-option v-for="user in userOptions" :key="user.value" :value="user.value">
{{ user.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="inspectionUnit" label="业主">
<a-input v-model="form.inspectionUnit" placeholder="请输入业主单位" @input="(val) => (form.farmName = val)" />
<!--风场名称同步业主 -->
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="inspectionContact" label="业主单位联系人">
<a-input v-model="form.inspectionContact" placeholder="请输入联系人" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="inspectionPhone" label="业主单位联系电话">
<a-input v-model="form.inspectionPhone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="client" label="委托单位">
<a-input v-model="form.client" placeholder="请输入委托单位" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="clientContact" label="委托单位联系人">
<a-input v-model="form.clientContact" placeholder="请输入联系人" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="clientPhone" label="委托单位联系电话">
<a-input v-model="form.clientPhone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-form-item field="projectContent" label="项目内容">
<a-textarea v-model="form.coverUrl" placeholder="请输入项目内容" :rows="4" />
</a-form-item>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="status" label="项目状态">
<a-select v-model="form.status" placeholder="请选择状态">
<a-option v-for="option in PROJECT_STATUS_OPTIONS" :key="option.value" :value="option.value">
{{ option.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="scale" label="项目规模">
<a-input-number v-model="form.scale" placeholder="请输入项目规模" :min="0" :max="999" :step="1" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="startDate" label="开始时间">
<a-date-picker v-model="form.startDate" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="endDate" label="结束时间">
<a-date-picker v-model="form.endDate" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="turbineModel" label="风机型号">
<a-input v-model="form.turbineModel" placeholder="请输入风机型号" />
</a-form-item>
</a-col>
</a-row>
<!-- 风场信息 -->
<a-divider orientation="left">风场信息可视化</a-divider>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item label="机组网格布局">
<a-space direction="vertical" style="width: 100%">
<TurbineGrid v-model:="form.turbineList"></TurbineGrid>
</a-space>
</a-form-item>
</a-col>
</a-row>
<a-divider orientation="middle">地图</a-divider>
</a-form>
</a-modal>
<!-- 导入项目弹窗 -->
<a-modal v-model:visible="importModalVisible" title="导入文件" @cancel="handleCancelImport" @before-ok="handleImport">
<div class="flex flex-col items-center justify-center p-8">
<div class="text-primary text-4xl mb-4">
<icon-file-upload />
</div>
<div class="text-lg font-medium mb-2">批量导入文件</div>
<div class="text-gray-500 mb-4">拖动文件到此处或点击下方按钮上传</div>
<a-upload :file-list="fileList" :limit="1" @change="handleFileChange">
<template #upload-button>
<a-button type="primary">选择文件</a-button>
</template>
</a-upload>
</div>
</a-modal>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { Message, Modal } from '@arco-design/web-vue'
import { addProject, deleteProject, listProject, updateProject, exportProject, importProject } from '@/apis/project'
import { isMobile } from '@/utils'
import has from '@/utils/has'
import http from '@/utils/http'
import type { ColumnItem } from '@/components/GiForm'
import type { TableColumnData } from '@arco-design/web-vue'
import type { ProjectResp, ProjectPageQuery } from '@/apis/project/type'
import * as T from '@/apis/project/type'
import TurbineGrid from './TurbineGrid.vue'
defineOptions({ name: 'ProjectManagement' })
// 项目状态常量定义 (API返回数字类型)
const PROJECT_STATUS = {
NOT_STARTED: 0, // 未开始/待施工
IN_PROGRESS: 1, // 施工中
COMPLETED: 2, // 已完成
} as const
// 项目状态映射
const PROJECT_STATUS_MAP = {
0: '待施工',
1: '施工中',
2: '已完成'
} as const
// 项目状态选项
const PROJECT_STATUS_OPTIONS = [
{ label: '待施工', value: 0 },
{ label: '施工中', value: 1 },
{ label: '已完成', value: 2 }
]
// 项目类别常量定义
const PROJECT_CATEGORY = {
EXTERNAL_WORK: '外部工作',
INTERNAL_PROJECT: '内部项目',
TECHNICAL_SERVICE: '技术服务'
} as const
// 项目类别选项
const PROJECT_CATEGORY_OPTIONS = [
{ label: PROJECT_CATEGORY.EXTERNAL_WORK, value: PROJECT_CATEGORY.EXTERNAL_WORK },
{ label: PROJECT_CATEGORY.INTERNAL_PROJECT, value: PROJECT_CATEGORY.INTERNAL_PROJECT },
{ label: PROJECT_CATEGORY.TECHNICAL_SERVICE, value: PROJECT_CATEGORY.TECHNICAL_SERVICE }
]
const router = useRouter()
const formRef = ref()
const loading = ref(false)
const addModalVisible = ref(false)
const importModalVisible = ref(false)
const isEdit = ref(false)
const currentId = ref<string | null>(null)
const fileList = ref([])
const dataList = ref<T.ProjectResp[]>([])
const userLoading = ref(false)
const userOptions = ref<{ label: string; value: string }[]>([])
let searchForm = reactive<Partial<ProjectPageQuery>>({
projectName: '',
status: undefined,
fieldName: '', // 保持使用fieldName因为API类型定义中使用的是这个字段名
})
const queryFormColumns: ColumnItem[] = reactive([
{
type: 'input',
label: '项目名称',
field: 'projectName',
span: { xs: 24, sm: 8, xxl: 8 },
props: {
placeholder: '请输入项目名称',
},
},
{
type: 'input',
label: '业主',
field: 'inspectionUnit',
span: { xs: 24, sm: 8, xxl: 8 },
props: {
placeholder: '请输入业主名称',
},
},
{
type: 'select',
label: '状态',
field: 'status',
span: { xs: 24, sm: 8, xxl: 8 },
props: {
options: PROJECT_STATUS_OPTIONS,
placeholder: '请选择状态',
},
},
])
const form = reactive({
projectId: '', // 项目id
projectName: '', // 项目名称
projectManagerId: '', // 项目经理id
client: '', // 委托单位
clientContact: '', // 委托单位联系人
clientPhone: '', // 委托单位联系电话
inspectionUnit: '', // 业主单位
inspectionContact: '', // 业主单位联系人
inspectionPhone: '', // 业主单位联系电话
farmName: '', // 风场名称 现在等同业主单位
farmAddress: '', // 风场地址 项目地址
scale: '', // 项目规模 风机数量
turbineModel: '', // 风机型号
status: '', // 状态0待施工1施工中2已完工3已审核4已验收
startDate: '', // 开始时间
endDate: '', // 结束时间
coverUrl: '', // 项目封面 现在填的是项目内容
constructionTeamLeaderId: '', // 施工组长id
constructorIds: '', // 施工人员id
qualityOfficerId: '', // 质量员id
auditorId: '', // 安全员id
turbineList: [] as { id: number; turbineNo: string; lat?: number; lng?: number; status: 0 | 1 | 2 }[], //风机组
})
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
showJumper: true,
showPageSize: true
})
const openMapModal = (item: any) => {
Message.info(`地图选点功能待开发,当前机组编号:${item.turbineNo}`)
}
const tableColumns = ref<TableColumnData[]>([
{
title: '序号',
width: 66,
align: 'center',
render: ({ rowIndex }) => rowIndex + 1 + (pagination.current - 1) * pagination.pageSize,
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '项目编号',
dataIndex: 'projectCode',
width: 120,
ellipsis: true,
tooltip: true,
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '项目名称',
dataIndex: 'projectName',
minWidth: 140,
ellipsis: true,
tooltip: true,
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '地点',
slotName: 'fieldInfo',
minWidth: 180,
ellipsis: true,
tooltip: true
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
align: 'center',
width: 100
},
{
title: '委托单位',
dataIndex: 'commissionUnit',
minWidth: 140,
ellipsis: true,
tooltip: true
},
{
title: '委托单位联系人/电话',
slotName: 'commissionInfo',
minWidth: 160,
ellipsis: true,
tooltip: true
},
{
title: '业主',
dataIndex: 'inspectionUnit',
minWidth: 140,
ellipsis: true,
tooltip: true
},
{
title: '业主联系人/电话',
slotName: 'inspectionInfo',
minWidth: 160,
ellipsis: true,
tooltip: true
},
{
title: '项目规模',
dataIndex: 'projectScale',
width: 100,
ellipsis: true,
tooltip: true
},
{
title: '机组型号',
dataIndex: 'orgNumber',
width: 100,
ellipsis: true,
tooltip: true
},
{
title: '项目经理/施工人员',
slotName: 'projectManager',
minWidth: 160,
ellipsis: true,
tooltip: true
},
{
title: '项目周期',
slotName: 'projectPeriod',
minWidth: 180,
ellipsis: true,
tooltip: true
},
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 180,
fixed: !isMobile() ? 'right' : undefined,
},
])
watch(() => form.scale, (newVal) => {
const count = Number(newVal)
if (count > 0 && count <= 999) {
form.turbineList = Array.from({ length: count }, (_, i) => ({
id: i + 1,
turbineNo: `${String(i + 1).padStart(3, '0')}`,
lat: undefined,
lng: undefined,
status: form.status,
}))
} else {
form.turbineList = []
}
}, { immediate: true })
const modalTitle = computed(() => isEdit.value ? '编辑项目' : '新增项目')
const getStatusColor = (status: number) => {
switch (status) {
case PROJECT_STATUS.IN_PROGRESS:
return 'blue'
case PROJECT_STATUS.COMPLETED:
return 'green'
case PROJECT_STATUS.NOT_STARTED:
return 'orange'
default:
return 'gray'
}
}
const fetchData = async () => {
loading.value = true
try {
const params: ProjectPageQuery = {
...searchForm,
page: pagination.current,
size: pagination.pageSize
}
const res = await listProject(params)
if (res.success && res.data) {
// API直接返回数组数据
const projects = Array.isArray(res.data) ? res.data : []
// 数据映射和兼容性处理
dataList.value = projects.map((item: any) => {
const mappedItem: T.ProjectResp = {
...item,
// 添加别名字段以保持兼容性
id: item.projectId,
fieldName: item.farmName,
fieldLocation: item.farmAddress,
commissionUnit: item.client,
commissionContact: item.clientContact,
commissionPhone: item.clientPhone,
orgNumber: item.turbineModel,
projectManager: item.projectManagerName,
projectScale: item.scale,
// 处理项目周期
projectPeriod: item.startDate && item.endDate ? [item.startDate, item.endDate] : []
};
return mappedItem;
})
// 由于API没有返回total使用当前数据长度
// 如果是完整数据可以用数据长度如果有分页需要从其他地方获取total
pagination.total = projects.length
// 如果返回的数据少于每页大小,说明已经是最后一页
if (projects.length < pagination.pageSize) {
pagination.total = (pagination.current - 1) * pagination.pageSize + projects.length
}
} else {
Message.error(res.msg || '获取数据失败')
dataList.value = []
pagination.total = 0
}
} catch (error) {
console.error('获取项目列表失败:', error)
Message.error('获取数据失败')
dataList.value = []
pagination.total = 0
} finally {
loading.value = false
}
}
const search = () => {
pagination.current = 1
fetchData()
}
const reset = () => {
// 重置搜索表单
Object.assign(searchForm, {
projectName: '',
fieldName: '', // 保持使用fieldName因为API类型定义中使用的是这个字段名
status: undefined,
})
// 重置分页并重新搜索
pagination.current = 1
search()
}
const onPageChange = (current: number) => {
pagination.current = current
fetchData()
}
const onPageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
pagination.current = 1
fetchData()
}
const resetForm = () => {
// 重置表单字段
Object.assign(form, {
projectId: '', // 项目id
projectName: '', // 项目名称
projectManagerId: '', // 项目经理id
client: '', // 委托单位
clientContact: '', // 委托单位联系人
clientPhone: '', // 委托单位联系电话
inspectionUnit: '', // 检查单位
inspectionContact: '', // 检查单位联系人
inspectionPhone: '', // 检查单位联系电话
farmName: '', // 风场名称
farmAddress: '', // 风场地址
scale: '', // 项目规模
turbineModel: '', // 风机型号
status: 0, // 状态0待施工1施工中2已完工3已审核4已验收
startDate: '', // 开始时间
endDate: '', // 结束时间
coverUrl: '', // 项目封面
constructionTeamLeaderId: '', // 施工组长id
constructorIds: '', // 施工人员id
qualityOfficerId: '', // 质量员id
auditorId: '' // 安全员id
})
isEdit.value = false
currentId.value = null
}
const openAddModal = () => {
resetForm()
addModalVisible.value = true
}
const openEditModal = (record: T.ProjectResp) => {
isEdit.value = true
currentId.value = record.id || record.projectId || null
// 重置表单
Object.keys(form).forEach(key => {
// @ts-ignore
form[key] = ''
})
// 填充表单数据
Object.keys(form).forEach(key => {
if (key in record && record[key as keyof T.ProjectResp] !== undefined) {
// @ts-ignore - 这里需要处理类型转换
form[key] = record[key as keyof T.ProjectResp]
}
})
// 处理特殊字段映射
if (record.farmName) form.farmName = record.farmName
if (record.farmAddress) form.farmAddress = record.farmAddress
if (record.client) form.client = record.client
if (record.clientContact) form.clientContact = record.clientContact
if (record.clientPhone) form.clientPhone = record.clientPhone
if (record.turbineModel) form.turbineModel = record.turbineModel
if (record.scale) form.scale = record.scale
// 处理日期字段
if (record.startDate) form.startDate = record.startDate
if (record.endDate) form.endDate = record.endDate
addModalVisible.value = true
}
// 添加表单验证规则
const formRules = {
projectName: [{ required: true, message: '请输入项目名称' }],
}
// 添加提交加载状态
const submitLoading = ref(false)
const handleSubmit = async () => {
console.log('表单提交开始', form)
submitLoading.value = true
try {
// 表单验证
console.log('开始验证表单', formRef.value)
await formRef.value.validate()
// 准备提交的数据
const submitData = {
...form,
// 确保projectId字段正确
projectId: isEdit.value && currentId.value ? currentId.value : form.projectId,
// 处理日期格式 - 确保是字符串格式 YYYY-MM-DD
startDate: form.startDate ? (typeof form.startDate === 'string' ? form.startDate : new Date(form.startDate).toISOString().split('T')[0]) : '',
endDate: form.endDate ? (typeof form.endDate === 'string' ? form.endDate : new Date(form.endDate).toISOString().split('T')[0]) : '',
// 处理施工人员ID - 如果是数组,转换为逗号分隔的字符串
constructorIds: Array.isArray(form.constructorIds) ? form.constructorIds.join(',') : form.constructorIds
}
console.log('提交数据:', submitData)
let res
if (isEdit.value && currentId.value) {
// 编辑模式
console.log('编辑模式提交', currentId.value)
res = await updateProject(submitData, currentId.value)
Message.success('更新成功')
} else {
// 新增模式
console.log('新增模式提交')
res = await addProject(submitData)
Message.success('添加成功')
}
console.log('API响应结果:', res)
// 检查操作是否成功
if (res && res.success === false) {
Message.error(res.msg || '操作失败')
submitLoading.value = false
return
}
addModalVisible.value = false
fetchData()
} catch (error: any) {
console.error('项目操作失败:', error)
if (error && error.type === 'form') {
// 表单验证失败
console.log('表单验证失败')
} else {
Message.error(error?.message || '操作失败')
}
} finally {
submitLoading.value = false
}
}
const confirmDelete = (record: T.ProjectResp) => {
Modal.warning({
title: '确认删除',
content: `确定要删除项目"${record.projectName}"吗?`,
onOk: () => deleteItem(record),
})
}
const deleteItem = async (record: T.ProjectResp) => {
const projectId = record.id || record.projectId
if (!projectId) {
Message.error('项目ID不存在')
return
}
try {
const res = await deleteProject(projectId)
// 检查删除操作是否成功
if (res && res.success === false) {
Message.error(res.msg || '删除失败')
return
}
Message.success('删除成功')
fetchData()
} catch (error) {
console.error('删除项目失败:', error)
Message.error('删除失败')
}
}
const viewDetail = (record: T.ProjectResp) => {
const projectId = record.id || record.projectId
if (!projectId) {
Message.error('项目ID不存在')
return
}
router.push({
name: 'ProjectDetail',
params: {
id: projectId.toString()
}
})
}
const openImportModal = () => {
fileList.value = []
importModalVisible.value = true
}
const handleFileChange = (files: any) => {
fileList.value = files
}
const handleCancelImport = () => {
fileList.value = []
importModalVisible.value = false
}
const handleImport = async () => {
if (fileList.value.length === 0) {
Message.warning('请选择文件')
return false
}
try {
const fileItem = fileList.value[0] as any
const file = fileItem?.file || fileItem
if (!file) {
Message.warning('请选择有效的文件')
return false
}
// 调用导入API
const res = await importProject(file)
if (res && res.success === false) {
Message.error(res.msg || '导入失败')
return false
}
Message.success('导入成功')
handleCancelImport()
fetchData() // 重新获取数据
return true
} catch (error) {
console.error('导入项目失败:', error)
Message.error('导入失败')
return false
}
}
const exportData = async () => {
try {
const params = {
projectName: searchForm.projectName,
status: searchForm.status,
fieldName: searchForm.fieldName, // 保持使用fieldName因为API类型定义中使用的是这个字段名
}
await exportProject(params)
Message.success('导出成功')
} catch (error) {
console.error('导出项目失败:', error)
Message.error('导出失败')
}
}
// 获取用户列表
const fetchUserList = async () => {
userLoading.value = true
try {
// 调用用户列表接口
const res = await http.get('/user/list')
if (res.data && Array.isArray(res.data)) {
userOptions.value = res.data.map(item => ({
label: item.userName || item.username || item.name || item.nickName || item.account || '未命名用户',
value: item.userId || item.id || ''
}))
} else {
userOptions.value = []
}
} catch (error) {
console.error('获取用户列表失败:', error)
Message.error('获取用户列表失败')
userOptions.value = []
} finally {
userLoading.value = false
}
}
onMounted(() => {
fetchData()
fetchUserList() // 获取用户列表
})
</script>
<style scoped>
:deep(.arco-tag) {
margin-right: 0;
}
:deep(.project-form-modal) {
.arco-form-item {
margin-bottom: 16px;
}
.arco-divider {
margin: 16px 0;
font-weight: 500;
color: var(--color-text-2);
}
}
</style>