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

1107 lines
36 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" :ok-button-props="{ loading: submitLoading }"
width="800px" modal-class="project-form-modal" @cancel="resetForm" @ok="handleSubmit"
>
<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-col :span="12">
<a-form-item field="projectOrigin" label="项目来源" :rules="[{ required: true, message: '请输入项目来源' }]">
<a-input v-model="form.projectOrigin" placeholder="请输入项目来源" />
</a-form-item>
</a-col>
</a-row>
<a-divider orientation="left">任务设置</a-divider>
<div class="mb-2">
<a-button type="dashed" size="small" @click="addTask">
<template #icon><icon-plus /></template>
新增任务
</a-button>
</div>
<div v-if="form.tasks.length === 0" class="text-gray-500 mb-2">暂无任务,请点击“新增任务”。</div>
<a-space direction="vertical" fill>
<a-card v-for="(task, tIndex) in form.tasks" :key="tIndex" size="small">
<template #title>
<div class="flex items-center justify-between">
<span>任务 {{ tIndex + 1 }}</span>
<a-space>
<a-button size="mini" @click="addSubtask(tIndex)">新增子任务</a-button>
<a-button size="mini" status="danger" @click="removeTask(tIndex)">删除</a-button>
</a-space>
</div>
</template>
<a-row :gutter="12">
<a-col :span="8">
<a-form-item :field="`tasks.${tIndex}.taskName`" label="任务名称" required>
<a-input v-model="task.taskName" placeholder="请输入任务名称" />
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item :field="`tasks.${tIndex}.taskCode`" label="任务编号">
<a-input v-model="task.taskCode" placeholder="编号" />
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item :field="`tasks.${tIndex}.mainUserId`" label="负责人">
<a-select v-model="task.mainUserId" placeholder="选择负责人" :loading="userLoading">
<a-option v-for="u in userOptions" :key="u.value" :value="u.value">{{ u.label }}</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="4">
<a-form-item :field="`tasks.${tIndex}.scales`" label="工量">
<a-input-number v-model="task.scales" :min="0" :max="9999" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :span="8">
<a-form-item :field="`tasks.${tIndex}.planStartDate`" label="计划开始">
<a-date-picker v-model="task.planStartDate" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :field="`tasks.${tIndex}.planEndDate`" label="计划结束">
<a-date-picker v-model="task.planEndDate" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :field="`tasks.${tIndex}.taskGroupId`" label="任务组">
<a-input-number v-model="task.taskGroupId" :min="0" placeholder="可选" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<!-- 子任务 -->
<div v-if="task.children && task.children.length">
<a-divider orientation="left">子任务</a-divider>
<a-card
v-for="(sub, sIndex) in task.children"
:key="sIndex"
size="small"
class="mb-2"
>
<template #title>
<div class="flex items-center justify-between">
<span>子任务 {{ tIndex + 1 }}-{{ sIndex + 1 }}</span>
<a-button size="mini" status="danger" @click="removeSubtask(tIndex, sIndex)">删除</a-button>
</div>
</template>
<a-row :gutter="12">
<a-col :span="8">
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskName`" label="任务名称" required>
<a-input v-model="sub.taskName" placeholder="请输入任务名称" />
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskCode`" label="任务编号">
<a-input v-model="sub.taskCode" placeholder="编号" />
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.mainUserId`" label="负责人">
<a-select v-model="sub.mainUserId" placeholder="选择负责人" :loading="userLoading">
<a-option v-for="u in userOptions" :key="u.value" :value="u.value">{{ u.label }}</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="4">
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.scales`" label="工量">
<a-input-number v-model="sub.scales" :min="0" :max="9999" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :span="8">
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.planStartDate`" label="计划开始">
<a-date-picker v-model="sub.planStartDate" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.planEndDate`" label="计划结束">
<a-date-picker v-model="sub.planEndDate" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskGroupId`" label="任务组">
<a-input-number v-model="sub.taskGroupId" :min="0" placeholder="可选" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
</a-card>
</div>
</a-card>
</a-space>
<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 { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Message, Modal } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import TurbineGrid from './TurbineGrid.vue'
import { addProject, deleteProject, exportProject, importProject, listProject, updateProject, getProjectDetail } from '@/apis/project'
import { isMobile } from '@/utils'
import http from '@/utils/http'
import type { ColumnItem } from '@/components/GiForm'
import type { ProjectPageQuery } from '@/apis/project/type'
import type * as T from '@/apis/project/type'
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 }[]>([])
const 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: '', // 风场地址 项目地址
projectOrigin: '', // 项目来源
scale: '', // 项目规模 风机数量
turbineModel: '', // 风机型号
status: '', // 状态0待施工1施工中2已完工3已审核4已验收
startDate: '', // 开始时间
endDate: '', // 结束时间
// coverUrl: '', // 项目封面 现在改为任务,不再使用
constructionTeamLeaderId: '', // 施工组长id
constructorIds: '', // 施工人员id
qualityOfficerId: '', // 质量员id
auditorId: '', // 安全员id
// 任务集合(支持子任务)
tasks: [] as Array<{
taskName: string
taskCode?: string
mainUserId?: string | number
planStartDate?: string
planEndDate?: string
scales?: number
taskGroupId?: number | string
children?: Array<{
taskName: string
taskCode?: string
mainUserId?: string | number
planStartDate?: string
planEndDate?: string
scales?: number
taskGroupId?: number | string
}>
}>,
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: '', // 风场地址
projectOrigin: '', // 项目来源(必填)
scale: '', // 项目规模
turbineModel: '', // 风机型号
status: 0, // 状态0待施工1施工中2已完工3已审核4已验收
startDate: '', // 开始时间
endDate: '', // 结束时间
// coverUrl: '', // 项目封面(已废弃)
constructionTeamLeaderId: '', // 施工组长id
constructorIds: '', // 施工人员id
qualityOfficerId: '', // 质量员id
auditorId: '', // 安全员id
tasks: [],
})
isEdit.value = false
currentId.value = null
}
const openAddModal = () => {
resetForm()
addModalVisible.value = true
}
// 任务增删改(仅在新增/编辑弹窗内部使用)
const addTask = () => {
;(form.tasks as any[]).push({ taskName: '', taskCode: '', mainUserId: undefined, planStartDate: '', planEndDate: '', scales: undefined, taskGroupId: undefined, children: [] })
}
const removeTask = (index: number) => {
;(form.tasks as any[]).splice(index, 1)
}
const addSubtask = (parentIndex: number) => {
const list = (form.tasks as any[])
if (!list[parentIndex].children) list[parentIndex].children = []
list[parentIndex].children!.push({ taskName: '', taskCode: '', mainUserId: undefined, planStartDate: '', planEndDate: '', scales: undefined, taskGroupId: undefined })
}
const removeSubtask = (parentIndex: number, index: number) => {
const list = (form.tasks as any[])
list[parentIndex].children!.splice(index, 1)
}
const openEditModal = (record: T.ProjectResp) => {
isEdit.value = true
currentId.value = record.id || record.projectId || null
// 若需要最新详情数据,调用标准详情接口回填
// 可避免列表接口缺少的字段在编辑时丢失
// 注意:后端若响应结构为 { code, data, msg },这里取 res.data
// await 的使用需要在该函数标记为 async这里暂保留同步如需启用请将函数改为 async 并解开注释
// try {
// const res = await getProjectDetail(currentId.value as any)
// const detail = (res as any).data || res
// Object.assign(form, {
// ...form,
// ...detail,
// })
// } catch (e) { console.error('获取项目详情失败', e) }
// 重置表单为默认(确保 tasks、turbineList 等为数组初始值)
resetForm()
// 填充表单数据
Object.keys(form).forEach((key) => {
if (key in record && record[key as keyof T.ProjectResp] !== undefined) {
// @ts-expect-error - 这里需要处理类型转换
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: '请输入项目名称' }],
projectOrigin: [{ 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 normalizeDate = (d: any) => (d ? (typeof d === 'string' ? d : new Date(d).toISOString().split('T')[0]) : '')
// 任务结构转换(只转换日期,其余保持)
const mapTasks = (tasks: any[]) =>
(tasks || []).map(t => ({
...t,
planStartDate: normalizeDate(t.planStartDate),
planEndDate: normalizeDate(t.planEndDate),
children: (t.children || []).map((c: any) => ({
...c,
planStartDate: normalizeDate(c.planStartDate),
planEndDate: normalizeDate(c.planEndDate),
}))
}))
const pickTaskFields = (t: any) => ({
mainUserId: t.mainUserId ?? '',
planEndDate: normalizeDate(t.planEndDate),
planStartDate: normalizeDate(t.planStartDate),
scales: t.scales ?? 0,
taskCode: t.taskCode ?? '',
taskGroupId: t.taskGroupId ?? '',
taskName: t.taskName ?? '',
})
const flattenTasks = (tasks: any[]) => {
const result: any[] = []
;(tasks || []).forEach((t) => {
result.push(pickTaskFields(t))
;(t.children || []).forEach((c: any) => result.push(pickTaskFields(c)))
})
return result
}
const submitData = {
auditorId: (form as any).auditorId || '',
bonusProvision: (form as any).bonusProvision ?? 0,
client: form.client || '',
clientContact: form.clientContact || '',
clientPhone: form.clientPhone || '',
constructionTeamLeaderId: form.constructionTeamLeaderId || '',
constructorIds: Array.isArray(form.constructorIds) ? form.constructorIds.join(',') : (form.constructorIds || ''),
coverUrl: (form as any).coverUrl || '',
duration: (form as any).duration ?? 0,
endDate: normalizeDate(form.endDate),
equipmentAmortization: (form as any).equipmentAmortization ?? 0,
farmAddress: form.farmAddress || '',
farmName: form.farmName || '',
inspectionContact: form.inspectionContact || '',
inspectionPhone: form.inspectionPhone || '',
inspectionUnit: form.inspectionUnit || '',
laborCost: (form as any).laborCost ?? 0,
othersCost: (form as any).othersCost ?? 0,
projectBudget: (form as any).projectBudget ?? 0,
projectId: isEdit.value && currentId.value ? currentId.value : form.projectId,
projectManagerId: form.projectManagerId || '',
projectName: form.projectName,
projectOrigin: (form as any).projectOrigin || '',
qualityOfficerId: form.qualityOfficerId || '',
scale: form.scale || '',
startDate: normalizeDate(form.startDate),
status: (form as any).status ?? 0,
tasks: flattenTasks(form.tasks as any[]),
transAccomMeals: (form as any).transAccomMeals ?? 0,
turbineModel: form.turbineModel || '',
}
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>