1107 lines
36 KiB
Vue
1107 lines
36 KiB
Vue
<!--
|
||
项目管理页面
|
||
已完成接口对接:
|
||
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>
|