main/src/views/project/index.vue

751 lines
21 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)">{{ 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"
@before-ok="handleSubmit"
width="800px"
>
<a-form ref="formRef" :model="form" label-position="left" :label-col-props="{ span: 8 }" :wrapper-col-props="{ span: 16 }">
<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="fieldName" label="风场名称" required>
<a-input v-model="form.fieldName" placeholder="请输入" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="fieldLocation" label="风场地址" required>
<a-input v-model="form.fieldLocation" placeholder="请输入" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="commissionUnit" label="委托单位" required>
<a-input v-model="form.commissionUnit" placeholder="请输入" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="commissionContact" label="委托单位联系人" required>
<a-input v-model="form.commissionContact" placeholder="请输入" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="commissionPhone" label="委托单位联系电话" required>
<a-input v-model="form.commissionPhone" placeholder="请输入" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="inspectionUnit" label="检查单位" required>
<a-input v-model="form.inspectionUnit" placeholder="请输入" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="inspectionContact" label="检查单位联系人" required>
<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="检查单位联系电话" required>
<a-input v-model="form.inspectionPhone" placeholder="请输入" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="projectScale" label="项目规模" required>
<a-input v-model="form.projectScale" placeholder="请输入" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="orgNumber" label="机组型号" required>
<a-input v-model="form.orgNumber" placeholder="请输入" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="projectCategory" label="项目类型/服务" required>
<a-select v-model="form.projectCategory" placeholder="请选择">
<a-option v-for="option in PROJECT_CATEGORY_OPTIONS" :key="option.value" :value="option.value">
{{ option.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="projectManager" label="项目经理" required>
<a-select v-model="form.projectManager" placeholder="请选择">
<a-option value="请选择">请选择</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="status" label="状态" required>
<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-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item field="projectIntro" label="项目描述">
<a-textarea v-model="form.projectIntro" placeholder="请输入" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item field="projectStaff" label="施工人员" required>
<a-select v-model="form.projectStaff" multiple placeholder="请选择">
<a-option value="全部员工">全部员工</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item field="projectPeriod" label="项目周期" required>
<a-range-picker v-model="form.projectPeriod" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
</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 type { ColumnItem } from '@/components/GiForm'
import type { TableColumnData } from '@arco-design/web-vue'
import type { ProjectResp, ProjectPageQuery } 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<ProjectResp[]>([])
const searchForm = reactive<Partial<ProjectPageQuery>>({
projectName: '',
status: undefined,
fieldName: '',
})
const queryFormColumns: ColumnItem[] = reactive([
{
type: 'input',
label: '项目名称',
field: 'projectName',
span: { xs: 24, sm: 8, xxl: 8 },
props: {
placeholder: '请输入项目名称',
},
},
{
type: 'input',
label: '风场名称',
field: 'fieldName',
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({
projectName: '',
projectIntro: '',
fieldName: '',
fieldLocation: '',
commissionUnit: '',
commissionContact: '',
commissionPhone: '',
inspectionUnit: '',
inspectionContact: '',
inspectionPhone: '',
projectScale: '',
orgNumber: '',
projectCategory: '',
projectManager: '',
projectStaff: [] as string[],
projectPeriod: [] as string[],
status: PROJECT_STATUS.IN_PROGRESS // 默认为施工中
})
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
showJumper: true,
showPageSize: true
})
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,
},
])
const modalTitle = computed(() => isEdit.value ? '编辑项目' : '新增项目')
const getStatusColor = (status: string) => {
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) => ({
...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,
// 处理项目周期
projectPeriod: item.startDate && item.endDate ? [item.startDate, item.endDate] : [],
}))
// 由于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: '',
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 = () => {
formRef.value?.resetFields()
isEdit.value = false
currentId.value = null
}
const openAddModal = () => {
resetForm()
addModalVisible.value = true
}
const openEditModal = (record: ProjectResp) => {
isEdit.value = true
currentId.value = record.id || record.projectId || null
// 安全地填充表单数据
Object.keys(form).forEach(key => {
if (key in record && record[key as keyof ProjectResp] !== undefined) {
// @ts-ignore - 这里需要处理类型转换
form[key] = record[key as keyof ProjectResp]
}
})
addModalVisible.value = true
}
const handleSubmit = async () => {
const valid = await formRef.value?.validate()
if (!valid) return false
try {
let res
if (isEdit.value && currentId.value) {
res = await updateProject(form, currentId.value)
Message.success('更新成功')
} else {
res = await addProject(form)
Message.success('添加成功')
}
// 如果API返回success字段检查操作是否真正成功
if (res && res.success === false) {
Message.error(res.msg || '操作失败')
return false
}
fetchData()
return true
} catch (error) {
console.error('项目操作失败:', error)
Message.error('操作失败')
return false
}
}
const confirmDelete = (record: ProjectResp) => {
Modal.warning({
title: '确认删除',
content: `确定要删除项目"${record.projectName}"吗?`,
onOk: () => deleteItem(record),
})
}
const deleteItem = async (record: 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: 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,
}
await exportProject(params)
Message.success('导出成功')
} catch (error) {
console.error('导出项目失败:', error)
Message.error('导出失败')
}
}
onMounted(() => {
fetchData()
})
</script>
<style scoped>
:deep(.arco-tag) {
margin-right: 0;
}
</style>