This commit is contained in:
马诗敏 2025-08-14 20:33:03 +08:00
commit de3c826d4e
14 changed files with 6392 additions and 655 deletions

188
src/apis/data/index.ts Normal file
View File

@ -0,0 +1,188 @@
// @/apis/data/index.ts - 数据管理API
import http from '@/utils/http'
import type {
FolderInfo,
FileInfo,
FolderListParams,
FileListParams,
FolderListResponse,
FileListResponse,
CreateFolderParams,
RenameFolderParams,
DeleteFolderParams,
UploadFileParams,
DownloadFileParams,
DeleteFileParams,
PreviewFileParams,
RenameFileParams
} from './type'
const { request, requestRaw } = http
// 导出类型定义
export type {
FolderInfo,
FileInfo,
FolderListParams,
FileListParams,
FolderListResponse,
FileListResponse,
CreateFolderParams,
RenameFolderParams,
DeleteFolderParams,
UploadFileParams,
DownloadFileParams,
DeleteFileParams,
PreviewFileParams,
RenameFileParams
}
// 获取文件夹列表(分页)
export function getFolderListApi(params?: FolderListParams) {
return request<FolderListResponse>({
url: '/data/folder/list',
method: 'get',
params: {
page: params?.page || 1,
pageSize: params?.pageSize || 10,
folderName: params?.folderName
}
})
}
// 获取文件列表(分页)
export function getFilesApi(params?: FileListParams) {
return request<FileListResponse>({
url: '/data/file/list',
method: 'get',
params: {
page: params?.page || 1,
pageSize: params?.pageSize || 10,
folderId: params?.folderId || '0',
fileName: params?.fileName,
sortField: params?.sortField,
sortOrder: params?.sortOrder
}
})
}
// 创建文件夹
export function createFolderApi(data: CreateFolderParams) {
return request({
url: '/data/folder/creatFolder',
method: 'post',
data: {
name: data.name,
parentId: data.parentId || '0'
}
})
}
// 重命名文件夹
export function updateFolderApi(folderId: string, newName: string) {
return request({
url: '/data/folder/rename',
method: 'put',
params: {
folderId: folderId,
newName: newName
}
})
}
// 删除文件夹
export function deleteFolderApi(folderId: string) {
return request({
url: '/data/folder/delete',
method: 'delete',
params: {
folderId: folderId
}
})
}
// 上传文件
export function uploadFileApi(
file: File,
folderId: string,
onUploadProgress?: (progressEvent: any) => void,
cancelToken?: any
) {
const formData = new FormData()
formData.append('file', file)
return requestRaw({
url: '/data/file/add',
method: 'post',
params: {
folderId: folderId
},
data: formData,
onUploadProgress,
cancelToken,
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(response => response.data)
.catch(error => {
// 确保错误不会抛出,而是返回一个错误对象
console.error('上传文件API错误:', error)
return {
code: 500,
msg: error.message || '上传失败',
success: false
}
})
}
// 下载文件
export function downloadFileApi(fileId: string) {
return request({
url: '/data/file/download',
method: 'get',
params: {
fileId: fileId
},
responseType: 'blob'
})
}
// 删除文件
export function deleteFileApi(fileId: string) {
return request({
url: '/data/file/delete',
method: 'delete',
params: {
fileId: fileId
}
})
}
// 预览文件
export function previewFileApi(fileId: string) {
return request({
url: '/data/file/preview',
method: 'get',
params: {
fileId: fileId
},
responseType: 'blob'
})
}
// 重命名文件
export function renameFileApi(fileId: string, newFileName: string) {
return request({
url: '/data/file/rename',
method: 'put',
params: {
fileId: fileId,
newFileName: newFileName
}
})
}
// 重命名文件(兼容旧接口)
export function updateFileNameApi(fileId: string, data: RenameFileParams) {
return renameFileApi(fileId, data.newFileName)
}

98
src/apis/data/type.ts Normal file
View File

@ -0,0 +1,98 @@
/** 文件夹信息接口 */
export interface FolderInfo {
folderId: string
name: string
parentId: string
createTime?: string
updateTime?: string
}
/** 文件信息接口 */
export interface FileInfo {
fileId: string
fileName: string
fileSize: number
fileType: string
folderId: string
createTime?: string
updateTime?: string
}
/** 文件夹列表查询参数 */
export interface FolderListParams {
page?: number
pageSize?: number
folderName?: string
}
/** 文件列表查询参数 */
export interface FileListParams {
page?: number
pageSize?: number
folderId?: string
fileName?: string
sortField?: string
sortOrder?: string
}
/** 文件夹列表响应 */
export interface FolderListResponse {
data: FolderInfo[]
total: number
current: number
size: number
}
/** 文件列表响应 */
export interface FileListResponse {
data: FileInfo[]
total: number
current: number
size: number
}
/** 创建文件夹请求参数 */
export interface CreateFolderParams {
name: string
parentId?: string
}
/** 重命名文件夹请求参数 */
export interface RenameFolderParams {
folderId: string
newName: string
}
/** 删除文件夹请求参数 */
export interface DeleteFolderParams {
folderId: string
}
/** 上传文件请求参数 */
export interface UploadFileParams {
file: File
folderId: string
onUploadProgress?: (progressEvent: any) => void
cancelToken?: any
}
/** 下载文件请求参数 */
export interface DownloadFileParams {
fileId: string
}
/** 删除文件请求参数 */
export interface DeleteFileParams {
fileId: string
}
/** 预览文件请求参数 */
export interface PreviewFileParams {
fileId: string
}
/** 重命名文件请求参数 */
export interface RenameFileParams {
fileId: string
newFileName: string
}

View File

@ -457,6 +457,7 @@ export interface ReceiptRequest {
useStatus?: string useStatus?: string
healthStatus?: string healthStatus?: string
receiptStatus?: string receiptStatus?: string
paymentStatus?: string
// 其他管理信息 // 其他管理信息
depreciationMethod?: string depreciationMethod?: string

View File

@ -20,6 +20,7 @@ export * as RegulationAPI from './regulation'
export * as TrainingAPI from './training' export * as TrainingAPI from './training'
export * as EquipmentAPI from './equipment' export * as EquipmentAPI from './equipment'
export * as BussinessAPI from './bussiness/bussiness' export * as BussinessAPI from './bussiness/bussiness'
export * as DataAPI from './data'
export * from './area/type' export * from './area/type'
export * from './auth/type' export * from './auth/type'

View File

@ -160,6 +160,18 @@ export const systemRoutes: RouteRecordRaw[] = [
sort: 3.5 sort: 3.5
} }
} }
,
{
path: '/task/gantt',
name: 'TaskGantt',
component: () => import('@/views/task/task-gantt/TaskGantt.vue'),
meta: {
title: '人力甘特图',
icon: 'workload', // 进度相关图标
hidden: false,
sort: 3.6
}
}
, ,
{ {
path: '/task/approval', path: '/task/approval',
@ -916,12 +928,61 @@ export const systemRoutes: RouteRecordRaw[] = [
}, },
], ],
}, },
{
path: '/construction-operation-platform',
name: 'ConstructionOperationPlatform',
component: Layout,
redirect: '/construction-operation-platform/implementation-workflow/field-construction',
meta: { title: '我的工作台', icon: 'tool', hidden: false, sort: 5 },
children: [
// {
// path: '/construction-operation-platform/implementation-workflow',
// name: 'ImplementationWorkflow',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/construction-operation-platform/implementation-workflow/field-construction',
// meta: { title: '项目实施工作流程', icon: 'fork', hidden: false },
// children: [
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/attachment',
name: 'AttachmentManagement',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/data-storage/index.vue'),
meta: { title: '附件管理', icon: 'attachment', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/model-config',
name: 'ModelConfig',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/model-config/index.vue'),
meta: { title: '模型配置', icon: 'robot', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/field-construction',
name: 'FieldConstruction',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/construction-operation-platform/implementation-workflow/field-construction/project-list',
meta: { title: '我的项目', icon: 'construction', hidden: false },
children: [
{
path: '/project-management/projects/list',
name: 'ProjectList',
component: () => import('@/views/project-management/projects/list/index.vue'),
meta: {
title: '项目列表',
icon: 'unordered-list',
hidden: false,
},
},
{
path: '/construction-operation-platform/implementation-workflow/field-construction/technology',
name: 'FieldConstructionTechnology',
component: () => import('@/views/project-operation-platform/implementation-workflow/field-construction/project-list/index.vue'),
meta: { title: '我的施工', icon: 'tool', hidden: false },
},
// start // start
// 数据管理 // 数据管理
{ {
path: '/data-management', path: '/data-management',
name: 'DataManagement', name: 'DataManagement',
component: Layout,
redirect: '/data-management/project-management/project-template', redirect: '/data-management/project-management/project-template',
meta: { title: '数据管理', icon: 'database', hidden: false, sort: 4 }, meta: { title: '数据管理', icon: 'database', hidden: false, sort: 4 },
children: [ children: [
@ -1037,55 +1098,6 @@ export const systemRoutes: RouteRecordRaw[] = [
], ],
}, },
// end // end
{
path: '/construction-operation-platform',
name: 'ConstructionOperationPlatform',
component: Layout,
redirect: '/construction-operation-platform/implementation-workflow/field-construction',
meta: { title: '我的工作台', icon: 'tool', hidden: false, sort: 5 },
children: [
// {
// path: '/construction-operation-platform/implementation-workflow',
// name: 'ImplementationWorkflow',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/construction-operation-platform/implementation-workflow/field-construction',
// meta: { title: '项目实施工作流程', icon: 'fork', hidden: false },
// children: [
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/attachment',
name: 'AttachmentManagement',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/data-storage/index.vue'),
meta: { title: '附件管理', icon: 'attachment', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/model-config',
name: 'ModelConfig',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/model-config/index.vue'),
meta: { title: '模型配置', icon: 'robot', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/field-construction',
name: 'FieldConstruction',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/construction-operation-platform/implementation-workflow/field-construction/project-list',
meta: { title: '我的项目', icon: 'construction', hidden: false },
children: [
{
path: '/project-management/projects/list',
name: 'ProjectList',
component: () => import('@/views/project-management/projects/list/index.vue'),
meta: {
title: '项目列表',
icon: 'unordered-list',
hidden: false,
},
},
{
path: '/construction-operation-platform/implementation-workflow/field-construction/technology',
name: 'FieldConstructionTechnology',
component: () => import('@/views/project-operation-platform/implementation-workflow/field-construction/project-list/index.vue'),
meta: { title: '我的施工', icon: 'tool', hidden: false },
},
{ {
path: '/construction-operation-platform/implementation-workflow/project-delivery', path: '/construction-operation-platform/implementation-workflow/project-delivery',
name: 'ProjectDelivery', name: 'ProjectDelivery',
@ -1194,7 +1206,7 @@ export const systemRoutes: RouteRecordRaw[] = [
name: 'bussinesskonwledge', name: 'bussinesskonwledge',
component: Layout, component: Layout,
redirect: '/bussiness-knowledge/data', redirect: '/bussiness-knowledge/data',
meta: { title: '商务资料知识库', icon: 'database', hidden: false, sort: 5.5 }, meta: { title: '智能商务', icon: 'database', hidden: false, sort: 5.5 },
children: [ children: [
{ {
path: '/bussiness-konwledge/data', path: '/bussiness-konwledge/data',
@ -1208,6 +1220,26 @@ export const systemRoutes: RouteRecordRaw[] = [
}, },
], ],
}, },
// 数据管理模块
{
path: '/data-management',
name: 'dataManagement',
component: Layout,
redirect: '/data-management/data',
meta: { title: '数据管理', icon: 'database', hidden: false, sort: 5.6 },
children: [
{
path: '/data-management/data',
name: 'data-management',
component: () => import('@/views/data/data.vue'),
meta: {
title: '数据管理',
icon: 'database',
hidden: false,
},
},
],
},
{ {
path: '/image-detection', path: '/image-detection',
name: 'ImageDetection', name: 'ImageDetection',

3787
src/views/data/data.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@ const emit = defineEmits<{
const turbine = computed({ const turbine = computed({
get: () => props.modelValue, get: () => props.modelValue,
set: v => emit('update:modelValue', v) set: (v) => emit('update:modelValue', v),
}) })
/* 状态文字 & 颜色 */ /* 状态文字 & 颜色 */
@ -47,8 +47,10 @@ function updateNo(val: string) {
<WindTurbine /> <WindTurbine />
<!-- 机组编号输入框 --> <!-- 机组编号输入框 -->
<a-input :model-value="turbine.turbineNo" @update:model-value="updateNo" size="small" class="turbine-input" <a-input
placeholder="编号" /> :model-value="turbine.turbineNo" size="small" class="turbine-input" placeholder="编号"
@update:model-value="updateNo"
/>
<!-- 地图选点按钮 --> <!-- 地图选点按钮 -->
<a-button size="mini" @click="$emit('map')"> <a-button size="mini" @click="$emit('map')">

View File

@ -332,6 +332,249 @@
</a-form> </a-form>
</a-modal> </a-modal>
<!-- 编辑项目弹窗与新增分离 -->
<a-modal
v-model:visible="editModalVisible"
title="编辑项目"
:ok-button-props="{ loading: submitLoading }"
width="800px"
modal-class="project-form-modal"
@cancel="() => { editModalVisible.value = false }"
@ok="handleEditSubmit"
>
<a-form
ref="editFormRef"
:model="editForm"
: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="editForm.projectName" placeholder="请输入项目名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="farmAddress" label="地址">
<a-input v-model="editForm.farmAddress" placeholder="请输入地址" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="projectManagerId" label="项目经理" required>
<a-select v-model="editForm.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="editForm.inspectionUnit" placeholder="请输入业主单位" @input="(val) => (editForm.farmName = val)" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="inspectionContact" label="业主单位联系人">
<a-input v-model="editForm.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="editForm.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="editForm.client" placeholder="请输入委托单位" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="clientContact" label="委托单位联系人">
<a-input v-model="editForm.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="editForm.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="editForm.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="addEditTask">
<template #icon><icon-plus /></template>
新增任务
</a-button>
</div>
<div v-if="editForm.tasks.length === 0" class="text-gray-500 mb-2">暂无任务</div>
<a-space direction="vertical" fill>
<a-card v-for="(task, tIndex) in editForm.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="addEditSubtask(tIndex)">新增子任务</a-button>
<a-button size="mini" status="danger" @click="removeEditTask(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="removeEditSubtask(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="editForm.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="editForm.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="editForm.startDate" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="endDate" label="结束时间">
<a-date-picker v-model="editForm.endDate" 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"> <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="flex flex-col items-center justify-center p-8">
@ -347,10 +590,114 @@
</a-upload> </a-upload>
</div> </div>
</a-modal> </a-modal>
<!-- 详情弹窗 -->
<a-modal
v-model:visible="detailVisible"
title="项目详情"
width="900px"
:footer="false"
:mask-closable="false"
>
<a-spin :loading="detailLoading">
<a-descriptions :column="2" bordered :label-style="{ width: '140px', fontWeight: 'bold' }">
<a-descriptions-item label="项目名称" :span="2">{{ detailData.projectName || '-' }}</a-descriptions-item>
<a-descriptions-item label="项目编号">{{ detailData.projectCode || '-' }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag v-if="detailData.status !== undefined" :color="getStatusColor(detailData.status)">
{{ PROJECT_STATUS_MAP[detailData.status] || detailData.statusLabel || '-' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="项目类型">{{ detailData.projectCategory || '-' }}</a-descriptions-item>
<a-descriptions-item label="业主">{{ detailData.inspectionUnit || '-' }}</a-descriptions-item>
<a-descriptions-item label="业主联系人">{{ detailData.inspectionContact || '-' }}</a-descriptions-item>
<a-descriptions-item label="业主电话">{{ detailData.inspectionPhone || '-' }}</a-descriptions-item>
<a-descriptions-item label="委托单位">{{ detailData.client || '-' }}</a-descriptions-item>
<a-descriptions-item label="委托联系人">{{ detailData.clientContact || '-' }}</a-descriptions-item>
<a-descriptions-item label="委托电话">{{ detailData.clientPhone || '-' }}</a-descriptions-item>
<a-descriptions-item label="风场名称">{{ detailData.farmName || '-' }}</a-descriptions-item>
<a-descriptions-item label="风场地址">{{ detailData.farmAddress || '-' }}</a-descriptions-item>
<a-descriptions-item label="项目经理">{{ detailData.projectManagerName || '-' }}</a-descriptions-item>
<a-descriptions-item label="项目规模">{{ detailData.scale || detailData.projectScale || '-' }}</a-descriptions-item>
<a-descriptions-item label="风机型号">{{ detailData.turbineModel || '-' }}</a-descriptions-item>
<a-descriptions-item label="开始时间">{{ detailData.startDate || '-' }}</a-descriptions-item>
<a-descriptions-item label="结束时间">{{ detailData.endDate || '-' }}</a-descriptions-item>
<a-descriptions-item label="项目简介" :span="2">
<a-typography-paragraph :ellipsis="{ rows: 3, expandable: true, collapseText: '收起', expandText: '展开' }">
{{ detailData.projectIntro || '-' }}
</a-typography-paragraph>
</a-descriptions-item>
</a-descriptions>
<a-divider orientation="left">任务列表</a-divider>
<a-table
:data="detailTasks"
:columns="detailTaskColumns as any"
:pagination="false"
size="small"
:bordered="false"
/>
</a-spin>
</a-modal>
</GiPageLayout> </GiPageLayout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const editModalVisible = ref(false)
const editFormRef = ref()
//
const editForm = reactive({
projectId: '',
projectName: '',
projectManagerId: '',
client: '',
clientContact: '',
clientPhone: '',
inspectionUnit: '',
inspectionContact: '',
inspectionPhone: '',
farmName: '',
farmAddress: '',
projectOrigin: '',
scale: '',
turbineModel: '',
status: 0,
startDate: '',
endDate: '',
constructionTeamLeaderId: '',
constructorIds: '',
qualityOfficerId: '',
auditorId: '',
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 }[],
})
import { computed, onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Message, Modal } from '@arco-design/web-vue' import { Message, Modal } from '@arco-design/web-vue'
@ -361,6 +708,18 @@ import { isMobile } from '@/utils'
import http from '@/utils/http' import http from '@/utils/http'
import type { ColumnItem } from '@/components/GiForm' import type { ColumnItem } from '@/components/GiForm'
import type { ProjectPageQuery } from '@/apis/project/type' import type { ProjectPageQuery } from '@/apis/project/type'
//
const detailTasks = ref<any[]>([])
const detailTaskColumns = [
{ title: '任务名称', dataIndex: 'taskName', ellipsis: true, tooltip: true },
{ title: '负责人', dataIndex: 'mainUserName', width: 120 },
{ title: '计划开始', dataIndex: 'planStartDate', width: 120 },
{ title: '计划结束', dataIndex: 'planEndDate', width: 120 },
{ title: '工量', dataIndex: 'scales', width: 80 },
{ title: '状态', dataIndex: 'statusLabel', width: 100 },
]
import type * as T from '@/apis/project/type' import type * as T from '@/apis/project/type'
defineOptions({ name: 'ProjectManagement' }) defineOptions({ name: 'ProjectManagement' })
@ -412,6 +771,12 @@ const dataList = ref<T.ProjectResp[]>([])
const userLoading = ref(false) const userLoading = ref(false)
const userOptions = ref<{ label: string, value: string }[]>([]) const userOptions = ref<{ label: string, value: string }[]>([])
//
const detailVisible = ref(false)
const detailLoading = ref(false)
const detailData = ref<any>({})
const searchForm = reactive<Partial<ProjectPageQuery>>({ const searchForm = reactive<Partial<ProjectPageQuery>>({
projectName: '', projectName: '',
status: undefined, status: undefined,
@ -681,6 +1046,94 @@ const fetchData = async () => {
} }
} else { } else {
Message.error(res.msg || '获取数据失败') Message.error(res.msg || '获取数据失败')
const handleEditSubmit = async () => {
submitLoading.value = true
try {
//
await editFormRef.value?.validate()
const normalizeDate = (d: any) => (d ? (typeof d === 'string' ? d : new Date(d).toISOString().split('T')[0]) : '')
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: (editForm as any).auditorId || '',
bonusProvision: (editForm as any).bonusProvision ?? 0,
client: editForm.client || '',
clientContact: editForm.clientContact || '',
clientPhone: editForm.clientPhone || '',
constructionTeamLeaderId: editForm.constructionTeamLeaderId || '',
constructorIds: Array.isArray(editForm.constructorIds) ? editForm.constructorIds.join(',') : (editForm.constructorIds || ''),
coverUrl: (editForm as any).coverUrl || '',
duration: (editForm as any).duration ?? 0,
endDate: normalizeDate(editForm.endDate),
equipmentAmortization: (editForm as any).equipmentAmortization ?? 0,
farmAddress: editForm.farmAddress || '',
farmName: editForm.farmName || '',
inspectionContact: editForm.inspectionContact || '',
inspectionPhone: editForm.inspectionPhone || '',
inspectionUnit: editForm.inspectionUnit || '',
laborCost: (editForm as any).laborCost ?? 0,
othersCost: (editForm as any).othersCost ?? 0,
projectBudget: (editForm as any).projectBudget ?? 0,
projectId: editForm.projectId || currentId.value || '',
projectManagerId: editForm.projectManagerId || '',
projectName: editForm.projectName,
projectOrigin: (editForm as any).projectOrigin || '',
qualityOfficerId: editForm.qualityOfficerId || '',
scale: editForm.scale || '',
startDate: normalizeDate(editForm.startDate),
status: (editForm as any).status ?? 0,
tasks: flattenTasks(editForm.tasks as any[]),
transAccomMeals: (editForm as any).transAccomMeals ?? 0,
turbineModel: editForm.turbineModel || '',
}
if (!currentId.value && submitData.projectId) currentId.value = submitData.projectId
if (!currentId.value) {
Message.error('缺少项目ID无法更新')
submitLoading.value = false
return
}
const res = await updateProject(submitData, currentId.value)
if (res && res.success === false) {
Message.error(res.msg || '更新失败')
submitLoading.value = false
return
}
Message.success('更新成功')
editModalVisible.value = false
fetchData()
} catch (e: any) {
if (e && e.type === 'form') {
//
} else {
console.error('编辑提交失败', e)
Message.error(e?.message || '编辑失败')
}
} finally {
submitLoading.value = false
}
}
dataList.value = [] dataList.value = []
pagination.total = 0 pagination.total = 0
} }
@ -757,7 +1210,7 @@ const resetForm = () => {
const openAddModal = () => { const openAddModal = () => {
resetForm() resetForm()
addModalVisible.value = true editModalVisible.value = true
} }
// /使 // /使
const addTask = () => { const addTask = () => {
@ -777,49 +1230,77 @@ const removeSubtask = (parentIndex: number, index: number) => {
} }
const openEditModal = (record: T.ProjectResp) => { const openEditModal = async (record: T.ProjectResp) => {
isEdit.value = true isEdit.value = true
currentId.value = record.id || record.projectId || null currentId.value = record.id || record.projectId || null
//
Object.assign(editForm, {
projectId: '', projectName: '', projectManagerId: '', client: '', clientContact: '', clientPhone: '',
inspectionUnit: '', inspectionContact: '', inspectionPhone: '', farmName: '', farmAddress: '', projectOrigin: '',
scale: '', turbineModel: '', status: 0, startDate: '', endDate: '', constructionTeamLeaderId: '', constructorIds: '',
qualityOfficerId: '', auditorId: '', tasks: [], turbineList: [],
})
// try {
// if (currentId.value) {
// { code, data, msg } res.data const res = await getProjectDetail(currentId.value as any)
// await 使 async async const detail = (res as any).data || res || {}
// 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) }
// tasksturbineList //
resetForm() Object.keys(editForm).forEach((key) => {
if (key in detail && (detail as any)[key] !== undefined) {
// // @ts-expect-error
Object.keys(form).forEach((key) => { editForm[key] = (detail as any)[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 editForm.farmName = detail.farmName || record.farmName || editForm.farmName
if (record.farmAddress) form.farmAddress = record.farmAddress editForm.farmAddress = detail.farmAddress || record.farmAddress || editForm.farmAddress
if (record.client) form.client = record.client editForm.client = detail.client || record.client || editForm.client
if (record.clientContact) form.clientContact = record.clientContact editForm.clientContact = detail.clientContact || record.clientContact || editForm.clientContact
if (record.clientPhone) form.clientPhone = record.clientPhone editForm.clientPhone = detail.clientPhone || record.clientPhone || editForm.clientPhone
if (record.turbineModel) form.turbineModel = record.turbineModel editForm.turbineModel = detail.turbineModel || record.turbineModel || editForm.turbineModel
if (record.scale) form.scale = record.scale editForm.scale = detail.scale || record.scale || editForm.scale
editForm.startDate = detail.startDate || record.startDate || editForm.startDate
editForm.endDate = detail.endDate || record.endDate || editForm.endDate
// //
if (record.startDate) form.startDate = record.startDate const mapTask = (t: any): any => ({
if (record.endDate) form.endDate = record.endDate taskName: t.taskName ?? t.name ?? '',
taskCode: t.taskCode ?? t.code ?? '',
mainUserId: t.mainUserId ?? t.mainUser?.id ?? t.mainUser ?? t.ownerId ?? '',
planStartDate: t.planStartDate ?? t.startDate ?? '',
planEndDate: t.planEndDate ?? t.endDate ?? '',
scales: t.scales ?? t.workload ?? 0,
taskGroupId: t.taskGroupId ?? t.groupId ?? '',
children: Array.isArray(t.children) ? t.children.map(mapTask) : [],
})
addModalVisible.value = true const tasksSource: any[] = Array.isArray(detail.tasks)
? detail.tasks
: (Array.isArray((detail as any).taskList) ? (detail as any).taskList : [])
if (Array.isArray(tasksSource) && tasksSource.length) {
;(editForm.tasks as any[]) = tasksSource.map(mapTask)
}
// projectId
editForm.projectId = (detail.projectId ?? currentId.value ?? record.projectId ?? '').toString()
}
} catch (e) {
console.error('获取项目详情失败', e)
// 退使
Object.keys(editForm).forEach((key) => {
if (key in record && (record as any)[key] !== undefined) {
// @ts-expect-error
editForm[key] = (record as any)[key]
}
})
}
//
editModalVisible.value = true
} }
// //
@ -979,19 +1460,52 @@ const deleteItem = async (record: T.ProjectResp) => {
} }
} }
const viewDetail = (record: T.ProjectResp) => { const viewDetail = async (record: T.ProjectResp) => {
const projectId = record.id || record.projectId const projectId = record.id || record.projectId
if (!projectId) { if (!projectId) {
Message.error('项目ID不存在') Message.error('项目ID不存在')
return return
} }
detailVisible.value = true
detailLoading.value = true
try {
// /project/detail/{projectId}
const res = await getProjectDetail(projectId)
const data = (res as any).data || res || {}
// status
if (typeof data.status === 'number' && !data.statusLabel) {
data.statusLabel = PROJECT_STATUS_MAP[data.status]
}
detailData.value = data
router.push({ // tasks taskList
name: 'ProjectDetail', const rawTasks: any[] = Array.isArray(data.tasks) ? data.tasks : (Array.isArray(data.taskList) ? data.taskList : [])
params: { //
id: projectId.toString(), const flatten = (tasks: any[]): any[] => {
}, const result: any[] = []
;(tasks || []).forEach(t => {
result.push({
taskName: t.taskName ?? '-',
mainUserName: t.mainUserName ?? t.mainUserId ?? '-',
planStartDate: t.planStartDate ?? '',
planEndDate: t.planEndDate ?? '',
scales: t.scales ?? '',
status: t.status,
statusLabel: t.statusLabel ?? (typeof t.status === 'number' ? PROJECT_STATUS_MAP[t.status as 0|1|2] : t.status ?? '-')
}) })
if (Array.isArray(t.children) && t.children.length) {
result.push(...flatten(t.children))
}
})
return result
}
detailTasks.value = flatten(rawTasks)
} catch (e) {
console.error('获取项目详情失败:', e)
Message.error('获取项目详情失败')
} finally {
detailLoading.value = false
}
} }
const openImportModal = () => { const openImportModal = () => {

View File

@ -0,0 +1,534 @@
<template>
<a-modal
:visible="visible"
title="合同发票管理"
width="800px"
:footer="false"
@cancel="handleCancel"
@update:visible="handleVisibleChange"
>
<div class="contract-invoice-container">
<!-- 设备基本信息 -->
<a-card title="设备信息" class="info-card" :bordered="false">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="设备名称">
{{ equipmentData?.equipmentName }}
</a-descriptions-item>
<a-descriptions-item label="设备型号">
{{ equipmentData?.equipmentModel }}
</a-descriptions-item>
<a-descriptions-item label="供应商">
{{ equipmentData?.supplierName }}
</a-descriptions-item>
<a-descriptions-item label="采购价格">
¥{{ formatPrice(equipmentData?.purchasePrice || 0) }}
</a-descriptions-item>
</a-descriptions>
</a-card>
<!-- 合同信息 -->
<a-card title="合同信息" class="info-card" :bordered="false">
<a-form
ref="contractFormRef"
:model="contractForm"
:rules="contractRules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="合同编号" name="contractNumber">
<a-input
v-model="contractForm.contractNumber"
placeholder="请输入合同编号"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="合同签订日期" name="contractDate">
<a-date-picker
v-model="contractForm.contractDate"
placeholder="请选择合同签订日期"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="合同金额" name="contractAmount">
<a-input-number
v-model="contractForm.contractAmount"
placeholder="请输入合同金额"
:precision="2"
:min="0"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="合同状态" name="contractStatus">
<a-select
v-model="contractForm.contractStatus"
placeholder="请选择合同状态"
allow-clear
>
<a-option value="DRAFT">草稿</a-option>
<a-option value="SIGNED">已签订</a-option>
<a-option value="EXECUTING">执行中</a-option>
<a-option value="COMPLETED">已完成</a-option>
<a-option value="TERMINATED">已终止</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="合同文件" name="contractFile">
<a-upload
v-model:file-list="contractForm.contractFile"
:action="uploadAction"
:headers="uploadHeaders"
:before-upload="beforeUpload"
:on-success="onContractUploadSuccess"
:on-error="onUploadError"
accept=".pdf,.doc,.docx"
:max-count="1"
>
<a-button>
<template #icon>
<IconUpload />
</template>
上传合同文件
</a-button>
<template #itemRender="{ file }">
<a-space>
<IconFile />
<span>{{ file.name }}</span>
<a-button
type="text"
size="small"
status="danger"
@click="removeContractFile(file)"
>
删除
</a-button>
</a-space>
</template>
</a-upload>
</a-form-item>
<a-form-item label="合同备注" name="contractRemark">
<a-textarea
v-model="contractForm.contractRemark"
placeholder="请输入合同备注信息"
:rows="3"
allow-clear
/>
</a-form-item>
</a-form>
</a-card>
<!-- 发票信息 -->
<a-card title="发票信息" class="info-card" :bordered="false">
<a-form
ref="invoiceFormRef"
:model="invoiceForm"
:rules="invoiceRules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="发票号码" name="invoiceNumber">
<a-input
v-model="invoiceForm.invoiceNumber"
placeholder="请输入发票号码"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="发票类型" name="invoiceType">
<a-select
v-model="invoiceForm.invoiceType"
placeholder="请选择发票类型"
allow-clear
>
<a-option value="VAT_SPECIAL">增值税专用发票</a-option>
<a-option value="VAT_COMMON">增值税普通发票</a-option>
<a-option value="ELECTRONIC">电子发票</a-option>
<a-option value="OTHER">其他</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="发票金额" name="invoiceAmount">
<a-input-number
v-model="invoiceForm.invoiceAmount"
placeholder="请输入发票金额"
:precision="2"
:min="0"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="开票日期" name="invoiceDate">
<a-date-picker
v-model="invoiceForm.invoiceDate"
placeholder="请选择开票日期"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="税率" name="taxRate">
<a-input-number
v-model="invoiceForm.taxRate"
placeholder="请输入税率"
:precision="2"
:min="0"
:max="100"
style="width: 100%"
addon-after="%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="税额" name="taxAmount">
<a-input-number
v-model="invoiceForm.taxAmount"
placeholder="税额将自动计算"
:precision="2"
:min="0"
style="width: 100%"
disabled
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="发票文件" name="invoiceFile">
<a-upload
v-model:file-list="invoiceForm.invoiceFile"
:action="uploadAction"
:headers="uploadHeaders"
:before-upload="beforeUpload"
:on-success="onInvoiceUploadSuccess"
:on-error="onUploadError"
accept=".pdf,.jpg,.jpeg,.png"
:max-count="1"
>
<a-button>
<template #icon>
<IconUpload />
</template>
上传发票文件
</a-button>
<template #itemRender="{ file }">
<a-space>
<IconFile />
<span>{{ file.name }}</span>
<a-button
type="text"
size="small"
status="danger"
@click="removeInvoiceFile(file)"
>
删除
</a-button>
</a-space>
</template>
</a-upload>
</a-form-item>
<a-form-item label="发票备注" name="invoiceRemark">
<a-textarea
v-model="invoiceForm.invoiceRemark"
placeholder="请输入发票备注信息"
:rows="3"
allow-clear
/>
</a-form-item>
</a-form>
</a-card>
<!-- 操作按钮 -->
<div class="action-buttons">
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" @click="handleSave" :loading="saving">
保存
</a-button>
<a-button
type="primary"
@click="handleComplete"
:loading="saving"
:disabled="!canComplete"
>
完成并进入收货阶段
</a-button>
</a-space>
</div>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'
import { IconUpload, IconFile } from '@arco-design/web-vue/es/icon'
import message from '@arco-design/web-vue/es/message'
import type { EquipmentResp } from '@/apis/equipment/type'
interface Props {
visible: boolean
equipmentData: EquipmentResp | null
}
interface ContractForm {
contractNumber: string
contractDate: string | null
contractAmount: number | null
contractStatus: string
contractFile: any[]
contractRemark: string
}
interface InvoiceForm {
invoiceNumber: string
invoiceType: string
invoiceAmount: number | null
invoiceDate: string | null
taxRate: number | null
taxAmount: number | null
invoiceFile: any[]
invoiceRemark: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:visible': [value: boolean]
'success': []
}>()
//
const contractFormRef = ref()
const invoiceFormRef = ref()
//
const saving = ref(false)
//
const contractForm = reactive<ContractForm>({
contractNumber: '',
contractDate: null,
contractAmount: null,
contractStatus: 'DRAFT',
contractFile: [],
contractRemark: ''
})
//
const invoiceForm = reactive<InvoiceForm>({
invoiceNumber: '',
invoiceType: '',
invoiceAmount: null,
invoiceDate: null,
taxRate: null,
taxAmount: null,
invoiceFile: [],
invoiceRemark: ''
})
//
const contractRules = {
contractNumber: [
{ required: true, message: '请输入合同编号', trigger: 'blur' }
],
contractDate: [
{ required: true, message: '请选择合同签订日期', trigger: 'change' }
],
contractAmount: [
{ required: true, message: '请输入合同金额', trigger: 'blur' }
],
contractStatus: [
{ required: true, message: '请选择合同状态', trigger: 'change' }
]
}
const invoiceRules = {
invoiceNumber: [
{ required: true, message: '请输入发票号码', trigger: 'blur' }
],
invoiceType: [
{ required: true, message: '请选择发票类型', trigger: 'change' }
],
invoiceAmount: [
{ required: true, message: '请输入发票金额', trigger: 'blur' }
],
invoiceDate: [
{ required: true, message: '请选择开票日期', trigger: 'change' }
],
taxRate: [
{ required: true, message: '请输入税率', trigger: 'blur' }
]
}
//
const uploadAction = '/api/file/upload'
const uploadHeaders = {
//
}
//
const canComplete = computed(() => {
return contractForm.contractStatus === 'SIGNED' &&
invoiceForm.invoiceNumber &&
invoiceForm.invoiceAmount
})
//
watch([() => invoiceForm.invoiceAmount, () => invoiceForm.taxRate], ([amount, rate]) => {
if (amount && rate) {
invoiceForm.taxAmount = Number((amount * rate / 100).toFixed(2))
} else {
invoiceForm.taxAmount = null
}
})
//
const formatPrice = (price: number) => {
return price.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
}
//
const beforeUpload = (file: File) => {
const isValidSize = file.size / 1024 / 1024 < 10 // 10MB
if (!isValidSize) {
message.error('文件大小不能超过10MB')
return false
}
return true
}
//
const onContractUploadSuccess = (response: any, file: any) => {
console.log('合同文件上传成功:', response, file)
message.success('合同文件上传成功')
}
//
const onInvoiceUploadSuccess = (response: any, file: any) => {
console.log('发票文件上传成功:', response, file)
message.success('发票文件上传成功')
}
//
const onUploadError = (error: any) => {
console.error('文件上传失败:', error)
message.error('文件上传失败')
}
//
const removeContractFile = (file: any) => {
const index = contractForm.contractFile.findIndex(f => f.uid === file.uid)
if (index > -1) {
contractForm.contractFile.splice(index, 1)
}
}
//
const removeInvoiceFile = (file: any) => {
const index = invoiceForm.invoiceFile.findIndex(f => f.uid === file.uid)
if (index > -1) {
invoiceForm.invoiceFile.splice(index, 1)
}
}
//
const handleCancel = () => {
emit('update:visible', false)
}
// visible
const handleVisibleChange = (newVisible: boolean) => {
emit('update:visible', newVisible)
}
//
const handleSave = async () => {
try {
saving.value = true
//
await contractFormRef.value?.validate()
//
await invoiceFormRef.value?.validate()
// API
console.log('保存合同发票信息:', {
contract: contractForm,
invoice: invoiceForm,
equipmentId: props.equipmentData?.equipmentId
})
message.success('保存成功')
} catch (error) {
console.error('保存失败:', error)
message.error('保存失败,请检查表单信息')
} finally {
saving.value = false
}
}
//
const handleComplete = async () => {
try {
saving.value = true
//
await contractFormRef.value?.validate()
await invoiceFormRef.value?.validate()
// API
console.log('完成合同发票管理:', {
contract: contractForm,
invoice: invoiceForm,
equipmentId: props.equipmentData?.equipmentId
})
message.success('合同发票管理完成,可以进入收货阶段')
emit('success')
emit('update:visible', false)
} catch (error) {
console.error('操作失败:', error)
message.error('操作失败,请检查表单信息')
} finally {
saving.value = false
}
}
</script>
<style scoped lang="scss">
.contract-invoice-container {
.info-card {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.action-buttons {
text-align: center;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--color-border);
}
}
</style>

View File

@ -1,474 +1,380 @@
<template> <template>
<a-modal <a-modal
:visible="visible" :visible="visible"
title="设备付款" title="付款申请"
width="900px" width="800px"
:confirm-loading="loading" :footer="false"
@cancel="handleCancel" @cancel="handleCancel"
@ok="handleSubmit" @update:visible="handleVisibleChange"
> >
<a-form <div class="payment-application-container">
ref="formRef" <!-- 设备基本信息 -->
:model="formData" <a-card title="设备信息" class="info-card" :bordered="false">
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="vertical"
>
<!-- 基本信息 -->
<a-card title="设备信息" class="detail-card" :bordered="false">
<a-descriptions :column="2" bordered> <a-descriptions :column="2" bordered>
<a-descriptions-item label="设备名称"> <a-descriptions-item label="设备名称">
{{ equipmentData?.equipmentName || '-' }} {{ equipmentData?.equipmentName }}
</a-descriptions-item>
<a-descriptions-item label="设备类型">
{{ equipmentData?.equipmentType || '-' }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="设备型号"> <a-descriptions-item label="设备型号">
{{ equipmentData?.equipmentModel || '-' }} {{ equipmentData?.equipmentModel }}
</a-descriptions-item> </a-descriptions-item>
<a-col :span="12">
<a-form-item label="品牌">
{{ equipmentData?.brand || '-' }}
</a-form-item>
</a-col>
<a-descriptions-item label="供应商"> <a-descriptions-item label="供应商">
{{ equipmentData?.supplierName || '-' }} {{ equipmentData?.supplierName }}
</a-descriptions-item>
<a-descriptions-item label="采购订单">
{{ equipmentData?.purchaseOrder || '-' }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="采购价格"> <a-descriptions-item label="采购价格">
¥{{ equipmentData?.purchasePrice || '-' }} ¥{{ formatPrice(equipmentData?.purchasePrice || 0) }}
</a-descriptions-item>
<a-descriptions-item label="采购数量">
{{ equipmentData?.quantity || '-' }}
</a-descriptions-item> </a-descriptions-item>
</a-descriptions> </a-descriptions>
</a-card> </a-card>
<!-- 支付信息 --> <!-- 付款申请信息 -->
<a-card title="支付信息" class="detail-card" :bordered="false"> <a-card title="付款申请信息" class="info-card" :bordered="false">
<a-form
ref="paymentFormRef"
:model="paymentForm"
:rules="paymentRules"
layout="vertical"
>
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="12"> <a-col :span="12">
<a-form-item label="支付方式" field="paymentMethod" required> <a-form-item label="申请日期" name="applicationDate">
<a-select <a-date-picker
v-model="formData.paymentMethod" v-model="paymentForm.applicationDate"
placeholder="请选择支付方式" placeholder="请选择申请日期"
style="width: 100%" style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="部门" name="department">
<a-input
v-model="paymentForm.department"
placeholder="请输入部门名称"
allow-clear
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="归属项目" name="projectName">
<a-input
v-model="paymentForm.projectName"
placeholder="请输入项目名称"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="支出类别" name="expenseCategory">
<a-select
v-model="paymentForm.expenseCategory"
placeholder="请选择支出类别"
allow-clear
> >
<a-option value="银行转账">银行转账</a-option> <a-option value="EXPENSE_TRANSPORT">a.费用支出-交通费</a-option>
<a-option value="现金">现金</a-option> <a-option value="EXPENSE_ACCOMMODATION">a.费用支出-住宿费</a-option>
<a-option value="支票">支票</a-option> <a-option value="EXPENSE_TOOLS">a.费用支出-小工具</a-option>
<a-option value="信用卡">信用卡</a-option> <a-option value="EXPENSE_VEHICLE_MAINTENANCE">a.费用支出-车辆保养</a-option>
<a-option value="其他">其他</a-option> <a-option value="PROCUREMENT_DRONE">b.采购支出-设备采购-无人机</a-option>
<a-option value="PROCUREMENT_VEHICLE">b.采购支出-设备采购-</a-option>
<a-option value="PROCUREMENT_COMPUTER">b.采购支出-设备采购-计算机</a-option>
<a-option value="SALARY">c.工资支出</a-option>
<a-option value="TAX">d.税款支出</a-option>
<a-option value="OTHER">f.其他支出</a-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12">
<a-form-item label="支付金额" field="paymentAmount" required>
<a-input-number
v-model="formData.paymentAmount"
placeholder="请输入支付金额"
:min="0.01"
:max="999999.99"
:precision="2"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row> </a-row>
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="12"> <a-col :span="12">
<a-form-item label="支付时间" field="paymentTime" required> <a-form-item label="事由" name="reason">
<a-date-picker
v-model="formData.paymentTime"
show-time
format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择支付时间"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="支付人" field="paymentPerson" required>
<a-input
v-model="formData.paymentPerson"
placeholder="请输入支付人姓名"
show-word-limit
:max-length="50"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item label="支付备注" field="paymentRemark">
<a-textarea <a-textarea
v-model="formData.paymentRemark" v-model="paymentForm.reason"
placeholder="请输入支付备注" placeholder="请输入付款事由"
:rows="3" :rows="3"
show-word-limit allow-clear
:max-length="200" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="经手人" name="handler">
<a-input
v-model="paymentForm.handler"
placeholder="请输入经手人姓名"
allow-clear
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
</a-card>
<!-- 发票信息 -->
<a-card title="发票信息" class="detail-card" :bordered="false">
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="12"> <a-col :span="12">
<a-form-item label="发票类型" field="invoiceType" required> <a-form-item label="金额" name="amount">
<a-select <a-input-number
v-model="formData.invoiceType" v-model="paymentForm.amount"
placeholder="请选择发票类型" placeholder="请输入付款金额"
:precision="2"
:min="0"
style="width: 100%" style="width: 100%"
addon-before="¥"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="收款单位" name="payeeUnit">
<a-input
v-model="paymentForm.payeeUnit"
placeholder="请输入收款单位名称"
allow-clear
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="收款账号" name="payeeAccount">
<a-input
v-model="paymentForm.payeeAccount"
placeholder="请输入收款账号"
allow-clear
/>
</a-form-item>
<!-- 发票附件及其他附件 -->
<a-form-item label="发票附件" name="invoiceAttachments">
<a-upload
v-model:file-list="paymentForm.invoiceAttachments"
:action="uploadAction"
:headers="uploadHeaders"
:before-upload="beforeUpload"
:on-success="onInvoiceUploadSuccess"
:on-error="onUploadError"
accept=".pdf,.jpg,.jpeg,.png"
:max-count="5"
> >
<a-option value="增值税专用发票">增值税专用发票</a-option> <a-button>
<a-option value="增值税普通发票">增值税普通发票</a-option> <template #icon>
<a-option value="电子发票">电子发票</a-option> <IconUpload />
<a-option value="其他">其他</a-option> </template>
</a-select> 上传发票附件
</a-button>
<template #itemRender="{ file }">
<a-space>
<IconFile />
<span>{{ file.name }}</span>
<a-button
type="text"
size="small"
status="danger"
@click="removeInvoiceAttachment(file)"
>
删除
</a-button>
</a-space>
</template>
</a-upload>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="发票号码" field="invoiceNumber" required>
<a-input
v-model="formData.invoiceNumber"
placeholder="请输入发票号码"
show-word-limit
:max-length="50"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16"> <!-- 银行付款凭证 -->
<a-col :span="12"> <a-form-item label="银行付款凭证" name="bankPaymentVouchers">
<a-form-item label="开票日期" field="invoiceDate" required> <a-upload
<a-date-picker v-model:file-list="paymentForm.bankPaymentVouchers"
v-model="formData.invoiceDate" :action="uploadAction"
format="YYYY-MM-DD" :headers="uploadHeaders"
placeholder="请选择开票日期" :before-upload="beforeUpload"
style="width: 100%" :on-success="onBankVoucherUploadSuccess"
/> :on-error="onUploadError"
accept=".pdf,.jpg,.jpeg,.png"
:max-count="3"
>
<a-button>
<template #icon>
<IconUpload />
</template>
上传银行付款凭证
</a-button>
<template #itemRender="{ file }">
<a-space>
<IconFile />
<span>{{ file.name }}</span>
<a-button
type="text"
size="small"
status="danger"
@click="removeBankVoucher(file)"
>
删除
</a-button>
</a-space>
</template>
</a-upload>
</a-form-item> </a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="发票金额" field="invoiceAmount" required>
<a-input-number
v-model="formData.invoiceAmount"
placeholder="请输入发票金额"
:min="0.01"
:max="999999.99"
:precision="2"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16"> <a-form-item label="备注" name="remarks">
<a-col :span="12"> <a-textarea
<a-form-item label="税率(%)" field="taxRate" required> v-model="paymentForm.remarks"
<a-input-number placeholder="请输入备注信息"
v-model="formData.taxRate" :rows="3"
placeholder="请输入税率" allow-clear
:min="0"
:max="100"
:precision="2"
style="width: 100%"
/> />
</a-form-item> </a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="税额" field="taxAmount" required>
<a-input-number
v-model="formData.taxAmount"
placeholder="请输入税额"
:min="0"
:max="999999.99"
:precision="2"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="不含税金额" field="amountWithoutTax" required>
<a-input-number
v-model="formData.amountWithoutTax"
placeholder="请输入不含税金额"
:min="0.01"
:max="999999.99"
:precision="2"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 合同信息 -->
<a-card title="合同信息" class="detail-card" :bordered="false">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="合同编号" field="contractNumber" required>
<a-input
v-model="formData.contractNumber"
placeholder="请输入合同编号"
show-word-limit
:max-length="50"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="合同金额" field="contractAmount" required>
<a-input-number
v-model="formData.contractAmount"
placeholder="请输入合同金额"
:min="0.01"
:max="999999.99"
:precision="2"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="签订日期" field="contractDate" required>
<a-date-picker
v-model="formData.contractDate"
format="YYYY-MM-DD"
placeholder="请选择签订日期"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="付款条件" field="paymentTerms" required>
<a-input
v-model="formData.paymentTerms"
placeholder="请输入付款条件"
show-word-limit
:max-length="100"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="付款期限" field="paymentDeadline" required>
<a-date-picker
v-model="formData.paymentDeadline"
format="YYYY-MM-DD"
placeholder="请选择付款期限"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
</a-form> </a-form>
</a-card>
<!-- 操作按钮 -->
<div class="action-buttons">
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" @click="handleSubmit" :loading="submitting">
提交付款申请
</a-button>
</a-space>
</div>
</div>
</a-modal> </a-modal>
</template> </template>
<script setup lang="ts"> <script lang="ts" setup>
import { ref, reactive, watch, computed } from 'vue' import { ref, reactive } from 'vue'
import { Message } from '@arco-design/web-vue' import { IconUpload, IconFile } from '@arco-design/web-vue/es/icon'
import type { FormInstance } from '@arco-design/web-vue' import message from '@arco-design/web-vue/es/message'
import type { EquipmentResp, PaymentRequest } from '@/apis/equipment/type' import type { EquipmentResp } from '@/apis/equipment/type'
import { equipmentProcurementApi } from '@/apis/equipment/procurement'
interface Props { interface Props {
visible: boolean visible: boolean
equipmentData?: EquipmentResp | null equipmentData: EquipmentResp | null
} }
const props = withDefaults(defineProps<Props>(), { interface PaymentForm {
visible: false, applicationDate: string | null
equipmentData: null, department: string
}) projectName: string
expenseCategory: string
reason: string
handler: string
amount: number | null
payeeUnit: string
payeeAccount: string
invoiceAttachments: any[]
bankPaymentVouchers: any[]
remarks: string
}
const props = defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
'update:visible': [value: boolean] 'update:visible': [value: boolean]
'success': [] 'success': []
}>() }>()
const formRef = ref<FormInstance>() //
const loading = ref(false) const paymentFormRef = ref()
// //
const formData = reactive<PaymentRequest>({ const submitting = ref(false)
paymentMethod: '',
paymentAmount: 0,
paymentTime: '',
paymentPerson: '',
paymentRemark: '',
invoiceType: '',
invoiceNumber: '',
invoiceDate: '',
invoiceAmount: 0,
taxRate: 13,
taxAmount: 0,
amountWithoutTax: 0,
contractNumber: '',
contractAmount: 0,
contractDate: '',
paymentTerms: '',
paymentDeadline: '',
})
// //
const calculatedTaxAmount = computed(() => { const paymentForm = reactive<PaymentForm>({
if (formData.invoiceAmount && formData.taxRate) { applicationDate: null,
return (formData.invoiceAmount * formData.taxRate) / 100 department: '',
} projectName: '',
return 0 expenseCategory: '',
}) reason: '',
handler: '',
const calculatedAmountWithoutTax = computed(() => { amount: null,
if (formData.invoiceAmount && formData.taxAmount) { payeeUnit: '',
return formData.invoiceAmount - formData.taxAmount payeeAccount: '',
} invoiceAttachments: [],
return 0 bankPaymentVouchers: [],
}) remarks: ''
//
watch([() => formData.invoiceAmount, () => formData.taxRate], () => {
formData.taxAmount = calculatedTaxAmount.value
formData.amountWithoutTax = calculatedAmountWithoutTax.value
}) })
// //
const rules = { const paymentRules = {
paymentMethod: [ applicationDate: [
{ required: true, message: '请选择支付方式' } { required: true, message: '请选择申请日期', trigger: 'change' }
], ],
paymentAmount: [ department: [
{ required: true, message: '请输入支付金额' }, { required: true, message: '请输入部门名称', trigger: 'blur' }
{ type: 'number', min: 0.01, message: '支付金额必须大于0' }
], ],
paymentTime: [ projectName: [
{ required: true, message: '请选择支付时间' } { required: true, message: '请输入项目名称', trigger: 'blur' }
], ],
paymentPerson: [ expenseCategory: [
{ required: true, message: '请输入支付人姓名' }, { required: true, message: '请选择支出类别', trigger: 'change' }
{ min: 2, max: 50, message: '支付人姓名长度应在2-50个字符之间' }
], ],
invoiceType: [ reason: [
{ required: true, message: '请选择发票类型' } { required: true, message: '请输入付款事由', trigger: 'blur' }
], ],
invoiceNumber: [ handler: [
{ required: true, message: '请输入发票号码' }, { required: true, message: '请输入经手人姓名', trigger: 'blur' }
{ min: 2, max: 50, message: '发票号码长度应在2-50个字符之间' }
], ],
invoiceDate: [ amount: [
{ required: true, message: '请选择开票日期' } { required: true, message: '请输入付款金额', trigger: 'blur' }
], ],
invoiceAmount: [ payeeUnit: [
{ required: true, message: '请输入发票金额' }, { required: true, message: '请输入收款单位名称', trigger: 'blur' }
{ type: 'number', min: 0.01, message: '发票金额必须大于0' }
],
taxRate: [
{ required: true, message: '请输入税率' },
{ type: 'number', min: 0, max: 100, message: '税率应在0-100之间' }
],
contractNumber: [
{ required: true, message: '请输入合同编号' },
{ min: 2, max: 50, message: '合同编号长度应在2-50个字符之间' }
],
contractAmount: [
{ required: true, message: '请输入合同金额' },
{ type: 'number', min: 0.01, message: '合同金额必须大于0' }
],
contractDate: [
{ required: true, message: '请选择签订日期' }
],
paymentTerms: [
{ required: true, message: '请输入付款条件' },
{ min: 2, max: 100, message: '付款条件长度应在2-100个字符之间' }
],
paymentDeadline: [
{ required: true, message: '请选择付款期限' }
], ],
payeeAccount: [
{ required: true, message: '请输入收款账号', trigger: 'blur' }
]
} }
// //
watch(() => props.visible, (visible) => { const uploadAction = '/api/file/upload'
if (visible && props.equipmentData) { const uploadHeaders = {
// //
Object.assign(formData, { }
paymentMethod: '',
paymentAmount: props.equipmentData.purchasePrice || 0, //
paymentTime: '', const formatPrice = (price: number) => {
paymentPerson: '', return price.toLocaleString('zh-CN', {
paymentRemark: '', minimumFractionDigits: 2,
invoiceType: '增值税专用发票', maximumFractionDigits: 2,
invoiceNumber: '',
invoiceDate: '',
invoiceAmount: props.equipmentData.purchasePrice || 0,
taxRate: 13,
taxAmount: 0,
amountWithoutTax: 0,
contractNumber: props.equipmentData.purchaseOrder || '',
contractAmount: props.equipmentData.purchasePrice || 0,
contractDate: '',
paymentTerms: '货到付款',
paymentDeadline: '',
}) })
//
formData.taxAmount = calculatedTaxAmount.value
formData.amountWithoutTax = calculatedAmountWithoutTax.value
formRef.value?.clearValidate()
}
})
//
const handleSubmit = async () => {
try {
await formRef.value?.validate()
loading.value = true
if (!props.equipmentData?.equipmentId) {
throw new Error('设备ID不能为空')
} }
// //
const paymentTime = formData.paymentTime ? new Date(formData.paymentTime).toISOString() : new Date().toISOString() const beforeUpload = (file: File) => {
const invoiceDate = formData.invoiceDate ? new Date(formData.invoiceDate).toISOString() : new Date().toISOString() const isValidSize = file.size / 1024 / 1024 < 10 // 10MB
const contractDate = formData.contractDate ? new Date(formData.contractDate).toISOString() : new Date().toISOString() if (!isValidSize) {
const paymentDeadline = formData.paymentDeadline ? new Date(formData.paymentDeadline).toISOString() : new Date().toISOString() message.error('文件大小不能超过10MB')
return false
const requestData: PaymentRequest = { }
...formData, return true
paymentTime,
invoiceDate,
contractDate,
paymentDeadline,
} }
await equipmentProcurementApi.makePayment(props.equipmentData.equipmentId, requestData) //
const onInvoiceUploadSuccess = (response: any, file: any) => {
console.log('发票附件上传成功:', response, file)
message.success('发票附件上传成功')
}
Message.success('付款成功') //
emit('success') const onBankVoucherUploadSuccess = (response: any, file: any) => {
emit('update:visible', false) console.log('银行付款凭证上传成功:', response, file)
} catch (error: any) { message.success('银行付款凭证上传成功')
console.error('付款失败:', error) }
Message.error(error?.message || '付款失败,请检查表单信息')
} finally { //
loading.value = false const onUploadError = (error: any) => {
console.error('文件上传失败:', error)
message.error('文件上传失败')
}
//
const removeInvoiceAttachment = (file: any) => {
const index = paymentForm.invoiceAttachments.findIndex(f => f.uid === file.uid)
if (index > -1) {
paymentForm.invoiceAttachments.splice(index, 1)
}
}
//
const removeBankVoucher = (file: any) => {
const index = paymentForm.bankPaymentVouchers.findIndex(f => f.uid === file.uid)
if (index > -1) {
paymentForm.bankPaymentVouchers.splice(index, 1)
} }
} }
@ -476,10 +382,42 @@ const handleSubmit = async () => {
const handleCancel = () => { const handleCancel = () => {
emit('update:visible', false) emit('update:visible', false)
} }
// visible
const handleVisibleChange = (newVisible: boolean) => {
emit('update:visible', newVisible)
}
//
const handleSubmit = async () => {
try {
submitting.value = true
//
await paymentFormRef.value?.validate()
// API
console.log('提交付款申请:', {
...paymentForm,
equipmentId: props.equipmentData?.equipmentId
})
message.success('付款申请提交成功')
emit('success')
emit('update:visible', false)
} catch (error) {
console.error('提交失败:', error)
message.error('提交失败,请检查表单信息')
} finally {
submitting.value = false
}
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.detail-card { .payment-application-container {
.info-card {
margin-bottom: 16px; margin-bottom: 16px;
&:last-child { &:last-child {
@ -487,19 +425,11 @@ const handleCancel = () => {
} }
} }
.arco-form-item { .action-buttons {
margin-bottom: 16px; text-align: center;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--color-border);
} }
.arco-input,
.arco-select,
.arco-input-number,
.arco-date-picker,
.arco-textarea {
border-radius: 6px;
}
.arco-textarea {
resize: vertical;
} }
</style> </style>

View File

@ -87,11 +87,15 @@
<a-form-item label="序列号" field="equipmentSn"> <a-form-item label="序列号" field="equipmentSn">
<a-input <a-input
v-model="formData.equipmentSn" v-model="formData.equipmentSn"
placeholder="请输入序列号" placeholder="选择设备类型后自动生成"
:disabled="isView" :disabled="true"
show-word-limit show-word-limit
:max-length="100" :max-length="100"
/> />
<div class="field-tip">
<IconInfoCircle style="color: #1890ff; margin-right: 4px;" />
选择设备类型后自动生成格式设备类型+顺序号+日期
</div>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
@ -528,6 +532,7 @@ import { Message } from '@arco-design/web-vue'
import type { FormInstance } from '@arco-design/web-vue' import type { FormInstance } from '@arco-design/web-vue'
import { equipmentProcurementApi } from '@/apis/equipment/procurement' import { equipmentProcurementApi } from '@/apis/equipment/procurement'
import type { EquipmentResp, EquipmentReq } from '@/apis/equipment/type' import type { EquipmentResp, EquipmentReq } from '@/apis/equipment/type'
import { IconInfoCircle } from '@arco-design/web-vue/es/icon'
interface Props { interface Props {
visible: boolean visible: boolean
@ -692,6 +697,40 @@ watch([() => formData.unitPrice, () => formData.quantity], ([newUnitPrice, newQu
} }
}) })
//
const generateEquipmentSn = (equipmentType: string) => {
//
const timestamp = Date.now()
const orderNumber = timestamp.toString().slice(-6) // 6
//
const typeMap: Record<string, string> = {
'detection': 'DET',
'security': 'SEC',
'office': 'OFF',
'car': 'CAR',
'other': 'OTH'
}
const typeCode = typeMap[equipmentType] || 'OTH'
//
const now = new Date()
const dateStr = now.getFullYear().toString().slice(-2) +
(now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0')
// ++
return `${typeCode}${orderNumber}${dateStr}`
}
//
watch(() => formData.equipmentType, (newType) => {
if (newType && !formData.equipmentSn) {
//
formData.equipmentSn = generateEquipmentSn(newType)
}
})
// //
const initFormData = () => { const initFormData = () => {
if (props.procurementData) { if (props.procurementData) {
@ -841,7 +880,7 @@ const fillTestData = () => {
const nextMaintenanceTime = nextMaintenanceDate.toISOString().slice(0, 19).replace('T', ' ') const nextMaintenanceTime = nextMaintenanceDate.toISOString().slice(0, 19).replace('T', ' ')
// //
const randomSn = 'SN' + Math.random().toString(36).substr(2, 8).toUpperCase() const randomSn = generateEquipmentSn('detection')
// //
const randomAssetCode = 'ZC' + Math.random().toString(36).substr(2, 6).toUpperCase() const randomAssetCode = 'ZC' + Math.random().toString(36).substr(2, 6).toUpperCase()
@ -949,6 +988,15 @@ const handleCancel = () => {
color: var(--color-text-1); color: var(--color-text-1);
margin-bottom: 8px; margin-bottom: 8px;
} }
.field-tip {
margin-top: 4px;
font-size: 12px;
color: var(--color-text-3);
display: flex;
align-items: center;
line-height: 1.4;
}
} }
.arco-input, .arco-input,

View File

@ -27,9 +27,15 @@
<a-descriptions-item label="设备型号"> <a-descriptions-item label="设备型号">
{{ equipmentData?.equipmentModel || '-' }} {{ equipmentData?.equipmentModel || '-' }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="设备序列号">
{{ equipmentData?.equipmentSn || '-' }}
</a-descriptions-item>
<a-descriptions-item label="品牌"> <a-descriptions-item label="品牌">
{{ equipmentData?.brand || '-' }} {{ equipmentData?.brand || '-' }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="配置规格">
{{ equipmentData?.specification || '-' }}
</a-descriptions-item>
<a-descriptions-item label="供应商"> <a-descriptions-item label="供应商">
{{ equipmentData?.supplierName || '-' }} {{ equipmentData?.supplierName || '-' }}
</a-descriptions-item> </a-descriptions-item>
@ -370,6 +376,32 @@ watch(() => props.equipmentData, () => {
} }
}, { deep: true }) }, { deep: true })
//
const generateEquipmentSn = (equipmentType: string, inStockTime: string) => {
//
const timestamp = Date.now()
const orderNumber = timestamp.toString().slice(-6) // 6
//
const typeMap: Record<string, string> = {
'detection': 'DET',
'security': 'SEC',
'office': 'OFF',
'car': 'CAR',
'other': 'OTH'
}
const typeCode = typeMap[equipmentType] || 'OTH'
//
const date = new Date(inStockTime)
const dateStr = date.getFullYear().toString().slice(-2) +
(date.getMonth() + 1).toString().padStart(2, '0') +
date.getDate().toString().padStart(2, '0')
// ++
return `${typeCode}${orderNumber}${dateStr}`
}
// //
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
@ -380,13 +412,37 @@ const handleSubmit = async () => {
throw new Error('设备ID不能为空') throw new Error('设备ID不能为空')
} }
console.log('📦 开始提交收货数据...') // 1.
console.log('📦 设备数据:', props.equipmentData) const procurementData = props.equipmentData
console.log('📦 表单数据:', formData)
// // 2.
const receiptData: ReceiptRequest = { const equipmentSn = generateEquipmentSn(
// procurementData.equipmentType || 'other',
formData.receiptTime || new Date().toISOString()
)
// 3.
const equipmentData = {
//
equipmentId: procurementData.equipmentId,
equipmentName: procurementData.equipmentName,
equipmentModel: procurementData.equipmentModel,
equipmentType: procurementData.equipmentType,
equipmentSn: equipmentSn, // 使
brand: procurementData.brand,
specification: procurementData.specification,
assetCode: procurementData.assetCode,
//
purchaseOrder: procurementData.purchaseOrder,
supplierName: procurementData.supplierName,
purchasePrice: procurementData.purchasePrice,
purchaseTime: procurementData.purchaseTime,
quantity: procurementData.quantity,
unitPrice: procurementData.unitPrice,
totalPrice: procurementData.totalPrice,
//
receiptTime: formData.receiptTime ? formatDateTime(formData.receiptTime) : formatDateTime(new Date()), receiptTime: formData.receiptTime ? formatDateTime(formData.receiptTime) : formatDateTime(new Date()),
receiptPerson: formData.receiptPerson, receiptPerson: formData.receiptPerson,
receiptQuantity: formData.receiptQuantity, receiptQuantity: formData.receiptQuantity,
@ -400,62 +456,31 @@ const handleSubmit = async () => {
storageLocation: formData.storageLocation, storageLocation: formData.storageLocation,
storageManager: formData.storageManager, storageManager: formData.storageManager,
// //
equipmentName: props.equipmentData.equipmentName,
equipmentModel: props.equipmentData.equipmentModel,
equipmentType: props.equipmentData.equipmentType,
equipmentSn: props.equipmentData.equipmentSn,
brand: props.equipmentData.brand,
specification: props.equipmentData.specification,
assetCode: props.equipmentData.assetCode,
//
purchaseOrder: props.equipmentData.purchaseOrder,
supplierName: props.equipmentData.supplierName,
purchasePrice: props.equipmentData.purchasePrice,
purchaseTime: props.equipmentData.purchaseTime,
quantity: props.equipmentData.quantity,
unitPrice: props.equipmentData.unitPrice,
totalPrice: props.equipmentData.totalPrice,
//
inStockTime: formData.receiptTime ? formatDateTime(formData.receiptTime) : formatDateTime(new Date()),
physicalLocation: formData.storageLocation,
locationStatus: 'in_stock',
responsiblePerson: formData.storageManager,
inventoryBarcode: props.equipmentData.inventoryBarcode || generateInventoryBarcode(),
//
equipmentStatus: 'normal', equipmentStatus: 'normal',
useStatus: '0', useStatus: '0', //
healthStatus: 'good', locationStatus: 'in_stock', //
receiptStatus: 'RECEIVED', healthStatus: 'excellent',
responsiblePerson: formData.storageManager,
// physicalLocation: formData.storageLocation,
depreciationMethod: props.equipmentData.depreciationMethod || 'straight_line', inStockTime: formData.receiptTime ? formatDateTime(formData.receiptTime) : formatDateTime(new Date()),
depreciationYears: props.equipmentData.depreciationYears || 5, inventoryBarcode: `BC${Date.now()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`,
salvageValue: props.equipmentData.salvageValue || 0, depreciationMethod: 'straight_line',
currentNetValue: props.equipmentData.purchasePrice || 0, warrantyExpireDate: procurementData.warrantyExpireDate,
assetRemark: `设备已收货入库,收货人:${formData.receiptPerson},入库时间:${formData.receiptTime}`
//
createTime: formatDateTime(new Date()),
updateTime: formatDateTime(new Date())
} }
console.log('📦 构建的收货数据:', receiptData) console.log('📦 准备提交收货数据:', equipmentData)
// API // 4. API
await equipmentProcurementApi.receiveGoods( await equipmentProcurementApi.receiveGoods(procurementData.equipmentId, equipmentData)
props.equipmentData.equipmentId,
receiptData
)
Message.success('收货成功设备已自动入库') Message.success('收货成功!设备已自动入库')
emit('success') emit('success')
emit('update:visible', false)
} catch (error: any) { } catch (error: any) {
console.error('收货失败:', error) console.error('收货失败:', error)
Message.error(error?.message || '收货失败,请检查表单信息') Message.error(error?.message || '收货失败,请重试')
} finally { } finally {
loading.value = false loading.value = false
} }

View File

@ -201,6 +201,8 @@
<a-button type="text" size="small" @click="handleEdit(record)"> <a-button type="text" size="small" @click="handleEdit(record)">
编辑 编辑
</a-button> </a-button>
<!-- 1. 采购相关操作 -->
<!-- 申请采购按钮 - 只在特定状态下显示 --> <!-- 申请采购按钮 - 只在特定状态下显示 -->
<a-button <a-button
v-if="canApplyProcurement(record)" v-if="canApplyProcurement(record)"
@ -210,6 +212,38 @@
> >
申请采购 申请采购
</a-button> </a-button>
<!-- 2. 合同发票管理相关操作 -->
<!-- 管理合同发票按钮 - 在采购审批通过后显示 -->
<a-button
v-if="canManageContractInvoice(record)"
type="outline"
size="small"
@click="handleManageContractInvoice(record)"
>
管理合同发票
</a-button>
<!-- 3. 支付相关操作 -->
<!-- 申请付款按钮 -->
<a-button
v-if="canMakePayment(record)"
type="outline"
size="small"
@click="handleMakePayment(record)"
>
申请付款
</a-button>
<a-button
v-if="record.paymentStatus === 'PAID'"
type="text"
size="small"
@click="handleViewPayment(record)"
>
查看支付详情
</a-button>
<!-- 4. 收货相关操作 -->
<!-- 收货操作按钮 --> <!-- 收货操作按钮 -->
<a-button <a-button
v-if="canReceiveGoods(record)" v-if="canReceiveGoods(record)"
@ -227,34 +261,56 @@
> >
查看收货 查看收货
</a-button> </a-button>
<!-- 支付操作按钮 -->
<a-button
v-if="canMakePayment(record)"
type="outline" <!-- 状态标签 - 按照要求的顺序排列 -->
size="small" <!-- 1. 采购状态 -->
@click="handleMakePayment(record)"
>
付款
</a-button>
<a-button
v-if="record.paymentStatus === 'PAID'"
type="text"
size="small"
@click="handleViewPayment(record)"
>
查看支付详情
</a-button>
<!-- 显示采购状态 - 优先显示采购状态 -->
<a-tag <a-tag
v-if="record.procurementStatus && record.procurementStatus !== 'NOT_STARTED'" v-if="record.procurementStatus && record.procurementStatus !== 'NOT_STARTED'"
:color="getProcurementStatusColor(record.procurementStatus)" :color="getProcurementStatusColor(record.procurementStatus)"
> >
{{ getProcurementStatusText(record.procurementStatus) }} {{ getProcurementStatusText(record.procurementStatus) }}
</a-tag> </a-tag>
<!-- 显示审批状态 -->
<a-tag v-if="record.approvalStatus" :color="getApprovalStatusColor(record.approvalStatus)"> <!-- 2. 合同发票管理状态 -->
{{ getApprovalStatusText(record.approvalStatus) }} <a-tag
v-if="hasContractInvoiceInfo(record)"
color="green"
>
已完善
</a-tag> </a-tag>
<a-tag
v-else-if="record.procurementStatus === 'APPROVED'"
color="orange"
>
待完善
</a-tag>
<a-tag
v-else
color="gray"
>
未开始
</a-tag>
<!-- 3. 支付状态 -->
<a-tag
v-if="record.paymentStatus && record.paymentStatus !== 'NOT_PAID'"
:color="getPaymentStatusColor(record.paymentStatus)"
>
{{ getPaymentStatusText(record.paymentStatus) }}
</a-tag>
<a-tag v-else color="gray">未支付</a-tag>
<!-- 4. 收货状态 -->
<a-tag
v-if="record.receiptStatus && record.receiptStatus !== 'NOT_RECEIVED'"
:color="getReceiptStatusColor(record.receiptStatus)"
>
{{ getReceiptStatusText(record.receiptStatus) }}
</a-tag>
<a-tag v-else color="gray">未收货</a-tag>
<!-- 删除按钮 --> <!-- 删除按钮 -->
<a-popconfirm <a-popconfirm
content="确定要删除这条记录吗?" content="确定要删除这条记录吗?"
@ -326,6 +382,13 @@
:equipment-data="currentPaymentData" :equipment-data="currentPaymentData"
@success="handlePaymentSuccess" @success="handlePaymentSuccess"
/> />
<!-- 合同发票管理弹窗 -->
<ContractInvoiceModal
v-model:visible="contractInvoiceModalVisible"
:equipment-data="currentContractInvoiceData"
@success="handleContractInvoiceSuccess"
/>
</div> </div>
</template> </template>
@ -349,6 +412,7 @@ import ReceiptDetailModal from './components/ReceiptDetailModal.vue'
import PaymentDetailModal from './components/PaymentDetailModal.vue' import PaymentDetailModal from './components/PaymentDetailModal.vue'
import ReceiptModal from './components/ReceiptModal.vue' import ReceiptModal from './components/ReceiptModal.vue'
import PaymentModal from './components/PaymentModal.vue' import PaymentModal from './components/PaymentModal.vue'
import ContractInvoiceModal from './components/ContractInvoiceModal.vue'
import { equipmentProcurementApi } from '@/apis/equipment/procurement' import { equipmentProcurementApi } from '@/apis/equipment/procurement'
import { equipmentApprovalApi } from '@/apis/equipment/approval' import { equipmentApprovalApi } from '@/apis/equipment/approval'
import type { EquipmentListReq, EquipmentResp } from '@/apis/equipment/type' import type { EquipmentListReq, EquipmentResp } from '@/apis/equipment/type'
@ -397,6 +461,10 @@ const currentPaymentData = ref<EquipmentResp | null>(null)
const receiptModalVisible = ref(false) const receiptModalVisible = ref(false)
const paymentModalVisible = ref(false) const paymentModalVisible = ref(false)
//
const contractInvoiceModalVisible = ref(false)
const currentContractInvoiceData = ref<EquipmentResp | null>(null)
// //
const selectedRowKeys = ref<string[]>([]) const selectedRowKeys = ref<string[]>([])
const rowSelection = reactive({ const rowSelection = reactive({
@ -936,13 +1004,42 @@ const handleModalSuccess = () => {
// //
const handleApplicationSuccess = async () => { const handleApplicationSuccess = async () => {
applicationModalVisible.value = false applicationModalVisible.value = false
console.log('采购申请成功,准备刷新数据...') console.log('✅ 采购申请成功,准备更新本地数据...')
//
setTimeout(async () => { //
console.log('开始刷新数据...') if (currentApplicationData.value) {
await loadData(currentSearchParams.value) const equipmentId = currentApplicationData.value.equipmentId
//
const recordIndex = tableData.value.findIndex(item => item.equipmentId === equipmentId)
if (recordIndex !== -1) {
//
tableData.value[recordIndex] = {
...tableData.value[recordIndex],
procurementStatus: 'PENDING_APPROVAL'
}
console.log('✅ 本地数据已更新,申请采购按钮应该消失')
console.log('🔍 更新后的记录:', tableData.value[recordIndex])
message.success('采购申请已提交,请等待审批') message.success('采购申请已提交,请等待审批')
}, 1000) } else {
console.warn('⚠️ 未找到对应的记录,无法更新本地状态')
message.warning('状态更新失败,请手动刷新页面')
}
}
//
setTimeout(async () => {
try {
console.log('🔄 延迟刷新数据,确保后端状态同步...')
await loadData(currentSearchParams.value)
console.log('✅ 后端数据同步完成')
} catch (error) {
console.error('❌ 后端数据同步失败:', error)
//
}
}, 1000) // 1
} }
// //
@ -993,17 +1090,45 @@ const getTotalAmount = () => {
// //
const canApplyProcurement = (record: EquipmentResp) => { const canApplyProcurement = (record: EquipmentResp) => {
// //
// //
const allowedStatuses = ['NOT_STARTED', 'REJECTED', 'COMPLETED', null, undefined] //
const allowedStatuses = ['NOT_STARTED', 'REJECTED', null, undefined]
const canApply = allowedStatuses.includes(record.procurementStatus) const canApply = allowedStatuses.includes(record.procurementStatus)
console.log(`设备 ${record.equipmentName} 采购状态: ${record.procurementStatus}, 可申请: ${canApply}`)
//
console.log(`🔍 申请采购按钮显示检查 - 设备: ${record.equipmentName}`)
console.log(`🔍 当前采购状态: ${record.procurementStatus}`)
console.log(`🔍 允许申请的状态: ${allowedStatuses.join(', ')}`)
console.log(`🔍 是否显示按钮: ${canApply}`)
console.log(`🔍 完整记录:`, record)
return canApply return canApply
} }
// //
const canReceiveGoods = (record: EquipmentResp) => { const canReceiveGoods = (record: EquipmentResp) => {
const receiptStatus = (record as any).receiptStatus //
return receiptStatus === 'NOT_RECEIVED' || receiptStatus === 'PARTIALLY_RECEIVED' //
const procurementStatus = record.procurementStatus
const receiptStatus = record.receiptStatus
//
// 使
const hasContractInvoice = true // true
console.log('🔍 canReceiveGoods 检查:', {
equipmentName: record.equipmentName,
procurementStatus,
receiptStatus,
hasContractInvoice,
canReceive: procurementStatus === 'APPROVED' &&
(receiptStatus === 'NOT_RECEIVED' || receiptStatus === 'PARTIALLY_RECEIVED') &&
hasContractInvoice
})
return procurementStatus === 'APPROVED' &&
(receiptStatus === 'NOT_RECEIVED' || receiptStatus === 'PARTIALLY_RECEIVED') &&
hasContractInvoice
} }
// //
@ -1021,7 +1146,10 @@ const handleViewReceipt = (record: EquipmentResp) => {
// //
const handleReceiptSuccess = () => { const handleReceiptSuccess = () => {
receiptModalVisible.value = false receiptModalVisible.value = false
//
//
loadData(currentSearchParams.value) loadData(currentSearchParams.value)
message.success('收货成功!设备已自动入库')
} }
// //
@ -1072,6 +1200,57 @@ const getApprovalStatusText = (status: string) => {
return textMap[status] || '未知' return textMap[status] || '未知'
} }
//
const handleRefreshRecord = async (record: EquipmentResp) => {
console.log('🔄 手动刷新记录:', record.equipmentId)
try {
//
const loadingMessage = message.loading('正在刷新数据...')
//
await loadData(currentSearchParams.value)
//
if (loadingMessage && typeof loadingMessage.close === 'function') {
loadingMessage.close()
}
message.success('记录状态已刷新')
console.log('✅ 表格数据刷新完成')
} catch (error: any) {
console.error('❌ 刷新失败:', error)
message.error(error?.message || '刷新失败')
}
}
//
const canManageContractInvoice = (record: EquipmentResp) => {
//
return record.procurementStatus === 'APPROVED'
}
//
const handleManageContractInvoice = (record: EquipmentResp) => {
currentContractInvoiceData.value = { ...record }
contractInvoiceModalVisible.value = true
}
//
const handleContractInvoiceSuccess = () => {
contractInvoiceModalVisible.value = false
loadData(currentSearchParams.value)
}
//
const hasContractInvoiceInfo = (record: EquipmentResp) => {
//
// 使
//
return record.procurementStatus === 'APPROVED' &&
(record.receiptStatus === 'RECEIVED' || record.paymentStatus === 'PAID')
}
onMounted(() => { onMounted(() => {
loadData() loadData()
}) })
@ -1258,6 +1437,8 @@ onMounted(() => {
} }
} }
} }
} }
// //

View File

@ -0,0 +1,396 @@
<template>
<GiPageLayout>
<div class="gantt-container">
<div class="page-header">
<h2 class="page-title">人力甘特图页面</h2>
</div>
<!-- 人员列表区域 -->
<div class="person-container">
<div v-for="(person, index) in personList" :key="person.id" class="person-gantt">
<!-- 人员标题栏 -->
<div class="person-header" @click="togglePerson(index)">
<div class="name-container">
<span class="avatar">{{ person.name.charAt(0) }}</span>
<span>{{ person.name }}</span>
<span class="task-count">({{ person.tasks.length }}个项目)</span>
</div>
<button class="expand-button">
<i :class="person.expanded ? 'collapse-icon' : 'expand-icon'"></i>
</button>
</div>
<!-- 甘特图容器 - 根据展开状态控制显示 -->
<div v-show="person.expanded" class="chart-container">
<div :ref="el => chartRefs[index] = el" class="progress-chart"></div>
</div>
</div>
</div>
</div>
</GiPageLayout>
</template>
<script setup lang='ts'>
import * as echarts from 'echarts'
import { ref, onMounted, onUnmounted, watch } from 'vue'
//
const personList = ref([
{
id: 1,
name: '张三',
expanded: true,
tasks: [
{ name: '项目一', startDate: '2025-08-01', days: 5, color: '#5470C6' },
{ name: '项目二', startDate: '2025-08-10', days: 7, color: '#91CC75' },
{ name: '项目三', startDate: '2025-08-20', days: 4, color: '#FAC858' }
]
},
{
id: 2,
name: '李四',
expanded: true,
tasks: [
{ name: '产品设计', startDate: '2025-08-05', days: 8, color: '#EE6666' },
{ name: '技术评审', startDate: '2025-08-15', days: 3, color: '#73C0DE' },
{ name: '系统测试', startDate: '2025-08-18', days: 6, color: '#3BA272' }
]
},
{
id: 3,
name: '王五',
expanded: true,
tasks: [
{ name: '需求分析', startDate: '2025-08-02', days: 4, color: '#FC8452' },
{ name: '前端开发', startDate: '2025-08-08', days: 7, color: '#9A60B4' },
{ name: '后端对接', startDate: '2025-08-17', days: 5, color: '#EA7CCC' }
]
},
{
id: 4,
name: '赵六',
expanded: false,
tasks: [
{ name: '文档编写', startDate: '2025-08-03', days: 6, color: '#5470C6' },
{ name: '用户培训', startDate: '2025-08-12', days: 4, color: '#91CC75' },
{ name: '上线支持', startDate: '2025-08-22', days: 7, color: '#FAC858' }
]
}
])
//
const chartRefs = ref<HTMLElement[]>([]);
//
const chartInstances = ref<echarts.ECharts[]>([]);
// YYYY-MM-DD
const formatDate = (date: Date): string => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
// /
const togglePerson = (index: number) => {
personList.value[index].expanded = !personList.value[index].expanded;
//
if (personList.value[index].expanded) {
setTimeout(() => {
if (chartRefs.value[index] && chartInstances.value[index]) {
chartInstances.value[index].resize();
} else {
initChart(index);
}
}, 10);
}
};
//
const initChart = (personIndex: number) => {
const container = chartRefs.value[personIndex];
if (!container) return;
//
if (chartInstances.value[personIndex]) {
chartInstances.value[personIndex].dispose();
}
const person = personList.value[personIndex];
const tasks = person.tasks;
// -
const today = new Date(2025, 7, 14);
//
const projectNames: string[] = [];
const dataItems: any[] = [];
const colors: string[] = [];
tasks.forEach((task) => {
const startDate = new Date(task.startDate);
const endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + task.days);
projectNames.push(task.name);
colors.push(task.color);
dataItems.push({
name: task.name,
value: [
formatDate(startDate),
formatDate(endDate),
task.days
],
itemStyle: {
color: task.color
}
});
});
//
const chart = echarts.init(container);
//
const option = {
tooltip: {
trigger: 'item',
formatter: (params: any) => {
const task = params.data;
return `
<div class="task-tooltip">
<strong>${task.name}</strong><br>
开始: ${params.value[0]}<br>
结束: ${params.value[1]}<br>
耗时: ${task.value[2]}
</div>
`;
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'time',
min: formatDate(new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000)),
max: formatDate(new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000)),
axisLabel: {
formatter: function (value: number) {
return echarts.time.format(value, '{MM}/{dd}', false);
}
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
opacity: 0.3
}
}
},
yAxis: {
type: 'category',
data: projectNames,
axisLine: {
show: true
},
axisTick: {
show: false
},
axisLabel: {
margin: 16
}
},
dataZoom: [{
type: 'inside',
start: 20,
end: 100
}],
series: [{
name: '项目进度',
type: 'bar',
data: dataItems,
barCategoryGap: '40%',
label: {
show: true,
position: 'inside',
formatter: '{c[2]}天'
},
barWidth: '60%'
}],
animation: true,
animationDuration: 800
};
chart.setOption(option);
chartInstances.value[personIndex] = chart;
}
//
const handleResize = () => {
chartInstances.value.forEach((chart, index) => {
if (chart && personList.value[index].expanded) {
chart.resize();
}
});
};
//
onMounted(() => {
window.addEventListener('resize', handleResize);
personList.value.forEach((_, index) => {
if (personList.value[index].expanded) {
initChart(index);
}
});
});
//
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
chartInstances.value.forEach(chart => {
if (chart) {
chart.dispose();
}
});
});
</script>
<style lang='scss' scoped>
.gantt-container {
height: 100%;
padding: 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.page-header {
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid #e5e7eb;
.page-title {
margin: 0;
font-size: 1.5rem;
color: #2c3e50;
font-weight: 600;
}
}
.person-container {
flex: 1;
overflow-y: auto;
padding-right: 8px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: #c2c6cc;
border-radius: 3px;
}
}
.person-gantt {
margin-bottom: 20px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
background-color: white;
&:last-child {
margin-bottom: 0;
}
}
.person-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #f8f9fa;
border-bottom: 1px solid #e9ecef;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: #f1f3f5;
}
.name-container {
display: flex;
align-items: center;
font-size: 1.1rem;
font-weight: 500;
color: #495057;
.avatar {
display: inline-flex;
justify-content: center;
align-items: center;
width: 28px;
height: 28px;
margin-right: 10px;
background-color: #3a7afe;
color: white;
border-radius: 50%;
font-weight: bold;
}
.task-count {
margin-left: 8px;
font-size: 0.85rem;
font-weight: normal;
color: #868e96;
}
}
.expand-button {
background: none;
border: none;
cursor: pointer;
width: 28px;
height: 28px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
&:hover {
background-color: #e9ecef;
}
i {
display: block;
width: 0;
height: 0;
border-style: solid;
}
.collapse-icon {
border-width: 0 8px 10px 8px;
border-color: transparent transparent #495057 transparent;
}
.expand-icon {
border-width: 10px 8px 0 8px;
border-color: #495057 transparent transparent transparent;
}
}
}
.chart-container {
padding: 15px;
background-color: #fff;
}
.progress-chart {
width: 100%;
height: 250px;
}
</style>